Merge pull request #252 from MikaylaFischler/225-consolidate-network-channels

225 Consolidate Network Channels
This commit is contained in:
Mikayla 2023-06-07 14:27:10 -04:00 committed by GitHub
commit 0b5ee8eabc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 758 additions and 539 deletions

View File

@ -1,11 +1,11 @@
local config = {} local config = {}
-- port of the SCADA supervisor -- supervisor comms channel
config.SCADA_SV_PORT = 16100 config.SVR_CHANNEL = 16240
-- port to listen to incoming packets from supervisor -- coordinator comms channel
config.SCADA_SV_CTL_LISTEN = 16101 config.CRD_CHANNEL = 16243
-- listen port for SCADA coordinator API access -- pocket comms channel
config.SCADA_API_LISTEN = 16200 config.PKT_CHANNEL = 16244
-- max trusted modem message distance (0 to disable check) -- max trusted modem message distance (0 to disable check)
config.TRUSTED_RANGE = 0 config.TRUSTED_RANGE = 0
-- time in seconds (>= 2) before assuming a remote device is no longer active -- time in seconds (>= 2) before assuming a remote device is no longer active

View File

@ -213,14 +213,15 @@ end
---@nodiscard ---@nodiscard
---@param version string coordinator version ---@param version string coordinator version
---@param modem table modem device ---@param modem table modem device
---@param sv_port integer port of configured supervisor ---@param crd_channel integer port of configured supervisor
---@param sv_listen integer listening port for supervisor replys ---@param svr_channel integer listening port for supervisor replys
---@param api_listen integer listening port for pocket API ---@param pkt_channel integer listening port for pocket API
---@param range integer trusted device connection range ---@param range integer trusted device connection range
---@param sv_watchdog watchdog ---@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 = { local self = {
sv_linked = false, sv_linked = false,
sv_addr = comms.BROADCAST,
sv_seq_num = 0, sv_seq_num = 0,
sv_r_seq_num = nil, sv_r_seq_num = nil,
sv_config_err = false, sv_config_err = false,
@ -236,8 +237,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
-- configure modem channels -- configure modem channels
local function _conf_channels() local function _conf_channels()
modem.closeAll() modem.closeAll()
modem.open(sv_listen) modem.open(crd_channel)
modem.open(api_listen)
end end
_conf_channels() _conf_channels()
@ -261,23 +261,24 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
end end
pkt.make(msg_type, msg) 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 self.sv_seq_num = self.sv_seq_num + 1
end end
-- send an API establish request response -- send an API establish request response
---@param dest integer ---@param packet scada_packet
---@param msg table ---@param ack ESTABLISH_ACK
local function _send_api_establish_ack(seq_id, dest, msg) local function _send_api_establish_ack(packet, ack)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet() local m_pkt = comms.mgmt_packet()
m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg) m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, { ack })
s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) 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 end
-- attempt connection establishment -- 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 -- close the connection to the server
function public.close() function public.close()
sv_watchdog.cancel() sv_watchdog.cancel()
self.sv_addr = comms.BROADCAST
self.sv_linked = false self.sv_linked = false
self.sv_r_seq_num = nil
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {}) _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {})
end 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 ---@param packet mgmt_frame|crdn_frame|capi_frame|nil
function public.handle_packet(packet) function public.handle_packet(packet)
if packet ~= nil then if packet ~= nil then
local l_port = packet.scada_frame.local_port() local l_chan = packet.scada_frame.local_channel()
local r_port = packet.scada_frame.remote_port() local r_chan = packet.scada_frame.remote_channel()
local src_addr = packet.scada_frame.src_addr()
local protocol = packet.scada_frame.protocol() 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 if protocol == PROTOCOL.COORD_API then
---@cast packet capi_frame ---@cast packet capi_frame
-- look for an associated session -- look for an associated session
local session = apisessions.find_session(r_port) local session = apisessions.find_session(src_addr)
-- API packet -- API packet
if session ~= nil then 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 elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
-- look for an associated session -- look for an associated session
local session = apisessions.find_session(r_port) local session = apisessions.find_session(src_addr)
-- SCADA management packet -- SCADA management packet
if session ~= nil then 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) session.in_queue.push_packet(packet)
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- establish a new session -- establish a new session
local next_seq_id = packet.scada_frame.seq_num() + 1
-- validate packet and continue -- validate packet and continue
if packet.length == 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then 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]
@ -474,42 +478,43 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
local dev_type = packet.data[3] local dev_type = packet.data[3]
if comms_v ~= comms.version then 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, ")")) 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 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 elseif dev_type == DEVICE_TYPE.PKT then
-- pocket linking request -- pocket linking request
local id = apisessions.establish_session(l_port, r_port, firmware_v) local id = apisessions.establish_session(src_addr, firmware_v)
println(util.c("API: pocket (", firmware_v, ") [:", r_port, "] connected with session ID ", id)) println(util.c("[API] pocket (", firmware_v, ") [@", src_addr, "] \xbb connected"))
coordinator.log_comms(util.c("API: pocket (", firmware_v, ") [:", r_port, "] connected with session ID ", id)) 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 }) _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW)
self.last_api_est_acks[r_port] = ESTABLISH_ACK.ALLOW
else else
log.debug(util.c("illegal establish packet for device ", dev_type, " on API listening channel")) log.debug(util.c("API_ESTABLISH: illegal establish packet for device ", dev_type, " on pocket channel"))
_send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
end end
else else
log.debug("invalid establish packet (on API listening channel)") 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 end
else else
-- any other packet should be session related, discard it -- 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 end
else else
log.debug("illegal packet type " .. protocol .. " on api listening channel", true) log.debug("illegal packet type " .. protocol .. " on pocket channel", true)
end end
elseif l_port == sv_listen then elseif r_chan == svr_channel then
-- check sequence number -- check sequence number
if self.sv_r_seq_num == nil then if self.sv_r_seq_num == nil then
self.sv_r_seq_num = packet.scada_frame.seq_num() 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 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: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return 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 else
self.sv_r_seq_num = packet.scada_frame.seq_num() self.sv_r_seq_num = packet.scada_frame.seq_num()
end end
@ -660,6 +665,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
-- init io controller -- init io controller
iocontrol.init(conf, public) iocontrol.init(conf, public)
self.sv_addr = src_addr
self.sv_linked = true self.sv_linked = true
self.sv_config_err = false self.sv_config_err = false
else else
@ -705,10 +711,10 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
local trip_time = util.time() - timestamp local trip_time = util.time() - timestamp
if trip_time > 750 then 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 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) 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 elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
-- handle session close -- handle session close
sv_watchdog.cancel() sv_watchdog.cancel()
self.sv_addr = comms.BROADCAST
self.sv_linked = false self.sv_linked = false
self.sv_r_seq_num = nil
println_ts("server connection closed by remote host") println_ts("server connection closed by remote host")
log.info("server connection closed by remote host") log.info("server connection closed by remote host")
else 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) log.debug("illegal packet type " .. protocol .. " on supervisor listening channel", true)
end end
else else
log.debug("received packet on unconfigured channel " .. l_port, true) log.debug("received packet for unknown channel " .. r_chan, true)
end end
end end
end end

View File

