diff --git a/coordinator/config.lua b/coordinator/config.lua index 3196e80..ecb7599 100644 --- a/coordinator/config.lua +++ b/coordinator/config.lua @@ -1,11 +1,11 @@ local config = {} --- port of the SCADA supervisor -config.SCADA_SV_PORT = 16100 --- port to listen to incoming packets from supervisor -config.SCADA_SV_CTL_LISTEN = 16101 --- listen port for SCADA coordinator API access -config.SCADA_API_LISTEN = 16200 +-- supervisor comms channel +config.SVR_CHANNEL = 16240 +-- coordinator comms channel +config.CRD_CHANNEL = 16243 +-- pocket comms channel +config.PKT_CHANNEL = 16244 -- max trusted modem message distance (0 to disable check) config.TRUSTED_RANGE = 0 -- time in seconds (>= 2) before assuming a remote device is no longer active diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index 55c6356..f6e8038 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -213,14 +213,15 @@ end ---@nodiscard ---@param version string coordinator version ---@param modem table modem device ----@param sv_port integer port of configured supervisor ----@param sv_listen integer listening port for supervisor replys ----@param api_listen integer listening port for pocket API +---@param crd_channel integer port of configured supervisor +---@param svr_channel integer listening port for supervisor replys +---@param pkt_channel integer listening port for pocket API ---@param range integer trusted device connection range ---@param sv_watchdog watchdog -function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range, sv_watchdog) +function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel, range, sv_watchdog) local self = { sv_linked = false, + sv_addr = comms.BROADCAST, sv_seq_num = 0, sv_r_seq_num = nil, sv_config_err = false, @@ -236,8 +237,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range -- configure modem channels local function _conf_channels() modem.closeAll() - modem.open(sv_listen) - modem.open(api_listen) + modem.open(crd_channel) end _conf_channels() @@ -261,23 +261,24 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range end pkt.make(msg_type, msg) - s_pkt.make(self.sv_seq_num, protocol, pkt.raw_sendable()) + s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable()) - modem.transmit(sv_port, sv_listen, s_pkt.raw_sendable()) + modem.transmit(svr_channel, crd_channel, s_pkt.raw_sendable()) self.sv_seq_num = self.sv_seq_num + 1 end -- send an API establish request response - ---@param dest integer - ---@param msg table - local function _send_api_establish_ack(seq_id, dest, msg) + ---@param packet scada_packet + ---@param ack ESTABLISH_ACK + local function _send_api_establish_ack(packet, ack) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() - m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg) - s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, { ack }) + s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - modem.transmit(dest, api_listen, s_pkt.raw_sendable()) + modem.transmit(pkt_channel, crd_channel, s_pkt.raw_sendable()) + self.last_api_est_acks[packet.src_addr()] = ack end -- attempt connection establishment @@ -307,7 +308,9 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range -- close the connection to the server function public.close() sv_watchdog.cancel() + self.sv_addr = comms.BROADCAST self.sv_linked = false + self.sv_r_seq_num = nil _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {}) end @@ -436,15 +439,18 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range ---@param packet mgmt_frame|crdn_frame|capi_frame|nil function public.handle_packet(packet) if packet ~= nil then - local l_port = packet.scada_frame.local_port() - local r_port = packet.scada_frame.remote_port() + 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() - if l_port == api_listen then + if l_chan ~= crd_channel then + log.debug("received packet on unconfigured channel " .. l_chan, true) + elseif r_chan == pkt_channel then if protocol == PROTOCOL.COORD_API then ---@cast packet capi_frame -- look for an associated session - local session = apisessions.find_session(r_port) + local session = apisessions.find_session(src_addr) -- API packet if session ~= nil then @@ -457,7 +463,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range elseif protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame -- look for an associated session - local session = apisessions.find_session(r_port) + local session = apisessions.find_session(src_addr) -- SCADA management packet if session ~= nil then @@ -465,8 +471,6 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range session.in_queue.push_packet(packet) elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then -- establish a new session - local next_seq_id = packet.scada_frame.seq_num() + 1 - -- 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] @@ -474,42 +478,43 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range local dev_type = packet.data[3] if comms_v ~= comms.version then - if self.last_api_est_acks[r_port] ~= ESTABLISH_ACK.BAD_VERSION then + if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_VERSION then log.info(util.c("dropping API establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) - self.last_api_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION end - _send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION }) + _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) elseif dev_type == DEVICE_TYPE.PKT then -- pocket linking request - local id = apisessions.establish_session(l_port, r_port, firmware_v) - println(util.c("API: pocket (", firmware_v, ") [:", r_port, "] connected with session ID ", id)) - coordinator.log_comms(util.c("API: pocket (", firmware_v, ") [:", r_port, "] connected with session ID ", id)) + local id = apisessions.establish_session(src_addr, firmware_v) + println(util.c("[API] pocket (", firmware_v, ") [@", src_addr, "] \xbb connected")) + coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id)) - _send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW }) - self.last_api_est_acks[r_port] = ESTABLISH_ACK.ALLOW + _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW) else - log.debug(util.c("illegal establish packet for device ", dev_type, " on API listening channel")) - _send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) + log.debug(util.c("API_ESTABLISH: illegal establish packet for device ", dev_type, " on pocket channel")) + _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY) end else log.debug("invalid establish packet (on API listening channel)") - _send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) + _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY) end else -- any other packet should be session related, discard it - log.debug(util.c(r_port, "->", l_port, ": discarding SCADA_MGMT packet without a known session")) + log.debug(util.c("discarding pocket SCADA_MGMT packet without a known session from computer ", src_addr)) end else - log.debug("illegal packet type " .. protocol .. " on api listening channel", true) + log.debug("illegal packet type " .. protocol .. " on pocket channel", true) end - elseif l_port == sv_listen then + elseif r_chan == 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 log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) return + 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 else self.sv_r_seq_num = packet.scada_frame.seq_num() end @@ -660,6 +665,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range -- init io controller iocontrol.init(conf, public) + self.sv_addr = src_addr self.sv_linked = true self.sv_config_err = false else @@ -705,10 +711,10 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range local trip_time = util.time() - timestamp if trip_time > 750 then - log.warning("coord KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)") + log.warning("coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)") end - -- log.debug("coord RTT = " .. trip_time .. "ms") + -- log.debug("coordinator RTT = " .. trip_time .. "ms") iocontrol.get_db().facility.ps.publish("sv_ping", trip_time) @@ -719,7 +725,9 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range elseif packet.type == SCADA_MGMT_TYPE.CLOSE then -- handle session close sv_watchdog.cancel() + self.sv_addr = comms.BROADCAST self.sv_linked = false + self.sv_r_seq_num = nil println_ts("server connection closed by remote host") log.info("server connection closed by remote host") else @@ -732,7 +740,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range log.debug("illegal packet type " .. protocol .. " on supervisor listening channel", true) end else - log.debug("received packet on unconfigured channel " .. l_port, true) + log.debug("received packet for unknown channel " .. r_chan, true) end end end diff --git a/coordinator/session/apisessions.lua b/coordinator/session/apisessions.lua index c6d5a20..17988f5 100644 --- a/coordinator/session/apisessions.lua +++ b/coordinator/session/apisessions.lua @@ -5,7 +5,7 @@ local util = require("scada-common.util") local config = require("coordinator.config") -local api = require("coordinator.session.api") +local pocket = require("coordinator.session.pocket") local apisessions = {} @@ -18,7 +18,7 @@ local self = { -- PRIVATE FUNCTIONS -- -- handle a session output queue ----@param session api_session_struct +---@param session pkt_session_struct local function _api_handle_outq(session) -- record handler start time local handle_start = util.time() @@ -31,7 +31,7 @@ local function _api_handle_outq(session) if msg ~= nil then if msg.qtype == mqueue.TYPE.PACKET then -- handle a packet to be sent - self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) + self.modem.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message.raw_sendable()) elseif msg.qtype == mqueue.TYPE.COMMAND then -- handle instruction/notification elseif msg.qtype == mqueue.TYPE.DATA then @@ -41,15 +41,15 @@ local function _api_handle_outq(session) -- max 100ms spent processing queue if util.time() - handle_start > 100 then - log.warning("API out queue handler exceeded 100ms queue process limit") - log.warning(util.c("offending session: port ", session.r_port)) + log.warning("[API] out queue handler exceeded 100ms queue process limit") + log.warning(util.c("[API] offending session: ", session)) break end end end -- cleanly close a session ----@param session api_session_struct +---@param session pkt_session_struct local function _shutdown(session) session.open = false session.instance.close() @@ -58,11 +58,11 @@ local function _shutdown(session) while session.out_queue.ready() do local msg = session.out_queue.pop() if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then - self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) + self.modem.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message.raw_sendable()) end end - log.debug(util.c("closed API session ", session.instance.get_id(), " on remote port ", session.r_port)) + log.debug(util.c("[API] closed session ", session)) end -- PUBLIC FUNCTIONS -- @@ -81,54 +81,60 @@ end -- find a session by remote port ---@nodiscard ----@param port integer ----@return api_session_struct|nil -function apisessions.find_session(port) +---@param source_addr integer +---@return pkt_session_struct|nil +function apisessions.find_session(source_addr) for i = 1, #self.sessions do - if self.sessions[i].r_port == port then return self.sessions[i] end + if self.sessions[i].s_addr == source_addr then return self.sessions[i] end end return nil end -- establish a new API session ---@nodiscard ----@param local_port integer ----@param remote_port integer +---@param source_addr integer ---@param version string ---@return integer session_id -function apisessions.establish_session(local_port, remote_port, version) - ---@class api_session_struct - local api_s = { +function apisessions.establish_session(source_addr, version) + ---@class pkt_session_struct + local pkt_s = { open = true, version = version, - l_port = local_port, - r_port = remote_port, + s_addr = source_addr, in_queue = mqueue.new(), out_queue = mqueue.new(), - instance = nil ---@type api_session + instance = nil ---@type pkt_session } - api_s.instance = api.new_session(self.next_id, api_s.in_queue, api_s.out_queue, config.API_TIMEOUT) - table.insert(self.sessions, api_s) + local id = self.next_id - log.debug(util.c("established new API session to ", remote_port, " with ID ", self.next_id)) + pkt_s.instance = pocket.new_session(id, source_addr, pkt_s.in_queue, pkt_s.out_queue, config.API_TIMEOUT) + table.insert(self.sessions, pkt_s) - self.next_id = self.next_id + 1 + local mt = { + ---@param s pkt_session_struct + __tostring = function (s) return util.c("PKT [", id, "] (@", s.s_addr, ")") end + } + + setmetatable(pkt_s, mt) + + log.debug(util.c("[API] established new session: ", pkt_s)) + + self.next_id = id + 1 -- success - return api_s.instance.get_id() + return pkt_s.instance.get_id() end -- attempt to identify which session's watchdog timer fired ---@param timer_event number function apisessions.check_all_watchdogs(timer_event) for i = 1, #self.sessions do - local session = self.sessions[i] ---@type api_session_struct + local session = self.sessions[i] ---@type pkt_session_struct if session.open then local triggered = session.instance.check_wd(timer_event) if triggered then - log.debug(util.c("watchdog closing API session ", session.instance.get_id(), - " on remote port ", session.r_port, "...")) + log.debug(util.c("[API] watchdog closing session ", session, "...")) _shutdown(session) end end @@ -138,7 +144,7 @@ end -- iterate all the API sessions function apisessions.iterate_all() for i = 1, #self.sessions do - local session = self.sessions[i] ---@type api_session_struct + local session = self.sessions[i] ---@type pkt_session_struct if session.open and session.instance.iterate() then _api_handle_outq(session) @@ -152,10 +158,9 @@ end function apisessions.free_all_closed() local f = function (session) return session.open end - ---@param session api_session_struct + ---@param session pkt_session_struct local on_delete = function (session) - log.debug(util.c("free'ing closed API session ", session.instance.get_id(), - " on remote port ", session.r_port)) + log.debug(util.c("[API] free'ing closed session ", session)) end util.filter_table(self.sessions, f, on_delete) @@ -164,7 +169,7 @@ end -- close all open connections function apisessions.close_all() for i = 1, #self.sessions do - local session = self.sessions[i] ---@type api_session_struct + local session = self.sessions[i] ---@type pkt_session_struct if session.open then _shutdown(session) end end diff --git a/coordinator/session/api.lua b/coordinator/session/pocket.lua similarity index 89% rename from coordinator/session/api.lua rename to coordinator/session/pocket.lua index 4ba7383..ddabdda 100644 --- a/coordinator/session/api.lua +++ b/coordinator/session/pocket.lua @@ -3,7 +3,7 @@ local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") local util = require("scada-common.util") -local api = {} +local pocket = {} local PROTOCOL = comms.PROTOCOL -- local CAPI_TYPE = comms.CAPI_TYPE @@ -21,8 +21,8 @@ local API_S_CMDS = { local API_S_DATA = { } -api.API_S_CMDS = API_S_CMDS -api.API_S_DATA = API_S_DATA +pocket.API_S_CMDS = API_S_CMDS +pocket.API_S_DATA = API_S_DATA local PERIODICS = { KEEP_ALIVE = 2000 @@ -31,11 +31,12 @@ local PERIODICS = { -- pocket API session ---@nodiscard ---@param id integer session ID +---@param s_addr integer device source address ---@param in_queue mqueue in message queue ---@param out_queue mqueue out message queue ---@param timeout number communications timeout -function api.new_session(id, in_queue, out_queue, timeout) - local log_header = "api_session(" .. id .. "): " +function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) + local log_header = "pkt_session(" .. id .. "): " local self = { -- connection properties @@ -61,10 +62,10 @@ function api.new_session(id, in_queue, out_queue, timeout) } } - ---@class api_session + ---@class pkt_session local public = {} - -- mark this API session as closed, stop watchdog + -- mark this pocket session as closed, stop watchdog local function _close() self.conn_watchdog.cancel() self.connected = false @@ -92,7 +93,7 @@ function api.new_session(id, in_queue, out_queue, timeout) local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 @@ -134,11 +135,11 @@ function api.new_session(id, in_queue, out_queue, timeout) self.last_rtt = srv_now - srv_start if self.last_rtt > 750 then - log.warning(log_header .. "API KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)") + log.warning(log_header .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)") end - -- log.debug(log_header .. "API RTT = " .. self.last_rtt .. "ms") - -- log.debug(log_header .. "API TT = " .. (srv_now - api_send) .. "ms") + -- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms") + -- log.debug(log_header .. "PKT TT = " .. (srv_now - api_send) .. "ms") else log.debug(log_header .. "SCADA keep alive packet length mismatch") end @@ -171,7 +172,7 @@ function api.new_session(id, in_queue, out_queue, timeout) function public.close() _close() _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) - println("connection to API session " .. id .. " closed by server") + println("connection to pocket session " .. id .. " closed by server") log.info(log_header .. "session closed by server") end @@ -210,7 +211,7 @@ function api.new_session(id, in_queue, out_queue, timeout) -- exit if connection was closed if not self.connected then - println("connection to API session " .. id .. " closed by remote host") + println("connection to pocket session " .. id .. " closed by remote host") log.info(log_header .. "session closed by remote host") return self.connected end @@ -246,4 +247,4 @@ function api.new_session(id, in_queue, out_queue, timeout) return public end -return api +return pocket diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 4a08166..5d4fec8 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -20,7 +20,7 @@ local sounder = require("coordinator.sounder") local apisessions = require("coordinator.session.apisessions") -local COORDINATOR_VERSION = "v0.15.8" +local COORDINATOR_VERSION = "v0.16.0" local println = util.println local println_ts = util.println_ts @@ -37,9 +37,9 @@ local log_comms_connecting = coordinator.log_comms_connecting local cfv = util.new_validator() -cfv.assert_port(config.SCADA_SV_PORT) -cfv.assert_port(config.SCADA_SV_CTL_LISTEN) -cfv.assert_port(config.SCADA_API_LISTEN) +cfv.assert_channel(config.SVR_CHANNEL) +cfv.assert_channel(config.CRD_CHANNEL) +cfv.assert_channel(config.PKT_CHANNEL) cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_num(config.SV_TIMEOUT) cfv.assert_min(config.SV_TIMEOUT, 2) @@ -148,8 +148,8 @@ local function main() log.debug("startup> conn watchdog created") -- start comms, open all channels - local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.SCADA_SV_PORT, config.SCADA_SV_CTL_LISTEN, - config.SCADA_API_LISTEN, config.TRUSTED_RANGE, conn_watchdog) + local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.CRD_CHANNEL, config.SVR_CHANNEL, + config.PKT_CHANNEL, config.TRUSTED_RANGE, conn_watchdog) log.debug("startup> comms init") log_comms("comms initialized") @@ -163,7 +163,7 @@ local function main() -- attempt to connect to the supervisor or exit local function init_connect_sv() - local tick_waiting, task_done = log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SCADA_SV_PORT) + local tick_waiting, task_done = log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SVR_CHANNEL) -- attempt to establish a connection with the supervisory computer if not coord_comms.sv_connect(60, tick_waiting, task_done) then diff --git a/pocket/config.lua b/pocket/config.lua index 27e1489..0c35b59 100644 --- a/pocket/config.lua +++ b/pocket/config.lua @@ -1,11 +1,11 @@ local config = {} --- port of the SCADA supervisor -config.SCADA_SV_PORT = 16100 --- port for SCADA coordinator API access -config.SCADA_API_PORT = 16200 --- port to listen to incoming packets FROM servers -config.LISTEN_PORT = 16201 +-- supervisor comms channel +config.SVR_CHANNEL = 16240 +-- coordinator comms channel +config.CRD_CHANNEL = 16243 +-- pocket comms channel +config.PKT_CHANNEL = 16244 -- max trusted modem message distance (0 to disable check) config.TRUSTED_RANGE = 0 -- time in seconds (>= 2) before assuming a remote device is no longer active diff --git a/pocket/pocket.lua b/pocket/pocket.lua index cc4d870..0281e92 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -18,22 +18,24 @@ local pocket = {} ---@nodiscard ---@param version string pocket version ---@param modem table modem device ----@param local_port integer local pocket port ----@param sv_port integer port of supervisor ----@param api_port integer port of coordinator API +---@param pkt_channel integer pocket comms channel +---@param svr_channel integer supervisor access channel +---@param crd_channel integer coordinator access channel ---@param range integer trusted device connection range ---@param sv_watchdog watchdog ---@param api_watchdog watchdog -function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_watchdog, api_watchdog) +function pocket.comms(version, modem, pkt_channel, svr_channel, crd_channel, range, sv_watchdog, api_watchdog) local self = { sv = { linked = false, + addr = comms.BROADCAST, seq_num = 0, 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 last_est_ack = ESTABLISH_ACK.ALLOW @@ -48,7 +50,7 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w -- configure modem channels local function _conf_channels() modem.closeAll() - modem.open(local_port) + modem.open(pkt_channel) end _conf_channels() @@ -61,9 +63,9 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w local pkt = comms.mgmt_packet() pkt.make(msg_type, msg) - s_pkt.make(self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) + s_pkt.make(self.sv.addr, self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) - modem.transmit(sv_port, local_port, s_pkt.raw_sendable()) + modem.transmit(svr_channel, pkt_channel, s_pkt.raw_sendable()) self.sv.seq_num = self.sv.seq_num + 1 end @@ -75,9 +77,9 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w local pkt = comms.mgmt_packet() pkt.make(msg_type, msg) - s_pkt.make(self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) + s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) - modem.transmit(api_port, local_port, s_pkt.raw_sendable()) + modem.transmit(crd_channel, pkt_channel, s_pkt.raw_sendable()) self.api.seq_num = self.api.seq_num + 1 end @@ -89,9 +91,9 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w -- local pkt = comms.capi_packet() -- pkt.make(msg_type, msg) - -- s_pkt.make(self.api.seq_num, PROTOCOL.COORD_API, pkt.raw_sendable()) + -- s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.COORD_API, pkt.raw_sendable()) - -- modem.transmit(api_port, local_port, s_pkt.raw_sendable()) + -- modem.transmit(crd_channel, pkt_channel, s_pkt.raw_sendable()) -- self.api.seq_num = self.api.seq_num + 1 -- end @@ -133,6 +135,8 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w function public.close_sv() sv_watchdog.cancel() self.sv.linked = false + self.sv.r_seq_num = nil + self.sv.addr = comms.BROADCAST _send_sv(SCADA_MGMT_TYPE.CLOSE, {}) end @@ -140,6 +144,8 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w function public.close_api() api_watchdog.cancel() self.api.linked = false + self.api.r_seq_num = nil + self.api.addr = comms.BROADCAST _send_crd(SCADA_MGMT_TYPE.CLOSE, {}) end @@ -214,18 +220,23 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w ---@param packet mgmt_frame|capi_frame|nil function public.handle_packet(packet) if packet ~= nil then - local l_port = packet.scada_frame.local_port() - local r_port = packet.scada_frame.remote_port() + local l_chan = packet.scada_frame.local_channel() + local r_chan = packet.scada_frame.remote_channel() local protocol = packet.scada_frame.protocol() + local src_addr = packet.scada_frame.src_addr() - if l_port ~= local_port then - log.debug("received packet on unconfigured channel " .. l_port, true) - elseif r_port == api_port then + if l_chan ~= pkt_channel then + log.debug("received packet on unconfigured channel " .. l_chan, true) + elseif r_chan == 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 - log.warning("sequence out-of-order: last = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) + 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 + log.debug("received packet from unknown computer " .. src_addr .. " while linked (API expected " .. self.api.addr .. + "); channel in use by another system?") return else self.api.r_seq_num = packet.scada_frame.seq_num() @@ -247,6 +258,7 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w log.info("coordinator connection established") self.establish_delay_counter = 0 self.api.linked = true + self.api.addr = src_addr if self.sv.linked then coreio.report_link_state(LINK_STATE.LINKED) @@ -294,6 +306,8 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w -- handle session close api_watchdog.cancel() self.api.linked = false + self.api.r_seq_num = nil + self.api.addr = comms.BROADCAST log.info("coordinator server connection closed by remote host") else log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator") @@ -304,12 +318,16 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w else log.debug("illegal packet type " .. protocol .. " from coordinator", true) end - elseif r_port == sv_port then + elseif r_chan == 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 - log.warning("sequence out-of-order: last = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) + 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 + log.debug("received packet from unknown computer " .. src_addr .. " while linked (SVR expected " .. self.sv.addr .. + "); channel in use by another system?") return else self.sv.r_seq_num = packet.scada_frame.seq_num() @@ -330,6 +348,7 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w log.info("supervisor connection established") self.establish_delay_counter = 0 self.sv.linked = true + self.sv.addr = src_addr if self.api.linked then coreio.report_link_state(LINK_STATE.LINKED) @@ -377,6 +396,8 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w -- handle session close sv_watchdog.cancel() self.sv.linked = false + self.sv.r_seq_num = nil + self.sv.addr = comms.BROADCAST log.info("supervisor server connection closed by remote host") else log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor") @@ -388,7 +409,7 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w log.debug("illegal packet type " .. protocol .. " from supervisor", true) end else - log.debug("received packet from unconfigured channel " .. r_port, true) + log.debug("received packet from unconfigured channel " .. r_chan, true) end end end diff --git a/pocket/startup.lua b/pocket/startup.lua index 32d4737..0683af6 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -17,7 +17,7 @@ local coreio = require("pocket.coreio") local pocket = require("pocket.pocket") local renderer = require("pocket.renderer") -local POCKET_VERSION = "alpha-v0.3.7" +local POCKET_VERSION = "alpha-v0.4.4" local println = util.println local println_ts = util.println_ts @@ -28,9 +28,9 @@ local println_ts = util.println_ts local cfv = util.new_validator() -cfv.assert_port(config.SCADA_SV_PORT) -cfv.assert_port(config.SCADA_API_PORT) -cfv.assert_port(config.LISTEN_PORT) +cfv.assert_channel(config.SVR_CHANNEL) +cfv.assert_channel(config.CRD_CHANNEL) +cfv.assert_channel(config.PKT_CHANNEL) cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_num(config.COMMS_TIMEOUT) cfv.assert_min(config.COMMS_TIMEOUT, 2) @@ -89,8 +89,8 @@ local function main() log.debug("startup> conn watchdogs created") -- start comms, open all channels - local pocket_comms = pocket.comms(POCKET_VERSION, modem, config.LISTEN_PORT, config.SCADA_SV_PORT, - config.SCADA_API_PORT, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api) + local pocket_comms = pocket.comms(POCKET_VERSION, modem, config.PKT_CHANNEL, config.SVR_CHANNEL, + config.CRD_CHANNEL, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api) log.debug("startup> comms init") -- base loop clock (2Hz, 10 ticks) diff --git a/reactor-plc/config.lua b/reactor-plc/config.lua index e402bbb..3462b2c 100644 --- a/reactor-plc/config.lua +++ b/reactor-plc/config.lua @@ -9,10 +9,10 @@ config.REACTOR_ID = 1 -- when emergency coolant is needed due to low coolant -- config.EMERGENCY_COOL = { side = "right", color = nil } --- port to send packets TO server -config.SERVER_PORT = 16000 --- port to listen to incoming packets FROM server -config.LISTEN_PORT = 14001 +-- supervisor comms channel +config.SVR_CHANNEL = 16240 +-- PLC comms channel +config.PLC_CHANNEL = 16241 -- max trusted modem message distance (0 to disable check) config.TRUSTED_RANGE = 0 -- time in seconds (>= 2) before assuming a remote device is no longer active diff --git a/reactor-plc/panel/front_panel.lua b/reactor-plc/panel/front_panel.lua index 8e28a75..12c8266 100644 --- a/reactor-plc/panel/front_panel.lua +++ b/reactor-plc/panel/front_panel.lua @@ -2,6 +2,7 @@ -- Main SCADA Coordinator GUI -- +local types = require("scada-common.types") local util = require("scada-common.util") local config = require("reactor-plc.config") @@ -49,7 +50,7 @@ local function init(panel) local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green} local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)} local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}} - network.update(5) + network.update(types.PANEL_LINK_STATE.DISCONNECTED) system.line_break() reactor.register(databus.ps, "reactor_dev_state", reactor.update) @@ -69,6 +70,10 @@ local function init(panel) rt_cmrx.register(databus.ps, "routine__comms_rx", rt_cmrx.update) rt_sctl.register(databus.ps, "routine__spctl", rt_sctl.update) +---@diagnostic disable-next-line: undefined-field + local comp_id = util.sprintf("(%d)", os.getComputerID()) + TextBox{parent=system,x=9,y=5,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)} + -- -- status & controls -- diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 4f04138..02e592a 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -446,14 +446,15 @@ end ---@param id integer reactor ID ---@param version string PLC version ---@param modem table modem device ----@param local_port integer local listening port ----@param server_port integer remote server port +---@param plc_channel integer PLC comms channel +---@param svr_channel integer supervisor server channel ---@param range integer trusted device connection range ---@param reactor table reactor device ---@param rps rps RPS reference ---@param conn_watchdog watchdog watchdog reference -function plc.comms(id, version, modem, local_port, server_port, range, reactor, rps, conn_watchdog) +function plc.comms(id, version, modem, plc_channel, svr_channel, range, reactor, rps, conn_watchdog) local self = { + sv_addr = comms.BROADCAST, seq_num = 0, r_seq_num = nil, scrammed = false, @@ -472,7 +473,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, -- configure modem channels local function _conf_channels() modem.closeAll() - modem.open(local_port) + modem.open(plc_channel) end _conf_channels() @@ -485,9 +486,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, local r_pkt = comms.rplc_packet() r_pkt.make(id, msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable()) + s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable()) - modem.transmit(server_port, local_port, s_pkt.raw_sendable()) + modem.transmit(svr_channel, plc_channel, s_pkt.raw_sendable()) self.seq_num = self.seq_num + 1 end @@ -499,9 +500,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - modem.transmit(server_port, local_port, s_pkt.raw_sendable()) + modem.transmit(svr_channel, plc_channel, s_pkt.raw_sendable()) self.seq_num = self.seq_num + 1 end @@ -667,9 +668,11 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, -- unlink from the server function public.unlink() + self.sv_addr = comms.BROADCAST self.linked = false self.r_seq_num = nil self.status_cache = nil + databus.tx_link_state(types.PANEL_LINK_STATE.DISCONNECTED) end -- close the connection to the server @@ -731,7 +734,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, end end - -- parse an RPLC packet + -- parse a packet ---@nodiscard ---@param side string ---@param sender integer @@ -760,14 +763,14 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, pkt = mgmt_pkt.get() end else - log.debug("illegal packet type " .. s_pkt.protocol(), true) + log.debug("unsupported packet type " .. s_pkt.protocol(), true) end end return pkt end - -- handle an RPLC packet + -- handle RPLC and MGMT packets ---@param packet rplc_frame|mgmt_frame packet frame ---@param plc_state plc_state PLC state ---@param setpoints setpoints setpoint control table @@ -775,16 +778,22 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, -- print a log message to the terminal as long as the UI isn't running local function println_ts(message) if not plc_state.fp_ok then util.println_ts(message) end end - local l_port = packet.scada_frame.local_port() + local protocol = packet.scada_frame.protocol() + local l_chan = packet.scada_frame.local_channel() + local src_addr = packet.scada_frame.src_addr() -- handle packets now that we have prints setup - if l_port == local_port then + if l_chan == 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 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 + log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr .. + "); channel in use by another system?") + return else self.r_seq_num = packet.scada_frame.seq_num() end @@ -792,11 +801,10 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, -- feed the watchdog first so it doesn't uhh...eat our packets :) conn_watchdog.feed() - local protocol = packet.scada_frame.protocol() - -- handle packet if protocol == PROTOCOL.RPLC then ---@cast packet rplc_frame + -- if linked, only accept packets from configured supervisor if self.linked then if packet.type == RPLC_TYPE.STATUS then -- request of full status, clear cache first @@ -933,6 +941,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, end elseif protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame + -- if linked, only accept packets from configured supervisor if self.linked then if packet.type == SCADA_MGMT_TYPE.ESTABLISH then -- link request confirmation @@ -945,22 +954,26 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, self.status_cache = nil _send_struct() public.send_status(plc_state.no_reactor, plc_state.reactor_formed) - log.debug("re-sent initial status data") - elseif est_ack == ESTABLISH_ACK.DENY then - println_ts("received unsolicited link denial, unlinking") - log.warning("unsolicited establish request denied") - elseif est_ack == ESTABLISH_ACK.COLLISION then - println_ts("received unsolicited link collision, unlinking") - log.warning("unsolicited establish request collision") - elseif est_ack == ESTABLISH_ACK.BAD_VERSION then - println_ts("received unsolicited link version mismatch, unlinking") - log.warning("unsolicited establish request version mismatch") + log.debug("re-sent initial status data due to re-establish") else - println_ts("invalid unsolicited link response") - log.debug("unsolicited unknown establish request response") - end + if est_ack == ESTABLISH_ACK.DENY then + println_ts("received unsolicited link denial, unlinking") + log.warning("unsolicited establish request denied") + elseif est_ack == ESTABLISH_ACK.COLLISION then + println_ts("received unsolicited link collision, unlinking") + log.warning("unsolicited establish request collision") + elseif est_ack == ESTABLISH_ACK.BAD_VERSION then + println_ts("received unsolicited link version mismatch, unlinking") + log.warning("unsolicited establish request version mismatch") + else + println_ts("invalid unsolicited link response") + log.debug("unsolicited unknown establish request response") + end - self.linked = est_ack == ESTABLISH_ACK.ALLOW + -- unlink + self.sv_addr = comms.BROADCAST + self.linked = false + end -- clear this since this is for something that was unsolicited self.last_est_ack = ESTABLISH_ACK.ALLOW @@ -980,7 +993,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, log.warning("PLC KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)") end - -- log.debug("RPLC RTT = " .. trip_time .. "ms") + -- log.debug("PLC RTT = " .. trip_time .. "ms") _send_keep_alive_ack(timestamp) else @@ -1002,9 +1015,11 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, if est_ack == ESTABLISH_ACK.ALLOW then println_ts("linked!") - log.info("supervisor establish request approved, PLC is linked") + log.info("supervisor establish request approved, linked to SV (CID#" .. src_addr .. ")") - -- reset remote sequence number and cache + -- link + reset remote sequence number and cache + self.sv_addr = src_addr + self.linked = true self.r_seq_num = nil self.status_cache = nil @@ -1012,23 +1027,28 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, public.send_status(plc_state.no_reactor, plc_state.reactor_formed) log.debug("sent initial status data") - elseif self.last_est_ack ~= est_ack then - if est_ack == ESTABLISH_ACK.DENY then - println_ts("link request denied, retrying...") - log.info("supervisor establish request denied, retrying") - elseif est_ack == ESTABLISH_ACK.COLLISION then - println_ts("reactor PLC ID collision (check config), retrying...") - log.warning("establish request collision, retrying") - elseif est_ack == ESTABLISH_ACK.BAD_VERSION then - println_ts("supervisor version mismatch (try updating), retrying...") - log.warning("establish request version mismatch, retrying") - else - println_ts("invalid link response, bad channel? retrying...") - log.error("unknown establish request response, retrying") + else + if self.last_est_ack ~= est_ack then + if est_ack == ESTABLISH_ACK.DENY then + println_ts("link request denied, retrying...") + log.info("supervisor establish request denied, retrying") + elseif est_ack == ESTABLISH_ACK.COLLISION then + println_ts("reactor PLC ID collision (check config), retrying...") + log.warning("establish request collision, retrying") + elseif est_ack == ESTABLISH_ACK.BAD_VERSION then + println_ts("supervisor version mismatch (try updating), retrying...") + log.warning("establish request version mismatch, retrying") + else + println_ts("invalid link response, bad channel? retrying...") + log.error("unknown establish request response, retrying") + end end + + -- unlink + self.sv_addr = comms.BROADCAST + self.linked = false end - self.linked = est_ack == ESTABLISH_ACK.ALLOW self.last_est_ack = est_ack -- report link state @@ -1044,7 +1064,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, log.error("illegal packet type " .. protocol, true) end else - log.debug("received packet on unconfigured channel " .. l_port, true) + log.debug("received packet on unconfigured channel " .. l_chan, true) end end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 812ed45..227bd27 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.3.7" +local R_PLC_VERSION = "v1.4.5" local println = util.println local println_ts = util.println_ts @@ -31,8 +31,8 @@ local cfv = util.new_validator() cfv.assert_type_bool(config.NETWORKED) cfv.assert_type_int(config.REACTOR_ID) -cfv.assert_port(config.SERVER_PORT) -cfv.assert_port(config.LISTEN_PORT) +cfv.assert_channel(config.SVR_CHANNEL) +cfv.assert_channel(config.PLC_CHANNEL) cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_num(config.COMMS_TIMEOUT) cfv.assert_min(config.COMMS_TIMEOUT, 2) @@ -197,7 +197,7 @@ local function main() log.debug("init> conn watchdog started") -- start comms - smem_sys.plc_comms = plc.comms(config.REACTOR_ID, R_PLC_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, + smem_sys.plc_comms = plc.comms(config.REACTOR_ID, R_PLC_VERSION, smem_dev.modem, config.PLC_CHANNEL, config.SVR_CHANNEL, config.TRUSTED_RANGE, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog) log.debug("init> comms init") else diff --git a/rtu/config.lua b/rtu/config.lua index 1b96bec..2279759 100644 --- a/rtu/config.lua +++ b/rtu/config.lua @@ -2,11 +2,11 @@ local rsio = require("scada-common.rsio") local config = {} --- port to send packets TO server -config.SERVER_PORT = 16000 --- port to listen to incoming packets FROM server -config.LISTEN_PORT = 15001 --- max trusted modem message distance (< 1 to disable check) +-- supervisor comms channel +config.SVR_CHANNEL = 16240 +-- RTU/MODBUS comms channel +config.RTU_CHANNEL = 16242 +-- max trusted modem message distance (0 to disable check) config.TRUSTED_RANGE = 0 -- time in seconds (>= 2) before assuming a remote device is no longer active config.COMMS_TIMEOUT = 5 diff --git a/rtu/panel/front_panel.lua b/rtu/panel/front_panel.lua index 606abbb..467386e 100644 --- a/rtu/panel/front_panel.lua +++ b/rtu/panel/front_panel.lua @@ -2,6 +2,7 @@ -- Main SCADA Coordinator GUI -- +local types = require("scada-common.types") local util = require("scada-common.util") local databus = require("rtu.databus") @@ -53,7 +54,7 @@ local function init(panel, units) local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)} local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}} - network.update(5) + network.update(types.PANEL_LINK_STATE.DISCONNECTED) system.line_break() modem.register(databus.ps, "has_modem", modem.update) @@ -66,6 +67,10 @@ local function init(panel, units) rt_main.register(databus.ps, "routine__main", rt_main.update) rt_comm.register(databus.ps, "routine__comms", rt_comm.update) +---@diagnostic disable-next-line: undefined-field + local comp_id = util.sprintf("(%d)", os.getComputerID()) + TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)} + -- -- about label -- diff --git a/rtu/rtu.lua b/rtu/rtu.lua index bb43706..831e231 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -159,12 +159,13 @@ end ---@nodiscard ---@param version string RTU version ---@param modem table modem device ----@param local_port integer local listening port ----@param server_port integer remote server port +---@param rtu_channel integer PLC comms channel +---@param svr_channel integer supervisor server channel ---@param range integer trusted device connection range ---@param conn_watchdog watchdog watchdog reference -function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog) +function rtu.comms(version, modem, rtu_channel, svr_channel, range, conn_watchdog) local self = { + sv_addr = comms.BROADCAST, seq_num = 0, r_seq_num = nil, txn_id = 0, @@ -180,7 +181,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog -- configure modem channels local function _conf_channels() modem.closeAll() - modem.open(local_port) + modem.open(rtu_channel) end _conf_channels() @@ -193,9 +194,9 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - modem.transmit(server_port, local_port, s_pkt.raw_sendable()) + modem.transmit(svr_channel, rtu_channel, s_pkt.raw_sendable()) self.seq_num = self.seq_num + 1 end @@ -238,8 +239,8 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog ---@param m_pkt modbus_packet function public.send_modbus(m_pkt) local s_pkt = comms.scada_packet() - s_pkt.make(self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) - modem.transmit(server_port, local_port, s_pkt.raw_sendable()) + s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) + modem.transmit(svr_channel, rtu_channel, s_pkt.raw_sendable()) self.seq_num = self.seq_num + 1 end @@ -254,7 +255,9 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog ---@param rtu_state rtu_state function public.unlink(rtu_state) rtu_state.linked = false + self.sv_addr = comms.BROADCAST self.r_seq_num = nil + databus.tx_link_state(types.PANEL_LINK_STATE.DISCONNECTED) end -- close the connection to the server @@ -327,13 +330,21 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog -- print a log message to the terminal as long as the UI isn't running local function println_ts(message) if not rtu_state.fp_ok then util.println_ts(message) end end - if packet.scada_frame.local_port() == local_port then + local protocol = packet.scada_frame.protocol() + local l_chan = packet.scada_frame.local_channel() + local src_addr = packet.scada_frame.src_addr() + + if l_chan == 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 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 + log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr .. + "); channel in use by another system?") + return else self.r_seq_num = packet.scada_frame.seq_num() end @@ -341,8 +352,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog -- feed watchdog on valid sequence number conn_watchdog.feed() - local protocol = packet.scada_frame.protocol() - + -- handle packet if protocol == PROTOCOL.MODBUS_TCP then ---@cast packet modbus_frame if rtu_state.linked then @@ -398,6 +408,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog if est_ack == ESTABLISH_ACK.ALLOW then -- 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") @@ -461,6 +472,8 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog -- should be unreachable assuming packet is from parse_packet() log.error("illegal packet type " .. protocol, true) end + else + log.debug("received packet on unconfigured channel " .. l_chan, true) end end diff --git a/rtu/startup.lua b/rtu/startup.lua index 4cb90ac..05b131b 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -28,7 +28,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.2.8" +local RTU_VERSION = "v1.3.5" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE @@ -42,8 +42,8 @@ local println_ts = util.println_ts local cfv = util.new_validator() -cfv.assert_port(config.SERVER_PORT) -cfv.assert_port(config.LISTEN_PORT) +cfv.assert_channel(config.SVR_CHANNEL) +cfv.assert_channel(config.RTU_CHANNEL) cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_num(config.COMMS_TIMEOUT) cfv.assert_min(config.COMMS_TIMEOUT, 2) @@ -467,7 +467,7 @@ local function main() log.debug("startup> conn watchdog started") -- setup comms - smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, + smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.RTU_CHANNEL, config.SVR_CHANNEL, config.TRUSTED_RANGE, smem_sys.conn_watchdog) log.debug("startup> comms init") diff --git a/scada-common/comms.lua b/scada-common/comms.lua index fb54101..c5e9fd2 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -4,14 +4,17 @@ local log = require("scada-common.log") +local insert = table.insert + +---@diagnostic disable-next-line: undefined-field +local C_ID = os.getComputerID() ---@type integer computer ID + +local max_distance = nil ---@type number|nil maximum acceptable transmission distance + ---@class comms local comms = {} -local insert = table.insert - -local max_distance = nil - -comms.version = "1.4.1" +comms.version = "2.0.0" ---@enum PROTOCOL local PROTOCOL = { @@ -122,27 +125,28 @@ comms.PLC_AUTO_ACK = PLC_AUTO_ACK comms.UNIT_COMMAND = UNIT_COMMAND comms.FAC_COMMAND = FAC_COMMAND +-- destination broadcast address (to all devices) +comms.BROADCAST = -1 + ---@alias packet scada_packet|modbus_packet|rplc_packet|mgmt_packet|crdn_packet|capi_packet ---@alias frame modbus_frame|rplc_frame|mgmt_frame|crdn_frame|capi_frame -- configure the maximum allowable message receive distance
-- packets received with distances greater than this will be silently discarded ----@param distance integer max modem message distance (less than 1 disables the limit) +---@param distance integer max modem message distance (0 disables the limit) function comms.set_trusted_range(distance) - if distance < 1 then - max_distance = nil - else - max_distance = distance - end + if distance == 0 then max_distance = nil else max_distance = distance end end -- generic SCADA packet object ---@nodiscard function comms.scada_packet() local self = { - modem_msg_in = nil, + modem_msg_in = nil, ---@type modem_message|nil valid = false, - raw = { -1, PROTOCOL.SCADA_MGMT, {} }, + raw = {}, + src_addr = comms.BROADCAST, + dest_addr = comms.BROADCAST, seq_num = -1, protocol = PROTOCOL.SCADA_MGMT, length = 0, @@ -153,34 +157,40 @@ function comms.scada_packet() local public = {} -- make a SCADA packet - ---@param seq_num integer + ---@param dest_addr integer destination computer address (ID) + ---@param seq_num integer sequence number ---@param protocol PROTOCOL ---@param payload table - function public.make(seq_num, protocol, payload) + function public.make(dest_addr, seq_num, protocol, payload) self.valid = true +---@diagnostic disable-next-line: undefined-field + self.src_addr = C_ID + self.dest_addr = dest_addr self.seq_num = seq_num self.protocol = protocol self.length = #payload self.payload = payload - self.raw = { self.seq_num, self.protocol, self.payload } + self.raw = { self.src_addr, self.dest_addr, self.seq_num, self.protocol, self.payload } end -- parse in a modem message as a SCADA packet ---@param side string modem side - ---@param sender integer sender port - ---@param reply_to integer reply port + ---@param sender integer sender channel + ---@param reply_to integer reply channel ---@param message any message body ---@param distance integer transmission distance ---@return boolean valid valid message received function public.receive(side, sender, reply_to, message, distance) + ---@class modem_message self.modem_msg_in = { iface = side, - s_port = sender, - r_port = reply_to, + s_channel = sender, + r_channel = reply_to, msg = message, dist = distance } + self.valid = false self.raw = self.modem_msg_in.msg if (type(max_distance) == "number") and (distance > max_distance) then @@ -188,20 +198,31 @@ function comms.scada_packet() -- log.debug("comms.scada_packet.receive(): discarding packet with distance " .. distance .. " outside of trusted range") else if type(self.raw) == "table" then - if #self.raw >= 3 then - self.seq_num = self.raw[1] - self.protocol = self.raw[2] + if #self.raw == 5 then + self.src_addr = self.raw[1] + self.dest_addr = self.raw[2] + self.seq_num = self.raw[3] + self.protocol = self.raw[4] - -- element 3 must be a table - if type(self.raw[3]) == "table" then - self.length = #self.raw[3] - self.payload = self.raw[3] + -- element 5 must be a table + if type(self.raw[5]) == "table" then + self.length = #self.raw[5] + self.payload = self.raw[5] end + else + self.src_addr = nil + self.dest_addr = nil + self.seq_num = nil + self.protocol = nil + self.length = 0 + self.payload = {} end - self.valid = type(self.seq_num) == "number" and - type(self.protocol) == "number" and - type(self.payload) == "table" + -- check if this packet is destined for this device + local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == C_ID) + + self.valid = is_destination and type(self.src_addr) == "number" and type(self.dest_addr) == "number" and + type(self.seq_num) == "number" and type(self.protocol) == "number" and type(self.payload) == "table" end end @@ -216,13 +237,17 @@ function comms.scada_packet() function public.raw_sendable() return self.raw end ---@nodiscard - function public.local_port() return self.modem_msg_in.s_port end + function public.local_channel() return self.modem_msg_in.s_channel end ---@nodiscard - function public.remote_port() return self.modem_msg_in.r_port end + function public.remote_channel() return self.modem_msg_in.r_channel end ---@nodiscard function public.is_valid() return self.valid end + ---@nodiscard + function public.src_addr() return self.src_addr end + ---@nodiscard + function public.dest_addr() return self.dest_addr end ---@nodiscard function public.seq_num() return self.seq_num end ---@nodiscard diff --git a/scada-common/types.lua b/scada-common/types.lua index 8df01c1..21429b5 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -74,6 +74,15 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end -- ENUMERATION TYPES -- --#region +---@enum PANEL_LINK_STATE +types.PANEL_LINK_STATE = { + LINKED = 1, + DENIED = 2, + COLLISION = 3, + BAD_VERSION = 4, + DISCONNECTED = 5 +} + ---@enum RTU_UNIT_TYPE types.RTU_UNIT_TYPE = { VIRTUAL = 0, -- virtual device diff --git a/scada-common/util.lua b/scada-common/util.lua index 063143c..99f62a6 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -65,7 +65,8 @@ end ---@return string function util.strval(val) local t = type(val) - if t == "table" or t == "function" then + -- this depends on Lua short-circuiting the or check for metatables (note: metatables won't have metatables) + if (t == "table" and (getmetatable(val) == nil or getmetatable(val).__tostring == nil)) or t == "function" then return "[" .. tostring(val) .. "]" else return tostring(val) @@ -539,7 +540,7 @@ function util.new_validator() function public.assert_range(check, min, max) valid = valid and check >= min and check <= max end function public.assert_range_ex(check, min, max) valid = valid and check > min and check < max end - function public.assert_port(port) valid = valid and type(port) == "number" and port >= 0 and port <= 65535 end + function public.assert_channel(channel) valid = valid and type(channel) == "number" and channel >= 0 and channel <= 65535 end -- check if all assertions passed successfully ---@nodiscard diff --git a/supervisor/config.lua b/supervisor/config.lua index b716d38..a4a595b 100644 --- a/supervisor/config.lua +++ b/supervisor/config.lua @@ -1,9 +1,15 @@ local config = {} --- scada network listen for PLC's and RTU's -config.SCADA_DEV_LISTEN = 16000 --- listen port for SCADA supervisor access -config.SCADA_SV_CTL_LISTEN = 16100 +-- supervisor comms channel +config.SVR_CHANNEL = 16240 +-- PLC comms channel +config.PLC_CHANNEL = 16241 +-- RTU/MODBUS comms channel +config.RTU_CHANNEL = 16242 +-- coordinator comms channel +config.CRD_CHANNEL = 16243 +-- pocket comms channel +config.PKT_CHANNEL = 16244 -- max trusted modem message distance (0 to disable check) config.TRUSTED_RANGE = 0 -- time in seconds (>= 2) before assuming a remote device is no longer active diff --git a/supervisor/databus.lua b/supervisor/databus.lua index a1e947e..00185c7 100644 --- a/supervisor/databus.lua +++ b/supervisor/databus.lua @@ -3,9 +3,14 @@ -- local psil = require("scada-common.psil") +local util = require("scada-common.util") local pgi = require("supervisor.panel.pgi") +-- nominal RTT is ping (0ms to 10ms usually) + 150ms for SV main loop tick +local WARN_RTT = 300 -- 2x as long as expected w/ 0 ping +local HIGH_RTT = 500 -- 3.33x as long as expected w/ 0 ping + local databus = {} -- databus PSIL @@ -31,11 +36,11 @@ end -- transmit PLC firmware version and session connection state ---@param reactor_id integer reactor unit ID ---@param fw string firmware version ----@param channel integer PLC remote port -function databus.tx_plc_connected(reactor_id, fw, channel) +---@param s_addr integer PLC computer ID +function databus.tx_plc_connected(reactor_id, fw, s_addr) databus.ps.publish("plc_" .. reactor_id .. "_fw", fw) databus.ps.publish("plc_" .. reactor_id .. "_conn", true) - databus.ps.publish("plc_" .. reactor_id .. "_chan", tostring(channel)) + databus.ps.publish("plc_" .. reactor_id .. "_addr", util.sprintf("@% 4d", s_addr)) end -- transmit PLC disconnected @@ -43,7 +48,7 @@ end function databus.tx_plc_disconnected(reactor_id) databus.ps.publish("plc_" .. reactor_id .. "_fw", " ------- ") databus.ps.publish("plc_" .. reactor_id .. "_conn", false) - databus.ps.publish("plc_" .. reactor_id .. "_chan", " --- ") + databus.ps.publish("plc_" .. reactor_id .. "_addr", " --- ") databus.ps.publish("plc_" .. reactor_id .. "_rtt", 0) databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.lightGray) end @@ -54,9 +59,9 @@ end function databus.tx_plc_rtt(reactor_id, rtt) databus.ps.publish("plc_" .. reactor_id .. "_rtt", rtt) - if rtt > 700 then + if rtt > HIGH_RTT then databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.red) - elseif rtt > 300 then + elseif rtt > WARN_RTT then databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.yellow_hc) else databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.green) @@ -66,10 +71,10 @@ end -- transmit RTU firmware version and session connection state ---@param session_id integer RTU session ---@param fw string firmware version ----@param channel integer RTU remote port -function databus.tx_rtu_connected(session_id, fw, channel) +---@param s_addr integer RTU computer ID +function databus.tx_rtu_connected(session_id, fw, s_addr) databus.ps.publish("rtu_" .. session_id .. "_fw", fw) - databus.ps.publish("rtu_" .. session_id .. "_chan", tostring(channel)) + databus.ps.publish("rtu_" .. session_id .. "_addr", util.sprintf("@ C% 3d", s_addr)) pgi.create_rtu_entry(session_id) end @@ -85,9 +90,9 @@ end function databus.tx_rtu_rtt(session_id, rtt) databus.ps.publish("rtu_" .. session_id .. "_rtt", rtt) - if rtt > 700 then + if rtt > HIGH_RTT then databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.red) - elseif rtt > 300 then + elseif rtt > WARN_RTT then databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.yellow_hc) else databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.green) @@ -103,18 +108,18 @@ end -- transmit coordinator firmware version and session connection state ---@param fw string firmware version ----@param channel integer coordinator remote port -function databus.tx_crd_connected(fw, channel) +---@param s_addr integer coordinator computer ID +function databus.tx_crd_connected(fw, s_addr) databus.ps.publish("crd_fw", fw) databus.ps.publish("crd_conn", true) - databus.ps.publish("crd_chan", tostring(channel)) + databus.ps.publish("crd_addr", tostring(s_addr)) end -- transmit coordinator disconnected function databus.tx_crd_disconnected() databus.ps.publish("crd_fw", " ------- ") databus.ps.publish("crd_conn", false) - databus.ps.publish("crd_chan", "---") + databus.ps.publish("crd_addr", "---") databus.ps.publish("crd_rtt", 0) databus.ps.publish("crd_rtt_color", colors.lightGray) end @@ -124,9 +129,9 @@ end function databus.tx_crd_rtt(rtt) databus.ps.publish("crd_rtt", rtt) - if rtt > 700 then + if rtt > HIGH_RTT then databus.ps.publish("crd_rtt_color", colors.red) - elseif rtt > 300 then + elseif rtt > WARN_RTT then databus.ps.publish("crd_rtt_color", colors.yellow_hc) else databus.ps.publish("crd_rtt_color", colors.green) @@ -136,10 +141,10 @@ end -- transmit PKT firmware version and PDG session connection state ---@param session_id integer PDG session ---@param fw string firmware version ----@param channel integer PDG remote port -function databus.tx_pdg_connected(session_id, fw, channel) +---@param s_addr integer PDG computer ID +function databus.tx_pdg_connected(session_id, fw, s_addr) databus.ps.publish("pdg_" .. session_id .. "_fw", fw) - databus.ps.publish("pdg_" .. session_id .. "_chan", tostring(channel)) + databus.ps.publish("pdg_" .. session_id .. "_addr", util.sprintf("@ C% 3d", s_addr)) pgi.create_pdg_entry(session_id) end @@ -155,9 +160,9 @@ end function databus.tx_pdg_rtt(session_id, rtt) databus.ps.publish("pdg_" .. session_id .. "_rtt", rtt) - if rtt > 700 then + if rtt > HIGH_RTT then databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.red) - elseif rtt > 300 then + elseif rtt > WARN_RTT then databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.yellow_hc) else databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.green) diff --git a/supervisor/panel/components/pdg_entry.lua b/supervisor/panel/components/pdg_entry.lua index 94cd385..1e49fca 100644 --- a/supervisor/panel/components/pdg_entry.lua +++ b/supervisor/panel/components/pdg_entry.lua @@ -2,8 +2,6 @@ -- Pocket Diagnostics Connection Entry -- -local util = require("scada-common.util") - local databus = require("supervisor.databus") local core = require("graphics.core") @@ -28,9 +26,9 @@ local function init(parent, id) local ps_prefix = "pdg_" .. id .. "_" TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} - local pdg_chan = TextBox{parent=entry,x=1,y=2,text=" :00000",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)} + local pdg_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)} TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} - pdg_chan.register(databus.ps, ps_prefix .. "chan", function (channel) pdg_chan.set_value(util.sprintf(" :%05d", channel)) end) + pdg_addr.register(databus.ps, ps_prefix .. "addr", pdg_addr.set_value) TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1} local pdg_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=cpair(colors.lightGray,colors.white)} diff --git a/supervisor/panel/components/rtu_entry.lua b/supervisor/panel/components/rtu_entry.lua index a4a5a4e..d9634e9 100644 --- a/supervisor/panel/components/rtu_entry.lua +++ b/supervisor/panel/components/rtu_entry.lua @@ -2,8 +2,6 @@ -- RTU Connection Entry -- -local util = require("scada-common.util") - local databus = require("supervisor.databus") local core = require("graphics.core") @@ -28,9 +26,9 @@ local function init(parent, id) local ps_prefix = "rtu_" .. id .. "_" TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} - local rtu_chan = TextBox{parent=entry,x=1,y=2,text=" :00000",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)} + local rtu_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)} TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} - rtu_chan.register(databus.ps, ps_prefix .. "chan", function (channel) rtu_chan.set_value(util.sprintf(" :%05d", channel)) end) + rtu_addr.register(databus.ps, ps_prefix .. "addr", rtu_addr.set_value) TextBox{parent=entry,x=10,y=2,text="UNITS:",width=7,height=1} local unit_count = DataIndicator{parent=entry,x=17,y=2,label="",unit="",format="%2d",value=0,width=2,fg_bg=cpair(colors.gray,colors.white)} diff --git a/supervisor/panel/front_panel.lua b/supervisor/panel/front_panel.lua index 8cdcf76..b6ee692 100644 --- a/supervisor/panel/front_panel.lua +++ b/supervisor/panel/front_panel.lua @@ -56,6 +56,10 @@ local function init(panel) modem.register(databus.ps, "has_modem", modem.update) +---@diagnostic disable-next-line: undefined-field + local comp_id = util.sprintf("(%d)", os.getComputerID()) + TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)} + -- -- about footer -- @@ -84,11 +88,11 @@ local function init(panel) TextBox{parent=plc_entry,x=1,y=2,text="UNIT "..i,alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} TextBox{parent=plc_entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} - local conn = LED{parent=plc_entry,x=10,y=2,label="CONN",colors=cpair(colors.green,colors.green_off)} + local conn = LED{parent=plc_entry,x=10,y=2,label="LINK",colors=cpair(colors.green,colors.green_off)} conn.register(databus.ps, ps_prefix .. "conn", conn.update) - local plc_chan = TextBox{parent=plc_entry,x=17,y=2,text=" --- ",width=5,height=1,fg_bg=cpair(colors.gray,colors.white)} - plc_chan.register(databus.ps, ps_prefix .. "chan", plc_chan.set_value) + local plc_addr = TextBox{parent=plc_entry,x=17,y=2,text=" --- ",width=5,height=1,fg_bg=cpair(colors.gray,colors.white)} + plc_addr.register(databus.ps, ps_prefix .. "addr", plc_addr.set_value) TextBox{parent=plc_entry,x=23,y=2,text="FW:",width=3,height=1} local plc_fw_v = TextBox{parent=plc_entry,x=27,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)} @@ -117,9 +121,9 @@ local function init(panel) local crd_conn = LED{parent=crd_box,x=2,y=2,label="CONNECTION",colors=cpair(colors.green,colors.green_off)} crd_conn.register(databus.ps, "crd_conn", crd_conn.update) - TextBox{parent=crd_box,x=4,y=3,text="CHANNEL ",width=8,height=1,fg_bg=cpair(colors.gray,colors.white)} - local crd_chan = TextBox{parent=crd_box,x=12,y=3,text="---",width=5,height=1,fg_bg=cpair(colors.gray,colors.white)} - crd_chan.register(databus.ps, "crd_chan", crd_chan.set_value) + TextBox{parent=crd_box,x=4,y=3,text="COMPUTER",width=8,height=1,fg_bg=cpair(colors.gray,colors.white)} + local crd_addr = TextBox{parent=crd_box,x=13,y=3,text="---",width=5,height=1,fg_bg=cpair(colors.gray,colors.white)} + crd_addr.register(databus.ps, "crd_addr", crd_addr.set_value) TextBox{parent=crd_box,x=22,y=2,text="FW:",width=3,height=1} local crd_fw_v = TextBox{parent=crd_box,x=26,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)} diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index 36a5241..1b75078 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -45,12 +45,13 @@ local PERIODICS = { -- coordinator supervisor session ---@nodiscard ---@param id integer session ID +---@param s_addr integer device source address ---@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, in_queue, out_queue, timeout, facility, fp_ok) +function coordinator.new_session(id, s_addr, 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 @@ -99,7 +100,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility, fp_ local c_pkt = comms.crdn_packet() c_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable()) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable()) out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 @@ -113,7 +114,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility, fp_ local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 @@ -334,7 +335,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility, fp_ end end - ---@class coord_session + ---@class crd_session local public = {} -- get the session ID diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 1436534..ac80ea6 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -45,12 +45,13 @@ local PERIODICS = { -- PLC supervisor session ---@nodiscard ---@param id integer session ID +---@param s_addr integer device source address ---@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, reactor_id, in_queue, out_queue, timeout, fp_ok) +function plc.new_session(id, s_addr, 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 @@ -250,7 +251,7 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout, fp_ok) local r_pkt = comms.rplc_packet() r_pkt.make(reactor_id, msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable()) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable()) out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 @@ -264,7 +265,7 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout, fp_ok) local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 diff --git a/supervisor/session/pocket.lua b/supervisor/session/pocket.lua index ba6d179..9de55ab 100644 --- a/supervisor/session/pocket.lua +++ b/supervisor/session/pocket.lua @@ -29,11 +29,12 @@ local PERIODICS = { -- pocket diagnostics session ---@nodiscard ---@param id integer session ID +---@param s_addr integer device source address ---@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 pocket.new_session(id, in_queue, out_queue, timeout, fp_ok) +function pocket.new_session(id, s_addr, 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 @@ -81,7 +82,7 @@ function pocket.new_session(id, in_queue, out_queue, timeout, fp_ok) local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 2f7091c..d1fbaec 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -31,13 +31,14 @@ local PERIODICS = { -- create a new RTU session ---@nodiscard ---@param id integer session ID +---@param s_addr integer device source address ---@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, in_queue, out_queue, timeout, advertisement, facility, fp_ok) +function rtu.new_session(id, s_addr, 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 @@ -204,7 +205,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili local function _send_modbus(m_pkt) local s_pkt = comms.scada_packet() - s_pkt.make(self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 @@ -218,7 +219,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 5d22e3d..a311ab9 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -27,7 +27,7 @@ local svsessions = {} local SESSION_TYPE = { RTU_SESSION = 0, -- RTU gateway PLC_SESSION = 1, -- reactor PLC - COORD_SESSION = 2, -- coordinator + CRD_SESSION = 2, -- coordinator PDG_SESSION = 3 -- pocket diagnostics } @@ -38,11 +38,11 @@ local self = { fp_ok = false, num_reactors = 0, facility = nil, ---@type facility|nil - sessions = { rtu = {}, plc = {}, coord = {}, pdg = {} }, - next_ids = { rtu = 0, plc = 0, coord = 0, pdg = 0 } + sessions = { rtu = {}, plc = {}, crd = {}, pdg = {} }, + next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 } } ----@alias sv_session_structs plc_session_struct|rtu_session_struct|coord_session_struct|pdg_session_struct +---@alias sv_session_structs plc_session_struct|rtu_session_struct|crd_session_struct|pdg_session_struct -- PRIVATE FUNCTIONS -- @@ -60,7 +60,7 @@ local function _sv_handle_outq(session) if msg ~= nil then if msg.qtype == mqueue.TYPE.PACKET then -- handle a packet to be sent - self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) + self.modem.transmit(session.r_chan, config.SVR_CHANNEL, msg.message.raw_sendable()) elseif msg.qtype == mqueue.TYPE.COMMAND then -- handle instruction/notification elseif msg.qtype == mqueue.TYPE.DATA then @@ -81,11 +81,11 @@ local function _sv_handle_outq(session) elseif cmd.key == SV_Q_DATA.SET_BURN and type(cmd.val) == "table" and #cmd.val == 2 then plc_s.in_queue.push_data(PLC_S_DATA.BURN_RATE, cmd.val[2]) else - log.debug(util.c("unknown PLC SV queue command ", cmd.key)) + log.debug(util.c("[SVS] unknown PLC SV queue command ", cmd.key)) end end else - local crd_s = svsessions.get_coord_session() + local crd_s = svsessions.get_crd_session() if crd_s ~= nil then if cmd.key == SV_Q_DATA.CRDN_ACK then -- ack to be sent to coordinator @@ -104,8 +104,8 @@ local function _sv_handle_outq(session) -- max 100ms spent processing queue if util.time() - handle_start > 100 then - log.warning("supervisor out queue handler exceeded 100ms queue process limit") - log.warning(util.c("offending session: port ", session.r_port, " type '", session.s_type, "'")) + log.warning("[SVS] supervisor out queue handler exceeded 100ms queue process limit") + log.warning(util.c("[SVS] offending session: ", session)) break end end @@ -131,15 +131,15 @@ local function _shutdown(session) session.open = false session.instance.close() - -- send packets in out queue (namely the close packet) + -- send packets in out queue (for the close packet) while session.out_queue.ready() do local msg = session.out_queue.pop() if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then - self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) + self.modem.transmit(session.r_chan, config.SVR_CHANNEL, msg.message.raw_sendable()) end end - log.debug(util.c("closed ", session.s_type, " session ", session.instance.get_id(), " on remote port ", session.r_port)) + log.debug(util.c("[SVS] closed session ", session)) end -- close connections @@ -160,8 +160,7 @@ local function _check_watchdogs(sessions, timer_event) if session.open then local triggered = session.instance.check_wd(timer_event) if triggered then - log.debug(util.c("watchdog closing ", session.s_type, " session ", session.instance.get_id(), - " on remote port ", session.r_port, "...")) + log.debug(util.c("[SVS] watchdog closing session ", session, "...")) _shutdown(session) end end @@ -175,21 +174,20 @@ local function _free_closed(sessions) ---@param session sv_session_structs local on_delete = function (session) - log.debug(util.c("free'ing closed ", session.s_type, " session ", session.instance.get_id(), - " on remote port ", session.r_port)) + log.debug(util.c("[SVS] free'ing closed session ", session)) end util.filter_table(sessions, f, on_delete) end --- find a session by remote port +-- find a session by computer ID ---@nodiscard ---@param list table ----@param port integer +---@param s_addr integer ---@return sv_session_structs|nil -local function _find_session(list, port) +local function _find_session(list, s_addr) for i = 1, #list do - if list[i].r_port == port then return list[i] end + if list[i].s_addr == s_addr then return list[i] end end return nil end @@ -214,63 +212,55 @@ function svsessions.relink_modem(modem) self.modem = modem end --- find an RTU session by the remote port +-- find an RTU session by the computer ID ---@nodiscard ----@param remote_port integer +---@param source_addr integer ---@return rtu_session_struct|nil -function svsessions.find_rtu_session(remote_port) +function svsessions.find_rtu_session(source_addr) -- check RTU sessions - local session = _find_session(self.sessions.rtu, remote_port) + local session = _find_session(self.sessions.rtu, source_addr) ---@cast session rtu_session_struct|nil return session end --- find a PLC session by the remote port +-- find a PLC session by the computer ID ---@nodiscard ----@param remote_port integer +---@param source_addr integer ---@return plc_session_struct|nil -function svsessions.find_plc_session(remote_port) +function svsessions.find_plc_session(source_addr) -- check PLC sessions - local session = _find_session(self.sessions.plc, remote_port) + local session = _find_session(self.sessions.plc, source_addr) ---@cast session plc_session_struct|nil return session end --- find a PLC/RTU session by the remote port +-- find a coordinator session by the computer ID ---@nodiscard ----@param remote_port integer ----@return plc_session_struct|rtu_session_struct|nil -function svsessions.find_device_session(remote_port) - -- check RTU sessions - local session = _find_session(self.sessions.rtu, remote_port) - - -- check PLC sessions - if session == nil then session = _find_session(self.sessions.plc, remote_port) end - ---@cast session plc_session_struct|rtu_session_struct|nil - +---@param source_addr integer +---@return crd_session_struct|nil +function svsessions.find_crd_session(source_addr) + -- check coordinator sessions + local session = _find_session(self.sessions.crd, source_addr) + ---@cast session crd_session_struct|nil return session end --- find a coordinator or diagnostic access session by the remote port +-- find a pocket diagnostics session by the computer ID ---@nodiscard ----@param remote_port integer ----@return coord_session_struct|pdg_session_struct|nil -function svsessions.find_svctl_session(remote_port) - -- check coordinator sessions - local session = _find_session(self.sessions.coord, remote_port) - +---@param source_addr integer +---@return pdg_session_struct|nil +function svsessions.find_pdg_session(source_addr) -- check diagnostic sessions - if session == nil then session = _find_session(self.sessions.pdg, remote_port) end - ---@cast session coord_session_struct|pdg_session_struct|nil - + local session = _find_session(self.sessions.pdg, source_addr) + ---@cast session pdg_session_struct|nil return session end -- get the a coordinator session if exists ---@nodiscard ----@return coord_session_struct|nil -function svsessions.get_coord_session() - return self.sessions.coord[1] +---@return crd_session_struct|nil +function svsessions.get_crd_session() + return self.sessions.crd[1] end -- get a session by reactor ID @@ -291,12 +281,11 @@ end -- establish a new PLC session ---@nodiscard ----@param local_port integer ----@param remote_port integer +---@param source_addr integer ---@param for_reactor integer ---@param version string ---@return integer|false session_id -function svsessions.establish_plc_session(local_port, remote_port, for_reactor, version) +function svsessions.establish_plc_session(source_addr, for_reactor, version) if svsessions.get_reactor_session(for_reactor) == nil and for_reactor >= 1 and for_reactor <= self.num_reactors then ---@class plc_session_struct local plc_s = { @@ -304,26 +293,34 @@ function svsessions.establish_plc_session(local_port, remote_port, for_reactor, open = true, reactor = for_reactor, version = version, - l_port = local_port, - r_port = remote_port, + r_chan = config.PLC_CHANNEL, + s_addr = source_addr, in_queue = mqueue.new(), out_queue = mqueue.new(), instance = nil ---@type plc_session } - plc_s.instance = plc.new_session(self.next_ids.plc, for_reactor, plc_s.in_queue, plc_s.out_queue, - config.PLC_TIMEOUT, self.fp_ok) + 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, + config.PLC_TIMEOUT, self.fp_ok) table.insert(self.sessions.plc, plc_s) local units = self.facility.get_units() units[for_reactor].link_plc_session(plc_s) - log.debug(util.c("established new PLC session to ", remote_port, " with ID ", self.next_ids.plc, - " for reactor ", for_reactor)) + local mt = { + ---@param s plc_session_struct + __tostring = function (s) return util.c("PLC [", s.instance.get_id(), "] for reactor #", s.reactor, + " (@", s.s_addr, ")") end + } - databus.tx_plc_connected(for_reactor, version, remote_port) + setmetatable(plc_s, mt) - self.next_ids.plc = self.next_ids.plc + 1 + databus.tx_plc_connected(for_reactor, version, source_addr) + log.debug(util.c("[SVS] established new session: ", plc_s)) + + self.next_ids.plc = id + 1 -- success return plc_s.instance.get_id() @@ -335,70 +332,84 @@ end -- establish a new RTU session ---@nodiscard ----@param local_port integer ----@param remote_port integer +---@param source_addr integer ---@param advertisement table ---@param version string ---@return integer session_id -function svsessions.establish_rtu_session(local_port, remote_port, advertisement, version) +function svsessions.establish_rtu_session(source_addr, advertisement, version) ---@class rtu_session_struct local rtu_s = { s_type = "rtu", open = true, version = version, - l_port = local_port, - r_port = remote_port, + r_chan = config.RTU_CHANNEL, + s_addr = source_addr, in_queue = mqueue.new(), out_queue = mqueue.new(), instance = nil ---@type rtu_session } - rtu_s.instance = rtu.new_session(self.next_ids.rtu, rtu_s.in_queue, rtu_s.out_queue, config.RTU_TIMEOUT, advertisement, - self.facility, self.fp_ok) + local id = self.next_ids.rtu + + rtu_s.instance = rtu.new_session(id, source_addr, rtu_s.in_queue, rtu_s.out_queue, config.RTU_TIMEOUT, + advertisement, self.facility, self.fp_ok) table.insert(self.sessions.rtu, rtu_s) - log.debug("established new RTU session to " .. remote_port .. " with ID " .. self.next_ids.rtu) + local mt = { + ---@param s rtu_session_struct + __tostring = function (s) return util.c("RTU [", s.instance.get_id(), "] (@", s.s_addr, ")") end + } - databus.tx_rtu_connected(self.next_ids.rtu, version, remote_port) + setmetatable(rtu_s, mt) - self.next_ids.rtu = self.next_ids.rtu + 1 + databus.tx_rtu_connected(id, version, source_addr) + log.debug(util.c("[SVS] established new session: ", rtu_s)) + + self.next_ids.rtu = id + 1 -- success - return rtu_s.instance.get_id() + return id end -- establish a new coordinator session ---@nodiscard ----@param local_port integer ----@param remote_port integer +---@param source_addr integer ---@param version string ---@return integer|false session_id -function svsessions.establish_coord_session(local_port, remote_port, version) - if svsessions.get_coord_session() == nil then - ---@class coord_session_struct - local coord_s = { +function svsessions.establish_crd_session(source_addr, version) + if svsessions.get_crd_session() == nil then + ---@class crd_session_struct + local crd_s = { s_type = "crd", open = true, version = version, - l_port = local_port, - r_port = remote_port, + r_chan = config.CRD_CHANNEL, + s_addr = source_addr, in_queue = mqueue.new(), out_queue = mqueue.new(), - instance = nil ---@type coord_session + instance = nil ---@type crd_session } - coord_s.instance = coordinator.new_session(self.next_ids.coord, coord_s.in_queue, coord_s.out_queue, config.CRD_TIMEOUT, + local id = self.next_ids.crd + + crd_s.instance = coordinator.new_session(id, source_addr, crd_s.in_queue, crd_s.out_queue, config.CRD_TIMEOUT, self.facility, self.fp_ok) - table.insert(self.sessions.coord, coord_s) + table.insert(self.sessions.crd, crd_s) - log.debug("established new coordinator session to " .. remote_port .. " with ID " .. self.next_ids.coord) + local mt = { + ---@param s crd_session_struct + __tostring = function (s) return util.c("CRD [", s.instance.get_id(), "] (@", s.s_addr, ")") end + } - databus.tx_crd_connected(version, remote_port) + setmetatable(crd_s, mt) - self.next_ids.coord = self.next_ids.coord + 1 + databus.tx_crd_connected(version, source_addr) + log.debug(util.c("[SVS] established new session: ", crd_s)) + + self.next_ids.crd = id + 1 -- success - return coord_s.instance.get_id() + return id else -- we already have a coordinator linked return false @@ -407,34 +418,41 @@ end -- establish a new pocket diagnostics session ---@nodiscard ----@param local_port integer ----@param remote_port integer +---@param source_addr integer ---@param version string ---@return integer|false session_id -function svsessions.establish_pdg_session(local_port, remote_port, version) +function svsessions.establish_pdg_session(source_addr, version) ---@class pdg_session_struct local pdg_s = { s_type = "pkt", open = true, version = version, - l_port = local_port, - r_port = remote_port, + r_chan = config.PKT_CHANNEL, + s_addr = source_addr, in_queue = mqueue.new(), out_queue = mqueue.new(), instance = nil ---@type pdg_session } - pdg_s.instance = pocket.new_session(self.next_ids.pdg, pdg_s.in_queue, pdg_s.out_queue, config.PKT_TIMEOUT, self.fp_ok) + local id = self.next_ids.pdg + + pdg_s.instance = pocket.new_session(id, source_addr, pdg_s.in_queue, pdg_s.out_queue, config.PKT_TIMEOUT, self.fp_ok) table.insert(self.sessions.pdg, pdg_s) - log.debug("established new pocket diagnostics session to " .. remote_port .. " with ID " .. self.next_ids.pdg) + local mt = { + ---@param s pdg_session_struct + __tostring = function (s) return util.c("PDG [", s.instance.get_id(), "] (@", s.s_addr, ")") end + } - databus.tx_pdg_connected(self.next_ids.pdg, version, remote_port) + setmetatable(pdg_s, mt) - self.next_ids.pdg = self.next_ids.pdg + 1 + databus.tx_pdg_connected(id, version, source_addr) + log.debug(util.c("[SVS] established new session: ", pdg_s)) + + self.next_ids.pdg = id + 1 -- success - return pdg_s.instance.get_id() + return id end -- attempt to identify which session's watchdog timer fired @@ -466,9 +484,7 @@ end -- close all open connections function svsessions.close_all() -- close sessions - for _, list in pairs(self.sessions) do - _close(list) - end + for _, list in pairs(self.sessions) do _close(list) end -- free sessions svsessions.free_all_closed() diff --git a/supervisor/startup.lua b/supervisor/startup.lua index ff4a4fe..687ede1 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -20,7 +20,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v0.16.8" +local SUPERVISOR_VERSION = "v0.17.5" local println = util.println local println_ts = util.println_ts @@ -31,8 +31,11 @@ local println_ts = util.println_ts local cfv = util.new_validator() -cfv.assert_port(config.SCADA_DEV_LISTEN) -cfv.assert_port(config.SCADA_SV_CTL_LISTEN) +cfv.assert_channel(config.SVR_CHANNEL) +cfv.assert_channel(config.PLC_CHANNEL) +cfv.assert_channel(config.RTU_CHANNEL) +cfv.assert_channel(config.CRD_CHANNEL) +cfv.assert_channel(config.PKT_CHANNEL) cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_num(config.PLC_TIMEOUT) cfv.assert_min(config.PLC_TIMEOUT, 2) @@ -112,9 +115,8 @@ local function main() println_ts = function (_) end end - -- start comms, open all channels - local superv_comms = supervisor.comms(SUPERVISOR_VERSION, config.NUM_REACTORS, config.REACTOR_COOLING, modem, - config.SCADA_DEV_LISTEN, config.SCADA_SV_CTL_LISTEN, config.TRUSTED_RANGE, fp_ok) + -- start comms + local superv_comms = supervisor.comms(SUPERVISOR_VERSION, modem, fp_ok) -- base loop clock (6.67Hz, 3 ticks) local MAIN_CLOCK = 0.15 diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 874aaa9..cfc52d8 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -2,6 +2,8 @@ local comms = require("scada-common.comms") local log = require("scada-common.log") local util = require("scada-common.util") +local config = require("supervisor.config") + local svsessions = require("supervisor.session.svsessions") local supervisor = {} @@ -14,31 +16,36 @@ local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE -- supervisory controller communications ---@nodiscard ---@param _version string supervisor version ----@param num_reactors integer number of reactors ----@param cooling_conf table cooling configuration table ---@param modem table modem device ----@param dev_listen integer listening port for PLC/RTU devices ----@param svctl_listen integer listening port for supervisor access ----@param range integer trusted device connection range ---@param fp_ok boolean if the front panel UI is running ---@diagnostic disable-next-line: unused-local -function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_listen, svctl_listen, range, fp_ok) +function supervisor.comms(_version, modem, 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 + -- channel list from config + local svr_channel = config.SVR_CHANNEL + local plc_channel = config.PLC_CHANNEL + local rtu_channel = config.RTU_CHANNEL + local crd_channel = config.CRD_CHANNEL + local pkt_channel = config.PKT_CHANNEL + + -- configuration data + local num_reactors = config.NUM_REACTORS + local cooling_conf = config.REACTOR_COOLING + local self = { last_est_acks = {} } - comms.set_trusted_range(range) + comms.set_trusted_range(config.TRUSTED_RANGE) -- PRIVATE FUNCTIONS -- -- configure modem channels local function _conf_channels() modem.closeAll() - modem.open(dev_listen) - modem.open(svctl_listen) + modem.open(svr_channel) end _conf_channels() @@ -46,31 +53,19 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste -- pass modem, status, and config data to svsessions svsessions.init(modem, fp_ok, num_reactors, cooling_conf) - -- send an establish request response to a PLC/RTU - ---@param dest integer - ---@param msg table - local function _send_dev_establish(seq_id, dest, msg) + -- send an establish request response + ---@param packet scada_packet + ---@param ack ESTABLISH_ACK + ---@param data? any optional data + local function _send_establish(packet, ack, data) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() - m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg) - s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, { ack, data }) + s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - modem.transmit(dest, dev_listen, s_pkt.raw_sendable()) - end - - -- send supervisor control access connection establish response - ---@param seq_id integer - ---@param dest integer - ---@param msg table - local function _send_svctl_establish(seq_id, dest, msg) - local s_pkt = comms.scada_packet() - local c_pkt = comms.mgmt_packet() - - c_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg) - s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, c_pkt.raw_sendable()) - - modem.transmit(dest, svctl_listen, s_pkt.raw_sendable()) + modem.transmit(packet.remote_channel(), svr_channel, s_pkt.raw_sendable()) + self.last_est_acks[packet.src_addr()] = ack end -- PUBLIC FUNCTIONS -- @@ -138,17 +133,94 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste ---@param packet modbus_frame|rplc_frame|mgmt_frame|crdn_frame|nil function public.handle_packet(packet) if packet ~= nil then - local l_port = packet.scada_frame.local_port() - local r_port = packet.scada_frame.remote_port() + 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() - -- device (RTU/PLC) listening channel - if l_port == dev_listen then + if l_chan ~= svr_channel then + log.debug("received packet on unconfigured channel " .. l_chan, true) + elseif r_chan == 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 + -- unknown session, force a re-link + log.debug("PLC_ESTABLISH: no session but not an establish, forcing relink") + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) + 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 == SCADA_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 + else + log.debug(util.c("illegal packet type ", protocol, " on PLC channel")) + end + elseif r_chan == 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 - -- look for an associated session - local session = svsessions.find_rtu_session(r_port) - -- MODBUS response if session ~= nil then -- pass the packet onto the session handler @@ -157,105 +229,59 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste -- any other packet should be session related, discard it log.debug("discarding MODBUS_TCP packet without a known session") end - elseif protocol == PROTOCOL.RPLC then - ---@cast packet rplc_frame - -- look for an associated session - local session = svsessions.find_plc_session(r_port) - - -- reactor PLC packet - if session ~= nil then - -- pass the packet onto the session handler - session.in_queue.push_packet(packet) - else - -- unknown session, force a re-link - log.debug("PLC_ESTABLISH: no session but not an establish, forcing relink") - _send_dev_establish(packet.scada_frame.seq_num() + 1, r_port, { ESTABLISH_ACK.DENY }) - end elseif protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame - -- look for an associated session - local session = svsessions.find_device_session(r_port) - -- SCADA management packet if session ~= nil then -- pass the packet onto the session handler session.in_queue.push_packet(packet) elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then -- establish a new session - local next_seq_id = packet.scada_frame.seq_num() + 1 + 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 comms_v = packet.data[1] local firmware_v = packet.data[2] - local dev_type = packet.data[3] + local dev_type = packet.data[3] if comms_v ~= comms.version then - if self.last_est_acks[r_port] ~= ESTABLISH_ACK.BAD_VERSION then - log.info(util.c("dropping device establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) - self.last_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION + 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_dev_establish(next_seq_id, r_port, { 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(l_port, r_port, reactor_id, firmware_v) - - if plc_id == false then - -- reactor already has a PLC assigned - if self.last_est_acks[r_port] ~= ESTABLISH_ACK.COLLISION then - log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id)) - self.last_est_acks[r_port] = ESTABLISH_ACK.COLLISION - end - - _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION }) - else - -- got an ID; assigned to a reactor successfully - println(util.c("PLC (", firmware_v, ") [:", r_port, "] \xbb reactor ", reactor_id, " connected")) - log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [:", r_port, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id)) - - _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW }) - self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW - end - else - log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type") - _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) - 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(l_port, r_port, rtu_advert, firmware_v) + local s_id = svsessions.establish_rtu_session(src_addr, rtu_advert, firmware_v) - println(util.c("RTU (", firmware_v, ") [:", r_port, "] \xbb connected")) - log.info(util.c("RTU_ESTABLISH: RTU (",firmware_v, ") [:", r_port, "] connected with session ID ", s_id)) - - _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW }) + 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_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) end else - log.debug(util.c("illegal establish packet for device ", dev_type, " on PLC/RTU listening channel")) - _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) + 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 PLC/RTU listening channel)") - _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) + 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(r_port, "->", l_port, ": discarding SCADA_MGMT packet without a known session")) + log.debug(util.c("discarding RTU SCADA_MGMT packet without a known session from computer ", src_addr)) end else - log.debug("illegal packet type " .. protocol .. " on device listening channel") + log.debug(util.c("illegal packet type ", protocol, " on RTU channel")) end - -- coordinator listening channel - elseif l_port == svctl_listen then + elseif r_chan == crd_channel then -- look for an associated session - local session = svsessions.find_svctl_session(r_port) + local session = svsessions.find_crd_session(src_addr) if protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame @@ -265,65 +291,53 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste session.in_queue.push_packet(packet) elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then -- establish a new session - local next_seq_id = packet.scada_frame.seq_num() + 1 + 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 comms_v = packet.data[1] local firmware_v = packet.data[2] - local dev_type = packet.data[3] + local dev_type = packet.data[3] if comms_v ~= comms.version then - if self.last_est_acks[r_port] ~= ESTABLISH_ACK.BAD_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, ")")) - self.last_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION end - _send_svctl_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION }) + _send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) elseif dev_type == DEVICE_TYPE.CRDN then -- this is an attempt to establish a new coordinator session - local s_id = svsessions.establish_coord_session(l_port, r_port, firmware_v) + local s_id = svsessions.establish_crd_session(src_addr, firmware_v) if s_id ~= false then - local config = { num_reactors } + local cfg = { num_reactors } for i = 1, #cooling_conf do - table.insert(config, cooling_conf[i].BOILERS) - table.insert(config, cooling_conf[i].TURBINES) + table.insert(cfg, cooling_conf[i].BOILERS) + table.insert(cfg, cooling_conf[i].TURBINES) end - println(util.c("CRD (", firmware_v, ") [:", r_port, "] \xbb connected")) - log.info(util.c("SVCTL_ESTABLISH: coordinator (", firmware_v, ") [:", r_port, "] connected with session ID ", s_id)) + 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_svctl_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW, config }) - self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW + _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, cfg) else - if self.last_est_acks[r_port] ~= ESTABLISH_ACK.COLLISION then - log.info("SVCTL_ESTABLISH: denied new coordinator due to already being connected to another coordinator") - self.last_est_acks[r_port] = ESTABLISH_ACK.COLLISION + 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_svctl_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION }) + _send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION) end - 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(l_port, r_port, firmware_v) - - println(util.c("PKT (", firmware_v, ") [:", r_port, "] \xbb connected")) - log.info(util.c("SVCTL_ESTABLISH: pocket (", firmware_v, ") [:", r_port, "] connected with session ID ", s_id)) - - _send_svctl_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW }) - self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW else - log.debug(util.c("illegal establish packet for device ", dev_type, " on SVCTL listening channel")) - _send_svctl_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) + 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("SVCTL_ESTABLISH: establish packet length mismatch") - _send_svctl_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) + 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(r_port .. "->" .. l_port .. ": discarding SCADA_MGMT packet without a known session") + 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 @@ -333,13 +347,72 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste session.in_queue.push_packet(packet) else -- any other packet should be session related, discard it - log.debug(r_port .. "->" .. l_port .. ": discarding SCADA_CRDN packet without a known session") + log.debug(util.c("discarding coordinator SCADA_CRDN packet without a known session from computer ", src_addr)) end else - log.debug("illegal packet type " .. protocol .. " on coordinator listening channel") + log.debug(util.c("illegal packet type ", protocol, " on coordinator channel")) + end + elseif r_chan == 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 == SCADA_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")) end else - log.debug("received packet on unconfigured channel " .. l_port, true) + log.debug("received packet for unknown channel " .. r_chan, true) end end end diff --git a/test/lockbox-benchmark.lua b/test/lockbox-benchmark.lua index 198f41b..7c6ae55 100644 --- a/test/lockbox-benchmark.lua +++ b/test/lockbox-benchmark.lua @@ -38,7 +38,7 @@ local pkt = comms.modbus_packet() ---@diagnostic disable-next-line: param-type-mismatch pkt.make(1, 2, 7, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) local spkt = comms.scada_packet() -spkt.make(1, 1, pkt.raw_sendable()) +spkt.make(0, 1, 1, pkt.raw_sendable()) start = util.time() local data = textutils.serialize(spkt.raw_sendable(), { allow_repetitions = true, compact = true })