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