-- -- Nuclear Generation Facility SCADA Coordinator -- 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 util = require("scada-common.util") local configure = require("coordinator.configure") local coordinator = require("coordinator.coordinator") local iocontrol = require("coordinator.iocontrol") local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") local threads = require("coordinator.threads") local COORDINATOR_VERSION = "v1.4.3" local CHUNK_LOAD_DELAY_S = 30.0 local println = util.println local println_ts = util.println_ts local log_render = coordinator.log_render local log_sys = coordinator.log_sys local log_boot = coordinator.log_boot local log_comms = coordinator.log_comms local log_crypto = coordinator.log_crypto ---------------------------------------- -- get configuration ---------------------------------------- -- mount connected devices (required for monitor setup) ppm.mount_all() local wait_on_load = true local loaded, monitors = coordinator.load_config() -- if the computer just started, its chunk may have just loaded (...or the user rebooted) -- if monitor config failed, maybe an adjacent chunk containing all or part of a monitor has not loaded yet, so keep trying while wait_on_load and loaded == 2 and os.clock() < CHUNK_LOAD_DELAY_S do term.clear() term.setCursorPos(1, 1) println("There was a monitor configuration problem at boot.\n") println("Startup will keep trying every 2s in case of chunk load delays.\n") println(util.sprintf("The configurator will be started in %ds if all attempts fail.\n", math.max(0, CHUNK_LOAD_DELAY_S - os.clock()))) println("(click to skip to the configurator)") local timer_id = util.start_timer(2) while true do local event, param1 = util.pull_event() if event == "timer" and param1 == timer_id then -- remount and re-attempt ppm.mount_all() loaded, monitors = coordinator.load_config() break elseif event == "mouse_click" or event == "terminate" then wait_on_load = false break end end end if loaded ~= 0 then -- try to reconfigure (user action) local success, error = configure.configure(loaded, monitors) if success then loaded, monitors = coordinator.load_config() if loaded ~= 0 then println(util.trinary(loaded == 2, "monitor configuration invalid", "failed to load a valid configuration") .. ", please reconfigure") return end else println("configuration error: " .. error) return end end -- passed checks, good now ---@cast monitors monitors_struct local config = coordinator.config ---------------------------------------- -- log init ---------------------------------------- log.init(config.LogPath, config.LogMode, config.LogDebug) log.info("========================================") log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION) log.info("========================================") println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<") crash.set_env("coordinator", COORDINATOR_VERSION) crash.dbg_log_env() ---------------------------------------- -- main application ---------------------------------------- local function main() ---------------------------------------- -- system startup ---------------------------------------- -- log mounts now since mounting was done before logging was ready ppm.log_mounts() -- report versions/init fp PSIL iocontrol.init_fp(COORDINATOR_VERSION, comms.version) -- init renderer renderer.configure(config) renderer.set_displays(monitors) renderer.init_displays() renderer.init_dmesg() -- lets get started! log.info("monitors ready, dmesg output incoming...") log_render("displays connected and reset") log_sys("system start on " .. os.date("%c")) log_boot("starting " .. COORDINATOR_VERSION) ---------------------------------------- -- memory allocation ---------------------------------------- -- shared memory across threads ---@class crd_shared_memory local __shared_memory = { -- time and date format for display date_format = util.trinary(config.Time24Hour, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y"), -- coordinator system state flags ---@class crd_state crd_state = { fp_ok = false, ui_ok = true, -- default true, used to abort on fail link_fail = false, shutdown = false }, -- core coordinator devices crd_dev = { speaker = ppm.get_device("speaker"), modem = ppm.get_wireless_modem() }, -- system objects crd_sys = { nic = nil, ---@type nic coord_comms = nil, ---@type coord_comms conn_watchdog = nil ---@type watchdog }, -- message queues q = { mq_render = mqueue.new() } } local smem_dev = __shared_memory.crd_dev local smem_sys = __shared_memory.crd_sys local crd_state = __shared_memory.crd_state ---------------------------------------- -- setup alarm sounder subsystem ---------------------------------------- if smem_dev.speaker == nil then log_boot("annunciator alarm speaker not found") println("startup> speaker not found") log.fatal("no annunciator alarm speaker found") return else local sounder_start = util.time_ms() log_boot("annunciator alarm speaker connected") sounder.init(smem_dev.speaker, config.SpeakerVolume) log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms") log_sys("annunciator alarm configured") iocontrol.fp_has_speaker(true) end ---------------------------------------- -- setup communications ---------------------------------------- -- message authentication init if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then local init_time = network.init_mac(config.AuthKey) log_crypto("HMAC init took " .. init_time .. "ms") end -- get the communications modem if smem_dev.modem == nil then log_comms("wireless modem not found") println("startup> wireless modem not found") log.fatal("no wireless modem on startup") return else log_comms("wireless modem connected") iocontrol.fp_has_modem(true) end -- create connection watchdog smem_sys.conn_watchdog = util.new_watchdog(config.SVR_Timeout) smem_sys.conn_watchdog.cancel() log.debug("startup> conn watchdog created") -- create network interface then setup comms smem_sys.nic = network.nic(smem_dev.modem) smem_sys.coord_comms = coordinator.comms(COORDINATOR_VERSION, smem_sys.nic, smem_sys.conn_watchdog) log.debug("startup> comms init") log_comms("comms initialized") ---------------------------------------- -- start front panel ---------------------------------------- log_render("starting front panel UI...") local fp_message crd_state.fp_ok, fp_message = renderer.try_start_fp() if not crd_state.fp_ok then log_render(util.c("front panel UI error: ", fp_message)) println_ts("front panel UI creation failed") log.fatal(util.c("front panel GUI render failed with error ", fp_message)) return else log_render("front panel ready") end ---------------------------------------- -- start system ---------------------------------------- -- init threads local main_thread = threads.thread__main(__shared_memory) local render_thread = threads.thread__render(__shared_memory) log.info("startup> completed") -- run threads parallel.waitForAll(main_thread.p_exec, render_thread.p_exec) renderer.close_ui() renderer.close_fp() sounder.stop() log_sys("system shutdown") if crd_state.link_fail then println_ts("failed to connect to supervisor") end if not crd_state.ui_ok then println_ts("main UI creation failed") end -- close on error exit (such as UI error) if smem_sys.coord_comms.is_linked() then smem_sys.coord_comms.close() end println_ts("exited") log.info("exited") end if not xpcall(main, crash.handler) then pcall(renderer.close_ui) pcall(renderer.close_fp) pcall(sounder.stop) crash.exit() else log.close() end