@ -5,7 +5,7 @@ local util = require("scada-common.util")
local config = require("coordinator.config") local config = require("coordinator.config")
local api = require("coordinator.session.api") local pocket = require("coordinator.session.pocket")
local apisessions = {} local apisessions = {}
@ -18,7 +18,7 @@ local self = {
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- handle a session output queue -- handle a session output queue
---@param session api_session_struct ---@param session pkt_session_struct
local function _api_handle_outq(session) local function _api_handle_outq(session)
-- record handler start time -- record handler start time
local handle_start = util.time() local handle_start = util.time()
@ -31,7 +31,7 @@ local function _api_handle_outq(session)
if msg ~= nil then if msg ~= nil then
if msg.qtype == mqueue.TYPE.PACKET then if msg.qtype == mqueue.TYPE.PACKET then
-- handle a packet to be sent -- 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 elseif msg.qtype == mqueue.TYPE.COMMAND then
-- handle instruction/notification -- handle instruction/notification
elseif msg.qtype == mqueue.TYPE.DATA then elseif msg.qtype == mqueue.TYPE.DATA then
@ -41,15 +41,15 @@ local function _api_handle_outq(session)
-- max 100ms spent processing queue -- max 100ms spent processing queue
if util.time() - handle_start > 100 then if util.time() - handle_start > 100 then
log.warning("API out queue handler exceeded 100ms queue process limit") log.warning("[API] out queue handler exceeded 100ms queue process limit")
log.warning(util.c("offending session: port ", session.r_port)) log.warning(util.c("[API] offending session: ", session))
break break
end end
end end
end end
-- cleanly close a session -- cleanly close a session
---@param session api_session_struct ---@param session pkt_session_struct
local function _shutdown(session) local function _shutdown(session)
session.open = false session.open = false
session.instance.close() session.instance.close()
@ -58,11 +58,11 @@ local function _shutdown(session)
while session.out_queue.ready() do while session.out_queue.ready() do
local msg = session.out_queue.pop() local msg = session.out_queue.pop()
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then 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
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 end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
@ -81,54 +81,60 @@ end
-- find a session by remote port -- find a session by remote port
---@nodiscard ---@nodiscard
---@param port integer ---@param source_addr integer
---@return api_session_struct|nil ---@return pkt_session_struct|nil
function apisessions.find_session(port) function apisessions.find_session(source_addr)
for i = 1, #self.sessions do 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 end
return nil return nil
end end
-- establish a new API session -- establish a new API session
---@nodiscard ---@nodiscard
---@param local_port integer ---@param source_addr integer
---@param remote_port integer
---@param version string ---@param version string
---@return integer session_id ---@return integer session_id
function apisessions.establish_session(local_port, remote_port, version) function apisessions.establish_session(source_addr, version)
---@class api_session_struct ---@class pkt_session_struct
local api_s = { local pkt_s = {
open = true, open = true,
version = version, version = version,
l_port = local_port, s_addr = source_addr,
r_port = remote_port,
in_queue = mqueue.new(), in_queue = mqueue.new(),
out_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) local id = self.next_id
table.insert(self.sessions, api_s)
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 -- success
return api_s.instance.get_id() return pkt_s.instance.get_id()
end end
-- attempt to identify which session's watchdog timer fired -- attempt to identify which session's watchdog timer fired
---@param timer_event number ---@param timer_event number
function apisessions.check_all_watchdogs(timer_event) function apisessions.check_all_watchdogs(timer_event)
for i = 1, #self.sessions do 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 if session.open then
local triggered = session.instance.check_wd(timer_event) local triggered = session.instance.check_wd(timer_event)
if triggered then if triggered then
log.debug(util.c("watchdog closing API session ", session.instance.get_id(), log.debug(util.c("[API] watchdog closing session ", session, "..."))
" on remote port ", session.r_port, "..."))
_shutdown(session) _shutdown(session)
end end
end end
@ -138,7 +144,7 @@ end
-- iterate all the API sessions -- iterate all the API sessions
function apisessions.iterate_all() function apisessions.iterate_all()
for i = 1, #self.sessions do 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 if session.open and session.instance.iterate() then
_api_handle_outq(session) _api_handle_outq(session)
@ -152,10 +158,9 @@ end
function apisessions.free_all_closed() function apisessions.free_all_closed()
local f = function (session) return session.open end local f = function (session) return session.open end
---@param session api_session_struct ---@param session pkt_session_struct
local on_delete = function (session) local on_delete = function (session)
log.debug(util.c("free'ing closed API session ", session.instance.get_id(), log.debug(util.c("[API] free'ing closed session ", session))
" on remote port ", session.r_port))
end end
util.filter_table(self.sessions, f, on_delete) util.filter_table(self.sessions, f, on_delete)
@ -164,7 +169,7 @@ end
-- close all open connections -- close all open connections
function apisessions.close_all() function apisessions.close_all()
for i = 1, #self.sessions do 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 if session.open then _shutdown(session) end
end end

View File

@ -3,7 +3,7 @@ local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util") local util = require("scada-common.util")
local api = {} local pocket = {}
local PROTOCOL = comms.PROTOCOL local PROTOCOL = comms.PROTOCOL
-- local CAPI_TYPE = comms.CAPI_TYPE -- local CAPI_TYPE = comms.CAPI_TYPE
@ -21,8 +21,8 @@ local API_S_CMDS = {
local API_S_DATA = { local API_S_DATA = {
} }
api.API_S_CMDS = API_S_CMDS pocket.API_S_CMDS = API_S_CMDS
api.API_S_DATA = API_S_DATA pocket.API_S_DATA = API_S_DATA
local PERIODICS = { local PERIODICS = {
KEEP_ALIVE = 2000 KEEP_ALIVE = 2000
@ -31,11 +31,12 @@ local PERIODICS = {
-- pocket API session -- pocket API session
---@nodiscard ---@nodiscard
---@param id integer session ID ---@param id integer session ID
---@param s_addr integer device source address
---@param in_queue mqueue in message queue ---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue ---@param out_queue mqueue out message queue
---@param timeout number communications timeout ---@param timeout number communications timeout
function api.new_session(id, in_queue, out_queue, timeout) function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
local log_header = "api_session(" .. id .. "): " local log_header = "pkt_session(" .. id .. "): "
local self = { local self = {
-- connection properties -- connection properties
@ -61,10 +62,10 @@ function api.new_session(id, in_queue, out_queue, timeout)
} }
} }
---@class api_session ---@class pkt_session
local public = {} local public = {}
-- mark this API session as closed, stop watchdog -- mark this pocket session as closed, stop watchdog
local function _close() local function _close()
self.conn_watchdog.cancel() self.conn_watchdog.cancel()
self.connected = false self.connected = false
@ -92,7 +93,7 @@ function api.new_session(id, in_queue, out_queue, timeout)
local m_pkt = comms.mgmt_packet() local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg) 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) out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1 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 self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then 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 end
-- log.debug(log_header .. "API RTT = " .. self.last_rtt .. "ms") -- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "API TT = " .. (srv_now - api_send) .. "ms") -- log.debug(log_header .. "PKT TT = " .. (srv_now - api_send) .. "ms")
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_header .. "SCADA keep alive packet length mismatch")
end end
@ -171,7 +172,7 @@ function api.new_session(id, in_queue, out_queue, timeout)
function public.close() function public.close()
_close() _close()
_send_mgmt(SCADA_MGMT_TYPE.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") log.info(log_header .. "session closed by server")
end end
@ -210,7 +211,7 @@ function api.new_session(id, in_queue, out_queue, timeout)
-- exit if connection was closed -- exit if connection was closed
if not self.connected then 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") log.info(log_header .. "session closed by remote host")
return self.connected return self.connected
end end
@ -246,4 +247,4 @@ function api.new_session(id, in_queue, out_queue, timeout)
return public return public
end end
return api return pocket

View File

@ -20,7 +20,7 @@ local sounder = require("coordinator.sounder")
local apisessions = require("coordinator.session.apisessions") local apisessions = require("coordinator.session.apisessions")
local COORDINATOR_VERSION = "v0.15.8" local COORDINATOR_VERSION = "v0.16.0"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -37,9 +37,9 @@ local log_comms_connecting = coordinator.log_comms_connecting
local cfv = util.new_validator() local cfv = util.new_validator()
cfv.assert_port(config.SCADA_SV_PORT) cfv.assert_channel(config.SVR_CHANNEL)
cfv.assert_port(config.SCADA_SV_CTL_LISTEN) cfv.assert_channel(config.CRD_CHANNEL)
cfv.assert_port(config.SCADA_API_LISTEN) cfv.assert_channel(config.PKT_CHANNEL)
cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_int(config.TRUSTED_RANGE)
cfv.assert_type_num(config.SV_TIMEOUT) cfv.assert_type_num(config.SV_TIMEOUT)
cfv.assert_min(config.SV_TIMEOUT, 2) cfv.assert_min(config.SV_TIMEOUT, 2)
@ -148,8 +148,8 @@ local function main()
log.debug("startup> conn watchdog created") log.debug("startup> conn watchdog created")
-- start comms, open all channels -- start comms, open all channels
local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.SCADA_SV_PORT, config.SCADA_SV_CTL_LISTEN, local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.CRD_CHANNEL, config.SVR_CHANNEL,
config.SCADA_API_LISTEN, config.TRUSTED_RANGE, conn_watchdog) config.PKT_CHANNEL, config.TRUSTED_RANGE, conn_watchdog)
log.debug("startup> comms init") log.debug("startup> comms init")
log_comms("comms initialized") log_comms("comms initialized")
@ -163,7 +163,7 @@ local function main()
-- attempt to connect to the supervisor or exit -- attempt to connect to the supervisor or exit
local function init_connect_sv() 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 -- attempt to establish a connection with the supervisory computer
if not coord_comms.sv_connect(60, tick_waiting, task_done) then if not coord_comms.sv_connect(60, tick_waiting, task_done) then

View File

@ -1,11 +1,11 @@
local config = {} local config = {}
-- port of the SCADA supervisor -- supervisor comms channel
config.SCADA_SV_PORT = 16100 config.SVR_CHANNEL = 16240
-- port for SCADA coordinator API access -- coordinator comms channel
config.SCADA_API_PORT = 16200 config.CRD_CHANNEL = 16243
-- port to listen to incoming packets FROM servers -- pocket comms channel
config.LISTEN_PORT = 16201 config.PKT_CHANNEL = 16244
-- max trusted modem message distance (0 to disable check) -- max trusted modem message distance (0 to disable check)
config.TRUSTED_RANGE = 0 config.TRUSTED_RANGE = 0
-- time in seconds (>= 2) before assuming a remote device is no longer active -- time in seconds (>= 2) before assuming a remote device is no longer active

View File

