From a083f8983b959a3fcb6d080c99d1d1058ad2706b Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 23 Oct 2025 23:13:13 +0000 Subject: [PATCH] #640 reworked PLC initialization --- reactor-plc/databus.lua | 1 - reactor-plc/panel/front_panel.lua | 4 +- reactor-plc/plc.lua | 221 ++++++++++++++++-------------- reactor-plc/startup.lua | 157 ++++++++++----------- reactor-plc/threads.lua | 174 ++++++++++------------- scada-common/network.lua | 45 ++++-- scada-common/util.lua | 2 +- 7 files changed, 300 insertions(+), 304 deletions(-) diff --git a/reactor-plc/databus.lua b/reactor-plc/databus.lua index b2bd1c2..65ecae9 100644 --- a/reactor-plc/databus.lua +++ b/reactor-plc/databus.lua @@ -53,7 +53,6 @@ function databus.tx_hw_status(plc_state) databus.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2))) databus.ps.publish("has_modem", not plc_state.no_modem) databus.ps.publish("degraded", plc_state.degraded) - databus.ps.publish("init_ok", plc_state.init_ok) end -- transmit thread (routine) statuses diff --git a/reactor-plc/panel/front_panel.lua b/reactor-plc/panel/front_panel.lua index 7a346df..fa05f3c 100644 --- a/reactor-plc/panel/front_panel.lua +++ b/reactor-plc/panel/front_panel.lua @@ -51,11 +51,11 @@ local function init(panel) local system = Div{parent=panel,width=14,height=18,x=2,y=3} - local init_ok = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)} + local degraded = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)} local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn} system.line_break() - init_ok.register(databus.ps, "init_ok", init_ok.update) + degraded.register(databus.ps, "degraded", degraded.update) heartbeat.register(databus.ps, "heartbeat", heartbeat.update) local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green} diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index a2929cb..7da563f 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -707,6 +707,112 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog) reactor.__p_enable_afc() end + -- handle a burn rate command + ---@param packet rplc_frame + ---@param setpoints plc_setpoints + --- EVENT_CONSUMER: this function consumes events + local function _handle_burn_rate(packet, setpoints) + 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] + + -- if no known max burn rate, check again + if self.max_burn_rate == nil then + self.max_burn_rate = 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 and burn_rate <= self.max_burn_rate then + if ramp then + setpoints.burn_rate_en = true + setpoints.burn_rate = burn_rate + success = true + else + reactor.setBurnRate(burn_rate) + success = reactor.__p_is_ok() + end + else + log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate) + end + end + + _send_ack(packet.type, success) + else + log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate") + end + end + + -- handle an auto burn rate command + ---@param packet rplc_frame + ---@param setpoints plc_setpoints + --- EVENT_CONSUMER: this function consumes events + local function _handle_auto_burn_rate(packet, setpoints) + if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then + local ack = AUTO_ACK.FAIL + local burn_rate = math.floor(packet.data[1] * 100) / 100 + local ramp = packet.data[2] + self.auto_ack_token = packet.data[3] + + -- if no known max burn rate, check again + if self.max_burn_rate == nil then + self.max_burn_rate = 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.01 then + if rps.is_active() then + -- auto scram to disable + log.debug("AUTO: stopping the reactor to meet 0.0 burn rate") + if rps.scram() then + ack = AUTO_ACK.ZERO_DIS_OK + else + log.warning("AUTO: automatic reactor stop failed") + 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 + log.debug("AUTO: activating the reactor") + + reactor.setBurnRate(0.01) + if reactor.__p_is_faulted() then + log.warning("AUTO: failed to reset burn rate for auto activation") + else + if not rps.auto_activate() then + log.warning("AUTO: automatic reactor activation failed") + end + end + end + + -- if active, set/ramp burn rate + if rps.is_active() then + if ramp then + log.debug(util.c("AUTO: setting burn rate ramp to ", burn_rate)) + setpoints.burn_rate_en = true + setpoints.burn_rate = burn_rate + ack = AUTO_ACK.RAMP_SET_OK + else + log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate)) + reactor.setBurnRate(burn_rate) + ack = util.trinary(reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK) + end + end + else + log.debug(util.c(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 + end + -- PUBLIC FUNCTIONS -- ---@class plc_comms @@ -748,8 +854,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog) ---@param formed boolean reactor formed (from PLC state) function public.send_status(no_reactor, formed) if self.linked then - local mek_data = nil ---@type table - local heating_rate = 0.0 ---@type number + local mek_data = nil ---@type table + local heating_rate = 0.0 ---@type number if (not no_reactor) and rps.is_formed() then if _update_status_cache() then mek_data = self.status_cache end @@ -803,15 +909,11 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog) -- get as RPLC packet if s_pkt.protocol() == PROTOCOL.RPLC then local rplc_pkt = comms.rplc_packet() - if rplc_pkt.decode(s_pkt) then - pkt = rplc_pkt.get() - end + if rplc_pkt.decode(s_pkt) then pkt = rplc_pkt.get() end -- get as SCADA management packet elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then local mgmt_pkt = comms.mgmt_packet() - if mgmt_pkt.decode(s_pkt) then - pkt = mgmt_pkt.get() - end + if mgmt_pkt.decode(s_pkt) then pkt = mgmt_pkt.get() end else log.debug("unsupported packet type " .. s_pkt.protocol(), true) end @@ -823,16 +925,13 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog) -- handle RPLC and MGMT packets ---@param packet rplc_frame|mgmt_frame packet frame ---@param plc_state plc_state PLC state - ---@param setpoints setpoints setpoint control table - function public.handle_packet(packet, plc_state, setpoints) - -- print a log message to the terminal as long as the UI isn't running - local function println_ts(message) if not plc_state.fp_ok then util.println_ts(message) end end - + ---@param setpoints plc_setpoints setpoint control table + ---@param println_ts function console print, when UI isn't running + function public.handle_packet(packet, plc_state, setpoints, println_ts) local protocol = packet.scada_frame.protocol() local l_chan = packet.scada_frame.local_channel() local src_addr = packet.scada_frame.src_addr() - -- handle packets now that we have prints setup if l_chan == config.PLC_Channel then -- check sequence number if self.r_seq_num == nil then @@ -867,36 +966,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog) log.debug("sent out structure again, did supervisor miss it?") elseif packet.type == RPLC_TYPE.MEK_BURN_RATE then -- set the burn rate - 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] - - -- if no known max burn rate, check again - if self.max_burn_rate == nil then - self.max_burn_rate = 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 and burn_rate <= self.max_burn_rate then - if ramp then - setpoints.burn_rate_en = true - setpoints.burn_rate = burn_rate - success = true - else - reactor.setBurnRate(burn_rate) - success = reactor.__p_is_ok() - end - else - log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate) - end - end - - _send_ack(packet.type, success) - else - log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate") - end + _handle_burn_rate(packet, setpoints) elseif packet.type == RPLC_TYPE.RPS_ENABLE then -- enable the reactor self.scrammed = false @@ -925,68 +995,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog) _send_ack(packet.type, true) elseif packet.type == RPLC_TYPE.AUTO_BURN_RATE then -- automatic control requested a new burn rate - if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then - local ack = AUTO_ACK.FAIL - local burn_rate = math.floor(packet.data[1] * 100) / 100 - local ramp = packet.data[2] - self.auto_ack_token = packet.data[3] - - -- if no known max burn rate, check again - if self.max_burn_rate == nil then - self.max_burn_rate = 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.01 then - if rps.is_active() then - -- auto scram to disable - log.debug("AUTO: stopping the reactor to meet 0.0 burn rate") - if rps.scram() then - ack = AUTO_ACK.ZERO_DIS_OK - else - log.warning("AUTO: automatic reactor stop failed") - 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 - log.debug("AUTO: activating the reactor") - - reactor.setBurnRate(0.01) - if reactor.__p_is_faulted() then - log.warning("AUTO: failed to reset burn rate for auto activation") - else - if not rps.auto_activate() then - log.warning("AUTO: automatic reactor activation failed") - end - end - end - - -- if active, set/ramp burn rate - if rps.is_active() then - if ramp then - log.debug(util.c("AUTO: setting burn rate ramp to ", burn_rate)) - setpoints.burn_rate_en = true - setpoints.burn_rate = burn_rate - ack = AUTO_ACK.RAMP_SET_OK - else - log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate)) - reactor.setBurnRate(burn_rate) - ack = util.trinary(reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK) - end - end - else - log.debug(util.c(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 + _handle_auto_burn_rate(packet, setpoints) else log.debug("received unknown RPLC packet type " .. packet.type) end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index ddf289a..fcbacd9 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc") local renderer = require("reactor-plc.renderer") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "v1.8.22" +local R_PLC_VERSION = "v1.9.0" local println = util.println local println_ts = util.println_ts @@ -87,7 +87,6 @@ local function main() -- PLC system state flags ---@class plc_state plc_state = { - init_ok = true, fp_ok = false, shutdown = false, degraded = true, @@ -97,19 +96,22 @@ local function main() }, -- control setpoints - ---@class setpoints + ---@class plc_setpoints setpoints = { burn_rate_en = false, burn_rate = 0.0 }, -- core PLC devices + ---@class plc_dev plc_dev = { - reactor = ppm.get_fission_reactor(), +---@diagnostic disable-next-line: assign-type-mismatch + reactor = ppm.get_fission_reactor(), ---@type table modem = ppm.get_wireless_modem() }, -- system objects + ---@class plc_sys plc_sys = { rps = nil, ---@type rps nic = nil, ---@type nic @@ -136,14 +138,20 @@ local function main() -- we need a reactor, can at least do some things even if it isn't formed though if plc_state.no_reactor then - println("init> fission reactor not found") - log.warning("init> no reactor on startup") + println("startup> fission reactor not found") + log.warning("startup> no reactor on startup") - plc_state.init_ok = false plc_state.degraded = true + plc_state.reactor_formed = false + + -- mount a virtual peripheral to init the RPS with + local _, dev = ppm.mount_virtual() + smem_dev.reactor = dev + + log.info("startup> mounted virtual device as reactor") elseif not smem_dev.reactor.isFormed() then - println("init> fission reactor is not formed") - log.warning("init> reactor logic adapter present, but reactor is not formed") + println("startup> fission reactor is not formed") + log.warning("startup> reactor logic adapter present, but reactor is not formed") plc_state.degraded = true plc_state.reactor_formed = false @@ -151,89 +159,74 @@ local function main() -- modem is required if networked if __shared_memory.networked and plc_state.no_modem then - println("init> wireless modem not found") - log.warning("init> no wireless modem on startup") + println("startup> wireless modem not found") + log.warning("startup> no wireless modem on startup") -- scram reactor if present and enabled if (smem_dev.reactor ~= nil) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then smem_dev.reactor.scram() end - plc_state.init_ok = false plc_state.degraded = true end - -- print a log message to the terminal as long as the UI isn't running - local function _println_no_fp(message) if not plc_state.fp_ok then println(message) end end - - -- PLC init
- --- EVENT_CONSUMER: this function consumes events - local function init() - -- scram on boot if networked, otherwise leave the reactor be - if __shared_memory.networked and (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then - smem_dev.reactor.scram() - end - - -- setup front panel - if not renderer.ui_ready() then - local message - plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode) - - -- ...or not - if not plc_state.fp_ok then - println_ts(util.c("UI error: ", message)) - println("init> running without front panel") - log.error(util.c("front panel GUI render failed with error ", message)) - log.info("init> running in headless mode without front panel") - end - end - - if plc_state.init_ok then - -- init reactor protection system - smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed) - log.debug("init> rps init") - - if __shared_memory.networked then - -- comms watchdog - smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout) - log.debug("init> conn watchdog started") - - -- create network interface then setup comms - smem_sys.nic = network.nic(smem_dev.modem) - smem_sys.plc_comms = plc.comms(R_PLC_VERSION, smem_sys.nic, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog) - log.debug("init> comms init") - else - _println_no_fp("init> starting in offline mode") - log.info("init> running without networking") - end - - -- notify user of emergency coolant configuration status - if config.EmerCoolEnable then - println("init> emergency coolant control ready") - log.info("init> running with emergency coolant control available") - end - - util.push_event("clock_start") - - _println_no_fp("init> completed") - log.info("init> startup completed") - else - _println_no_fp("init> system in degraded state, awaiting devices...") - log.warning("init> started in a degraded state, awaiting peripheral connections...") - end - - databus.tx_hw_status(plc_state) + -- scram on boot if networked, otherwise leave the reactor be + if __shared_memory.networked and (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then + log.debug("startup> power-on SCRAM") + smem_dev.reactor.scram() end + -- setup front panel + local message + plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode) + + -- ...or not + if not plc_state.fp_ok then + println_ts(util.c("UI error: ", message)) + println("startup> running without front panel") + log.error(util.c("front panel GUI render failed with error ", message)) + log.info("startup> running in headless mode without front panel") + end + + -- print a log message to the terminal as long as the UI isn't running + local function _println_no_fp(msg) if not plc_state.fp_ok then println(msg) end end + ---------------------------------------- - -- start system + -- initialize PLC ---------------------------------------- - -- initialize PLC - init() + -- init reactor protection system + smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed) + log.debug("startup> rps init") + + -- notify user of emergency coolant configuration status + if config.EmerCoolEnable then + _println_no_fp("startup> emergency coolant control ready") + log.info("startup> emergency coolant control available") + end + + -- conditionally init comms + if __shared_memory.networked then + -- comms watchdog + smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout) + log.debug("startup> conn watchdog started") + + -- create network interface then setup comms + smem_sys.nic = network.nic(smem_dev.modem) + smem_sys.plc_comms = plc.comms(R_PLC_VERSION, smem_sys.nic, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog) + log.debug("startup> comms init") + else + _println_no_fp("startup> starting in non-networked mode") + log.info("startup> starting without networking") + end + + databus.tx_hw_status(plc_state) + + _println_no_fp("startup> completed") + log.info("startup> completed") -- init threads - local main_thread = threads.thread__main(__shared_memory, init) + local main_thread = threads.thread__main(__shared_memory) local rps_thread = threads.thread__rps(__shared_memory) if __shared_memory.networked then @@ -247,14 +240,12 @@ local function main() -- run threads parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec, comms_thread_tx.p_exec, comms_thread_rx.p_exec, sp_ctrl_thread.p_exec) - if plc_state.init_ok then - -- send status one last time after RPS shutdown - smem_sys.plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed) - smem_sys.plc_comms.send_rps_status() + -- send status one last time after RPS shutdown + smem_sys.plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed) + smem_sys.plc_comms.send_rps_status() - -- close connection - smem_sys.plc_comms.close() - end + -- close connection + smem_sys.plc_comms.close() else -- run threads, excluding comms parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec) diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index b56ccc7..5703dc7 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -31,8 +31,7 @@ local MQ__COMM_CMD = { -- main thread ---@nodiscard ---@param smem plc_shared_memory ----@param init function -function threads.thread__main(smem, init) +function threads.thread__main(smem) -- print a log message to the terminal as long as the UI isn't running local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end @@ -42,7 +41,7 @@ function threads.thread__main(smem, init) -- execute thread function public.exec() databus.tx_rt_status("main", true) - log.debug("main thread init, clock inactive") + log.debug("main thread start") -- send status updates at 2Hz (every 10 server ticks) (every loop tick) -- send link requests at 0.5Hz (every 40 server ticks) (every 8 loop ticks) @@ -55,6 +54,9 @@ function threads.thread__main(smem, init) local plc_state = smem.plc_state local plc_dev = smem.plc_dev + -- start clock + loop_clock.start() + -- event loop while true do -- get plc_sys fields (may have been set late due to degraded boot) @@ -67,7 +69,6 @@ function threads.thread__main(smem, init) -- handle event if event == "timer" and loop_clock.is_clock(param1) then - -- note: loop clock is only running if init_ok = true -- blink heartbeat indicator databus.heartbeat() @@ -118,14 +119,14 @@ function threads.thread__main(smem, init) -- update indicators databus.tx_hw_status(plc_state) - elseif event == "modem_message" and networked and plc_state.init_ok and nic.is_connected() then + elseif event == "modem_message" and networked and nic.is_connected() then -- got a packet local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5) if packet ~= nil then -- pass the packet onto the comms message queue smem.q.mq_comms_rx.push_packet(packet) end - elseif event == "timer" and networked and plc_state.init_ok and conn_watchdog.is_timer(param1) then + elseif event == "timer" and networked and conn_watchdog.is_timer(param1) then -- haven't heard from server recently? close connection and shutdown reactor plc_comms.close() smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT) @@ -146,8 +147,7 @@ function threads.thread__main(smem, init) elseif networked and type == "modem" then ---@cast device Modem -- we only care if this is our wireless modem - -- note, check init_ok first since nic will be nil if it is false - if plc_state.init_ok and nic.is_modem(device) then + if nic.is_modem(device) then nic.disconnect() println_ts("comms modem disconnected!") @@ -161,10 +161,8 @@ function threads.thread__main(smem, init) plc_state.no_modem = true plc_state.degraded = true - if plc_state.init_ok then - -- try to scram reactor if it is still connected - smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM) - end + -- try to scram reactor if it is still connected + smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM) end else log.warning("a modem was disconnected") @@ -196,29 +194,27 @@ function threads.thread__main(smem, init) plc_state.degraded = false end - if plc_state.init_ok then - smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM) + smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM) - rps.reconnect_reactor(plc_dev.reactor) - if networked then - plc_comms.reconnect_reactor(plc_dev.reactor) - end - - -- partial reset of RPS, specific to becoming formed/reconnected - -- without this, auto control can't resume on chunk load - rps.reset_formed() + rps.reconnect_reactor(plc_dev.reactor) + if networked then + plc_comms.reconnect_reactor(plc_dev.reactor) end + + -- partial reset of RPS, specific to becoming formed/reconnected + -- without this, auto control can't resume on chunk load + rps.reset_formed() elseif networked and type == "modem" then ---@cast device Modem -- note, check init_ok first since nic will be nil if it is false - if device.isWireless() and not (plc_state.init_ok and nic.is_connected()) then + if device.isWireless() and not nic.is_connected() then -- reconnected modem plc_dev.modem = device plc_state.no_modem = false - if plc_state.init_ok then nic.connect(device) end + nic.connect(device) - println_ts("wireless modem reconnected.") + println_ts("comms modem reconnected") log.info("comms modem reconnected") -- determine if we are still in a degraded state @@ -226,29 +222,19 @@ function threads.thread__main(smem, init) plc_state.degraded = false end elseif device.isWireless() then - log.info("unused wireless modem reconnected") + log.info("unused wireless modem connected") else - log.info("wired modem reconnected") + log.info("wired modem connected") end end end - -- if not init'd and no longer degraded, proceed to init - if not plc_state.init_ok and not plc_state.degraded then - plc_state.init_ok = true - init() - end - -- update indicators databus.tx_hw_status(plc_state) elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then -- handle a mouse event renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) - elseif event == "clock_start" then - -- start loop clock - loop_clock.start() - log.debug("main thread clock started") end -- check for termination request @@ -278,7 +264,6 @@ function threads.thread__main(smem, init) -- this thread cannot be slept because it will miss events (namely "terminate" otherwise) if not plc_state.shutdown then log.info("main thread restarting now...") - util.push_event("clock_start") end end end @@ -316,47 +301,39 @@ function threads.thread__rps(smem) -- get plc_sys fields (may have been set late due to degraded boot) local rps = smem.plc_sys.rps local plc_comms = smem.plc_sys.plc_comms - -- get reactor, may have changed do to disconnect/reconnect + -- get reactor, it may have changed do to disconnect/reconnect local reactor = plc_dev.reactor - -- RPS checks - if plc_state.init_ok then - -- SCRAM if no open connection - if networked and not plc_comms.is_linked() then - if was_linked then - was_linked = false - rps.trip_timeout() - end - else - was_linked = true + -- SCRAM if no open connection + if networked and not plc_comms.is_linked() then + if was_linked then + was_linked = false + rps.trip_timeout() end + else was_linked = true end - if (not plc_state.no_reactor) and rps.is_formed() then - -- check reactor status ----@diagnostic disable-next-line: need-check-nil - local reactor_status = reactor.getStatus() - databus.tx_reactor_state(reactor_status) + -- check reactor status + if (not plc_state.no_reactor) and rps.is_formed() then + local reactor_status = reactor.getStatus() + databus.tx_reactor_state(reactor_status) - -- if we tried to SCRAM but failed, keep trying - -- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check) - if rps.is_tripped() and reactor_status then - rps.scram() - end - end + -- if we tried to SCRAM but failed, keep trying + -- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check) + if rps.is_tripped() and reactor_status then rps.scram() end + end - -- if we are in standalone mode and the front panel isn't working, continuously reset RPS - -- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable - if not (networked or smem.plc_state.fp_ok) then rps.reset(true) end + -- if we are in standalone mode and the front panel isn't working, continuously reset RPS + -- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable + if not (networked or smem.plc_state.fp_ok) then rps.reset(true) end - -- check safety (SCRAM occurs if tripped) - if not plc_state.no_reactor then - local rps_tripped, rps_status_string, rps_first = rps.check() + -- check safety (SCRAM occurs if tripped) + if not plc_state.no_reactor then + local rps_tripped, rps_status_string, rps_first = rps.check() - if rps_tripped and rps_first then - println_ts("[RPS] SCRAM! safety trip: " .. rps_status_string) - if networked and not plc_state.no_modem then - plc_comms.send_rps_alarm(rps_status_string) - end + if rps_tripped and rps_first then + println_ts("[RPS] SCRAM! safety trip: " .. rps_status_string) + if networked and not plc_state.no_modem then + plc_comms.send_rps_alarm(rps_status_string) end end end @@ -368,19 +345,17 @@ function threads.thread__rps(smem) if msg ~= nil then if msg.qtype == mqueue.TYPE.COMMAND then -- received a command - if plc_state.init_ok then - if msg.message == MQ__RPS_CMD.SCRAM then - -- SCRAM - rps.scram() - elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then - -- lost peripheral(s) - rps.trip_fault() - elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then - -- watchdog tripped - rps.trip_timeout() - println_ts("server timeout") - log.warning("server timeout") - end + if msg.message == MQ__RPS_CMD.SCRAM then + -- SCRAM + rps.scram() + elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then + -- lost peripheral(s) + rps.trip_fault() + elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then + -- watchdog tripped + rps.trip_timeout() + println_ts("supervisor timeout") + log.warning("supervisor timeout") end elseif msg.qtype == mqueue.TYPE.DATA then -- received data @@ -397,15 +372,15 @@ function threads.thread__rps(smem) if plc_state.shutdown then -- safe exit log.info("rps thread shutdown initiated") - if plc_state.init_ok then - if rps.scram() then - println_ts("reactor disabled") - log.info("rps thread reactor SCRAM OK") - else - println_ts("exiting, reactor failed to disable") - log.error("rps thread failed to SCRAM reactor on exit") - end + + if rps.scram() then + println_ts("exiting, reactor disabled") + log.info("rps thread reactor SCRAM OK") + else + println_ts("exiting, reactor failed to disable") + log.error("rps thread failed to SCRAM reactor on exit") end + log.info("rps thread exiting") break end @@ -428,7 +403,7 @@ function threads.thread__rps(smem) databus.tx_rt_status("rps", false) if not plc_state.shutdown then - if plc_state.init_ok then smem.plc_sys.rps.scram() end + smem.plc_sys.rps.scram() log.info("rps thread restarting in 5 seconds...") util.psleep(5) end @@ -465,7 +440,7 @@ function threads.thread__comms_tx(smem) while comms_queue.ready() and not plc_state.shutdown do local msg = comms_queue.pop() - if msg ~= nil and plc_state.init_ok then + if msg ~= nil then if msg.qtype == mqueue.TYPE.COMMAND then -- received a command if msg.message == MQ__COMM_CMD.SEND_STATUS then @@ -521,6 +496,9 @@ end ---@nodiscard ---@param smem plc_shared_memory function threads.thread__comms_rx(smem) + -- print a log message to the terminal as long as the UI isn't running + local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end + ---@class parallel_thread local public = {} @@ -546,7 +524,7 @@ function threads.thread__comms_rx(smem) while comms_queue.ready() and not plc_state.shutdown do local msg = comms_queue.pop() - if msg ~= nil and plc_state.init_ok then + if msg ~= nil then if msg.qtype == mqueue.TYPE.COMMAND then -- received a command elseif msg.qtype == mqueue.TYPE.DATA then @@ -555,7 +533,7 @@ function threads.thread__comms_rx(smem) -- received a packet -- handle the packet (setpoints passed to update burn rate setpoint) -- (plc_state passed to check if degraded) - plc_comms.handle_packet(msg.message, plc_state, setpoints) + plc_comms.handle_packet(msg.message, plc_state, setpoints, println_ts) end end @@ -629,9 +607,7 @@ function threads.thread__setpoint_control(smem) -- get reactor, may have changed do to disconnect/reconnect local reactor = plc_dev.reactor - if plc_state.init_ok and (not plc_state.no_reactor) then - ---@cast reactor table won't be nil - + if not plc_state.no_reactor then -- check if we should start ramping if setpoints.burn_rate_en and (setpoints.burn_rate ~= last_burn_sp) then local cur_burn_rate = reactor.getBurnRate() diff --git a/scada-common/network.lua b/scada-common/network.lua index 7eccff7..f7213bd 100644 --- a/scada-common/network.lua +++ b/scada-common/network.lua @@ -4,6 +4,7 @@ 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 md5 = require("lockbox.digest.md5") @@ -76,17 +77,29 @@ local function compute_hmac(message) end -- NIC: Network Interface Controller
--- utilizes HMAC-MD5 for message authentication, if enabled ----@param modem Modem modem to use +-- utilizes HMAC-MD5 for message authentication, if enabled and this is wireless +---@param modem Modem|nil modem to use function network.nic(modem) local self = { - connected = true, -- used to avoid costly MAC calculations if modem isn't even present + -- modem interface name + iface = "?", + -- phy name + name = "?", + -- used to quickly return out of tx/rx functions if there is nothing to do + connected = false, + -- used to avoid costly MAC calculations if not required + use_hash = false, + -- open channels channels = {} } ---@class nic:Modem local public = {} + -- get the phy name + ---@nodiscard + function public.phy_name() return self.name end + -- check if this NIC has a connected modem ---@nodiscard function public.is_connected() return self.connected end @@ -95,9 +108,14 @@ function network.nic(modem) ---@param reconnected_modem Modem function public.connect(reconnected_modem) modem = reconnected_modem - self.connected = true - -- open previously opened channels + self.iface = ppm.get_iface(modem) + self.name = util.c(util.trinary(modem.isWireless(), "WLAN_PHY", "ETH_PHY"), "{", self.iface, "}") + self.connected = true + self.use_hash = c_eng.hmac and modem.isWireless() + + -- open only previously opened channels + modem.closeAll() for _, channel in ipairs(self.channels) do modem.open(channel) end @@ -117,13 +135,13 @@ function network.nic(modem) function public.is_modem(device) return device == modem end -- wrap modem functions, then create custom functions - public.connect(modem) + if modem then public.connect(modem) end -- open a channel on the modem
-- if disconnected *after* opening, previousy opened channels will be re-opened on reconnection ---@param channel integer function public.open(channel) - modem.open(channel) + if modem then modem.open(channel) end local already_open = false for i = 1, #self.channels do @@ -141,7 +159,7 @@ function network.nic(modem) -- close a channel on the modem ---@param channel integer function public.close(channel) - modem.close(channel) + if modem then modem.close(channel) end for i = 1, #self.channels do if self.channels[i] == channel then @@ -153,7 +171,7 @@ function network.nic(modem) -- close all channels on the modem function public.closeAll() - modem.closeAll() + if modem then modem.closeAll() end self.channels = {} end @@ -165,7 +183,7 @@ function network.nic(modem) if self.connected then local tx_packet = packet ---@type authd_packet|scada_packet - if c_eng.hmac ~= nil then + if self.use_hash then -- local start = util.time_ms() tx_packet = comms.authd_packet() @@ -175,7 +193,10 @@ function network.nic(modem) -- log.debug("network.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms") end +---@diagnostic disable-next-line: need-check-nil modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable()) + else + log.debug("network.transmit tx dropped, link is down") end end @@ -190,10 +211,10 @@ function network.nic(modem) function public.receive(side, sender, reply_to, message, distance) local packet = nil - if self.connected then + if self.connected and side == self.iface then local s_packet = comms.scada_packet() - if c_eng.hmac ~= nil then + if self.use_hash then -- parse packet as an authenticated SCADA packet local a_packet = comms.authd_packet() a_packet.receive(side, sender, reply_to, message, distance) diff --git a/scada-common/util.lua b/scada-common/util.lua index 031eaa0..c889037 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -24,7 +24,7 @@ local t_pack = table.pack local util = {} -- scada-common version -util.version = "1.5.4" +util.version = "1.5.5" util.TICK_TIME_S = 0.05 util.TICK_TIME_MS = 50