diff --git a/ccmsi.lua b/ccmsi.lua index cc4a5d0..86f2094 100644 --- a/ccmsi.lua +++ b/ccmsi.lua @@ -138,6 +138,7 @@ local function gen_tree(manifest) for i = 1, #list do local split = {} +---@diagnostic disable-next-line: discard-returns string.gsub(list[i], "([^/]+)", function(c) split[#split + 1] = c end) if #split == 1 then table.insert(tree, list[i]) else table.insert(tree, _tree_add(tree, split)) end diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index f6a2018..27543ec 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -232,8 +232,8 @@ function coordinator.comms(version, nic, sv_watchdog) local self = { sv_linked = false, sv_addr = comms.BROADCAST, - sv_seq_num = 0, - sv_r_seq_num = nil, + sv_seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate + sv_r_seq_num = nil, ---@type nil|integer sv_config_err = false, last_est_ack = ESTABLISH_ACK.ALLOW, last_api_est_acks = {}, @@ -492,7 +492,7 @@ function coordinator.comms(version, nic, sv_watchdog) _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_API_VERSION) elseif dev_type == DEVICE_TYPE.PKT then -- pocket linking request - local id = apisessions.establish_session(src_addr, firmware_v) + local id = apisessions.establish_session(src_addr, packet.scada_frame.seq_num(), firmware_v) coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id)) local conf = iocontrol.get_db().facility.conf @@ -515,15 +515,15 @@ function coordinator.comms(version, nic, sv_watchdog) elseif r_chan == config.SVR_Channel then -- check sequence number if self.sv_r_seq_num == nil then - self.sv_r_seq_num = packet.scada_frame.seq_num() - elseif self.sv_linked and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then + self.sv_r_seq_num = packet.scada_frame.seq_num() + 1 + elseif self.sv_r_seq_num ~= packet.scada_frame.seq_num() then log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) return false elseif self.sv_linked and src_addr ~= self.sv_addr then log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?") return false else - self.sv_r_seq_num = packet.scada_frame.seq_num() + self.sv_r_seq_num = packet.scada_frame.seq_num() + 1 end -- feed watchdog on valid sequence number @@ -706,7 +706,6 @@ function coordinator.comms(version, nic, sv_watchdog) self.sv_addr = src_addr self.sv_linked = true - self.sv_r_seq_num = nil self.sv_config_err = false iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED) diff --git a/coordinator/session/apisessions.lua b/coordinator/session/apisessions.lua index 516b91b..5c4a38a 100644 --- a/coordinator/session/apisessions.lua +++ b/coordinator/session/apisessions.lua @@ -89,10 +89,11 @@ end -- establish a new API session ---@nodiscard ----@param source_addr integer ----@param version string +---@param source_addr integer pocket computer ID +---@param i_seq_num integer initial (most recent) sequence number +---@param version string pocket version ---@return integer session_id -function apisessions.establish_session(source_addr, version) +function apisessions.establish_session(source_addr, i_seq_num, version) ---@class pkt_session_struct local pkt_s = { open = true, @@ -105,7 +106,7 @@ function apisessions.establish_session(source_addr, version) local id = self.next_id - pkt_s.instance = pocket.new_session(id, source_addr, pkt_s.in_queue, pkt_s.out_queue, self.config.API_Timeout) + pkt_s.instance = pocket.new_session(id, source_addr, i_seq_num, pkt_s.in_queue, pkt_s.out_queue, self.config.API_Timeout) table.insert(self.sessions, pkt_s) local mt = { diff --git a/coordinator/session/pocket.lua b/coordinator/session/pocket.lua index cd03fc1..c554c64 100644 --- a/coordinator/session/pocket.lua +++ b/coordinator/session/pocket.lua @@ -32,16 +32,17 @@ local PERIODICS = { ---@nodiscard ---@param id integer session ID ---@param s_addr integer device source address +---@param i_seq_num integer initial sequence number ---@param in_queue mqueue in message queue ---@param out_queue mqueue out message queue ---@param timeout number communications timeout -function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) +function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout) local log_header = "pkt_session(" .. id .. "): " local self = { -- connection properties - seq_num = 0, - r_seq_num = nil, + seq_num = i_seq_num + 2, -- next after the establish approval was sent + r_seq_num = i_seq_num + 1, connected = true, conn_watchdog = util.new_watchdog(timeout), last_rtt = 0, @@ -104,13 +105,11 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) ---@param pkt mgmt_frame|crdn_frame local function _handle_packet(pkt) -- check sequence number - if self.r_seq_num == nil then - self.r_seq_num = pkt.scada_frame.seq_num() - elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then + if self.r_seq_num ~= pkt.scada_frame.seq_num() then log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num()) return else - self.r_seq_num = pkt.scada_frame.seq_num() + self.r_seq_num = pkt.scada_frame.seq_num() + 1 end -- feed watchdog diff --git a/coordinator/startup.lua b/coordinator/startup.lua index f152bc8..1960e58 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") local threads = require("coordinator.threads") -local COORDINATOR_VERSION = "v1.4.7" +local COORDINATOR_VERSION = "v1.5.1" local CHUNK_LOAD_DELAY_S = 30.0 @@ -151,8 +151,8 @@ local function main() -- core coordinator devices crd_dev = { - speaker = ppm.get_device("speaker"), - modem = ppm.get_wireless_modem() + modem = ppm.get_wireless_modem(), + speaker = ppm.get_device("speaker") }, -- system objects diff --git a/pocket/iocontrol.lua b/pocket/iocontrol.lua index da9c0d7..de69dd3 100644 --- a/pocket/iocontrol.lua +++ b/pocket/iocontrol.lua @@ -342,10 +342,8 @@ function iocontrol.report_link_state(state, sv_addr, api_addr) io.ps.publish("crd_conn_quality", 0) end - if state == LINK_STATE.LINKED then - io.ps.publish("sv_addr", sv_addr) - io.ps.publish("api_addr", api_addr) - end + if sv_addr then io.ps.publish("sv_addr", sv_addr) end + if api_addr then io.ps.publish("api_addr", api_addr) end end -- determine supervisor connection quality (trip time) @@ -525,7 +523,10 @@ function iocontrol.record_unit_data(data) reactor_state = 6 -- SCRAM rps_status = 2 end - else rps_status = 4 end + else + rps_status = 4 + reactor_state = 4 + end -- update reactor/control status if unit.reactor_data.mek_status.status then diff --git a/pocket/pocket.lua b/pocket/pocket.lua index 0afc068..fab0827 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -78,15 +78,16 @@ end ---@enum POCKET_APP_ID local APP_ID = { ROOT = 1, + LOADER = 2, -- main app pages - UNITS = 2, - GUIDE = 3, - ABOUT = 4, + UNITS = 3, + GUIDE = 4, + ABOUT = 5, -- diag app page - ALARMS = 5, + ALARMS = 6, -- other - DUMMY = 6, - NUM_APPS = 6 + DUMMY = 7, + NUM_APPS = 7 } pocket.APP_ID = APP_ID @@ -98,9 +99,9 @@ pocket.APP_ID = APP_ID ---@field switcher function|nil function to switch between children ---@field tasks table tasks to run while viewing this page --- allocate the page navigation system ----@param render_queue mqueue -function pocket.init_nav(render_queue) +-- initialize the page navigation system +---@param smem pkt_shared_memory +function pocket.init_nav(smem) local self = { pane = nil, ---@type graphics_element sidebar = nil, ---@type graphics_element @@ -108,6 +109,7 @@ function pocket.init_nav(render_queue) containers = {}, help_map = {}, help_return = nil, + loader_return = nil, cur_app = APP_ID.ROOT } @@ -143,10 +145,13 @@ function pocket.init_nav(render_queue) app.load = function () app.loaded = true end app.unload = function () app.loaded = false end - -- check which connections this requires + -- check which connections this requires (for unload) ---@return boolean requires_sv, boolean requires_api function app.check_requires() return require_sv or false, require_api or false end + -- check if any connection is required (for load) + function app.requires_conn() return require_sv or require_api or false end + -- delayed set of the pane if it wasn't ready at the start ---@param root_pane graphics_element multipane function app.set_root_pane(root_pane) @@ -254,7 +259,14 @@ function pocket.init_nav(render_queue) local app = self.apps[app_id] ---@type pocket_app if app then - if not app.loaded then render_queue.push_data(MQ__RENDER_DATA.LOAD_APP, app_id) end + if app.requires_conn() and not smem.pkt_sys.pocket_comms.is_linked() then + -- bring up the app loader + self.loader_return = app_id + app_id = APP_ID.LOADER + app = self.apps[app_id] + else self.loader_return = nil end + + if not app.loaded then smem.q.mq_render.push_data(MQ__RENDER_DATA.LOAD_APP, app_id) end self.cur_app = app_id self.pane.set_value(app_id) @@ -267,6 +279,13 @@ function pocket.init_nav(render_queue) end end + -- open the app that was blocked on connecting + function nav.on_loader_connected() + if self.loader_return then + nav.open_app(self.loader_return) + end + end + -- load a given app ---@param app_id POCKET_APP_ID function nav.load_app(app_id) @@ -351,15 +370,15 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav) sv = { linked = false, addr = comms.BROADCAST, - seq_num = 0, - r_seq_num = nil, ---@type nil|integer + seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate + r_seq_num = nil, ---@type nil|integer last_est_ack = ESTABLISH_ACK.ALLOW }, api = { linked = false, addr = comms.BROADCAST, - seq_num = 0, - r_seq_num = nil, ---@type nil|integer + seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate + r_seq_num = nil, ---@type nil|integer last_est_ack = ESTABLISH_ACK.ALLOW }, establish_delay_counter = 0 @@ -585,8 +604,8 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav) elseif r_chan == config.CRD_Channel then -- check sequence number if self.api.r_seq_num == nil then - self.api.r_seq_num = packet.scada_frame.seq_num() - elseif self.connected and ((self.api.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then + self.api.r_seq_num = packet.scada_frame.seq_num() + 1 + elseif self.api.r_seq_num ~= packet.scada_frame.seq_num() then log.warning("sequence out-of-order (API): last = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) return elseif self.api.linked and (src_addr ~= self.api.addr) then @@ -594,7 +613,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav) "); channel in use by another system?") return else - self.api.r_seq_num = packet.scada_frame.seq_num() + self.api.r_seq_num = packet.scada_frame.seq_num() + 1 end -- feed watchdog on valid sequence number @@ -705,8 +724,8 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav) elseif r_chan == config.SVR_Channel then -- check sequence number if self.sv.r_seq_num == nil then - self.sv.r_seq_num = packet.scada_frame.seq_num() - elseif self.connected and ((self.sv.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then + self.sv.r_seq_num = packet.scada_frame.seq_num() + 1 + elseif self.sv.r_seq_num ~= packet.scada_frame.seq_num() then log.warning("sequence out-of-order (SVR): last = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) return elseif self.sv.linked and (src_addr ~= self.sv.addr) then @@ -714,7 +733,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav) "); channel in use by another system?") return else - self.sv.r_seq_num = packet.scada_frame.seq_num() + self.sv.r_seq_num = packet.scada_frame.seq_num() + 1 end -- feed watchdog on valid sequence number @@ -844,6 +863,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav) ---@nodiscard function public.is_api_linked() return self.api.linked end + -- check if we are still linked with the supervisor and coordinator + ---@nodiscard + function public.is_linked() return self.sv.linked and self.api.linked end + return public end diff --git a/pocket/startup.lua b/pocket/startup.lua index b56da4f..2701efb 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -20,7 +20,7 @@ local pocket = require("pocket.pocket") local renderer = require("pocket.renderer") local threads = require("pocket.threads") -local POCKET_VERSION = "v0.10.0-alpha" +local POCKET_VERSION = "v0.11.0-alpha" local println = util.println local println_ts = util.println_ts @@ -123,7 +123,7 @@ local function main() -- setup system ---------------------------------------- - smem_sys.nav = pocket.init_nav(__shared_memory.q.mq_render) + smem_sys.nav = pocket.init_nav(__shared_memory) -- message authentication init if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then diff --git a/pocket/ui/apps/diag_apps.lua b/pocket/ui/apps/diag_apps.lua index be63e95..27ef837 100644 --- a/pocket/ui/apps/diag_apps.lua +++ b/pocket/ui/apps/diag_apps.lua @@ -32,7 +32,7 @@ local function create_pages(root) local alarm_test = Div{parent=root,x=1,y=1} - local alarm_app = db.nav.register_app(APP_ID.ALARMS, alarm_test) + local alarm_app = db.nav.register_app(APP_ID.ALARMS, alarm_test, nil, true) local page = alarm_app.new_page(nil, function () end) page.tasks = { db.diag.tone_test.get_tone_states } diff --git a/pocket/ui/apps/loader.lua b/pocket/ui/apps/loader.lua new file mode 100644 index 0000000..8ea72f0 --- /dev/null +++ b/pocket/ui/apps/loader.lua @@ -0,0 +1,49 @@ +-- +-- Loading Screen App +-- + +local iocontrol = require("pocket.iocontrol") +local pocket = require("pocket.pocket") + +local conn_waiting = require("pocket.ui.components.conn_waiting") + +local core = require("graphics.core") + +local Div = require("graphics.elements.div") +local MultiPane = require("graphics.elements.multipane") +local TextBox = require("graphics.elements.textbox") + +local APP_ID = pocket.APP_ID + +local LINK_STATE = iocontrol.LINK_STATE + +-- create the connecting to SV & API page +---@param root graphics_element parent +local function create_pages(root) + local db = iocontrol.get_db() + + local main = Div{parent=root,x=1,y=1} + + db.nav.register_app(APP_ID.LOADER, main).new_page(nil, function () end) + + local conn_sv_wait = conn_waiting(main, 6, false) + local conn_api_wait = conn_waiting(main, 6, true) + local main_pane = Div{parent=main,x=1,y=2} + + local root_pane = MultiPane{parent=main,x=1,y=1,panes={conn_sv_wait,conn_api_wait,main_pane}} + + root_pane.register(db.ps, "link_state", function (state) + if state == LINK_STATE.UNLINKED or state == LINK_STATE.API_LINK_ONLY then + root_pane.set_value(1) + elseif state == LINK_STATE.SV_LINK_ONLY then + root_pane.set_value(2) + else + root_pane.set_value(3) + db.nav.on_loader_connected() + end + end) + + TextBox{parent=main_pane,text="Connected!",x=1,y=6,alignment=core.ALIGN.CENTER} +end + +return create_pages diff --git a/pocket/ui/components/conn_waiting.lua b/pocket/ui/components/conn_waiting.lua index 9cbe1b3..6b69650 100644 --- a/pocket/ui/components/conn_waiting.lua +++ b/pocket/ui/components/conn_waiting.lua @@ -23,16 +23,16 @@ local function init(parent, y, is_api) local root = Div{parent=parent,x=1,y=1} -- bounding box div - local box = Div{parent=root,x=1,y=y,height=5} + local box = Div{parent=root,x=1,y=y,height=6} local waiting_x = math.floor(parent.get_width() / 2) - 1 if is_api then WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)} - TextBox{parent=box,text="Connecting to API",alignment=ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)} + TextBox{parent=box,text="Connecting to API",alignment=ALIGN.CENTER,y=5,fg_bg=cpair(colors.white,style.root.bkg)} else WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.green,style.root.bkg)} - TextBox{parent=box,text="Connecting to Supervisor",alignment=ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)} + TextBox{parent=box,text="Connecting to Supervisor",alignment=ALIGN.CENTER,y=5,fg_bg=cpair(colors.white,style.root.bkg)} end return root diff --git a/pocket/ui/main.lua b/pocket/ui/main.lua index 40b1104..4a33327 100644 --- a/pocket/ui/main.lua +++ b/pocket/ui/main.lua @@ -10,11 +10,10 @@ local pocket = require("pocket.pocket") local diag_apps = require("pocket.ui.apps.diag_apps") local dummy_app = require("pocket.ui.apps.dummy_app") local guide_app = require("pocket.ui.apps.guide") +local loader_app = require("pocket.ui.apps.loader") local sys_apps = require("pocket.ui.apps.sys_apps") local unit_app = require("pocket.ui.apps.unit") -local conn_waiting = require("pocket.ui.components.conn_waiting") - local home_page = require("pocket.ui.pages.home_page") local style = require("pocket.ui.style") @@ -33,8 +32,6 @@ local SignalBar = require("graphics.elements.indicators.signal") local ALIGN = core.ALIGN local cpair = core.cpair -local LINK_STATE = iocontrol.LINK_STATE - local APP_ID = pocket.APP_ID -- create new main view @@ -42,7 +39,9 @@ local APP_ID = pocket.APP_ID local function init(main) local db = iocontrol.get_db() - -- window header message + local main_pane = Div{parent=main,x=1,y=2} + + -- window header message and connection status TextBox{parent=main,y=1,text="EARLY ACCESS ALPHA S C ",alignment=ALIGN.LEFT,height=1,fg_bg=style.header} local svr_conn = SignalBar{parent=main,y=1,x=22,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)} local crd_conn = SignalBar{parent=main,y=1,x=26,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)} @@ -50,40 +49,18 @@ local function init(main) db.ps.subscribe("svr_conn_quality", svr_conn.set_value) db.ps.subscribe("crd_conn_quality", crd_conn.set_value) - --#region root panel panes (connection screens + main screen) - - local root_pane_div = Div{parent=main,x=1,y=2} - - local conn_sv_wait = conn_waiting(root_pane_div, 6, false) - local conn_api_wait = conn_waiting(root_pane_div, 6, true) - local main_pane = Div{parent=main,x=1,y=2} - - local root_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={conn_sv_wait,conn_api_wait,main_pane}} - - root_pane.register(db.ps, "link_state", function (state) - if state == LINK_STATE.UNLINKED or state == LINK_STATE.API_LINK_ONLY then - root_pane.set_value(1) - elseif state == LINK_STATE.SV_LINK_ONLY then - root_pane.set_value(2) - else - root_pane.set_value(3) - end - end) - - --#endregion - - --#region main page panel panes & sidebar - local page_div = Div{parent=main_pane,x=4,y=1} + -- create all the apps & pages home_page(page_div) - unit_app(page_div) guide_app(page_div) + loader_app(page_div) sys_apps(page_div) diag_apps(page_div) dummy_app(page_div) + -- verify all apps were created assert(util.table_len(db.nav.get_containers()) == APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered") db.nav.set_pane(MultiPane{parent=page_div,x=1,y=1,panes=db.nav.get_containers()}) @@ -92,8 +69,6 @@ local function init(main) PushButton{parent=main_pane,x=1,y=19,text="\x1b",min_width=3,fg_bg=cpair(colors.white,colors.gray),active_fg_bg=cpair(colors.gray,colors.black),callback=db.nav.nav_up} db.nav.open_app(APP_ID.ROOT) - - --#endregion end return init diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 2fb869a..c89974f 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -524,8 +524,8 @@ end function plc.comms(version, nic, reactor, rps, conn_watchdog) local self = { sv_addr = comms.BROADCAST, - seq_num = 0, - r_seq_num = nil, + seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate + r_seq_num = nil, ---@type nil|integer scrammed = false, linked = false, last_est_ack = ESTABLISH_ACK.ALLOW, @@ -571,33 +571,17 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog) self.seq_num = self.seq_num + 1 end - -- variable reactor status information, excluding heating rate + -- dynamic reactor status information, excluding heating rate ---@return table data_table, boolean faulted - local function _reactor_status() + local function _get_reactor_status() local fuel = nil local waste = nil local coolant = nil local hcoolant = nil - local data_table = { - false, -- getStatus - 0, -- getBurnRate - 0, -- getActualBurnRate - 0, -- getTemperature - 0, -- getDamagePercent - 0, -- getBoilEfficiency - 0, -- getEnvironmentalLoss - 0, -- fuel_amnt - 0, -- getFuelFilledPercentage - 0, -- waste_amnt - 0, -- getWasteFilledPercentage - "", -- coolant_name - 0, -- coolant_amnt - 0, -- getCoolantFilledPercentage - "", -- hcoolant_name - 0, -- hcoolant_amnt - 0 -- getHeatedCoolantFilledPercentage - } + local data_table = {} + + reactor.__p_disable_afc() local tasks = { function () data_table[1] = reactor.getStatus() end, @@ -637,30 +621,32 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog) data_table[16] = hcoolant.amount end + reactor.__p_enable_afc() + return data_table, reactor.__p_is_faulted() end -- update the status cache if changed ---@return boolean changed local function _update_status_cache() - local status, faulted = _reactor_status() + local status, faulted = _get_reactor_status() local changed = false - if self.status_cache ~= nil then - if not faulted then + if not faulted then + if self.status_cache ~= nil then for i = 1, #status do if status[i] ~= self.status_cache[i] then changed = true break end end + else + changed = true end - else - changed = true - end - if changed and not faulted then - self.status_cache = status + if changed then + self.status_cache = status + end end return changed @@ -679,9 +665,11 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog) _send(msg_type, { status }) end - -- send structure properties (these should not change, server will cache these) + -- send static structure properties, cached by server local function _send_struct() - local mek_data = { false, 0, 0, 0, types.new_zero_coordinate(), types.new_zero_coordinate(), 0, 0, 0, 0, 0, 0, 0, 0 } + local mek_data = {} + + reactor.__p_disable_afc() local tasks = { function () mek_data[1] = reactor.getLength() end, @@ -705,6 +693,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog) _send(RPLC_TYPE.MEK_STRUCT, mek_data) self.resend_build = false end + + reactor.__p_enable_afc() end -- PUBLIC FUNCTIONS -- @@ -835,8 +825,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog) if l_chan == config.PLC_Channel then -- check sequence number if self.r_seq_num == nil then - self.r_seq_num = packet.scada_frame.seq_num() - elseif self.linked and ((self.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then + self.r_seq_num = packet.scada_frame.seq_num() + 1 + elseif self.r_seq_num ~= packet.scada_frame.seq_num() then log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) return elseif self.linked and (src_addr ~= self.sv_addr) then @@ -844,7 +834,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog) "); channel in use by another system?") return else - self.r_seq_num = packet.scada_frame.seq_num() + self.r_seq_num = packet.scada_frame.seq_num() + 1 end -- feed the watchdog first so it doesn't uhh...eat our packets :) @@ -1030,10 +1020,9 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog) println_ts("linked!") log.info("supervisor establish request approved, linked to SV (CID#" .. src_addr .. ")") - -- link + reset remote sequence number and cache + -- link + reset cache self.sv_addr = src_addr self.linked = true - self.r_seq_num = nil self.status_cache = nil if plc_state.reactor_formed then _send_struct() end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 1b8b2d6..6f6a27b 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc") local renderer = require("reactor-plc.renderer") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "v1.7.11" +local R_PLC_VERSION = "v1.8.0" local println = util.println local println_ts = util.println_ts diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 71cea40..231c261 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -284,8 +284,8 @@ end function rtu.comms(version, nic, conn_watchdog) local self = { sv_addr = comms.BROADCAST, - seq_num = 0, - r_seq_num = nil, + seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate + r_seq_num = nil, ---@type nil|integer txn_id = 0, last_est_ack = ESTABLISH_ACK.ALLOW } @@ -442,8 +442,8 @@ function rtu.comms(version, nic, conn_watchdog) if l_chan == config.RTU_Channel then -- check sequence number if self.r_seq_num == nil then - self.r_seq_num = packet.scada_frame.seq_num() - elseif rtu_state.linked and ((self.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then + self.r_seq_num = packet.scada_frame.seq_num() + 1 + elseif self.r_seq_num ~= packet.scada_frame.seq_num() then log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) return elseif rtu_state.linked and (src_addr ~= self.sv_addr) then @@ -451,7 +451,7 @@ function rtu.comms(version, nic, conn_watchdog) "); channel in use by another system?") return else - self.r_seq_num = packet.scada_frame.seq_num() + self.r_seq_num = packet.scada_frame.seq_num() + 1 end -- feed watchdog on valid sequence number @@ -556,7 +556,6 @@ function rtu.comms(version, nic, conn_watchdog) -- establish allowed rtu_state.linked = true self.sv_addr = packet.scada_frame.src_addr() - self.r_seq_num = nil println_ts("supervisor connection established") log.info("supervisor connection established") else diff --git a/rtu/startup.lua b/rtu/startup.lua index ae2e72b..c5000ea 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -31,7 +31,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 = "v1.9.6" +local RTU_VERSION = "v1.10.1" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE @@ -93,14 +93,6 @@ local function main() network.init_mac(config.AuthKey) end - -- get modem - 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 - -- generate alarm tones audio.generate_tones() @@ -116,14 +108,15 @@ local function main() -- RTU gateway devices (not RTU units) rtu_dev = { + modem = ppm.get_wireless_modem(), sounders = {} }, -- system objects rtu_sys = { - nic = network.nic(modem), - rtu_comms = nil, ---@type rtu_comms - conn_watchdog = nil, ---@type watchdog + nic = nil, ---@type nic + rtu_comms = nil, ---@type rtu_comms + conn_watchdog = nil, ---@type watchdog units = {} }, @@ -134,8 +127,9 @@ local function main() } local smem_sys = __shared_memory.rtu_sys + local smem_dev = __shared_memory.rtu_dev - databus.tx_hw_modem(true) + local rtu_state = __shared_memory.rtu_state ---------------------------------------- -- interpret config and init units @@ -501,8 +495,6 @@ local function main() -- start system ---------------------------------------- - local rtu_state = __shared_memory.rtu_state - log.debug("boot> running sys_config()") if sys_config() then @@ -517,23 +509,33 @@ local function main() log.info("startup> running in headless mode without front panel") end + -- check modem + if smem_dev.modem == nil then + println("startup> wireless modem not found") + log.fatal("no wireless 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(__shared_memory.rtu_dev.sounders, sounder) + table.insert(smem_dev.sounders, sounder) log.debug(util.c("startup> added speaker, attached as ", sounder.name)) end - databus.tx_hw_spkr_count(#__shared_memory.rtu_dev.sounders) + databus.tx_hw_spkr_count(#smem_dev.sounders) -- start connection watchdog smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout) 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") diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 3e65ed6..fa6f6ff 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -17,7 +17,7 @@ local max_distance = nil local comms = {} -- protocol/data versions (protocol/data independent changes tracked by util.lua version) -comms.version = "2.5.2" +comms.version = "3.0.0" comms.api_version = "0.0.3" ---@enum PROTOCOL @@ -240,6 +240,8 @@ function comms.scada_packet() ---@nodiscard function public.modem_event() return self.modem_msg_in end ---@nodiscard + function public.raw_header() return { self.src_addr, self.dest_addr, self.seq_num, self.protocol } end + ---@nodiscard function public.raw_sendable() return self.raw end ---@nodiscard @@ -278,7 +280,7 @@ function comms.authd_packet() src_addr = comms.BROADCAST, dest_addr = comms.BROADCAST, mac = "", - payload = "" + payload = {} } ---@class authd_packet @@ -286,14 +288,13 @@ function comms.authd_packet() -- make an authenticated SCADA packet ---@param s_packet scada_packet scada packet to authenticate - ---@param mac function message authentication function + ---@param mac function message authentication hash function function public.make(s_packet, mac) self.valid = true self.src_addr = s_packet.src_addr() self.dest_addr = s_packet.dest_addr() - self.payload = textutils.serialize(s_packet.raw_sendable(), { allow_repetitions = true, compact = true }) - self.mac = mac(self.payload) - self.raw = { self.src_addr, self.dest_addr, self.mac, self.payload } + self.mac = mac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true })) + self.raw = { self.src_addr, self.dest_addr, self.mac, s_packet.raw_sendable() } end -- parse in a modem message as an authenticated SCADA packet @@ -330,14 +331,14 @@ function comms.authd_packet() self.src_addr = nil self.dest_addr = nil self.mac = "" - self.payload = "" + self.payload = {} end -- check if this packet is destined for this device local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == COMPUTER_ID) self.valid = is_destination and type(self.src_addr) == "number" and type(self.dest_addr) == "number" and - type(self.mac) == "string" and type(self.payload) == "string" + type(self.mac) == "string" and type(self.payload) == "table" end end diff --git a/scada-common/constants.lua b/scada-common/constants.lua index b2755b9..0472a6c 100644 --- a/scada-common/constants.lua +++ b/scada-common/constants.lua @@ -52,7 +52,6 @@ local alarms = {} -- unit alarms -alarms.HIGH_TEMP = 1150 -- temp >= 1150K alarms.HIGH_WASTE = 0.85 -- fill > 85% alarms.HIGH_RADIATION = 0.00005 -- 50 uSv/h, not yet damaging but this isn't good diff --git a/scada-common/network.lua b/scada-common/network.lua index bdaf5c3..c34a1d5 100644 --- a/scada-common/network.lua +++ b/scada-common/network.lua @@ -80,7 +80,7 @@ end ---@param modem table modem to use function network.nic(modem) local self = { - connected = true, -- used to avoid costly MAC calculations if modem isn't even present + connected = true, -- used to avoid costly MAC calculations if modem isn't even present channels = {} } @@ -114,7 +114,7 @@ function network.nic(modem) modem.open(channel) end - -- link all public functions except for transmit + -- link all public functions except for transmit, open, and close for key, func in pairs(modem) do if key ~= "transmit" and key ~= "open" and key ~= "close" and key ~= "closeAll" then public[key] = func end end @@ -175,7 +175,7 @@ function network.nic(modem) ---@param packet scada_packet packet function public.transmit(dest_channel, local_channel, packet) if self.connected then - local tx_packet = packet ---@type authd_packet|scada_packet + local tx_packet = packet ---@type authd_packet|scada_packet if c_eng.hmac ~= nil then -- local start = util.time_ms() @@ -184,7 +184,7 @@ function network.nic(modem) ---@cast tx_packet authd_packet tx_packet.make(packet, compute_hmac) - -- log.debug("crypto.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms") + -- log.debug("network.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms") end modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable()) @@ -211,17 +211,18 @@ function network.nic(modem) a_packet.receive(side, sender, reply_to, message, distance) if a_packet.is_valid() then - -- local start = util.time_ms() - local packet_hmac = a_packet.mac() - local msg = a_packet.data() - local computed_hmac = compute_hmac(msg) + s_packet.receive(side, sender, reply_to, a_packet.data(), distance) - if packet_hmac == computed_hmac then - -- log.debug("crypto.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms") - s_packet.receive(side, sender, reply_to, textutils.unserialize(msg), distance) - s_packet.stamp_authenticated() - else - -- log.debug("crypto.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms") + if s_packet.is_valid() then + -- local start = util.time_ms() + local computed_hmac = compute_hmac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true })) + + if a_packet.mac() == computed_hmac then + -- log.debug("network.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms") + s_packet.stamp_authenticated() + else + -- log.debug("network.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms") + end end end else diff --git a/startup.lua b/startup.lua index 811f510..662d989 100644 --- a/startup.lua +++ b/startup.lua @@ -1,35 +1,31 @@ -local util = require("scada-common.util") +local BOOTLOADER_VERSION = "1.1" -local println = util.println - -local BOOTLOADER_VERSION = "1.0" - -println("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION) -println("BOOT> SCANNING FOR APPLICATIONS...") +print("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION) +print("BOOT> SCANNING FOR APPLICATIONS...") local exit_code if fs.exists("reactor-plc/startup.lua") then - println("BOOT> EXEC REACTOR PLC STARTUP") + print("BOOT> EXEC REACTOR PLC STARTUP") exit_code = shell.execute("reactor-plc/startup") elseif fs.exists("rtu/startup.lua") then - println("BOOT> EXEC RTU STARTUP") + print("BOOT> EXEC RTU STARTUP") exit_code = shell.execute("rtu/startup") elseif fs.exists("supervisor/startup.lua") then - println("BOOT> EXEC SUPERVISOR STARTUP") + print("BOOT> EXEC SUPERVISOR STARTUP") exit_code = shell.execute("supervisor/startup") elseif fs.exists("coordinator/startup.lua") then - println("BOOT> EXEC COORDINATOR STARTUP") + print("BOOT> EXEC COORDINATOR STARTUP") exit_code = shell.execute("coordinator/startup") elseif fs.exists("pocket/startup.lua") then - println("BOOT> EXEC POCKET STARTUP") + print("BOOT> EXEC POCKET STARTUP") exit_code = shell.execute("pocket/startup") else - println("BOOT> NO SCADA STARTUP FOUND") - println("BOOT> EXIT") + print("BOOT> NO SCADA STARTUP FOUND") + print("BOOT> EXIT") return false end -if not exit_code then println("BOOT> APPLICATION CRASHED") end +if not exit_code then print("BOOT> APPLICATION CRASHED") end return exit_code diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index 9c9284f..a135a44 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -43,12 +43,13 @@ local PERIODICS = { ---@nodiscard ---@param id integer session ID ---@param s_addr integer device source address +---@param i_seq_num integer initial sequence number ---@param in_queue mqueue in message queue ---@param out_queue mqueue out message queue ---@param timeout number communications timeout ---@param facility facility facility data table ---@param fp_ok boolean if the front panel UI is running -function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facility, fp_ok) +function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, facility, fp_ok) -- print a log message to the terminal as long as the UI isn't running local function println(message) if not fp_ok then util.println_ts(message) end end @@ -57,8 +58,8 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil local self = { units = facility.get_units(), -- connection properties - seq_num = 0, - r_seq_num = nil, + seq_num = i_seq_num + 2, -- next after the establish approval was sent + r_seq_num = i_seq_num + 1, connected = true, conn_watchdog = util.new_watchdog(timeout), establish_time = util.time_s(), @@ -182,13 +183,11 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil ---@param pkt mgmt_frame|crdn_frame local function _handle_packet(pkt) -- check sequence number - if self.r_seq_num == nil then - self.r_seq_num = pkt.scada_frame.seq_num() - elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then + if self.r_seq_num ~= pkt.scada_frame.seq_num() then log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num()) return else - self.r_seq_num = pkt.scada_frame.seq_num() + self.r_seq_num = pkt.scada_frame.seq_num() + 1 end -- feed watchdog diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index a4a8271..4aad6d3 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -48,12 +48,13 @@ local PERIODICS = { ---@nodiscard ---@param id integer session ID ---@param s_addr integer device source address +---@param i_seq_num integer initial sequence number ---@param reactor_id integer reactor ID ---@param in_queue mqueue in message queue ---@param out_queue mqueue out message queue ---@param timeout number communications timeout ---@param fp_ok boolean if the front panel UI is running -function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, fp_ok) +function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue, timeout, fp_ok) -- print a log message to the terminal as long as the UI isn't running local function println(message) if not fp_ok then util.println_ts(message) end end @@ -66,8 +67,8 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f ramping_rate = false, auto_lock = false, -- connection properties - seq_num = 0, - r_seq_num = nil, + seq_num = i_seq_num + 2, -- next after the establish approval was sent + r_seq_num = i_seq_num + 1, connected = true, received_struct = false, received_status_cache = false, @@ -190,71 +191,111 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f -- copy in the RPS status ---@param rps_status table local function _copy_rps_status(rps_status) - self.sDB.rps_tripped = rps_status[1] - self.sDB.rps_trip_cause = rps_status[2] - self.sDB.rps_status.high_dmg = rps_status[3] - self.sDB.rps_status.high_temp = rps_status[4] - self.sDB.rps_status.low_cool = rps_status[5] - self.sDB.rps_status.ex_waste = rps_status[6] - self.sDB.rps_status.ex_hcool = rps_status[7] - self.sDB.rps_status.no_fuel = rps_status[8] - self.sDB.rps_status.fault = rps_status[9] - self.sDB.rps_status.timeout = rps_status[10] - self.sDB.rps_status.manual = rps_status[11] - self.sDB.rps_status.automatic = rps_status[12] - self.sDB.rps_status.sys_fail = rps_status[13] - self.sDB.rps_status.force_dis = rps_status[14] + local rps = self.sDB.rps_status + + self.sDB.rps_tripped = rps_status[1] + self.sDB.rps_trip_cause = rps_status[2] + + rps.high_dmg = rps_status[3] + rps.high_temp = rps_status[4] + rps.low_cool = rps_status[5] + rps.ex_waste = rps_status[6] + rps.ex_hcool = rps_status[7] + rps.no_fuel = rps_status[8] + rps.fault = rps_status[9] + rps.timeout = rps_status[10] + rps.manual = rps_status[11] + rps.automatic = rps_status[12] + rps.sys_fail = rps_status[13] + rps.force_dis = rps_status[14] end -- copy in the reactor status ---@param mek_data table local function _copy_status(mek_data) + local stat = self.sDB.mek_status + local struct = self.sDB.mek_struct + -- copy status information - self.sDB.mek_status.status = mek_data[1] - self.sDB.mek_status.burn_rate = mek_data[2] - self.sDB.mek_status.act_burn_rate = mek_data[3] - self.sDB.mek_status.temp = mek_data[4] - self.sDB.mek_status.damage = mek_data[5] - self.sDB.mek_status.boil_eff = mek_data[6] - self.sDB.mek_status.env_loss = mek_data[7] + stat.status = mek_data[1] + stat.burn_rate = mek_data[2] + stat.act_burn_rate = mek_data[3] + stat.temp = mek_data[4] + stat.damage = mek_data[5] + stat.boil_eff = mek_data[6] + stat.env_loss = mek_data[7] -- copy container information - self.sDB.mek_status.fuel = mek_data[8] - self.sDB.mek_status.fuel_fill = mek_data[9] - self.sDB.mek_status.waste = mek_data[10] - self.sDB.mek_status.waste_fill = mek_data[11] - self.sDB.mek_status.ccool_type = mek_data[12] - self.sDB.mek_status.ccool_amnt = mek_data[13] - self.sDB.mek_status.ccool_fill = mek_data[14] - self.sDB.mek_status.hcool_type = mek_data[15] - self.sDB.mek_status.hcool_amnt = mek_data[16] - self.sDB.mek_status.hcool_fill = mek_data[17] + stat.fuel = mek_data[8] + stat.fuel_fill = mek_data[9] + stat.waste = mek_data[10] + stat.waste_fill = mek_data[11] + stat.ccool_type = mek_data[12] + stat.ccool_amnt = mek_data[13] + stat.ccool_fill = mek_data[14] + stat.hcool_type = mek_data[15] + stat.hcool_amnt = mek_data[16] + stat.hcool_fill = mek_data[17] -- update computable fields if we have our structure if self.received_struct then - self.sDB.mek_status.fuel_need = self.sDB.mek_struct.fuel_cap - self.sDB.mek_status.fuel_fill - self.sDB.mek_status.waste_need = self.sDB.mek_struct.waste_cap - self.sDB.mek_status.waste_fill - self.sDB.mek_status.cool_need = self.sDB.mek_struct.ccool_cap - self.sDB.mek_status.ccool_fill - self.sDB.mek_status.hcool_need = self.sDB.mek_struct.hcool_cap - self.sDB.mek_status.hcool_fill + stat.fuel_need = struct.fuel_cap - stat.fuel_fill + stat.waste_need = struct.waste_cap - stat.waste_fill + stat.cool_need = struct.ccool_cap - stat.ccool_fill + stat.hcool_need = struct.hcool_cap - stat.hcool_fill end end -- copy in the reactor structure ---@param mek_data table local function _copy_struct(mek_data) - self.sDB.mek_struct.length = mek_data[1] - self.sDB.mek_struct.width = mek_data[2] - self.sDB.mek_struct.height = mek_data[3] - self.sDB.mek_struct.min_pos = mek_data[4] - self.sDB.mek_struct.max_pos = mek_data[5] - self.sDB.mek_struct.heat_cap = mek_data[6] - self.sDB.mek_struct.fuel_asm = mek_data[7] - self.sDB.mek_struct.fuel_sa = mek_data[8] - self.sDB.mek_struct.fuel_cap = mek_data[9] - self.sDB.mek_struct.waste_cap = mek_data[10] - self.sDB.mek_struct.ccool_cap = mek_data[11] - self.sDB.mek_struct.hcool_cap = mek_data[12] - self.sDB.mek_struct.max_burn = mek_data[13] + local struct = self.sDB.mek_struct + + struct.length = mek_data[1] + struct.width = mek_data[2] + struct.height = mek_data[3] + struct.min_pos = mek_data[4] + struct.max_pos = mek_data[5] + struct.heat_cap = mek_data[6] + struct.fuel_asm = mek_data[7] + struct.fuel_sa = mek_data[8] + struct.fuel_cap = mek_data[9] + struct.waste_cap = mek_data[10] + struct.ccool_cap = mek_data[11] + struct.hcool_cap = mek_data[12] + struct.max_burn = mek_data[13] + end + + -- handle a reactor status packet + ---@param pkt rplc_frame + local function _handle_status(pkt) + local valid = (type(pkt.data[1]) == "number") and (type(pkt.data[2]) == "boolean") and + (type(pkt.data[3]) == "boolean") and (type(pkt.data[4]) == "boolean") and + (type(pkt.data[5]) == "number") + + if valid then + self.sDB.last_status_update = pkt.data[1] + self.sDB.control_state = pkt.data[2] + self.sDB.no_reactor = pkt.data[3] + self.sDB.formed = pkt.data[4] + self.sDB.auto_ack_token = pkt.data[5] + + if (not self.sDB.no_reactor) and self.sDB.formed and (type(pkt.data[6]) == "number") then + self.sDB.mek_status.heating_rate = pkt.data[6] or 0.0 + + -- attempt to read mek_data table + if type(pkt.data[7]) == "table" then + if #pkt.data[7] == 17 then + _copy_status(pkt.data[7]) + self.received_status_cache = true + else + log.error(log_header .. "RPLC status packet reactor data length mismatch") + end + end + end + else + log.debug(log_header .. "RPLC status packet invalid") + end end -- mark this PLC session as closed, stop watchdog @@ -309,13 +350,11 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f ---@param pkt mgmt_frame|rplc_frame local function _handle_packet(pkt) -- check sequence number - if self.r_seq_num == nil then - self.r_seq_num = pkt.scada_frame.seq_num() - elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then + if self.r_seq_num ~= pkt.scada_frame.seq_num() then log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num()) return else - self.r_seq_num = pkt.scada_frame.seq_num() + self.r_seq_num = pkt.scada_frame.seq_num() + 1 end -- process packet @@ -334,48 +373,17 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f if pkt.type == RPLC_TYPE.STATUS then -- status packet received, update data if pkt.length >= 5 then - if (type(pkt.data[1]) == "number") and (type(pkt.data[2]) == "boolean") and (type(pkt.data[3]) == "boolean") and - (type(pkt.data[4]) == "boolean") and (type(pkt.data[5]) == "number") then - self.sDB.last_status_update = pkt.data[1] - self.sDB.control_state = pkt.data[2] - self.sDB.no_reactor = pkt.data[3] - self.sDB.formed = pkt.data[4] - self.sDB.auto_ack_token = pkt.data[5] - - if (not self.sDB.no_reactor) and self.sDB.formed and (type(pkt.data[6]) == "number") then - self.sDB.mek_status.heating_rate = pkt.data[6] or 0.0 - - -- attempt to read mek_data table - if type(pkt.data[7]) == "table" then - local status = pcall(_copy_status, pkt.data[7]) - if status then - -- copied in status data OK - self.received_status_cache = true - else - -- error copying status data - log.error(log_header .. "failed to parse status packet data") - end - end - end - else - log.debug(log_header .. "RPLC status packet invalid") - end + _handle_status(pkt) else log.debug(log_header .. "RPLC status packet length mismatch") end elseif pkt.type == RPLC_TYPE.MEK_STRUCT then -- received reactor structure, record it - if pkt.length == 14 then - local status = pcall(_copy_struct, pkt.data) - if status then - -- copied in structure data OK - _compute_op_temps() - self.received_struct = true - out_queue.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, reactor_id) - else - -- error copying structure data - log.error(log_header .. "failed to parse struct packet data") - end + if pkt.length == 13 then + _copy_struct(pkt.data) + _compute_op_temps() + self.received_struct = true + out_queue.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, reactor_id) else log.debug(log_header .. "RPLC struct packet length mismatch") end diff --git a/supervisor/session/pocket.lua b/supervisor/session/pocket.lua index 48756ea..94a4467 100644 --- a/supervisor/session/pocket.lua +++ b/supervisor/session/pocket.lua @@ -30,12 +30,13 @@ local PERIODICS = { ---@nodiscard ---@param id integer session ID ---@param s_addr integer device source address +---@param i_seq_num integer initial sequence number ---@param in_queue mqueue in message queue ---@param out_queue mqueue out message queue ---@param timeout number communications timeout ---@param facility facility facility data table ---@param fp_ok boolean if the front panel UI is running -function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility, fp_ok) +function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, facility, fp_ok) -- print a log message to the terminal as long as the UI isn't running local function println(message) if not fp_ok then util.println_ts(message) end end @@ -43,8 +44,8 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility, local self = { -- connection properties - seq_num = 0, - r_seq_num = nil, + seq_num = i_seq_num + 2, -- next after the establish approval was sent + r_seq_num = i_seq_num + 1, connected = true, conn_watchdog = util.new_watchdog(timeout), last_rtt = 0, @@ -93,13 +94,11 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility, ---@param pkt mgmt_frame local function _handle_packet(pkt) -- check sequence number - if self.r_seq_num == nil then - self.r_seq_num = pkt.scada_frame.seq_num() - elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then + if self.r_seq_num ~= pkt.scada_frame.seq_num() then log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num()) return else - self.r_seq_num = pkt.scada_frame.seq_num() + self.r_seq_num = pkt.scada_frame.seq_num() + 1 end -- feed watchdog diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 6bebb87..756ee01 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -34,13 +34,14 @@ local PERIODICS = { ---@nodiscard ---@param id integer session ID ---@param s_addr integer device source address +---@param i_seq_num integer initial sequence number ---@param in_queue mqueue in message queue ---@param out_queue mqueue out message queue ---@param timeout number communications timeout ---@param advertisement table RTU device advertisement ---@param facility facility facility data table ---@param fp_ok boolean if the front panel UI is running -function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement, facility, fp_ok) +function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, advertisement, facility, fp_ok) -- print a log message to the terminal as long as the UI isn't running local function println(message) if not fp_ok then util.println_ts(message) end end @@ -51,8 +52,8 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement advert = advertisement, fac_units = facility.get_units(), -- connection properties - seq_num = 0, - r_seq_num = nil, + seq_num = i_seq_num + 2, -- next after the establish approval was sent + r_seq_num = i_seq_num + 1, connected = true, conn_watchdog = util.new_watchdog(timeout), last_rtt = 0, @@ -240,13 +241,11 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement ---@param pkt modbus_frame|mgmt_frame local function _handle_packet(pkt) -- check sequence number - if self.r_seq_num == nil then - self.r_seq_num = pkt.scada_frame.seq_num() - elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then + if self.r_seq_num ~= pkt.scada_frame.seq_num() then log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num()) return else - self.r_seq_num = pkt.scada_frame.seq_num() + self.r_seq_num = pkt.scada_frame.seq_num() + 1 end -- feed watchdog diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 7300162..002dd56 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -273,11 +273,12 @@ end -- establish a new PLC session ---@nodiscard ----@param source_addr integer ----@param for_reactor integer ----@param version string +---@param source_addr integer PLC computer ID +---@param i_seq_num integer initial (most recent) sequence number +---@param for_reactor integer unit ID +---@param version string PLC version ---@return integer|false session_id -function svsessions.establish_plc_session(source_addr, for_reactor, version) +function svsessions.establish_plc_session(source_addr, i_seq_num, for_reactor, version) if svsessions.get_reactor_session(for_reactor) == nil and for_reactor >= 1 and for_reactor <= self.config.UnitCount then ---@class plc_session_struct local plc_s = { @@ -294,7 +295,7 @@ function svsessions.establish_plc_session(source_addr, for_reactor, version) local id = self.next_ids.plc - plc_s.instance = plc.new_session(id, source_addr, for_reactor, plc_s.in_queue, plc_s.out_queue, self.config.PLC_Timeout, self.fp_ok) + plc_s.instance = plc.new_session(id, source_addr, i_seq_num, for_reactor, plc_s.in_queue, plc_s.out_queue, self.config.PLC_Timeout, self.fp_ok) table.insert(self.sessions.plc, plc_s) local units = self.facility.get_units() @@ -320,13 +321,14 @@ function svsessions.establish_plc_session(source_addr, for_reactor, version) end end --- establish a new RTU session +-- establish a new RTU gateway session ---@nodiscard ----@param source_addr integer ----@param advertisement table ----@param version string +---@param source_addr integer RTU gateway computer ID +---@param i_seq_num integer initial (most recent) sequence number +---@param advertisement table RTU capability advertisement +---@param version string RTU gateway version ---@return integer session_id -function svsessions.establish_rtu_session(source_addr, advertisement, version) +function svsessions.establish_rtu_session(source_addr, i_seq_num, advertisement, version) ---@class rtu_session_struct local rtu_s = { s_type = "rtu", @@ -341,7 +343,7 @@ function svsessions.establish_rtu_session(source_addr, advertisement, version) local id = self.next_ids.rtu - rtu_s.instance = rtu.new_session(id, source_addr, rtu_s.in_queue, rtu_s.out_queue, self.config.RTU_Timeout, advertisement, self.facility, self.fp_ok) + rtu_s.instance = rtu.new_session(id, source_addr, i_seq_num, rtu_s.in_queue, rtu_s.out_queue, self.config.RTU_Timeout, advertisement, self.facility, self.fp_ok) table.insert(self.sessions.rtu, rtu_s) local mt = { @@ -362,10 +364,11 @@ end -- establish a new coordinator session ---@nodiscard ----@param source_addr integer ----@param version string +---@param source_addr integer coordinator computer ID +---@param i_seq_num integer initial (most recent) sequence number +---@param version string coordinator version ---@return integer|false session_id -function svsessions.establish_crd_session(source_addr, version) +function svsessions.establish_crd_session(source_addr, i_seq_num, version) if svsessions.get_crd_session() == nil then ---@class crd_session_struct local crd_s = { @@ -381,7 +384,7 @@ function svsessions.establish_crd_session(source_addr, version) local id = self.next_ids.crd - crd_s.instance = coordinator.new_session(id, source_addr, crd_s.in_queue, crd_s.out_queue, self.config.CRD_Timeout, self.facility, self.fp_ok) + crd_s.instance = coordinator.new_session(id, source_addr, i_seq_num, crd_s.in_queue, crd_s.out_queue, self.config.CRD_Timeout, self.facility, self.fp_ok) table.insert(self.sessions.crd, crd_s) local mt = { @@ -406,10 +409,11 @@ end -- establish a new pocket diagnostics session ---@nodiscard ----@param source_addr integer ----@param version string +---@param source_addr integer pocket computer ID +---@param i_seq_num integer initial (most recent) sequence number +---@param version string pocket version ---@return integer|false session_id -function svsessions.establish_pdg_session(source_addr, version) +function svsessions.establish_pdg_session(source_addr, i_seq_num, version) ---@class pdg_session_struct local pdg_s = { s_type = "pkt", @@ -424,7 +428,7 @@ function svsessions.establish_pdg_session(source_addr, version) local id = self.next_ids.pdg - pdg_s.instance = pocket.new_session(id, source_addr, pdg_s.in_queue, pdg_s.out_queue, self.config.PKT_Timeout, self.facility, self.fp_ok) + pdg_s.instance = pocket.new_session(id, source_addr, i_seq_num, pdg_s.in_queue, pdg_s.out_queue, self.config.PKT_Timeout, self.facility, self.fp_ok) table.insert(self.sessions.pdg, pdg_s) local mt = { diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 89c38d8..edc60f1 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.3.12" +local SUPERVISOR_VERSION = "v1.4.0" local println = util.println local println_ts = util.println_ts @@ -214,7 +214,7 @@ local function main() elseif event == "modem_message" then -- got a packet local packet = superv_comms.parse_packet(param1, param2, param3, param4, param5) - superv_comms.handle_packet(packet) + if packet then superv_comms.handle_packet(packet) end elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then -- handle a mouse event diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 1d79e72..69a98f5 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -191,283 +191,282 @@ function supervisor.comms(_version, nic, fp_ok) end -- handle a packet - ---@param packet modbus_frame|rplc_frame|mgmt_frame|crdn_frame|nil + ---@param packet modbus_frame|rplc_frame|mgmt_frame|crdn_frame function public.handle_packet(packet) - if packet ~= nil then - local l_chan = packet.scada_frame.local_channel() - local r_chan = packet.scada_frame.remote_channel() - local src_addr = packet.scada_frame.src_addr() - local protocol = packet.scada_frame.protocol() + local l_chan = packet.scada_frame.local_channel() + local r_chan = packet.scada_frame.remote_channel() + local src_addr = packet.scada_frame.src_addr() + local protocol = packet.scada_frame.protocol() + local i_seq_num = packet.scada_frame.seq_num() - if l_chan ~= config.SVR_Channel then - log.debug("received packet on unconfigured channel " .. l_chan, true) - elseif r_chan == config.PLC_Channel then - -- look for an associated session - local session = svsessions.find_plc_session(src_addr) + if l_chan ~= config.SVR_Channel then + log.debug("received packet on unconfigured channel " .. l_chan, true) + elseif r_chan == config.PLC_Channel then + -- look for an associated session + local session = svsessions.find_plc_session(src_addr) - if protocol == PROTOCOL.RPLC then - ---@cast packet rplc_frame - -- reactor PLC packet - if session ~= nil then - -- pass the packet onto the session handler - session.in_queue.push_packet(packet) - else - -- any other packet should be session related, discard it - log.debug("discarding RPLC packet without a known session") - end - elseif protocol == PROTOCOL.SCADA_MGMT then - ---@cast packet mgmt_frame - -- SCADA management packet - if session ~= nil then - -- pass the packet onto the session handler - session.in_queue.push_packet(packet) - elseif packet.type == MGMT_TYPE.ESTABLISH then - -- establish a new session - local last_ack = self.last_est_acks[src_addr] - - -- validate packet and continue - if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then - local comms_v = packet.data[1] - local firmware_v = packet.data[2] - local dev_type = packet.data[3] - - if comms_v ~= comms.version then - if last_ack ~= ESTABLISH_ACK.BAD_VERSION then - log.info(util.c("dropping PLC establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) - end - - _send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) - elseif dev_type == DEVICE_TYPE.PLC then - -- PLC linking request - if packet.length == 4 and type(packet.data[4]) == "number" then - local reactor_id = packet.data[4] - local plc_id = svsessions.establish_plc_session(src_addr, reactor_id, firmware_v) - - if plc_id == false then - -- reactor already has a PLC assigned - if last_ack ~= ESTABLISH_ACK.COLLISION then - log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id)) - end - - _send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION) - else - -- got an ID; assigned to a reactor successfully - println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \xbb reactor ", reactor_id, " connected")) - log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [@", src_addr, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id)) - _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW) - end - else - log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type") - _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) - end - else - log.debug(util.c("illegal establish packet for device ", dev_type, " on PLC channel")) - _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) - end - else - log.debug("invalid establish packet (on PLC channel)") - _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) - end - else - -- any other packet should be session related, discard it - log.debug(util.c("discarding PLC SCADA_MGMT packet without a known session from computer ", src_addr)) - end + if protocol == PROTOCOL.RPLC then + ---@cast packet rplc_frame + -- reactor PLC packet + if session ~= nil then + -- pass the packet onto the session handler + session.in_queue.push_packet(packet) else - log.debug(util.c("illegal packet type ", protocol, " on PLC channel")) + -- any other packet should be session related, discard it + log.debug("discarding RPLC packet without a known session") end - elseif r_chan == config.RTU_Channel then - -- look for an associated session - local session = svsessions.find_rtu_session(src_addr) + elseif protocol == PROTOCOL.SCADA_MGMT then + ---@cast packet mgmt_frame + -- SCADA management packet + if session ~= nil then + -- pass the packet onto the session handler + session.in_queue.push_packet(packet) + elseif packet.type == MGMT_TYPE.ESTABLISH then + -- establish a new session + local last_ack = self.last_est_acks[src_addr] - if protocol == PROTOCOL.MODBUS_TCP then - ---@cast packet modbus_frame - -- MODBUS response - if session ~= nil then - -- pass the packet onto the session handler - session.in_queue.push_packet(packet) - else - -- any other packet should be session related, discard it - log.debug("discarding MODBUS_TCP packet without a known session") - end - elseif protocol == PROTOCOL.SCADA_MGMT then - ---@cast packet mgmt_frame - -- SCADA management packet - if session ~= nil then - -- pass the packet onto the session handler - session.in_queue.push_packet(packet) - elseif packet.type == MGMT_TYPE.ESTABLISH then - -- establish a new session - local last_ack = self.last_est_acks[src_addr] + -- validate packet and continue + if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then + local comms_v = packet.data[1] + local firmware_v = packet.data[2] + local dev_type = packet.data[3] - -- validate packet and continue - if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then - local comms_v = packet.data[1] - local firmware_v = packet.data[2] - local dev_type = packet.data[3] - - if comms_v ~= comms.version then - if last_ack ~= ESTABLISH_ACK.BAD_VERSION then - log.info(util.c("dropping RTU establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) - end - - _send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) - elseif dev_type == DEVICE_TYPE.RTU then - if packet.length == 4 then - -- this is an RTU advertisement for a new session - local rtu_advert = packet.data[4] - local s_id = svsessions.establish_rtu_session(src_addr, rtu_advert, firmware_v) - - println(util.c("RTU (", firmware_v, ") [@", src_addr, "] \xbb connected")) - log.info(util.c("RTU_ESTABLISH: RTU (",firmware_v, ") [@", src_addr, "] connected with session ID ", s_id)) - _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW) - else - log.debug("RTU_ESTABLISH: packet length mismatch") - _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) - end - else - log.debug(util.c("illegal establish packet for device ", dev_type, " on RTU channel")) - _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) + if comms_v ~= comms.version then + if last_ack ~= ESTABLISH_ACK.BAD_VERSION then + log.info(util.c("dropping PLC establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) end - else - log.debug("invalid establish packet (on RTU channel)") - _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) - end - else - -- any other packet should be session related, discard it - log.debug(util.c("discarding RTU SCADA_MGMT packet without a known session from computer ", src_addr)) - end - else - log.debug(util.c("illegal packet type ", protocol, " on RTU channel")) - end - elseif r_chan == config.CRD_Channel then - -- look for an associated session - local session = svsessions.find_crd_session(src_addr) - if protocol == PROTOCOL.SCADA_MGMT then - ---@cast packet mgmt_frame - -- SCADA management packet - if session ~= nil then - -- pass the packet onto the session handler - session.in_queue.push_packet(packet) - elseif packet.type == MGMT_TYPE.ESTABLISH then - -- establish a new session - local last_ack = self.last_est_acks[src_addr] + _send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) + elseif dev_type == DEVICE_TYPE.PLC then + -- PLC linking request + if packet.length == 4 and type(packet.data[4]) == "number" then + local reactor_id = packet.data[4] + local plc_id = svsessions.establish_plc_session(src_addr, i_seq_num, reactor_id, firmware_v) - -- validate packet and continue - if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then - local comms_v = packet.data[1] - local firmware_v = packet.data[2] - local dev_type = packet.data[3] - - if comms_v ~= comms.version then - if last_ack ~= ESTABLISH_ACK.BAD_VERSION then - log.info(util.c("dropping coordinator establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) - end - - _send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) - elseif dev_type == DEVICE_TYPE.CRD then - -- this is an attempt to establish a new coordinator session - local s_id = svsessions.establish_crd_session(src_addr, firmware_v) - - if s_id ~= false then - println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected")) - log.info(util.c("CRD_ESTABLISH: coordinator (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id)) - - _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { config.UnitCount, cooling_conf }) - else + if plc_id == false then + -- reactor already has a PLC assigned if last_ack ~= ESTABLISH_ACK.COLLISION then - log.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator") + log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id)) end _send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION) + else + -- got an ID; assigned to a reactor successfully + println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \xbb reactor ", reactor_id, " connected")) + log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [@", src_addr, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id)) + _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW) end else - log.debug(util.c("illegal establish packet for device ", dev_type, " on coordinator channel")) + log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type") _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) end else - log.debug("CRD_ESTABLISH: establish packet length mismatch") + log.debug(util.c("illegal establish packet for device ", dev_type, " on PLC channel")) _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) end else - -- any other packet should be session related, discard it - log.debug(util.c("discarding coordinator SCADA_MGMT packet without a known session from computer ", src_addr)) - end - elseif protocol == PROTOCOL.SCADA_CRDN then - ---@cast packet crdn_frame - -- coordinator packet - if session ~= nil then - -- pass the packet onto the session handler - session.in_queue.push_packet(packet) - else - -- any other packet should be session related, discard it - log.debug(util.c("discarding coordinator SCADA_CRDN packet without a known session from computer ", src_addr)) + log.debug("invalid establish packet (on PLC channel)") + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) end else - log.debug(util.c("illegal packet type ", protocol, " on coordinator channel")) - end - elseif r_chan == config.PKT_Channel then - -- look for an associated session - local session = svsessions.find_pdg_session(src_addr) - - if protocol == PROTOCOL.SCADA_MGMT then - ---@cast packet mgmt_frame - -- SCADA management packet - if session ~= nil then - -- pass the packet onto the session handler - session.in_queue.push_packet(packet) - elseif packet.type == MGMT_TYPE.ESTABLISH then - -- establish a new session - local last_ack = self.last_est_acks[src_addr] - - -- validate packet and continue - if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then - local comms_v = packet.data[1] - local firmware_v = packet.data[2] - local dev_type = packet.data[3] - - if comms_v ~= comms.version then - if last_ack ~= ESTABLISH_ACK.BAD_VERSION then - log.info(util.c("dropping PDG establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) - end - - _send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) - elseif dev_type == DEVICE_TYPE.PKT then - -- this is an attempt to establish a new pocket diagnostic session - local s_id = svsessions.establish_pdg_session(src_addr, firmware_v) - - println(util.c("PKT (", firmware_v, ") [@", src_addr, "] \xbb connected")) - log.info(util.c("PDG_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id)) - - _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW) - else - log.debug(util.c("illegal establish packet for device ", dev_type, " on pocket channel")) - _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) - end - else - log.debug("PDG_ESTABLISH: establish packet length mismatch") - _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) - end - else - -- any other packet should be session related, discard it - log.debug(util.c("discarding pocket SCADA_MGMT packet without a known session from computer ", src_addr)) - end - elseif protocol == PROTOCOL.SCADA_CRDN then - ---@cast packet crdn_frame - -- coordinator packet - if session ~= nil then - -- pass the packet onto the session handler - session.in_queue.push_packet(packet) - else - -- any other packet should be session related, discard it - log.debug(util.c("discarding pocket SCADA_CRDN packet without a known session from computer ", src_addr)) - end - else - log.debug(util.c("illegal packet type ", protocol, " on pocket channel")) + -- any other packet should be session related, discard it + log.debug(util.c("discarding PLC SCADA_MGMT packet without a known session from computer ", src_addr)) end else - log.debug("received packet for unknown channel " .. r_chan, true) + log.debug(util.c("illegal packet type ", protocol, " on PLC channel")) end + elseif r_chan == config.RTU_Channel then + -- look for an associated session + local session = svsessions.find_rtu_session(src_addr) + + if protocol == PROTOCOL.MODBUS_TCP then + ---@cast packet modbus_frame + -- MODBUS response + if session ~= nil then + -- pass the packet onto the session handler + session.in_queue.push_packet(packet) + else + -- any other packet should be session related, discard it + log.debug("discarding MODBUS_TCP packet without a known session") + end + elseif protocol == PROTOCOL.SCADA_MGMT then + ---@cast packet mgmt_frame + -- SCADA management packet + if session ~= nil then + -- pass the packet onto the session handler + session.in_queue.push_packet(packet) + elseif packet.type == MGMT_TYPE.ESTABLISH then + -- establish a new session + local last_ack = self.last_est_acks[src_addr] + + -- validate packet and continue + if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then + local comms_v = packet.data[1] + local firmware_v = packet.data[2] + local dev_type = packet.data[3] + + if comms_v ~= comms.version then + if last_ack ~= ESTABLISH_ACK.BAD_VERSION then + log.info(util.c("dropping RTU establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) + end + + _send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) + elseif dev_type == DEVICE_TYPE.RTU then + if packet.length == 4 then + -- this is an RTU advertisement for a new session + local rtu_advert = packet.data[4] + local s_id = svsessions.establish_rtu_session(src_addr, i_seq_num, rtu_advert, firmware_v) + + println(util.c("RTU (", firmware_v, ") [@", src_addr, "] \xbb connected")) + log.info(util.c("RTU_ESTABLISH: RTU (",firmware_v, ") [@", src_addr, "] connected with session ID ", s_id)) + _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW) + else + log.debug("RTU_ESTABLISH: packet length mismatch") + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) + end + else + log.debug(util.c("illegal establish packet for device ", dev_type, " on RTU channel")) + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) + end + else + log.debug("invalid establish packet (on RTU channel)") + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) + end + else + -- any other packet should be session related, discard it + log.debug(util.c("discarding RTU SCADA_MGMT packet without a known session from computer ", src_addr)) + end + else + log.debug(util.c("illegal packet type ", protocol, " on RTU channel")) + end + elseif r_chan == config.CRD_Channel then + -- look for an associated session + local session = svsessions.find_crd_session(src_addr) + + if protocol == PROTOCOL.SCADA_MGMT then + ---@cast packet mgmt_frame + -- SCADA management packet + if session ~= nil then + -- pass the packet onto the session handler + session.in_queue.push_packet(packet) + elseif packet.type == MGMT_TYPE.ESTABLISH then + -- establish a new session + local last_ack = self.last_est_acks[src_addr] + + -- validate packet and continue + if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then + local comms_v = packet.data[1] + local firmware_v = packet.data[2] + local dev_type = packet.data[3] + + if comms_v ~= comms.version then + if last_ack ~= ESTABLISH_ACK.BAD_VERSION then + log.info(util.c("dropping coordinator establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) + end + + _send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) + elseif dev_type == DEVICE_TYPE.CRD then + -- this is an attempt to establish a new coordinator session + local s_id = svsessions.establish_crd_session(src_addr, i_seq_num, firmware_v) + + if s_id ~= false then + println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected")) + log.info(util.c("CRD_ESTABLISH: coordinator (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id)) + + _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { config.UnitCount, cooling_conf }) + else + if last_ack ~= ESTABLISH_ACK.COLLISION then + log.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator") + end + + _send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION) + end + else + log.debug(util.c("illegal establish packet for device ", dev_type, " on coordinator channel")) + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) + end + else + log.debug("CRD_ESTABLISH: establish packet length mismatch") + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) + end + else + -- any other packet should be session related, discard it + log.debug(util.c("discarding coordinator SCADA_MGMT packet without a known session from computer ", src_addr)) + end + elseif protocol == PROTOCOL.SCADA_CRDN then + ---@cast packet crdn_frame + -- coordinator packet + if session ~= nil then + -- pass the packet onto the session handler + session.in_queue.push_packet(packet) + else + -- any other packet should be session related, discard it + log.debug(util.c("discarding coordinator SCADA_CRDN packet without a known session from computer ", src_addr)) + end + else + log.debug(util.c("illegal packet type ", protocol, " on coordinator channel")) + end + elseif r_chan == config.PKT_Channel then + -- look for an associated session + local session = svsessions.find_pdg_session(src_addr) + + if protocol == PROTOCOL.SCADA_MGMT then + ---@cast packet mgmt_frame + -- SCADA management packet + if session ~= nil then + -- pass the packet onto the session handler + session.in_queue.push_packet(packet) + elseif packet.type == MGMT_TYPE.ESTABLISH then + -- establish a new session + local last_ack = self.last_est_acks[src_addr] + + -- validate packet and continue + if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then + local comms_v = packet.data[1] + local firmware_v = packet.data[2] + local dev_type = packet.data[3] + + if comms_v ~= comms.version then + if last_ack ~= ESTABLISH_ACK.BAD_VERSION then + log.info(util.c("dropping PDG establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) + end + + _send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) + elseif dev_type == DEVICE_TYPE.PKT then + -- this is an attempt to establish a new pocket diagnostic session + local s_id = svsessions.establish_pdg_session(src_addr, i_seq_num, firmware_v) + + println(util.c("PKT (", firmware_v, ") [@", src_addr, "] \xbb connected")) + log.info(util.c("PDG_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id)) + + _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW) + else + log.debug(util.c("illegal establish packet for device ", dev_type, " on pocket channel")) + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) + end + else + log.debug("PDG_ESTABLISH: establish packet length mismatch") + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) + end + else + -- any other packet should be session related, discard it + log.debug(util.c("discarding pocket SCADA_MGMT packet without a known session from computer ", src_addr)) + end + elseif protocol == PROTOCOL.SCADA_CRDN then + ---@cast packet crdn_frame + -- coordinator packet + if session ~= nil then + -- pass the packet onto the session handler + session.in_queue.push_packet(packet) + else + -- any other packet should be session related, discard it + log.debug(util.c("discarding pocket SCADA_CRDN packet without a known session from computer ", src_addr)) + end + else + log.debug(util.c("illegal packet type ", protocol, " on pocket channel")) + end + else + log.debug("received packet for unknown channel " .. r_chan, true) end end