diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index ff81e5d..11287e7 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -364,8 +364,9 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel, -- send a facility command ---@param cmd FAC_COMMAND command - function public.send_fac_command(cmd) - _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd }) + ---@param option any? optional option options for the optional options (like waste mode) + function public.send_fac_command(cmd, option) + _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd, option }) end -- send the auto process control configuration with a start command @@ -379,7 +380,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel, -- send a unit command ---@param cmd UNIT_COMMAND command ---@param unit integer unit ID - ---@param option any? optional option options for the optional options (like burn rate) (does option still look like a word?) + ---@param option any? optional option options for the optional options (like burn rate) function public.send_unit_command(cmd, unit, option) _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_CMD, { cmd, unit, option }) end @@ -563,6 +564,10 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel, end elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then iocontrol.get_db().facility.ack_alarms_ack(ack) + elseif cmd == FAC_COMMAND.SET_WASTE_MODE then + process.waste_ack_handle(packet.data[2]) + elseif cmd == FAC_COMMAND.SET_PU_FB then + process.pu_fb_ack_handle(packet.data[2]) else log.debug(util.c("received facility command ack with unknown command ", cmd)) end diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index a15a236..2eeb5e2 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -52,6 +52,10 @@ function iocontrol.init(conf, comms) gen_fault = false }, + ---@type WASTE_PRODUCT + auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM, + auto_pu_fallback_active = false, + radiation = types.new_zero_radiation_reading(), save_cfg_ack = __generic_ack, @@ -65,16 +69,18 @@ function iocontrol.init(conf, comms) induction_ps_tbl = {}, induction_data_tbl = {}, + sps_ps_tbl = {}, + sps_data_tbl = {}, + env_d_ps = psil.create(), env_d_data = {} } - -- create induction tables (currently only 1 is supported) - for _ = 1, conf.num_units do - local data = {} ---@type imatrix_session_db - table.insert(io.facility.induction_ps_tbl, psil.create()) - table.insert(io.facility.induction_data_tbl, data) - end + -- create induction and SPS tables (currently only 1 of each is supported) + table.insert(io.facility.induction_ps_tbl, psil.create()) + table.insert(io.facility.induction_data_tbl, {}) + table.insert(io.facility.sps_ps_tbl, psil.create()) + table.insert(io.facility.sps_data_tbl, {}) io.units = {} for i = 1, conf.num_units do @@ -87,11 +93,15 @@ function iocontrol.init(conf, comms) num_boilers = 0, num_turbines = 0, + num_snas = 0, control_state = false, burn_rate_cmd = 0.0, - waste_control = 0, radiation = types.new_zero_radiation_reading(), + sna_prod_rate = 0.0, + + waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM, + waste_product = types.WASTE_PRODUCT.PLUTONIUM, -- auto control group a_group = 0, @@ -100,10 +110,10 @@ function iocontrol.init(conf, comms) scram = function () process.scram(i) end, reset_rps = function () process.reset_rps(i) end, ack_alarms = function () process.ack_all_alarms(i) end, - set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate - set_waste = function (mode) process.set_waste(i, mode) end, ---@param mode integer waste processing mode + set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate + set_waste = function (mode) process.set_unit_waste(i, mode) end, ---@param mode WASTE_MODE waste processing mode - set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0 + set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0 for manual start_ack = __generic_ack, scram_ack = __generic_ack, @@ -206,6 +216,25 @@ function iocontrol.record_facility_builds(build) end end end + + -- SPS + if type(build.sps) == "table" then + for id, sps in pairs(build.sps) do + if type(fac.sps_data_tbl[id]) == "table" then + fac.sps_data_tbl[id].formed = sps[1] ---@type boolean + fac.sps_data_tbl[id].build = sps[2] ---@type table + + fac.sps_ps_tbl[id].publish("formed", sps[1]) + + for key, val in pairs(fac.sps_data_tbl[id].build) do + fac.sps_ps_tbl[id].publish(key, val) + end + else + log.debug(util.c("iocontrol.record_facility_builds: invalid SPS id ", id)) + valid = false + end + end + end else log.debug("facility builds not a table") valid = false @@ -306,7 +335,7 @@ function iocontrol.update_facility_status(status) local ctl_status = status[1] - if type(ctl_status) == "table" and #ctl_status == 14 then + if type(ctl_status) == "table" and #ctl_status == 16 then fac.all_sys_ok = ctl_status[1] fac.auto_ready = ctl_status[2] @@ -354,6 +383,12 @@ function iocontrol.update_facility_status(status) io.units[i].unit_ps.publish("auto_group", names[group_map[i] + 1]) end end + + fac.auto_current_waste_product = ctl_status[15] + fac.auto_pu_fallback_active = ctl_status[16] + + fac.ps.publish("current_waste_product", fac.auto_current_waste_product) + fac.ps.publish("pu_fallback_active", fac.auto_pu_fallback_active) else log.debug(log_header .. "control status not a table or length mismatch") valid = false @@ -430,6 +465,52 @@ function iocontrol.update_facility_status(status) valid = false end + -- SPS statuses + if type(rtu_statuses.sps) == "table" then + for id = 1, #fac.sps_ps_tbl do + if rtu_statuses.sps[id] == nil then + -- disconnected + fac.sps_ps_tbl[id].publish("computed_status", 1) + end + end + + for id, sps in pairs(rtu_statuses.sps) do + if type(fac.sps_data_tbl[id]) == "table" then + local rtu_faulted = sps[1] ---@type boolean + fac.sps_data_tbl[id].formed = sps[2] ---@type boolean + fac.sps_data_tbl[id].state = sps[3] ---@type table + fac.sps_data_tbl[id].tanks = sps[4] ---@type table + + local data = fac.sps_data_tbl[id] ---@type sps_session_db + + fac.sps_ps_tbl[id].publish("formed", data.formed) + fac.sps_ps_tbl[id].publish("faulted", rtu_faulted) + + if data.formed then + if rtu_faulted then + fac.sps_ps_tbl[id].publish("computed_status", 3) -- faulted + elseif data.state.process_rate > 0 then + fac.sps_ps_tbl[id].publish("computed_status", 5) -- active + else + fac.sps_ps_tbl[id].publish("computed_status", 4) -- idle + end + else + fac.sps_ps_tbl[id].publish("computed_status", 2) -- not formed + end + + for key, val in pairs(data.state) do fac.sps_ps_tbl[id].publish(key, val) end + for key, val in pairs(data.tanks) do fac.sps_ps_tbl[id].publish(key, val) end + + io.facility.ps.publish("am_rate", data.state.process_rate * 1000) + else + log.debug(util.c(log_header, "invalid sps id ", id)) + end + end + else + log.debug(log_header .. "sps list not a table") + valid = false + end + -- environment detector status if type(rtu_statuses.rad_mon) == "table" then if #rtu_statuses.rad_mon > 0 then @@ -472,6 +553,9 @@ function iocontrol.update_unit_statuses(statuses) valid = false else local burn_rate_sum = 0.0 + local sna_count_sum = 0 + local pu_rate = 0.0 + local po_rate = 0.0 -- get all unit statuses for i = 1, #statuses do @@ -480,6 +564,8 @@ function iocontrol.update_unit_statuses(statuses) local unit = io.units[i] ---@type ioctl_unit local status = statuses[i] + local burn_rate = 0.0 + if type(status) ~= "table" or #status ~= 5 then log.debug(log_header .. "invalid status entry in unit statuses (not a table or invalid length)") valid = false @@ -515,7 +601,8 @@ function iocontrol.update_unit_statuses(statuses) -- if status hasn't been received, mek_status = {} if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then - burn_rate_sum = burn_rate_sum + unit.reactor_data.mek_status.act_burn_rate + burn_rate = unit.reactor_data.mek_status.act_burn_rate + burn_rate_sum = burn_rate_sum + burn_rate end if unit.reactor_data.mek_status.status then @@ -662,6 +749,19 @@ function iocontrol.update_unit_statuses(statuses) valid = false end + -- solar neutron activator status info + if type(rtu_statuses.sna) == "table" then + unit.num_snas = rtu_statuses.sna[1] ---@type integer + unit.sna_prod_rate = rtu_statuses.sna[2] ---@type number + + unit.unit_ps.publish("sna_prod_rate", unit.sna_prod_rate) + + sna_count_sum = sna_count_sum + unit.num_snas + else + log.debug(log_header .. "sna statistic list not a table") + valid = false + end + -- environment detector status if type(rtu_statuses.rad_mon) == "table" then if #rtu_statuses.rad_mon > 0 then @@ -740,13 +840,16 @@ function iocontrol.update_unit_statuses(statuses) if type(unit_state) == "table" then if #unit_state == 6 then + unit.waste_mode = unit_state[5] + unit.waste_product = unit_state[6] + unit.unit_ps.publish("U_StatusLine1", unit_state[1]) unit.unit_ps.publish("U_StatusLine2", unit_state[2]) unit.unit_ps.publish("U_AutoReady", unit_state[3]) unit.unit_ps.publish("U_AutoDegraded", unit_state[4]) - unit.unit_ps.publish("U_AutoWaste", unit_state[5] == types.WASTE_MODE.AUTO) - unit.unit_ps.publish("U_WasteMode", unit_state[5]) - unit.unit_ps.publish("U_WasteProduct", unit_state[6]) + unit.unit_ps.publish("U_AutoWaste", unit.waste_mode == types.WASTE_MODE.AUTO) + unit.unit_ps.publish("U_WasteMode", unit.waste_mode) + unit.unit_ps.publish("U_WasteProduct", unit.waste_product) else log.debug(log_header .. "unit state length mismatch") valid = false @@ -755,10 +858,18 @@ function iocontrol.update_unit_statuses(statuses) log.debug(log_header .. "unit state not a table") valid = false end + + -- determine waste production for this unit, add to statistics + local is_pu = unit.waste_product == types.WASTE_PRODUCT.PLUTONIUM + pu_rate = pu_rate + util.trinary(is_pu, burn_rate / 10.0, 0.0) + po_rate = po_rate + util.trinary(not is_pu, math.min(burn_rate / 10.0, unit.sna_prod_rate), 0.0) end end io.facility.ps.publish("burn_sum", burn_rate_sum) + io.facility.ps.publish("sna_count", sna_count_sum) + io.facility.ps.publish("pu_rate", pu_rate) + io.facility.ps.publish("po_rate", po_rate) -- update alarm sounder sounder.eval(io.units) diff --git a/coordinator/process.lua b/coordinator/process.lua index 1e318ed..72152c8 100644 --- a/coordinator/process.lua +++ b/coordinator/process.lua @@ -11,6 +11,7 @@ local FAC_COMMAND = comms.FAC_COMMAND local UNIT_COMMAND = comms.UNIT_COMMAND local PROCESS = types.PROCESS +local PRODUCT = types.WASTE_PRODUCT ---@class process_controller local process = {} @@ -24,7 +25,9 @@ local self = { burn_target = 0.0, charge_target = 0.0, gen_target = 0.0, - limits = {} + limits = {}, + waste_product = PRODUCT.PLUTONIUM, + pu_fallback = false } } @@ -48,19 +51,23 @@ function process.init(iocontrol, coord_comms) log.error("process.init(): failed to load coordinator settings file") end + -- facility auto control configuration local config = settings.get("PROCESS") ---@type coord_auto_config|nil - if type(config) == "table" then self.config.mode = config.mode self.config.burn_target = config.burn_target self.config.charge_target = config.charge_target self.config.gen_target = config.gen_target self.config.limits = config.limits + self.config.waste_product = config.waste_product + self.config.pu_fallback = config.pu_fallback self.io.facility.ps.publish("process_mode", self.config.mode) self.io.facility.ps.publish("process_burn_target", self.config.burn_target) self.io.facility.ps.publish("process_charge_target", self.config.charge_target) self.io.facility.ps.publish("process_gen_target", self.config.gen_target) + self.io.facility.ps.publish("process_waste_product", self.config.waste_product) + self.io.facility.ps.publish("process_pu_fallback", self.config.pu_fallback) for id = 1, math.min(#self.config.limits, self.io.facility.num_units) do local unit = self.io.units[id] ---@type ioctl_unit @@ -70,18 +77,18 @@ function process.init(iocontrol, coord_comms) log.info("PROCESS: loaded auto control settings from coord.settings") end - local waste_mode = settings.get("WASTE_MODES") ---@type table|nil - - if type(waste_mode) == "table" then - for id, mode in pairs(waste_mode) do + -- unit waste states + local waste_modes = settings.get("WASTE_MODES") ---@type table|nil + if type(waste_modes) == "table" then + for id, mode in pairs(waste_modes) do self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode) end - log.info("PROCESS: loaded waste mode settings from coord.settings") + log.info("PROCESS: loaded unit waste mode settings from coord.settings") end + -- unit priority groups local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil - if type(prio_groups) == "table" then for id, group in pairs(prio_groups) do self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group) @@ -137,7 +144,7 @@ end -- set waste mode ---@param id integer unit ID ---@param mode integer waste mode -function process.set_waste(id, mode) +function process.set_unit_waste(id, mode) -- publish so that if it fails then it gets reset self.io.units[id].unit_ps.publish("U_WasteMode", mode) @@ -153,7 +160,7 @@ function process.set_waste(id, mode) settings.set("WASTE_MODES", waste_mode) if not settings.save("/coord.settings") then - log.error("process.set_waste(): failed to save coordinator settings file") + log.error("process.set_unit_waste(): failed to save coordinator settings file") end end @@ -204,6 +211,24 @@ end -- AUTO PROCESS CONTROL -- -------------------------- +-- write auto process control to config file +local function _write_auto_config() + -- attempt to load settings + if not settings.load("/coord.settings") then + log.warning("process._write_config(): failed to load coordinator settings file") + end + + -- save config + settings.set("PROCESS", self.config) + local saved = settings.save("/coord.settings") + + if not saved then + log.warning("process._write_config(): failed to save coordinator settings file") + end + + return not not saved +end + -- stop automatic process control function process.stop_auto() self.comms.send_fac_command(FAC_COMMAND.STOP) @@ -216,6 +241,30 @@ function process.start_auto() log.debug("PROCESS: START AUTO CTL") end +-- set automatic process control waste mode +---@param product WASTE_PRODUCT waste product for auto control +function process.set_process_waste(product) + self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, product) + + log.debug(util.c("PROCESS: SET WASTE ", product)) + + -- update config table and save + self.config.waste_product = product + _write_auto_config() +end + +-- set automatic process control plutonium fallback +---@param enabled boolean whether to enable plutonium fallback +function process.set_pu_fallback(enabled) + self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, enabled) + + log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled)) + + -- update config table and save + self.config.pu_fallback = enabled + _write_auto_config() +end + -- save process control settings ---@param mode PROCESS control mode ---@param burn_target number burn rate target @@ -223,29 +272,17 @@ end ---@param gen_target number generation rate target ---@param limits table unit burn rate limits function process.save(mode, burn_target, charge_target, gen_target, limits) - -- attempt to load settings - if not settings.load("/coord.settings") then - log.warning("process.save(): failed to load coordinator settings file") - end + log.debug("PROCESS: SAVE") - -- config table - self.config = { - mode = mode, - burn_target = burn_target, - charge_target = charge_target, - gen_target = gen_target, - limits = limits - } + -- update config table + self.config.mode = mode + self.config.burn_target = burn_target + self.config.charge_target = charge_target + self.config.gen_target = gen_target + self.config.limits = limits -- save config - settings.set("PROCESS", self.config) - local saved = settings.save("/coord.settings") - - if not saved then - log.warning("process.save(): failed to save coordinator settings file") - end - - self.io.facility.save_cfg_ack(saved) + self.io.facility.save_cfg_ack(_write_auto_config()) end -- handle a start command acknowledgement @@ -258,16 +295,33 @@ function process.start_ack_handle(response) self.config.charge_target = response[4] self.config.gen_target = response[5] - for i = 1, #response[6] do + for i = 1, math.min(#response[6], self.io.facility.num_units) do self.config.limits[i] = response[6][i] + + local unit = self.io.units[i] ---@type ioctl_unit + unit.unit_ps.publish("burn_limit", self.config.limits[i]) end - self.io.facility.ps.publish("auto_mode", self.config.mode) - self.io.facility.ps.publish("burn_target", self.config.burn_target) - self.io.facility.ps.publish("charge_target", self.config.charge_target) - self.io.facility.ps.publish("gen_target", self.config.gen_target) + self.io.facility.ps.publish("process_mode", self.config.mode) + self.io.facility.ps.publish("process_burn_target", self.config.burn_target) + self.io.facility.ps.publish("process_charge_target", self.config.charge_target) + self.io.facility.ps.publish("process_gen_target", self.config.gen_target) self.io.facility.start_ack(ack) end +-- record waste product state after attempting to change it +---@param response WASTE_PRODUCT supervisor waste product state +function process.waste_ack_handle(response) + self.config.waste_product = response + self.io.facility.ps.publish("process_waste_product", response) +end + +-- record plutonium fallback state after attempting to change it +---@param response boolean supervisor plutonium fallback state +function process.pu_fb_ack_handle(response) + self.config.pu_fallback = response + self.io.facility.ps.publish("process_pu_fallback", response) +end + return process diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 8bbc550..f58642d 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -21,7 +21,7 @@ local sounder = require("coordinator.sounder") local apisessions = require("coordinator.session.apisessions") -local COORDINATOR_VERSION = "v0.17.1" +local COORDINATOR_VERSION = "v0.18.0" local println = util.println local println_ts = util.println_ts diff --git a/coordinator/ui/components/process_ctl.lua b/coordinator/ui/components/process_ctl.lua index b945914..0716619 100644 --- a/coordinator/ui/components/process_ctl.lua +++ b/coordinator/ui/components/process_ctl.lua @@ -56,11 +56,12 @@ local function new_view(root, x, y) local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=cpair(colors.green,colors.red)} local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green} local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=cpair(colors.green,colors.gray)} - local sps = IndicatorLight{parent=main,label="SPS Online",colors=cpair(colors.green,colors.gray)} + local sps = IndicatorLight{parent=main,label="SPS Connected",colors=cpair(colors.green,colors.gray)} all_ok.register(facility.ps, "all_sys_ok", all_ok.update) - ind_mat.register(facility.induction_ps_tbl[1], "computed_status", function (status) ind_mat.update(status > 1) end) rad_mon.register(facility.ps, "rad_computed_status", rad_mon.update) + ind_mat.register(facility.induction_ps_tbl[1], "computed_status", function (status) ind_mat.update(status > 1) end) + sps.register(facility.sps_ps_tbl[1], "computed_status", function (status) sps.update(status > 1) end) main.line_break() @@ -321,12 +322,18 @@ local function new_view(root, x, y) local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3} local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17} - RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=status.update,radio_colors=cpair(colors.white,colors.black),radio_bg=colors.brown} + status.register(facility.ps, "current_waste_product", status.update) - local pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=function()end,box_fg_bg=cpair(colors.green,colors.black)} + local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.white,colors.black),radio_bg=colors.brown} + local pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.green,colors.black)} + + waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value) + pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value) local fb_active = IndicatorLight{parent=rect,x=2,y=9,label="Fallback Active",colors=cpair(colors.white,colors.gray)} + fb_active.register(facility.ps, "pu_fallback_active", fb_active.update) + TextBox{parent=rect,x=2,y=11,text="Plutonium Rate",height=1,width=17,fg_bg=style.label} local pu_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17} @@ -334,13 +341,15 @@ local function new_view(root, x, y) local po_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17} TextBox{parent=rect,x=2,y=17,text="Antimatter Rate",height=1,width=17,fg_bg=style.label} - local am_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17} + local am_rate = DataIndicator{parent=rect,x=2,label="",unit="\xb5B/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17} + + pu_rate.register(facility.ps, "pu_rate", pu_rate.update) + po_rate.register(facility.ps, "po_rate", po_rate.update) + am_rate.register(facility.ps, "am_rate", am_rate.update) local sna_count = DataIndicator{parent=rect,x=2,y=20,label="Linked SNAs:",format="%4d",value=0,lu_colors=lu_cpair,width=17} - -- local text_fg_bg = cpair(colors.black, colors.lightGray) - -- local label_fg_bg = cpair(colors.gray, colors.lightGray) - -- local lu_col = cpair(colors.gray, colors.gray) + sna_count.register(facility.ps, "sna_count", sna_count.update) end return new_view diff --git a/coordinator/ui/style.lua b/coordinator/ui/style.lua index 5db54d5..46faf4a 100644 --- a/coordinator/ui/style.lua +++ b/coordinator/ui/style.lua @@ -155,6 +155,32 @@ style.imatrix = { } } +style.sps = { + -- SPS states + states = { + { + color = cpair(colors.black, colors.yellow), + text = "OFF-LINE" + }, + { + color = cpair(colors.black, colors.orange), + text = "NOT FORMED" + }, + { + color = cpair(colors.black, colors.orange), + text = "RTU FAULT" + }, + { + color = cpair(colors.black, colors.gray), + text = "IDLE" + }, + { + color = cpair(colors.black, colors.green), + text = "ACTIVE" + } + } +} + style.waste = { -- auto waste processing states states = { @@ -162,10 +188,6 @@ style.waste = { color = cpair(colors.black, colors.green), text = "PLUTONIUM" }, - { - color = cpair(colors.black, colors.green), - text = "PLUTONIUM (FB)" - }, { color = cpair(colors.black, colors.cyan), text = "POLONIUM" diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 8d52e45..a9fa5a5 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -92,9 +92,11 @@ local PLC_AUTO_ACK = { ---@enum FAC_COMMAND local FAC_COMMAND = { SCRAM_ALL = 0, -- SCRAM all reactors - STOP = 1, -- stop automatic control - START = 2, -- start automatic control - ACK_ALL_ALARMS = 3 -- acknowledge all alarms on all units + STOP = 1, -- stop automatic process control + START = 2, -- start automatic process control + ACK_ALL_ALARMS = 3, -- acknowledge all alarms on all units + SET_WASTE_MODE = 4, -- set automatic waste processing mode + SET_PU_FB = 5 -- set plutonium fallback mode } ---@enum UNIT_COMMAND diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 87c99b2..52870fa 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -11,6 +11,9 @@ local rsctl = require("supervisor.session.rsctl") local PROCESS = types.PROCESS local PROCESS_NAMES = types.PROCESS_NAMES local PRIO = types.ALARM_PRIORITY +local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE +local WASTE = types.WASTE_PRODUCT +local WASTE_MODE = types.WASTE_MODE local IO = rsio.IO @@ -61,6 +64,7 @@ function facility.new(num_reactors, cooling_conf) rtu_conn_count = 0, redstone = {}, induction = {}, + sps = {}, envd = {}, -- redstone I/O control io_ctl = nil, ---@type rs_controller @@ -99,6 +103,10 @@ function facility.new(num_reactors, cooling_conf) last_update = 0, last_error = 0.0, last_time = 0.0, + -- waste processing + waste_product = WASTE.PLUTONIUM, + current_waste_product = WASTE.PLUTONIUM, + pu_fallback = false, -- statistics im_stat_init = false, avg_charge = util.mov_avg(3, 0.0), @@ -211,6 +219,12 @@ function facility.new(num_reactors, cooling_conf) table.insert(self.induction, imatrix) end + -- link an SPS RTU session + ---@param sps unit_session + function public.add_sps(sps) + table.insert(self.sps, sps) + end + -- link an environment detector RTU session ---@param envd unit_session function public.add_envd(envd) @@ -222,6 +236,7 @@ function facility.new(num_reactors, cooling_conf) function public.purge_rtu_devices(session) util.filter_table(self.redstone, function (s) return s.get_session_id() ~= session end) util.filter_table(self.induction, function (s) return s.get_session_id() ~= session end) + util.filter_table(self.sps, function (s) return s.get_session_id() ~= session end) util.filter_table(self.envd, function (s) return s.get_session_id() ~= session end) end @@ -238,6 +253,7 @@ function facility.new(num_reactors, cooling_conf) -- unlink RTU unit sessions if they are closed _unlink_disconnected_units(self.redstone) _unlink_disconnected_units(self.induction) + _unlink_disconnected_units(self.sps) _unlink_disconnected_units(self.envd) -- current state for process control @@ -277,6 +293,8 @@ function facility.new(num_reactors, cooling_conf) -- Run Process Control -- ------------------------- + --#region Process Control + local avg_charge = self.avg_charge.compute() local avg_inflow = self.avg_inflow.compute() @@ -542,10 +560,14 @@ function facility.new(num_reactors, cooling_conf) next_mode = PROCESS.INACTIVE end + --#endregion + ------------------------------ -- Evaluate Automatic SCRAM -- ------------------------------ + --#region Automatic SCRAM + local astatus = self.ascram_status if self.induction[1] ~= nil then @@ -659,6 +681,8 @@ function facility.new(num_reactors, cooling_conf) end end + --#endregion + -- update last mode and set next mode self.last_mode = self.mode self.mode = next_mode @@ -692,12 +716,33 @@ function facility.new(num_reactors, cooling_conf) self.io_ctl.digital_write(IO.F_ALARM, has_alarm) end + + ----------------------------- + -- Update Waste Processing -- + ----------------------------- + + local insufficent_po_rate = false + for i = 1, #self.units do + local u = self.units[i] ---@type reactor_unit + if u.get_control_inf().waste_mode == WASTE_MODE.AUTO then + if (u.get_sna_rate() * 10.0) < u.get_burn_rate() then + insufficent_po_rate = true + break + end + end + end + + if self.waste_product == WASTE.PLUTONIUM or (self.pu_fallback and insufficent_po_rate) then + self.current_waste_product = WASTE.PLUTONIUM + else self.current_waste_product = self.waste_product end end - -- call the update function of all units in the facility + -- call the update function of all units in the facility
+ -- additionally sets the requested auto waste mode if applicable function public.update_units() for i = 1, #self.units do local u = self.units[i] ---@type reactor_unit + u.auto_set_waste(self.current_waste_product) u.update() end end @@ -721,15 +766,15 @@ function facility.new(num_reactors, cooling_conf) end -- stop auto control - function public.auto_stop() - self.mode = PROCESS.INACTIVE - end + function public.auto_stop() self.mode = PROCESS.INACTIVE end -- set automatic control configuration and start the process ---@param config coord_auto_config configuration ---@return table response ready state (successfully started) and current configuration (after updating) function public.auto_start(config) - local ready = false + local charge_scaler = 1000000 -- convert MFE to FE + local gen_scaler = 1000 -- convert kFE to FE + local ready = false -- load up current limits local limits = {} @@ -749,11 +794,11 @@ function facility.new(num_reactors, cooling_conf) end if (type(config.charge_target) == "number") and config.charge_target >= 0 then - self.charge_setpoint = config.charge_target * 1000000 -- convert MFE to FE + self.charge_setpoint = config.charge_target * charge_scaler end if (type(config.gen_target) == "number") and config.gen_target >= 0 then - self.gen_rate_setpoint = config.gen_target * 1000 -- convert kFE to FE + self.gen_rate_setpoint = config.gen_target * gen_scaler end if (type(config.limits) == "table") and (#config.limits == num_reactors) then @@ -782,7 +827,14 @@ function facility.new(num_reactors, cooling_conf) if ready then self.mode = self.mode_set end end - return { ready, self.mode_set, self.burn_target, self.charge_setpoint, self.gen_rate_setpoint, limits } + return { + ready, + self.mode_set, + self.burn_target, + self.charge_setpoint / charge_scaler, + self.gen_rate_setpoint / gen_scaler, + limits + } end -- SETTINGS -- @@ -807,15 +859,35 @@ function facility.new(num_reactors, cooling_conf) end end + -- set waste production + ---@param product WASTE_PRODUCT target product + ---@return WASTE_PRODUCT product newly set value, if valid + function public.set_waste_product(product) + if product == WASTE.PLUTONIUM or product == WASTE.POLONIUM or product == WASTE.ANTI_MATTER then + self.waste_product = product + end + + return self.waste_product + end + + -- enable/disable plutonium fallback + ---@param enabled boolean requested state + ---@return boolean enabled newly set value + function public.set_pu_fallback(enabled) + self.pu_fallback = enabled == true + return self.pu_fallback + end + -- READ STATES/PROPERTIES -- - -- get build properties of all machines + -- get build properties of all facility devices ---@nodiscard - ---@param inc_imatrix boolean? true/nil to include induction matrix build, false to exclude - function public.get_build(inc_imatrix) + ---@param type RTU_UNIT_TYPE? type or nil to include only a particular unit type, or to include all if nil + function public.get_build(type) + local all = type == nil local build = {} - if inc_imatrix ~= false then + if all or type == RTU_UNIT_TYPE.IMATRIX then build.induction = {} for i = 1, #self.induction do local matrix = self.induction[i] ---@type unit_session @@ -823,6 +895,14 @@ function facility.new(num_reactors, cooling_conf) end end + if all or type == RTU_UNIT_TYPE.SPS then + build.sps = {} + for i = 1, #self.sps do + local sps = self.sps[i] ---@type unit_session + build.sps[sps.get_device_idx()] = { sps.get_db().formed, sps.get_db().build } + end + end + return build end @@ -844,7 +924,9 @@ function facility.new(num_reactors, cooling_conf) astat.gen_fault or self.mode == PROCESS.GEN_RATE_FAULT_IDLE, self.status_text[1], self.status_text[2], - self.group_map + self.group_map, + self.current_waste_product, + (self.current_waste_product == WASTE.PLUTONIUM) and (self.waste_product ~= WASTE.PLUTONIUM) } end @@ -875,6 +957,18 @@ function facility.new(num_reactors, cooling_conf) } end + -- status of sps + status.sps = {} + for i = 1, #self.sps do + local sps = self.sps[i] ---@type unit_session + status.sps[sps.get_device_idx()] = { + sps.is_faulted(), + sps.get_db().formed, + sps.get_db().state, + sps.get_db().tanks + } + end + -- radiation monitors (environment detectors) status.rad_mon = {} for i = 1, #self.envd do diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index 92303d8..eb8935f 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -258,6 +258,18 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then facility.ack_all() _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true }) + elseif cmd == FAC_COMMAND.SET_WASTE_MODE then + if pkt.length == 2 then + _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, facility.set_waste_product(pkt.data[2]) }) + else + log.debug(log_header .. "CRDN set waste mode packet length mismatch") + end + elseif cmd == FAC_COMMAND.SET_PU_FB then + if pkt.length == 2 then + _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, facility.set_pu_fallback(pkt.data[2]) }) + else + log.debug(log_header .. "CRDN set pu fallback packet length mismatch") + end else log.debug(log_header .. "CRDN facility command unknown") end @@ -417,7 +429,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil self.retry_times.f_builds_packet = util.time() + PARTIAL_RETRY_PERIOD self.acks.fac_builds = false - _send(SCADA_CRDN_TYPE.FAC_BUILDS, { facility.get_build(cmd.val.type == RTU_UNIT_TYPE.IMATRIX) }) + _send(SCADA_CRDN_TYPE.FAC_BUILDS, { facility.get_build(cmd.val.type) }) end else log.error(log_header .. "unsupported data command received in in_queue (this is a bug)", true) diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index fdf6f55..ce5e08d 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -165,6 +165,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement elseif u_type == RTU_UNIT_TYPE.SPS then -- super-critical phase shifter unit = svrs_sps.new(id, i, unit_advert, self.modbus_q) + if type(unit) ~= "nil" then facility.add_sps(unit) end elseif u_type == RTU_UNIT_TYPE.ENV_DETECTOR then -- environment detector unit = svrs_envd.new(id, i, unit_advert, self.modbus_q) diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 3dc850e..34f295b 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v0.18.0" +local SUPERVISOR_VERSION = "v0.19.0" local println = util.println local println_ts = util.println_ts diff --git a/supervisor/unit.lua b/supervisor/unit.lua index acf03b5..3e2986a 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -74,6 +74,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) turbines = {}, sna = {}, envd = {}, + sna_prod_rate = 0, -- redstone control io_ctl = nil, ---@type rs_controller valves = {}, ---@type unit_valves @@ -91,7 +92,6 @@ function unit.new(reactor_id, num_boilers, num_turbines) damage_start = 0, damage_last = 0, damage_est_last = 0, - waste_mode = WASTE_MODE.AUTO, ---@type WASTE_MODE waste_product = WASTE.PLUTONIUM, ---@type WASTE_PRODUCT status_text = { "UNKNOWN", "awaiting connection..." }, -- logic for alarms @@ -224,7 +224,8 @@ function unit.new(reactor_id, num_boilers, num_turbines) degraded = false, blade_count = 0, br100 = 0, - lim_br100 = 0 + lim_br100 = 0, + waste_mode = WASTE_MODE.AUTO ---@type WASTE_MODE } } } @@ -616,7 +617,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) -- set automatic waste product if mode is set to auto ---@param product WASTE_PRODUCT waste product to generate function public.auto_set_waste(product) - if self.waste_mode == WASTE_MODE.AUTO then + if self.db.control.waste_mode == WASTE_MODE.AUTO then self.waste_product = product _set_waste_valves(product) end @@ -669,7 +670,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) -- set waste processing mode ---@param mode WASTE_MODE processing mode function public.set_waste_mode(mode) - self.waste_mode = mode + self.db.control.waste_mode = mode if mode == WASTE_MODE.MANUAL_PLUTONIUM then _set_waste_valves(WASTE.PLUTONIUM) @@ -759,6 +760,14 @@ function unit.new(reactor_id, num_boilers, num_turbines) return status end + -- get the current burn rate (actual rate) + ---@nodiscard + function public.get_burn_rate() + local rate = 0 + if self.plc_i ~= nil then rate = self.plc_i.get_status().act_burn_rate end + return rate or 0 + end + -- get RTU statuses ---@nodiscard function public.get_rtu_statuses() @@ -779,7 +788,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) -- status of turbines (including tanks) status.turbines = {} for i = 1, #self.turbines do - local turbine = self.turbines[i] ---@type unit_session + local turbine = self.turbines[i] ---@type unit_session status.turbines[turbine.get_device_idx()] = { turbine.is_faulted(), turbine.get_db().formed, @@ -788,10 +797,13 @@ function unit.new(reactor_id, num_boilers, num_turbines) } end + -- basic SNA statistical information, don't send everything, it's not necessary + status.sna = { #self.sna, public.get_sna_rate() } + -- radiation monitors (environment detectors) status.rad_mon = {} for i = 1, #self.envd do - local envd = self.envd[i] ---@type unit_session + local envd = self.envd[i] ---@type unit_session status.rad_mon[envd.get_device_idx()] = { envd.is_faulted(), envd.get_db().radiation @@ -801,6 +813,36 @@ function unit.new(reactor_id, num_boilers, num_turbines) return status end + -- get the current total [max] production rate is + ---@nodiscard + ---@return number total_avail_rate + function public.get_sna_rate() + local total_avail_rate = 0 + + for i = 1, #self.sna do + local db = self.sna[i].get_db() ---@type sna_session_db + total_avail_rate = total_avail_rate + db.state.production_rate + end + + return total_avail_rate + end + + -- check plutonium and polonium estimated production rates + ---@nodiscard + ---@return number pu_rate, number po_rate + function public.get_waste_rates() + local pu, po = 0.0, 0.0 + local br = public.get_burn_rate() + + if self.waste_product == WASTE.PLUTONIUM then + pu = br / 10.0 + else + po = math.min(br / 10.0, public.get_sna_rate()) + end + + return pu, po + end + -- get the annunciator status ---@nodiscard function public.get_annunciator() return self.db.annunciator end @@ -821,7 +863,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) self.status_text[2], self.db.control.ready, self.db.control.degraded, - self.waste_mode, + self.db.control.waste_mode, self.waste_product } end