-- -- Reactor Programmable Logic Controller -- require("/initenv").init_env() 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 rsio = require("scada-common.rsio") local util = require("scada-common.util") local config = require("reactor-plc.config") 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.5.1" local println = util.println local println_ts = util.println_ts ---------------------------------------- -- config validation ---------------------------------------- local cfv = util.new_validator() cfv.assert_type_bool(config.NETWORKED) cfv.assert_type_int(config.REACTOR_ID) cfv.assert_channel(config.SVR_CHANNEL) cfv.assert_channel(config.PLC_CHANNEL) cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_num(config.COMMS_TIMEOUT) cfv.assert_min(config.COMMS_TIMEOUT, 2) cfv.assert_type_str(config.LOG_PATH) cfv.assert_type_int(config.LOG_MODE) assert(cfv.valid(), "bad config file: missing/invalid fields") -- check emergency coolant configuration if type(config.EMERGENCY_COOL) == "table" then if not rsio.is_valid_side(config.EMERGENCY_COOL.side) then assert(false, "bad config file: emergency coolant side unrecognized") elseif config.EMERGENCY_COOL.color ~= nil and not rsio.is_color(config.EMERGENCY_COOL.color) then assert(false, "bad config file: emergency coolant invalid redstone channel color provided") end end ---------------------------------------- -- log init ---------------------------------------- log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true) log.info("========================================") log.info("BOOTING reactor-plc.startup " .. R_PLC_VERSION) log.info("========================================") println(">> Reactor PLC " .. R_PLC_VERSION .. " <<") crash.set_env("plc", R_PLC_VERSION) ---------------------------------------- -- main application ---------------------------------------- local function main() ---------------------------------------- -- startup ---------------------------------------- -- record firmware versions and ID databus.tx_versions(R_PLC_VERSION, comms.version) databus.tx_id(config.REACTOR_ID) -- mount connected devices ppm.mount_all() -- message authentication init if type(config.AUTH_KEY) == "string" then network.init_mac(config.AUTH_KEY) end -- shared memory across threads ---@class plc_shared_memory local __shared_memory = { -- networked setting networked = config.NETWORKED, ---@type boolean -- PLC system state flags ---@class plc_state plc_state = { init_ok = true, fp_ok = false, shutdown = false, degraded = true, reactor_formed = true, no_reactor = true, no_modem = true }, -- control setpoints ---@class setpoints setpoints = { burn_rate_en = false, burn_rate = 0.0 }, -- core PLC devices plc_dev = { reactor = ppm.get_fission_reactor(), modem = ppm.get_wireless_modem() }, -- system objects 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() } } local smem_dev = __shared_memory.plc_dev local smem_sys = __shared_memory.plc_sys local plc_state = __shared_memory.plc_state -- initial state evaluation plc_state.no_reactor = smem_dev.reactor == nil plc_state.no_modem = smem_dev.modem == nil -- we need a reactor, can at least do some things even if it isn't formed though if plc_state.no_reactor then println("init> fission reactor not found"); log.warning("init> no reactor on startup") plc_state.init_ok = false plc_state.degraded = true elseif not smem_dev.reactor.isFormed() then println("init> fission reactor not formed"); log.warning("init> reactor logic adapter present, but reactor is not formed") plc_state.degraded = true plc_state.reactor_formed = false end -- modem is required if networked if __shared_memory.networked and plc_state.no_modem then println("init> wireless modem not found") log.warning("init> no wireless modem on startup") -- scram reactor if present and enabled if (smem_dev.reactor ~= nil) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then smem_dev.reactor.scram() end plc_state.init_ok = false plc_state.degraded = true end -- print a log message to the terminal as long as the UI isn't running local function _println_no_fp(message) if not plc_state.fp_ok then println(message) end end -- PLC init
--- EVENT_CONSUMER: this function consumes events local function init() -- just booting up, no fission allowed (neutrons stay put thanks) if (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then smem_dev.reactor.scram() end -- front panel time! if not renderer.ui_ready() then local message plc_state.fp_ok, message = pcall(renderer.start_ui) if not plc_state.fp_ok then renderer.close_ui() 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, config.EMERGENCY_COOL) log.debug("init> rps init") if __shared_memory.networked then -- comms watchdog smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT) 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(config.REACTOR_ID, R_PLC_VERSION, smem_sys.nic, config.PLC_CHANNEL, config.SVR_CHANNEL, config.TRUSTED_RANGE, 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.EMERGENCY_COOL ~= nil 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 ---------------------------------------- -- start system ---------------------------------------- -- initialize PLC init() -- init threads local main_thread = threads.thread__main(__shared_memory, init) 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) if plc_state.init_ok then -- send status one last time after RPS shutdown smem_sys.plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed) smem_sys.plc_comms.send_rps_status() -- close connection smem_sys.plc_comms.close() end 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