@ -18,22 +18,24 @@ local pocket = {}
---@nodiscard ---@nodiscard
---@param version string pocket version ---@param version string pocket version
---@param modem table modem device ---@param modem table modem device
---@param local_port integer local pocket port ---@param pkt_channel integer pocket comms channel
---@param sv_port integer port of supervisor ---@param svr_channel integer supervisor access channel
---@param api_port integer port of coordinator API ---@param crd_channel integer coordinator access channel
---@param range integer trusted device connection range ---@param range integer trusted device connection range
---@param sv_watchdog watchdog ---@param sv_watchdog watchdog
---@param api_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 = { local self = {
sv = { sv = {
linked = false, linked = false,
addr = comms.BROADCAST,
seq_num = 0, seq_num = 0,
r_seq_num = nil, ---@type nil|integer r_seq_num = nil, ---@type nil|integer
last_est_ack = ESTABLISH_ACK.ALLOW last_est_ack = ESTABLISH_ACK.ALLOW
}, },
api = { api = {
linked = false, linked = false,
addr = comms.BROADCAST,
seq_num = 0, seq_num = 0,
r_seq_num = nil, ---@type nil|integer r_seq_num = nil, ---@type nil|integer
last_est_ack = ESTABLISH_ACK.ALLOW 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 -- configure modem channels
local function _conf_channels() local function _conf_channels()
modem.closeAll() modem.closeAll()
modem.open(local_port) modem.open(pkt_channel)
end end
_conf_channels() _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() local pkt = comms.mgmt_packet()
pkt.make(msg_type, msg) 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 self.sv.seq_num = self.sv.seq_num + 1
end end
@ -75,9 +77,9 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
local pkt = comms.mgmt_packet() local pkt = comms.mgmt_packet()
pkt.make(msg_type, msg) 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 self.api.seq_num = self.api.seq_num + 1
end end
@ -89,9 +91,9 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
-- local pkt = comms.capi_packet() -- local pkt = comms.capi_packet()
-- pkt.make(msg_type, msg) -- 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 -- self.api.seq_num = self.api.seq_num + 1
-- end -- end
@ -133,6 +135,8 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
function public.close_sv() function public.close_sv()
sv_watchdog.cancel() sv_watchdog.cancel()
self.sv.linked = false self.sv.linked = false
self.sv.r_seq_num = nil
self.sv.addr = comms.BROADCAST
_send_sv(SCADA_MGMT_TYPE.CLOSE, {}) _send_sv(SCADA_MGMT_TYPE.CLOSE, {})
end end
@ -140,6 +144,8 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
function public.close_api() function public.close_api()
api_watchdog.cancel() api_watchdog.cancel()
self.api.linked = false self.api.linked = false
self.api.r_seq_num = nil
self.api.addr = comms.BROADCAST
_send_crd(SCADA_MGMT_TYPE.CLOSE, {}) _send_crd(SCADA_MGMT_TYPE.CLOSE, {})
end 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 ---@param packet mgmt_frame|capi_frame|nil
function public.handle_packet(packet) function public.handle_packet(packet)
if packet ~= nil then if packet ~= nil then
local l_port = packet.scada_frame.local_port() local l_chan = packet.scada_frame.local_channel()
local r_port = packet.scada_frame.remote_port() local r_chan = packet.scada_frame.remote_channel()
local protocol = packet.scada_frame.protocol() local protocol = packet.scada_frame.protocol()
local src_addr = packet.scada_frame.src_addr()
if l_port ~= local_port then if l_chan ~= pkt_channel then
log.debug("received packet on unconfigured channel " .. l_port, true) log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_port == api_port then elseif r_chan == crd_channel then
-- check sequence number -- check sequence number
if self.api.r_seq_num == nil then if self.api.r_seq_num == nil then
self.api.r_seq_num = packet.scada_frame.seq_num() 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 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 return
else else
self.api.r_seq_num = packet.scada_frame.seq_num() 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") log.info("coordinator connection established")
self.establish_delay_counter = 0 self.establish_delay_counter = 0
self.api.linked = true self.api.linked = true
self.api.addr = src_addr
if self.sv.linked then if self.sv.linked then
coreio.report_link_state(LINK_STATE.LINKED) 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 -- handle session close
api_watchdog.cancel() api_watchdog.cancel()
self.api.linked = false self.api.linked = false
self.api.r_seq_num = nil
self.api.addr = comms.BROADCAST
log.info("coordinator server connection closed by remote host") log.info("coordinator server connection closed by remote host")
else else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator") 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 else
log.debug("illegal packet type " .. protocol .. " from coordinator", true) log.debug("illegal packet type " .. protocol .. " from coordinator", true)
end end
elseif r_port == sv_port then elseif r_chan == svr_channel then
-- check sequence number -- check sequence number
if self.sv.r_seq_num == nil then if self.sv.r_seq_num == nil then
self.sv.r_seq_num = packet.scada_frame.seq_num() 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 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 return
else else
self.sv.r_seq_num = packet.scada_frame.seq_num() 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") log.info("supervisor connection established")
self.establish_delay_counter = 0 self.establish_delay_counter = 0
self.sv.linked = true self.sv.linked = true
self.sv.addr = src_addr
if self.api.linked then if self.api.linked then
coreio.report_link_state(LINK_STATE.LINKED) 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 -- handle session close
sv_watchdog.cancel() sv_watchdog.cancel()
self.sv.linked = false self.sv.linked = false
self.sv.r_seq_num = nil
self.sv.addr = comms.BROADCAST
log.info("supervisor server connection closed by remote host") log.info("supervisor server connection closed by remote host")
else else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor") 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) log.debug("illegal packet type " .. protocol .. " from supervisor", true)
end end
else else
log.debug("received packet from unconfigured channel " .. r_port, true) log.debug("received packet from unconfigured channel " .. r_chan, true)
end end
end end
end end

View File

@ -17,7 +17,7 @@ local coreio = require("pocket.coreio")
local pocket = require("pocket.pocket") local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer") 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 = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -28,9 +28,9 @@ local println_ts = util.println_ts
local cfv = util.new_validator() local cfv = util.new_validator()
cfv.assert_port(config.SCADA_SV_PORT) cfv.assert_channel(config.SVR_CHANNEL)
cfv.assert_port(config.SCADA_API_PORT) cfv.assert_channel(config.CRD_CHANNEL)
cfv.assert_port(config.LISTEN_PORT) cfv.assert_channel(config.PKT_CHANNEL)
cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_int(config.TRUSTED_RANGE)
cfv.assert_type_num(config.COMMS_TIMEOUT) cfv.assert_type_num(config.COMMS_TIMEOUT)
cfv.assert_min(config.COMMS_TIMEOUT, 2) cfv.assert_min(config.COMMS_TIMEOUT, 2)
@ -89,8 +89,8 @@ local function main()
log.debug("startup> conn watchdogs created") log.debug("startup> conn watchdogs created")
-- start comms, open all channels -- start comms, open all channels
local pocket_comms = pocket.comms(POCKET_VERSION, modem, config.LISTEN_PORT, config.SCADA_SV_PORT, local pocket_comms = pocket.comms(POCKET_VERSION, modem, config.PKT_CHANNEL, config.SVR_CHANNEL,
config.SCADA_API_PORT, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api) config.CRD_CHANNEL, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api)
log.debug("startup> comms init") log.debug("startup> comms init")
-- base loop clock (2Hz, 10 ticks) -- base loop clock (2Hz, 10 ticks)

View File

@ -9,10 +9,10 @@ config.REACTOR_ID = 1
-- when emergency coolant is needed due to low coolant -- when emergency coolant is needed due to low coolant
-- config.EMERGENCY_COOL = { side = "right", color = nil } -- config.EMERGENCY_COOL = { side = "right", color = nil }
-- port to send packets TO server -- supervisor comms channel
config.SERVER_PORT = 16000 config.SVR_CHANNEL = 16240
-- port to listen to incoming packets FROM server -- PLC comms channel
config.LISTEN_PORT = 14001 config.PLC_CHANNEL = 16241
-- max trusted modem message distance (0 to disable check) -- max trusted modem message distance (0 to disable check)
config.TRUSTED_RANGE = 0 config.TRUSTED_RANGE = 0
-- time in seconds (>= 2) before assuming a remote device is no longer active -- time in seconds (>= 2) before assuming a remote device is no longer active

View File

@ -2,6 +2,7 @@
-- Main SCADA Coordinator GUI -- Main SCADA Coordinator GUI
-- --
local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local config = require("reactor-plc.config") 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 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 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}} 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() system.line_break()
reactor.register(databus.ps, "reactor_dev_state", reactor.update) 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_cmrx.register(databus.ps, "routine__comms_rx", rt_cmrx.update)
rt_sctl.register(databus.ps, "routine__spctl", rt_sctl.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 -- status & controls
-- --

View File

@ -446,14 +446,15 @@ end
---@param id integer reactor ID ---@param id integer reactor ID
---@param version string PLC version ---@param version string PLC version
---@param modem table modem device ---@param modem table modem device
---@param local_port integer local listening port ---@param plc_channel integer PLC comms channel
---@param server_port integer remote server port ---@param svr_channel integer supervisor server channel
---@param range integer trusted device connection range ---@param range integer trusted device connection range
---@param reactor table reactor device ---@param reactor table reactor device
---@param rps rps RPS reference ---@param rps rps RPS reference
---@param conn_watchdog watchdog watchdog 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 = { local self = {
sv_addr = comms.BROADCAST,
seq_num = 0, seq_num = 0,
r_seq_num = nil, r_seq_num = nil,
scrammed = false, scrammed = false,
@ -472,7 +473,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
-- configure modem channels -- configure modem channels
local function _conf_channels() local function _conf_channels()
modem.closeAll() modem.closeAll()
modem.open(local_port) modem.open(plc_channel)
end end
_conf_channels() _conf_channels()
@ -485,9 +486,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
local r_pkt = comms.rplc_packet() local r_pkt = comms.rplc_packet()
r_pkt.make(id, msg_type, msg) 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 self.seq_num = self.seq_num + 1
end end
@ -499,9 +500,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
local m_pkt = comms.mgmt_packet() local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg) 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 self.seq_num = self.seq_num + 1
end end
@ -667,9 +668,11 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
-- unlink from the server -- unlink from the server
function public.unlink() function public.unlink()
self.sv_addr = comms.BROADCAST
self.linked = false self.linked = false
self.r_seq_num = nil self.r_seq_num = nil
self.status_cache = nil self.status_cache = nil
databus.tx_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
end end
-- close the connection to the server -- close the connection to the server
@ -731,7 +734,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
end end
end end
-- parse an RPLC packet -- parse a packet
---@nodiscard ---@nodiscard
---@param side string ---@param side string
---@param sender integer ---@param sender integer
@ -760,14 +763,14 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
pkt = mgmt_pkt.get() pkt = mgmt_pkt.get()
end end
else else
log.debug("illegal packet type " .. s_pkt.protocol(), true) log.debug("unsupported packet type " .. s_pkt.protocol(), true)
end end
end end
return pkt return pkt
end end
-- handle an RPLC packet -- handle RPLC and MGMT packets
---@param packet rplc_frame|mgmt_frame packet frame ---@param packet rplc_frame|mgmt_frame packet frame
---@param plc_state plc_state PLC state ---@param plc_state plc_state PLC state
---@param setpoints setpoints setpoint control table ---@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 -- 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 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 -- handle packets now that we have prints setup
if l_port == local_port then if l_chan == plc_channel then
-- check sequence number -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num() self.r_seq_num = packet.scada_frame.seq_num()
elseif self.linked and ((self.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then 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()) log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return 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 else
self.r_seq_num = packet.scada_frame.seq_num() self.r_seq_num = packet.scada_frame.seq_num()
end 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 :) -- feed the watchdog first so it doesn't uhh...eat our packets :)
conn_watchdog.feed() conn_watchdog.feed()
local protocol = packet.scada_frame.protocol()
-- handle packet -- handle packet
if protocol == PROTOCOL.RPLC then if protocol == PROTOCOL.RPLC then
---@cast packet rplc_frame ---@cast packet rplc_frame
-- if linked, only accept packets from configured supervisor
if self.linked then if self.linked then
if packet.type == RPLC_TYPE.STATUS then if packet.type == RPLC_TYPE.STATUS then
-- request of full status, clear cache first -- request of full status, clear cache first
@ -933,6 +941,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
end end
elseif protocol == PROTOCOL.SCADA_MGMT then elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
-- if linked, only accept packets from configured supervisor
if self.linked then if self.linked then
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- link request confirmation -- link request confirmation
@ -945,8 +954,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
self.status_cache = nil self.status_cache = nil
_send_struct() _send_struct()
public.send_status(plc_state.no_reactor, plc_state.reactor_formed) public.send_status(plc_state.no_reactor, plc_state.reactor_formed)
log.debug("re-sent initial status data") log.debug("re-sent initial status data due to re-establish")
elseif est_ack == ESTABLISH_ACK.DENY then else
if est_ack == ESTABLISH_ACK.DENY then
println_ts("received unsolicited link denial, unlinking") println_ts("received unsolicited link denial, unlinking")
log.warning("unsolicited establish request denied") log.warning("unsolicited establish request denied")
elseif est_ack == ESTABLISH_ACK.COLLISION then elseif est_ack == ESTABLISH_ACK.COLLISION then
@ -960,7 +970,10 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
log.debug("unsolicited unknown establish request response") log.debug("unsolicited unknown establish request response")
end 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 -- clear this since this is for something that was unsolicited
self.last_est_ack = ESTABLISH_ACK.ALLOW 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)") log.warning("PLC KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
end end
-- log.debug("RPLC RTT = " .. trip_time .. "ms") -- log.debug("PLC RTT = " .. trip_time .. "ms")
_send_keep_alive_ack(timestamp) _send_keep_alive_ack(timestamp)
else else
@ -1002,9 +1015,11 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
if est_ack == ESTABLISH_ACK.ALLOW then if est_ack == ESTABLISH_ACK.ALLOW then
println_ts("linked!") 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.r_seq_num = nil
self.status_cache = nil self.status_cache = nil
@ -1012,7 +1027,8 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
public.send_status(plc_state.no_reactor, plc_state.reactor_formed) public.send_status(plc_state.no_reactor, plc_state.reactor_formed)
log.debug("sent initial status data") log.debug("sent initial status data")
elseif self.last_est_ack ~= est_ack then else
if self.last_est_ack ~= est_ack then
if est_ack == ESTABLISH_ACK.DENY then if est_ack == ESTABLISH_ACK.DENY then
println_ts("link request denied, retrying...") println_ts("link request denied, retrying...")
log.info("supervisor establish request denied, retrying") log.info("supervisor establish request denied, retrying")
@ -1028,7 +1044,11 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
end end
end end
self.linked = est_ack == ESTABLISH_ACK.ALLOW -- unlink
self.sv_addr = comms.BROADCAST
self.linked = false
end
self.last_est_ack = est_ack self.last_est_ack = est_ack
-- report link state -- 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) log.error("illegal packet type " .. protocol, true)
end end
else else
log.debug("received packet on unconfigured channel " .. l_port, true) log.debug("received packet on unconfigured channel " .. l_chan, true)
end end
end end

View File

@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer") local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads") 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 = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -31,8 +31,8 @@ local cfv = util.new_validator()
cfv.assert_type_bool(config.NETWORKED) cfv.assert_type_bool(config.NETWORKED)
cfv.assert_type_int(config.REACTOR_ID) cfv.assert_type_int(config.REACTOR_ID)
cfv.assert_port(config.SERVER_PORT) cfv.assert_channel(config.SVR_CHANNEL)
cfv.assert_port(config.LISTEN_PORT) cfv.assert_channel(config.PLC_CHANNEL)
cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_int(config.TRUSTED_RANGE)
cfv.assert_type_num(config.COMMS_TIMEOUT) cfv.assert_type_num(config.COMMS_TIMEOUT)
cfv.assert_min(config.COMMS_TIMEOUT, 2) cfv.assert_min(config.COMMS_TIMEOUT, 2)
@ -197,7 +197,7 @@ local function main()
log.debug("init> conn watchdog started") log.debug("init> conn watchdog started")
-- start comms -- 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) config.TRUSTED_RANGE, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
log.debug("init> comms init") log.debug("init> comms init")
else else

