Merge pull request #641 from MikaylaFischler/640-plc-initialization-enhancements

640 plc initialization enhancements
This commit is contained in:
Mikayla 2025-10-26 16:50:29 -04:00 committed by GitHub
commit 1a7cb9eaa8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 475 additions and 479 deletions

View File

@ -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("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("has_modem", not plc_state.no_modem)
databus.ps.publish("degraded", plc_state.degraded) databus.ps.publish("degraded", plc_state.degraded)
databus.ps.publish("init_ok", plc_state.init_ok)
end end
-- transmit thread (routine) statuses -- transmit thread (routine) statuses

View File

@ -51,11 +51,11 @@ local function init(panel)
local system = Div{parent=panel,width=14,height=18,x=2,y=3} 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} local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn}
system.line_break() 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) heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green} local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green}

View File

@ -118,7 +118,7 @@ function plc.rps_init(reactor, is_formed)
reactor_enabled = false, reactor_enabled = false,
enabled_at = 0, enabled_at = 0,
emer_cool_active = nil, ---@type boolean emer_cool_active = nil, ---@type boolean
formed = is_formed, formed = is_formed, ---@type boolean|nil
force_disabled = false, force_disabled = false,
tripped = false, tripped = false,
trip_cause = "ok" ---@type rps_trip_cause trip_cause = "ok" ---@type rps_trip_cause
@ -364,14 +364,16 @@ function plc.rps_init(reactor, is_formed)
return public.activate() return public.activate()
end end
-- check all safety conditions -- check all safety conditions if we have a formed reactor, otherwise handle a subset of conditions
---@nodiscard ---@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 ---@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 status = RPS_TRIP_CAUSE.OK
local was_tripped = self.tripped local was_tripped = self.tripped
local first_trip = false local first_trip = false
if has_reactor then
if self.formed then if self.formed then
-- update state -- update state
parallel.waitForAll( parallel.waitForAll(
@ -388,6 +390,10 @@ function plc.rps_init(reactor, is_formed)
-- check to see if its now formed -- check to see if its now formed
_is_formed() _is_formed()
end end
else
self.formed = nil
self.state[CHK.SYS_FAIL] = true
end
-- check system states in order of severity -- check system states in order of severity
if self.tripped then if self.tripped then
@ -474,6 +480,7 @@ function plc.rps_init(reactor, is_formed)
---@nodiscard ---@nodiscard
function public.is_active() return self.reactor_enabled end function public.is_active() return self.reactor_enabled end
---@nodiscard ---@nodiscard
---@return boolean|nil formed true if formed, false if not, nil if unknown
function public.is_formed() return self.formed end function public.is_formed() return self.formed end
---@nodiscard ---@nodiscard
function public.is_force_disabled() return self.force_disabled end function public.is_force_disabled() return self.force_disabled end
@ -495,14 +502,14 @@ function plc.rps_init(reactor, is_formed)
end end
-- partial RPS reset that only clears fault and sys_fail -- partial RPS reset that only clears fault and sys_fail
function public.reset_formed() function public.reset_reattach()
self.tripped = false self.tripped = false
self.trip_cause = RPS_TRIP_CAUSE.OK self.trip_cause = RPS_TRIP_CAUSE.OK
self.state[CHK.FAULT] = false self.state[CHK.FAULT] = false
self.state[CHK.SYS_FAIL] = false self.state[CHK.SYS_FAIL] = false
log.info("RPS: partial reset on formed") log.info("RPS: partial reset on connected or formed")
end end
-- reset the automatic and timeout trip flags, then clear trip if that was the trip cause -- 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 -- dynamic reactor status information, excluding heating rate
---@return table data_table, boolean faulted ---@return table data_table, boolean faulted
local function _get_reactor_status() local function _get_reactor_status()
local fuel = nil local fuel, waste, coolant, hcoolant = nil, nil, nil, nil
local waste = nil
local coolant = nil
local hcoolant = nil
local data_table = {} local data_table = {}
reactor.__p_disable_afc() reactor.__p_disable_afc()
@ -707,6 +710,112 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
reactor.__p_enable_afc() reactor.__p_enable_afc()
end 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 -- -- PUBLIC FUNCTIONS --
---@class plc_comms ---@class plc_comms
@ -803,15 +912,11 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
-- get as RPLC packet -- get as RPLC packet
if s_pkt.protocol() == PROTOCOL.RPLC then if s_pkt.protocol() == PROTOCOL.RPLC then
local rplc_pkt = comms.rplc_packet() local rplc_pkt = comms.rplc_packet()
if rplc_pkt.decode(s_pkt) then if rplc_pkt.decode(s_pkt) then pkt = rplc_pkt.get() end
pkt = rplc_pkt.get()
end
-- get as SCADA management packet -- get as SCADA management packet
elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet() local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then if mgmt_pkt.decode(s_pkt) then pkt = mgmt_pkt.get() end
pkt = mgmt_pkt.get()
end
else else
log.debug("unsupported packet type " .. s_pkt.protocol(), true) log.debug("unsupported packet type " .. s_pkt.protocol(), true)
end end
@ -823,16 +928,13 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
-- handle RPLC and MGMT packets -- handle RPLC and MGMT packets
---@param packet rplc_frame|mgmt_frame packet frame ---@param packet rplc_frame|mgmt_frame packet frame
---@param plc_state plc_state PLC state ---@param plc_state plc_state PLC state
---@param setpoints setpoints setpoint control table ---@param setpoints plc_setpoints setpoint control table
function public.handle_packet(packet, plc_state, setpoints) ---@param println_ts function console print, when UI isn't running
-- print a log message to the terminal as long as the UI isn't running function public.handle_packet(packet, plc_state, setpoints, println_ts)
local function println_ts(message) if not plc_state.fp_ok then util.println_ts(message) end end
local protocol = packet.scada_frame.protocol() local protocol = packet.scada_frame.protocol()
local l_chan = packet.scada_frame.local_channel() local l_chan = packet.scada_frame.local_channel()
local src_addr = packet.scada_frame.src_addr() local src_addr = packet.scada_frame.src_addr()
-- handle packets now that we have prints setup
if l_chan == config.PLC_Channel then if l_chan == config.PLC_Channel then
-- check sequence number -- check sequence number
if self.r_seq_num == nil then 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?") log.debug("sent out structure again, did supervisor miss it?")
elseif packet.type == RPLC_TYPE.MEK_BURN_RATE then elseif packet.type == RPLC_TYPE.MEK_BURN_RATE then
-- set the burn rate -- set the burn rate
if (packet.length == 2) and (type(packet.data[1]) == "number") then _handle_burn_rate(packet, setpoints)
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
elseif packet.type == RPLC_TYPE.RPS_ENABLE then elseif packet.type == RPLC_TYPE.RPS_ENABLE then
-- enable the reactor -- enable the reactor
self.scrammed = false self.scrammed = false
@ -925,68 +998,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
_send_ack(packet.type, true) _send_ack(packet.type, true)
elseif packet.type == RPLC_TYPE.AUTO_BURN_RATE then elseif packet.type == RPLC_TYPE.AUTO_BURN_RATE then
-- automatic control requested a new burn rate -- automatic control requested a new burn rate
if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then _handle_auto_burn_rate(packet, setpoints)
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
else else
log.debug("received unknown RPLC packet type " .. packet.type) log.debug("received unknown RPLC packet type " .. packet.type)
end end

View File

@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer") local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads") 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 = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -87,7 +87,6 @@ local function main()
-- PLC system state flags -- PLC system state flags
---@class plc_state ---@class plc_state
plc_state = { plc_state = {
init_ok = true,
fp_ok = false, fp_ok = false,
shutdown = false, shutdown = false,
degraded = true, degraded = true,
@ -97,19 +96,22 @@ local function main()
}, },
-- control setpoints -- control setpoints
---@class setpoints ---@class plc_setpoints
setpoints = { setpoints = {
burn_rate_en = false, burn_rate_en = false,
burn_rate = 0.0 burn_rate = 0.0
}, },
-- core PLC devices -- core PLC devices
---@class plc_dev
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() modem = ppm.get_wireless_modem()
}, },
-- system objects -- system objects
---@class plc_sys
plc_sys = { plc_sys = {
rps = nil, ---@type rps rps = nil, ---@type rps
nic = nil, ---@type nic 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 -- we need a reactor, can at least do some things even if it isn't formed though
if plc_state.no_reactor then if plc_state.no_reactor then
println("init> fission reactor not found") println("startup> fission reactor not found")
log.warning("init> no reactor on startup") log.warning("startup> no reactor on startup")
plc_state.init_ok = false
plc_state.degraded = true 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 elseif not smem_dev.reactor.isFormed() then
println("init> fission reactor is not formed") println("startup> fission reactor is not formed")
log.warning("init> reactor logic adapter present, but reactor is not formed") log.warning("startup> reactor logic adapter present, but reactor is not formed")
plc_state.degraded = true plc_state.degraded = true
plc_state.reactor_formed = false plc_state.reactor_formed = false
@ -151,89 +159,74 @@ local function main()
-- modem is required if networked -- modem is required if networked
if __shared_memory.networked and plc_state.no_modem then if __shared_memory.networked and plc_state.no_modem then
println("init> wireless modem not found") println("startup> wireless modem not found")
log.warning("init> no wireless modem on startup") log.warning("startup> no wireless modem on startup")
-- scram reactor if present and enabled -- scram reactor if present and enabled
if (smem_dev.reactor ~= nil) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then if (smem_dev.reactor ~= nil) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
smem_dev.reactor.scram() smem_dev.reactor.scram()
end end
plc_state.init_ok = false
plc_state.degraded = true plc_state.degraded = true
end 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<br>
--- EVENT_CONSUMER: this function consumes events
local function init()
-- scram on boot if networked, otherwise leave the reactor be -- 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 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() smem_dev.reactor.scram()
end end
-- setup front panel -- setup front panel
if not renderer.ui_ready() then
local message local message
plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode) plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode)
-- ...or not -- ...or not
if not plc_state.fp_ok then if not plc_state.fp_ok then
println_ts(util.c("UI error: ", message)) println_ts(util.c("UI error: ", message))
println("init> running without front panel") println("startup> running without front panel")
log.error(util.c("front panel GUI render failed with error ", message)) log.error(util.c("front panel GUI render failed with error ", message))
log.info("init> running in headless mode without front panel") log.info("startup> running in headless mode without front panel")
end
end end
if plc_state.init_ok then -- 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
----------------------------------------
-- initialize PLC
----------------------------------------
-- init reactor protection system -- init reactor protection system
smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed) smem_sys.rps = plc.rps_init(smem_dev.reactor, util.trinary(plc_state.no_reactor, nil, plc_state.reactor_formed))
log.debug("init> rps init") 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 if __shared_memory.networked then
-- comms watchdog -- comms watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout) smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("init> conn watchdog started") log.debug("startup> conn watchdog started")
-- create network interface then setup comms -- create network interface then setup comms
smem_sys.nic = network.nic(smem_dev.modem) 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) 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") log.debug("startup> comms init")
else else
_println_no_fp("init> starting in offline mode") _println_no_fp("startup> starting in non-networked mode")
log.info("init> running without networking") log.info("startup> starting 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 end
databus.tx_hw_status(plc_state) databus.tx_hw_status(plc_state)
end
---------------------------------------- _println_no_fp("startup> completed")
-- start system log.info("startup> completed")
----------------------------------------
-- initialize PLC
init()
-- init threads -- 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) local rps_thread = threads.thread__rps(__shared_memory)
if __shared_memory.networked then if __shared_memory.networked then
@ -247,14 +240,12 @@ local function main()
-- run threads -- 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) 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 -- 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_status(plc_state.no_reactor, plc_state.reactor_formed)
smem_sys.plc_comms.send_rps_status() smem_sys.plc_comms.send_rps_status()
-- close connection -- close connection
smem_sys.plc_comms.close() smem_sys.plc_comms.close()
end
else else
-- run threads, excluding comms -- run threads, excluding comms
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec) parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec)

