diff --git a/rtu/backplane.lua b/rtu/backplane.lua new file mode 100644 index 0000000..78b6ac2 --- /dev/null +++ b/rtu/backplane.lua @@ -0,0 +1,268 @@ +-- +-- RTU Gateway 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 databus = require("rtu.databus") +local rtu = require("rtu.rtu") + +---@class rtu_backplane +local backplane = {} + +local _bp = { + smem = nil, ---@type rtu_shared_memory + + wlan_en = true, + wlan_pref = true, + lan_en = false, + lan_iface = "", + + act_nic = nil, ---@type nic|nil + wl_act = true, + wd_nic = nil, ---@type nic|nil + wl_nic = nil, ---@type nic|nil + + sounders = {} ---@type rtu_speaker_sounder[] +} + +-- initialize the system peripheral backplane +---@param config rtu_config +---@param __shared_memory rtu_shared_memory +function backplane.init(config, __shared_memory) + _bp.smem = __shared_memory + _bp.wlan_en = config.WirelessModem + _bp.wlan_pref = config.PreferWireless + _bp.lan_en = type(config.WiredModem) == "string" + _bp.lan_iface = config.WiredModem + + -- init wired NIC + if _bp.lan_en then + local modem = ppm.get_wired_modem(_bp.lan_iface) + + if modem then + _bp.wd_nic = network.nic(modem) + log.info("BKPLN: WIRED PHY_UP " .. _bp.lan_iface) + end + end + + -- init wireless NIC(s) + if _bp.wlan_en then + local modem, iface = ppm.get_wireless_modem() + + if modem then + _bp.wl_nic = network.nic(modem) + log.info("BKPLN: WIRELESS PHY_UP " .. iface) + end + end + + -- grab the preferred active NIC + if _bp.wlan_pref then + _bp.wl_act = true + _bp.act_nic = _bp.wl_nics[1] + else + _bp.wl_act = false + _bp.act_nic = _bp.wd_nic + end + + databus.tx_hw_modem(_bp.act_nic ~= nil) + + -- find and setup all speakers + local speakers = ppm.get_all_devices("speaker") + for _, s in pairs(speakers) do + local sounder = rtu.init_sounder(s) + + table.insert(_bp.sounders, sounder) + + log.debug(util.c("BKPLN: added speaker, attached as ", sounder.name)) + end + + databus.tx_hw_spkr_count(#_bp.sounders) +end + +-- get the active NIC +---@return nic|nil +function backplane.active_nic() return _bp.act_nic end + +-- get the sounder interfaces +---@return rtu_speaker_sounder[] +function backplane.sounders() return _bp.sounders end + +-- handle a backplane peripheral detach +---@param type string +---@param device table +---@param iface string +function backplane.detach(type, device, iface) + local function println_ts(message) if not _bp.smem.rtu_state.fp_ok then util.println_ts(message) end end + + local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic + + local comms = _bp.smem.rtu_sys.rtu_comms + + if type == "modem" then + ---@cast device Modem + + local was_active = _bp.act_nic and _bp.act_nic.is_modem(device) + local was_wd = wd_nic and wd_nic.is_modem(device) + local was_wl = wl_nic and wl_nic.is_modem(device) + + if wd_nic and was_wd then + log.info("BKPLN: WIRED PHY_DOWN " .. iface) + wd_nic.disconnect() + elseif wl_nic and was_wl then + log.info("BKPLN: WIRELESS PHY_DOWN " .. iface) + wl_nic.disconnect() + end + + -- we only care if this is our active comms modem + if was_active then + println_ts("active comms modem disconnected!") + log.warning("active comms modem disconnected") + + -- failover and try to find a new comms modem + if _bp.wl_act then + -- try to find another wireless modem, otherwise switch to wired + local other_modem = ppm.get_wireless_modem() + if other_modem then + log.info("found another wireless modem, using it for comms") + + -- note: must assign to self.wl_nic if creating a nic, otherwise it only changes locally + if wl_nic then + wl_nic.connect(other_modem) + else _bp.wl_nic = network.nic(other_modem) end + + log.info("BKPLN: WIRELESS PHY_UP " .. iface) + + _bp.act_nic = wl_nic + comms.assign_nic(_bp.act_nic) + log.info("BKPLN: switched comms to new wireless modem") + elseif wd_nic and wd_nic.is_connected() then + _bp.wl_act = false + _bp.act_nic = _bp.wd_nic + + comms.assign_nic(_bp.act_nic) + log.info("BKPLN: switched comms to wired modem") + else + _bp.act_nic = nil + databus.tx_hw_modem(false) + comms.unassign_nic() + end + else + -- switch to wireless if able + if wl_nic then + _bp.wl_act = true + _bp.act_nic = wl_nic + + comms.assign_nic(_bp.act_nic) + log.info("BKPLN: switched comms to wireless modem") + else + _bp.act_nic = nil + databus.tx_hw_modem(false) + comms.unassign_nic() + end + end + else + log.warning("modem disconnected") + end + elseif type == "speaker" then + ---@cast device Speaker + for i = 1, #_bp.sounders do + if _bp.sounders[i].speaker == device then + table.remove(_bp.sounders, i) + + log.warning(util.c("speaker ", iface, " disconnected")) + println_ts("speaker disconnected") + + databus.tx_hw_spkr_count(#_bp.sounders) + break + end + end + end +end + +-- handle a backplane peripheral attach +---@param type string +---@param device table +---@param iface string +function backplane.attach(type, device, iface) + local function println_ts(message) if not _bp.smem.rtu_state.fp_ok then util.println_ts(message) end end + + local comms = _bp.smem.rtu_sys.rtu_comms + + if type == "modem" then + ---@cast device Modem + + local is_wd = _bp.lan_iface == iface + local is_wl = ((not _bp.wl_nic) or (not _bp.wl_nic.is_connected())) and device.isWireless() + + if is_wd then + -- connect this as the wired NIC + if _bp.wd_nic then + _bp.wd_nic.connect(device) + else _bp.wd_nic = network.nic(device) end + + log.info("BKPLN: WIRED PHY_UP " .. iface) + + if _bp.act_nic == nil then + -- set as active + _bp.wl_act = false + _bp.act_nic = _bp.wd_nic + + comms.assign_nic(_bp.act_nic) + databus.tx_hw_modem(true) + println_ts("comms modem reconnected.") + log.info("BKPLN: switched comms to wired modem") + elseif _bp.wl_act and not _bp.wlan_pref then + -- switch back to preferred wired + _bp.wl_act = false + _bp.act_nic = _bp.wd_nic + + comms.assign_nic(_bp.act_nic) + log.info("BKPLN: switched comms to wired modem (preferred)") + end + elseif is_wl then + -- connect this as the wireless NIC + if _bp.wl_nic then + _bp.wl_nic.connect(device) + else _bp.wl_nic = network.nic(device) end + + log.info("BKPLN: WIRELESS PHY_UP " .. iface) + + if _bp.act_nic == nil then + -- set as active + _bp.wl_act = true + _bp.act_nic = _bp.wl_nic + + comms.assign_nic(_bp.act_nic) + databus.tx_hw_modem(true) + println_ts("comms modem reconnected.") + log.info("BKPLN: switched comms to wireless modem") + elseif (not _bp.wl_act) and _bp.wlan_pref then + -- switch back to preferred wireless + _bp.wl_act = true + _bp.act_nic = _bp.wl_nic + + comms.assign_nic(_bp.act_nic) + log.info("BKPLN: switched comms to wireless modem (preferred)") + end + elseif device.isWireless() then + -- the wireless NIC already has a modem + log.info("standby wireless modem connected") + else + log.info("wired modem connected") + end + elseif type == "speaker" then + ---@cast device Speaker + table.insert(_bp.sounders, rtu.init_sounder(device)) + + println_ts("speaker connected") + log.info(util.c("connected speaker ", iface)) + + databus.tx_hw_spkr_count(#_bp.sounders) + end +end + +return backplane diff --git a/rtu/configure.lua b/rtu/configure.lua index 2925841..1333b2b 100644 --- a/rtu/configure.lua +++ b/rtu/configure.lua @@ -64,40 +64,42 @@ local tool_ctl = { viewing_config = false, jumped_to_color = false, - view_gw_cfg = nil, ---@type PushButton - dev_cfg = nil, ---@type PushButton - rs_cfg = nil, ---@type PushButton - color_cfg = nil, ---@type PushButton - color_next = nil, ---@type PushButton - color_apply = nil, ---@type PushButton - settings_apply = nil, ---@type PushButton - settings_confirm = nil, ---@type PushButton + view_gw_cfg = nil, ---@type PushButton + dev_cfg = nil, ---@type PushButton + rs_cfg = nil, ---@type PushButton + color_cfg = nil, ---@type PushButton + color_next = nil, ---@type PushButton + color_apply = nil, ---@type PushButton + settings_apply = nil, ---@type PushButton + settings_confirm = nil, ---@type PushButton - go_home = nil, ---@type function - gen_summary = nil, ---@type function - load_legacy = nil, ---@type function - update_peri_list = nil, ---@type function - update_relay_list = nil, ---@type function - gen_peri_summary = nil, ---@type function - gen_rs_summary = nil, ---@type function + go_home = nil, ---@type function + gen_summary = nil, ---@type function + load_legacy = nil, ---@type function + update_peri_list = nil, ---@type function + update_relay_list = nil, ---@type function + gen_peri_summary = nil, ---@type function + gen_rs_summary = nil, ---@type function } ---@class rtu_config local tmp_cfg = { SpeakerVolume = 1.0, - Peripherals = {}, ---@type rtu_peri_definition[] - Redstone = {}, ---@type rtu_rs_definition[] - SVR_Channel = nil, ---@type integer - RTU_Channel = nil, ---@type integer - ConnTimeout = nil, ---@type number - WiredModem = false, ---@type string|false - TrustedRange = nil, ---@type number - AuthKey = nil, ---@type string - LogMode = 0, ---@type LOG_MODE + Peripherals = {}, ---@type rtu_peri_definition[] + Redstone = {}, ---@type rtu_rs_definition[] + SVR_Channel = nil, ---@type integer + RTU_Channel = nil, ---@type integer + ConnTimeout = nil, ---@type number + WirelessModem = true, + WiredModem = false, ---@type string|false + PreferWireless = true, + TrustedRange = nil, ---@type number + AuthKey = nil, ---@type string + LogMode = 0, ---@type LOG_MODE LogPath = "", LogDebug = false, - FrontPanelTheme = 1, ---@type FP_THEME - ColorMode = 1 ---@type COLOR_MODE + FrontPanelTheme = 1, ---@type FP_THEME + ColorMode = 1 ---@type COLOR_MODE } ---@class rtu_config diff --git a/rtu/rtu.lua b/rtu/rtu.lua index a203518..49984aa 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -36,7 +36,9 @@ function rtu.load_config() config.SVR_Channel = settings.get("SVR_Channel") config.RTU_Channel = settings.get("RTU_Channel") config.ConnTimeout = settings.get("ConnTimeout") + config.WirelessModem = settings.get("WirelessModem") config.WiredModem = settings.get("WiredModem") + config.PreferWireless = settings.get("PreferWireless") config.TrustedRange = settings.get("TrustedRange") config.AuthKey = settings.get("AuthKey") @@ -62,7 +64,9 @@ function rtu.validate_config(cfg) cfv.assert_channel(cfg.RTU_Channel) cfv.assert_type_num(cfg.ConnTimeout) cfv.assert_min(cfg.ConnTimeout, 2) + cfv.assert_type_bool(cfg.WirelessModem) cfv.assert((cfg.WiredModem == false) or (type(cfg.WiredModem) == "string")) + cfv.assert_type_bool(cfg.PreferWireless) cfv.assert_type_num(cfg.TrustedRange) cfv.assert_min(cfg.TrustedRange, 0) cfv.assert_type_str(cfg.AuthKey) @@ -288,7 +292,7 @@ end -- RTU Communications ---@nodiscard ---@param version string RTU version ----@param nic nic network interface device +---@param nic nic|nil network interface device ---@param conn_watchdog watchdog watchdog reference function rtu.comms(version, nic, conn_watchdog) local self = { @@ -301,30 +305,43 @@ function rtu.comms(version, nic, conn_watchdog) local insert = table.insert - if nic.isWireless() then - comms.set_trusted_range(config.TrustedRange) - end + -- CONDITIONAL PRIVATE FUNCTIONS -- - -- PRIVATE FUNCTIONS -- - - -- configure modem channels - nic.closeAll() - nic.open(config.RTU_Channel) + -- these don't check for nic to be nil to save execution time on functions called extremely often + -- when the nic isn't present, the aliases _send and _send_modbus are cleared -- send a scada management packet ---@param msg_type MGMT_TYPE ---@param msg table - local function _send(msg_type, msg) + local function _nic_send(msg_type, msg) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) +---@diagnostic disable-next-line: need-check-nil nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt) self.seq_num = self.seq_num + 1 end + -- send a MODBUS TCP packet + ---@param m_pkt modbus_packet + local function _nic_send_modbus(m_pkt) + local s_pkt = comms.scada_packet() + + s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) + +---@diagnostic disable-next-line: need-check-nil + nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt) + self.seq_num = self.seq_num + 1 + end + + -- PRIVATE FUNCTIONS -- + + -- send a scada management packet + local _send = _nic_send + -- keep alive ack ---@param srv_time integer local function _send_keep_alive_ack(srv_time) @@ -355,13 +372,7 @@ function rtu.comms(version, nic, conn_watchdog) local public = {} -- send a MODBUS TCP packet - ---@param m_pkt modbus_packet - function public.send_modbus(m_pkt) - local s_pkt = comms.scada_packet() - s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) - nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt) - self.seq_num = self.seq_num + 1 - end + public.send_modbus = _nic_send_modbus -- unlink from the server ---@param rtu_state rtu_state @@ -408,6 +419,8 @@ function rtu.comms(version, nic, conn_watchdog) ---@param distance integer ---@return modbus_frame|mgmt_frame|nil packet function public.parse_packet(side, sender, reply_to, message, distance) + -- unreachable if there isn't a nic +---@diagnostic disable-next-line: need-check-nil local s_pkt = nic.receive(side, sender, reply_to, message, distance) local pkt = nil @@ -598,6 +611,34 @@ function rtu.comms(version, nic, conn_watchdog) end end + -- set the current NIC + ---@param _nic nic + function public.assign_nic(_nic) + if nic then nic.closeAll() end + + if _nic.isWireless() then + comms.set_trusted_range(config.TrustedRange) + end + + -- configure receive channels + _nic.closeAll() + _nic.open(config.RTU_Channel) + + nic = _nic + _send = _nic_send + public.send_modbus = _nic_send_modbus + end + + -- clear the current NIC + function public.unassign_nic() + _send = function () end + public.send_modbus = function () end + nic = nil + end + + -- set the NIC if one was given + if nic then public.assign_nic(nic) else public.unassign_nic() end + return public end diff --git a/rtu/startup.lua b/rtu/startup.lua index 089053d..649750d 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -4,23 +4,24 @@ require("/initenv").init_env() -local audio = require("scada-common.audio") -local comms = require("scada-common.comms") -local crash = require("scada-common.crash") -local log = require("scada-common.log") -local mqueue = require("scada-common.mqueue") -local network = require("scada-common.network") -local ppm = require("scada-common.ppm") -local util = require("scada-common.util") +local audio = require("scada-common.audio") +local comms = require("scada-common.comms") +local crash = require("scada-common.crash") +local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") +local network = require("scada-common.network") +local ppm = require("scada-common.ppm") +local util = require("scada-common.util") -local configure = require("rtu.configure") -local databus = require("rtu.databus") -local renderer = require("rtu.renderer") -local rtu = require("rtu.rtu") -local threads = require("rtu.threads") -local uinit = require("rtu.uinit") +local backplane = require("rtu.backplane") +local configure = require("rtu.configure") +local databus = require("rtu.databus") +local renderer = require("rtu.renderer") +local rtu = require("rtu.rtu") +local threads = require("rtu.threads") +local uinit = require("rtu.uinit") -local RTU_VERSION = "v1.12.3" +local RTU_VERSION = "v1.13.0" local println = util.println local println_ts = util.println_ts @@ -92,17 +93,9 @@ local function main() shutdown = false }, - -- RTU gateway devices (not RTU units) - rtu_dev = { - modem_wired = type(config.WiredModem) == "string", - modem_iface = config.WiredModem, - modem = nil, - sounders = {} ---@type rtu_speaker_sounder[] - }, - -- system objects + ---@class rtu_sys rtu_sys = { - nic = nil, ---@type nic rtu_comms = nil, ---@type rtu_comms conn_watchdog = nil, ---@type watchdog units = {} ---@type rtu_registry_entry[] @@ -115,15 +108,9 @@ local function main() } local smem_sys = __shared_memory.rtu_sys - local smem_dev = __shared_memory.rtu_dev local rtu_state = __shared_memory.rtu_state local units = __shared_memory.rtu_sys.units - -- get the configured modem - if smem_dev.modem_wired then - smem_dev.modem = ppm.get_wired_modem(smem_dev.modem_iface) - else smem_dev.modem = ppm.get_wireless_modem() end - ---------------------------------------- -- start system ---------------------------------------- @@ -131,26 +118,8 @@ local function main() log.debug("boot> running uinit()") if uinit(config, __shared_memory) then - -- check comms modem - if smem_dev.modem == nil then - println("startup> comms modem not found") - log.fatal("no comms modem on startup") - return - end - - databus.tx_hw_modem(true) - - -- find and setup all speakers - local speakers = ppm.get_all_devices("speaker") - for _, s in pairs(speakers) do - local sounder = rtu.init_sounder(s) - - table.insert(smem_dev.sounders, sounder) - - log.debug(util.c("startup> added speaker, attached as ", sounder.name)) - end - - databus.tx_hw_spkr_count(#smem_dev.sounders) + -- init backplane peripherals + backplane.init(config, __shared_memory) -- start UI local message @@ -168,9 +137,13 @@ local function main() log.debug("startup> conn watchdog started") -- setup comms - smem_sys.nic = network.nic(smem_dev.modem) - smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, smem_sys.conn_watchdog) - log.debug("startup> comms init") + local nic = backplane.active_nic() + smem_sys.rtu_comms = rtu.comms(RTU_VERSION, nic, smem_sys.conn_watchdog) + if nic then + log.debug("startup> comms init") + else + log.warning("startup> no comms modem on startup") + end -- init threads local main_thread = threads.thread__main(__shared_memory) diff --git a/rtu/threads.lua b/rtu/threads.lua index 1c47efc..1008085 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -5,10 +5,10 @@ local tcd = require("scada-common.tcd") local types = require("scada-common.types") local util = require("scada-common.util") +local backplane = require("rtu.backplane") local databus = require("rtu.databus") local modbus = require("rtu.modbus") local renderer = require("rtu.renderer") -local rtu = require("rtu.rtu") local boilerv_rtu = require("rtu.dev.boilerv_rtu") local dynamicv_rtu = require("rtu.dev.dynamicv_rtu") @@ -191,13 +191,12 @@ function threads.thread__main(smem) -- load in from shared memory local rtu_state = smem.rtu_state - local rtu_dev = smem.rtu_dev - local sounders = smem.rtu_dev.sounders - local nic = smem.rtu_sys.nic local rtu_comms = smem.rtu_sys.rtu_comms local conn_watchdog = smem.rtu_sys.conn_watchdog local units = smem.rtu_sys.units + local sounders = backplane.sounders() + -- start unlinked (in case of restart) rtu_comms.unlink(rtu_state) @@ -247,38 +246,8 @@ function threads.thread__main(smem) local type, device = ppm.handle_unmount(param1) if type ~= nil and device ~= nil then - if type == "modem" then - ---@cast device Modem - -- we only care if this is our comms modem - if nic.is_modem(device) then - nic.disconnect() - - println_ts("comms modem disconnected!") - log.warning("comms modem disconnected") - - local other_modem = ppm.get_wireless_modem() - if other_modem then - log.info("found another wireless modem, using it for comms") - nic.connect(other_modem) - else - databus.tx_hw_modem(false) - end - else - log.warning("non-comms modem disconnected") - end - elseif type == "speaker" then - ---@cast device Speaker - for i = 1, #sounders do - if sounders[i].speaker == device then - table.remove(sounders, i) - - log.warning(util.c("speaker ", param1, " disconnected")) - println_ts("speaker disconnected") - - databus.tx_hw_spkr_count(#sounders) - break - end - end + if type == "modem" or type == "speaker" then + backplane.detach(type, device, param1) else for i = 1, #units do -- find disconnected device @@ -302,31 +271,8 @@ function threads.thread__main(smem) local type, device = ppm.mount(param1) if type ~= nil and device ~= nil then - if type == "modem" then - ---@cast device Modem - local is_comms_modem = util.trinary(rtu_dev.modem_wired, rtu_dev.modem_iface == param1, device.isWireless()) - - if is_comms_modem and not nic.is_connected() then - -- reconnected modem - nic.connect(device) - - println_ts("comms modem reconnected.") - log.info("comms modem reconnected") - - databus.tx_hw_modem(true) - elseif device.isWireless() then - log.info("unused wireless modem connected") - else - log.info("non-comms wired modem connected") - end - elseif type == "speaker" then - ---@cast device Speaker - table.insert(sounders, rtu.init_sounder(device)) - - println_ts("speaker connected") - log.info(util.c("connected speaker ", param1)) - - databus.tx_hw_spkr_count(#sounders) + if type == "modem" or type == "speaker" then + backplane.attach(type, device, param1) else -- relink lost peripheral to correct unit entry for i = 1, #units do @@ -394,12 +340,12 @@ function threads.thread__comms(smem) -- load in from shared memory local rtu_state = smem.rtu_state - local sounders = smem.rtu_dev.sounders local rtu_comms = smem.rtu_sys.rtu_comms local units = smem.rtu_sys.units - local comms_queue = smem.q.mq_comms + local sounders = backplane.sounders() + local last_update = util.time() -- thread loop