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 a2929cb..208197e 100644
--- a/reactor-plc/plc.lua
+++ b/reactor-plc/plc.lua
@@ -118,7 +118,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
@@ -364,29 +364,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
@@ -474,6 +480,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
@@ -495,14 +502,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
@@ -584,11 +591,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()
@@ -707,6 +710,112 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
reactor.__p_enable_afc()
end
+ -- handle a burn rate command
+ ---@param packet rplc_frame
+ ---@param setpoints plc_setpoints
+ --- EVENT_CONSUMER: this function consumes events
+ local function _handle_burn_rate(packet, setpoints)
+ if (packet.length == 2) and (type(packet.data[1]) == "number") then
+ local success = false
+ local burn_rate = math.floor(packet.data[1] * 10) / 10
+ local ramp = packet.data[2]
+
+ -- if no known max burn rate, check again
+ if self.max_burn_rate == nil then
+ self.max_burn_rate = reactor.getMaxBurnRate()
+ end
+
+ -- if we know our max burn rate, update current burn rate setpoint if in range
+ if self.max_burn_rate ~= ppm.ACCESS_FAULT then
+ if burn_rate > 0 and burn_rate <= self.max_burn_rate then
+ if ramp then
+ setpoints.burn_rate_en = true
+ setpoints.burn_rate = burn_rate
+ success = true
+ else
+ reactor.setBurnRate(burn_rate)
+ success = reactor.__p_is_ok()
+ end
+ else
+ log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate)
+ end
+ end
+
+ _send_ack(packet.type, success)
+ else
+ log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate")
+ end
+ end
+
+ -- handle an auto burn rate command
+ ---@param packet rplc_frame
+ ---@param setpoints plc_setpoints
+ --- EVENT_CONSUMER: this function consumes events
+ local function _handle_auto_burn_rate(packet, setpoints)
+ if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then
+ local ack = AUTO_ACK.FAIL
+ local burn_rate = math.floor(packet.data[1] * 100) / 100
+ local ramp = packet.data[2]
+ self.auto_ack_token = packet.data[3]
+
+ -- if no known max burn rate, check again
+ if self.max_burn_rate == nil then
+ self.max_burn_rate = reactor.getMaxBurnRate()
+ end
+
+ -- if we know our max burn rate, update current burn rate setpoint if in range
+ if self.max_burn_rate ~= ppm.ACCESS_FAULT then
+ if burn_rate < 0.01 then
+ if rps.is_active() then
+ -- auto scram to disable
+ log.debug("AUTO: stopping the reactor to meet 0.0 burn rate")
+ if rps.scram() then
+ ack = AUTO_ACK.ZERO_DIS_OK
+ else
+ log.warning("AUTO: automatic reactor stop failed")
+ end
+ else
+ ack = AUTO_ACK.ZERO_DIS_OK
+ end
+ elseif burn_rate <= self.max_burn_rate then
+ if not rps.is_active() then
+ -- activate the reactor
+ log.debug("AUTO: activating the reactor")
+
+ reactor.setBurnRate(0.01)
+ if reactor.__p_is_faulted() then
+ log.warning("AUTO: failed to reset burn rate for auto activation")
+ else
+ if not rps.auto_activate() then
+ log.warning("AUTO: automatic reactor activation failed")
+ end
+ end
+ end
+
+ -- if active, set/ramp burn rate
+ if rps.is_active() then
+ if ramp then
+ log.debug(util.c("AUTO: setting burn rate ramp to ", burn_rate))
+ setpoints.burn_rate_en = true
+ setpoints.burn_rate = burn_rate
+ ack = AUTO_ACK.RAMP_SET_OK
+ else
+ log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate))
+ reactor.setBurnRate(burn_rate)
+ ack = util.trinary(reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK)
+ end
+ end
+ else
+ log.debug(util.c(burn_rate, " rate outside of 0 < x <= ", self.max_burn_rate))
+ end
+ end
+
+ _send_ack(packet.type, ack)
+ else
+ log.debug("RPLC set automatic burn rate packet length mismatch or non-numeric burn rate")
+ end
+ end
+
-- PUBLIC FUNCTIONS --
---@class plc_comms
@@ -748,8 +857,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
---@param formed boolean reactor formed (from PLC state)
function public.send_status(no_reactor, formed)
if self.linked then
- local mek_data = nil ---@type table
- local heating_rate = 0.0 ---@type number
+ local mek_data = nil ---@type table
+ local heating_rate = 0.0 ---@type number
if (not no_reactor) and rps.is_formed() then
if _update_status_cache() then mek_data = self.status_cache end
@@ -803,15 +912,11 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
-- get as RPLC packet
if s_pkt.protocol() == PROTOCOL.RPLC then
local rplc_pkt = comms.rplc_packet()
- if rplc_pkt.decode(s_pkt) then
- pkt = rplc_pkt.get()
- end
+ if rplc_pkt.decode(s_pkt) then pkt = rplc_pkt.get() end
-- get as SCADA management packet
elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet()
- if mgmt_pkt.decode(s_pkt) then
- pkt = mgmt_pkt.get()
- end
+ if mgmt_pkt.decode(s_pkt) then pkt = mgmt_pkt.get() end
else
log.debug("unsupported packet type " .. s_pkt.protocol(), true)
end
@@ -823,16 +928,13 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
-- handle RPLC and MGMT packets
---@param packet rplc_frame|mgmt_frame packet frame
---@param plc_state plc_state PLC state
- ---@param setpoints setpoints setpoint control table
- function public.handle_packet(packet, plc_state, setpoints)
- -- print a log message to the terminal as long as the UI isn't running
- local function println_ts(message) if not plc_state.fp_ok then util.println_ts(message) end end
-
+ ---@param setpoints plc_setpoints setpoint control table
+ ---@param println_ts function console print, when UI isn't running
+ function public.handle_packet(packet, plc_state, setpoints, println_ts)
local protocol = packet.scada_frame.protocol()
local l_chan = packet.scada_frame.local_channel()
local src_addr = packet.scada_frame.src_addr()
- -- handle packets now that we have prints setup
if l_chan == config.PLC_Channel then
-- check sequence number
if self.r_seq_num == nil then
@@ -867,36 +969,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
log.debug("sent out structure again, did supervisor miss it?")
elseif packet.type == RPLC_TYPE.MEK_BURN_RATE then
-- set the burn rate
- if (packet.length == 2) and (type(packet.data[1]) == "number") then
- local success = false
- local burn_rate = math.floor(packet.data[1] * 10) / 10
- local ramp = packet.data[2]
-
- -- if no known max burn rate, check again
- if self.max_burn_rate == nil then
- self.max_burn_rate = reactor.getMaxBurnRate()
- end
-
- -- if we know our max burn rate, update current burn rate setpoint if in range
- if self.max_burn_rate ~= ppm.ACCESS_FAULT then
- if burn_rate > 0 and burn_rate <= self.max_burn_rate then
- if ramp then
- setpoints.burn_rate_en = true
- setpoints.burn_rate = burn_rate
- success = true
- else
- reactor.setBurnRate(burn_rate)
- success = reactor.__p_is_ok()
- end
- else
- log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate)
- end
- end
-
- _send_ack(packet.type, success)
- else
- log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate")
- end
+ _handle_burn_rate(packet, setpoints)
elseif packet.type == RPLC_TYPE.RPS_ENABLE then
-- enable the reactor
self.scrammed = false
@@ -925,68 +998,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
_send_ack(packet.type, true)
elseif packet.type == RPLC_TYPE.AUTO_BURN_RATE then
-- automatic control requested a new burn rate
- if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then
- local ack = AUTO_ACK.FAIL
- local burn_rate = math.floor(packet.data[1] * 100) / 100
- local ramp = packet.data[2]
- self.auto_ack_token = packet.data[3]
-
- -- if no known max burn rate, check again
- if self.max_burn_rate == nil then
- self.max_burn_rate = reactor.getMaxBurnRate()
- end
-
- -- if we know our max burn rate, update current burn rate setpoint if in range
- if self.max_burn_rate ~= ppm.ACCESS_FAULT then
- if burn_rate < 0.01 then
- if rps.is_active() then
- -- auto scram to disable
- log.debug("AUTO: stopping the reactor to meet 0.0 burn rate")
- if rps.scram() then
- ack = AUTO_ACK.ZERO_DIS_OK
- else
- log.warning("AUTO: automatic reactor stop failed")
- end
- else
- ack = AUTO_ACK.ZERO_DIS_OK
- end
- elseif burn_rate <= self.max_burn_rate then
- if not rps.is_active() then
- -- activate the reactor
- log.debug("AUTO: activating the reactor")
-
- reactor.setBurnRate(0.01)
- if reactor.__p_is_faulted() then
- log.warning("AUTO: failed to reset burn rate for auto activation")
- else
- if not rps.auto_activate() then
- log.warning("AUTO: automatic reactor activation failed")
- end
- end
- end
-
- -- if active, set/ramp burn rate
- if rps.is_active() then
- if ramp then
- log.debug(util.c("AUTO: setting burn rate ramp to ", burn_rate))
- setpoints.burn_rate_en = true
- setpoints.burn_rate = burn_rate
- ack = AUTO_ACK.RAMP_SET_OK
- else
- log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate))
- reactor.setBurnRate(burn_rate)
- ack = util.trinary(reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK)
- end
- end
- else
- log.debug(util.c(burn_rate, " rate outside of 0 < x <= ", self.max_burn_rate))
- end
- end
-
- _send_ack(packet.type, ack)
- else
- log.debug("RPLC set automatic burn rate packet length mismatch or non-numeric burn rate")
- end
+ _handle_auto_burn_rate(packet, setpoints)
else
log.debug("received unknown RPLC packet type " .. packet.type)
end
diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua
index ddf289a..289ab8d 100644
--- a/reactor-plc/startup.lua
+++ b/reactor-plc/startup.lua
@@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads")
-local R_PLC_VERSION = "v1.8.22"
+local R_PLC_VERSION = "v1.9.0"
local println = util.println
local println_ts = util.println_ts
@@ -87,7 +87,6 @@ local function main()
-- PLC system state flags
---@class plc_state
plc_state = {
- init_ok = true,
fp_ok = false,
shutdown = false,
degraded = true,
@@ -97,19 +96,22 @@ local function main()
},
-- control setpoints
- ---@class setpoints
+ ---@class plc_setpoints
setpoints = {
burn_rate_en = false,
burn_rate = 0.0
},
-- core PLC devices
+ ---@class plc_dev
plc_dev = {
- reactor = ppm.get_fission_reactor(),
+---@diagnostic disable-next-line: assign-type-mismatch
+ reactor = ppm.get_fission_reactor(), ---@type table
modem = ppm.get_wireless_modem()
},
-- system objects
+ ---@class plc_sys
plc_sys = {
rps = nil, ---@type rps
nic = nil, ---@type nic
@@ -136,14 +138,20 @@ local function main()
-- we need a reactor, can at least do some things even if it isn't formed though
if plc_state.no_reactor then
- println("init> fission reactor not found")
- log.warning("init> no reactor on startup")
+ println("startup> fission reactor not found")
+ log.warning("startup> no reactor on startup")
- plc_state.init_ok = false
plc_state.degraded = true
+ plc_state.reactor_formed = false
+
+ -- mount a virtual peripheral to init the RPS with
+ local _, dev = ppm.mount_virtual()
+ smem_dev.reactor = dev
+
+ log.info("startup> mounted virtual device as reactor")
elseif not smem_dev.reactor.isFormed() then
- println("init> fission reactor is not formed")
- log.warning("init> reactor logic adapter present, but reactor is not formed")
+ println("startup> fission reactor is not formed")
+ log.warning("startup> reactor logic adapter present, but reactor is not formed")
plc_state.degraded = true
plc_state.reactor_formed = false
@@ -151,89 +159,74 @@ local function main()
-- modem is required if networked
if __shared_memory.networked and plc_state.no_modem then
- println("init> wireless modem not found")
- log.warning("init> no wireless modem on startup")
+ println("startup> wireless modem not found")
+ log.warning("startup> no wireless modem on startup")
-- scram reactor if present and enabled
if (smem_dev.reactor ~= nil) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
smem_dev.reactor.scram()
end
- plc_state.init_ok = false
plc_state.degraded = true
end
- -- print a log message to the terminal as long as the UI isn't running
- local function _println_no_fp(message) if not plc_state.fp_ok then println(message) end end
-
- -- PLC init
- --- EVENT_CONSUMER: this function consumes events
- local function init()
- -- scram on boot if networked, otherwise leave the reactor be
- if __shared_memory.networked and (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
- smem_dev.reactor.scram()
- end
-
- -- setup front panel
- if not renderer.ui_ready() then
- local message
- plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode)
-
- -- ...or not
- if not plc_state.fp_ok then
- println_ts(util.c("UI error: ", message))
- println("init> running without front panel")
- log.error(util.c("front panel GUI render failed with error ", message))
- log.info("init> running in headless mode without front panel")
- end
- end
-
- if plc_state.init_ok then
- -- init reactor protection system
- smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed)
- log.debug("init> rps init")
-
- if __shared_memory.networked then
- -- comms watchdog
- smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
- log.debug("init> conn watchdog started")
-
- -- create network interface then setup comms
- smem_sys.nic = network.nic(smem_dev.modem)
- smem_sys.plc_comms = plc.comms(R_PLC_VERSION, smem_sys.nic, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
- log.debug("init> comms init")
- else
- _println_no_fp("init> starting in offline mode")
- log.info("init> running without networking")
- end
-
- -- notify user of emergency coolant configuration status
- if config.EmerCoolEnable then
- println("init> emergency coolant control ready")
- log.info("init> running with emergency coolant control available")
- end
-
- util.push_event("clock_start")
-
- _println_no_fp("init> completed")
- log.info("init> startup completed")
- else
- _println_no_fp("init> system in degraded state, awaiting devices...")
- log.warning("init> started in a degraded state, awaiting peripheral connections...")
- end
-
- databus.tx_hw_status(plc_state)
+ -- scram on boot if networked, otherwise leave the reactor be
+ if __shared_memory.networked and (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
+ log.debug("startup> power-on SCRAM")
+ smem_dev.reactor.scram()
end
+ -- setup front panel
+ local message
+ plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode)
+
+ -- ...or not
+ if not plc_state.fp_ok then
+ println_ts(util.c("UI error: ", message))
+ println("startup> running without front panel")
+ log.error(util.c("front panel GUI render failed with error ", message))
+ log.info("startup> running in headless mode without front panel")
+ end
+
+ -- print a log message to the terminal as long as the UI isn't running
+ local function _println_no_fp(msg) if not plc_state.fp_ok then println(msg) end end
+
----------------------------------------
- -- start system
+ -- initialize PLC
----------------------------------------
- -- initialize PLC
- init()
+ -- init reactor protection system
+ smem_sys.rps = plc.rps_init(smem_dev.reactor, 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
@@ -247,14 +240,12 @@ local function main()
-- run threads
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec, comms_thread_tx.p_exec, comms_thread_rx.p_exec, sp_ctrl_thread.p_exec)
- if plc_state.init_ok then
- -- send status one last time after RPS shutdown
- smem_sys.plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed)
- smem_sys.plc_comms.send_rps_status()
+ -- send status one last time after RPS shutdown
+ smem_sys.plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed)
+ smem_sys.plc_comms.send_rps_status()
- -- close connection
- smem_sys.plc_comms.close()
- end
+ -- close connection
+ smem_sys.plc_comms.close()
else
-- run threads, excluding comms
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec)
diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua
index b56ccc7..4474bd7 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 wireless modem
- -- note, check init_ok first since nic will be nil if it is false
- if plc_state.init_ok and nic.is_modem(device) then
+ if nic.is_modem(device) then
nic.disconnect()
println_ts("comms modem disconnected!")
@@ -161,10 +161,8 @@ function threads.thread__main(smem, init)
plc_state.no_modem = true
plc_state.degraded = true
- if plc_state.init_ok then
- -- try to scram reactor if it is still connected
- smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
- end
+ -- try to scram reactor if it is still connected
+ smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
end
else
log.warning("a modem was disconnected")
@@ -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,64 +194,52 @@ 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
-- note, check init_ok first since nic will be nil if it is false
- if device.isWireless() and not (plc_state.init_ok and nic.is_connected()) then
+ if device.isWireless() and not nic.is_connected() then
-- reconnected modem
plc_dev.modem = device
plc_state.no_modem = false
- if plc_state.init_ok then nic.connect(device) end
+ nic.connect(device)
- println_ts("wireless modem reconnected.")
+ println_ts("comms modem reconnected")
log.info("comms modem reconnected")
-- determine if we are still in a degraded state
- 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
- log.info("unused wireless modem reconnected")
+ log.info("unused wireless modem connected")
else
- log.info("wired modem reconnected")
+ log.info("wired modem connected")
end
end
end
- -- if not init'd and no longer degraded, proceed to init
- if not plc_state.init_ok and not plc_state.degraded then
- plc_state.init_ok = true
- init()
- end
-
-- update indicators
databus.tx_hw_status(plc_state)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
event == "double_click" then
-- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
- elseif event == "clock_start" then
- -- start loop clock
- loop_clock.start()
- log.debug("main thread clock started")
end
-- check for termination request
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
@@ -277,8 +263,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
@@ -299,7 +284,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
@@ -316,49 +301,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
@@ -368,19 +340,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
@@ -396,17 +368,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
@@ -428,8 +400,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
@@ -448,7 +420,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
@@ -465,7 +437,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
@@ -486,7 +458,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
@@ -508,7 +480,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
@@ -521,13 +493,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
@@ -546,7 +521,7 @@ function threads.thread__comms_rx(smem)
while comms_queue.ready() and not plc_state.shutdown do
local msg = comms_queue.pop()
- if msg ~= nil and plc_state.init_ok then
+ if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command
elseif msg.qtype == mqueue.TYPE.DATA then
@@ -555,7 +530,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
@@ -565,7 +540,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
@@ -587,7 +562,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
@@ -606,7 +581,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
@@ -629,9 +604,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()
@@ -698,7 +671,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
@@ -720,7 +693,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 18f2d48..17fbee8 100644
--- a/scada-common/comms.lua
+++ b/scada-common/comms.lua
@@ -205,7 +205,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
@@ -326,7 +326,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
@@ -412,7 +412,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
@@ -435,11 +435,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
@@ -498,7 +498,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
@@ -521,11 +521,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
@@ -580,7 +580,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
@@ -601,11 +601,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
@@ -659,7 +659,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
@@ -680,11 +680,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 7eccff7..317d517 100644
--- a/scada-common/network.lua
+++ b/scada-common/network.lua
@@ -1,9 +1,10 @@
--
--- Network Communications
+-- Network Communications and Message Authentication
--
local comms = require("scada-common.comms")
local log = require("scada-common.log")
+local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local md5 = require("lockbox.digest.md5")
@@ -17,7 +18,7 @@ local array = require("lockbox.util.array")
local network = {}
-- cryptography engine
-local c_eng = {
+local _crypt = {
key = nil,
hmac = nil
}
@@ -39,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
@@ -64,29 +65,41 @@ 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
----@param modem Modem modem to use
+-- utilizes HMAC-MD5 for message authentication, if enabled and this is wireless
+---@param modem Modem|nil modem to use
function network.nic(modem)
local self = {
- connected = true, -- used to avoid costly MAC calculations if modem isn't even present
+ -- modem interface name
+ iface = "?",
+ -- phy name
+ name = "?",
+ -- used to quickly return out of tx/rx functions if there is nothing to do
+ connected = false,
+ -- used to avoid costly MAC calculations if not required
+ use_hash = false,
+ -- open channels
channels = {}
}
---@class nic:Modem
local public = {}
+ -- get the phy name
+ ---@nodiscard
+ function public.phy_name() return self.name end
+
-- check if this NIC has a connected modem
---@nodiscard
function public.is_connected() return self.connected end
@@ -95,9 +108,14 @@ function network.nic(modem)
---@param reconnected_modem Modem
function public.connect(reconnected_modem)
modem = reconnected_modem
- self.connected = true
- -- open previously opened channels
+ self.iface = ppm.get_iface(modem)
+ self.name = util.c(util.trinary(modem.isWireless(), "WLAN_PHY", "ETH_PHY"), "{", self.iface, "}")
+ self.connected = true
+ self.use_hash = _crypt.hmac and modem.isWireless()
+
+ -- open only previously opened channels
+ modem.closeAll()
for _, channel in ipairs(self.channels) do
modem.open(channel)
end
@@ -117,13 +135,13 @@ function network.nic(modem)
function public.is_modem(device) return device == modem end
-- wrap modem functions, then create custom functions
- public.connect(modem)
+ if modem then public.connect(modem) end
-- open a channel on the modem
-- if disconnected *after* opening, previousy opened channels will be re-opened on reconnection
---@param channel integer
function public.open(channel)
- modem.open(channel)
+ if modem then modem.open(channel) end
local already_open = false
for i = 1, #self.channels do
@@ -141,7 +159,7 @@ function network.nic(modem)
-- close a channel on the modem
---@param channel integer
function public.close(channel)
- modem.close(channel)
+ if modem then modem.close(channel) end
for i = 1, #self.channels do
if self.channels[i] == channel then
@@ -153,7 +171,7 @@ function network.nic(modem)
-- close all channels on the modem
function public.closeAll()
- modem.closeAll()
+ if modem then modem.closeAll() end
self.channels = {}
end
@@ -165,17 +183,20 @@ function network.nic(modem)
if self.connected then
local tx_packet = packet ---@type authd_packet|scada_packet
- if c_eng.hmac ~= nil then
+ if self.use_hash then
-- local start = util.time_ms()
tx_packet = comms.authd_packet()
---@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
@@ -190,10 +211,10 @@ function network.nic(modem)
function public.receive(side, sender, reply_to, message, distance)
local packet = nil
- if self.connected then
+ if self.connected and side == self.iface then
local s_packet = comms.scada_packet()
- if c_eng.hmac ~= nil then
+ if self.use_hash then
-- parse packet as an authenticated SCADA packet
local a_packet = comms.authd_packet()
a_packet.receive(side, sender, reply_to, message, distance)
@@ -206,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 ea310a3..faf74a6 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
@@ -455,7 +455,7 @@ function ppm.get_wireless_modem()
local w_modem = nil
local emulated_env = periphemu ~= nil
- for _, device in pairs(ppm_sys.mounts) do
+ for _, device in pairs(_ppm.mounts) do
if device.type == "modem" and (emulated_env or device.dev.isWireless()) then
w_modem = device.dev
break
@@ -471,7 +471,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
diff --git a/scada-common/util.lua b/scada-common/util.lua
index 031eaa0..c889037 100644
--- a/scada-common/util.lua
+++ b/scada-common/util.lua
@@ -24,7 +24,7 @@ local t_pack = table.pack
local util = {}
-- scada-common version
-util.version = "1.5.4"
+util.version = "1.5.5"
util.TICK_TIME_S = 0.05
util.TICK_TIME_MS = 50