View File

@ -31,8 +31,7 @@ local MQ__COMM_CMD = {
-- main thread -- main thread
---@nodiscard ---@nodiscard
---@param smem plc_shared_memory ---@param smem plc_shared_memory
---@param init function function threads.thread__main(smem)
function threads.thread__main(smem, init)
-- print a log message to the terminal as long as the UI isn't running -- 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 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 -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("main", true) 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 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) -- 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_state = smem.plc_state
local plc_dev = smem.plc_dev local plc_dev = smem.plc_dev
-- start clock
loop_clock.start()
-- event loop -- event loop
while true do while true do
-- get plc_sys fields (may have been set late due to degraded boot) -- 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 -- handle event
if event == "timer" and loop_clock.is_clock(param1) then if event == "timer" and loop_clock.is_clock(param1) then
-- note: loop clock is only running if init_ok = true
-- blink heartbeat indicator -- blink heartbeat indicator
databus.heartbeat() databus.heartbeat()
@ -93,7 +94,7 @@ function threads.thread__main(smem, init)
-- reactor now formed -- reactor now formed
plc_state.reactor_formed = true plc_state.reactor_formed = true
println_ts("reactor is now formed.") println_ts("reactor is now formed")
log.info("reactor is now formed") log.info("reactor is now formed")
-- SCRAM newly formed reactor -- SCRAM newly formed reactor
@ -106,10 +107,10 @@ function threads.thread__main(smem, init)
-- partial reset of RPS, specific to becoming formed -- partial reset of RPS, specific to becoming formed
-- without this, auto control can't resume on chunk load -- without this, auto control can't resume on chunk load
rps.reset_formed() rps.reset_reattach()
elseif plc_state.reactor_formed and not rps.is_formed() then elseif plc_state.reactor_formed and (rps.is_formed() == false) then
-- reactor no longer formed -- 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") log.info("reactor is no longer formed")
plc_state.reactor_formed = false plc_state.reactor_formed = false
@ -118,14 +119,14 @@ function threads.thread__main(smem, init)
-- update indicators -- update indicators
databus.tx_hw_status(plc_state) 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 -- got a packet
local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5) local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5)
if packet ~= nil then if packet ~= nil then
-- pass the packet onto the comms message queue -- pass the packet onto the comms message queue
smem.q.mq_comms_rx.push_packet(packet) smem.q.mq_comms_rx.push_packet(packet)
end 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 -- haven't heard from server recently? close connection and shutdown reactor
plc_comms.close() plc_comms.close()
smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT) 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 elseif networked and type == "modem" then
---@cast device Modem ---@cast device Modem
-- we only care if this is our wireless 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 nic.is_modem(device) then
if plc_state.init_ok and nic.is_modem(device) then
nic.disconnect() nic.disconnect()
println_ts("comms modem disconnected!") println_ts("comms modem disconnected!")
@ -161,11 +161,9 @@ function threads.thread__main(smem, init)
plc_state.no_modem = true plc_state.no_modem = true
plc_state.degraded = true plc_state.degraded = true
if plc_state.init_ok then
-- try to scram reactor if it is still connected -- try to scram reactor if it is still connected
smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM) smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
end end
end
else else
log.warning("a modem was disconnected") log.warning("a modem was disconnected")
end end
@ -184,7 +182,7 @@ function threads.thread__main(smem, init)
plc_dev.reactor = device plc_dev.reactor = device
plc_state.no_reactor = false plc_state.no_reactor = false
println_ts("reactor reconnected.") println_ts("reactor reconnected")
log.info("reactor reconnected") log.info("reactor reconnected")
-- we need to assume formed here as we cannot check in this main loop -- we need to assume formed here as we cannot check in this main loop
@ -196,7 +194,6 @@ function threads.thread__main(smem, init)
plc_state.degraded = false plc_state.degraded = false
end 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) rps.reconnect_reactor(plc_dev.reactor)
@ -206,54 +203,43 @@ function threads.thread__main(smem, init)
-- partial reset of RPS, specific to becoming formed/reconnected -- partial reset of RPS, specific to becoming formed/reconnected
-- without this, auto control can't resume on chunk load -- without this, auto control can't resume on chunk load
rps.reset_formed() rps.reset_reattach()
end
elseif networked and type == "modem" then elseif networked and type == "modem" then
---@cast device Modem ---@cast device Modem
-- note, check init_ok first since nic will be nil if it is false -- 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 -- reconnected modem
plc_dev.modem = device plc_dev.modem = device
plc_state.no_modem = false 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") log.info("comms modem reconnected")
-- determine if we are still in a degraded state -- 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 plc_state.degraded = false
end end
elseif device.isWireless() then elseif device.isWireless() then
log.info("unused wireless modem reconnected") log.info("unused wireless modem connected")
else else
log.info("wired modem reconnected") log.info("wired modem connected")
end end
end 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 -- update indicators
databus.tx_hw_status(plc_state) databus.tx_hw_status(plc_state)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
event == "double_click" then event == "double_click" then
-- handle a mouse event -- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) 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 end
-- check for termination request -- check for termination request
if event == "terminate" or ppm.should_terminate() then 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 -- rps handles reactor shutdown
plc_state.shutdown = true plc_state.shutdown = true
break break
@ -277,8 +263,7 @@ function threads.thread__main(smem, init)
-- if not, we need to restart the clock -- if not, we need to restart the clock
-- this thread cannot be slept because it will miss events (namely "terminate" otherwise) -- this thread cannot be slept because it will miss events (namely "terminate" otherwise)
if not plc_state.shutdown then if not plc_state.shutdown then
log.info("main thread restarting now...") log.info("OS: main thread restarting now...")
util.push_event("clock_start")
end end
end end
end end
@ -299,7 +284,7 @@ function threads.thread__rps(smem)
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("rps", true) databus.tx_rt_status("rps", true)
log.debug("rps thread start") log.debug("OS: rps thread start")
-- load in from shared memory -- load in from shared memory
local networked = smem.networked local networked = smem.networked
@ -316,32 +301,25 @@ function threads.thread__rps(smem)
-- get plc_sys fields (may have been set late due to degraded boot) -- get plc_sys fields (may have been set late due to degraded boot)
local rps = smem.plc_sys.rps local rps = smem.plc_sys.rps
local plc_comms = smem.plc_sys.plc_comms 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 local reactor = plc_dev.reactor
-- RPS checks
if plc_state.init_ok then
-- SCRAM if no open connection -- SCRAM if no open connection
if networked and not plc_comms.is_linked() then if networked and not plc_comms.is_linked() then
if was_linked then if was_linked then
was_linked = false was_linked = false
rps.trip_timeout() rps.trip_timeout()
end end
else else was_linked = true end
was_linked = true
end
if (not plc_state.no_reactor) and rps.is_formed() then
-- check reactor status -- check reactor status
---@diagnostic disable-next-line: need-check-nil if (not plc_state.no_reactor) and rps.is_formed() then
local reactor_status = reactor.getStatus() local reactor_status = reactor.getStatus()
databus.tx_reactor_state(reactor_status) databus.tx_reactor_state(reactor_status)
-- if we tried to SCRAM but failed, keep trying -- 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) -- 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 if rps.is_tripped() and reactor_status then rps.scram() end
rps.scram()
end
end end
-- if we are in standalone mode and the front panel isn't working, continuously reset RPS -- if we are in standalone mode and the front panel isn't working, continuously reset RPS
@ -349,16 +327,10 @@ function threads.thread__rps(smem)
if not (networked or smem.plc_state.fp_ok) then rps.reset(true) end if not (networked or smem.plc_state.fp_ok) then rps.reset(true) end
-- check safety (SCRAM occurs if tripped) -- check safety (SCRAM occurs if tripped)
if not plc_state.no_reactor then local rps_tripped, rps_status_string, rps_first = rps.check(not plc_state.no_reactor)
local rps_tripped, rps_status_string, rps_first = rps.check()
if rps_tripped and rps_first then if rps_tripped and rps_first then
println_ts("[RPS] SCRAM! safety trip: " .. rps_status_string) println_ts("RPS: SCRAM on safety trip (" .. rps_status_string .. ")")
if networked and not plc_state.no_modem then if networked then plc_comms.send_rps_alarm(rps_status_string) end
plc_comms.send_rps_alarm(rps_status_string)
end
end
end
end end
-- check for messages in the message queue -- check for messages in the message queue
@ -368,19 +340,19 @@ function threads.thread__rps(smem)
if msg ~= nil then if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command -- received a command
if plc_state.init_ok then
if msg.message == MQ__RPS_CMD.SCRAM then if msg.message == MQ__RPS_CMD.SCRAM then
-- SCRAM -- SCRAM
log.info("RPS: OS requested SCRAM")
rps.scram() rps.scram()
elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then
-- lost peripheral(s) -- lost peripheral(s)
log.info("RPS: received PLC degraded alert")
rps.trip_fault() rps.trip_fault()
elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then
-- watchdog tripped -- watchdog tripped
println_ts("RPS: supervisor timeout")
log.warning("RPS: received supervisor timeout alert")
rps.trip_timeout() rps.trip_timeout()
println_ts("server timeout")
log.warning("server timeout")
end
end end
elseif msg.qtype == mqueue.TYPE.DATA then elseif msg.qtype == mqueue.TYPE.DATA then
-- received data -- received data
@ -396,17 +368,17 @@ function threads.thread__rps(smem)
-- check for termination request -- check for termination request
if plc_state.shutdown then if plc_state.shutdown then
-- safe exit -- safe exit
log.info("rps thread shutdown initiated") log.info("OS: rps thread shutdown initiated")
if plc_state.init_ok then
if rps.scram() then if rps.scram() then
println_ts("reactor disabled") println_ts("exiting, reactor disabled")
log.info("rps thread reactor SCRAM OK") log.info("OS: rps thread reactor SCRAM OK on exit")
else else
println_ts("exiting, reactor failed to disable") println_ts("exiting, reactor failed to disable")
log.error("rps thread failed to SCRAM reactor on exit") log.error("OS: rps thread failed to SCRAM reactor on exit")
end end
end
log.info("rps thread exiting") log.info("OS: rps thread exiting")
break break
end end
@ -428,8 +400,8 @@ function threads.thread__rps(smem)
databus.tx_rt_status("rps", false) databus.tx_rt_status("rps", false)
if not plc_state.shutdown then 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...") log.info("OS: rps thread restarting in 5 seconds...")
util.psleep(5) util.psleep(5)
end end
end end
@ -448,7 +420,7 @@ function threads.thread__comms_tx(smem)
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("comms_tx", true) 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 -- load in from shared memory
local plc_state = smem.plc_state 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 while comms_queue.ready() and not plc_state.shutdown do
local msg = comms_queue.pop() 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 if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command -- received a command
if msg.message == MQ__COMM_CMD.SEND_STATUS then if msg.message == MQ__COMM_CMD.SEND_STATUS then
@ -486,7 +458,7 @@ function threads.thread__comms_tx(smem)
-- check for termination request -- check for termination request
if plc_state.shutdown then if plc_state.shutdown then
log.info("comms tx thread exiting") log.info("OS: comms tx thread exiting")
break break
end end
@ -508,7 +480,7 @@ function threads.thread__comms_tx(smem)
databus.tx_rt_status("comms_tx", false) databus.tx_rt_status("comms_tx", false)
if not plc_state.shutdown then 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) util.psleep(5)
end end
end end
@ -521,13 +493,16 @@ end
---@nodiscard ---@nodiscard
---@param smem plc_shared_memory ---@param smem plc_shared_memory
function threads.thread__comms_rx(smem) 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 ---@class parallel_thread
local public = {} local public = {}
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("comms_rx", true) 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 -- load in from shared memory
local plc_state = smem.plc_state 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 while comms_queue.ready() and not plc_state.shutdown do
local msg = comms_queue.pop() 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 if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command -- received a command
elseif msg.qtype == mqueue.TYPE.DATA then elseif msg.qtype == mqueue.TYPE.DATA then
@ -555,7 +530,7 @@ function threads.thread__comms_rx(smem)
-- received a packet -- received a packet
-- handle the packet (setpoints passed to update burn rate setpoint) -- handle the packet (setpoints passed to update burn rate setpoint)
-- (plc_state passed to check if degraded) -- (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
end end
@ -565,7 +540,7 @@ function threads.thread__comms_rx(smem)
-- check for termination request -- check for termination request
if plc_state.shutdown then if plc_state.shutdown then
log.info("comms rx thread exiting") log.info("OS: comms rx thread exiting")
break break
end end
@ -587,7 +562,7 @@ function threads.thread__comms_rx(smem)
databus.tx_rt_status("comms_rx", false) databus.tx_rt_status("comms_rx", false)
if not plc_state.shutdown then 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) util.psleep(5)
end end
end end
@ -606,7 +581,7 @@ function threads.thread__setpoint_control(smem)
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("spctl", true) databus.tx_rt_status("spctl", true)
log.debug("setpoint control thread start") log.debug("OS: setpoint control thread start")
-- load in from shared memory -- load in from shared memory
local plc_state = smem.plc_state 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 -- get reactor, may have changed do to disconnect/reconnect
local reactor = plc_dev.reactor local reactor = plc_dev.reactor
if plc_state.init_ok and (not plc_state.no_reactor) then if not plc_state.no_reactor then
---@cast reactor table won't be nil
-- check if we should start ramping -- check if we should start ramping
if setpoints.burn_rate_en and (setpoints.burn_rate ~= last_burn_sp) then if setpoints.burn_rate_en and (setpoints.burn_rate ~= last_burn_sp) then
local cur_burn_rate = reactor.getBurnRate() local cur_burn_rate = reactor.getBurnRate()
@ -698,7 +671,7 @@ function threads.thread__setpoint_control(smem)
-- check for termination request -- check for termination request
if plc_state.shutdown then if plc_state.shutdown then
log.info("setpoint control thread exiting") log.info("OS: setpoint control thread exiting")
break break
end end
@ -720,7 +693,7 @@ function threads.thread__setpoint_control(smem)
databus.tx_rt_status("spctl", false) databus.tx_rt_status("spctl", false)
if not plc_state.shutdown then 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) util.psleep(5)
end end
end end

