diff --git a/coordinator/backplane.lua b/coordinator/backplane.lua index 0647dd4..e0deec8 100644 --- a/coordinator/backplane.lua +++ b/coordinator/backplane.lua @@ -80,6 +80,8 @@ function backplane.init_displays(config) log.info("BKPLN: DISPLAY LINK_" .. util.trinary(disp, "UP", "DOWN") .. " MAIN/" .. iface) + iocontrol.fp_monitor_state("main", disp ~= nil) + if not disp then return false, "Main monitor is not connected." end @@ -101,6 +103,8 @@ function backplane.init_displays(config) log.info("BKPLN: DISPLAY LINK_" .. util.trinary(disp, "UP", "DOWN") .. " FLOW/" .. iface) + iocontrol.fp_monitor_state("flow", disp ~= nil) + if not disp then return false, "Flow monitor is not connected." end @@ -123,6 +127,8 @@ function backplane.init_displays(config) log.info("BKPLN: DISPLAY LINK_" .. util.trinary(disp, "UP", "DOWN") .. " UNIT_" .. i .. "/" .. iface) + iocontrol.fp_monitor_state(i, disp ~= nil) + if not disp then return false, "Unit " .. i .. " monitor is not connected." end @@ -181,6 +187,9 @@ function backplane.init(config, __shared_memory) _bp.wl_nic = wl_nic + wl_nic.closeAll() + wl_nic.open(config.CRD_Channel) + iocontrol.fp_has_wl_modem(modem ~= nil) end @@ -220,9 +229,177 @@ function backplane.init(config, __shared_memory) end -- get the active NIC ----@return nic function backplane.active_nic() return _bp.act_nic end +-- get the wireless NIC +function backplane.wireless_nic() return _bp.wl_nic end + +-- get the configured displays function backplane.displays() return _bp.displays end +-- handle a backplane peripheral attach +---@param type string +---@param device table +---@param iface string +function backplane.attach(type, device, iface) + local MQ__RENDER_DATA = _bp.smem.q_types.MQ__RENDER_DATA + + local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic + + local comms = _bp.smem.crd_sys.coord_comms + + if type == "modem" then + ---@cast device Modem + + local m_is_wl = device.isWireless() + + log.info(util.c("BKPLN: ", util.trinary(m_is_wl, "WIRELESS", "WIRED"), " PHY_ATTACH ", iface)) + + if wd_nic and (_bp.lan_iface == iface) then + -- connect this as the wired NIC + wd_nic.connect(device) + + log.info("BKPLN: WIRED PHY_UP " .. iface) + log_sys("wired comms modem reconnected") + + iocontrol.fp_has_wd_modem(true) + + if (_bp.act_nic ~= wd_nic) and not _bp.wlan_pref then + -- switch back to preferred wired + _bp.act_nic = wd_nic + + comms.switch_nic(_bp.act_nic) + log.info("BKPLN: switched comms to wired modem (preferred)") + end + elseif wl_nic and (not wl_nic.is_connected()) and m_is_wl then + -- connect this as the wireless NIC + wl_nic.connect(device) + + log.info("BKPLN: WIRELESS PHY_UP " .. iface) + log_sys("wireless comms modem reconnected") + + iocontrol.fp_has_wl_modem(true) + + if (_bp.act_nic ~= wl_nic) and _bp.wlan_pref then + -- switch back to preferred wireless + _bp.act_nic = wl_nic + + comms.switch_nic(_bp.act_nic) + log.info("BKPLN: switched comms to wireless modem (preferred)") + end + elseif wl_nic and m_is_wl then + -- the wireless NIC already has a modem + log_sys("standby wireless modem connected") + log.info("BKPLN: standby wireless modem connected") + else + log_sys("unassigned modem connected") + log.warning("BKPLN: unassigned modem connected") + end + elseif type == "monitor" then + ---@cast device Monitor + _bp.smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, { name = iface, device = device }) + elseif type == "speaker" then + ---@cast device Speaker + log_sys("alarm sounder speaker reconnected") + log.info("BKPLN: SPEAKER LINK_UP " .. iface) + + sounder.reconnect(device) + + iocontrol.fp_has_speaker(true) + end +end + +-- handle a backplane peripheral detach +---@param type string +---@param device table +---@param iface string +function backplane.detach(type, device, iface) + local MQ__RENDER_CMD = _bp.smem.q_types.MQ__RENDER_CMD + local MQ__RENDER_DATA = _bp.smem.q_types.MQ__RENDER_DATA + + local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic + + local comms = _bp.smem.crd_sys.coord_comms + + if type == "modem" then + ---@cast device Modem + + local m_is_wl = device.isWireless() + + log.info(util.c("BKPLN: ", util.trinary(m_is_wl, "WIRELESS", "WIRED"), " PHY_DETACH ", iface)) + + if wd_nic and wd_nic.is_modem(device) then + wd_nic.disconnect() + log.info("BKPLN: WIRED PHY_DOWN " .. iface) + + iocontrol.fp_has_wd_modem(false) + elseif wl_nic and wl_nic.is_modem(device) then + wl_nic.disconnect() + log.info("BKPLN: WIRELESS PHY_DOWN " .. iface) + + iocontrol.fp_has_wl_modem(false) + end + + -- we only care if this is our active comms modem + if _bp.act_nic.is_modem(device) then + log_sys("active comms modem disconnected") + log.warning("BKPLN: active comms modem disconnected") + + -- failover and try to find a new comms modem + if _bp.act_nic == wl_nic then + -- wireless active disconnected + -- try to find another wireless modem, otherwise switch to wired + local modem, m_iface = ppm.get_wireless_modem() + if wl_nic and modem then + log_sys("found another wireless modem, using it for comms") + log.info("BKPLN: found another wireless modem, using it for comms") + + wl_nic.connect(modem) + + log.info("BKPLN: WIRELESS PHY_UP " .. m_iface) + + iocontrol.fp_has_wl_modem(true) + elseif wd_nic and wd_nic.is_connected() then + _bp.act_nic = wd_nic + + comms.switch_nic(_bp.act_nic) + log.info("BKPLN: switched comms to wired modem") + else + -- close out main UI + _bp.smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI) + + -- alert user to status + log_sys("awaiting comms modem reconnect...") + end + elseif wl_nic and wl_nic.is_connected() then + -- wired active disconnected, wireless available + _bp.act_nic = wl_nic + + comms.switch_nic(_bp.act_nic) + log.info("BKPLN: switched comms to wireless modem") + else + -- wired active disconnected, wireless unavailable + end + elseif _bp.wl_nic and m_is_wl then + -- wireless, but not active + log_sys("standby wireless modem disconnected") + log.info("BKPLN: standby wireless modem disconnected") + else + log_sys("unassigned modem disconnected") + log.warning("BKPLN: unassigned modem disconnected") + end + elseif type == "monitor" then + ---@cast device Monitor + _bp.smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, device) + elseif type == "speaker" then + ---@cast device Speaker + log_sys("lost alarm sounder speaker") + + log.info("BKPLN: SPEAKER LINK_DOWN " .. iface) + + iocontrol.fp_has_speaker(false) + end +end + + return backplane diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index 6edc504..324486e 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -163,9 +163,10 @@ end -- coordinator communications ---@nodiscard ---@param version string coordinator version ----@param nic nic network interface device +---@param nic nic active network interface device +---@param wl_nic nic|nil pocket wireless network interface device ---@param sv_watchdog watchdog -function coordinator.comms(version, nic, sv_watchdog) +function coordinator.comms(version, nic, wl_nic, sv_watchdog) local self = { sv_linked = false, sv_addr = comms.BROADCAST, @@ -180,14 +181,18 @@ function coordinator.comms(version, nic, sv_watchdog) est_task_done = nil } - comms.set_trusted_range(config.TrustedRange) + if config.WirelessModem then + comms.set_trusted_range(config.TrustedRange) + end -- configure network channels nic.closeAll() nic.open(config.CRD_Channel) -- pass config to apisessions - apisessions.init(nic, config) + if config.API_Enabled and wl_nic then + apisessions.init(wl_nic, config) + end -- PRIVATE FUNCTIONS -- @@ -224,7 +229,8 @@ function coordinator.comms(version, nic, sv_watchdog) m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data }) s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt) +---@diagnostic disable-next-line: need-check-nil + wl_nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt) self.last_api_est_acks[packet.src_addr()] = ack end @@ -245,6 +251,18 @@ function coordinator.comms(version, nic, sv_watchdog) ---@class coord_comms local public = {} + -- switch the current active NIC + ---@param _nic nic + function public.switch_nic(_nic) + nic.closeAll() + + -- configure receive channels + _nic.closeAll() + _nic.open(config.CRD_Channel) + + nic = _nic + end + -- try to connect to the supervisor if not already linked ---@param abort boolean? true to print out cancel info if not linked (use on program terminate) ---@return boolean ok, boolean start_ui @@ -399,7 +417,9 @@ function coordinator.comms(version, nic, sv_watchdog) if l_chan ~= config.CRD_Channel then log.debug("received packet on unconfigured channel " .. l_chan, true) elseif r_chan == config.PKT_Channel then - if not self.sv_linked then + if not config.API_Enabled then + -- log.debug("discarding pocket API packet due to the API being disabled") + elseif not self.sv_linked then log.debug("discarding pocket API packet before linked to supervisor") elseif protocol == PROTOCOL.SCADA_CRDN then ---@cast packet crdn_frame diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua index 6c96676..c6bb3f1 100644 --- a/coordinator/renderer.lua +++ b/coordinator/renderer.lua @@ -82,19 +82,11 @@ function renderer.configure(config) engine.disable_flow_view = config.DisableFlowView end --- link to the monitor peripherals +-- init all displays in use by the renderer ---@param monitors crd_displays -function renderer.set_displays(monitors) +function renderer.init_displays(monitors) engine.monitors = monitors - -- report to front panel as connected - iocontrol.fp_monitor_state("main", engine.monitors.main ~= nil) - iocontrol.fp_monitor_state("flow", engine.monitors.flow ~= nil) - for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end -end - --- init all displays in use by the renderer -function renderer.init_displays() -- init main and flow monitors _init_display(engine.monitors.main) if not engine.disable_flow_view then _init_display(engine.monitors.flow) end diff --git a/coordinator/session/apisessions.lua b/coordinator/session/apisessions.lua index 30fee09..24191d2 100644 --- a/coordinator/session/apisessions.lua +++ b/coordinator/session/apisessions.lua @@ -69,7 +69,7 @@ end -- PUBLIC FUNCTIONS -- -- initialize apisessions ----@param nic nic network interface +---@param nic nic API network interface ---@param config crd_config coordinator config function apisessions.init(nic, config) self.nic = nic diff --git a/coordinator/startup.lua b/coordinator/startup.lua index b096b74..b2dd23c 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -140,8 +140,7 @@ local function main() -- init renderer renderer.configure(config) - renderer.set_displays(backplane.displays()) - renderer.init_displays() + renderer.init_displays(backplane.displays()) renderer.init_dmesg() -- lets get started! @@ -186,6 +185,19 @@ local function main() -- message queues q = { mq_render = mqueue.new() + }, + + -- message queue message types + q_types = { + MQ__RENDER_CMD = { + START_MAIN_UI = 1, + CLOSE_MAIN_UI = 2 + }, + MQ__RENDER_DATA = { + MON_CONNECT = 1, + MON_DISCONNECT = 2, + MON_RESIZE = 3 + } } } @@ -217,7 +229,7 @@ local function main() log.debug("startup> conn watchdog created") -- setup comms - smem_sys.coord_comms = coordinator.comms(COORDINATOR_VERSION, backplane.active_nic(), smem_sys.conn_watchdog) + smem_sys.coord_comms = coordinator.comms(COORDINATOR_VERSION, backplane.active_nic(), backplane.wireless_nic(), smem_sys.conn_watchdog) log.debug("startup> comms init") log_comms("comms initialized") diff --git a/coordinator/threads.lua b/coordinator/threads.lua index 8c6036a..f80f28f 100644 --- a/coordinator/threads.lua +++ b/coordinator/threads.lua @@ -4,6 +4,7 @@ local ppm = require("scada-common.ppm") local tcd = require("scada-common.tcd") local util = require("scada-common.util") +local backplane = require("coordinator.backplane") local coordinator = require("coordinator.coordinator") local iocontrol = require("coordinator.iocontrol") local process = require("coordinator.process") @@ -23,17 +24,6 @@ local threads = {} local MAIN_CLOCK = 0.5 -- 2Hz, 10 ticks local RENDER_SLEEP = 100 -- 100ms, 2 ticks -local MQ__RENDER_CMD = { - START_MAIN_UI = 1, - CLOSE_MAIN_UI = 2 -} - -local MQ__RENDER_DATA = { - MON_CONNECT = 1, - MON_DISCONNECT = 2, - MON_RESIZE = 3 -} - -- main thread ---@nodiscard ---@param smem crd_shared_memory @@ -54,10 +44,12 @@ function threads.thread__main(smem) log_sys("system started successfully") -- load in from shared memory - local crd_state = smem.crd_state - local nic = smem.crd_sys.nic - local coord_comms = smem.crd_sys.coord_comms - local conn_watchdog = smem.crd_sys.conn_watchdog + local crd_state = smem.crd_state + local coord_comms = smem.crd_sys.coord_comms + local conn_watchdog = smem.crd_sys.conn_watchdog + + local MQ__RENDER_CMD = smem.q_types.MQ__RENDER_CMD + local MQ__RENDER_DATA = smem.q_types.MQ__RENDER_DATA -- event loop while true do @@ -66,66 +58,13 @@ function threads.thread__main(smem) -- 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 - ---@cast device Modem - -- we only really care if this is our wireless modem - -- if it is another modem, handle other peripheral losses separately - if nic.is_modem(device) then - nic.disconnect() - log_sys("comms modem disconnected") - - local other_modem = ppm.get_wireless_modem() - if other_modem then - log_sys("found another wireless modem, using it for comms") - nic.connect(other_modem) - else - -- close out main UI - smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI) - - -- alert user to status - log_sys("awaiting comms modem reconnect...") - - iocontrol.fp_has_modem(false) - end - else - log_sys("non-comms modem disconnected") - end - elseif type == "monitor" then - ---@cast device Monitor - smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, device) - elseif type == "speaker" then - ---@cast device Speaker - log_sys("lost alarm sounder speaker") - iocontrol.fp_has_speaker(false) - end + backplane.detach(type, device, param1) end elseif event == "peripheral" then local type, device = ppm.mount(param1) - if type ~= nil and device ~= nil then - if type == "modem" then - ---@cast device Modem - if device.isWireless() and not nic.is_connected() then - -- reconnected modem - log_sys("comms modem reconnected") - nic.connect(device) - iocontrol.fp_has_modem(true) - elseif device.isWireless() then - log.info("unused wireless modem reconnected") - else - log_sys("wired modem reconnected") - end - elseif type == "monitor" then - ---@cast device Monitor - smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, { name = param1, device = device }) - elseif type == "speaker" then - ---@cast device Speaker - log_sys("alarm sounder speaker reconnected") - sounder.reconnect(device) - iocontrol.fp_has_speaker(true) - end + backplane.attach(type, device, param1) end elseif event == "monitor_resize" then smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_RESIZE, param1) @@ -137,18 +76,16 @@ function threads.thread__main(smem) iocontrol.heartbeat() -- maintain connection - if nic.is_connected() then - local ok, start_ui = coord_comms.try_connect() - if not ok then - crd_state.link_fail = true - crd_state.shutdown = true - log_sys("supervisor connection failed, shutting down...") - log.fatal("failed to connect to supervisor") - break - elseif start_ui then - log_sys("supervisor connected, dispatching main UI start") - smem.q.mq_render.push_command(MQ__RENDER_CMD.START_MAIN_UI) - end + local ok, start_ui = coord_comms.try_connect() + if not ok then + crd_state.link_fail = true + crd_state.shutdown = true + log_sys("supervisor connection failed, shutting down...") + log.fatal("failed to connect to supervisor") + break + elseif start_ui then + log_sys("supervisor connected, dispatching main UI start") + smem.q.mq_render.push_command(MQ__RENDER_CMD.START_MAIN_UI) end -- iterate sessions and free any closed ones @@ -268,8 +205,11 @@ function threads.thread__render(smem) log.debug("render thread start") -- load in from shared memory - local crd_state = smem.crd_state - local render_queue = smem.q.mq_render + local crd_state = smem.crd_state + local render_queue = smem.q.mq_render + + local MQ__RENDER_CMD = smem.q_types.MQ__RENDER_CMD + local MQ__RENDER_DATA = smem.q_types.MQ__RENDER_DATA local last_update = util.time()