diff --git a/coordinator/backplane.lua b/coordinator/backplane.lua new file mode 100644 index 0000000..0647dd4 --- /dev/null +++ b/coordinator/backplane.lua @@ -0,0 +1,228 @@ +-- +-- Coordinator System Core Peripheral Backplane +-- + +local log = require("scada-common.log") +local network = require("scada-common.network") +local ppm = require("scada-common.ppm") +local util = require("scada-common.util") + +local coordinator = require("coordinator.coordinator") +local iocontrol = require("coordinator.iocontrol") +local sounder = require("coordinator.sounder") + +local println = util.println + +local log_sys = coordinator.log_sys +local log_boot = coordinator.log_boot +local log_comms = coordinator.log_comms + +---@class crd_backplane +local backplane = {} + +local _bp = { + smem = nil, ---@type crd_shared_memory + + wlan_pref = true, + lan_iface = "", + + act_nic = nil, ---@type nic + wd_nic = nil, ---@type nic|nil + wl_nic = nil, ---@type nic|nil + + speaker = nil, ---@type Speaker|nil + + ---@class crd_displays + displays = { + main = nil, ---@type Monitor|nil + main_iface = "", + flow = nil, ---@type Monitor|nil + flow_iface = "", + unit_displays = {}, ---@type Monitor[] + unit_ifaces = {} ---@type string[] + } +} + +-- initialize the display peripheral backplane +---@param config crd_config +---@return boolean success, string error_msg +function backplane.init_displays(config) + local displays = _bp.displays + + local w, h, _ + + log.info("BKPLN: DISPLAY INIT") + + -- monitor configuration verification + + local mon_cfv = util.new_validator() + + mon_cfv.assert_type_str(config.MainDisplay) + if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end + + mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount) + for i = 1, #config.UnitDisplays do + mon_cfv.assert_type_str(config.UnitDisplays[i]) + end + + if not mon_cfv.valid() then + return false, "Monitor configuration invalid." + end + + -- setup and check display peripherals + + -- main display + + local disp, iface = ppm.get_periph(config.MainDisplay), config.MainDisplay + + displays.main = disp + displays.main_iface = iface + + log.info("BKPLN: DISPLAY LINK_" .. util.trinary(disp, "UP", "DOWN") .. " MAIN/" .. iface) + + if not disp then + return false, "Main monitor is not connected." + end + + disp.setTextScale(0.5) + w, _ = ppm.monitor_block_size(disp.getSize()) + if w ~= 8 then + log.info("BKPLN: DISPLAY MAIN/" .. iface .. " BAD RESOLUTION") + return false, util.c("Main monitor width is incorrect (was ", w, ", must be 8).") + end + + -- flow display + + if not config.DisableFlowView then + disp, iface = ppm.get_periph(config.FlowDisplay), config.FlowDisplay + + displays.flow = disp + displays.flow_iface = iface + + log.info("BKPLN: DISPLAY LINK_" .. util.trinary(disp, "UP", "DOWN") .. " FLOW/" .. iface) + + if not disp then + return false, "Flow monitor is not connected." + end + + disp.setTextScale(0.5) + w, _ = ppm.monitor_block_size(disp.getSize()) + if w ~= 8 then + log.info("BKPLN: DISPLAY FLOW/" .. iface .. " BAD RESOLUTION") + return false, util.c("Flow monitor width is incorrect (was ", w, ", must be 8).") + end + end + + -- unit display(s) + + for i = 1, config.UnitCount do + disp, iface = ppm.get_periph(config.UnitDisplays[i]), config.UnitDisplays[i] + + displays.unit_displays[i] = disp + displays.unit_ifaces[i] = iface + + log.info("BKPLN: DISPLAY LINK_" .. util.trinary(disp, "UP", "DOWN") .. " UNIT_" .. i .. "/" .. iface) + + if not disp then + return false, "Unit " .. i .. " monitor is not connected." + end + + disp.setTextScale(0.5) + w, h = ppm.monitor_block_size(disp.getSize()) + if w ~= 4 or h ~= 4 then + log.info("BKPLN: DISPLAY UNIT_" .. i .. "/" .. iface .. " BAD RESOLUTION") + return false, util.c("Unit ", i, " monitor size is incorrect (was ", w, " by ", h,", must be 4 by 4).") + end + end + + log.info("BKPLN: DISPLAY INIT OK") + + return true, "" +end + +-- initialize the system peripheral backplane +---@param config crd_config +---@param __shared_memory crd_shared_memory +---@return boolean success +function backplane.init(config, __shared_memory) + _bp.smem = __shared_memory + _bp.wlan_pref = config.PreferWireless + _bp.lan_iface = config.WiredModem + + -- Modem Init + + -- init wired NIC + if type(config.WiredModem) == "string" then + local modem = ppm.get_modem(_bp.lan_iface) + local wd_nic = network.nic(modem) + + log.info("BKPLN: WIRED PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. _bp.lan_iface) + log_comms("wired comms modem " .. util.trinary(modem, "connected", "not found")) + + -- set this as active for now + _bp.act_nic = wd_nic + _bp.wd_nic = wd_nic + + iocontrol.fp_has_wd_modem(modem ~= nil) + end + + -- init wireless NIC(s) + if config.WirelessModem then + local modem, iface = ppm.get_wireless_modem() + local wl_nic = network.nic(modem) + + log.info("BKPLN: WIRELESS PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. iface) + log_comms("wireless comms modem " .. util.trinary(modem, "connected", "not found")) + + -- set this as active if connected or if both modems are disconnected and this is preferred + if (modem and _bp.wlan_pref) or not (_bp.act_nic and _bp.act_nic.is_connected()) then + _bp.act_nic = wl_nic + end + + _bp.wl_nic = wl_nic + + iocontrol.fp_has_wl_modem(modem ~= nil) + end + + -- at least one comms modem is required + if not ((_bp.wd_nic and _bp.wd_nic.is_connected()) or (_bp.wl_nic and _bp.wl_nic.is_connected())) then + log_comms("no comms modem found") + println("startup> no comms modem found") + log.warning("BKPLN: no comms modem on startup") + return false + end + + -- Speaker Init + + _bp.speaker = ppm.get_device("speaker") + + if not _bp.speaker then + log_boot("annunciator alarm speaker not found") + + println("startup> speaker not found") + log.fatal("BKPLN: no annunciator alarm speaker found") + + return false + else + log.info("BKPLN: SPEAKER LINK_UP " .. ppm.get_iface(_bp.speaker)) + log_boot("annunciator alarm speaker connected") + + local sounder_start = util.time_ms() + sounder.init(_bp.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 + + return true +end + +-- get the active NIC +---@return nic +function backplane.active_nic() return _bp.act_nic end + +function backplane.displays() return _bp.displays end + +return backplane diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index 7999679..6edc504 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -29,11 +29,9 @@ local config = {} coordinator.config = config --- load the coordinator configuration
--- status of 0 is OK, 1 is bad config, 2 is bad monitor config ----@return 0|1|2 status, nil|monitors_struct|string monitors (or error message) +-- load the coordinator configuration function coordinator.load_config() - if not settings.load("/coordinator.settings") then return 1 end + if not settings.load("/coordinator.settings") then return false end config.UnitCount = settings.get("UnitCount") config.SpeakerVolume = settings.get("SpeakerVolume") @@ -121,85 +119,7 @@ function coordinator.load_config() cfv.assert_type_int(config.ColorMode) cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES) - -- Monitor Setup - - ---@class monitors_struct - local monitors = { - main = nil, ---@type Monitor|nil - main_name = "", - flow = nil, ---@type Monitor|nil - flow_name = "", - unit_displays = {}, ---@type Monitor[] - unit_name_map = {} ---@type string[] - } - - local mon_cfv = util.new_validator() - - -- get all interface names - local names = {} - for iface, _ in pairs(ppm.get_monitor_list()) do table.insert(names, iface) end - - local function setup_monitors() - mon_cfv.assert_type_str(config.MainDisplay) - if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end - mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount) - - if mon_cfv.valid() then - local w, h, _ - - if not util.table_contains(names, config.MainDisplay) then - return 2, "Main monitor is not connected." - end - - monitors.main = ppm.get_periph(config.MainDisplay) - monitors.main_name = config.MainDisplay - - monitors.main.setTextScale(0.5) - w, _ = ppm.monitor_block_size(monitors.main.getSize()) - if w ~= 8 then - return 2, util.c("Main monitor width is incorrect (was ", w, ", must be 8).") - end - - if not config.DisableFlowView then - if not util.table_contains(names, config.FlowDisplay) then - return 2, "Flow monitor is not connected." - end - - monitors.flow = ppm.get_periph(config.FlowDisplay) - monitors.flow_name = config.FlowDisplay - - monitors.flow.setTextScale(0.5) - w, _ = ppm.monitor_block_size(monitors.flow.getSize()) - if w ~= 8 then - return 2, util.c("Flow monitor width is incorrect (was ", w, ", must be 8).") - end - end - - for i = 1, config.UnitCount do - local display = config.UnitDisplays[i] - if type(display) ~= "string" or not util.table_contains(names, display) then - return 2, "Unit " .. i .. " monitor is not connected." - end - - monitors.unit_displays[i] = ppm.get_periph(display) - monitors.unit_name_map[i] = display - - monitors.unit_displays[i].setTextScale(0.5) - w, h = ppm.monitor_block_size(monitors.unit_displays[i].getSize()) - if w ~= 4 or h ~= 4 then - return 2, util.c("Unit ", i, " monitor size is incorrect (was ", w, " by ", h,", must be 4 by 4).") - end - end - else return 2, "Monitor configuration invalid." end - end - - if cfv.valid() then - local ok, result, message = pcall(setup_monitors) - assert(ok, util.c("fatal error while trying to verify monitors: ", result)) - if result == 2 then return 2, message end - else return 1 end - - return 0, monitors + return cfv.valid() end -- dmesg print wrapper diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 9a788db..305dff5 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -290,9 +290,13 @@ end -- toggle heartbeat indicator function iocontrol.heartbeat() io.fp.ps.toggle("heartbeat") end +-- report presence of the wired modem +---@param has_modem boolean +function iocontrol.fp_has_wd_modem(has_modem) io.fp.ps.publish("has_wd_modem", has_modem) end + -- report presence of the wireless modem ---@param has_modem boolean -function iocontrol.fp_has_modem(has_modem) io.fp.ps.publish("has_modem", has_modem) end +function iocontrol.fp_has_wl_modem(has_modem) io.fp.ps.publish("has_wl_modem", has_modem) end -- report presence of the speaker ---@param has_speaker boolean diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua index d3ca0cd..6c96676 100644 --- a/coordinator/renderer.lua +++ b/coordinator/renderer.lua @@ -29,7 +29,7 @@ local renderer = {} -- render engine local engine = { color_mode = 1, ---@type COLOR_MODE - monitors = nil, ---@type monitors_struct|nil + monitors = nil, ---@type crd_displays|nil dmesg_window = nil, ---@type Window|nil ui_ready = false, fp_ready = false, @@ -83,7 +83,7 @@ function renderer.configure(config) end -- link to the monitor peripherals ----@param monitors monitors_struct +---@param monitors crd_displays function renderer.set_displays(monitors) engine.monitors = monitors @@ -336,18 +336,18 @@ function renderer.handle_reconnect(name, device) -- note: handle_resize is a more adaptive way of re-initializing a connected monitor -- since it can handle a monitor being reconnected that isn't the right size - if engine.monitors.main_name == name then + if engine.monitors.main_iface == name then is_used = true engine.monitors.main = device renderer.handle_resize(name) - elseif engine.monitors.flow_name == name then + elseif engine.monitors.flow_iface == name then is_used = true engine.monitors.flow = device renderer.handle_resize(name) else - for idx, monitor in ipairs(engine.monitors.unit_name_map) do + for idx, monitor in ipairs(engine.monitors.unit_ifaces) do if monitor == name then is_used = true engine.monitors.unit_displays[idx] = device @@ -372,7 +372,7 @@ function renderer.handle_resize(name) if not engine.monitors then return false, false end - if engine.monitors.main_name == name and engine.monitors.main then + if engine.monitors.main_iface == name and engine.monitors.main then local device = engine.monitors.main ---@type Monitor -- this is necessary if the bottom left block was broken and on reconnect @@ -415,7 +415,7 @@ function renderer.handle_resize(name) is_ok = false end else engine.dmesg_window.redraw() end - elseif engine.monitors.flow_name == name and engine.monitors.flow then + elseif engine.monitors.flow_iface == name and engine.monitors.flow then local device = engine.monitors.flow ---@type Monitor -- this is necessary if the bottom left block was broken and on reconnect @@ -452,7 +452,7 @@ function renderer.handle_resize(name) end end else - for idx, monitor in ipairs(engine.monitors.unit_name_map) do + for idx, monitor in ipairs(engine.monitors.unit_ifaces) do local device = engine.monitors.unit_displays[idx] if monitor == name and device then @@ -505,12 +505,12 @@ function renderer.handle_mouse(event) if engine.fp_ready and event.monitor == "terminal" then engine.ui.front_panel.handle_mouse(event) elseif engine.ui_ready then - if event.monitor == engine.monitors.main_name then + if event.monitor == engine.monitors.main_iface then if engine.ui.main_display then engine.ui.main_display.handle_mouse(event) end - elseif event.monitor == engine.monitors.flow_name then + elseif event.monitor == engine.monitors.flow_iface then if engine.ui.flow_display then engine.ui.flow_display.handle_mouse(event) end else - for id, monitor in ipairs(engine.monitors.unit_name_map) do + for id, monitor in ipairs(engine.monitors.unit_ifaces) do local display = engine.ui.unit_displays[id] if event.monitor == monitor and display then if display then display.handle_mouse(event) end diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 01b6d58..b096b74 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -12,6 +12,7 @@ local network = require("scada-common.network") local ppm = require("scada-common.ppm") local util = require("scada-common.util") +local backplane = require("coordinator.backplane") local configure = require("coordinator.configure") local coordinator = require("coordinator.coordinator") local iocontrol = require("coordinator.iocontrol") @@ -36,45 +37,13 @@ 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 +-- first pass configuration check before validating monitors +if not coordinator.load_config() then -- try to reconfigure (user action) - local success, error = configure.configure(loaded, monitors) + local success, error = configure.configure(1) 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") + if not coordinator.load_config() then + println("failed to load a valid configuration, please reconfigure") return end else @@ -83,9 +52,6 @@ if loaded ~= 0 then end end --- passed checks, good now ----@cast monitors monitors_struct - local config = coordinator.config ---------------------------------------- @@ -102,6 +68,64 @@ println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<") crash.set_env("coordinator", COORDINATOR_VERSION) crash.dbg_log_env() +---------------------------------------- +-- display init +---------------------------------------- + +-- mount connected devices (required for monitor setup) +ppm.mount_all() + +local wait_on_load = true + +local disp_ok, disp_err = backplane.init_displays(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 (not disp_ok) 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() + disp_ok, disp_err = backplane.init_displays(config) + break + elseif event == "mouse_click" or event == "terminate" then + wait_on_load = false + break + end + end +end + +if not disp_ok then + -- try to reconfigure (user action) + local success, error = configure.configure(2, disp_err) + if success then + if not coordinator.load_config() then + println("failed to load a valid configuration, please reconfigure") + return + else + disp_ok, disp_err = backplane.init_displays(config) + + if not disp_ok then + println("monitor configuration invalid, please reconfigure") + return + end + end + else + println("configuration error: " .. error) + return + end +end + ---------------------------------------- -- main application ---------------------------------------- @@ -111,15 +135,12 @@ 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.set_displays(backplane.displays()) renderer.init_displays() renderer.init_dmesg() @@ -130,6 +151,12 @@ local function main() log_sys("system start on " .. os.date("%c")) log_boot("starting " .. COORDINATOR_VERSION) + -- 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 + ---------------------------------------- -- memory allocation ---------------------------------------- @@ -149,15 +176,9 @@ local function main() shutdown = false }, - -- core coordinator devices - crd_dev = { - modem = ppm.get_wireless_modem(), - speaker = ppm.get_device("speaker") ---@type Speaker|nil - }, - -- system objects + ---@class crd_sys crd_sys = { - nic = nil, ---@type nic coord_comms = nil, ---@type coord_comms conn_watchdog = nil ---@type watchdog }, @@ -168,65 +189,17 @@ local function main() } } - local smem_dev = __shared_memory.crd_dev - local smem_sys = __shared_memory.crd_sys - + local smem_sys = __shared_memory.crd_sys local crd_state = __shared_memory.crd_state ---------------------------------------- - -- setup alarm sounder subsystem + -- init system ---------------------------------------- - 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 + -- modem and speaker initialization + if not backplane.init(config, __shared_memory) then return 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 @@ -238,6 +211,16 @@ local function main() return else log_render("front panel ready") 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") + + -- setup comms + smem_sys.coord_comms = coordinator.comms(COORDINATOR_VERSION, backplane.active_nic(), smem_sys.conn_watchdog) + log.debug("startup> comms init") + log_comms("comms initialized") + ---------------------------------------- -- start system ----------------------------------------