View File

@ -205,7 +205,7 @@ function comms.scada_packet()
if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then
-- outside of maximum allowable transmission distance -- 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 else
if type(self.raw) == "table" then if type(self.raw) == "table" then
if #self.raw == 5 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 if (type(max_distance) == "number") and ((type(distance) ~= "number") or (distance > max_distance)) then
-- outside of maximum allowable transmission distance -- 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 else
if type(self.raw) == "table" then if type(self.raw) == "table" then
if #self.raw == 4 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 } self.raw = { self.txn_id, self.unit_id, self.func_code }
for i = 1, self.length do insert(self.raw, data[i]) end for i = 1, self.length do insert(self.raw, data[i]) end
else else
log.error("comms.modbus_packet.make(): data not a table") log.error("COMMS: modbus_packet.make(): data not a table")
end end
end end
@ -435,11 +435,11 @@ function comms.modbus_packet()
return size_ok and valid return size_ok and valid
else 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 return false
end end
else else
log.debug("nil frame encountered", true) log.debug("COMMS: nil frame encountered", true)
return false return false
end end
end end
@ -498,7 +498,7 @@ function comms.rplc_packet()
self.raw = { self.id, self.type } self.raw = { self.id, self.type }
for i = 1, #data do insert(self.raw, data[i]) end for i = 1, #data do insert(self.raw, data[i]) end
else else
log.error("comms.rplc_packet.make(): data not a table") log.error("COMMS: rplc_packet.make(): data not a table")
end end
end end
@ -521,11 +521,11 @@ function comms.rplc_packet()
return ok return ok
else 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 return false
end end
else else
log.debug("nil frame encountered", true) log.debug("COMMS: nil frame encountered", true)
return false return false
end end
end end
@ -580,7 +580,7 @@ function comms.mgmt_packet()
self.raw = { self.type } self.raw = { self.type }
for i = 1, #data do insert(self.raw, data[i]) end for i = 1, #data do insert(self.raw, data[i]) end
else else
log.error("comms.mgmt_packet.make(): data not a table") log.error("COMMS: mgmt_packet.make(): data not a table")
end end
end end
@ -601,11 +601,11 @@ function comms.mgmt_packet()
return ok return ok
else 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 return false
end end
else else
log.debug("nil frame encountered", true) log.debug("COMMS: nil frame encountered", true)
return false return false
end end
end end
@ -659,7 +659,7 @@ function comms.crdn_packet()
self.raw = { self.type } self.raw = { self.type }
for i = 1, #data do insert(self.raw, data[i]) end for i = 1, #data do insert(self.raw, data[i]) end
else else
log.error("comms.crdn_packet.make(): data not a table") log.error("COMMS: crdn_packet.make(): data not a table")
end end
end end
@ -680,11 +680,11 @@ function comms.crdn_packet()
return ok return ok
else 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 return false
end end
else else
log.debug("nil frame encountered", true) log.debug("COMMS: nil frame encountered", true)
return false return false
end end
end end

