#640 reworked PLC initialization

This commit is contained in:
Mikayla 2025-10-23 23:13:13 +00:00
parent f7fe9754fe
commit a083f8983b
7 changed files with 300 additions and 304 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.green,colors.red)}
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

@ -707,6 +707,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
@ -748,8 +854,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
---@param formed boolean reactor formed (from PLC state) ---@param formed boolean reactor formed (from PLC state)
function public.send_status(no_reactor, formed) function public.send_status(no_reactor, formed)
if self.linked then if self.linked then
local mek_data = nil ---@type table local mek_data = nil ---@type table
local heating_rate = 0.0 ---@type number local heating_rate = 0.0 ---@type number
if (not no_reactor) and rps.is_formed() then if (not no_reactor) and rps.is_formed() then
if _update_status_cache() then mek_data = self.status_cache end if _update_status_cache() then mek_data = self.status_cache end
@ -803,15 +909,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 +925,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 +966,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 +995,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 -- scram on boot if networked, otherwise leave the reactor be
local function _println_no_fp(message) if not plc_state.fp_ok then println(message) end end 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")
-- PLC init<br> smem_dev.reactor.scram()
--- 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)
end 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 reactor protection system
init() smem_sys.rps = plc.rps_init(smem_dev.reactor, 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 -- 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("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()
@ -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,10 +161,8 @@ 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")
@ -196,29 +194,27 @@ 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)
if networked then if networked then
plc_comms.reconnect_reactor(plc_dev.reactor) 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()
end end
-- partial reset of RPS, specific to becoming formed/reconnected
-- without this, auto control can't resume on chunk load
rps.reset_formed()
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
@ -226,29 +222,19 @@ function threads.thread__main(smem, init)
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
@ -278,7 +264,6 @@ function threads.thread__main(smem, init)
-- 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("main thread restarting now...")
util.push_event("clock_start")
end end
end end
end end
@ -316,47 +301,39 @@ 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 do to disconnect/reconnect
local reactor = plc_dev.reactor local reactor = plc_dev.reactor
-- RPS checks -- SCRAM if no open connection
if plc_state.init_ok then if networked and not plc_comms.is_linked() then
-- SCRAM if no open connection if was_linked then
if networked and not plc_comms.is_linked() then was_linked = false
if was_linked then rps.trip_timeout()
was_linked = false
rps.trip_timeout()
end
else
was_linked = true
end end
else was_linked = true end
if (not plc_state.no_reactor) and rps.is_formed() then -- check reactor status
-- check reactor status if (not plc_state.no_reactor) and rps.is_formed() then
---@diagnostic disable-next-line: need-check-nil 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
-- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable -- 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 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 if not plc_state.no_reactor then
local rps_tripped, rps_status_string, rps_first = rps.check() 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! safety trip: " .. rps_status_string)
if networked and not plc_state.no_modem then if networked and not plc_state.no_modem then
plc_comms.send_rps_alarm(rps_status_string) plc_comms.send_rps_alarm(rps_status_string)
end
end end
end end
end end
@ -368,19 +345,17 @@ 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 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) 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 rps.trip_timeout()
rps.trip_timeout() println_ts("supervisor timeout")
println_ts("server timeout") log.warning("supervisor 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
@ -397,15 +372,15 @@ function threads.thread__rps(smem)
if plc_state.shutdown then if plc_state.shutdown then
-- safe exit -- safe exit
log.info("rps thread shutdown initiated") log.info("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("rps thread reactor SCRAM OK")
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("rps thread failed to SCRAM reactor on exit")
end
end end
log.info("rps thread exiting") log.info("rps thread exiting")
break break
end end
@ -428,7 +403,7 @@ 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("rps thread restarting in 5 seconds...")
util.psleep(5) util.psleep(5)
end end
@ -465,7 +440,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
@ -521,6 +496,9 @@ 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 = {}
@ -546,7 +524,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 +533,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
@ -629,9 +607,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()

View File

@ -4,6 +4,7 @@
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")
@ -76,17 +77,29 @@ local function compute_hmac(message)
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 = c_eng.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,7 +183,7 @@ 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()
@ -175,7 +193,10 @@ function network.nic(modem)
-- log.debug("network.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms") -- log.debug("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("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)

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