#634 coordinator backplane attach/detach

This commit is contained in:
Mikayla
2025-11-07 16:51:43 +00:00
parent b2baaa2090
commit 212e1f8fe8
6 changed files with 246 additions and 105 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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()