-- -- Process Control Management -- local comms = require("scada-common.comms") local log = require("scada-common.log") local types = require("scada-common.types") local util = require("scada-common.util") local F_CMD = comms.FAC_COMMAND local U_CMD = comms.UNIT_COMMAND local PROCESS = types.PROCESS local PRODUCT = types.WASTE_PRODUCT local REQUEST_TIMEOUT_MS = 10000 ---@class process_controller local process = {} local pctl = { io = nil, ---@type ioctl comms = nil, ---@type coord_comms ---@class sys_control_states control_states = { ---@class sys_auto_config process = { mode = PROCESS.INACTIVE, ---@type PROCESS burn_target = 0.0, charge_target = 0.0, gen_target = 0.0, limits = {}, ---@type number[] waste_product = PRODUCT.PLUTONIUM, ---@type WASTE_PRODUCT pu_fallback = false, sps_low_power = false }, waste_modes = {}, ---@type WASTE_MODE[] priority_groups = {} ---@type AUTO_GROUP[] }, commands = { unit = {}, ---@type process_command_state[][] fac = {} ---@type process_command_state[] } } ---@class process_command_state ---@field active boolean if this command is live ---@field timeout integer expiration time of this command request ---@field requestors function[] list of callbacks from the requestors -- write auto process control to config file ---@return boolean saved local function _write_auto_config() -- save config settings.set("ControlStates", pctl.control_states) local saved = settings.save("/coordinator.settings") if not saved then log.warning("process._write_auto_config(): failed to save coordinator settings file") end return saved end --#region Core -- initialize the process controller ---@param iocontrol ioctl iocontrl system ---@param coord_comms coord_comms coordinator communications function process.init(iocontrol, coord_comms) pctl.io = iocontrol pctl.comms = coord_comms -- create command handling objects for _, v in pairs(F_CMD) do pctl.commands.fac[v] = { active = false, timeout = 0, requestors = {} } end for i = 1, pctl.io.facility.num_units do pctl.commands.unit[i] = {} for _, v in pairs(U_CMD) do pctl.commands.unit[i][v] = { active = false, timeout = 0, requestors = {} } end end local ctl_proc = pctl.control_states.process for i = 1, pctl.io.facility.num_units do ctl_proc.limits[i] = 0.1 end local ctrl_states = settings.get("ControlStates", {}) ---@type sys_control_states local config = ctrl_states.process -- facility auto control configuration if type(config) == "table" then ctl_proc.mode = config.mode ctl_proc.burn_target = config.burn_target ctl_proc.charge_target = config.charge_target ctl_proc.gen_target = config.gen_target ctl_proc.limits = config.limits ctl_proc.waste_product = config.waste_product ctl_proc.pu_fallback = config.pu_fallback ctl_proc.sps_low_power = config.sps_low_power pctl.io.facility.ps.publish("process_mode", ctl_proc.mode) pctl.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target) pctl.io.facility.ps.publish("process_charge_target", pctl.io.energy_convert_from_fe(ctl_proc.charge_target)) pctl.io.facility.ps.publish("process_gen_target", pctl.io.energy_convert_from_fe(ctl_proc.gen_target)) pctl.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product) pctl.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback) pctl.io.facility.ps.publish("process_sps_low_power", ctl_proc.sps_low_power) for id = 1, math.min(#ctl_proc.limits, pctl.io.facility.num_units) do local unit = pctl.io.units[id] unit.unit_ps.publish("burn_limit", ctl_proc.limits[id]) end log.info("PROCESS: loaded auto control settings") -- notify supervisor of auto waste config pctl.comms.send_fac_command(F_CMD.SET_WASTE_MODE, ctl_proc.waste_product) pctl.comms.send_fac_command(F_CMD.SET_PU_FB, ctl_proc.pu_fallback) pctl.comms.send_fac_command(F_CMD.SET_SPS_LP, ctl_proc.sps_low_power) end -- unit waste states local waste_modes = ctrl_states.waste_modes if type(waste_modes) == "table" then for id, mode in pairs(waste_modes) do pctl.control_states.waste_modes[id] = mode pctl.comms.send_unit_command(U_CMD.SET_WASTE, id, mode) end log.info("PROCESS: loaded unit waste mode settings") end -- unit priority groups local prio_groups = ctrl_states.priority_groups if type(prio_groups) == "table" then for id, group in pairs(prio_groups) do pctl.control_states.priority_groups[id] = group pctl.comms.send_unit_command(U_CMD.SET_GROUP, id, group) end log.info("PROCESS: loaded priority groups settings") end end -- create a handle to process control for usage of commands that get acknowledgements function process.create_handle() ---@class process_handle local handle = {} -- add this handle to the requestors and activate the command if inactive ---@param cmd process_command_state ---@param ack function local function request(cmd, ack) local new = not cmd.active if new then cmd.active = true cmd.timeout = util.time_ms() + REQUEST_TIMEOUT_MS end table.insert(cmd.requestors, ack) return new end local function u_request(u_id, cmd_id, ack) return request(pctl.commands.unit[u_id][cmd_id], ack) end local function f_request(cmd_id, ack) return request(pctl.commands.fac[cmd_id], ack) end --#region Facility Commands -- facility SCRAM command function handle.fac_scram() if f_request(F_CMD.SCRAM_ALL, handle.fac_ack.on_scram) then pctl.comms.send_fac_command(F_CMD.SCRAM_ALL) log.debug("PROCESS: FAC SCRAM ALL") end end -- facility alarm acknowledge command function handle.fac_ack_alarms() if f_request(F_CMD.ACK_ALL_ALARMS, handle.fac_ack.on_ack_alarms) then pctl.comms.send_fac_command(F_CMD.ACK_ALL_ALARMS) log.debug("PROCESS: FAC ACK ALL ALARMS") end end -- start automatic process control with current settings function handle.process_start() if f_request(F_CMD.START, handle.fac_ack.on_start) then local p = pctl.control_states.process pctl.comms.send_auto_start(p.mode, p.burn_target, p.charge_target, p.gen_target, p.limits) log.debug("PROCESS: START AUTO CTRL") end end -- start automatic process control with remote settings that haven't been set on the coordinator ---@param mode PROCESS process control mode ---@param burn_target number burn rate target ---@param charge_target number charge level target ---@param gen_target number generation rate target ---@param limits number[] unit burn rate limits function handle.process_start_remote(mode, burn_target, charge_target, gen_target, limits) if f_request(F_CMD.START, handle.fac_ack.on_start) then pctl.comms.send_auto_start(mode, burn_target, charge_target, gen_target, limits) log.debug("PROCESS: START AUTO CTRL") end end -- stop process control function handle.process_stop() if f_request(F_CMD.STOP, handle.fac_ack.on_stop) then pctl.comms.send_fac_command(F_CMD.STOP) log.debug("PROCESS: STOP AUTO CTRL") end end handle.fac_ack = {} -- luacheck: no unused args -- facility SCRAM ack, override to implement ---@param success boolean ---@diagnostic disable-next-line: unused-local function handle.fac_ack.on_scram(success) end -- facility acknowledge all alarms ack, override to implement ---@param success boolean ---@diagnostic disable-next-line: unused-local function handle.fac_ack.on_ack_alarms(success) end -- facility auto control start ack, override to implement ---@param success boolean ---@diagnostic disable-next-line: unused-local function handle.fac_ack.on_start(success) end -- facility auto control stop ack, override to implement ---@param success boolean ---@diagnostic disable-next-line: unused-local function handle.fac_ack.on_stop(success) end -- luacheck: unused args --#endregion --#region Unit Commands -- start a reactor ---@param id integer unit ID function handle.start(id) if u_request(id, U_CMD.START, handle.unit_ack[id].on_start) then pctl.io.units[id].control_state = true pctl.comms.send_unit_command(U_CMD.START, id) log.debug(util.c("PROCESS: UNIT[", id, "] START")) end end -- SCRAM reactor ---@param id integer unit ID function handle.scram(id) if u_request(id, U_CMD.SCRAM, handle.unit_ack[id].on_scram) then pctl.io.units[id].control_state = false pctl.comms.send_unit_command(U_CMD.SCRAM, id) log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM")) end end -- reset reactor protection system ---@param id integer unit ID function handle.reset_rps(id) if u_request(id, U_CMD.RESET_RPS, handle.unit_ack[id].on_rps_reset) then pctl.comms.send_unit_command(U_CMD.RESET_RPS, id) log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS")) end end -- acknowledge all alarms ---@param id integer unit ID function handle.ack_all_alarms(id) if u_request(id, U_CMD.ACK_ALL_ALARMS, handle.unit_ack[id].on_ack_alarms) then pctl.comms.send_unit_command(U_CMD.ACK_ALL_ALARMS, id) log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS")) end end -- unit command acknowledgement callbacks, indexed by unit ID ---@type process_unit_ack[] handle.unit_ack = {} for u = 1, pctl.io.facility.num_units do ---@diagnostic disable-next-line: missing-fields handle.unit_ack[u] = {} ---@class process_unit_ack local u_ack = handle.unit_ack[u] -- luacheck: no unused args -- unit start ack, override to implement ---@param success boolean ---@diagnostic disable-next-line: unused-local function u_ack.on_start(success) end -- unit SCRAM ack, override to implement ---@param success boolean ---@diagnostic disable-next-line: unused-local function u_ack.on_scram(success) end -- unit RPS reset ack, override to implement ---@param success boolean ---@diagnostic disable-next-line: unused-local function u_ack.on_rps_reset(success) end -- unit acknowledge all alarms ack, override to implement ---@param success boolean ---@diagnostic disable-next-line: unused-local function u_ack.on_ack_alarms(success) end -- luacheck: unused args end --#endregion return handle end -- clear outstanding process commands that have timed out function process.clear_timed_out() local now = util.time_ms() local objs = { pctl.commands.fac, table.unpack(pctl.commands.unit) } for _, obj in pairs(objs) do -- cancel expired requests for _, cmd in pairs(obj) do if cmd.active and now > cmd.timeout then cmd.active = false cmd.requestors = {} end end end end -- get the control states table ---@nodiscard function process.get_control_states() return pctl.control_states end --#endregion --#region Command Handling -- handle a command acknowledgement ---@param cmd_state process_command_state ---@param success boolean if the command was successful local function cmd_ack(cmd_state, success) if cmd_state.active then cmd_state.active = false -- call all acknowledge callback functions for i = 1, #cmd_state.requestors do cmd_state.requestors[i](success) end cmd_state.requestors = {} end end -- handle a facility command acknowledgement ---@param command FAC_COMMAND command ---@param success boolean if the command was successful function process.fac_ack(command, success) cmd_ack(pctl.commands.fac[command], success) end -- handle a unit command acknowledgement ---@param unit integer unit ID ---@param command UNIT_COMMAND command ---@param success boolean if the command was successful function process.unit_ack(unit, command, success) cmd_ack(pctl.commands.unit[unit][command], success) end --#region One-Way Commands (no acknowledgements) -- set burn rate ---@param id integer unit ID ---@param rate number burn rate function process.set_rate(id, rate) pctl.comms.send_unit_command(U_CMD.SET_BURN, id, rate) log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate)) end -- assign a unit to a group ---@param unit_id integer unit ID ---@param group_id integer|0 group ID or 0 for independent function process.set_group(unit_id, group_id) pctl.comms.send_unit_command(U_CMD.SET_GROUP, unit_id, group_id) log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id)) pctl.control_states.priority_groups[unit_id] = group_id settings.set("ControlStates", pctl.control_states) if not settings.save("/coordinator.settings") then log.error("process.set_group(): failed to save coordinator settings file") end end -- set waste mode ---@param id integer unit ID ---@param mode integer waste mode function process.set_unit_waste(id, mode) -- publish so that if it fails then it gets reset pctl.io.units[id].unit_ps.publish("U_WasteMode", mode) pctl.comms.send_unit_command(U_CMD.SET_WASTE, id, mode) log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode)) pctl.control_states.waste_modes[id] = mode settings.set("ControlStates", pctl.control_states) if not settings.save("/coordinator.settings") then log.error("process.set_unit_waste(): failed to save coordinator settings file") end end -- acknowledge an alarm ---@param id integer unit ID ---@param alarm integer alarm ID function process.ack_alarm(id, alarm) pctl.comms.send_unit_command(U_CMD.ACK_ALARM, id, alarm) log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm)) end -- reset an alarm ---@param id integer unit ID ---@param alarm integer alarm ID function process.reset_alarm(id, alarm) pctl.comms.send_unit_command(U_CMD.RESET_ALARM, id, alarm) log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm)) end --#endregion -------------------------- -- AUTO PROCESS CONTROL -- -------------------------- -- set automatic process control waste mode ---@param product WASTE_PRODUCT waste product for auto control function process.set_process_waste(product) pctl.comms.send_fac_command(F_CMD.SET_WASTE_MODE, product) log.debug(util.c("PROCESS: SET WASTE ", product)) end -- set automatic process control plutonium fallback ---@param enabled boolean whether to enable plutonium fallback function process.set_pu_fallback(enabled) pctl.comms.send_fac_command(F_CMD.SET_PU_FB, enabled) log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled)) end -- set automatic process control SPS usage at low power ---@param enabled boolean whether to enable SPS usage at low power function process.set_sps_low_power(enabled) pctl.comms.send_fac_command(F_CMD.SET_SPS_LP, enabled) log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled)) end -- save process control settings ---@param mode PROCESS process control mode ---@param burn_target number burn rate target ---@param charge_target number charge level target ---@param gen_target number generation rate target ---@param limits number[] unit burn rate limits function process.save(mode, burn_target, charge_target, gen_target, limits) log.debug("PROCESS: SAVE") -- update config table local ctl_proc = pctl.control_states.process ctl_proc.mode = mode ctl_proc.burn_target = burn_target ctl_proc.charge_target = charge_target ctl_proc.gen_target = gen_target ctl_proc.limits = limits -- save config pctl.io.facility.save_cfg_ack(_write_auto_config()) end -- handle a start command acknowledgement ---@param response table ack and configuration reply function process.start_ack_handle(response) local ack = response[1] local ctl_proc = pctl.control_states.process ctl_proc.mode = response[2] ctl_proc.burn_target = response[3] ctl_proc.charge_target = response[4] ctl_proc.gen_target = response[5] for i = 1, math.min(#response[6], pctl.io.facility.num_units) do ctl_proc.limits[i] = response[6][i] pctl.io.units[i].unit_ps.publish("burn_limit", ctl_proc.limits[i]) end pctl.io.facility.ps.publish("process_mode", ctl_proc.mode) pctl.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target) pctl.io.facility.ps.publish("process_charge_target", pctl.io.energy_convert_from_fe(ctl_proc.charge_target)) pctl.io.facility.ps.publish("process_gen_target", pctl.io.energy_convert_from_fe(ctl_proc.gen_target)) _write_auto_config() process.fac_ack(F_CMD.START, ack) end -- record waste product settting after attempting to change it ---@param response WASTE_PRODUCT supervisor waste product settting function process.waste_ack_handle(response) -- update config table and save pctl.control_states.process.waste_product = response _write_auto_config() pctl.io.facility.ps.publish("process_waste_product", response) end -- record plutonium fallback settting after attempting to change it ---@param response boolean supervisor plutonium fallback settting function process.pu_fb_ack_handle(response) -- update config table and save pctl.control_states.process.pu_fallback = response _write_auto_config() pctl.io.facility.ps.publish("process_pu_fallback", response) end -- record SPS low power settting after attempting to change it ---@param response boolean supervisor SPS low power settting function process.sps_lp_ack_handle(response) -- update config table and save pctl.control_states.process.sps_low_power = response _write_auto_config() pctl.io.facility.ps.publish("process_sps_low_power", response) end --#endregion return process