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..dfd9d19 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.red,colors.green)} 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 4381511..4b879d8 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -120,7 +120,7 @@ function plc.rps_init(reactor, is_formed) reactor_enabled = false, enabled_at = 0, emer_cool_active = nil, ---@type boolean - formed = is_formed, + formed = is_formed, ---@type boolean|nil force_disabled = false, tripped = false, trip_cause = "ok" ---@type rps_trip_cause @@ -366,29 +366,35 @@ function plc.rps_init(reactor, is_formed) return public.activate() end - -- check all safety conditions + -- check all safety conditions if we have a formed reactor, otherwise handle a subset of conditions ---@nodiscard + ---@param has_reactor boolean if the PLC state indicates we have a reactor ---@return boolean tripped, rps_trip_cause trip_status, boolean first_trip - function public.check() + function public.check(has_reactor) local status = RPS_TRIP_CAUSE.OK local was_tripped = self.tripped local first_trip = false - if self.formed then - -- update state - parallel.waitForAll( - _is_formed, - _is_force_disabled, - _high_damage, - _high_temp, - _low_coolant, - _excess_waste, - _excess_heated_coolant, - _insufficient_fuel - ) + if has_reactor then + if self.formed then + -- update state + parallel.waitForAll( + _is_formed, + _is_force_disabled, + _high_damage, + _high_temp, + _low_coolant, + _excess_waste, + _excess_heated_coolant, + _insufficient_fuel + ) + else + -- check to see if its now formed + _is_formed() + end else - -- check to see if its now formed - _is_formed() + self.formed = nil + self.state[CHK.SYS_FAIL] = true end -- check system states in order of severity @@ -476,6 +482,7 @@ function plc.rps_init(reactor, is_formed) ---@nodiscard function public.is_active() return self.reactor_enabled end ---@nodiscard + ---@return boolean|nil formed true if formed, false if not, nil if unknown function public.is_formed() return self.formed end ---@nodiscard function public.is_force_disabled() return self.force_disabled end @@ -497,14 +504,14 @@ function plc.rps_init(reactor, is_formed) end -- partial RPS reset that only clears fault and sys_fail - function public.reset_formed() + function public.reset_reattach() self.tripped = false self.trip_cause = RPS_TRIP_CAUSE.OK self.state[CHK.FAULT] = false self.state[CHK.SYS_FAIL] = false - log.info("RPS: partial reset on formed") + log.info("RPS: partial reset on connected or formed") end -- reset the automatic and timeout trip flags, then clear trip if that was the trip cause @@ -588,11 +595,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog) -- dynamic reactor status information, excluding heating rate ---@return table data_table, boolean faulted local function _get_reactor_status() - local fuel = nil - local waste = nil - local coolant = nil - local hcoolant = nil - + local fuel, waste, coolant, hcoolant = nil, nil, nil, nil local data_table = {} reactor.__p_disable_afc() @@ -711,6 +714,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 @@ -752,8 +861,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 @@ -807,15 +916,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 @@ -827,16 +932,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 @@ -871,36 +973,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 @@ -929,68 +1002,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 5cb8f72..bb08252 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -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,21 +96,24 @@ 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 = nil, ---@type Modem|nil modem_wired = type(config.WiredModem) == "string", - modem_iface = config.WiredModem, - modem = nil + modem_iface = config.WiredModem }, -- system objects + ---@class plc_sys plc_sys = { rps = nil, ---@type rps nic = nil, ---@type nic @@ -143,14 +145,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 @@ -158,89 +166,74 @@ local function main() -- comms modem is required if networked if __shared_memory.networked and plc_state.no_modem then - println("init> comms modem not found") - log.warning("init> no comms modem on startup") + println("startup> comms modem not found") + log.warning("startup> no comms 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, util.trinary(plc_state.no_reactor, nil, 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 @@ -254,14 +247,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 f262b4e..4216d13 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("OS: 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() @@ -93,7 +94,7 @@ function threads.thread__main(smem, init) -- reactor now formed plc_state.reactor_formed = true - println_ts("reactor is now formed.") + println_ts("reactor is now formed") log.info("reactor is now formed") -- SCRAM newly formed reactor @@ -106,10 +107,10 @@ function threads.thread__main(smem, init) -- partial reset of RPS, specific to becoming formed -- without this, auto control can't resume on chunk load - rps.reset_formed() - elseif plc_state.reactor_formed and not rps.is_formed() then + rps.reset_reattach() + elseif plc_state.reactor_formed and (rps.is_formed() == false) then -- reactor no longer formed - println_ts("reactor is no longer formed.") + println_ts("reactor is no longer formed") log.info("reactor is no longer formed") plc_state.reactor_formed = false @@ -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 comms 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("non-comms modem disconnected") @@ -184,7 +182,7 @@ function threads.thread__main(smem, init) plc_dev.reactor = device plc_state.no_reactor = false - println_ts("reactor reconnected.") + println_ts("reactor reconnected") log.info("reactor reconnected") -- we need to assume formed here as we cannot check in this main loop @@ -196,35 +194,33 @@ 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_reattach() elseif networked and type == "modem" then ---@cast device Modem local is_comms_modem = util.trinary(plc_dev.modem_wired, plc_dev.modem_iface == param1, device.isWireless()) -- note, check init_ok first since nic will be nil if it is false - if is_comms_modem and not (plc_state.init_ok and nic.is_connected()) then + if is_comms_modem 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("comms modem reconnected.") + println_ts("comms modem reconnected") log.info("comms modem reconnected") -- determine if we are still in a degraded state - if not plc_state.no_reactor then + if plc_state.reactor_formed and not plc_state.no_reactor then plc_state.degraded = false end elseif device.isWireless() then @@ -235,27 +231,17 @@ function threads.thread__main(smem, init) 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 if event == "terminate" or ppm.should_terminate() then - log.info("terminate requested, main thread exiting") + log.info("OS: terminate requested, main thread exiting") -- rps handles reactor shutdown plc_state.shutdown = true break @@ -279,8 +265,7 @@ function threads.thread__main(smem, init) -- if not, we need to restart the clock -- 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") + log.info("OS: main thread restarting now...") end end end @@ -301,7 +286,7 @@ function threads.thread__rps(smem) -- execute thread function public.exec() databus.tx_rt_status("rps", true) - log.debug("rps thread start") + log.debug("OS: rps thread start") -- load in from shared memory local networked = smem.networked @@ -318,49 +303,36 @@ 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 due to a 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() - - 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 + -- check safety (SCRAM occurs if tripped) + local rps_tripped, rps_status_string, rps_first = rps.check(not plc_state.no_reactor) + if rps_tripped and rps_first then + println_ts("RPS: SCRAM on safety trip (" .. rps_status_string .. ")") + if networked then plc_comms.send_rps_alarm(rps_status_string) end end -- check for messages in the message queue @@ -370,19 +342,19 @@ 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 + log.info("RPS: OS requested SCRAM") + rps.scram() + elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then + -- lost peripheral(s) + log.info("RPS: received PLC degraded alert") + rps.trip_fault() + elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then + -- watchdog tripped + println_ts("RPS: supervisor timeout") + log.warning("RPS: received supervisor timeout alert") + rps.trip_timeout() end elseif msg.qtype == mqueue.TYPE.DATA then -- received data @@ -398,17 +370,17 @@ function threads.thread__rps(smem) -- check for termination request 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 + log.info("OS: rps thread shutdown initiated") + + if rps.scram() then + println_ts("exiting, reactor disabled") + log.info("OS: rps thread reactor SCRAM OK on exit") + else + println_ts("exiting, reactor failed to disable") + log.error("OS: rps thread failed to SCRAM reactor on exit") end - log.info("rps thread exiting") + + log.info("OS: rps thread exiting") break end @@ -430,8 +402,8 @@ 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 - log.info("rps thread restarting in 5 seconds...") + smem.plc_sys.rps.scram() + log.info("OS: rps thread restarting in 5 seconds...") util.psleep(5) end end @@ -450,7 +422,7 @@ function threads.thread__comms_tx(smem) -- execute thread function public.exec() databus.tx_rt_status("comms_tx", true) - log.debug("comms tx thread start") + log.debug("OS: comms tx thread start") -- load in from shared memory local plc_state = smem.plc_state @@ -467,7 +439,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 @@ -488,7 +460,7 @@ function threads.thread__comms_tx(smem) -- check for termination request if plc_state.shutdown then - log.info("comms tx thread exiting") + log.info("OS: comms tx thread exiting") break end @@ -510,7 +482,7 @@ function threads.thread__comms_tx(smem) databus.tx_rt_status("comms_tx", false) if not plc_state.shutdown then - log.info("comms tx thread restarting in 5 seconds...") + log.info("OS: comms tx thread restarting in 5 seconds...") util.psleep(5) end end @@ -523,13 +495,16 @@ 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 = {} -- execute thread function public.exec() databus.tx_rt_status("comms_rx", true) - log.debug("comms rx thread start") + log.debug("OS: comms rx thread start") -- load in from shared memory local plc_state = smem.plc_state @@ -548,7 +523,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 @@ -557,7 +532,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 @@ -567,7 +542,7 @@ function threads.thread__comms_rx(smem) -- check for termination request if plc_state.shutdown then - log.info("comms rx thread exiting") + log.info("OS: comms rx thread exiting") break end @@ -589,7 +564,7 @@ function threads.thread__comms_rx(smem) databus.tx_rt_status("comms_rx", false) if not plc_state.shutdown then - log.info("comms rx thread restarting in 5 seconds...") + log.info("OS: comms rx thread restarting in 5 seconds...") util.psleep(5) end end @@ -608,7 +583,7 @@ function threads.thread__setpoint_control(smem) -- execute thread function public.exec() databus.tx_rt_status("spctl", true) - log.debug("setpoint control thread start") + log.debug("OS: setpoint control thread start") -- load in from shared memory local plc_state = smem.plc_state @@ -631,9 +606,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() @@ -700,7 +673,7 @@ function threads.thread__setpoint_control(smem) -- check for termination request if plc_state.shutdown then - log.info("setpoint control thread exiting") + log.info("OS: setpoint control thread exiting") break end @@ -722,7 +695,7 @@ function threads.thread__setpoint_control(smem) databus.tx_rt_status("spctl", false) if not plc_state.shutdown then - log.info("setpoint control thread restarting in 5 seconds...") + log.info("OS: setpoint control thread restarting in 5 seconds...") util.psleep(5) end end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 04563b8..93a0e69 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -214,7 +214,7 @@ function comms.scada_packet() if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then -- outside of maximum allowable transmission distance - -- log.debug("comms.scada_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)") + -- log.debug("COMMS: comms.scada_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)") else if type(self.raw) == "table" then if #self.raw == 5 then @@ -337,7 +337,7 @@ function comms.authd_packet() if (type(max_distance) == "number") and ((type(distance) ~= "number") or (distance > max_distance)) then -- outside of maximum allowable transmission distance - -- log.debug("comms.authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)") + -- log.debug("COMMS: comms.authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)") else if type(self.raw) == "table" then if #self.raw == 4 then @@ -423,7 +423,7 @@ function comms.modbus_packet() self.raw = { self.txn_id, self.unit_id, self.func_code } for i = 1, self.length do insert(self.raw, data[i]) end else - log.error("comms.modbus_packet.make(): data not a table") + log.error("COMMS: modbus_packet.make(): data not a table") end end @@ -446,11 +446,11 @@ function comms.modbus_packet() return size_ok and valid else - log.debug("attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true) + log.debug("COMMS: attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true) return false end else - log.debug("nil frame encountered", true) + log.debug("COMMS: nil frame encountered", true) return false end end @@ -509,7 +509,7 @@ function comms.rplc_packet() self.raw = { self.id, self.type } for i = 1, #data do insert(self.raw, data[i]) end else - log.error("comms.rplc_packet.make(): data not a table") + log.error("COMMS: rplc_packet.make(): data not a table") end end @@ -532,11 +532,11 @@ function comms.rplc_packet() return ok else - log.debug("attempted RPLC parse of incorrect protocol " .. frame.protocol(), true) + log.debug("COMMS: attempted RPLC parse of incorrect protocol " .. frame.protocol(), true) return false end else - log.debug("nil frame encountered", true) + log.debug("COMMS: nil frame encountered", true) return false end end @@ -591,7 +591,7 @@ function comms.mgmt_packet() self.raw = { self.type } for i = 1, #data do insert(self.raw, data[i]) end else - log.error("comms.mgmt_packet.make(): data not a table") + log.error("COMMS: mgmt_packet.make(): data not a table") end end @@ -612,11 +612,11 @@ function comms.mgmt_packet() return ok else - log.debug("attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true) + log.debug("COMMS: attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true) return false end else - log.debug("nil frame encountered", true) + log.debug("COMMS: nil frame encountered", true) return false end end @@ -670,7 +670,7 @@ function comms.crdn_packet() self.raw = { self.type } for i = 1, #data do insert(self.raw, data[i]) end else - log.error("comms.crdn_packet.make(): data not a table") + log.error("COMMS: crdn_packet.make(): data not a table") end end @@ -691,11 +691,11 @@ function comms.crdn_packet() return ok else - log.debug("attempted SCADA_CRDN parse of incorrect protocol " .. frame.protocol(), true) + log.debug("COMMS: attempted SCADA_CRDN parse of incorrect protocol " .. frame.protocol(), true) return false end else - log.debug("nil frame encountered", true) + log.debug("COMMS: nil frame encountered", true) return false end end diff --git a/scada-common/log.lua b/scada-common/log.lua index 8ebc987..5a839fd 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -20,7 +20,7 @@ local MODE = { APPEND = 0, NEW = 1 } log.MODE = MODE -local logger = { +local _log = { not_ready = true, path = "/log.txt", mode = MODE.APPEND, @@ -42,36 +42,36 @@ local free_space = fs.getFreeSpace ---@param err_msg string|nil error message ---@return boolean out_of_space local function check_out_of_space(err_msg) - return (free_space(logger.path) < MIN_SPACE) or ((err_msg ~= nil) and (string.find(err_msg, OUT_OF_SPACE) ~= nil)) + return (free_space(_log.path) < MIN_SPACE) or ((err_msg ~= nil) and (string.find(err_msg, OUT_OF_SPACE) ~= nil)) end -- private log write function ---@param msg_bits any[] -local function _log(msg_bits) - if logger.not_ready then return end +local function write_log(msg_bits) + if _log.not_ready then return end local time_stamp = os.date(TIME_FMT) local stamped = util.c(time_stamp, table.unpack(msg_bits)) -- attempt to write log local status, result = pcall(function () - logger.file.writeLine(stamped) - logger.file.flush() + _log.file.writeLine(stamped) + _log.file.flush() end) -- if we don't have space, we need to create a new log file if check_out_of_space() then -- delete the old log file before opening a new one - logger.file.close() - fs.delete(logger.path) + _log.file.close() + fs.delete(_log.path) -- re-init logger and pass dmesg_out so that it doesn't change - log.init(logger.path, logger.mode, logger.debug, logger.dmesg_out) + log.init(_log.path, _log.mode, _log.debug, _log.dmesg_out) -- log the message and recycle warning - logger.file.writeLine(time_stamp .. WRN_TAG .. "recycled log file") - logger.file.writeLine(stamped) - logger.file.flush() + _log.file.writeLine(time_stamp .. WRN_TAG .. "recycled log file") + _log.file.writeLine(stamped) + _log.file.flush() elseif (not status) and (result ~= nil) then util.println("unexpected error writing to the log file: " .. result) end @@ -89,45 +89,45 @@ end function log.init(path, write_mode, include_debug, dmesg_redirect) local err_msg - logger.path = path - logger.mode = write_mode - logger.debug = include_debug - logger.file, err_msg = fs.open(path, util.trinary(logger.mode == MODE.APPEND, "a", "w")) + _log.path = path + _log.mode = write_mode + _log.debug = include_debug + _log.file, err_msg = fs.open(path, util.trinary(_log.mode == MODE.APPEND, "a", "w")) if dmesg_redirect then - logger.dmesg_out = dmesg_redirect + _log.dmesg_out = dmesg_redirect else - logger.dmesg_out = term.current() + _log.dmesg_out = term.current() end -- check for space issues local out_of_space = check_out_of_space(err_msg) -- try to handle problems - if logger.file == nil or out_of_space then + if _log.file == nil or out_of_space then if out_of_space then - if fs.exists(logger.path) then - fs.delete(logger.path) + if fs.exists(_log.path) then + fs.delete(_log.path) - logger.file, err_msg = fs.open(path, util.trinary(logger.mode == MODE.APPEND, "a", "w")) + _log.file, err_msg = fs.open(path, util.trinary(_log.mode == MODE.APPEND, "a", "w")) - if logger.file then - logger.file.writeLine(os.date(TIME_FMT) .. WRN_TAG .. "init recycled log file") - logger.file.flush() + if _log.file then + _log.file.writeLine(os.date(TIME_FMT) .. WRN_TAG .. "init recycled log file") + _log.file.flush() else error("failed to setup the log file: " .. err_msg) end else error("failed to make space for the log file, please delete unused files") end else error("unexpected error setting up the log file: " .. err_msg) end end - logger.not_ready = false + _log.not_ready = false end -- close the log file handle -function log.close() logger.file.close() end +function log.close() _log.file.close() end -- direct dmesg output to a monitor/window ---@param window Window window or terminal reference -function log.direct_dmesg(window) logger.dmesg_out = window end +function log.direct_dmesg(window) _log.dmesg_out = window end -- dmesg style logging for boot because I like linux-y things ---@param msg any message @@ -142,7 +142,7 @@ function log.dmesg(msg, tag, tag_color) tag = util.strval(tag or "") local t_stamp = string.format("%12.2f", os.clock()) - local out = logger.dmesg_out + local out = _log.dmesg_out if out ~= nil then local out_w, out_h = out.getSize() @@ -180,7 +180,7 @@ function log.dmesg(msg, tag, tag_color) if cur_y == out_h then out.scroll(1) out.setCursorPos(1, cur_y) - logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1 + _log.dmesg_scroll_count = _log.dmesg_scroll_count + 1 else out.setCursorPos(1, cur_y + 1) end @@ -216,7 +216,7 @@ function log.dmesg(msg, tag, tag_color) if cur_y == out_h then out.scroll(1) out.setCursorPos(1, cur_y) - logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1 + _log.dmesg_scroll_count = _log.dmesg_scroll_count + 1 else out.setCursorPos(1, cur_y + 1) end @@ -225,9 +225,9 @@ function log.dmesg(msg, tag, tag_color) out.write(lines[i]) end - logger.dmesg_restore_coord = { out.getCursorPos() } + _log.dmesg_restore_coord = { out.getCursorPos() } - _log{"[", t_stamp, "] [", tag, "] ", msg} + write_log{"[", t_stamp, "] [", tag, "] ", msg} end return ts_coord @@ -241,9 +241,9 @@ end ---@return function update, function done function log.dmesg_working(msg, tag, tag_color) local ts_coord = log.dmesg(msg, tag, tag_color) - local initial_scroll = logger.dmesg_scroll_count + local initial_scroll = _log.dmesg_scroll_count - local out = logger.dmesg_out + local out = _log.dmesg_out local width = (ts_coord.x2 - ts_coord.x1) + 1 if out ~= nil then @@ -252,7 +252,7 @@ function log.dmesg_working(msg, tag, tag_color) local counter = 0 local function update(sec_remaining) - local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll) + local new_y = ts_coord.y - (_log.dmesg_scroll_count - initial_scroll) if new_y < 1 then return end local time = util.sprintf("%ds", sec_remaining) @@ -280,11 +280,11 @@ function log.dmesg_working(msg, tag, tag_color) counter = counter + 1 - out.setCursorPos(table.unpack(logger.dmesg_restore_coord)) + out.setCursorPos(table.unpack(_log.dmesg_restore_coord)) end local function done(ok) - local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll) + local new_y = ts_coord.y - (_log.dmesg_scroll_count - initial_scroll) if new_y < 1 then return end out.setCursorPos(ts_coord.x1, new_y) @@ -299,7 +299,7 @@ function log.dmesg_working(msg, tag, tag_color) out.setTextColor(initial_color) - out.setCursorPos(table.unpack(logger.dmesg_restore_coord)) + out.setCursorPos(table.unpack(_log.dmesg_restore_coord)) end return update, done @@ -312,28 +312,28 @@ end ---@param msg any message ---@param trace? boolean include file trace function log.debug(msg, trace) - if logger.debug then + if _log.debug then if trace then local info = debug.getinfo(2) if info.name ~= nil then - _log{DBG_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg} + write_log{DBG_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg} else - _log{DBG_TAG, info.short_src, COLON, info.currentline, ARROW, msg} + write_log{DBG_TAG, info.short_src, COLON, info.currentline, ARROW, msg} end else - _log{DBG_TAG, msg} + write_log{DBG_TAG, msg} end end end -- log info messages ---@param msg any message -function log.info(msg) _log{INF_TAG, msg} end +function log.info(msg) write_log{INF_TAG, msg} end -- log warning messages ---@param msg any message -function log.warning(msg) _log{WRN_TAG, msg} end +function log.warning(msg) write_log{WRN_TAG, msg} end -- log error messages ---@param msg any message @@ -343,17 +343,17 @@ function log.error(msg, trace) local info = debug.getinfo(2) if info.name ~= nil then - _log{ERR_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg} + write_log{ERR_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg} else - _log{ERR_TAG, info.short_src, COLON, info.currentline, ARROW, msg} + write_log{ERR_TAG, info.short_src, COLON, info.currentline, ARROW, msg} end else - _log{ERR_TAG, msg} + write_log{ERR_TAG, msg} end end -- log fatal errors ---@param msg any message -function log.fatal(msg) _log{FTL_TAG, msg} end +function log.fatal(msg) write_log{FTL_TAG, msg} end return log diff --git a/scada-common/network.lua b/scada-common/network.lua index 8066adb..317d517 100644 --- a/scada-common/network.lua +++ b/scada-common/network.lua @@ -1,5 +1,5 @@ -- --- Network Communications +-- Network Communications and Message Authentication -- local comms = require("scada-common.comms") @@ -18,7 +18,7 @@ local array = require("lockbox.util.array") local network = {} -- cryptography engine -local c_eng = { +local _crypt = { key = nil, hmac = nil } @@ -40,23 +40,23 @@ function network.init_mac(passkey) key_deriv.setPassword(passkey) key_deriv.finish() - c_eng.key = array.fromHex(key_deriv.asHex()) + _crypt.key = array.fromHex(key_deriv.asHex()) -- initialize HMAC - c_eng.hmac = hmac() - c_eng.hmac.setBlockSize(64) - c_eng.hmac.setDigest(md5) - c_eng.hmac.setKey(c_eng.key) + _crypt.hmac = hmac() + _crypt.hmac.setBlockSize(64) + _crypt.hmac.setDigest(md5) + _crypt.hmac.setKey(_crypt.key) local init_time = util.time_ms() - start - log.info("network.init_mac completed in " .. init_time .. "ms") + log.info("NET: network.init_mac completed in " .. init_time .. "ms") return init_time end -- de-initialize message authentication system function network.deinit_mac() - c_eng.key, c_eng.hmac = nil, nil + _crypt.key, _crypt.hmac = nil, nil end -- generate HMAC of message @@ -65,20 +65,20 @@ end local function compute_hmac(message) -- local start = util.time_ms() - c_eng.hmac.init() - c_eng.hmac.update(stream.fromString(message)) - c_eng.hmac.finish() + _crypt.hmac.init() + _crypt.hmac.update(stream.fromString(message)) + _crypt.hmac.finish() - local hash = c_eng.hmac.asHex() + local hash = _crypt.hmac.asHex() - -- log.debug("compute_hmac(): hmac-md5 = " .. util.strval(hash) .. " (took " .. (util.time_ms() - start) .. "ms)") + -- log.debug("NET: compute_hmac(): hmac-md5 = " .. util.strval(hash) .. " (took " .. (util.time_ms() - start) .. "ms)") return hash end -- NIC: Network Interface Controller
-- utilizes HMAC-MD5 for message authentication, if enabled and this is wireless ----@param modem Modem modem to use +---@param modem Modem|nil modem to use function network.nic(modem) local self = { -- modem interface name @@ -86,9 +86,9 @@ function network.nic(modem) -- phy name name = "?", -- used to quickly return out of tx/rx functions if there is nothing to do - connected = true, + connected = false, -- used to avoid costly MAC calculations if not required - use_hash = c_eng.hmac and modem.isWireless(), + use_hash = false, -- open channels channels = {} } @@ -112,7 +112,7 @@ function network.nic(modem) 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() + self.use_hash = _crypt.hmac and modem.isWireless() -- open only previously opened channels modem.closeAll() @@ -135,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 @@ -159,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 @@ -171,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 @@ -190,10 +190,13 @@ function network.nic(modem) ---@cast tx_packet authd_packet tx_packet.make(packet, compute_hmac) - -- log.debug("network.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms") + -- log.debug("NET: 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("NET: network.transmit tx dropped, link is down") end end @@ -224,10 +227,10 @@ function network.nic(modem) local computed_hmac = compute_hmac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true })) if a_packet.mac() == computed_hmac then - -- log.debug("network.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms") + -- log.debug("NET: network.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms") s_packet.stamp_authenticated() else - -- log.debug("network.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms") + -- log.debug("NET: network.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms") end end end diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index bf5df5e..6feed0d 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -22,7 +22,7 @@ ppm.VIRTUAL_DEVICE_TYPE = VIRTUAL_DEVICE_TYPE local REPORT_FREQUENCY = 20 -- log every 20 faults per function -local ppm_sys = { +local _ppm = { mounts = {}, ---@type { [string]: ppm_entry } next_vid = 0, auto_cf = false, @@ -66,7 +66,7 @@ local function peri_init(iface) if status then -- auto fault clear if self.auto_cf then self.faulted = false end - if ppm_sys.auto_cf then ppm_sys.faulted = false end + if _ppm.auto_cf then _ppm.faulted = false end self.fault_counts[key] = 0 @@ -78,10 +78,10 @@ local function peri_init(iface) self.faulted = true self.last_fault = result - ppm_sys.faulted = true - ppm_sys.last_fault = result + _ppm.faulted = true + _ppm.last_fault = result - if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then + if not _ppm.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then local count_str = "" if self.fault_counts[key] > 0 then count_str = " [" .. self.fault_counts[key] .. " total faults]" @@ -92,7 +92,7 @@ local function peri_init(iface) self.fault_counts[key] = self.fault_counts[key] + 1 - if result == "Terminated" then ppm_sys.terminate = true end + if result == "Terminated" then _ppm.terminate = true end return ACCESS_FAULT, result end @@ -159,10 +159,10 @@ local function peri_init(iface) self.faulted = true self.last_fault = UNDEFINED_FIELD - ppm_sys.faulted = true - ppm_sys.last_fault = UNDEFINED_FIELD + _ppm.faulted = true + _ppm.last_fault = UNDEFINED_FIELD - if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then + if not _ppm.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then local count_str = "" if self.fault_counts[key] > 0 then count_str = " [" .. self.fault_counts[key] .. " total calls]" @@ -193,35 +193,35 @@ end -- REPORTING -- -- silence error prints -function ppm.disable_reporting() ppm_sys.mute = true end +function ppm.disable_reporting() _ppm.mute = true end -- allow error prints -function ppm.enable_reporting() ppm_sys.mute = false end +function ppm.enable_reporting() _ppm.mute = false end -- FAULT MEMORY -- -- enable automatically clearing fault flag -function ppm.enable_afc() ppm_sys.auto_cf = true end +function ppm.enable_afc() _ppm.auto_cf = true end -- disable automatically clearing fault flag -function ppm.disable_afc() ppm_sys.auto_cf = false end +function ppm.disable_afc() _ppm.auto_cf = false end -- clear fault flag -function ppm.clear_fault() ppm_sys.faulted = false end +function ppm.clear_fault() _ppm.faulted = false end -- check fault flag ---@nodiscard -function ppm.is_faulted() return ppm_sys.faulted end +function ppm.is_faulted() return _ppm.faulted end -- get the last fault message ---@nodiscard -function ppm.get_last_fault() return ppm_sys.last_fault end +function ppm.get_last_fault() return _ppm.last_fault end -- TERMINATION -- -- if a caught error was a termination request ---@nodiscard -function ppm.should_terminate() return ppm_sys.terminate end +function ppm.should_terminate() return _ppm.terminate end -- MOUNTING -- @@ -229,12 +229,12 @@ function ppm.should_terminate() return ppm_sys.terminate end function ppm.mount_all() local ifaces = peripheral.getNames() - ppm_sys.mounts = {} + _ppm.mounts = {} for i = 1, #ifaces do - ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i]) + _ppm.mounts[ifaces[i]] = peri_init(ifaces[i]) - log.info(util.c("PPM: found a ", ppm_sys.mounts[ifaces[i]].type, " (", ifaces[i], ")")) + log.info(util.c("PPM: found a ", _ppm.mounts[ifaces[i]].type, " (", ifaces[i], ")")) end if #ifaces == 0 then @@ -253,10 +253,10 @@ function ppm.mount(iface) for i = 1, #ifaces do if iface == ifaces[i] then - ppm_sys.mounts[iface] = peri_init(iface) + _ppm.mounts[iface] = peri_init(iface) - pm_type = ppm_sys.mounts[iface].type - pm_dev = ppm_sys.mounts[iface].dev + pm_type = _ppm.mounts[iface].type + pm_dev = _ppm.mounts[iface].dev log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type)) break @@ -278,12 +278,12 @@ function ppm.remount(iface) for i = 1, #ifaces do if iface == ifaces[i] then log.info(util.c("PPM: remount(", iface, ") -> is a ", pm_type)) - ppm.unmount(ppm_sys.mounts[iface].dev) + ppm.unmount(_ppm.mounts[iface].dev) - ppm_sys.mounts[iface] = peri_init(iface) + _ppm.mounts[iface] = peri_init(iface) - pm_type = ppm_sys.mounts[iface].type - pm_dev = ppm_sys.mounts[iface].dev + pm_type = _ppm.mounts[iface].type + pm_dev = _ppm.mounts[iface].dev log.info(util.c("PPM: remount(", iface, ") -> remounted a ", pm_type)) break @@ -297,24 +297,24 @@ end ---@nodiscard ---@return string type, table device function ppm.mount_virtual() - local iface = "ppm_vdev_" .. ppm_sys.next_vid + local iface = "ppm_vdev_" .. _ppm.next_vid - ppm_sys.mounts[iface] = peri_init("__virtual__") - ppm_sys.next_vid = ppm_sys.next_vid + 1 + _ppm.mounts[iface] = peri_init("__virtual__") + _ppm.next_vid = _ppm.next_vid + 1 log.info(util.c("PPM: mount_virtual() -> allocated new virtual device ", iface)) - return ppm_sys.mounts[iface].type, ppm_sys.mounts[iface].dev + return _ppm.mounts[iface].type, _ppm.mounts[iface].dev end -- manually unmount a peripheral from the PPM ---@param device table device table function ppm.unmount(device) if device then - for iface, data in pairs(ppm_sys.mounts) do + for iface, data in pairs(_ppm.mounts) do if data.dev == device then log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", iface)) - ppm_sys.mounts[iface] = nil + _ppm.mounts[iface] = nil break end end @@ -330,7 +330,7 @@ function ppm.handle_unmount(iface) local pm_type = nil -- what got disconnected? - local lost_dev = ppm_sys.mounts[iface] + local lost_dev = _ppm.mounts[iface] if lost_dev then pm_type = lost_dev.type @@ -341,18 +341,18 @@ function ppm.handle_unmount(iface) log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface)) end - ppm_sys.mounts[iface] = nil + _ppm.mounts[iface] = nil return pm_type, pm_dev end -- log all mounts, to be used if `ppm.mount_all` is called before logging is ready function ppm.log_mounts() - for iface, mount in pairs(ppm_sys.mounts) do + for iface, mount in pairs(_ppm.mounts) do log.info(util.c("PPM: had found a ", mount.type, " (", iface, ")")) end - if util.table_len(ppm_sys.mounts) == 0 then + if util.table_len(_ppm.mounts) == 0 then log.warning("PPM: no devices had been found") end end @@ -369,7 +369,7 @@ function ppm.list_avail() return peripheral.getNames() end ---@return { [string]: ppm_entry } mounts function ppm.list_mounts() local list = {} - for k, v in pairs(ppm_sys.mounts) do list[k] = v end + for k, v in pairs(_ppm.mounts) do list[k] = v end return list end @@ -379,7 +379,7 @@ end ---@return string|nil iface CC peripheral interface function ppm.get_iface(device) if device then - for iface, data in pairs(ppm_sys.mounts) do + for iface, data in pairs(_ppm.mounts) do if data.dev == device then return iface end end end @@ -392,8 +392,8 @@ end ---@param iface string CC peripheral interface ---@return { [string]: function }|nil device function table function ppm.get_periph(iface) - if ppm_sys.mounts[iface] then - return ppm_sys.mounts[iface].dev + if _ppm.mounts[iface] then + return _ppm.mounts[iface].dev else return nil end end @@ -402,8 +402,8 @@ end ---@param iface string CC peripheral interface ---@return string|nil type function ppm.get_type(iface) - if ppm_sys.mounts[iface] then - return ppm_sys.mounts[iface].type + if _ppm.mounts[iface] then + return _ppm.mounts[iface].type else return nil end end @@ -414,7 +414,7 @@ end function ppm.get_all_devices(name) local devices = {} - for _, data in pairs(ppm_sys.mounts) do + for _, data in pairs(_ppm.mounts) do if data.type == name then table.insert(devices, data.dev) end @@ -430,7 +430,7 @@ end function ppm.get_device(name) local device = nil - for _, data in pairs(ppm_sys.mounts) do + for _, data in pairs(_ppm.mounts) do if data.type == name then device = data.dev break @@ -468,7 +468,7 @@ function ppm.get_wireless_modem() local w_modem, w_iface = nil, nil local emulated_env = periphemu ~= nil - for iface, device in pairs(ppm_sys.mounts) do + for iface, device in pairs(_ppm.mounts) do if device.type == "modem" and (emulated_env or device.dev.isWireless()) then w_iface = iface w_modem = device.dev @@ -498,7 +498,7 @@ end function ppm.get_monitor_list() local list = {} - for iface, device in pairs(ppm_sys.mounts) do + for iface, device in pairs(_ppm.mounts) do if device.type == "monitor" then list[iface] = device end end