From 9761228b8eb87848f848387b5c553299f3f4f95a Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 13 Nov 2022 15:56:27 -0500 Subject: [PATCH] #124 debug stack trace on error --- .vscode/settings.json | 3 +- coordinator/startup.lua | 447 +++++++++++++++--------------- reactor-plc/startup.lua | 277 ++++++++++--------- rtu/startup.lua | 591 ++++++++++++++++++++-------------------- scada-common/crash.lua | 46 ++++ supervisor/startup.lua | 177 ++++++------ 6 files changed, 816 insertions(+), 725 deletions(-) create mode 100644 scada-common/crash.lua diff --git a/.vscode/settings.json b/.vscode/settings.json index 46b3a86..0cc34d6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,6 +13,7 @@ "window", "read", "periphemu", - "mekanismEnergyHelper" + "mekanismEnergyHelper", + "_HOST" ] } diff --git a/coordinator/startup.lua b/coordinator/startup.lua index e7028e7..1289dd8 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -4,6 +4,7 @@ require("/initenv").init_env() +local crash = require("scada-common.crash") local log = require("scada-common.log") local ppm = require("scada-common.ppm") local tcallbackdsp = require("scada-common.tcallbackdsp") @@ -16,7 +17,7 @@ local config = require("coordinator.config") local coordinator = require("coordinator.coordinator") local renderer = require("coordinator.renderer") -local COORDINATOR_VERSION = "alpha-v0.6.11" +local COORDINATOR_VERSION = "alpha-v0.6.12" local print = util.print local println = util.println @@ -57,262 +58,272 @@ log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION) log.info("========================================") println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<") ----------------------------------------- --- system startup ----------------------------------------- - --- mount connected devices -ppm.mount_all() - --- setup monitors -local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS) -if not configured or monitors == nil then - println("boot> monitor setup failed") - log.fatal("monitor configuration failed") - return -end - -log.info("monitors ready, dmesg output incoming...") - --- init renderer -renderer.set_displays(monitors) -renderer.reset(config.RECOLOR) -renderer.init_dmesg() - -log_graphics("displays connected and reset") -log_sys("system start on " .. os.date("%c")) -log_boot("starting " .. COORDINATOR_VERSION) +crash.set_env("coordinator", COORDINATOR_VERSION) ---------------------------------------- --- setup communications +-- main application ---------------------------------------- --- get the communications modem -local modem = ppm.get_wireless_modem() -if modem == nil then - log_comms("wireless modem not found") - println("boot> wireless modem not found") - log.fatal("no wireless modem on startup") - return -else - log_comms("wireless modem connected") -end +local function main() + ---------------------------------------- + -- system startup + ---------------------------------------- --- create connection watchdog -local conn_watchdog = util.new_watchdog(5) -conn_watchdog.cancel() -log.debug("boot> conn watchdog created") + -- mount connected devices + ppm.mount_all() --- start comms, open all channels -local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.SCADA_SV_PORT, config.SCADA_SV_LISTEN, config.SCADA_API_LISTEN, conn_watchdog) -log.debug("boot> comms init") -log_comms("comms initialized") - --- base loop clock (2Hz, 10 ticks) -local MAIN_CLOCK = 0.5 -local loop_clock = util.new_clock(MAIN_CLOCK) - ----------------------------------------- --- connect to the supervisor ----------------------------------------- - --- attempt to connect to the supervisor or exit -local function init_connect_sv() - local tick_waiting, task_done = log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SCADA_SV_PORT) - - -- attempt to establish a connection with the supervisory computer - if not coord_comms.sv_connect(60, tick_waiting, task_done) then - log_comms("supervisor connection failed") - log.fatal("failed to connect to supervisor") - return false + -- setup monitors + local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS) + if not configured or monitors == nil then + println("boot> monitor setup failed") + log.fatal("monitor configuration failed") + return end - return true -end + log.info("monitors ready, dmesg output incoming...") -if not init_connect_sv() then - println("boot> failed to connect to supervisor") - log_sys("system shutdown") - return -else - log_sys("supervisor connected, proceeding to UI start") -end + -- init renderer + renderer.set_displays(monitors) + renderer.reset(config.RECOLOR) + renderer.init_dmesg() ----------------------------------------- --- start the UI ----------------------------------------- + log_graphics("displays connected and reset") + log_sys("system start on " .. os.date("%c")) + log_boot("starting " .. COORDINATOR_VERSION) --- start up the UI ----@return boolean ui_ok started ok -local function init_start_ui() - log_graphics("starting UI...") + ---------------------------------------- + -- setup communications + ---------------------------------------- - local draw_start = util.time_ms() - - local ui_ok, message = pcall(renderer.start_ui) - if not ui_ok then - renderer.close_ui() - log_graphics(util.c("UI crashed: ", message)) - println_ts("UI crashed") - log.fatal(util.c("ui crashed with error ", message)) + -- get the communications modem + local modem = ppm.get_wireless_modem() + if modem == nil then + log_comms("wireless modem not found") + println("boot> wireless modem not found") + log.fatal("no wireless modem on startup") + return else - log_graphics("first UI draw took " .. (util.time_ms() - draw_start) .. "ms") - - -- start clock - loop_clock.start() + log_comms("wireless modem connected") end - return ui_ok -end + -- create connection watchdog + local conn_watchdog = util.new_watchdog(5) + conn_watchdog.cancel() + log.debug("boot> conn watchdog created") -local ui_ok = init_start_ui() + -- start comms, open all channels + local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.SCADA_SV_PORT, config.SCADA_SV_LISTEN, config.SCADA_API_LISTEN, conn_watchdog) + log.debug("boot> comms init") + log_comms("comms initialized") ----------------------------------------- --- main event loop ----------------------------------------- + -- base loop clock (2Hz, 10 ticks) + local MAIN_CLOCK = 0.5 + local loop_clock = util.new_clock(MAIN_CLOCK) -local no_modem = false + ---------------------------------------- + -- connect to the supervisor + ---------------------------------------- -if ui_ok then - -- start connection watchdog - conn_watchdog.feed() - log.debug("boot> conn watchdog started") + -- attempt to connect to the supervisor or exit + local function init_connect_sv() + local tick_waiting, task_done = log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SCADA_SV_PORT) - log_sys("system started successfully") -end + -- attempt to establish a connection with the supervisory computer + if not coord_comms.sv_connect(60, tick_waiting, task_done) then + log_comms("supervisor connection failed") + log.fatal("failed to connect to supervisor") + return false + end --- main event loop -while ui_ok do - local event, param1, param2, param3, param4, param5 = util.pull_event() + return true + end - -- handle event - if event == "peripheral_detach" then - local type, device = ppm.handle_unmount(param1) + if not init_connect_sv() then + println("boot> failed to connect to supervisor") + log_sys("system shutdown") + return + else + log_sys("supervisor connected, proceeding to UI start") + end - if type ~= nil and device ~= nil then - if type == "modem" then - -- we only really care if this is our wireless modem - if device == modem then - no_modem = true - log_sys("comms modem disconnected") - println_ts("wireless modem disconnected!") - log.error("comms modem disconnected!") + ---------------------------------------- + -- start the UI + ---------------------------------------- - -- close out UI - renderer.close_ui() + -- start up the UI + ---@return boolean ui_ok started ok + local function init_start_ui() + log_graphics("starting UI...") - -- alert user to status - log_sys("awaiting comms modem reconnect...") - else - log_sys("non-comms modem disconnected") - log.warning("non-comms modem disconnected") - end - elseif type == "monitor" then - if renderer.is_monitor_used(device) then - -- "halt and catch fire" style handling - log_sys("lost a configured monitor, system will now exit") - break - else - log_sys("lost unused monitor, ignoring") + local draw_start = util.time_ms() + + local ui_ok, message = pcall(renderer.start_ui) + if not ui_ok then + renderer.close_ui() + log_graphics(util.c("UI crashed: ", message)) + println_ts("UI crashed") + log.fatal(util.c("ui crashed with error ", message)) + else + log_graphics("first UI draw took " .. (util.time_ms() - draw_start) .. "ms") + + -- start clock + loop_clock.start() + end + + return ui_ok + end + + local ui_ok = init_start_ui() + + ---------------------------------------- + -- main event loop + ---------------------------------------- + + local no_modem = false + + if ui_ok then + -- start connection watchdog + conn_watchdog.feed() + log.debug("boot> conn watchdog started") + + log_sys("system started successfully") + end + + -- main event loop + while ui_ok do + local event, param1, param2, param3, param4, param5 = util.pull_event() + + -- handle event + if event == "peripheral_detach" then + local type, device = ppm.handle_unmount(param1) + + if type ~= nil and device ~= nil then + if type == "modem" then + -- we only really care if this is our wireless modem + if device == modem then + no_modem = true + log_sys("comms modem disconnected") + println_ts("wireless modem disconnected!") + log.error("comms modem disconnected!") + + -- close out UI + renderer.close_ui() + + -- alert user to status + log_sys("awaiting comms modem reconnect...") + else + log_sys("non-comms modem disconnected") + log.warning("non-comms modem disconnected") + end + elseif type == "monitor" then + if renderer.is_monitor_used(device) then + -- "halt and catch fire" style handling + log_sys("lost a configured monitor, system will now exit") + break + else + log_sys("lost unused monitor, ignoring") + end end end - end - elseif event == "peripheral" then - local type, device = ppm.mount(param1) + elseif event == "peripheral" then + local type, device = ppm.mount(param1) - if type ~= nil and device ~= nil then - if type == "modem" then - if device.isWireless() then - -- reconnected modem - no_modem = false - modem = device - coord_comms.reconnect_modem(modem) + if type ~= nil and device ~= nil then + if type == "modem" then + if device.isWireless() then + -- reconnected modem + no_modem = false + modem = device + coord_comms.reconnect_modem(modem) - log_sys("comms modem reconnected") - println_ts("wireless modem reconnected.") + log_sys("comms modem reconnected") + println_ts("wireless modem reconnected.") - -- re-init system + -- re-init system + if not init_connect_sv() then break end + ui_ok = init_start_ui() + else + log_sys("wired modem reconnected") + end + elseif type == "monitor" then + -- not supported, system will exit on loss of in-use monitors + end + end + elseif event == "timer" then + if loop_clock.is_clock(param1) then + -- main loop tick + + -- free any closed sessions + --apisessions.free_all_closed() + + loop_clock.start() + elseif conn_watchdog.is_timer(param1) then + -- supervisor watchdog timeout + local msg = "supervisor server timeout" + log_comms(msg) + println_ts(msg) + log.warning(msg) + + -- close connection and UI + coord_comms.close() + renderer.close_ui() + + if not no_modem then + -- try to re-connect to the supervisor if not init_connect_sv() then break end ui_ok = init_start_ui() - else - log_sys("wired modem reconnected") end - elseif type == "monitor" then - -- not supported, system will exit on loss of in-use monitors + else + -- a non-clock/main watchdog timer event + + --check API watchdogs + --apisessions.check_all_watchdogs(param1) + + -- notify timer callback dispatcher + tcallbackdsp.handle(param1) end + elseif event == "modem_message" then + -- got a packet + local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5) + coord_comms.handle_packet(packet) + + -- check if it was a disconnect + if not coord_comms.is_linked() then + log_comms("supervisor closed connection") + + -- close connection and UI + coord_comms.close() + renderer.close_ui() + + if not no_modem then + -- try to re-connect to the supervisor + if not init_connect_sv() then break end + ui_ok = init_start_ui() + end + end + elseif event == "monitor_touch" then + -- handle a monitor touch event + renderer.handle_touch(core.events.touch(param1, param2, param3)) end - elseif event == "timer" then - if loop_clock.is_clock(param1) then - -- main loop tick - -- free any closed sessions - --apisessions.free_all_closed() - - loop_clock.start() - elseif conn_watchdog.is_timer(param1) then - -- supervisor watchdog timeout - local msg = "supervisor server timeout" - log_comms(msg) - println_ts(msg) - log.warning(msg) - - -- close connection and UI + -- check for termination request + if event == "terminate" or ppm.should_terminate() then + println_ts("terminate requested, closing connections...") + log_comms("terminate requested, closing supervisor connection...") coord_comms.close() - renderer.close_ui() - - if not no_modem then - -- try to re-connect to the supervisor - if not init_connect_sv() then break end - ui_ok = init_start_ui() - end - else - -- a non-clock/main watchdog timer event - - --check API watchdogs - --apisessions.check_all_watchdogs(param1) - - -- notify timer callback dispatcher - tcallbackdsp.handle(param1) + log_comms("supervisor connection closed") + log_comms("closing api sessions...") + apisessions.close_all() + log_comms("api sessions closed") + break end - elseif event == "modem_message" then - -- got a packet - local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5) - coord_comms.handle_packet(packet) - - -- check if it was a disconnect - if not coord_comms.is_linked() then - log_comms("supervisor closed connection") - - -- close connection and UI - coord_comms.close() - renderer.close_ui() - - if not no_modem then - -- try to re-connect to the supervisor - if not init_connect_sv() then break end - ui_ok = init_start_ui() - end - end - elseif event == "monitor_touch" then - -- handle a monitor touch event - renderer.handle_touch(core.events.touch(param1, param2, param3)) end - -- check for termination request - if event == "terminate" or ppm.should_terminate() then - println_ts("terminate requested, closing connections...") - log_comms("terminate requested, closing supervisor connection...") - coord_comms.close() - log_comms("supervisor connection closed") - log_comms("closing api sessions...") - apisessions.close_all() - log_comms("api sessions closed") - break - end + renderer.close_ui() + log_sys("system shutdown") + + println_ts("exited") + log.info("exited") end -renderer.close_ui() -log_sys("system shutdown") - -println_ts("exited") -log.info("exited") +if not xpcall(main, crash.handler) then crash.exit() end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 84a653d..3f6c0d5 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -4,6 +4,7 @@ require("/initenv").init_env() +local crash = require("scada-common.crash") local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") local ppm = require("scada-common.ppm") @@ -13,7 +14,7 @@ local config = require("reactor-plc.config") local plc = require("reactor-plc.plc") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "beta-v0.9.6" +local R_PLC_VERSION = "beta-v0.9.7" local print = util.print local println = util.println @@ -45,166 +46,176 @@ log.info("BOOTING reactor-plc.startup " .. R_PLC_VERSION) log.info("========================================") println(">> Reactor PLC " .. R_PLC_VERSION .. " <<") +crash.set_env("plc", R_PLC_VERSION) + ---------------------------------------- --- startup +-- main application ---------------------------------------- --- mount connected devices -ppm.mount_all() +local function main() + ---------------------------------------- + -- startup + ---------------------------------------- --- shared memory across threads ----@class plc_shared_memory -local __shared_memory = { - -- networked setting - networked = config.NETWORKED, ---@type boolean + -- mount connected devices + ppm.mount_all() - -- PLC system state flags - ---@class plc_state - plc_state = { - init_ok = true, - shutdown = false, - degraded = false, - reactor_formed = true, - no_reactor = false, - no_modem = false - }, + -- shared memory across threads + ---@class plc_shared_memory + local __shared_memory = { + -- networked setting + networked = config.NETWORKED, ---@type boolean - -- control setpoints - ---@class setpoints - setpoints = { - burn_rate_en = false, - burn_rate = 0.0 - }, + -- PLC system state flags + ---@class plc_state + plc_state = { + init_ok = true, + shutdown = false, + degraded = false, + reactor_formed = true, + no_reactor = false, + no_modem = false + }, - -- core PLC devices - plc_dev = { - reactor = ppm.get_fission_reactor(), - modem = ppm.get_wireless_modem() - }, + -- control setpoints + ---@class setpoints + setpoints = { + burn_rate_en = false, + burn_rate = 0.0 + }, - -- system objects - plc_sys = { - rps = nil, ---@type rps - plc_comms = nil, ---@type plc_comms - conn_watchdog = nil ---@type watchdog - }, + -- core PLC devices + plc_dev = { + reactor = ppm.get_fission_reactor(), + modem = ppm.get_wireless_modem() + }, - -- message queues - q = { - mq_rps = mqueue.new(), - mq_comms_tx = mqueue.new(), - mq_comms_rx = mqueue.new() + -- system objects + plc_sys = { + rps = nil, ---@type rps + 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 smem_dev = __shared_memory.plc_dev + local smem_sys = __shared_memory.plc_sys -local plc_state = __shared_memory.plc_state + local plc_state = __shared_memory.plc_state --- we need a reactor, can at least do some things even if it isn't formed though -if smem_dev.reactor == nil then - println("boot> fission reactor not found"); - log.warning("no reactor on startup") + -- we need a reactor, can at least do some things even if it isn't formed though + if smem_dev.reactor == nil then + println("boot> fission reactor not found"); + log.warning("no reactor on startup") - plc_state.init_ok = false - plc_state.degraded = true - plc_state.no_reactor = true -elseif not smem_dev.reactor.isFormed() then - println("boot> fission reactor not formed"); - log.warning("reactor logic adapter present, but reactor is not formed") + plc_state.init_ok = false + plc_state.degraded = true + plc_state.no_reactor = true + elseif not smem_dev.reactor.isFormed() then + println("boot> fission reactor not formed"); + log.warning("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 smem_dev.modem == nil then - println("boot> wireless modem not found") - log.warning("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() + plc_state.degraded = true + plc_state.reactor_formed = false end - plc_state.init_ok = false - plc_state.degraded = true - plc_state.no_modem = true -end + -- modem is required if networked + if __shared_memory.networked and smem_dev.modem == nil then + println("boot> wireless modem not found") + log.warning("no wireless modem on startup") --- PLC init ---- ---- EVENT_CONSUMER: this function consumes events -local function init() - if plc_state.init_ok then - -- just booting up, no fission allowed (neutrons stay put thanks) - if plc_state.reactor_formed and smem_dev.reactor.getStatus() then + -- 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 - -- init reactor protection system - smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed) - log.debug("init> rps init") + plc_state.init_ok = false + plc_state.degraded = true + plc_state.no_modem = true + end - if __shared_memory.networked then - -- comms watchdog, 3 second timeout - smem_sys.conn_watchdog = util.new_watchdog(3) - log.debug("init> conn watchdog started") + -- PLC init + --- + --- EVENT_CONSUMER: this function consumes events + local function init() + if plc_state.init_ok then + -- just booting up, no fission allowed (neutrons stay put thanks) + if plc_state.reactor_formed and smem_dev.reactor.getStatus() then + smem_dev.reactor.scram() + end - -- start comms - smem_sys.plc_comms = plc.comms(config.REACTOR_ID, R_PLC_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, - smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog) - log.debug("init> comms init") + -- 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, 3 second timeout + smem_sys.conn_watchdog = util.new_watchdog(3) + log.debug("init> conn watchdog started") + + -- start comms + smem_sys.plc_comms = plc.comms(config.REACTOR_ID, R_PLC_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, + smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog) + log.debug("init> comms init") + else + println("boot> starting in offline mode") + log.debug("init> running without networking") + end + + util.push_event("clock_start") + + println("boot> completed") + log.debug("init> boot completed") else - println("boot> starting in offline mode") - log.debug("init> running without networking") + println("boot> system in degraded state, awaiting devices...") + log.warning("init> booted in a degraded state, awaiting peripheral connections...") end + end - util.push_event("clock_start") + ---------------------------------------- + -- start system + ---------------------------------------- - println("boot> completed") - log.debug("init> boot completed") + -- 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 - println("boot> system in degraded state, awaiting devices...") - log.warning("init> booted in a degraded state, awaiting peripheral connections...") + -- run threads, excluding comms + parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec) end + + println_ts("exited") + log.info("exited") 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 - -println_ts("exited") -log.info("exited") +if not xpcall(main, crash.handler) then crash.exit() end diff --git a/rtu/startup.lua b/rtu/startup.lua index 03dfbaa..8cce249 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -4,6 +4,7 @@ require("/initenv").init_env() +local crash = require("scada-common.crash") local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") local ppm = require("scada-common.ppm") @@ -24,7 +25,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "beta-v0.9.3" +local RTU_VERSION = "beta-v0.9.4" local rtu_t = types.rtu_t @@ -58,341 +59,351 @@ log.info("BOOTING rtu.startup " .. RTU_VERSION) log.info("========================================") println(">> RTU GATEWAY " .. RTU_VERSION .. " <<") +crash.set_env("rtu", RTU_VERSION) + ---------------------------------------- --- startup +-- main application ---------------------------------------- --- mount connected devices -ppm.mount_all() +local function main() + ---------------------------------------- + -- startup + ---------------------------------------- ----@class rtu_shared_memory -local __shared_memory = { - -- RTU system state flags - ---@class rtu_state - rtu_state = { - linked = false, - shutdown = false - }, + -- mount connected devices + ppm.mount_all() - -- core RTU devices - rtu_dev = { - modem = ppm.get_wireless_modem() - }, + ---@class rtu_shared_memory + local __shared_memory = { + -- RTU system state flags + ---@class rtu_state + rtu_state = { + linked = false, + shutdown = false + }, - -- system objects - rtu_sys = { - rtu_comms = nil, ---@type rtu_comms - conn_watchdog = nil, ---@type watchdog - units = {} ---@type table - }, + -- core RTU devices + rtu_dev = { + modem = ppm.get_wireless_modem() + }, - -- message queues - q = { - mq_comms = mqueue.new() + -- system objects + rtu_sys = { + rtu_comms = nil, ---@type rtu_comms + conn_watchdog = nil, ---@type watchdog + units = {} ---@type table + }, + + -- message queues + q = { + mq_comms = mqueue.new() + } } -} -local smem_dev = __shared_memory.rtu_dev -local smem_sys = __shared_memory.rtu_sys + local smem_dev = __shared_memory.rtu_dev + local smem_sys = __shared_memory.rtu_sys --- get modem -if smem_dev.modem == nil then - println("boot> wireless modem not found") - log.fatal("no wireless modem on startup") - return -end + -- get modem + if smem_dev.modem == nil then + println("boot> wireless modem not found") + log.fatal("no wireless modem on startup") + return + end ----------------------------------------- --- interpret config and init units ----------------------------------------- + ---------------------------------------- + -- interpret config and init units + ---------------------------------------- -local units = __shared_memory.rtu_sys.units + local units = __shared_memory.rtu_sys.units -local rtu_redstone = config.RTU_REDSTONE -local rtu_devices = config.RTU_DEVICES + local rtu_redstone = config.RTU_REDSTONE + local rtu_devices = config.RTU_DEVICES --- configure RTU gateway based on config file definitions -local function configure() - -- redstone interfaces - for entry_idx = 1, #rtu_redstone do - local rs_rtu = redstone_rtu.new() - local io_table = rtu_redstone[entry_idx].io ---@type table - local io_reactor = rtu_redstone[entry_idx].for_reactor ---@type integer + -- configure RTU gateway based on config file definitions + local function configure() + -- redstone interfaces + for entry_idx = 1, #rtu_redstone do + local rs_rtu = redstone_rtu.new() + local io_table = rtu_redstone[entry_idx].io ---@type table + local io_reactor = rtu_redstone[entry_idx].for_reactor ---@type integer - -- CHECK: reactor ID must be >= to 1 - if (not util.is_int(io_reactor)) or (io_reactor <= 0) then - println(util.c("configure> redstone entry #", entry_idx, " : ", io_reactor, " isn't an integer >= 1")) - return false - end + -- CHECK: reactor ID must be >= to 1 + if (not util.is_int(io_reactor)) or (io_reactor <= 0) then + println(util.c("configure> redstone entry #", entry_idx, " : ", io_reactor, " isn't an integer >= 1")) + return false + end - -- CHECK: io table exists - if type(io_table) ~= "table" then - println(util.c("configure> redstone entry #", entry_idx, " no IO table found")) - return false - end + -- CHECK: io table exists + if type(io_table) ~= "table" then + println(util.c("configure> redstone entry #", entry_idx, " no IO table found")) + return false + end - local capabilities = {} + local capabilities = {} - log.debug(util.c("configure> starting redstone RTU I/O linking for reactor ", io_reactor, "...")) + log.debug(util.c("configure> starting redstone RTU I/O linking for reactor ", io_reactor, "...")) - local continue = true + local continue = true - -- check for duplicate entries - for i = 1, #units do - local unit = units[i] ---@type rtu_unit_registry_entry - if unit.reactor == io_reactor and unit.type == rtu_t.redstone then - -- duplicate entry - local message = util.c("configure> skipping definition block #", entry_idx, " for reactor ", io_reactor, - " with already defined redstone I/O") - println(message) - log.warning(message) - continue = false - break + -- check for duplicate entries + for i = 1, #units do + local unit = units[i] ---@type rtu_unit_registry_entry + if unit.reactor == io_reactor and unit.type == rtu_t.redstone then + -- duplicate entry + local message = util.c("configure> skipping definition block #", entry_idx, " for reactor ", io_reactor, + " with already defined redstone I/O") + println(message) + log.warning(message) + continue = false + break + end + end + + -- not a duplicate + if continue then + for i = 1, #io_table do + local valid = false + local conf = io_table[i] + + -- verify configuration + if rsio.is_valid_channel(conf.channel) and rsio.is_valid_side(conf.side) then + if conf.bundled_color then + valid = rsio.is_color(conf.bundled_color) + else + valid = true + end + end + + if not valid then + local message = util.c("configure> invalid redstone definition at index ", i, " in definition block #", entry_idx, + " (for reactor ", io_reactor, ")") + println(message) + log.error(message) + return false + else + -- link redstone in RTU + local mode = rsio.get_io_mode(conf.channel) + if mode == rsio.IO_MODE.DIGITAL_IN then + -- can't have duplicate inputs + if util.table_contains(capabilities, conf.channel) then + local message = util.c("configure> skipping duplicate input for channel ", rsio.to_string(conf.channel), " on side ", conf.side) + println(message) + log.warning(message) + else + rs_rtu.link_di(conf.side, conf.bundled_color) + end + elseif mode == rsio.IO_MODE.DIGITAL_OUT then + rs_rtu.link_do(conf.channel, conf.side, conf.bundled_color) + elseif mode == rsio.IO_MODE.ANALOG_IN then + -- can't have duplicate inputs + if util.table_contains(capabilities, conf.channel) then + local message = util.c("configure> skipping duplicate input for channel ", rsio.to_string(conf.channel), " on side ", conf.side) + println(message) + log.warning(message) + else + rs_rtu.link_ai(conf.side) + end + elseif mode == rsio.IO_MODE.ANALOG_OUT then + rs_rtu.link_ao(conf.side) + else + -- should be unreachable code, we already validated channels + log.error("configure> fell through if chain attempting to identify IO mode", true) + println("configure> encountered a software error, check logs") + return false + end + + table.insert(capabilities, conf.channel) + + log.debug(util.c("configure> linked redstone ", #capabilities, ": ", rsio.to_string(conf.channel), + " (", conf.side, ") for reactor ", io_reactor)) + end + end + + ---@class rtu_unit_registry_entry + local unit = { + name = "redstone_io", + type = rtu_t.redstone, + index = entry_idx, + reactor = io_reactor, + device = capabilities, -- use device field for redstone channels + formed = nil, ---@type boolean|nil + rtu = rs_rtu, ---@type rtu_device|rtu_rs_device + modbus_io = modbus.new(rs_rtu, false), + pkt_queue = nil, ---@type mqueue|nil + thread = nil + } + + table.insert(units, unit) + + log.debug(util.c("init> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for reactor ", io_reactor)) end end - -- not a duplicate - if continue then - for i = 1, #io_table do - local valid = false - local conf = io_table[i] + -- mounted peripherals + for i = 1, #rtu_devices do + local name = rtu_devices[i].name + local index = rtu_devices[i].index + local for_reactor = rtu_devices[i].for_reactor - -- verify configuration - if rsio.is_valid_channel(conf.channel) and rsio.is_valid_side(conf.side) then - if conf.bundled_color then - valid = rsio.is_color(conf.bundled_color) - else - valid = true - end - end + -- CHECK: name is a string + if type(name) ~= "string" then + println(util.c("configure> device entry #", i, ": device ", name, " isn't a string")) + return false + end - if not valid then - local message = util.c("configure> invalid redstone definition at index ", i, " in definition block #", entry_idx, - " (for reactor ", io_reactor, ")") - println(message) - log.error(message) + -- CHECK: index is an integer >= 1 + if (not util.is_int(index)) or (index <= 0) then + println(util.c("configure> device entry #", i, ": index ", index, " isn't an integer >= 1")) + return false + end + + -- CHECK: reactor is an integer >= 1 + if (not util.is_int(for_reactor)) or (for_reactor <= 0) then + println(util.c("configure> device entry #", i, ": reactor ", for_reactor, " isn't an integer >= 1")) + return false + end + + local device = ppm.get_periph(name) + + local type = nil + local rtu_iface = nil ---@type rtu_device + local rtu_type = "" + local formed = nil ---@type boolean|nil + + if device == nil then + local message = util.c("configure> '", name, "' not found, using placeholder") + println(message) + log.warning(message) + + -- mount a virtual (placeholder) device + type, device = ppm.mount_virtual() + else + type = ppm.get_type(name) + end + + if type == "boilerValve" then + -- boiler multiblock + rtu_type = rtu_t.boiler_valve + rtu_iface = boilerv_rtu.new(device) + formed = device.isFormed() + + if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then + println_ts(util.c("configure> failed to check if '", name, "' is formed")) + log.fatal(util.c("configure> failed to check if '", name, "' is a formed boiler multiblock")) return false - else - -- link redstone in RTU - local mode = rsio.get_io_mode(conf.channel) - if mode == rsio.IO_MODE.DIGITAL_IN then - -- can't have duplicate inputs - if util.table_contains(capabilities, conf.channel) then - local message = util.c("configure> skipping duplicate input for channel ", rsio.to_string(conf.channel), " on side ", conf.side) - println(message) - log.warning(message) - else - rs_rtu.link_di(conf.side, conf.bundled_color) - end - elseif mode == rsio.IO_MODE.DIGITAL_OUT then - rs_rtu.link_do(conf.channel, conf.side, conf.bundled_color) - elseif mode == rsio.IO_MODE.ANALOG_IN then - -- can't have duplicate inputs - if util.table_contains(capabilities, conf.channel) then - local message = util.c("configure> skipping duplicate input for channel ", rsio.to_string(conf.channel), " on side ", conf.side) - println(message) - log.warning(message) - else - rs_rtu.link_ai(conf.side) - end - elseif mode == rsio.IO_MODE.ANALOG_OUT then - rs_rtu.link_ao(conf.side) - else - -- should be unreachable code, we already validated channels - log.error("configure> fell through if chain attempting to identify IO mode", true) - println("configure> encountered a software error, check logs") - return false - end - - table.insert(capabilities, conf.channel) - - log.debug(util.c("configure> linked redstone ", #capabilities, ": ", rsio.to_string(conf.channel), - " (", conf.side, ") for reactor ", io_reactor)) end + elseif type == "turbineValve" then + -- turbine multiblock + rtu_type = rtu_t.turbine_valve + rtu_iface = turbinev_rtu.new(device) + formed = device.isFormed() + + if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then + println_ts(util.c("configure> failed to check if '", name, "' is formed")) + log.fatal(util.c("configure> failed to check if '", name, "' is a formed turbine multiblock")) + return false + end + elseif type == "inductionPort" then + -- induction matrix multiblock + rtu_type = rtu_t.induction_matrix + rtu_iface = imatrix_rtu.new(device) + formed = device.isFormed() + + if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then + println_ts(util.c("configure> failed to check if '", name, "' is formed")) + log.fatal(util.c("configure> failed to check if '", name, "' is a formed induction matrix multiblock")) + return false + end + elseif type == "spsPort" then + -- SPS multiblock + rtu_type = rtu_t.sps + rtu_iface = sps_rtu.new(device) + formed = device.isFormed() + + if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then + println_ts(util.c("configure> failed to check if '", name, "' is formed")) + log.fatal(util.c("configure> failed to check if '", name, "' is a formed SPS multiblock")) + return false + end + elseif type == "solarNeutronActivator" then + -- SNA + rtu_type = rtu_t.sna + rtu_iface = sna_rtu.new(device) + elseif type == "environmentDetector" then + -- advanced peripherals environment detector + rtu_type = rtu_t.env_detector + rtu_iface = envd_rtu.new(device) + elseif type == ppm.VIRTUAL_DEVICE_TYPE then + -- placeholder device + rtu_type = "virtual" + rtu_iface = rtu.init_unit().interface() + else + local message = util.c("configure> device '", name, "' is not a known type (", type, ")") + println_ts(message) + log.fatal(message) + return false end ---@class rtu_unit_registry_entry - local unit = { - name = "redstone_io", - type = rtu_t.redstone, - index = entry_idx, - reactor = io_reactor, - device = capabilities, -- use device field for redstone channels - formed = nil, ---@type boolean|nil - rtu = rs_rtu, ---@type rtu_device|rtu_rs_device - modbus_io = modbus.new(rs_rtu, false), - pkt_queue = nil, ---@type mqueue|nil + local rtu_unit = { + name = name, + type = rtu_type, + index = index, + reactor = for_reactor, + device = device, + formed = formed, + rtu = rtu_iface, ---@type rtu_device|rtu_rs_device + modbus_io = modbus.new(rtu_iface, true), + pkt_queue = mqueue.new(), ---@type mqueue|nil thread = nil } - table.insert(units, unit) + rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit) - log.debug(util.c("init> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for reactor ", io_reactor)) + table.insert(units, rtu_unit) + + log.debug(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", rtu_type, ") [", index, "] for reactor ", for_reactor)) end + + -- we made it through all that trusting-user-to-write-a-config-file chaos + return true end - -- mounted peripherals - for i = 1, #rtu_devices do - local name = rtu_devices[i].name - local index = rtu_devices[i].index - local for_reactor = rtu_devices[i].for_reactor + ---------------------------------------- + -- start system + ---------------------------------------- - -- CHECK: name is a string - if type(name) ~= "string" then - println(util.c("configure> device entry #", i, ": device ", name, " isn't a string")) - return false - end + log.debug("boot> running configure()") - -- CHECK: index is an integer >= 1 - if (not util.is_int(index)) or (index <= 0) then - println(util.c("configure> device entry #", i, ": index ", index, " isn't an integer >= 1")) - return false - end + if configure() then + -- start connection watchdog + smem_sys.conn_watchdog = util.new_watchdog(5) + log.debug("boot> conn watchdog started") - -- CHECK: reactor is an integer >= 1 - if (not util.is_int(for_reactor)) or (for_reactor <= 0) then - println(util.c("configure> device entry #", i, ": reactor ", for_reactor, " isn't an integer >= 1")) - return false - end + -- setup comms + smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_sys.conn_watchdog) + log.debug("boot> comms init") - local device = ppm.get_periph(name) + -- init threads + local main_thread = threads.thread__main(__shared_memory) + local comms_thread = threads.thread__comms(__shared_memory) - local type = nil - local rtu_iface = nil ---@type rtu_device - local rtu_type = "" - local formed = nil ---@type boolean|nil - - if device == nil then - local message = util.c("configure> '", name, "' not found, using placeholder") - println(message) - log.warning(message) - - -- mount a virtual (placeholder) device - type, device = ppm.mount_virtual() - else - type = ppm.get_type(name) - end - - if type == "boilerValve" then - -- boiler multiblock - rtu_type = rtu_t.boiler_valve - rtu_iface = boilerv_rtu.new(device) - formed = device.isFormed() - - if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then - println_ts(util.c("configure> failed to check if '", name, "' is formed")) - log.fatal(util.c("configure> failed to check if '", name, "' is a formed boiler multiblock")) - return false + -- assemble thread list + local _threads = { main_thread.p_exec, comms_thread.p_exec } + for i = 1, #units do + if units[i].thread ~= nil then + table.insert(_threads, units[i].thread.p_exec) end - elseif type == "turbineValve" then - -- turbine multiblock - rtu_type = rtu_t.turbine_valve - rtu_iface = turbinev_rtu.new(device) - formed = device.isFormed() - - if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then - println_ts(util.c("configure> failed to check if '", name, "' is formed")) - log.fatal(util.c("configure> failed to check if '", name, "' is a formed turbine multiblock")) - return false - end - elseif type == "inductionPort" then - -- induction matrix multiblock - rtu_type = rtu_t.induction_matrix - rtu_iface = imatrix_rtu.new(device) - formed = device.isFormed() - - if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then - println_ts(util.c("configure> failed to check if '", name, "' is formed")) - log.fatal(util.c("configure> failed to check if '", name, "' is a formed induction matrix multiblock")) - return false - end - elseif type == "spsPort" then - -- SPS multiblock - rtu_type = rtu_t.sps - rtu_iface = sps_rtu.new(device) - formed = device.isFormed() - - if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then - println_ts(util.c("configure> failed to check if '", name, "' is formed")) - log.fatal(util.c("configure> failed to check if '", name, "' is a formed SPS multiblock")) - return false - end - elseif type == "solarNeutronActivator" then - -- SNA - rtu_type = rtu_t.sna - rtu_iface = sna_rtu.new(device) - elseif type == "environmentDetector" then - -- advanced peripherals environment detector - rtu_type = rtu_t.env_detector - rtu_iface = envd_rtu.new(device) - elseif type == ppm.VIRTUAL_DEVICE_TYPE then - -- placeholder device - rtu_type = "virtual" - rtu_iface = rtu.init_unit().interface() - else - local message = util.c("configure> device '", name, "' is not a known type (", type, ")") - println_ts(message) - log.fatal(message) - return false end - ---@class rtu_unit_registry_entry - local rtu_unit = { - name = name, - type = rtu_type, - index = index, - reactor = for_reactor, - device = device, - formed = formed, - rtu = rtu_iface, ---@type rtu_device|rtu_rs_device - modbus_io = modbus.new(rtu_iface, true), - pkt_queue = mqueue.new(), ---@type mqueue|nil - thread = nil - } - - rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit) - - table.insert(units, rtu_unit) - - log.debug(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", rtu_type, ") [", index, "] for reactor ", for_reactor)) + -- run threads + parallel.waitForAll(table.unpack(_threads)) + else + println("configuration failed, exiting...") end - -- we made it through all that trusting-user-to-write-a-config-file chaos - return true + println_ts("exited") + log.info("exited") end ----------------------------------------- --- start system ----------------------------------------- - -log.debug("boot> running configure()") - -if configure() then - -- start connection watchdog - smem_sys.conn_watchdog = util.new_watchdog(5) - log.debug("boot> conn watchdog started") - - -- setup comms - smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_sys.conn_watchdog) - log.debug("boot> comms init") - - -- init threads - local main_thread = threads.thread__main(__shared_memory) - local comms_thread = threads.thread__comms(__shared_memory) - - -- assemble thread list - local _threads = { main_thread.p_exec, comms_thread.p_exec } - for i = 1, #units do - if units[i].thread ~= nil then - table.insert(_threads, units[i].thread.p_exec) - end - end - - -- run threads - parallel.waitForAll(table.unpack(_threads)) -else - println("configuration failed, exiting...") -end - -println_ts("exited") -log.info("exited") +if not xpcall(main, crash.handler) then crash.exit() end diff --git a/scada-common/crash.lua b/scada-common/crash.lua new file mode 100644 index 0000000..66bfda1 --- /dev/null +++ b/scada-common/crash.lua @@ -0,0 +1,46 @@ +-- +-- Crash Handler +-- + +local comms = require("scada-common.comms") +local log = require("scada-common.log") +local util = require("scada-common.util") + +local crash = {} + +local app = "unknown" +local ver = "v0.0.0" +local err = "" + +-- set crash environment +---@param application string app name +---@param version string version +function crash.set_env(application, version) + app = application + ver = version +end + +-- handle a crash error +---@param error string error message +function crash.handler(error) + err = error + log.info("=====> FATAL SOFTWARE FAULT <=====") + log.fatal(error) + log.info("----------------------------------") + log.info(util.c("RUNTIME: ", _HOST)) + log.info(util.c("LUA VERSION: ", _VERSION)) + log.info(util.c("APPLICATION: ", app)) + log.info(util.c("FIRMWARE VERSION: ", ver)) + log.info(util.c("COMMS VERSION: ", comms.version)) + log.info("----------------------------------") + log.info(debug.traceback("--- begin debug trace ---", 1)) + log.info("--- end debug trace ---") +end + +-- final error print on failed xpcall, app exits here +function crash.exit() + util.println("fatal error occured in main application:") + error(err, 0) +end + +return crash diff --git a/supervisor/startup.lua b/supervisor/startup.lua index f213501..8f1238a 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -4,6 +4,7 @@ require("/initenv").init_env() +local crash = require("scada-common.crash") local log = require("scada-common.log") local ppm = require("scada-common.ppm") local util = require("scada-common.util") @@ -13,7 +14,7 @@ local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "beta-v0.7.5" +local SUPERVISOR_VERSION = "beta-v0.7.6" local print = util.print local println = util.println @@ -57,95 +58,105 @@ log.info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION) log.info("========================================") println(">> SCADA Supervisor " .. SUPERVISOR_VERSION .. " <<") +crash.set_env("supervisor", SUPERVISOR_VERSION) + ---------------------------------------- --- startup +-- main application ---------------------------------------- --- mount connected devices -ppm.mount_all() +local function main() + ---------------------------------------- + -- startup + ---------------------------------------- -local modem = ppm.get_wireless_modem() -if modem == nil then - println("boot> wireless modem not found") - log.fatal("no wireless modem on startup") - return -end + -- mount connected devices + ppm.mount_all() --- start comms, open all channels -local superv_comms = supervisor.comms(SUPERVISOR_VERSION, config.NUM_REACTORS, config.REACTOR_COOLING, modem, - config.SCADA_DEV_LISTEN, config.SCADA_SV_LISTEN) - --- base loop clock (6.67Hz, 3 ticks) -local MAIN_CLOCK = 0.15 -local loop_clock = util.new_clock(MAIN_CLOCK) - --- start clock -loop_clock.start() - --- event loop -while true do - local event, param1, param2, param3, param4, param5 = util.pull_event() - - -- handle event - if event == "peripheral_detach" then - local type, device = ppm.handle_unmount(param1) - - if type ~= nil and device ~= nil then - if type == "modem" then - -- we only care if this is our wireless modem - if device == modem then - println_ts("wireless modem disconnected!") - log.error("comms modem disconnected!") - else - log.warning("non-comms modem disconnected") - end - end - end - elseif event == "peripheral" then - local type, device = ppm.mount(param1) - - if type ~= nil and device ~= nil then - if type == "modem" then - if device.isWireless() then - -- reconnected modem - modem = device - superv_comms.reconnect_modem(modem) - - println_ts("wireless modem reconnected.") - log.info("comms modem reconnected.") - else - log.info("wired modem reconnected.") - end - end - end - elseif event == "timer" and loop_clock.is_clock(param1) then - -- main loop tick - - -- iterate sessions - svsessions.iterate_all() - - -- free any closed sessions - svsessions.free_all_closed() - - loop_clock.start() - elseif event == "timer" then - -- a non-clock timer event, check watchdogs - svsessions.check_all_watchdogs(param1) - elseif event == "modem_message" then - -- got a packet - local packet = superv_comms.parse_packet(param1, param2, param3, param4, param5) - superv_comms.handle_packet(packet) + local modem = ppm.get_wireless_modem() + if modem == nil then + println("boot> wireless modem not found") + log.fatal("no wireless modem on startup") + return end - -- check for termination request - if event == "terminate" or ppm.should_terminate() then - println_ts("closing sessions...") - log.info("terminate requested, closing sessions...") - svsessions.close_all() - log.info("sessions closed") - break + -- start comms, open all channels + local superv_comms = supervisor.comms(SUPERVISOR_VERSION, config.NUM_REACTORS, config.REACTOR_COOLING, modem, + config.SCADA_DEV_LISTEN, config.SCADA_SV_LISTEN) + + -- base loop clock (6.67Hz, 3 ticks) + local MAIN_CLOCK = 0.15 + local loop_clock = util.new_clock(MAIN_CLOCK) + + -- start clock + loop_clock.start() + + -- event loop + while true do + local event, param1, param2, param3, param4, param5 = util.pull_event() + + -- handle event + if event == "peripheral_detach" then + local type, device = ppm.handle_unmount(param1) + + if type ~= nil and device ~= nil then + if type == "modem" then + -- we only care if this is our wireless modem + if device == modem then + println_ts("wireless modem disconnected!") + log.error("comms modem disconnected!") + else + log.warning("non-comms modem disconnected") + end + end + end + elseif event == "peripheral" then + local type, device = ppm.mount(param1) + + if type ~= nil and device ~= nil then + if type == "modem" then + if device.isWireless() then + -- reconnected modem + modem = device + superv_comms.reconnect_modem(modem) + + println_ts("wireless modem reconnected.") + log.info("comms modem reconnected.") + else + log.info("wired modem reconnected.") + end + end + end + elseif event == "timer" and loop_clock.is_clock(param1) then + -- main loop tick + + -- iterate sessions + svsessions.iterate_all() + + -- free any closed sessions + svsessions.free_all_closed() + + loop_clock.start() + elseif event == "timer" then + -- a non-clock timer event, check watchdogs + svsessions.check_all_watchdogs(param1) + elseif event == "modem_message" then + -- got a packet + local packet = superv_comms.parse_packet(param1, param2, param3, param4, param5) + superv_comms.handle_packet(packet) + end + + -- check for termination request + if event == "terminate" or ppm.should_terminate() then + println_ts("closing sessions...") + log.info("terminate requested, closing sessions...") + svsessions.close_all() + log.info("sessions closed") + break + end end + + println_ts("exited") + log.info("exited") end -println_ts("exited") -log.info("exited") +if not xpcall(main, crash.handler) then crash.exit() end