View File

@ -2,11 +2,11 @@ local rsio = require("scada-common.rsio")
local config = {} local config = {}
-- port to send packets TO server -- supervisor comms channel
config.SERVER_PORT = 16000 config.SVR_CHANNEL = 16240
-- port to listen to incoming packets FROM server -- RTU/MODBUS comms channel
config.LISTEN_PORT = 15001 config.RTU_CHANNEL = 16242
-- max trusted modem message distance (< 1 to disable check) -- max trusted modem message distance (0 to disable check)
config.TRUSTED_RANGE = 0 config.TRUSTED_RANGE = 0
-- time in seconds (>= 2) before assuming a remote device is no longer active -- time in seconds (>= 2) before assuming a remote device is no longer active
config.COMMS_TIMEOUT = 5 config.COMMS_TIMEOUT = 5

View File

@ -2,6 +2,7 @@
-- Main SCADA Coordinator GUI -- Main SCADA Coordinator GUI
-- --
local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local databus = require("rtu.databus") 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 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}} 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() system.line_break()
modem.register(databus.ps, "has_modem", modem.update) 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_main.register(databus.ps, "routine__main", rt_main.update)
rt_comm.register(databus.ps, "routine__comms", rt_comm.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 -- about label
-- --

View File

@ -159,12 +159,13 @@ end
---@nodiscard ---@nodiscard
---@param version string RTU version ---@param version string RTU version
---@param modem table modem device ---@param modem table modem device
---@param local_port integer local listening port ---@param rtu_channel integer PLC comms channel
---@param server_port integer remote server port ---@param svr_channel integer supervisor server channel
---@param range integer trusted device connection range ---@param range integer trusted device connection range
---@param conn_watchdog watchdog watchdog reference ---@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 = { local self = {
sv_addr = comms.BROADCAST,
seq_num = 0, seq_num = 0,
r_seq_num = nil, r_seq_num = nil,
txn_id = 0, txn_id = 0,
@ -180,7 +181,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
-- configure modem channels -- configure modem channels
local function _conf_channels() local function _conf_channels()
modem.closeAll() modem.closeAll()
modem.open(local_port) modem.open(rtu_channel)
end end
_conf_channels() _conf_channels()
@ -193,9 +194,9 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
local m_pkt = comms.mgmt_packet() local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg) 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 self.seq_num = self.seq_num + 1
end end
@ -238,8 +239,8 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
---@param m_pkt modbus_packet ---@param m_pkt modbus_packet
function public.send_modbus(m_pkt) function public.send_modbus(m_pkt)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
s_pkt.make(self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, 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 self.seq_num = self.seq_num + 1
end end
@ -254,7 +255,9 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
---@param rtu_state rtu_state ---@param rtu_state rtu_state
function public.unlink(rtu_state) function public.unlink(rtu_state)
rtu_state.linked = false rtu_state.linked = false
self.sv_addr = comms.BROADCAST
self.r_seq_num = nil self.r_seq_num = nil
databus.tx_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
end end
-- close the connection to the server -- 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 -- 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 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 -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num() 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 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()) log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return 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 else
self.r_seq_num = packet.scada_frame.seq_num() self.r_seq_num = packet.scada_frame.seq_num()
end end
@ -341,8 +352,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
-- feed watchdog on valid sequence number -- feed watchdog on valid sequence number
conn_watchdog.feed() conn_watchdog.feed()
local protocol = packet.scada_frame.protocol() -- handle packet
if protocol == PROTOCOL.MODBUS_TCP then if protocol == PROTOCOL.MODBUS_TCP then
---@cast packet modbus_frame ---@cast packet modbus_frame
if rtu_state.linked then 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 if est_ack == ESTABLISH_ACK.ALLOW then
-- establish allowed -- establish allowed
rtu_state.linked = true rtu_state.linked = true
self.sv_addr = packet.scada_frame.src_addr()
self.r_seq_num = nil self.r_seq_num = nil
println_ts("supervisor connection established") println_ts("supervisor connection established")
log.info("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() -- should be unreachable assuming packet is from parse_packet()
log.error("illegal packet type " .. protocol, true) log.error("illegal packet type " .. protocol, true)
end end
else
log.debug("received packet on unconfigured channel " .. l_chan, true)
end end
end end

View File

@ -28,7 +28,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu") local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_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_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE 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() local cfv = util.new_validator()
cfv.assert_port(config.SERVER_PORT) cfv.assert_channel(config.SVR_CHANNEL)
cfv.assert_port(config.LISTEN_PORT) cfv.assert_channel(config.RTU_CHANNEL)
cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_int(config.TRUSTED_RANGE)
cfv.assert_type_num(config.COMMS_TIMEOUT) cfv.assert_type_num(config.COMMS_TIMEOUT)
cfv.assert_min(config.COMMS_TIMEOUT, 2) cfv.assert_min(config.COMMS_TIMEOUT, 2)
@ -467,7 +467,7 @@ local function main()
log.debug("startup> conn watchdog started") log.debug("startup> conn watchdog started")
-- setup comms -- 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) config.TRUSTED_RANGE, smem_sys.conn_watchdog)
log.debug("startup> comms init") log.debug("startup> comms init")

View File

@ -4,14 +4,17 @@
local log = require("scada-common.log") 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 ---@class comms
local comms = {} local comms = {}
local insert = table.insert comms.version = "2.0.0"
local max_distance = nil
comms.version = "1.4.1"
---@enum PROTOCOL ---@enum PROTOCOL
local PROTOCOL = { local PROTOCOL = {
@ -122,27 +125,28 @@ comms.PLC_AUTO_ACK = PLC_AUTO_ACK
comms.UNIT_COMMAND = UNIT_COMMAND comms.UNIT_COMMAND = UNIT_COMMAND
comms.FAC_COMMAND = FAC_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 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 ---@alias frame modbus_frame|rplc_frame|mgmt_frame|crdn_frame|capi_frame
-- configure the maximum allowable message receive distance<br> -- configure the maximum allowable message receive distance<br>
-- packets received with distances greater than this will be silently discarded -- 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) function comms.set_trusted_range(distance)
if distance < 1 then if distance == 0 then max_distance = nil else max_distance = distance end
max_distance = nil
else
max_distance = distance
end
end end
-- generic SCADA packet object -- generic SCADA packet object
---@nodiscard ---@nodiscard
function comms.scada_packet() function comms.scada_packet()
local self = { local self = {
modem_msg_in = nil, modem_msg_in = nil, ---@type modem_message|nil
valid = false, valid = false,
raw = { -1, PROTOCOL.SCADA_MGMT, {} }, raw = {},
src_addr = comms.BROADCAST,
dest_addr = comms.BROADCAST,
seq_num = -1, seq_num = -1,
protocol = PROTOCOL.SCADA_MGMT, protocol = PROTOCOL.SCADA_MGMT,
length = 0, length = 0,
@ -153,34 +157,40 @@ function comms.scada_packet()
local public = {} local public = {}
-- make a SCADA packet -- 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 protocol PROTOCOL
---@param payload table ---@param payload table
function public.make(seq_num, protocol, payload) function public.make(dest_addr, seq_num, protocol, payload)
self.valid = true 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.seq_num = seq_num
self.protocol = protocol self.protocol = protocol
self.length = #payload self.length = #payload
self.payload = 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 end
-- parse in a modem message as a SCADA packet -- parse in a modem message as a SCADA packet
---@param side string modem side ---@param side string modem side
---@param sender integer sender port ---@param sender integer sender channel
---@param reply_to integer reply port ---@param reply_to integer reply channel
---@param message any message body ---@param message any message body
---@param distance integer transmission distance ---@param distance integer transmission distance
---@return boolean valid valid message received ---@return boolean valid valid message received
function public.receive(side, sender, reply_to, message, distance) function public.receive(side, sender, reply_to, message, distance)
---@class modem_message
self.modem_msg_in = { self.modem_msg_in = {
iface = side, iface = side,
s_port = sender, s_channel = sender,
r_port = reply_to, r_channel = reply_to,
msg = message, msg = message,
dist = distance dist = distance
} }
self.valid = false
self.raw = self.modem_msg_in.msg self.raw = self.modem_msg_in.msg
if (type(max_distance) == "number") and (distance > max_distance) then 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") -- log.debug("comms.scada_packet.receive(): discarding packet with distance " .. distance .. " outside of trusted range")
else else
if type(self.raw) == "table" then if type(self.raw) == "table" then
if #self.raw >= 3 then if #self.raw == 5 then
self.seq_num = self.raw[1] self.src_addr = self.raw[1]
self.protocol = self.raw[2] self.dest_addr = self.raw[2]
self.seq_num = self.raw[3]
self.protocol = self.raw[4]
-- element 3 must be a table -- element 5 must be a table
if type(self.raw[3]) == "table" then if type(self.raw[5]) == "table" then
self.length = #self.raw[3] self.length = #self.raw[5]
self.payload = self.raw[3] self.payload = self.raw[5]
end end
else
self.src_addr = nil
self.dest_addr = nil
self.seq_num = nil
self.protocol = nil
self.length = 0
self.payload = {}
end end
self.valid = type(self.seq_num) == "number" and -- check if this packet is destined for this device
type(self.protocol) == "number" and local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == C_ID)
type(self.payload) == "table"
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
end end
@ -216,13 +237,17 @@ function comms.scada_packet()
function public.raw_sendable() return self.raw end function public.raw_sendable() return self.raw end
---@nodiscard ---@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 ---@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 ---@nodiscard
function public.is_valid() return self.valid end 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 ---@nodiscard
function public.seq_num() return self.seq_num end function public.seq_num() return self.seq_num end
---@nodiscard ---@nodiscard

View File

@ -74,6 +74,15 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
-- ENUMERATION TYPES -- -- ENUMERATION TYPES --
--#region --#region
---@enum PANEL_LINK_STATE
types.PANEL_LINK_STATE = {
LINKED = 1,
DENIED = 2,
COLLISION = 3,
BAD_VERSION = 4,
DISCONNECTED = 5
}
---@enum RTU_UNIT_TYPE ---@enum RTU_UNIT_TYPE
types.RTU_UNIT_TYPE = { types.RTU_UNIT_TYPE = {
VIRTUAL = 0, -- virtual device VIRTUAL = 0, -- virtual device

View File

@ -65,7 +65,8 @@ end
---@return string ---@return string
function util.strval(val) function util.strval(val)
local t = type(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) .. "]" return "[" .. tostring(val) .. "]"
else else
return tostring(val) 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(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_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 -- check if all assertions passed successfully
---@nodiscard ---@nodiscard

View File

@ -1,9 +1,15 @@
local config = {} local config = {}
-- scada network listen for PLC's and RTU's -- supervisor comms channel
config.SCADA_DEV_LISTEN = 16000 config.SVR_CHANNEL = 16240
-- listen port for SCADA supervisor access -- PLC comms channel
config.SCADA_SV_CTL_LISTEN = 16100 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) -- max trusted modem message distance (0 to disable check)
config.TRUSTED_RANGE = 0 config.TRUSTED_RANGE = 0
-- time in seconds (>= 2) before assuming a remote device is no longer active -- time in seconds (>= 2) before assuming a remote device is no longer active

View File

@ -3,9 +3,14 @@
-- --
local psil = require("scada-common.psil") local psil = require("scada-common.psil")
local util = require("scada-common.util")
local pgi = require("supervisor.panel.pgi") 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 = {} local databus = {}
-- databus PSIL -- databus PSIL
@ -31,11 +36,11 @@ end
-- transmit PLC firmware version and session connection state -- transmit PLC firmware version and session connection state
---@param reactor_id integer reactor unit ID ---@param reactor_id integer reactor unit ID
---@param fw string firmware version ---@param fw string firmware version
---@param channel integer PLC remote port ---@param s_addr integer PLC computer ID
function databus.tx_plc_connected(reactor_id, fw, channel) function databus.tx_plc_connected(reactor_id, fw, s_addr)
databus.ps.publish("plc_" .. reactor_id .. "_fw", fw) databus.ps.publish("plc_" .. reactor_id .. "_fw", fw)
databus.ps.publish("plc_" .. reactor_id .. "_conn", true) 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 end
-- transmit PLC disconnected -- transmit PLC disconnected
@ -43,7 +48,7 @@ end
function databus.tx_plc_disconnected(reactor_id) function databus.tx_plc_disconnected(reactor_id)
databus.ps.publish("plc_" .. reactor_id .. "_fw", " ------- ") databus.ps.publish("plc_" .. reactor_id .. "_fw", " ------- ")
databus.ps.publish("plc_" .. reactor_id .. "_conn", false) 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", 0)
databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.lightGray) databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.lightGray)
end end
@ -54,9 +59,9 @@ end
function databus.tx_plc_rtt(reactor_id, rtt) function databus.tx_plc_rtt(reactor_id, rtt)
databus.ps.publish("plc_" .. reactor_id .. "_rtt", 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) 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) databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.yellow_hc)
else else
databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.green) databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.green)
@ -66,10 +71,10 @@ end
-- transmit RTU firmware version and session connection state -- transmit RTU firmware version and session connection state
---@param session_id integer RTU session ---@param session_id integer RTU session
---@param fw string firmware version ---@param fw string firmware version
---@param channel integer RTU remote port ---@param s_addr integer RTU computer ID
function databus.tx_rtu_connected(session_id, fw, channel) function databus.tx_rtu_connected(session_id, fw, s_addr)
databus.ps.publish("rtu_" .. session_id .. "_fw", fw) 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) pgi.create_rtu_entry(session_id)
end end
@ -85,9 +90,9 @@ end
function databus.tx_rtu_rtt(session_id, rtt) function databus.tx_rtu_rtt(session_id, rtt)
databus.ps.publish("rtu_" .. session_id .. "_rtt", 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) 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) databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.yellow_hc)
else else
databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.green) databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.green)
@ -103,18 +108,18 @@ end
-- transmit coordinator firmware version and session connection state -- transmit coordinator firmware version and session connection state
---@param fw string firmware version ---@param fw string firmware version
---@param channel integer coordinator remote port ---@param s_addr integer coordinator computer ID
function databus.tx_crd_connected(fw, channel) function databus.tx_crd_connected(fw, s_addr)
databus.ps.publish("crd_fw", fw) databus.ps.publish("crd_fw", fw)
databus.ps.publish("crd_conn", true) databus.ps.publish("crd_conn", true)
databus.ps.publish("crd_chan", tostring(channel)) databus.ps.publish("crd_addr", tostring(s_addr))
end end
-- transmit coordinator disconnected -- transmit coordinator disconnected
function databus.tx_crd_disconnected() function databus.tx_crd_disconnected()
databus.ps.publish("crd_fw", " ------- ") databus.ps.publish("crd_fw", " ------- ")
databus.ps.publish("crd_conn", false) 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", 0)
databus.ps.publish("crd_rtt_color", colors.lightGray) databus.ps.publish("crd_rtt_color", colors.lightGray)
end end
@ -124,9 +129,9 @@ end
function databus.tx_crd_rtt(rtt) function databus.tx_crd_rtt(rtt)
databus.ps.publish("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) 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) databus.ps.publish("crd_rtt_color", colors.yellow_hc)
else else
databus.ps.publish("crd_rtt_color", colors.green) databus.ps.publish("crd_rtt_color", colors.green)
@ -136,10 +141,10 @@ end
-- transmit PKT firmware version and PDG session connection state -- transmit PKT firmware version and PDG session connection state
---@param session_id integer PDG session ---@param session_id integer PDG session
---@param fw string firmware version ---@param fw string firmware version
---@param channel integer PDG remote port ---@param s_addr integer PDG computer ID
function databus.tx_pdg_connected(session_id, fw, channel) function databus.tx_pdg_connected(session_id, fw, s_addr)
databus.ps.publish("pdg_" .. session_id .. "_fw", fw) 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) pgi.create_pdg_entry(session_id)
end end
@ -155,9 +160,9 @@ end
function databus.tx_pdg_rtt(session_id, rtt) function databus.tx_pdg_rtt(session_id, rtt)
databus.ps.publish("pdg_" .. session_id .. "_rtt", 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) 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) databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.yellow_hc)
else else
databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.green) databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.green)

