diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index a0fe10f..b13f4a2 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -2,10 +2,10 @@ local comms = require("scada-common.comms") local log = require("scada-common.log") local ppm = require("scada-common.ppm") local util = require("scada-common.util") -local process = require("coordinator.process") local apisessions = require("coordinator.apisessions") local iocontrol = require("coordinator.iocontrol") +local process = require("coordinator.process") local dialog = require("coordinator.ui.dialog") @@ -20,6 +20,7 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES local UNIT_COMMANDS = comms.UNIT_COMMANDS +local FAC_COMMANDS = comms.FAC_COMMANDS local coordinator = {} @@ -313,11 +314,25 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_wa return self.sv_linked end + -- send a facility command + ---@param cmd FAC_COMMANDS command + function public.send_fac_command(cmd) + _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_CMD, { cmd }) + end + + -- send the auto process control configuration with a start command + ---@param config coord_auto_config configuration + function public.send_auto_start(config) + _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_CMD, { + FAC_COMMANDS.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits + }) + end + -- send a unit command ---@param cmd UNIT_COMMANDS command ---@param unit integer unit ID - ---@param option any? optional options (like burn rate) - function public.send_command(cmd, unit, option) + ---@param option any? optional option options for the optional options (like burn rate) (does option still look like a word?) + function public.send_unit_command(cmd, unit, option) _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.UNIT_CMD, { cmd, unit, option }) end @@ -412,6 +427,26 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_wa end elseif packet.type == SCADA_CRDN_TYPES.FAC_CMD then -- facility command acknowledgement + if packet.length >= 2 then + local cmd = packet.data[1] + local ack = packet.data[2] == true + + if cmd == FAC_COMMANDS.SCRAM_ALL then + iocontrol.get_db().facility.scram_ack(ack) + elseif cmd == FAC_COMMANDS.STOP then + iocontrol.get_db().facility.stop_ack(ack) + elseif cmd == FAC_COMMANDS.START then + if packet.length == 7 then + process.start_ack_handle({ table.unpack(packet.data, 2) }) + else + log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch") + end + else + log.debug(util.c("received facility command ack with unknown command ", cmd)) + end + else + log.debug("SCADA_CRDN facility command ack packet length mismatch") + end elseif packet.type == SCADA_CRDN_TYPES.UNIT_BUILDS then -- record builds if iocontrol.record_unit_builds(packet.data) then diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 0d0aa35..7d8444b 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -24,9 +24,17 @@ function iocontrol.init(conf, comms) ---@class ioctl_facility io.facility = { auto_active = false, - scram = false, + auto_ramping = false, + auto_scram = false, + auto_scram_cause = "ok", ---@type auto_scram_cause + + num_units = conf.num_units, ---@type integer + + save_cfg_ack = function (success) end, ---@param success boolean + start_ack = function (success) end, ---@param success boolean + stop_ack = function (success) end, ---@param success boolean + scram_ack = function (success) end, ---@param success boolean - num_units = conf.num_units, ---@type integer ps = psil.create(), induction_ps_tbl = {}, @@ -69,7 +77,6 @@ function iocontrol.init(conf, comms) set_waste = function (mode) process.set_waste(i, mode) end, ---@param mode integer waste processing mode set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0 - set_limit = function (lim) process.set_limit(i, lim) end, ---@param lim number burn rate limit start_ack = function (success) end, ---@param success boolean scram_ack = function (success) end, ---@param success boolean @@ -195,7 +202,7 @@ function iocontrol.record_unit_builds(builds) -- reactor build if type(build.reactor) == "table" then - unit.reactor_data.mek_struct = build.reactor + unit.reactor_data.mek_struct = build.reactor ---@type mek_struct for key, val in pairs(unit.reactor_data.mek_struct) do unit.reactor_ps.publish(key, val) end @@ -257,11 +264,38 @@ function iocontrol.update_facility_status(status) else local fac = io.facility + -- auto control status information + + local ctl_status = status[1] + + if type(ctl_status) == "table" then + fac.auto_active = ctl_status[1] > 0 + fac.auto_ramping = ctl_status[2] + fac.auto_scram = ctl_status[3] + fac.auto_scram_cause = ctl_status[4] + + fac.ps.publish("auto_active", fac.auto_active) + fac.ps.publish("auto_ramping", fac.auto_ramping) + fac.ps.publish("auto_scram", fac.auto_scram) + fac.ps.publish("auto_scram_cause", fac.auto_scram_cause) + else + log.debug(log_header .. "control status not a table") + end + -- RTU statuses - local rtu_statuses = status[1] + local rtu_statuses = status[2] if type(rtu_statuses) == "table" then + -- power statistics + if type(rtu_statuses.power) == "table" then + fac.ps.publish("avg_charge", rtu_statuses.power[1]) + fac.ps.publish("avg_inflow", rtu_statuses.power[2]) + fac.ps.publish("avg_outflow", rtu_statuses.power[3]) + else + log.debug(log_header .. "power statistics list not a table") + end + -- induction matricies statuses if type(rtu_statuses.induction) == "table" then for id = 1, #fac.induction_ps_tbl do @@ -328,6 +362,8 @@ function iocontrol.update_unit_statuses(statuses) log.debug("iocontrol.update_unit_statuses: number of provided unit statuses does not match expected number of units") return false else + local burn_rate_sum = 0.0 + -- get all unit statuses for i = 1, #statuses do local log_header = util.c("iocontrol.update_unit_statuses[unit ", i, "]: ") @@ -369,6 +405,11 @@ function iocontrol.update_unit_statuses(statuses) unit.reactor_data.rps_status = rps_status ---@type rps_status unit.reactor_data.mek_status = mek_status ---@type mek_status + -- 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 + end + if unit.reactor_data.mek_status.status then unit.reactor_ps.publish("computed_status", 5) -- running else @@ -596,8 +637,8 @@ function iocontrol.update_unit_statuses(statuses) local auto_ctl_state = status[6] if type(auto_ctl_state) == "table" then - if #auto_ctl_state == 1 then - unit.reactor_ps.publish("burn_limit", auto_ctl_state[1]) + if #auto_ctl_state == 0 then + ---@todo else log.debug(log_header .. "auto control state length mismatch") end @@ -606,6 +647,8 @@ function iocontrol.update_unit_statuses(statuses) end end + io.facility.ps.publish("burn_sum", burn_rate_sum) + -- update alarm sounder sounder.eval(io.units) end diff --git a/coordinator/process.lua b/coordinator/process.lua index 963e2c7..e1d067f 100644 --- a/coordinator/process.lua +++ b/coordinator/process.lua @@ -1,16 +1,28 @@ 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 FAC_COMMANDS = comms.FAC_COMMANDS local UNIT_COMMANDS = comms.UNIT_COMMANDS +local PROCESS = types.PROCESS + ---@class process_controller local process = {} local self = { io = nil, ---@type ioctl - comms = nil ---@type coord_comms + comms = nil, ---@type coord_comms + ---@class coord_auto_config + config = { + mode = 0, ---@type PROCESS + burn_target = 0.0, + charge_target = 0.0, + gen_target = 0.0, + limits = {} ---@type table + } } -------------------------- @@ -24,11 +36,37 @@ function process.init(iocontrol, comms) self.io = iocontrol self.comms = comms + for i = 1, self.io.facility.num_units do + self.config.limits[i] = 0.1 + end + -- load settings if not settings.load("/coord.settings") then log.error("process.init(): failed to load coordinator settings file") end + 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.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) + + for id = 1, math.min(#self.config.limits, self.io.facility.num_units) do + local unit = self.io.units[id] ---@type ioctl_unit + unit.reactor_ps.publish("burn_limit", self.config.limits[id]) + end + + 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 @@ -44,7 +82,7 @@ end ---@param id integer unit ID function process.start(id) self.io.units[id].control_state = true - self.comms.send_command(UNIT_COMMANDS.START, id) + self.comms.send_unit_command(UNIT_COMMANDS.START, id) log.debug(util.c("UNIT[", id, "]: START")) end @@ -52,14 +90,14 @@ end ---@param id integer unit ID function process.scram(id) self.io.units[id].control_state = false - self.comms.send_command(UNIT_COMMANDS.SCRAM, id) + self.comms.send_unit_command(UNIT_COMMANDS.SCRAM, id) log.debug(util.c("UNIT[", id, "]: SCRAM")) end -- reset reactor protection system ---@param id integer unit ID function process.reset_rps(id) - self.comms.send_command(UNIT_COMMANDS.RESET_RPS, id) + self.comms.send_unit_command(UNIT_COMMANDS.RESET_RPS, id) log.debug(util.c("UNIT[", id, "]: RESET RPS")) end @@ -67,7 +105,7 @@ end ---@param id integer unit ID ---@param rate number burn rate function process.set_rate(id, rate) - self.comms.send_command(UNIT_COMMANDS.SET_BURN, id, rate) + self.comms.send_unit_command(UNIT_COMMANDS.SET_BURN, id, rate) log.debug(util.c("UNIT[", id, "]: SET BURN = ", rate)) end @@ -75,7 +113,7 @@ end ---@param id integer unit ID ---@param mode integer waste mode function process.set_waste(id, mode) - self.comms.send_command(UNIT_COMMANDS.SET_WASTE, id, mode) + self.comms.send_unit_command(UNIT_COMMANDS.SET_WASTE, id, mode) log.debug(util.c("UNIT[", id, "]: SET WASTE = ", mode)) local waste_mode = settings.get("WASTE_MODES") ---@type table|nil @@ -96,7 +134,7 @@ end -- acknowledge all alarms ---@param id integer unit ID function process.ack_all_alarms(id) - self.comms.send_command(UNIT_COMMANDS.ACK_ALL_ALARMS, id) + self.comms.send_unit_command(UNIT_COMMANDS.ACK_ALL_ALARMS, id) log.debug(util.c("UNIT[", id, "]: ACK ALL ALARMS")) end @@ -104,7 +142,7 @@ end ---@param id integer unit ID ---@param alarm integer alarm ID function process.ack_alarm(id, alarm) - self.comms.send_command(UNIT_COMMANDS.ACK_ALARM, id, alarm) + self.comms.send_unit_command(UNIT_COMMANDS.ACK_ALARM, id, alarm) log.debug(util.c("UNIT[", id, "]: ACK ALARM ", alarm)) end @@ -112,7 +150,7 @@ end ---@param id integer unit ID ---@param alarm integer alarm ID function process.reset_alarm(id, alarm) - self.comms.send_command(UNIT_COMMANDS.RESET_ALARM, id, alarm) + self.comms.send_unit_command(UNIT_COMMANDS.RESET_ALARM, id, alarm) log.debug(util.c("UNIT[", id, "]: RESET ALARM ", alarm)) end @@ -120,16 +158,86 @@ end ---@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) - self.comms.send_command(UNIT_COMMANDS.SET_GROUP, unit_id, group_id) + self.comms.send_unit_command(UNIT_COMMANDS.SET_GROUP, unit_id, group_id) log.debug(util.c("UNIT[", unit_id, "]: SET GROUP ", group_id)) end --- set the burn rate limit ----@param id integer unit ID ----@param limit number burn rate limit -function process.set_limit(id, limit) - self.comms.send_command(UNIT_COMMANDS.SET_LIMIT, id, limit) - log.debug(util.c("UNIT[", id, "]: SET LIMIT = ", limit)) +-------------------------- +-- AUTO PROCESS CONTROL -- +-------------------------- + +-- facility SCRAM command +function process.fac_scram() + self.comms.send_fac_command(FAC_COMMANDS.SCRAM_ALL) + log.debug("FAC: SCRAM ALL") +end + +-- stop automatic process control +function process.stop_auto() + self.comms.send_fac_command(FAC_COMMANDS.STOP) + log.debug("FAC: STOP AUTO") +end + +-- start automatic process control +function process.start_auto() + self.comms.send_auto_start(self.config) + log.debug("FAC: START AUTO") +end + +-- save process control settings +---@param mode PROCESS control mode +---@param burn_target number burn rate target +---@param charge_target number charge target +---@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 + + -- config table + self.config = { + mode = mode, + burn_target = burn_target, + charge_target = charge_target, + gen_target = gen_target, + 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 + + log.debug("saved = " .. util.strval(saved)) + + self.io.facility.save_cfg_ack(saved) +end + +-- handle a start command acknowledgement +---@param response table ack and configuration reply +function process.start_ack_handle(response) + local ack = response[1] + + self.config.mode = response[2] + self.config.burn_target = response[3] + self.config.charge_target = response[4] + self.config.gen_target = response[5] + + for i = 1, #response[6] do + self.config.limits[i] = response[6][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.start_ack(ack) end -------------------------- diff --git a/coordinator/ui/components/processctl.lua b/coordinator/ui/components/processctl.lua index d8dece4..91f888d 100644 --- a/coordinator/ui/components/processctl.lua +++ b/coordinator/ui/components/processctl.lua @@ -1,6 +1,8 @@ +local tcd = require("scada-common.tcallbackdsp") local util = require("scada-common.util") local iocontrol = require("coordinator.iocontrol") +local process = require("coordinator.process") local style = require("coordinator.ui.style") @@ -36,29 +38,127 @@ local function new_view(root, x, y) local facility = iocontrol.get_db().facility local units = iocontrol.get_db().units - local bw_fg_bg = cpair(colors.black, colors.white) + local bw_fg_bg = cpair(colors.black, colors.white) + local hzd_fg_bg = cpair(colors.white, colors.gray) + local dis_colors = cpair(colors.white, colors.lightGray) - local proc = Div{parent=root,width=60,height=24,x=x,y=y} + local main = Div{parent=root,width=80,height=24,x=x,y=y} - local limits = Div{parent=proc,width=40,height=24,x=30,y=1} + local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,fg_bg=hzd_fg_bg} + + facility.scram_ack = scram.on_response + + --------------------- + -- process control -- + --------------------- + + local proc = Div{parent=main,width=54,height=24,x=27,y=1} + + ----------------------------- + -- process control targets -- + ----------------------------- + + local targets = Div{parent=proc,width=31,height=24,x=1,y=1} + + local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)} + TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2} + + local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)} + local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg} + TextBox{parent=burn_target,x=18,y=2,text="mB/t"} + local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)} + + facility.ps.subscribe("process_burn_target", b_target.set_value) + facility.ps.subscribe("burn_sum", burn_sum.update) + + local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)} + TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2} + + local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)} + local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg} + TextBox{parent=chg_target,x=18,y=2,text="kFE"} + local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="kFE",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)} + + facility.ps.subscribe("process_charge_target", c_target.set_value) + facility.induction_ps_tbl[1].subscribe("energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000) end) + + local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)} + TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2} + + local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)} + local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg} + TextBox{parent=gen_target,x=18,y=2,text="kFE/t"} + local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)} + + facility.ps.subscribe("process_gen_target", g_target.set_value) + facility.induction_ps_tbl[1].subscribe("last_input", function (j) cur_gen.update(util.joules_to_fe(j) / 1000) end) + + ----------------- + -- unit limits -- + ----------------- + + local limit_div = Div{parent=proc,width=40,height=19,x=34,y=6} + + local rate_limits = {} for i = 1, facility.num_units do - local unit = units[i] ---@type ioctl_entry + local unit = units[i] ---@type ioctl_unit - local _y = ((i - 1) * 4) + 1 + local _y = ((i - 1) * 5) + 1 - TextBox{parent=limits,x=1,y=_y+1,text="Unit "..i} - - local lim_ctl = Div{parent=limits,x=8,y=_y,width=20,height=3,fg_bg=cpair(colors.gray,colors.white)} - local burn_rate = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,max=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg} - - unit.reactor_ps.subscribe("max_burn", burn_rate.set_max) - unit.reactor_ps.subscribe("burn_limit", burn_rate.set_value) + local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=cpair(colors.black,colors.lightBlue)} + TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2} + local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(colors.gray,colors.white)} + rate_limits[i] = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg} TextBox{parent=lim_ctl,x=9,y=2,text="mB/t"} - local set_burn = function () unit.set_limit(burn_rate.get_value()) end - PushButton{parent=lim_ctl,x=14,y=2,text="SAVE",min_width=6,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=set_burn} + unit.reactor_ps.subscribe("max_burn", rate_limits[i].set_max) + unit.reactor_ps.subscribe("burn_limit", rate_limits[i].set_value) + + local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(colors.black,colors.black),width=14,fg_bg=cpair(colors.black,colors.brown)} + + unit.reactor_ps.subscribe("act_burn_rate", cur_burn.update) + end + + ------------------------- + -- controls and status -- + ------------------------- + + local ctl_opts = { "Regulated", "Burn Rate", "Charge Level", "Generation Rate" } + local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.purple,colors.black),radio_bg=colors.gray} + + facility.ps.subscribe("process_mode", mode.set_value) + + local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg} + local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg} + local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)} + + local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=cpair(colors.gray,colors.white)} + + -- save the automatic process control configuration without starting + local function _save_cfg() + local limits = {} + for i = 1, #rate_limits do limits[i] = rate_limits[i].get_value() end + + process.save(mode.get_value(), b_target.get_value(), c_target.get_value(), g_target.get_value(), limits) + end + + -- start automatic control after saving process control settings + local function _start_auto() + _save_cfg() + process.start_auto() + end + + local save = HazardButton{parent=auto_controls,x=2,y=2,text="SAVE",accent=colors.purple,dis_colors=dis_colors,callback=_save_cfg,fg_bg=hzd_fg_bg} + local start = HazardButton{parent=auto_controls,x=13,y=2,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=_start_auto,fg_bg=hzd_fg_bg} + local stop = HazardButton{parent=auto_controls,x=23,y=2,text="STOP",accent=colors.orange,dis_colors=dis_colors,callback=process.stop_auto,fg_bg=hzd_fg_bg} + + facility.start_ack = start.on_response + facility.stop_ack = stop.on_response + + function facility.save_cfg_ack(ack) + tcd.dispatch(0.2, function () save.on_response(ack) end) end end diff --git a/coordinator/ui/layout/main_view.lua b/coordinator/ui/layout/main_view.lua index 8609f73..59ac653 100644 --- a/coordinator/ui/layout/main_view.lua +++ b/coordinator/ui/layout/main_view.lua @@ -83,7 +83,7 @@ local function init(monitor) -- testing ---@fixme remove test code - ColorMap{parent=main,x=2,y=(main.height()-1)} + ColorMap{parent=main,x=132,y=(main.height()-1)} local audio = Div{parent=main,width=34,height=15,x=95,y=cnc_y_start} diff --git a/graphics/elements/controls/hazard_button.lua b/graphics/elements/controls/hazard_button.lua index dec01d2..0b59df6 100644 --- a/graphics/elements/controls/hazard_button.lua +++ b/graphics/elements/controls/hazard_button.lua @@ -145,9 +145,6 @@ local function hazard_button(args) ---@diagnostic disable-next-line: unused-local function e.handle_touch(event) if e.enabled then - -- call the touch callback - args.callback() - -- change text color to indicate clicked e.window.setTextColor(args.accent) e.window.setCursorPos(3, 2) @@ -160,6 +157,9 @@ local function hazard_button(args) -- 1.5 second timeout tcd.dispatch(1.5, on_timeout) + + -- call the touch callback + args.callback() end end diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index afcd255..468d190 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -13,6 +13,7 @@ local DEVICE_TYPES = comms.DEVICE_TYPES local ESTABLISH_ACK = comms.ESTABLISH_ACK local RPLC_TYPES = comms.RPLC_TYPES local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES +local AUTO_ACK = comms.PLC_AUTO_ACK local print = util.print local println = util.println @@ -24,6 +25,8 @@ local println_ts = util.println_ts local PCALL_SCRAM_MSG = "pcall: Scram requires the reactor to be active." local PCALL_START_MSG = "pcall: Reactor is already active." +local AUTO_TOGGLE_DELAY_MS = 5000 + -- RPS SAFETY CONSTANTS local MAX_DAMAGE_PERCENT = 90 @@ -61,6 +64,7 @@ function plc.rps_init(reactor, is_formed) reactor = reactor, state = { false, false, false, false, false, false, false, false, false, false, false, false }, reactor_enabled = false, + enabled_at = 0, formed = is_formed, force_disabled = false, tripped = false, @@ -215,7 +219,7 @@ function plc.rps_init(reactor, is_formed) self.state[state_keys.sys_fail] = true end - -- SCRAM the reactor now + -- SCRAM the reactor now (blocks waiting for server tick) ---@return boolean success function public.scram() log.info("RPS: reactor SCRAM") @@ -226,11 +230,12 @@ function plc.rps_init(reactor, is_formed) return false else self.reactor_enabled = false + self.last_runtime = util.time_ms() - self.enabled_at return true end end - -- start the reactor + -- start the reactor now (blocks waiting for server tick) ---@return boolean success function public.activate() if not self.tripped then @@ -241,6 +246,7 @@ function plc.rps_init(reactor, is_formed) log.error("RPS: failed reactor start") else self.reactor_enabled = true + self.enabled_at = util.time_ms() return true end else @@ -250,6 +256,21 @@ function plc.rps_init(reactor, is_formed) return false end + -- automatic control activate/re-activate + ---@return boolean success + function public.auto_activate() + -- clear automatic SCRAM if it was the cause + if self.tripped and self.trip_cause == "automatic" then + self.state[state_keys.automatic] = true + self.trip_cause = rps_status_t.ok + self.tripped = false + + log.debug("RPS: cleared automatic SCRAM for re-activation") + end + + return public.activate() + end + -- check all safety conditions ---@return boolean tripped, rps_status_t trip_status, boolean first_trip function public.check() @@ -324,7 +345,8 @@ function plc.rps_init(reactor, is_formed) self.tripped = true self.trip_cause = status - -- in the case that the reactor is detected to be active, it will be scrammed shortly after this in the main RPS loop if we don't here + -- in the case that the reactor is detected to be active, + -- it will be scrammed shortly after this in the main RPS loop if we don't here if self.formed then if not self.force_disabled then public.scram() @@ -348,6 +370,10 @@ function plc.rps_init(reactor, is_formed) function public.is_formed() return self.formed end function public.is_force_disabled() return self.force_disabled end + -- get the runtime of the reactor if active, or the last runtime if disabled + ---@return integer runtime time since last enable + function public.get_runtime() return util.trinary(self.reactor_enabled, util.time_ms() - self.enabled_at, self.last_runtime) end + -- reset the RPS ---@param quiet? boolean true to suppress the info log message function public.reset(quiet) @@ -383,8 +409,8 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co reactor = reactor, scrammed = false, linked = false, - status_cache = nil, resend_build = false, + status_cache = nil, max_burn_rate = nil } @@ -532,9 +558,9 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co -- general ack ---@param msg_type RPLC_TYPES - ---@param succeeded boolean - local function _send_ack(msg_type, succeeded) - _send(msg_type, { succeeded }) + ---@param status boolean|integer + local function _send_ack(msg_type, status) + _send(msg_type, { status }) end -- send structure properties (these should not change, server will cache these) @@ -587,6 +613,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co self.reactor = reactor self.status_cache = nil self.resend_build = true + self.max_burn_rate = nil end -- unlink from the server @@ -731,7 +758,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co log.debug("sent out structure again, did supervisor miss it?") elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then -- set the burn rate - if packet.length == 2 then + if (packet.length == 2) and (type(packet.data[1]) == "number") then local success = false local burn_rate = math.floor(packet.data[1] * 10) / 10 local ramp = packet.data[2] @@ -759,7 +786,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co _send_ack(packet.type, success) else - log.debug("RPLC set burn rate packet length mismatch") + log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate") end elseif packet.type == RPLC_TYPES.RPS_ENABLE then -- enable the reactor @@ -779,6 +806,63 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co -- reset the RPS status rps.reset() _send_ack(packet.type, true) + elseif packet.type == RPLC_TYPES.AUTO_BURN_RATE then + -- automatic control requested a new burn rate + if (packet.length == 2) and (type(packet.data[1]) == "number") then + local ack = AUTO_ACK.FAIL + local burn_rate = math.floor(packet.data[1] * 10) / 10 + local ramp = packet.data[2] + + -- if no known max burn rate, check again + if self.max_burn_rate == nil then + self.max_burn_rate = self.reactor.getMaxBurnRate() + end + + -- if we know our max burn rate, update current burn rate setpoint if in range + if self.max_burn_rate ~= ppm.ACCESS_FAULT then + if burn_rate < 0.1 then + if rps.is_active() then + if rps.get_runtime() > AUTO_TOGGLE_DELAY_MS then + -- auto scram to disable + if rps.scram() then + ack = AUTO_ACK.ZERO_DIS_OK + self.auto_last_disable = util.time_ms() + end + else + -- too soon to disable + ack = AUTO_ACK.ZERO_DIS_WAIT + end + else + ack = AUTO_ACK.ZERO_DIS_OK + end + elseif burn_rate <= self.max_burn_rate then + if not rps.is_active() then + -- activate the reactor + if not rps.auto_activate() then + log.debug("automatic reactor activation failed") + end + end + + -- if active, set/ramp burn rate + if rps.is_active() then + if ramp then + setpoints.burn_rate_en = true + setpoints.burn_rate = burn_rate + ack = AUTO_ACK.RAMP_SET_OK + else + self.reactor.setBurnRate(burn_rate) + ack = util.trinary(self.reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK) + end + end + else + log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate) + end + end + + _send_ack(packet.type, ack) + else + log.debug("RPLC set automatic burn rate packet length mismatch or non-numeric burn rate") + end else log.warning("received unknown RPLC packet type " .. packet.type) end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 5e0dca5..427f8f7 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -14,7 +14,7 @@ local config = require("reactor-plc.config") local plc = require("reactor-plc.plc") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "beta-v0.9.10" +local R_PLC_VERSION = "beta-v0.10.0" local print = util.print local println = util.println diff --git a/rtu/startup.lua b/rtu/startup.lua index ec5fc0a..2188549 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -25,7 +25,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "beta-v0.9.10" +local RTU_VERSION = "beta-v0.9.11" local rtu_t = types.rtu_t diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 2abc533..6fed4d5 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -12,7 +12,7 @@ local rtu_t = types.rtu_t local insert = table.insert -comms.version = "1.1.1" +comms.version = "1.1.2" ---@alias PROTOCOLS integer local PROTOCOLS = { @@ -23,14 +23,6 @@ local PROTOCOLS = { COORD_API = 4 -- data/control packets for pocket computers to/from coordinators } ----@alias DEVICE_TYPES integer -local DEVICE_TYPES = { - PLC = 0, -- PLC device type for establish - RTU = 1, -- RTU device type for establish - SV = 2, -- supervisor device type for establish - CRDN = 3 -- coordinator device type for establish -} - ---@alias RPLC_TYPES integer local RPLC_TYPES = { STATUS = 0, -- reactor/system status @@ -41,7 +33,8 @@ local RPLC_TYPES = { RPS_ASCRAM = 5, -- SCRAM reactor (automatic request) RPS_STATUS = 6, -- RPS status RPS_ALARM = 7, -- RPS alarm broadcast - RPS_RESET = 8 -- clear RPS trip (if in bad state, will trip immediately) + RPS_RESET = 8, -- clear RPS trip (if in bad state, will trip immediately) + AUTO_BURN_RATE = 9 -- set an automatic burn rate, PLC will respond with status, enable toggle speed limited } ---@alias SCADA_MGMT_TYPES integer @@ -53,13 +46,6 @@ local SCADA_MGMT_TYPES = { RTU_DEV_REMOUNT = 4 -- RTU multiblock possbily changed (formed, unformed) due to PPM remount } ----@alias ESTABLISH_ACK integer -local ESTABLISH_ACK = { - ALLOW = 0, -- link approved - DENY = 1, -- link denied - COLLISION = 2 -- link denied due to existing active link -} - ---@alias SCADA_CRDN_TYPES integer local SCADA_CRDN_TYPES = { FAC_BUILDS = 0, -- facility RTU builds @@ -70,25 +56,26 @@ local SCADA_CRDN_TYPES = { UNIT_CMD = 5 -- command a reactor unit } ----@alias UNIT_COMMANDS integer -local UNIT_COMMANDS = { - SCRAM = 0, -- SCRAM the reactor - START = 1, -- start the reactor - RESET_RPS = 2, -- reset the RPS - SET_BURN = 3, -- set the burn rate - SET_WASTE = 4, -- set the waste processing mode - ACK_ALL_ALARMS = 5, -- ack all active alarms - ACK_ALARM = 6, -- ack a particular alarm - RESET_ALARM = 7, -- reset a particular alarm - SET_GROUP = 8, -- assign this unit to a group - SET_LIMIT = 9 -- set this unit maximum auto burn rate -} - ---@alias CAPI_TYPES integer local CAPI_TYPES = { ESTABLISH = 0 -- initial greeting } +---@alias ESTABLISH_ACK integer +local ESTABLISH_ACK = { + ALLOW = 0, -- link approved + DENY = 1, -- link denied + COLLISION = 2 -- link denied due to existing active link +} + +---@alias DEVICE_TYPES integer +local DEVICE_TYPES = { + PLC = 0, -- PLC device type for establish + RTU = 1, -- RTU device type for establish + SV = 2, -- supervisor device type for establish + CRDN = 3 -- coordinator device type for establish +} + ---@alias RTU_UNIT_TYPES integer local RTU_UNIT_TYPES = { REDSTONE = 0, -- redstone I/O @@ -100,16 +87,51 @@ local RTU_UNIT_TYPES = { ENV_DETECTOR = 6 -- environment detector } +---@alias PLC_AUTO_ACK integer +local PLC_AUTO_ACK = { + FAIL = 0, -- failed to set burn rate/burn rate invalid + DIRECT_SET_OK = 1, -- successfully set burn rate + RAMP_SET_OK = 2, -- successfully started burn rate ramping + ZERO_DIS_OK = 3, -- successfully disabled reactor with < 0.1 burn rate + ZERO_DIS_WAIT = 4 -- too soon to disable reactor with < 0.1 burn rate +} + +---@alias FAC_COMMANDS integer +local FAC_COMMANDS = { + SCRAM_ALL = 0, -- SCRAM all reactors + STOP = 1, -- stop automatic control + START = 2 -- start automatic control +} + +---@alias UNIT_COMMANDS integer +local UNIT_COMMANDS = { + SCRAM = 0, -- SCRAM the reactor + START = 1, -- start the reactor + RESET_RPS = 2, -- reset the RPS + SET_BURN = 3, -- set the burn rate + SET_WASTE = 4, -- set the waste processing mode + ACK_ALL_ALARMS = 5, -- ack all active alarms + ACK_ALARM = 6, -- ack a particular alarm + RESET_ALARM = 7, -- reset a particular alarm + SET_GROUP = 8 -- assign this unit to a group +} + comms.PROTOCOLS = PROTOCOLS -comms.DEVICE_TYPES = DEVICE_TYPES + comms.RPLC_TYPES = RPLC_TYPES -comms.ESTABLISH_ACK = ESTABLISH_ACK comms.SCADA_MGMT_TYPES = SCADA_MGMT_TYPES comms.SCADA_CRDN_TYPES = SCADA_CRDN_TYPES -comms.UNIT_COMMANDS = UNIT_COMMANDS comms.CAPI_TYPES = CAPI_TYPES + +comms.ESTABLISH_ACK = ESTABLISH_ACK +comms.DEVICE_TYPES = DEVICE_TYPES comms.RTU_UNIT_TYPES = RTU_UNIT_TYPES +comms.PLC_AUTO_ACK = PLC_AUTO_ACK + +comms.UNIT_COMMANDS = UNIT_COMMANDS +comms.FAC_COMMANDS = FAC_COMMANDS + ---@alias packet scada_packet|modbus_packet|rplc_packet|mgmt_packet|crdn_packet|capi_packet ---@alias frame modbus_frame|rplc_frame|mgmt_frame|crdn_frame|capi_frame @@ -308,9 +330,10 @@ function comms.rplc_packet() self.type == RPLC_TYPES.RPS_ENABLE or self.type == RPLC_TYPES.RPS_SCRAM or self.type == RPLC_TYPES.RPS_ASCRAM or - self.type == RPLC_TYPES.RPS_ALARM or self.type == RPLC_TYPES.RPS_STATUS or - self.type == RPLC_TYPES.RPS_RESET + self.type == RPLC_TYPES.RPS_ALARM or + self.type == RPLC_TYPES.RPS_RESET or + self.type == RPLC_TYPES.AUTO_BURN_RATE end -- make an RPLC packet diff --git a/scada-common/types.lua b/scada-common/types.lua index cfdfcad..d4db1e9 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -35,6 +35,15 @@ types.TRI_FAIL = { FULL = 2 } +---@alias PROCESS integer +types.PROCESS = { + INACTIVE = 0, + SIMPLE = 1, + BURN_RATE = 2, + CHARGE = 3, + GEN_RATE = 4 +} + ---@alias WASTE_MODE integer types.WASTE_MODE = { AUTO = 1, @@ -164,6 +173,9 @@ types.ALARM_STATE = { ---| "sys_fail" ---| "force_disabled" +---@alias auto_scram_cause +---| "ok" + ---@alias rtu_t string types.rtu_t = { redstone = "redstone", diff --git a/scada-common/util.lua b/scada-common/util.lua index 979172e..8b88758 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -202,6 +202,13 @@ function util.is_int(x) return type(x) == "number" and x == math.floor(x) end +-- get the sign of a number +---@param x number value +---@return integer sign (-1 for < 0, 1 otherwise) +function util.sign(x) + return util.trinary(x < 0, -1, 1) +end + -- round a number to an integer ---@return integer rounded function util.round(x) diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index 860fa1b..c1d3047 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -11,6 +11,7 @@ local PROTOCOLS = comms.PROTOCOLS local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES local UNIT_COMMANDS = comms.UNIT_COMMANDS +local FAC_COMMANDS = comms.FAC_COMMANDS local SV_Q_CMDS = svqtypes.SV_Q_CMDS local SV_Q_DATA = svqtypes.SV_Q_DATA @@ -133,6 +134,7 @@ function coordinator.new_session(id, in_queue, out_queue, facility) -- send facility status local function _send_fac_status() local status = { + facility.get_control_status(), facility.get_rtu_statuses() } @@ -146,9 +148,7 @@ function coordinator.new_session(id, in_queue, out_queue, facility) for i = 1, #self.units do local unit = self.units[i] ---@type reactor_unit - local auto_ctl = { - unit.get_control_inf().lim_br10 / 10 - } + local auto_ctl = {} status[unit.get_id()] = { unit.get_reactor_status(), @@ -208,6 +208,37 @@ function coordinator.new_session(id, in_queue, out_queue, facility) if pkt.type == SCADA_CRDN_TYPES.FAC_BUILDS then -- acknowledgement to coordinator receiving builds self.acks.fac_builds = true + elseif pkt.type == SCADA_CRDN_TYPES.FAC_CMD then + if pkt.length >= 1 then + local cmd = pkt.data[1] + + if cmd == FAC_COMMANDS.SCRAM_ALL then + facility.scram_all() + _send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true }) + elseif cmd == FAC_COMMANDS.STOP then + facility.auto_stop() + _send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true }) + elseif cmd == FAC_COMMANDS.START then + if pkt.length == 6 then + ---@type coord_auto_config + local config = { + mode = pkt.data[2], + burn_target = pkt.data[3], + charge_target = pkt.data[4], + gen_target = pkt.data[5], + limits = pkt.data[6] + } + + _send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) }) + else + log.debug(log_header .. "CRDN auto start (with configuration) packet length mismatch") + end + else + log.debug(log_header .. "CRDN facility command unknown") + end + else + log.debug(log_header .. "CRDN facility command packet length mismatch") + end elseif pkt.type == SCADA_CRDN_TYPES.UNIT_BUILDS then -- acknowledgement to coordinator receiving builds self.acks.unit_builds = true @@ -234,13 +265,13 @@ function coordinator.new_session(id, in_queue, out_queue, facility) if pkt.length == 3 then self.out_q.push_data(SV_Q_DATA.SET_BURN, data) else - log.debug(log_header .. "CRDN command unit burn rate missing option") + log.debug(log_header .. "CRDN unit command burn rate missing option") end elseif cmd == UNIT_COMMANDS.SET_WASTE then if pkt.length == 3 then unit.set_waste(pkt.data[3]) else - log.debug(log_header .. "CRDN command unit set waste missing option") + log.debug(log_header .. "CRDN unit command set waste missing option") end elseif cmd == UNIT_COMMANDS.ACK_ALL_ALARMS then unit.ack_all() @@ -249,36 +280,29 @@ function coordinator.new_session(id, in_queue, out_queue, facility) if pkt.length == 3 then unit.ack_alarm(pkt.data[3]) else - log.debug(log_header .. "CRDN command unit ack alarm missing alarm id") + log.debug(log_header .. "CRDN unit command ack alarm missing alarm id") end elseif cmd == UNIT_COMMANDS.RESET_ALARM then if pkt.length == 3 then unit.reset_alarm(pkt.data[3]) else - log.debug(log_header .. "CRDN command unit reset alarm missing alarm id") + log.debug(log_header .. "CRDN unit command reset alarm missing alarm id") end elseif cmd == UNIT_COMMANDS.SET_GROUP then if pkt.length == 3 then facility.set_group(unit.get_id(), pkt.data[3]) _send(SCADA_CRDN_TYPES.UNIT_CMD, { cmd, uid, pkt.data[3] }) else - log.debug(log_header .. "CRDN command unit set group missing group id") - end - elseif cmd == UNIT_COMMANDS.SET_LIMIT then - if pkt.length == 3 then - unit.set_burn_limit(pkt.data[3]) - _send(SCADA_CRDN_TYPES.UNIT_CMD, { cmd, uid, pkt.data[3] }) - else - log.debug(log_header .. "CRDN command unit set limit missing group id") + log.debug(log_header .. "CRDN unit command set group missing group id") end else - log.debug(log_header .. "CRDN command unknown") + log.debug(log_header .. "CRDN unit command unknown") end else - log.debug(log_header .. "CRDN command unit invalid") + log.debug(log_header .. "CRDN unit command invalid") end else - log.debug(log_header .. "CRDN command unit packet length mismatch") + log.debug(log_header .. "CRDN unit command packet length mismatch") end else log.debug(log_header .. "handler received unexpected SCADA_CRDN packet type " .. pkt.type) @@ -331,6 +355,8 @@ function coordinator.new_session(id, in_queue, out_queue, facility) self.retry_times.builds_packet = util.time() + RETRY_PERIOD _send_fac_builds() _send_unit_builds() + else + log.warning(log_header .. "unsupported command received in in_queue (this is a bug)") end elseif message.qtype == mqueue.TYPE.DATA then -- instruction with body @@ -339,6 +365,8 @@ function coordinator.new_session(id, in_queue, out_queue, facility) if cmd.key == CRD_S_DATA.CMD_ACK then local ack = cmd.val ---@type coord_ack _send(SCADA_CRDN_TYPES.UNIT_CMD, { ack.cmd, ack.unit, ack.ack }) + else + log.warning(log_header .. "unsupported data command received in in_queue (this is a bug)") end end end diff --git a/supervisor/session/facility.lua b/supervisor/session/facility.lua index d7ceadc..d554fca 100644 --- a/supervisor/session/facility.lua +++ b/supervisor/session/facility.lua @@ -1,12 +1,12 @@ local log = require("scada-common.log") local rsio = require("scada-common.rsio") +local types = require("scada-common.types") local util = require("scada-common.util") local rsctl = require("supervisor.session.rsctl") local unit = require("supervisor.session.unit") -local HEATING_WATER = 20000 -local HEATING_SODIUM = 200000 +local PROCESS = types.PROCESS -- 7.14 kJ per blade for 1 mB of fissile fuel
-- 2856 FE per blade per 1 mB, 285.6 FE per blade per 0.1 mB (minimum) @@ -15,15 +15,6 @@ local POWER_PER_BLADE = util.joules_to_fe(7140) local MAX_CHARGE = 0.99 local RE_ENABLE_CHARGE = 0.95 ----@alias PROCESS integer -local PROCESS = { - INACTIVE = 1, - SIMPLE = 2, - CHARGE = 3, - GEN_RATE = 4, - BURN_RATE = 5 -} - local AUTO_SCRAM = { NONE = 0, MATRIX_DC = 1, @@ -31,7 +22,7 @@ local AUTO_SCRAM = { } local charge_Kp = 1.0 -local charge_Ki = 0.0 +local charge_Ki = 0.00001 local charge_Kd = 0.0 local rate_Kp = 1.0 @@ -41,8 +32,6 @@ local rate_Kd = 0.0 ---@class facility_management local facility = {} -facility.PROCESS_MODES = PROCESS - -- create a new facility management object ---@param num_reactors integer number of reactor units ---@param cooling_conf table cooling configurations of reactor units @@ -54,9 +43,11 @@ function facility.new(num_reactors, cooling_conf) -- process control mode = PROCESS.INACTIVE, last_mode = PROCESS.INACTIVE, - burn_target = 0.0, -- burn rate target for aggregate burn mode + mode_set = PROCESS.SIMPLE, + max_burn_combined = 0.0, -- maximum burn rate to clamp at + burn_target = 0.1, -- burn rate target for aggregate burn mode charge_target = 0, -- FE charge target - charge_rate = 0, -- FE/t charge rate target + gen_rate_target = 0, -- FE/t charge rate target group_map = { 0, 0, 0, 0 }, -- units -> group IDs prio_defs = { {}, {}, {}, {} }, -- priority definitions (each level is a table of units) ascram = false, @@ -67,6 +58,7 @@ function facility.new(num_reactors, cooling_conf) initial_ramp = true, waiting_on_ramp = false, accumulator = 0.0, + saturated = false, last_error = 0.0, last_time = 0.0, -- statistics @@ -214,6 +206,8 @@ function facility.new(num_reactors, cooling_conf) if state_changed then if self.last_mode == PROCESS.INACTIVE then local blade_count = 0 + self.max_burn_combined = 0.0 + for i = 1, #self.prio_defs do table.sort(self.prio_defs[i], ---@param a reactor_unit @@ -224,6 +218,7 @@ function facility.new(num_reactors, cooling_conf) for _, u in pairs(self.prio_defs[i]) do blade_count = blade_count + u.get_db().blade_count u.a_engage() + self.max_burn_combined = self.max_burn_combined + (u.get_control_inf().lim_br10 / 10.0) end end @@ -247,6 +242,18 @@ function facility.new(num_reactors, cooling_conf) if state_changed then self.time_start = now end + elseif self.mode == PROCESS.BURN_RATE then + -- a total aggregate burn rate + if state_changed then + -- nothing special to do + elseif self.waiting_on_ramp and _all_units_ramped() then + self.waiting_on_ramp = false + self.time_start = now + end + + if not self.waiting_on_ramp then + _allocate_burn_rate(self.burn_target, self.initial_ramp) + end elseif self.mode == PROCESS.CHARGE then -- target a level of charge local error = (self.charge_target - avg_charge) / self.charge_conversion @@ -261,23 +268,32 @@ function facility.new(num_reactors, cooling_conf) end if not self.waiting_on_ramp then - self.accumulator = self.accumulator + (avg_charge / self.charge_conversion) + if not self.saturated then + self.accumulator = self.accumulator + ((avg_charge / self.charge_conversion) * (now - self.last_time)) + end local runtime = now - self.time_start - local integral = self.accumulator / runtime - local derivative = (error - self.last_error) / (now - self.last_time) + local integral = self.accumulator + -- local derivative = (error - self.last_error) / (now - self.last_time) local P = (charge_Kp * error) local I = (charge_Ki * integral) - local D = (charge_Kd * derivative) + local D = 0 -- (charge_Kd * derivative) local setpoint = P + I + D + + -- round setpoint -> setpoint rounded (sp_r) local sp_r = util.round(setpoint * 10.0) / 10.0 - log.debug(util.sprintf("PROC_CHRG[%f] { CHRG[%f] ERR[%f] INT[%f] => SP[%f] SP_R[%f] <= P[%f] I[%f] D[%d] }", - runtime, avg_charge, error, integral, setpoint, sp_r, P, I, D)) + -- clamp at range -> setpoint clamped (sp_c) + local sp_c = math.max(0, math.min(sp_r, self.max_burn_combined)) - _allocate_burn_rate(sp_r, self.initial_ramp) + self.saturated = sp_r ~= sp_c + + log.debug(util.sprintf("PROC_CHRG[%f] { CHRG[%f] ERR[%f] INT[%f] => SP[%f] SP_C[%f] <= P[%f] I[%f] D[%d] }", + runtime, avg_charge, error, integral, setpoint, sp_c, P, I, D)) + + _allocate_burn_rate(sp_c, self.initial_ramp) if self.initial_ramp then self.waiting_on_ramp = true @@ -285,7 +301,7 @@ function facility.new(num_reactors, cooling_conf) end elseif self.mode == PROCESS.GEN_RATE then -- target a rate of generation - local error = (self.charge_rate - avg_inflow) / self.charge_conversion + local error = (self.gen_rate_target - avg_inflow) / self.charge_conversion local setpoint = 0.0 if state_changed then @@ -303,36 +319,32 @@ function facility.new(num_reactors, cooling_conf) end if not self.waiting_on_ramp then - self.accumulator = self.accumulator + (avg_inflow / self.charge_conversion) + if not self.saturated then + self.accumulator = self.accumulator + ((avg_inflow / self.charge_conversion) * (now - self.last_time)) + end local runtime = util.time_s() - self.time_start - local integral = self.accumulator / runtime - local derivative = (error - self.last_error) / (now - self.last_time) + local integral = self.accumulator + -- local derivative = (error - self.last_error) / (now - self.last_time) local P = (rate_Kp * error) local I = (rate_Ki * integral) - local D = (rate_Kd * derivative) + local D = 0 -- (rate_Kd * derivative) setpoint = P + I + D + -- round setpoint -> setpoint rounded (sp_r) local sp_r = util.round(setpoint * 10.0) / 10.0 - log.debug(util.sprintf("PROC_RATE[%f] { RATE[%f] ERR[%f] INT[%f] => SP[%f] SP_R[%f] <= P[%f] I[%f] D[%f] }", - runtime, avg_inflow, error, integral, setpoint, sp_r, P, I, D)) + -- clamp at range -> setpoint clamped (sp_c) + local sp_c = math.max(0, math.min(sp_r, self.max_burn_combined)) - _allocate_burn_rate(sp_r, false) - end - elseif self.mode == PROCESS.BURN_RATE then - -- a total aggregate burn rate - if state_changed then - -- nothing special to do - elseif self.waiting_on_ramp and _all_units_ramped() then - self.waiting_on_ramp = false - self.time_start = now - end + self.saturated = sp_r ~= sp_c - if not self.waiting_on_ramp then - _allocate_burn_rate(self.burn_target, self.initial_ramp) + log.debug(util.sprintf("PROC_RATE[%f] { RATE[%f] ERR[%f] INT[%f] => SP[%f] SP_C[%f] <= P[%f] I[%f] D[%f] }", + runtime, avg_inflow, error, integral, setpoint, sp_c, P, I, D)) + + _allocate_burn_rate(sp_c, false) end end @@ -387,6 +399,83 @@ function facility.new(num_reactors, cooling_conf) end end + -- COMMANDS -- + + -- SCRAM all reactor units + function public.scram_all() + for i = 1, #self.units do + local u = self.units[i] ---@type reactor_unit + u.scram() + end + end + + -- stop auto control + 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 + + -- load up current limits + local limits = {} + for i = 1, num_reactors do + local u = self.units[i] ---@type reactor_unit + limits[i] = u.get_control_inf().lim_br10 * 10 + end + + -- only allow changes if not running + if self.mode == PROCESS.INACTIVE then + if (type(config.mode) == "number") and (config.mode > PROCESS.INACTIVE) and (config.mode <= PROCESS.SIMPLE) then + self.mode_set = config.mode + end + + if (type(config.burn_target) == "number") and config.burn_target >= 0.1 then + self.burn_target = config.burn_target + log.debug("SET BURN TARGET " .. config.burn_target) + end + + if (type(config.charge_target) == "number") and config.charge_target >= 0 then + self.charge_target = config.charge_target + log.debug("SET CHARGE TARGET " .. config.charge_target) + end + + if (type(config.gen_target) == "number") and config.gen_target >= 0 then + self.gen_rate_target = config.gen_target + log.debug("SET RATE TARGET " .. config.gen_target) + end + + if (type(config.limits) == "table") and (#config.limits == num_reactors) then + for i = 1, num_reactors do + local limit = config.limits[i] + + if (type(limit) == "number") and (limit >= 0.1) then + limits[i] = limit + self.units[i].set_burn_limit(limit) + log.debug("SET UNIT " .. i .. " LIMIT " .. limit) + end + end + end + + ready = self.mode_set > 0 + + if (self.mode_set == PROCESS.CHARGE) and (self.charge_target <= 0) then + ready = false + elseif (self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_target <= 0) then + ready = false + elseif (self.mode_set == PROCESS.BURN_RATE) and (self.burn_target <= 0.1) then + ready = false + end + + if ready then self.mode = self.mode_set end + end + + return { ready, self.mode_set, self.burn_target, self.charge_target, self.gen_rate_target, limits } + end + -- SETTINGS -- -- set the automatic control group of a unit @@ -424,10 +513,27 @@ function facility.new(num_reactors, cooling_conf) return build end + -- get automatic process control status + function public.get_control_status() + return { + self.mode, + self.waiting_on_ramp, + self.ascram, + self.ascram_reason + } + end + -- get RTU statuses function public.get_rtu_statuses() local status = {} + -- power averages from induction matricies + status.power = { + self.avg_charge, + self.avg_inflow, + self.avg_outflow + } + -- status of induction matricies (including tanks) status.induction = {} for i = 1, #self.induction do diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 9027e5f..ecd6cc6 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -10,6 +10,7 @@ local plc = {} local PROTOCOLS = comms.PROTOCOLS local RPLC_TYPES = comms.RPLC_TYPES local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES +local PLC_AUTO_ACK = comms.PLC_AUTO_ACK local UNIT_COMMANDS = comms.UNIT_COMMANDS @@ -19,8 +20,9 @@ local print_ts = util.print_ts local println_ts = util.println_ts -- retry time constants in ms -local INITIAL_WAIT = 1500 -local RETRY_PERIOD = 1000 +local INITIAL_WAIT = 1500 +local INITIAL_AUTO_WAIT = 1000 +local RETRY_PERIOD = 1000 local PLC_S_CMDS = { SCRAM = 1, @@ -440,6 +442,21 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) cmd = UNIT_COMMANDS.RESET_RPS, ack = ack }) + elseif pkt.type == RPLC_TYPES.AUTO_BURN_RATE then + if pkt.length == 1 then + local ack = pkt.data[1] + + self.acks.burn_rate = ack ~= PLC_AUTO_ACK.FAIL + + if ack == PLC_AUTO_ACK.FAIL then + elseif ack == PLC_AUTO_ACK.DIRECT_SET_OK then + elseif ack == PLC_AUTO_ACK.RAMP_SET_OK then + elseif ack == PLC_AUTO_ACK.ZERO_DIS_OK then + elseif ack == PLC_AUTO_ACK.ZERO_DIS_WAIT then + end + else + log.debug(log_header .. "RPLC automatic burn rate ack packet length mismatch") + end else log.debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type) end @@ -517,6 +534,11 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) ---@param engage boolean true to engage the lockout function public.auto_lock(engage) self.auto_lock = engage + + -- stop retrying a burn rate command + if engage then + self.acks.burn_rate = true + end end -- set the burn rate on behalf of automatic control @@ -583,6 +605,8 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) self.acks.rps_reset = false self.retry_times.rps_reset_req = util.time() + INITIAL_WAIT _send(RPLC_TYPES.RPS_RESET, {}) + else + log.warning(log_header .. "unsupported command received in in_queue (this is a bug)") end elseif message.qtype == mqueue.TYPE.DATA then -- instruction with body @@ -613,13 +637,20 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) end elseif cmd.key == PLC_S_DATA.AUTO_BURN_RATE then -- set automatic burn rate - cmd.val = math.floor(cmd.val * 10) / 10 -- round to 10ths place - if cmd.val > 0 and cmd.val <= self.sDB.mek_struct.max_burn then - self.commanded_burn_rate = cmd.val - self.acks.burn_rate = false - self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT - _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) + if self.auto_lock then + cmd.val = math.floor(cmd.val * 10) / 10 -- round to 10ths place + if cmd.val > 0 and cmd.val <= self.sDB.mek_struct.max_burn then + self.commanded_burn_rate = cmd.val + + -- this is only for manual control, only retry auto ramps + self.acks.burn_rate = not self.ramping_rate + self.retry_times.burn_rate_req = util.time() + INITIAL_AUTO_WAIT + + _send(RPLC_TYPES.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) + end end + else + log.warning(log_header .. "unsupported data command received in in_queue (this is a bug)") end end end @@ -685,7 +716,12 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) if not self.acks.burn_rate then if rtimes.burn_rate_req - util.time() <= 0 then - _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) + if self.auto_lock then + _send(RPLC_TYPES.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) + else + _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) + end + rtimes.burn_rate_req = util.time() + RETRY_PERIOD end end diff --git a/supervisor/session/unit.lua b/supervisor/session/unit.lua index 7b5e08d..e084fed 100644 --- a/supervisor/session/unit.lua +++ b/supervisor/session/unit.lua @@ -450,6 +450,13 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- OPERATIONS -- + -- queue a command to SCRAM the reactor + function public.scram() + if self.plc_s ~= nil then + self.plc_s.in_queue.push_command(PLC_S_CMDS.SCRAM) + end + end + -- acknowledge all alarms (if possible) function public.ack_all() for i = 1, #self.db.alarm_states do diff --git a/supervisor/startup.lua b/supervisor/startup.lua index f4f28da..b284a35 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -14,7 +14,7 @@ local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "beta-v0.9.6" +local SUPERVISOR_VERSION = "beta-v0.9.7" local print = util.print local println = util.println