2025-10-26 22:19:37 -04:00

243 lines
7.3 KiB
Lua

--
-- Reactor Programmable Logic Controller
--
require("/initenv").init_env()
local backplane = require("reactor-plc.backplane")
local comms = require("scada-common.comms")
local crash = require("scada-common.crash")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local configure = require("reactor-plc.configure")
local databus = require("reactor-plc.databus")
local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "v1.9.1"
local println = util.println
local println_ts = util.println_ts
----------------------------------------
-- get configuration
----------------------------------------
if not plc.load_config() then
-- try to reconfigure (user action)
local success, error = configure.configure(true)
if success then
if not plc.load_config() then
println("failed to load a valid configuration, please reconfigure")
return
end
else
println("configuration error: " .. error)
return
end
end
local config = plc.config
----------------------------------------
-- log init
----------------------------------------
log.init(config.LogPath, config.LogMode, config.LogDebug)
log.info("========================================")
log.info("BOOTING reactor-plc.startup " .. R_PLC_VERSION)
log.info("========================================")
println(">> Reactor PLC " .. R_PLC_VERSION .. " <<")
crash.set_env("reactor-plc", R_PLC_VERSION)
crash.dbg_log_env()
----------------------------------------
-- main application
----------------------------------------
local function main()
----------------------------------------
-- startup
----------------------------------------
-- record firmware versions and ID
databus.tx_versions(R_PLC_VERSION, comms.version)
databus.tx_id(config.UnitID)
-- mount connected devices
ppm.mount_all()
-- message authentication init
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
network.init_mac(config.AuthKey)
end
-- shared memory across threads
---@class plc_shared_memory
local __shared_memory = {
-- networked setting
networked = config.Networked,
-- PLC system state flags
---@class plc_state
plc_state = {
fp_ok = false,
shutdown = false,
degraded = true,
no_reactor = true,
reactor_formed = true,
wd_modem = false,
wl_modem = false
},
-- control setpoints
---@class plc_setpoints
setpoints = {
burn_rate_en = false,
burn_rate = 0.0
},
-- global PLC devices, still initialized by the backplane
---@class plc_dev
plc_dev = {
reactor = nil ---@type table
},
-- system objects
---@class plc_sys
plc_sys = {
rps = nil, ---@type rps
nic = nil, ---@type nic
plc_comms = nil, ---@type plc_comms
conn_watchdog = nil ---@type watchdog
},
-- message queues
q = {
mq_rps = mqueue.new(),
mq_comms_tx = mqueue.new(),
mq_comms_rx = mqueue.new()
},
-- message queue commands
q_cmds = {
MQ__RPS_CMD = {
SCRAM = 1,
DEGRADED_SCRAM = 2,
TRIP_TIMEOUT = 3
},
MQ__COMM_CMD = {
SEND_STATUS = 1
}
}
}
local smem_dev = __shared_memory.plc_dev
local smem_sys = __shared_memory.plc_sys
local plc_state = __shared_memory.plc_state
-- reactor and modem initialization
backplane.init(config, __shared_memory)
-- scram on boot if networked, otherwise leave the reactor be
if __shared_memory.networked and (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
log.debug("startup> power-on SCRAM")
smem_dev.reactor.scram()
end
-- setup front panel
local message
plc_state.fp_ok, message = renderer.try_start_ui(config)
-- ...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
----------------------------------------
-- initialize PLC
----------------------------------------
-- init reactor protection system
smem_sys.rps = plc.rps_init(smem_dev.reactor, util.trinary(plc_state.no_reactor, nil, plc_state.reactor_formed))
log.debug("startup> rps init")
-- notify user of emergency coolant configuration status
if config.EmerCoolEnable then
_println_no_fp("startup> emergency coolant control ready")
log.info("startup> emergency coolant control available")
end
-- conditionally init comms
if __shared_memory.networked then
-- comms watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("startup> conn watchdog started")
-- create network interface then setup comms
smem_sys.nic = backplane.active_nic()
smem_sys.plc_comms = plc.comms(R_PLC_VERSION, smem_sys.nic, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
log.debug("startup> comms init")
else
_println_no_fp("startup> starting in non-networked mode")
log.info("startup> starting without networking")
end
databus.tx_hw_status(plc_state)
_println_no_fp("startup> completed")
log.info("startup> completed")
-- init threads
local main_thread = threads.thread__main(__shared_memory)
local rps_thread = threads.thread__rps(__shared_memory)
if __shared_memory.networked then
-- init comms threads
local comms_thread_tx = threads.thread__comms_tx(__shared_memory)
local comms_thread_rx = threads.thread__comms_rx(__shared_memory)
-- setpoint control only needed when networked
local sp_ctrl_thread = threads.thread__setpoint_control(__shared_memory)
-- 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)
-- send status one last time after RPS shutdown
smem_sys.plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed)
smem_sys.plc_comms.send_rps_status()
-- close connection
smem_sys.plc_comms.close()
else
-- run threads, excluding comms
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec)
end
renderer.close_ui()
println_ts("exited")
log.info("exited")
end
if not xpcall(main, crash.handler) then
pcall(renderer.close_ui)
crash.exit()
else
log.close()
end