diff --git a/reactor-plc/backplane.lua b/reactor-plc/backplane.lua new file mode 100644 index 0000000..07d7694 --- /dev/null +++ b/reactor-plc/backplane.lua @@ -0,0 +1,189 @@ +-- +-- Reactor PLC System Core Peripheral Backplane +-- + +local log = require("scada-common.log") +local network = require("scada-common.network") +local ppm = require("scada-common.ppm") +local util = require("scada-common.util") + +local databus = require("reactor-plc.databus") +local plc = require("reactor-plc.plc") + +local println = util.println + +---@class plc_backplane +local backplane = {} + +local _bp = { + smem = nil, ---@type plc_shared_memory + + wlan_pref = true, + lan_iface = "", + + act_nic = nil, ---@type nic + wl_act = true, + wd_nic = nil, ---@type nic|nil + wl_nic = nil ---@type nic|nil +} + +-- initialize the system peripheral backplane
+---@param config plc_config +---@param __shared_memory plc_shared_memory +--- EVENT_CONSUMER: this function consumes events +function backplane.init(config, __shared_memory) + _bp.smem = __shared_memory + _bp.wlan_pref = config.PreferWireless + _bp.lan_iface = config.WiredModem + + local plc_dev = __shared_memory.plc_dev + local plc_state = __shared_memory.plc_state + + -- Modem Init + + -- init wired NIC + if type(config.WiredModem) == "string" then + local modem = ppm.get_modem(_bp.lan_iface) + _bp.wd_nic = network.nic(modem) + + log.info("BKPLN: WIRED PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. _bp.lan_iface) + + -- set this as active for now + _bp.wl_act = false + _bp.act_nic = _bp.wd_nic + end + + -- init wireless NIC(s) + if config.WirelessModem then + local modem, iface = ppm.get_wireless_modem() + _bp.wl_nic = network.nic(modem) + + log.info("BKPLN: WIRELESS PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. iface) + + -- set this as active if connected or if both modems are disconnected and this is preferred + if (modem and _bp.wlan_pref) or not (_bp.act_nic and _bp.act_nic.is_connected()) then + _bp.wl_act = true + _bp.act_nic = _bp.wl_nic + end + end + + plc_state.no_modem = not _bp.act_nic.is_connected() + + databus.tx_hw_modem(not plc_state.no_modem) + + -- comms modem is required if networked + if __shared_memory.networked and plc_state.no_modem then + println("startup> comms modem not found") + log.warning("BKPLN: no comms modem on startup") + + plc_state.degraded = true + end + + -- Reactor Init + +---@diagnostic disable-next-line: assign-type-mismatch + plc_dev.reactor = ppm.get_fission_reactor() + plc_state.no_reactor = plc_dev.reactor == nil + + -- we need a reactor, can at least do some things even if it isn't formed though + if plc_state.no_reactor then + println("startup> fission reactor not found") + log.warning("BKPLN: no reactor on startup") + + plc_state.degraded = true + plc_state.reactor_formed = false + + -- mount a virtual peripheral to init the RPS with + local _, dev = ppm.mount_virtual() + plc_dev.reactor = dev + + log.info("BKPLN: mounted virtual device as reactor") + elseif not plc_dev.reactor.isFormed() then + println("startup> fission reactor is not formed") + log.warning("BKPLN: reactor logic adapter present, but reactor is not formed") + + plc_state.degraded = true + plc_state.reactor_formed = false + else + log.info("BKPLN: reactor detected") + end +end + +-- get the active NIC +---@return nic|nil +function backplane.active_nic() return _bp.act_nic end + +-- handle a backplane peripheral attach +---@param iface string +---@param type string +---@param device table +---@param print_no_fp function +function backplane.attach(iface, type, device, print_no_fp) + local networked = _bp.smem.networked + local state = _bp.smem.plc_state + local dev = _bp.smem.plc_dev + local sys = _bp.smem.plc_sys + + if state.no_reactor and (type == "fissionReactorLogicAdapter") then + -- reconnected reactor + dev.reactor = device + state.no_reactor = false + + print_no_fp("reactor reconnected") + log.info("BKPLN: reactor reconnected") + + -- we need to assume formed here as we cannot check in this main loop + -- RPS will identify if it isn't and this will get set false later + state.reactor_formed = true + + -- determine if we are still in a degraded state + if (not networked or not state.no_modem) and state.reactor_formed then + state.degraded = false + end + + _bp.smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM) + + sys.rps.reconnect_reactor(dev.reactor) + if networked then + sys.plc_comms.reconnect_reactor(dev.reactor) + end + + -- partial reset of RPS, specific to becoming formed/reconnected + -- without this, auto control can't resume on chunk load + sys.rps.reset_formed() + elseif networked and type == "modem" then + ---@cast device Modem + local is_comms_modem = util.trinary(dev.modem_wired, dev.modem_iface == iface, device.isWireless()) + + -- note, check init_ok first since nic will be nil if it is false + if is_comms_modem and not (state.init_ok and nic.is_connected()) then + -- reconnected modem + dev.modem = device + state.no_modem = false + + if state.init_ok then nic.connect(device) end + + print_no_fp("comms modem reconnected") + log.info("comms modem reconnected") + + -- determine if we are still in a degraded state + if not state.no_reactor then + state.degraded = false + end + elseif device.isWireless() then + log.info("unused wireless modem connected") + else + log.info("non-comms wired modem connected") + end + end +end + +-- handle a backplane peripheral detach +---@param iface string +---@param type string +---@param device table +---@param print_no_fp function +function backplane.detach(iface, type, device, print_no_fp) +end + +return backplane diff --git a/reactor-plc/configure.lua b/reactor-plc/configure.lua index 1f7fa68..47fdc8c 100644 --- a/reactor-plc/configure.lua +++ b/reactor-plc/configure.lua @@ -81,7 +81,9 @@ local tmp_cfg = { SVR_Channel = nil, ---@type integer PLC_Channel = nil, ---@type integer ConnTimeout = nil, ---@type number + WirelessModem = true, WiredModem = false, ---@type string|false + PreferWireless = true, TrustedRange = nil, ---@type number AuthKey = "", ---@type string LogMode = 0, ---@type LOG_MODE @@ -107,7 +109,9 @@ local fields = { { "SVR_Channel", "SVR Channel", 16240 }, { "PLC_Channel", "PLC Channel", 16241 }, { "ConnTimeout", "Connection Timeout", 5 }, - { "WiredModem", "Wired Modem", false }, + { "WirelessModem", "Wireless/Ender Comms Modem", true }, + { "WiredModem", "Wired Comms Modem", false }, + { "PreferWireless", "Prefer Wireless Modem", true }, { "TrustedRange", "Trusted Range", 0 }, { "AuthKey", "Facility Auth Key" , ""}, { "LogMode", "Log Mode", log.MODE.APPEND }, diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 4381511..f69f289 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -48,7 +48,9 @@ function plc.load_config() config.SVR_Channel = settings.get("SVR_Channel") config.PLC_Channel = settings.get("PLC_Channel") config.ConnTimeout = settings.get("ConnTimeout") + config.WirelessModem = settings.get("WirelessModem") config.WiredModem = settings.get("WiredModem") + config.PreferWireless = settings.get("PreferWireless") config.TrustedRange = settings.get("TrustedRange") config.AuthKey = settings.get("AuthKey") @@ -71,12 +73,15 @@ function plc.validate_config(cfg) cfv.assert_type_int(cfg.UnitID) cfv.assert_type_bool(cfg.EmerCoolEnable) - if cfg.Networked == true then + if cfg.Networked then cfv.assert_channel(cfg.SVR_Channel) cfv.assert_channel(cfg.PLC_Channel) cfv.assert_type_num(cfg.ConnTimeout) cfv.assert_min(cfg.ConnTimeout, 2) + cfv.assert_type_bool(cfg.WirelessModem) cfv.assert((cfg.WiredModem == false) or (type(cfg.WiredModem) == "string")) + cfv.assert(cfg.WirelessModem or (type(cfg.WiredModem) == "string")) + cfv.assert_type_bool(cfg.PreferWireless) cfv.assert_type_num(cfg.TrustedRange) cfv.assert_min(cfg.TrustedRange, 0) cfv.assert_type_str(cfg.AuthKey) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 5cb8f72..1e10f93 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -3,6 +3,7 @@ -- require("/initenv").init_env() +local backplane = require("reactor-plc.backplane") local comms = require("scada-common.comms") local crash = require("scada-common.crash") @@ -87,7 +88,6 @@ local function main() -- PLC system state flags ---@class plc_state plc_state = { - init_ok = true, fp_ok = false, shutdown = false, degraded = true, @@ -103,15 +103,14 @@ local function main() burn_rate = 0.0 }, - -- core PLC devices + -- global PLC devices, still initialized by the backplane + ---@class plc_dev plc_dev = { - reactor = ppm.get_fission_reactor(), - modem_wired = type(config.WiredModem) == "string", - modem_iface = config.WiredModem, - modem = nil + reactor = nil ---@type table }, -- system objects + ---@class plc_sys plc_sys = { rps = nil, ---@type rps nic = nil, ---@type nic @@ -132,115 +131,66 @@ local function main() local plc_state = __shared_memory.plc_state - -- get the configured modem - if smem_dev.modem_wired then - smem_dev.modem = ppm.get_modem(smem_dev.modem_iface) - else smem_dev.modem = ppm.get_wireless_modem() end + -- reactor and modem initialization + backplane.init(config, __shared_memory) - -- initial state evaluation - plc_state.no_reactor = smem_dev.reactor == nil - plc_state.no_modem = smem_dev.modem == nil - - -- 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") - - plc_state.init_ok = false - plc_state.degraded = true - 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") - - plc_state.degraded = true - plc_state.reactor_formed = false + -- 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 - -- 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") + -- setup front panel + local message + plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode) - -- 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 + -- ...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(message) if not plc_state.fp_ok then println(message) end end + local function _println_no_fp(msg) if not plc_state.fp_ok then println(msg) 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 + ---------------------------------------- + -- initialize PLC + ---------------------------------------- - -- setup front panel - if not renderer.ui_ready() then - local message - plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode) + -- init reactor protection system + smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed) + log.debug("startup> rps init") - -- ...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) + -- 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 - ---------------------------------------- - -- start system - ---------------------------------------- + -- 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") - -- initialize PLC - init() + -- create network interface then setup comms + smem_sys.nic = network.nic(backplane.active_nic()) + 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 +204,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..492a11e 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 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!") @@ -184,7 +184,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 @@ -220,7 +220,7 @@ function threads.thread__main(smem, init) if plc_state.init_ok then nic.connect(device) end - println_ts("comms modem reconnected.") + println_ts("comms modem reconnected") log.info("comms modem reconnected") -- determine if we are still in a degraded state @@ -235,22 +235,12 @@ 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 @@ -280,7 +270,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 @@ -399,15 +388,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 @@ -430,7 +419,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 diff --git a/scada-common/network.lua b/scada-common/network.lua index 8066adb..f7213bd 100644 --- a/scada-common/network.lua +++ b/scada-common/network.lua @@ -78,7 +78,7 @@ 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 = {} } @@ -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 @@ -193,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