View File

@ -2,8 +2,6 @@
-- Pocket Diagnostics Connection Entry -- Pocket Diagnostics Connection Entry
-- --
local util = require("scada-common.util")
local databus = require("supervisor.databus") local databus = require("supervisor.databus")
local core = require("graphics.core") local core = require("graphics.core")
@ -28,9 +26,9 @@ local function init(parent, id)
local ps_prefix = "pdg_" .. 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)} 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)} 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} 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)} local pdg_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=cpair(colors.lightGray,colors.white)}

View File

@ -2,8 +2,6 @@
-- RTU Connection Entry -- RTU Connection Entry
-- --
local util = require("scada-common.util")
local databus = require("supervisor.databus") local databus = require("supervisor.databus")
local core = require("graphics.core") local core = require("graphics.core")
@ -28,9 +26,9 @@ local function init(parent, id)
local ps_prefix = "rtu_" .. 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)} 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)} 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} 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)} 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)}

View File

@ -56,6 +56,10 @@ local function init(panel)
modem.register(databus.ps, "has_modem", modem.update) 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 -- 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=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)} 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) 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)} local plc_addr = 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) 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} 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)} 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)} 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) 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)} TextBox{parent=crd_box,x=4,y=3,text="COMPUTER",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)} local crd_addr = TextBox{parent=crd_box,x=13,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) 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} 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)} local crd_fw_v = TextBox{parent=crd_box,x=26,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)}