View File

@ -20,7 +20,7 @@ local MODE = { APPEND = 0, NEW = 1 }
log.MODE = MODE log.MODE = MODE
local logger = { local _log = {
not_ready = true, not_ready = true,
path = "/log.txt", path = "/log.txt",
mode = MODE.APPEND, mode = MODE.APPEND,
@ -42,36 +42,36 @@ local free_space = fs.getFreeSpace
---@param err_msg string|nil error message ---@param err_msg string|nil error message
---@return boolean out_of_space ---@return boolean out_of_space
local function check_out_of_space(err_msg) 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 end
-- private log write function -- private log write function
---@param msg_bits any[] ---@param msg_bits any[]
local function _log(msg_bits) local function write_log(msg_bits)
if logger.not_ready then return end if _log.not_ready then return end
local time_stamp = os.date(TIME_FMT) local time_stamp = os.date(TIME_FMT)
local stamped = util.c(time_stamp, table.unpack(msg_bits)) local stamped = util.c(time_stamp, table.unpack(msg_bits))
-- attempt to write log -- attempt to write log
local status, result = pcall(function () local status, result = pcall(function ()
logger.file.writeLine(stamped) _log.file.writeLine(stamped)
logger.file.flush() _log.file.flush()
end) end)
-- if we don't have space, we need to create a new log file -- if we don't have space, we need to create a new log file
if check_out_of_space() then if check_out_of_space() then
-- delete the old log file before opening a new one -- delete the old log file before opening a new one
logger.file.close() _log.file.close()
fs.delete(logger.path) fs.delete(_log.path)
-- re-init logger and pass dmesg_out so that it doesn't change -- 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 -- log the message and recycle warning
logger.file.writeLine(time_stamp .. WRN_TAG .. "recycled log file") _log.file.writeLine(time_stamp .. WRN_TAG .. "recycled log file")
logger.file.writeLine(stamped) _log.file.writeLine(stamped)
logger.file.flush() _log.file.flush()
elseif (not status) and (result ~= nil) then elseif (not status) and (result ~= nil) then
util.println("unexpected error writing to the log file: " .. result) util.println("unexpected error writing to the log file: " .. result)
end end
@ -89,45 +89,45 @@ end
function log.init(path, write_mode, include_debug, dmesg_redirect) function log.init(path, write_mode, include_debug, dmesg_redirect)
local err_msg local err_msg
logger.path = path _log.path = path
logger.mode = write_mode _log.mode = write_mode
logger.debug = include_debug _log.debug = include_debug
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 dmesg_redirect then if dmesg_redirect then
logger.dmesg_out = dmesg_redirect _log.dmesg_out = dmesg_redirect
else else
logger.dmesg_out = term.current() _log.dmesg_out = term.current()
end end
-- check for space issues -- check for space issues
local out_of_space = check_out_of_space(err_msg) local out_of_space = check_out_of_space(err_msg)
-- try to handle problems -- 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 out_of_space then
if fs.exists(logger.path) then if fs.exists(_log.path) then
fs.delete(logger.path) 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 if _log.file then
logger.file.writeLine(os.date(TIME_FMT) .. WRN_TAG .. "init recycled log file") _log.file.writeLine(os.date(TIME_FMT) .. WRN_TAG .. "init recycled log file")
logger.file.flush() _log.file.flush()
else error("failed to setup the log file: " .. err_msg) end 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("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 else error("unexpected error setting up the log file: " .. err_msg) end
end end
logger.not_ready = false _log.not_ready = false
end end
-- close the log file handle -- 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 -- direct dmesg output to a monitor/window
---@param window Window window or terminal reference ---@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 -- dmesg style logging for boot because I like linux-y things
---@param msg any message ---@param msg any message
@ -142,7 +142,7 @@ function log.dmesg(msg, tag, tag_color)
tag = util.strval(tag or "") tag = util.strval(tag or "")
local t_stamp = string.format("%12.2f", os.clock()) local t_stamp = string.format("%12.2f", os.clock())
local out = logger.dmesg_out local out = _log.dmesg_out
if out ~= nil then if out ~= nil then
local out_w, out_h = out.getSize() local out_w, out_h = out.getSize()
@ -180,7 +180,7 @@ function log.dmesg(msg, tag, tag_color)
if cur_y == out_h then if cur_y == out_h then
out.scroll(1) out.scroll(1)
out.setCursorPos(1, cur_y) out.setCursorPos(1, cur_y)
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1 _log.dmesg_scroll_count = _log.dmesg_scroll_count + 1
else else
out.setCursorPos(1, cur_y + 1) out.setCursorPos(1, cur_y + 1)
end end
@ -216,7 +216,7 @@ function log.dmesg(msg, tag, tag_color)
if cur_y == out_h then if cur_y == out_h then
out.scroll(1) out.scroll(1)
out.setCursorPos(1, cur_y) out.setCursorPos(1, cur_y)
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1 _log.dmesg_scroll_count = _log.dmesg_scroll_count + 1
else else
out.setCursorPos(1, cur_y + 1) out.setCursorPos(1, cur_y + 1)
end end
@ -225,9 +225,9 @@ function log.dmesg(msg, tag, tag_color)
out.write(lines[i]) out.write(lines[i])
end 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 end
return ts_coord return ts_coord
@ -241,9 +241,9 @@ end
---@return function update, function done ---@return function update, function done
function log.dmesg_working(msg, tag, tag_color) function log.dmesg_working(msg, tag, tag_color)
local ts_coord = log.dmesg(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 local width = (ts_coord.x2 - ts_coord.x1) + 1
if out ~= nil then if out ~= nil then
@ -252,7 +252,7 @@ function log.dmesg_working(msg, tag, tag_color)
local counter = 0 local counter = 0
local function update(sec_remaining) 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 if new_y < 1 then return end
local time = util.sprintf("%ds", sec_remaining) local time = util.sprintf("%ds", sec_remaining)
@ -280,11 +280,11 @@ function log.dmesg_working(msg, tag, tag_color)
counter = counter + 1 counter = counter + 1
out.setCursorPos(table.unpack(logger.dmesg_restore_coord)) out.setCursorPos(table.unpack(_log.dmesg_restore_coord))
end end
local function done(ok) 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 if new_y < 1 then return end
out.setCursorPos(ts_coord.x1, new_y) out.setCursorPos(ts_coord.x1, new_y)
@ -299,7 +299,7 @@ function log.dmesg_working(msg, tag, tag_color)
out.setTextColor(initial_color) out.setTextColor(initial_color)
out.setCursorPos(table.unpack(logger.dmesg_restore_coord)) out.setCursorPos(table.unpack(_log.dmesg_restore_coord))
end end
return update, done return update, done
@ -312,28 +312,28 @@ end
---@param msg any message ---@param msg any message
---@param trace? boolean include file trace ---@param trace? boolean include file trace
function log.debug(msg, trace) function log.debug(msg, trace)
if logger.debug then if _log.debug then
if trace then if trace then
local info = debug.getinfo(2) local info = debug.getinfo(2)
if info.name ~= nil then 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 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 end
else else
_log{DBG_TAG, msg} write_log{DBG_TAG, msg}
end end
end end
end end
-- log info messages -- log info messages
---@param msg any message ---@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 -- log warning messages
---@param msg any message ---@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 -- log error messages
---@param msg any message ---@param msg any message
@ -343,17 +343,17 @@ function log.error(msg, trace)
local info = debug.getinfo(2) local info = debug.getinfo(2)
if info.name ~= nil then 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 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 end
else else
_log{ERR_TAG, msg} write_log{ERR_TAG, msg}
end end
end end
-- log fatal errors -- log fatal errors
---@param msg any message ---@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 return log

View File

@ -1,9 +1,10 @@
-- --
-- Network Communications -- Network Communications and Message Authentication
-- --
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util") local util = require("scada-common.util")
local md5 = require("lockbox.digest.md5") local md5 = require("lockbox.digest.md5")
@ -17,7 +18,7 @@ local array = require("lockbox.util.array")
local network = {} local network = {}
-- cryptography engine -- cryptography engine
local c_eng = { local _crypt = {
key = nil, key = nil,
hmac = nil hmac = nil
} }
@ -39,23 +40,23 @@ function network.init_mac(passkey)
key_deriv.setPassword(passkey) key_deriv.setPassword(passkey)
key_deriv.finish() key_deriv.finish()
c_eng.key = array.fromHex(key_deriv.asHex()) _crypt.key = array.fromHex(key_deriv.asHex())
-- initialize HMAC -- initialize HMAC
c_eng.hmac = hmac() _crypt.hmac = hmac()
c_eng.hmac.setBlockSize(64) _crypt.hmac.setBlockSize(64)
c_eng.hmac.setDigest(md5) _crypt.hmac.setDigest(md5)
c_eng.hmac.setKey(c_eng.key) _crypt.hmac.setKey(_crypt.key)
local init_time = util.time_ms() - start 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 return init_time
end end
-- de-initialize message authentication system -- de-initialize message authentication system
function network.deinit_mac() function network.deinit_mac()
c_eng.key, c_eng.hmac = nil, nil _crypt.key, _crypt.hmac = nil, nil
end end
-- generate HMAC of message -- generate HMAC of message
@ -64,29 +65,41 @@ end
local function compute_hmac(message) local function compute_hmac(message)
-- local start = util.time_ms() -- local start = util.time_ms()
c_eng.hmac.init() _crypt.hmac.init()
c_eng.hmac.update(stream.fromString(message)) _crypt.hmac.update(stream.fromString(message))
c_eng.hmac.finish() _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 return hash
end end
-- NIC: Network Interface Controller<br> -- NIC: Network Interface Controller<br>
-- utilizes HMAC-MD5 for message authentication, if enabled -- 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) function network.nic(modem)
local self = { 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 = {} channels = {}
} }
---@class nic:Modem ---@class nic:Modem
local public = {} local public = {}
-- get the phy name
---@nodiscard
function public.phy_name() return self.name end
-- check if this NIC has a connected modem -- check if this NIC has a connected modem
---@nodiscard ---@nodiscard
function public.is_connected() return self.connected end function public.is_connected() return self.connected end
@ -95,9 +108,14 @@ function network.nic(modem)
---@param reconnected_modem Modem ---@param reconnected_modem Modem
function public.connect(reconnected_modem) function public.connect(reconnected_modem)
modem = 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 for _, channel in ipairs(self.channels) do
modem.open(channel) modem.open(channel)
end end
@ -117,13 +135,13 @@ function network.nic(modem)
function public.is_modem(device) return device == modem end function public.is_modem(device) return device == modem end
-- wrap modem functions, then create custom functions -- wrap modem functions, then create custom functions
public.connect(modem) if modem then public.connect(modem) end
-- open a channel on the modem<br> -- open a channel on the modem<br>
-- if disconnected *after* opening, previousy opened channels will be re-opened on reconnection -- if disconnected *after* opening, previousy opened channels will be re-opened on reconnection
---@param channel integer ---@param channel integer
function public.open(channel) function public.open(channel)
modem.open(channel) if modem then modem.open(channel) end
local already_open = false local already_open = false
for i = 1, #self.channels do for i = 1, #self.channels do
@ -141,7 +159,7 @@ function network.nic(modem)
-- close a channel on the modem -- close a channel on the modem
---@param channel integer ---@param channel integer
function public.close(channel) function public.close(channel)
modem.close(channel) if modem then modem.close(channel) end
for i = 1, #self.channels do for i = 1, #self.channels do
if self.channels[i] == channel then if self.channels[i] == channel then
@ -153,7 +171,7 @@ function network.nic(modem)
-- close all channels on the modem -- close all channels on the modem
function public.closeAll() function public.closeAll()
modem.closeAll() if modem then modem.closeAll() end
self.channels = {} self.channels = {}
end end
@ -165,17 +183,20 @@ function network.nic(modem)
if self.connected then if self.connected then
local tx_packet = packet ---@type authd_packet|scada_packet 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() -- local start = util.time_ms()
tx_packet = comms.authd_packet() tx_packet = comms.authd_packet()
---@cast tx_packet authd_packet ---@cast tx_packet authd_packet
tx_packet.make(packet, compute_hmac) 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 end
---@diagnostic disable-next-line: need-check-nil
modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable()) modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable())
else
log.debug("NET: network.transmit tx dropped, link is down")
end end
end end
@ -190,10 +211,10 @@ function network.nic(modem)
function public.receive(side, sender, reply_to, message, distance) function public.receive(side, sender, reply_to, message, distance)
local packet = nil local packet = nil
if self.connected then if self.connected and side == self.iface then
local s_packet = comms.scada_packet() local s_packet = comms.scada_packet()
if c_eng.hmac ~= nil then if self.use_hash then
-- parse packet as an authenticated SCADA packet -- parse packet as an authenticated SCADA packet
local a_packet = comms.authd_packet() local a_packet = comms.authd_packet()
a_packet.receive(side, sender, reply_to, message, distance) 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 })) local computed_hmac = compute_hmac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true }))
if a_packet.mac() == computed_hmac then 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() s_packet.stamp_authenticated()
else 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 end
end end