View File

@ -45,12 +45,13 @@ local PERIODICS = {
-- coordinator supervisor session -- coordinator supervisor session
---@nodiscard ---@nodiscard
---@param id integer session ID ---@param id integer session ID
---@param s_addr integer device source address
---@param in_queue mqueue in message queue ---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue ---@param out_queue mqueue out message queue
---@param timeout number communications timeout ---@param timeout number communications timeout
---@param facility facility facility data table ---@param facility facility facility data table
---@param fp_ok boolean if the front panel UI is running ---@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 -- 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 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() local c_pkt = comms.crdn_packet()
c_pkt.make(msg_type, msg) 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) out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1 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() local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg) 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) out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1 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
end end
---@class coord_session ---@class crd_session
local public = {} local public = {}
-- get the session ID -- get the session ID

View File

@ -45,12 +45,13 @@ local PERIODICS = {
-- PLC supervisor session -- PLC supervisor session
---@nodiscard ---@nodiscard
---@param id integer session ID ---@param id integer session ID
---@param s_addr integer device source address
---@param reactor_id integer reactor ID ---@param reactor_id integer reactor ID
---@param in_queue mqueue in message queue ---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue ---@param out_queue mqueue out message queue
---@param timeout number communications timeout ---@param timeout number communications timeout
---@param fp_ok boolean if the front panel UI is running ---@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 -- 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 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() local r_pkt = comms.rplc_packet()
r_pkt.make(reactor_id, msg_type, msg) 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) out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1 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() local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg) 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) out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1

View File

@ -29,11 +29,12 @@ local PERIODICS = {
-- pocket diagnostics session -- pocket diagnostics session
---@nodiscard ---@nodiscard
---@param id integer session ID ---@param id integer session ID
---@param s_addr integer device source address
---@param in_queue mqueue in message queue ---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue ---@param out_queue mqueue out message queue
---@param timeout number communications timeout ---@param timeout number communications timeout
---@param fp_ok boolean if the front panel UI is running ---@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 -- 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 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() local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg) 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) out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1

View File

@ -31,13 +31,14 @@ local PERIODICS = {
-- create a new RTU session -- create a new RTU session
---@nodiscard ---@nodiscard
---@param id integer session ID ---@param id integer session ID
---@param s_addr integer device source address
---@param in_queue mqueue in message queue ---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue ---@param out_queue mqueue out message queue
---@param timeout number communications timeout ---@param timeout number communications timeout
---@param advertisement table RTU device advertisement ---@param advertisement table RTU device advertisement
---@param facility facility facility data table ---@param facility facility facility data table
---@param fp_ok boolean if the front panel UI is running ---@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 -- 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 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 function _send_modbus(m_pkt)
local s_pkt = comms.scada_packet() 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) out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1 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() local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg) 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) out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1

View File

@ -27,7 +27,7 @@ local svsessions = {}
local SESSION_TYPE = { local SESSION_TYPE = {
RTU_SESSION = 0, -- RTU gateway RTU_SESSION = 0, -- RTU gateway
PLC_SESSION = 1, -- reactor PLC PLC_SESSION = 1, -- reactor PLC
COORD_SESSION = 2, -- coordinator CRD_SESSION = 2, -- coordinator
PDG_SESSION = 3 -- pocket diagnostics PDG_SESSION = 3 -- pocket diagnostics
} }
@ -38,11 +38,11 @@ local self = {
fp_ok = false, fp_ok = false,
num_reactors = 0, num_reactors = 0,
facility = nil, ---@type facility|nil facility = nil, ---@type facility|nil
sessions = { rtu = {}, plc = {}, coord = {}, pdg = {} }, sessions = { rtu = {}, plc = {}, crd = {}, pdg = {} },
next_ids = { rtu = 0, plc = 0, coord = 0, pdg = 0 } 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 -- -- PRIVATE FUNCTIONS --
@ -60,7 +60,7 @@ local function _sv_handle_outq(session)
if msg ~= nil then if msg ~= nil then
if msg.qtype == mqueue.TYPE.PACKET then if msg.qtype == mqueue.TYPE.PACKET then
-- handle a packet to be sent -- 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 elseif msg.qtype == mqueue.TYPE.COMMAND then
-- handle instruction/notification -- handle instruction/notification
elseif msg.qtype == mqueue.TYPE.DATA then 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 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]) plc_s.in_queue.push_data(PLC_S_DATA.BURN_RATE, cmd.val[2])
else 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
end end
else else
local crd_s = svsessions.get_coord_session() local crd_s = svsessions.get_crd_session()
if crd_s ~= nil then if crd_s ~= nil then
if cmd.key == SV_Q_DATA.CRDN_ACK then if cmd.key == SV_Q_DATA.CRDN_ACK then
-- ack to be sent to coordinator -- ack to be sent to coordinator
@ -104,8 +104,8 @@ local function _sv_handle_outq(session)
-- max 100ms spent processing queue -- max 100ms spent processing queue
if util.time() - handle_start > 100 then if util.time() - handle_start > 100 then
log.warning("supervisor out queue handler exceeded 100ms queue process limit") log.warning("[SVS] 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(util.c("[SVS] offending session: ", session))
break break
end end
end end
@ -131,15 +131,15 @@ local function _shutdown(session)
session.open = false session.open = false
session.instance.close() 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 while session.out_queue.ready() do
local msg = session.out_queue.pop() local msg = session.out_queue.pop()
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then 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
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 end
-- close connections -- close connections
@ -160,8 +160,7 @@ local function _check_watchdogs(sessions, timer_event)
if session.open then if session.open then
local triggered = session.instance.check_wd(timer_event) local triggered = session.instance.check_wd(timer_event)
if triggered then if triggered then
log.debug(util.c("watchdog closing ", session.s_type, " session ", session.instance.get_id(), log.debug(util.c("[SVS] watchdog closing session ", session, "..."))
" on remote port ", session.r_port, "..."))
_shutdown(session) _shutdown(session)
end end
end end
@ -175,21 +174,20 @@ local function _free_closed(sessions)
---@param session sv_session_structs ---@param session sv_session_structs
local on_delete = function (session) local on_delete = function (session)
log.debug(util.c("free'ing closed ", session.s_type, " session ", session.instance.get_id(), log.debug(util.c("[SVS] free'ing closed session ", session))
" on remote port ", session.r_port))
end end
util.filter_table(sessions, f, on_delete) util.filter_table(sessions, f, on_delete)
end end
-- find a session by remote port -- find a session by computer ID
---@nodiscard ---@nodiscard
---@param list table ---@param list table
---@param port integer ---@param s_addr integer
---@return sv_session_structs|nil ---@return sv_session_structs|nil
local function _find_session(list, port) local function _find_session(list, s_addr)
for i = 1, #list do 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 end
return nil return nil
end end
@ -214,63 +212,55 @@ function svsessions.relink_modem(modem)
self.modem = modem self.modem = modem
end end
-- find an RTU session by the remote port -- find an RTU session by the computer ID
---@nodiscard ---@nodiscard
---@param remote_port integer ---@param source_addr integer
---@return rtu_session_struct|nil ---@return rtu_session_struct|nil
function svsessions.find_rtu_session(remote_port) function svsessions.find_rtu_session(source_addr)
-- check RTU sessions -- 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 ---@cast session rtu_session_struct|nil
return session return session
end end
-- find a PLC session by the remote port -- find a PLC session by the computer ID
---@nodiscard ---@nodiscard
---@param remote_port integer ---@param source_addr integer
---@return plc_session_struct|nil ---@return plc_session_struct|nil
function svsessions.find_plc_session(remote_port) function svsessions.find_plc_session(source_addr)
-- check PLC sessions -- 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 ---@cast session plc_session_struct|nil
return session return session
end end
-- find a PLC/RTU session by the remote port -- find a coordinator session by the computer ID
---@nodiscard ---@nodiscard
---@param remote_port integer ---@param source_addr integer
---@return plc_session_struct|rtu_session_struct|nil ---@return crd_session_struct|nil
function svsessions.find_device_session(remote_port) function svsessions.find_crd_session(source_addr)
-- check RTU sessions -- check coordinator sessions
local session = _find_session(self.sessions.rtu, remote_port) local session = _find_session(self.sessions.crd, source_addr)
---@cast session crd_session_struct|nil
-- 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
return session return session
end end
-- find a coordinator or diagnostic access session by the remote port -- find a pocket diagnostics session by the computer ID
---@nodiscard ---@nodiscard
---@param remote_port integer ---@param source_addr integer
---@return coord_session_struct|pdg_session_struct|nil ---@return pdg_session_struct|nil
function svsessions.find_svctl_session(remote_port) function svsessions.find_pdg_session(source_addr)
-- check coordinator sessions
local session = _find_session(self.sessions.coord, remote_port)
-- check diagnostic sessions -- check diagnostic sessions
if session == nil then session = _find_session(self.sessions.pdg, remote_port) end local session = _find_session(self.sessions.pdg, source_addr)
---@cast session coord_session_struct|pdg_session_struct|nil ---@cast session pdg_session_struct|nil
return session return session
end end
-- get the a coordinator session if exists -- get the a coordinator session if exists
---@nodiscard ---@nodiscard
---@return coord_session_struct|nil ---@return crd_session_struct|nil
function svsessions.get_coord_session() function svsessions.get_crd_session()
return self.sessions.coord[1] return self.sessions.crd[1]
end end
-- get a session by reactor ID -- get a session by reactor ID
@ -291,12 +281,11 @@ end
-- establish a new PLC session -- establish a new PLC session
---@nodiscard ---@nodiscard
---@param local_port integer ---@param source_addr integer
---@param remote_port integer
---@param for_reactor integer ---@param for_reactor integer
---@param version string ---@param version string
---@return integer|false session_id ---@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 if svsessions.get_reactor_session(for_reactor) == nil and for_reactor >= 1 and for_reactor <= self.num_reactors then
---@class plc_session_struct ---@class plc_session_struct
local plc_s = { local plc_s = {
@ -304,26 +293,34 @@ function svsessions.establish_plc_session(local_port, remote_port, for_reactor,
open = true, open = true,
reactor = for_reactor, reactor = for_reactor,
version = version, version = version,
l_port = local_port, r_chan = config.PLC_CHANNEL,
r_port = remote_port, s_addr = source_addr,
in_queue = mqueue.new(), in_queue = mqueue.new(),
out_queue = mqueue.new(), out_queue = mqueue.new(),
instance = nil ---@type plc_session 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, 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) config.PLC_TIMEOUT, self.fp_ok)
table.insert(self.sessions.plc, plc_s) table.insert(self.sessions.plc, plc_s)
local units = self.facility.get_units() local units = self.facility.get_units()
units[for_reactor].link_plc_session(plc_s) 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, local mt = {
" for reactor ", for_reactor)) ---@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 -- success
return plc_s.instance.get_id() return plc_s.instance.get_id()
@ -335,70 +332,84 @@ end
-- establish a new RTU session -- establish a new RTU session
---@nodiscard ---@nodiscard
---@param local_port integer ---@param source_addr integer
---@param remote_port integer
---@param advertisement table ---@param advertisement table
---@param version string ---@param version string
---@return integer session_id ---@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 ---@class rtu_session_struct
local rtu_s = { local rtu_s = {
s_type = "rtu", s_type = "rtu",
open = true, open = true,
version = version, version = version,
l_port = local_port, r_chan = config.RTU_CHANNEL,
r_port = remote_port, s_addr = source_addr,
in_queue = mqueue.new(), in_queue = mqueue.new(),
out_queue = mqueue.new(), out_queue = mqueue.new(),
instance = nil ---@type rtu_session 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, local id = self.next_ids.rtu
self.facility, self.fp_ok)
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) 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 -- success
return rtu_s.instance.get_id() return id
end end
-- establish a new coordinator session -- establish a new coordinator session
---@nodiscard ---@nodiscard
---@param local_port integer ---@param source_addr integer
---@param remote_port integer
---@param version string ---@param version string
---@return integer|false session_id ---@return integer|false session_id
function svsessions.establish_coord_session(local_port, remote_port, version) function svsessions.establish_crd_session(source_addr, version)
if svsessions.get_coord_session() == nil then if svsessions.get_crd_session() == nil then
---@class coord_session_struct ---@class crd_session_struct
local coord_s = { local crd_s = {
s_type = "crd", s_type = "crd",
open = true, open = true,
version = version, version = version,
l_port = local_port, r_chan = config.CRD_CHANNEL,
r_port = remote_port, s_addr = source_addr,
in_queue = mqueue.new(), in_queue = mqueue.new(),
out_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) 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 -- success
return coord_s.instance.get_id() return id
else else
-- we already have a coordinator linked -- we already have a coordinator linked
return false return false
@ -407,34 +418,41 @@ end
-- establish a new pocket diagnostics session -- establish a new pocket diagnostics session
---@nodiscard ---@nodiscard
---@param local_port integer ---@param source_addr integer
---@param remote_port integer
---@param version string ---@param version string
---@return integer|false session_id ---@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 ---@class pdg_session_struct
local pdg_s = { local pdg_s = {
s_type = "pkt", s_type = "pkt",
open = true, open = true,
version = version, version = version,
l_port = local_port, r_chan = config.PKT_CHANNEL,
r_port = remote_port, s_addr = source_addr,
in_queue = mqueue.new(), in_queue = mqueue.new(),
out_queue = mqueue.new(), out_queue = mqueue.new(),
instance = nil ---@type pdg_session 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) 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 -- success
return pdg_s.instance.get_id() return id
end end
-- attempt to identify which session's watchdog timer fired -- attempt to identify which session's watchdog timer fired
@ -466,9 +484,7 @@ end
-- close all open connections -- close all open connections
function svsessions.close_all() function svsessions.close_all()
-- close sessions -- close sessions
for _, list in pairs(self.sessions) do for _, list in pairs(self.sessions) do _close(list) end
_close(list)
end
-- free sessions -- free sessions
svsessions.free_all_closed() svsessions.free_all_closed()

View File

@ -20,7 +20,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v0.16.8" local SUPERVISOR_VERSION = "v0.17.5"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -31,8 +31,11 @@ local println_ts = util.println_ts
local cfv = util.new_validator() local cfv = util.new_validator()
cfv.assert_port(config.SCADA_DEV_LISTEN) cfv.assert_channel(config.SVR_CHANNEL)
cfv.assert_port(config.SCADA_SV_CTL_LISTEN) 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_int(config.TRUSTED_RANGE)
cfv.assert_type_num(config.PLC_TIMEOUT) cfv.assert_type_num(config.PLC_TIMEOUT)
cfv.assert_min(config.PLC_TIMEOUT, 2) cfv.assert_min(config.PLC_TIMEOUT, 2)
@ -112,9 +115,8 @@ local function main()
println_ts = function (_) end println_ts = function (_) end
end end
-- start comms, open all channels -- start comms
local superv_comms = supervisor.comms(SUPERVISOR_VERSION, config.NUM_REACTORS, config.REACTOR_COOLING, modem, local superv_comms = supervisor.comms(SUPERVISOR_VERSION, modem, fp_ok)
config.SCADA_DEV_LISTEN, config.SCADA_SV_CTL_LISTEN, config.TRUSTED_RANGE, fp_ok)
-- base loop clock (6.67Hz, 3 ticks) -- base loop clock (6.67Hz, 3 ticks)
local MAIN_CLOCK = 0.15 local MAIN_CLOCK = 0.15

View File

@ -2,6 +2,8 @@ local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local util = require("scada-common.util") local util = require("scada-common.util")
local config = require("supervisor.config")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local supervisor = {} local supervisor = {}
@ -14,31 +16,36 @@ local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
-- supervisory controller communications -- supervisory controller communications
---@nodiscard ---@nodiscard
---@param _version string supervisor version ---@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 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 ---@param fp_ok boolean if the front panel UI is running
---@diagnostic disable-next-line: unused-local ---@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 -- 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 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 = { local self = {
last_est_acks = {} last_est_acks = {}
} }
comms.set_trusted_range(range) comms.set_trusted_range(config.TRUSTED_RANGE)
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure modem channels -- configure modem channels
local function _conf_channels() local function _conf_channels()
modem.closeAll() modem.closeAll()
modem.open(dev_listen) modem.open(svr_channel)
modem.open(svctl_listen)
end end
_conf_channels() _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 -- pass modem, status, and config data to svsessions
svsessions.init(modem, fp_ok, num_reactors, cooling_conf) svsessions.init(modem, fp_ok, num_reactors, cooling_conf)
-- send an establish request response to a PLC/RTU -- send an establish request response
---@param dest integer ---@param packet scada_packet
---@param msg table ---@param ack ESTABLISH_ACK
local function _send_dev_establish(seq_id, dest, msg) ---@param data? any optional data
local function _send_establish(packet, ack, data)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet() local m_pkt = comms.mgmt_packet()
m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg) m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, { ack, data })
s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) 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()) modem.transmit(packet.remote_channel(), svr_channel, s_pkt.raw_sendable())
end self.last_est_acks[packet.src_addr()] = ack
-- 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())
end end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
@ -138,30 +133,19 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste
---@param packet modbus_frame|rplc_frame|mgmt_frame|crdn_frame|nil ---@param packet modbus_frame|rplc_frame|mgmt_frame|crdn_frame|nil
function public.handle_packet(packet) function public.handle_packet(packet)
if packet ~= nil then if packet ~= nil then
local l_port = packet.scada_frame.local_port() local l_chan = packet.scada_frame.local_channel()
local r_port = packet.scada_frame.remote_port() local r_chan = packet.scada_frame.remote_channel()
local src_addr = packet.scada_frame.src_addr()
local protocol = packet.scada_frame.protocol() local protocol = packet.scada_frame.protocol()
-- device (RTU/PLC) listening channel if l_chan ~= svr_channel then
if l_port == dev_listen then log.debug("received packet on unconfigured channel " .. l_chan, true)
if protocol == PROTOCOL.MODBUS_TCP then elseif r_chan == plc_channel then
---@cast packet modbus_frame
-- look for an associated session -- look for an associated session
local session = svsessions.find_rtu_session(r_port) local session = svsessions.find_plc_session(src_addr)
-- MODBUS response if protocol == PROTOCOL.RPLC then
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug("discarding MODBUS_TCP packet without a known session")
end
elseif protocol == PROTOCOL.RPLC then
---@cast packet rplc_frame ---@cast packet rplc_frame
-- look for an associated session
local session = svsessions.find_plc_session(r_port)
-- reactor PLC packet -- reactor PLC packet
if session ~= nil then if session ~= nil then
-- pass the packet onto the session handler -- pass the packet onto the session handler
@ -169,20 +153,17 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste
else else
-- unknown session, force a re-link -- unknown session, force a re-link
log.debug("PLC_ESTABLISH: no session but not an establish, forcing relink") 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 }) _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end end
elseif protocol == PROTOCOL.SCADA_MGMT then elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
-- look for an associated session
local session = svsessions.find_device_session(r_port)
-- SCADA management packet -- SCADA management packet
if session ~= nil then if session ~= nil then
-- pass the packet onto the session handler -- pass the packet onto the session handler
session.in_queue.push_packet(packet) session.in_queue.push_packet(packet)
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- establish a new session -- 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 -- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
@ -191,71 +172,116 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste
local dev_type = packet.data[3] local dev_type = packet.data[3]
if comms_v ~= comms.version then 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 device establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) log.info(util.c("dropping PLC establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
self.last_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION
end end
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION }) _send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.PLC then elseif dev_type == DEVICE_TYPE.PLC then
-- PLC linking request -- PLC linking request
if packet.length == 4 and type(packet.data[4]) == "number" then if packet.length == 4 and type(packet.data[4]) == "number" then
local reactor_id = packet.data[4] local reactor_id = packet.data[4]
local plc_id = svsessions.establish_plc_session(l_port, r_port, reactor_id, firmware_v) local plc_id = svsessions.establish_plc_session(src_addr, reactor_id, firmware_v)
if plc_id == false then if plc_id == false then
-- reactor already has a PLC assigned -- reactor already has a PLC assigned
if self.last_est_acks[r_port] ~= ESTABLISH_ACK.COLLISION then if last_ack ~= ESTABLISH_ACK.COLLISION then
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id)) log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
self.last_est_acks[r_port] = ESTABLISH_ACK.COLLISION
end end
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION }) _send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION)
else else
-- got an ID; assigned to a reactor successfully -- got an ID; assigned to a reactor successfully
println(util.c("PLC (", firmware_v, ") [:", r_port, "] \xbb reactor ", reactor_id, " connected")) println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \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)) 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)
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW })
self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW
end end
else else
log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type") log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type")
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end 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
-- MODBUS response
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug("discarding MODBUS_TCP packet without a known session")
end
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == 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 RTU establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.RTU then elseif dev_type == DEVICE_TYPE.RTU then
if packet.length == 4 then if packet.length == 4 then
-- this is an RTU advertisement for a new session -- this is an RTU advertisement for a new session
local rtu_advert = packet.data[4] 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")) println(util.c("RTU (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("RTU_ESTABLISH: RTU (",firmware_v, ") [:", r_port, "] connected with session ID ", s_id)) 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)
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW })
else else
log.debug("RTU_ESTABLISH: packet length mismatch") 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 end
else else
log.debug(util.c("illegal establish packet for device ", dev_type, " on PLC/RTU listening channel")) log.debug(util.c("illegal establish packet for device ", dev_type, " on RTU channel"))
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end end
else else
log.debug("invalid establish packet (on PLC/RTU listening channel)") log.debug("invalid establish packet (on RTU channel)")
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end end
else else
-- any other packet should be session related, discard it -- 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 end
else else
log.debug("illegal packet type " .. protocol .. " on device listening channel") log.debug(util.c("illegal packet type ", protocol, " on RTU channel"))
end end
-- coordinator listening channel elseif r_chan == crd_channel then
elseif l_port == svctl_listen then
-- look for an associated session -- 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 if protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
@ -265,7 +291,7 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste
session.in_queue.push_packet(packet) session.in_queue.push_packet(packet)
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- establish a new session -- 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 -- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
@ -274,56 +300,44 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste
local dev_type = packet.data[3] local dev_type = packet.data[3]
if comms_v ~= comms.version then 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, ")")) 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 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 elseif dev_type == DEVICE_TYPE.CRDN then
-- this is an attempt to establish a new coordinator session -- 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 if s_id ~= false then
local config = { num_reactors } local cfg = { num_reactors }
for i = 1, #cooling_conf do for i = 1, #cooling_conf do
table.insert(config, cooling_conf[i].BOILERS) table.insert(cfg, cooling_conf[i].BOILERS)
table.insert(config, cooling_conf[i].TURBINES) table.insert(cfg, cooling_conf[i].TURBINES)
end end
println(util.c("CRD (", firmware_v, ") [:", r_port, "] \xbb connected")) println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("SVCTL_ESTABLISH: coordinator (", firmware_v, ") [:", r_port, "] connected with session ID ", s_id)) 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 }) _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, cfg)
self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW
else else
if self.last_est_acks[r_port] ~= ESTABLISH_ACK.COLLISION then if last_ack ~= ESTABLISH_ACK.COLLISION then
log.info("SVCTL_ESTABLISH: denied new coordinator due to already being connected to another coordinator") log.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator")
self.last_est_acks[r_port] = ESTABLISH_ACK.COLLISION
end 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 })
end end
else else
log.debug("SVCTL_ESTABLISH: establish packet length mismatch") log.debug(util.c("illegal establish packet for device ", dev_type, " on coordinator channel"))
_send_svctl_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug("CRD_ESTABLISH: establish packet length mismatch")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end end
else else
-- any other packet should be session related, discard it -- 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 end
elseif protocol == PROTOCOL.SCADA_CRDN then elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame ---@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) session.in_queue.push_packet(packet)
else else
-- any other packet should be session related, discard it -- 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 end
else 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 end
else else
log.debug("received packet on unconfigured channel " .. l_port, true) log.debug("PDG_ESTABLISH: establish packet length mismatch")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding pocket SCADA_MGMT packet without a known session from computer ", src_addr))
end
elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
-- coordinator packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding pocket SCADA_CRDN packet without a known session from computer ", src_addr))
end
else
log.debug(util.c("illegal packet type ", protocol, " on pocket channel"))
end
else
log.debug("received packet for unknown channel " .. r_chan, true)
end end
end end
end end

View File

@ -38,7 +38,7 @@ local pkt = comms.modbus_packet()
---@diagnostic disable-next-line: param-type-mismatch ---@diagnostic disable-next-line: param-type-mismatch
pkt.make(1, 2, 7, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) pkt.make(1, 2, 7, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
local spkt = comms.scada_packet() local spkt = comms.scada_packet()
spkt.make(1, 1, pkt.raw_sendable()) spkt.make(0, 1, 1, pkt.raw_sendable())
start = util.time() start = util.time()
local data = textutils.serialize(spkt.raw_sendable(), { allow_repetitions = true, compact = true }) local data = textutils.serialize(spkt.raw_sendable(), { allow_repetitions = true, compact = true })