View File

@ -22,7 +22,7 @@ ppm.VIRTUAL_DEVICE_TYPE = VIRTUAL_DEVICE_TYPE
local REPORT_FREQUENCY = 20 -- log every 20 faults per function local REPORT_FREQUENCY = 20 -- log every 20 faults per function
local ppm_sys = { local _ppm = {
mounts = {}, ---@type { [string]: ppm_entry } mounts = {}, ---@type { [string]: ppm_entry }
next_vid = 0, next_vid = 0,
auto_cf = false, auto_cf = false,
@ -66,7 +66,7 @@ local function peri_init(iface)
if status then if status then
-- auto fault clear -- auto fault clear
if self.auto_cf then self.faulted = false end 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 self.fault_counts[key] = 0
@ -78,10 +78,10 @@ local function peri_init(iface)
self.faulted = true self.faulted = true
self.last_fault = result self.last_fault = result
ppm_sys.faulted = true _ppm.faulted = true
ppm_sys.last_fault = result _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 = "" local count_str = ""
if self.fault_counts[key] > 0 then if self.fault_counts[key] > 0 then
count_str = " [" .. self.fault_counts[key] .. " total faults]" 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 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 return ACCESS_FAULT, result
end end
@ -159,10 +159,10 @@ local function peri_init(iface)
self.faulted = true self.faulted = true
self.last_fault = UNDEFINED_FIELD self.last_fault = UNDEFINED_FIELD
ppm_sys.faulted = true _ppm.faulted = true
ppm_sys.last_fault = UNDEFINED_FIELD _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 = "" local count_str = ""
if self.fault_counts[key] > 0 then if self.fault_counts[key] > 0 then
count_str = " [" .. self.fault_counts[key] .. " total calls]" count_str = " [" .. self.fault_counts[key] .. " total calls]"
@ -193,35 +193,35 @@ end
-- REPORTING -- -- REPORTING --
-- silence error prints -- silence error prints
function ppm.disable_reporting() ppm_sys.mute = true end function ppm.disable_reporting() _ppm.mute = true end
-- allow error prints -- allow error prints
function ppm.enable_reporting() ppm_sys.mute = false end function ppm.enable_reporting() _ppm.mute = false end
-- FAULT MEMORY -- -- FAULT MEMORY --
-- enable automatically clearing fault flag -- 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 -- 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 -- clear fault flag
function ppm.clear_fault() ppm_sys.faulted = false end function ppm.clear_fault() _ppm.faulted = false end
-- check fault flag -- check fault flag
---@nodiscard ---@nodiscard
function ppm.is_faulted() return ppm_sys.faulted end function ppm.is_faulted() return _ppm.faulted end
-- get the last fault message -- get the last fault message
---@nodiscard ---@nodiscard
function ppm.get_last_fault() return ppm_sys.last_fault end function ppm.get_last_fault() return _ppm.last_fault end
-- TERMINATION -- -- TERMINATION --
-- if a caught error was a termination request -- if a caught error was a termination request
---@nodiscard ---@nodiscard
function ppm.should_terminate() return ppm_sys.terminate end function ppm.should_terminate() return _ppm.terminate end
-- MOUNTING -- -- MOUNTING --
@ -229,12 +229,12 @@ function ppm.should_terminate() return ppm_sys.terminate end
function ppm.mount_all() function ppm.mount_all()
local ifaces = peripheral.getNames() local ifaces = peripheral.getNames()
ppm_sys.mounts = {} _ppm.mounts = {}
for i = 1, #ifaces do 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 end
if #ifaces == 0 then if #ifaces == 0 then
@ -253,10 +253,10 @@ function ppm.mount(iface)
for i = 1, #ifaces do for i = 1, #ifaces do
if iface == ifaces[i] then 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_type = _ppm.mounts[iface].type
pm_dev = ppm_sys.mounts[iface].dev pm_dev = _ppm.mounts[iface].dev
log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type)) log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type))
break break
@ -278,12 +278,12 @@ function ppm.remount(iface)
for i = 1, #ifaces do for i = 1, #ifaces do
if iface == ifaces[i] then if iface == ifaces[i] then
log.info(util.c("PPM: remount(", iface, ") -> is a ", pm_type)) 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_type = _ppm.mounts[iface].type
pm_dev = ppm_sys.mounts[iface].dev pm_dev = _ppm.mounts[iface].dev
log.info(util.c("PPM: remount(", iface, ") -> remounted a ", pm_type)) log.info(util.c("PPM: remount(", iface, ") -> remounted a ", pm_type))
break break
@ -297,24 +297,24 @@ end
---@nodiscard ---@nodiscard
---@return string type, table device ---@return string type, table device
function ppm.mount_virtual() 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.mounts[iface] = peri_init("__virtual__")
ppm_sys.next_vid = ppm_sys.next_vid + 1 _ppm.next_vid = _ppm.next_vid + 1
log.info(util.c("PPM: mount_virtual() -> allocated new virtual device ", iface)) 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 end
-- manually unmount a peripheral from the PPM -- manually unmount a peripheral from the PPM
---@param device table device table ---@param device table device table
function ppm.unmount(device) function ppm.unmount(device)
if device then 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 if data.dev == device then
log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", iface)) log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", iface))
ppm_sys.mounts[iface] = nil _ppm.mounts[iface] = nil
break break
end end
end end
@ -330,7 +330,7 @@ function ppm.handle_unmount(iface)
local pm_type = nil local pm_type = nil
-- what got disconnected? -- what got disconnected?
local lost_dev = ppm_sys.mounts[iface] local lost_dev = _ppm.mounts[iface]
if lost_dev then if lost_dev then
pm_type = lost_dev.type 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)) log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface))
end end
ppm_sys.mounts[iface] = nil _ppm.mounts[iface] = nil
return pm_type, pm_dev return pm_type, pm_dev
end end
-- log all mounts, to be used if `ppm.mount_all` is called before logging is ready -- log all mounts, to be used if `ppm.mount_all` is called before logging is ready
function ppm.log_mounts() 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, ")")) log.info(util.c("PPM: had found a ", mount.type, " (", iface, ")"))
end 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") log.warning("PPM: no devices had been found")
end end
end end
@ -369,7 +369,7 @@ function ppm.list_avail() return peripheral.getNames() end
---@return { [string]: ppm_entry } mounts ---@return { [string]: ppm_entry } mounts
function ppm.list_mounts() function ppm.list_mounts()
local list = {} 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 return list
end end
@ -379,7 +379,7 @@ end
---@return string|nil iface CC peripheral interface ---@return string|nil iface CC peripheral interface
function ppm.get_iface(device) function ppm.get_iface(device)
if device then 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 if data.dev == device then return iface end
end end
end end
@ -392,8 +392,8 @@ end
---@param iface string CC peripheral interface ---@param iface string CC peripheral interface
---@return { [string]: function }|nil device function table ---@return { [string]: function }|nil device function table
function ppm.get_periph(iface) function ppm.get_periph(iface)
if ppm_sys.mounts[iface] then if _ppm.mounts[iface] then
return ppm_sys.mounts[iface].dev return _ppm.mounts[iface].dev
else return nil end else return nil end
end end
@ -402,8 +402,8 @@ end
---@param iface string CC peripheral interface ---@param iface string CC peripheral interface
---@return string|nil type ---@return string|nil type
function ppm.get_type(iface) function ppm.get_type(iface)
if ppm_sys.mounts[iface] then if _ppm.mounts[iface] then
return ppm_sys.mounts[iface].type return _ppm.mounts[iface].type
else return nil end else return nil end
end end
@ -414,7 +414,7 @@ end
function ppm.get_all_devices(name) function ppm.get_all_devices(name)
local devices = {} local devices = {}
for _, data in pairs(ppm_sys.mounts) do for _, data in pairs(_ppm.mounts) do
if data.type == name then if data.type == name then
table.insert(devices, data.dev) table.insert(devices, data.dev)
end end
@ -430,7 +430,7 @@ end
function ppm.get_device(name) function ppm.get_device(name)
local device = nil local device = nil
for _, data in pairs(ppm_sys.mounts) do for _, data in pairs(_ppm.mounts) do
if data.type == name then if data.type == name then
device = data.dev device = data.dev
break break
@ -455,7 +455,7 @@ function ppm.get_wireless_modem()
local w_modem = nil local w_modem = nil
local emulated_env = periphemu ~= 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 if device.type == "modem" and (emulated_env or device.dev.isWireless()) then
w_modem = device.dev w_modem = device.dev
break break
@ -471,7 +471,7 @@ end
function ppm.get_monitor_list() function ppm.get_monitor_list()
local 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 if device.type == "monitor" then list[iface] = device end
end end

View File

@ -24,7 +24,7 @@ local t_pack = table.pack
local util = {} local util = {}
-- scada-common version -- scada-common version
util.version = "1.5.4" util.version = "1.5.5"
util.TICK_TIME_S = 0.05 util.TICK_TIME_S = 0.05
util.TICK_TIME_MS = 50 util.TICK_TIME_MS = 50