Merge branch 'devel' into 506-single-file-off-line-installer

This commit is contained in:
Mikayla Fischler 2024-06-29 15:53:27 -04:00
commit d424cf74d3
27 changed files with 612 additions and 569 deletions

View File

@ -138,6 +138,7 @@ local function gen_tree(manifest)
for i = 1, #list do
local split = {}
---@diagnostic disable-next-line: discard-returns
string.gsub(list[i], "([^/]+)", function(c) split[#split + 1] = c end)
if #split == 1 then table.insert(tree, list[i])
else table.insert(tree, _tree_add(tree, split)) end

View File

@ -232,8 +232,8 @@ function coordinator.comms(version, nic, sv_watchdog)
local self = {
sv_linked = false,
sv_addr = comms.BROADCAST,
sv_seq_num = 0,
sv_r_seq_num = nil,
sv_seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
sv_r_seq_num = nil, ---@type nil|integer
sv_config_err = false,
last_est_ack = ESTABLISH_ACK.ALLOW,
last_api_est_acks = {},
@ -492,7 +492,7 @@ function coordinator.comms(version, nic, sv_watchdog)
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_API_VERSION)
elseif dev_type == DEVICE_TYPE.PKT then
-- pocket linking request
local id = apisessions.establish_session(src_addr, firmware_v)
local id = apisessions.establish_session(src_addr, packet.scada_frame.seq_num(), firmware_v)
coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id))
local conf = iocontrol.get_db().facility.conf
@ -515,15 +515,15 @@ function coordinator.comms(version, nic, sv_watchdog)
elseif r_chan == config.SVR_Channel then
-- check sequence number
if self.sv_r_seq_num == nil then
self.sv_r_seq_num = packet.scada_frame.seq_num()
elseif self.sv_linked and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.sv_r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return false
elseif self.sv_linked and src_addr ~= self.sv_addr then
log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?")
return false
else
self.sv_r_seq_num = packet.scada_frame.seq_num()
self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
end
-- feed watchdog on valid sequence number
@ -706,7 +706,6 @@ function coordinator.comms(version, nic, sv_watchdog)
self.sv_addr = src_addr
self.sv_linked = true
self.sv_r_seq_num = nil
self.sv_config_err = false
iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED)

View File

@ -89,10 +89,11 @@ end
-- establish a new API session
---@nodiscard
---@param source_addr integer
---@param version string
---@param source_addr integer pocket computer ID
---@param i_seq_num integer initial (most recent) sequence number
---@param version string pocket version
---@return integer session_id
function apisessions.establish_session(source_addr, version)
function apisessions.establish_session(source_addr, i_seq_num, version)
---@class pkt_session_struct
local pkt_s = {
open = true,
@ -105,7 +106,7 @@ function apisessions.establish_session(source_addr, version)
local id = self.next_id
pkt_s.instance = pocket.new_session(id, source_addr, pkt_s.in_queue, pkt_s.out_queue, self.config.API_Timeout)
pkt_s.instance = pocket.new_session(id, source_addr, i_seq_num, pkt_s.in_queue, pkt_s.out_queue, self.config.API_Timeout)
table.insert(self.sessions, pkt_s)
local mt = {

View File

@ -32,16 +32,17 @@ local PERIODICS = {
---@nodiscard
---@param id integer session ID
---@param s_addr integer device source address
---@param i_seq_num integer initial sequence number
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
local log_header = "pkt_session(" .. id .. "): "
local self = {
-- connection properties
seq_num = 0,
r_seq_num = nil,
seq_num = i_seq_num + 2, -- next after the establish approval was sent
r_seq_num = i_seq_num + 1,
connected = true,
conn_watchdog = util.new_watchdog(timeout),
last_rtt = 0,
@ -104,13 +105,11 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
---@param pkt mgmt_frame|crdn_frame
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return
else
self.r_seq_num = pkt.scada_frame.seq_num()
self.r_seq_num = pkt.scada_frame.seq_num() + 1
end
-- feed watchdog

View File

@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder")
local threads = require("coordinator.threads")
local COORDINATOR_VERSION = "v1.4.7"
local COORDINATOR_VERSION = "v1.5.1"
local CHUNK_LOAD_DELAY_S = 30.0
@ -151,8 +151,8 @@ local function main()
-- core coordinator devices
crd_dev = {
speaker = ppm.get_device("speaker"),
modem = ppm.get_wireless_modem()
modem = ppm.get_wireless_modem(),
speaker = ppm.get_device("speaker")
},
-- system objects

View File

@ -342,10 +342,8 @@ function iocontrol.report_link_state(state, sv_addr, api_addr)
io.ps.publish("crd_conn_quality", 0)
end
if state == LINK_STATE.LINKED then
io.ps.publish("sv_addr", sv_addr)
io.ps.publish("api_addr", api_addr)
end
if sv_addr then io.ps.publish("sv_addr", sv_addr) end
if api_addr then io.ps.publish("api_addr", api_addr) end
end
-- determine supervisor connection quality (trip time)
@ -525,7 +523,10 @@ function iocontrol.record_unit_data(data)
reactor_state = 6 -- SCRAM
rps_status = 2
end
else rps_status = 4 end
else
rps_status = 4
reactor_state = 4
end
-- update reactor/control status
if unit.reactor_data.mek_status.status then

View File

@ -78,15 +78,16 @@ end
---@enum POCKET_APP_ID
local APP_ID = {
ROOT = 1,
LOADER = 2,
-- main app pages
UNITS = 2,
GUIDE = 3,
ABOUT = 4,
UNITS = 3,
GUIDE = 4,
ABOUT = 5,
-- diag app page
ALARMS = 5,
ALARMS = 6,
-- other
DUMMY = 6,
NUM_APPS = 6
DUMMY = 7,
NUM_APPS = 7
}
pocket.APP_ID = APP_ID
@ -98,9 +99,9 @@ pocket.APP_ID = APP_ID
---@field switcher function|nil function to switch between children
---@field tasks table tasks to run while viewing this page
-- allocate the page navigation system
---@param render_queue mqueue
function pocket.init_nav(render_queue)
-- initialize the page navigation system
---@param smem pkt_shared_memory
function pocket.init_nav(smem)
local self = {
pane = nil, ---@type graphics_element
sidebar = nil, ---@type graphics_element
@ -108,6 +109,7 @@ function pocket.init_nav(render_queue)
containers = {},
help_map = {},
help_return = nil,
loader_return = nil,
cur_app = APP_ID.ROOT
}
@ -143,10 +145,13 @@ function pocket.init_nav(render_queue)
app.load = function () app.loaded = true end
app.unload = function () app.loaded = false end
-- check which connections this requires
-- check which connections this requires (for unload)
---@return boolean requires_sv, boolean requires_api
function app.check_requires() return require_sv or false, require_api or false end
-- check if any connection is required (for load)
function app.requires_conn() return require_sv or require_api or false end
-- delayed set of the pane if it wasn't ready at the start
---@param root_pane graphics_element multipane
function app.set_root_pane(root_pane)
@ -254,7 +259,14 @@ function pocket.init_nav(render_queue)
local app = self.apps[app_id] ---@type pocket_app
if app then
if not app.loaded then render_queue.push_data(MQ__RENDER_DATA.LOAD_APP, app_id) end
if app.requires_conn() and not smem.pkt_sys.pocket_comms.is_linked() then
-- bring up the app loader
self.loader_return = app_id
app_id = APP_ID.LOADER
app = self.apps[app_id]
else self.loader_return = nil end
if not app.loaded then smem.q.mq_render.push_data(MQ__RENDER_DATA.LOAD_APP, app_id) end
self.cur_app = app_id
self.pane.set_value(app_id)
@ -267,6 +279,13 @@ function pocket.init_nav(render_queue)
end
end
-- open the app that was blocked on connecting
function nav.on_loader_connected()
if self.loader_return then
nav.open_app(self.loader_return)
end
end
-- load a given app
---@param app_id POCKET_APP_ID
function nav.load_app(app_id)
@ -351,15 +370,15 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
sv = {
linked = false,
addr = comms.BROADCAST,
seq_num = 0,
r_seq_num = nil, ---@type nil|integer
seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, ---@type nil|integer
last_est_ack = ESTABLISH_ACK.ALLOW
},
api = {
linked = false,
addr = comms.BROADCAST,
seq_num = 0,
r_seq_num = nil, ---@type nil|integer
seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, ---@type nil|integer
last_est_ack = ESTABLISH_ACK.ALLOW
},
establish_delay_counter = 0
@ -585,8 +604,8 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
elseif r_chan == config.CRD_Channel then
-- check sequence number
if self.api.r_seq_num == nil then
self.api.r_seq_num = packet.scada_frame.seq_num()
elseif self.connected and ((self.api.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
self.api.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.api.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order (API): last = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
elseif self.api.linked and (src_addr ~= self.api.addr) then
@ -594,7 +613,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
"); channel in use by another system?")
return
else
self.api.r_seq_num = packet.scada_frame.seq_num()
self.api.r_seq_num = packet.scada_frame.seq_num() + 1
end
-- feed watchdog on valid sequence number
@ -705,8 +724,8 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
elseif r_chan == config.SVR_Channel then
-- check sequence number
if self.sv.r_seq_num == nil then
self.sv.r_seq_num = packet.scada_frame.seq_num()
elseif self.connected and ((self.sv.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
self.sv.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.sv.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order (SVR): last = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
elseif self.sv.linked and (src_addr ~= self.sv.addr) then
@ -714,7 +733,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
"); channel in use by another system?")
return
else
self.sv.r_seq_num = packet.scada_frame.seq_num()
self.sv.r_seq_num = packet.scada_frame.seq_num() + 1
end
-- feed watchdog on valid sequence number
@ -844,6 +863,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
---@nodiscard
function public.is_api_linked() return self.api.linked end
-- check if we are still linked with the supervisor and coordinator
---@nodiscard
function public.is_linked() return self.sv.linked and self.api.linked end
return public
end

View File

@ -20,7 +20,7 @@ local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer")
local threads = require("pocket.threads")
local POCKET_VERSION = "v0.10.0-alpha"
local POCKET_VERSION = "v0.11.0-alpha"
local println = util.println
local println_ts = util.println_ts
@ -123,7 +123,7 @@ local function main()
-- setup system
----------------------------------------
smem_sys.nav = pocket.init_nav(__shared_memory.q.mq_render)
smem_sys.nav = pocket.init_nav(__shared_memory)
-- message authentication init
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then

View File

@ -32,7 +32,7 @@ local function create_pages(root)
local alarm_test = Div{parent=root,x=1,y=1}
local alarm_app = db.nav.register_app(APP_ID.ALARMS, alarm_test)
local alarm_app = db.nav.register_app(APP_ID.ALARMS, alarm_test, nil, true)
local page = alarm_app.new_page(nil, function () end)
page.tasks = { db.diag.tone_test.get_tone_states }

49
pocket/ui/apps/loader.lua Normal file
View File

@ -0,0 +1,49 @@
--
-- Loading Screen App
--
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local conn_waiting = require("pocket.ui.components.conn_waiting")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local APP_ID = pocket.APP_ID
local LINK_STATE = iocontrol.LINK_STATE
-- create the connecting to SV & API page
---@param root graphics_element parent
local function create_pages(root)
local db = iocontrol.get_db()
local main = Div{parent=root,x=1,y=1}
db.nav.register_app(APP_ID.LOADER, main).new_page(nil, function () end)
local conn_sv_wait = conn_waiting(main, 6, false)
local conn_api_wait = conn_waiting(main, 6, true)
local main_pane = Div{parent=main,x=1,y=2}
local root_pane = MultiPane{parent=main,x=1,y=1,panes={conn_sv_wait,conn_api_wait,main_pane}}
root_pane.register(db.ps, "link_state", function (state)
if state == LINK_STATE.UNLINKED or state == LINK_STATE.API_LINK_ONLY then
root_pane.set_value(1)
elseif state == LINK_STATE.SV_LINK_ONLY then
root_pane.set_value(2)
else
root_pane.set_value(3)
db.nav.on_loader_connected()
end
end)
TextBox{parent=main_pane,text="Connected!",x=1,y=6,alignment=core.ALIGN.CENTER}
end
return create_pages

View File

@ -23,16 +23,16 @@ local function init(parent, y, is_api)
local root = Div{parent=parent,x=1,y=1}
-- bounding box div
local box = Div{parent=root,x=1,y=y,height=5}
local box = Div{parent=root,x=1,y=y,height=6}
local waiting_x = math.floor(parent.get_width() / 2) - 1
if is_api then
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)}
TextBox{parent=box,text="Connecting to API",alignment=ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)}
TextBox{parent=box,text="Connecting to API",alignment=ALIGN.CENTER,y=5,fg_bg=cpair(colors.white,style.root.bkg)}
else
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.green,style.root.bkg)}
TextBox{parent=box,text="Connecting to Supervisor",alignment=ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)}
TextBox{parent=box,text="Connecting to Supervisor",alignment=ALIGN.CENTER,y=5,fg_bg=cpair(colors.white,style.root.bkg)}
end
return root

View File

@ -10,11 +10,10 @@ local pocket = require("pocket.pocket")
local diag_apps = require("pocket.ui.apps.diag_apps")
local dummy_app = require("pocket.ui.apps.dummy_app")
local guide_app = require("pocket.ui.apps.guide")
local loader_app = require("pocket.ui.apps.loader")
local sys_apps = require("pocket.ui.apps.sys_apps")
local unit_app = require("pocket.ui.apps.unit")
local conn_waiting = require("pocket.ui.components.conn_waiting")
local home_page = require("pocket.ui.pages.home_page")
local style = require("pocket.ui.style")
@ -33,8 +32,6 @@ local SignalBar = require("graphics.elements.indicators.signal")
local ALIGN = core.ALIGN
local cpair = core.cpair
local LINK_STATE = iocontrol.LINK_STATE
local APP_ID = pocket.APP_ID
-- create new main view
@ -42,7 +39,9 @@ local APP_ID = pocket.APP_ID
local function init(main)
local db = iocontrol.get_db()
-- window header message
local main_pane = Div{parent=main,x=1,y=2}
-- window header message and connection status
TextBox{parent=main,y=1,text="EARLY ACCESS ALPHA S C ",alignment=ALIGN.LEFT,height=1,fg_bg=style.header}
local svr_conn = SignalBar{parent=main,y=1,x=22,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
local crd_conn = SignalBar{parent=main,y=1,x=26,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
@ -50,40 +49,18 @@ local function init(main)
db.ps.subscribe("svr_conn_quality", svr_conn.set_value)
db.ps.subscribe("crd_conn_quality", crd_conn.set_value)
--#region root panel panes (connection screens + main screen)
local root_pane_div = Div{parent=main,x=1,y=2}
local conn_sv_wait = conn_waiting(root_pane_div, 6, false)
local conn_api_wait = conn_waiting(root_pane_div, 6, true)
local main_pane = Div{parent=main,x=1,y=2}
local root_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={conn_sv_wait,conn_api_wait,main_pane}}
root_pane.register(db.ps, "link_state", function (state)
if state == LINK_STATE.UNLINKED or state == LINK_STATE.API_LINK_ONLY then
root_pane.set_value(1)
elseif state == LINK_STATE.SV_LINK_ONLY then
root_pane.set_value(2)
else
root_pane.set_value(3)
end
end)
--#endregion
--#region main page panel panes & sidebar
local page_div = Div{parent=main_pane,x=4,y=1}
-- create all the apps & pages
home_page(page_div)
unit_app(page_div)
guide_app(page_div)
loader_app(page_div)
sys_apps(page_div)
diag_apps(page_div)
dummy_app(page_div)
-- verify all apps were created
assert(util.table_len(db.nav.get_containers()) == APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered")
db.nav.set_pane(MultiPane{parent=page_div,x=1,y=1,panes=db.nav.get_containers()})
@ -92,8 +69,6 @@ local function init(main)
PushButton{parent=main_pane,x=1,y=19,text="\x1b",min_width=3,fg_bg=cpair(colors.white,colors.gray),active_fg_bg=cpair(colors.gray,colors.black),callback=db.nav.nav_up}
db.nav.open_app(APP_ID.ROOT)
--#endregion
end
return init

View File

@ -524,8 +524,8 @@ end
function plc.comms(version, nic, reactor, rps, conn_watchdog)
local self = {
sv_addr = comms.BROADCAST,
seq_num = 0,
r_seq_num = nil,
seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, ---@type nil|integer
scrammed = false,
linked = false,
last_est_ack = ESTABLISH_ACK.ALLOW,
@ -571,33 +571,17 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
self.seq_num = self.seq_num + 1
end
-- variable reactor status information, excluding heating rate
-- dynamic reactor status information, excluding heating rate
---@return table data_table, boolean faulted
local function _reactor_status()
local function _get_reactor_status()
local fuel = nil
local waste = nil
local coolant = nil
local hcoolant = nil
local data_table = {
false, -- getStatus
0, -- getBurnRate
0, -- getActualBurnRate
0, -- getTemperature
0, -- getDamagePercent
0, -- getBoilEfficiency
0, -- getEnvironmentalLoss
0, -- fuel_amnt
0, -- getFuelFilledPercentage
0, -- waste_amnt
0, -- getWasteFilledPercentage
"", -- coolant_name
0, -- coolant_amnt
0, -- getCoolantFilledPercentage
"", -- hcoolant_name
0, -- hcoolant_amnt
0 -- getHeatedCoolantFilledPercentage
}
local data_table = {}
reactor.__p_disable_afc()
local tasks = {
function () data_table[1] = reactor.getStatus() end,
@ -637,30 +621,32 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
data_table[16] = hcoolant.amount
end
reactor.__p_enable_afc()
return data_table, reactor.__p_is_faulted()
end
-- update the status cache if changed
---@return boolean changed
local function _update_status_cache()
local status, faulted = _reactor_status()
local status, faulted = _get_reactor_status()
local changed = false
if self.status_cache ~= nil then
if not faulted then
if not faulted then
if self.status_cache ~= nil then
for i = 1, #status do
if status[i] ~= self.status_cache[i] then
changed = true
break
end
end
else
changed = true
end
else
changed = true
end
if changed and not faulted then
self.status_cache = status
if changed then
self.status_cache = status
end
end
return changed
@ -679,9 +665,11 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
_send(msg_type, { status })
end
-- send structure properties (these should not change, server will cache these)
-- send static structure properties, cached by server
local function _send_struct()
local mek_data = { false, 0, 0, 0, types.new_zero_coordinate(), types.new_zero_coordinate(), 0, 0, 0, 0, 0, 0, 0, 0 }
local mek_data = {}
reactor.__p_disable_afc()
local tasks = {
function () mek_data[1] = reactor.getLength() end,
@ -705,6 +693,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
_send(RPLC_TYPE.MEK_STRUCT, mek_data)
self.resend_build = false
end
reactor.__p_enable_afc()
end
-- PUBLIC FUNCTIONS --
@ -835,8 +825,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
if l_chan == config.PLC_Channel then
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num()
elseif self.linked and ((self.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
self.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
elseif self.linked and (src_addr ~= self.sv_addr) then
@ -844,7 +834,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
"); channel in use by another system?")
return
else
self.r_seq_num = packet.scada_frame.seq_num()
self.r_seq_num = packet.scada_frame.seq_num() + 1
end
-- feed the watchdog first so it doesn't uhh...eat our packets :)
@ -1030,10 +1020,9 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
println_ts("linked!")
log.info("supervisor establish request approved, linked to SV (CID#" .. src_addr .. ")")
-- link + reset remote sequence number and cache
-- link + reset cache
self.sv_addr = src_addr
self.linked = true
self.r_seq_num = nil
self.status_cache = nil
if plc_state.reactor_formed then _send_struct() end

View File

@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "v1.7.11"
local R_PLC_VERSION = "v1.8.0"
local println = util.println
local println_ts = util.println_ts

View File

@ -284,8 +284,8 @@ end
function rtu.comms(version, nic, conn_watchdog)
local self = {
sv_addr = comms.BROADCAST,
seq_num = 0,
r_seq_num = nil,
seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, ---@type nil|integer
txn_id = 0,
last_est_ack = ESTABLISH_ACK.ALLOW
}
@ -442,8 +442,8 @@ function rtu.comms(version, nic, conn_watchdog)
if l_chan == config.RTU_Channel then
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num()
elseif rtu_state.linked and ((self.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
self.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
elseif rtu_state.linked and (src_addr ~= self.sv_addr) then
@ -451,7 +451,7 @@ function rtu.comms(version, nic, conn_watchdog)
"); channel in use by another system?")
return
else
self.r_seq_num = packet.scada_frame.seq_num()
self.r_seq_num = packet.scada_frame.seq_num() + 1
end
-- feed watchdog on valid sequence number
@ -556,7 +556,6 @@ function rtu.comms(version, nic, conn_watchdog)
-- establish allowed
rtu_state.linked = true
self.sv_addr = packet.scada_frame.src_addr()
self.r_seq_num = nil
println_ts("supervisor connection established")
log.info("supervisor connection established")
else

View File

@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "v1.9.6"
local RTU_VERSION = "v1.10.1"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
@ -93,14 +93,6 @@ local function main()
network.init_mac(config.AuthKey)
end
-- get modem
local modem = ppm.get_wireless_modem()
if modem == nil then
println("boot> wireless modem not found")
log.fatal("no wireless modem on startup")
return
end
-- generate alarm tones
audio.generate_tones()
@ -116,14 +108,15 @@ local function main()
-- RTU gateway devices (not RTU units)
rtu_dev = {
modem = ppm.get_wireless_modem(),
sounders = {}
},
-- system objects
rtu_sys = {
nic = network.nic(modem),
rtu_comms = nil, ---@type rtu_comms
conn_watchdog = nil, ---@type watchdog
nic = nil, ---@type nic
rtu_comms = nil, ---@type rtu_comms
conn_watchdog = nil, ---@type watchdog
units = {}
},
@ -134,8 +127,9 @@ local function main()
}
local smem_sys = __shared_memory.rtu_sys
local smem_dev = __shared_memory.rtu_dev
databus.tx_hw_modem(true)
local rtu_state = __shared_memory.rtu_state
----------------------------------------
-- interpret config and init units
@ -501,8 +495,6 @@ local function main()
-- start system
----------------------------------------
local rtu_state = __shared_memory.rtu_state
log.debug("boot> running sys_config()")
if sys_config() then
@ -517,23 +509,33 @@ local function main()
log.info("startup> running in headless mode without front panel")
end
-- check modem
if smem_dev.modem == nil then
println("startup> wireless modem not found")
log.fatal("no wireless modem on startup")
return
end
databus.tx_hw_modem(true)
-- find and setup all speakers
local speakers = ppm.get_all_devices("speaker")
for _, s in pairs(speakers) do
local sounder = rtu.init_sounder(s)
table.insert(__shared_memory.rtu_dev.sounders, sounder)
table.insert(smem_dev.sounders, sounder)
log.debug(util.c("startup> added speaker, attached as ", sounder.name))
end
databus.tx_hw_spkr_count(#__shared_memory.rtu_dev.sounders)
databus.tx_hw_spkr_count(#smem_dev.sounders)
-- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("startup> conn watchdog started")
-- setup comms
smem_sys.nic = network.nic(smem_dev.modem)
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
log.debug("startup> comms init")

View File

@ -17,7 +17,7 @@ local max_distance = nil
local comms = {}
-- protocol/data versions (protocol/data independent changes tracked by util.lua version)
comms.version = "2.5.2"
comms.version = "3.0.0"
comms.api_version = "0.0.3"
---@enum PROTOCOL
@ -240,6 +240,8 @@ function comms.scada_packet()
---@nodiscard
function public.modem_event() return self.modem_msg_in end
---@nodiscard
function public.raw_header() return { self.src_addr, self.dest_addr, self.seq_num, self.protocol } end
---@nodiscard
function public.raw_sendable() return self.raw end
---@nodiscard
@ -278,7 +280,7 @@ function comms.authd_packet()
src_addr = comms.BROADCAST,
dest_addr = comms.BROADCAST,
mac = "",
payload = ""
payload = {}
}
---@class authd_packet
@ -286,14 +288,13 @@ function comms.authd_packet()
-- make an authenticated SCADA packet
---@param s_packet scada_packet scada packet to authenticate
---@param mac function message authentication function
---@param mac function message authentication hash function
function public.make(s_packet, mac)
self.valid = true
self.src_addr = s_packet.src_addr()
self.dest_addr = s_packet.dest_addr()
self.payload = textutils.serialize(s_packet.raw_sendable(), { allow_repetitions = true, compact = true })
self.mac = mac(self.payload)
self.raw = { self.src_addr, self.dest_addr, self.mac, self.payload }
self.mac = mac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true }))
self.raw = { self.src_addr, self.dest_addr, self.mac, s_packet.raw_sendable() }
end
-- parse in a modem message as an authenticated SCADA packet
@ -330,14 +331,14 @@ function comms.authd_packet()
self.src_addr = nil
self.dest_addr = nil
self.mac = ""
self.payload = ""
self.payload = {}
end
-- check if this packet is destined for this device
local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == COMPUTER_ID)
self.valid = is_destination and type(self.src_addr) == "number" and type(self.dest_addr) == "number" and
type(self.mac) == "string" and type(self.payload) == "string"
type(self.mac) == "string" and type(self.payload) == "table"
end
end

View File

@ -52,7 +52,6 @@ local alarms = {}
-- unit alarms
alarms.HIGH_TEMP = 1150 -- temp >= 1150K
alarms.HIGH_WASTE = 0.85 -- fill > 85%
alarms.HIGH_RADIATION = 0.00005 -- 50 uSv/h, not yet damaging but this isn't good

View File

@ -80,7 +80,7 @@ end
---@param modem table modem to use
function network.nic(modem)
local self = {
connected = true, -- used to avoid costly MAC calculations if modem isn't even present
connected = true, -- used to avoid costly MAC calculations if modem isn't even present
channels = {}
}
@ -114,7 +114,7 @@ function network.nic(modem)
modem.open(channel)
end
-- link all public functions except for transmit
-- link all public functions except for transmit, open, and close
for key, func in pairs(modem) do
if key ~= "transmit" and key ~= "open" and key ~= "close" and key ~= "closeAll" then public[key] = func end
end
@ -175,7 +175,7 @@ function network.nic(modem)
---@param packet scada_packet packet
function public.transmit(dest_channel, local_channel, packet)
if self.connected then
local tx_packet = packet ---@type authd_packet|scada_packet
local tx_packet = packet ---@type authd_packet|scada_packet
if c_eng.hmac ~= nil then
-- local start = util.time_ms()
@ -184,7 +184,7 @@ function network.nic(modem)
---@cast tx_packet authd_packet
tx_packet.make(packet, compute_hmac)
-- log.debug("crypto.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms")
-- log.debug("network.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms")
end
modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable())
@ -211,17 +211,18 @@ function network.nic(modem)
a_packet.receive(side, sender, reply_to, message, distance)
if a_packet.is_valid() then
-- local start = util.time_ms()
local packet_hmac = a_packet.mac()
local msg = a_packet.data()
local computed_hmac = compute_hmac(msg)
s_packet.receive(side, sender, reply_to, a_packet.data(), distance)
if packet_hmac == computed_hmac then
-- log.debug("crypto.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms")
s_packet.receive(side, sender, reply_to, textutils.unserialize(msg), distance)
s_packet.stamp_authenticated()
else
-- log.debug("crypto.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
if s_packet.is_valid() then
-- local start = util.time_ms()
local computed_hmac = compute_hmac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true }))
if a_packet.mac() == computed_hmac then
-- log.debug("network.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms")
s_packet.stamp_authenticated()
else
-- log.debug("network.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
end
end
end
else

View File

@ -1,35 +1,31 @@
local util = require("scada-common.util")
local BOOTLOADER_VERSION = "1.1"
local println = util.println
local BOOTLOADER_VERSION = "1.0"
println("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION)
println("BOOT> SCANNING FOR APPLICATIONS...")
print("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION)
print("BOOT> SCANNING FOR APPLICATIONS...")
local exit_code
if fs.exists("reactor-plc/startup.lua") then
println("BOOT> EXEC REACTOR PLC STARTUP")
print("BOOT> EXEC REACTOR PLC STARTUP")
exit_code = shell.execute("reactor-plc/startup")
elseif fs.exists("rtu/startup.lua") then
println("BOOT> EXEC RTU STARTUP")
print("BOOT> EXEC RTU STARTUP")
exit_code = shell.execute("rtu/startup")
elseif fs.exists("supervisor/startup.lua") then
println("BOOT> EXEC SUPERVISOR STARTUP")
print("BOOT> EXEC SUPERVISOR STARTUP")
exit_code = shell.execute("supervisor/startup")
elseif fs.exists("coordinator/startup.lua") then
println("BOOT> EXEC COORDINATOR STARTUP")
print("BOOT> EXEC COORDINATOR STARTUP")
exit_code = shell.execute("coordinator/startup")
elseif fs.exists("pocket/startup.lua") then
println("BOOT> EXEC POCKET STARTUP")
print("BOOT> EXEC POCKET STARTUP")
exit_code = shell.execute("pocket/startup")
else
println("BOOT> NO SCADA STARTUP FOUND")
println("BOOT> EXIT")
print("BOOT> NO SCADA STARTUP FOUND")
print("BOOT> EXIT")
return false
end
if not exit_code then println("BOOT> APPLICATION CRASHED") end
if not exit_code then print("BOOT> APPLICATION CRASHED") end
return exit_code

View File

@ -43,12 +43,13 @@ local PERIODICS = {
---@nodiscard
---@param id integer session ID
---@param s_addr integer device source address
---@param i_seq_num integer initial sequence number
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
---@param facility facility facility data table
---@param fp_ok boolean if the front panel UI is running
function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facility, fp_ok)
function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, facility, fp_ok)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end
@ -57,8 +58,8 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
local self = {
units = facility.get_units(),
-- connection properties
seq_num = 0,
r_seq_num = nil,
seq_num = i_seq_num + 2, -- next after the establish approval was sent
r_seq_num = i_seq_num + 1,
connected = true,
conn_watchdog = util.new_watchdog(timeout),
establish_time = util.time_s(),
@ -182,13 +183,11 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
---@param pkt mgmt_frame|crdn_frame
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return
else
self.r_seq_num = pkt.scada_frame.seq_num()
self.r_seq_num = pkt.scada_frame.seq_num() + 1
end
-- feed watchdog

View File

@ -48,12 +48,13 @@ local PERIODICS = {
---@nodiscard
---@param id integer session ID
---@param s_addr integer device source address
---@param i_seq_num integer initial sequence number
---@param reactor_id integer reactor ID
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
---@param fp_ok boolean if the front panel UI is running
function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, fp_ok)
function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue, timeout, fp_ok)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end
@ -66,8 +67,8 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
ramping_rate = false,
auto_lock = false,
-- connection properties
seq_num = 0,
r_seq_num = nil,
seq_num = i_seq_num + 2, -- next after the establish approval was sent
r_seq_num = i_seq_num + 1,
connected = true,
received_struct = false,
received_status_cache = false,
@ -190,71 +191,111 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
-- copy in the RPS status
---@param rps_status table
local function _copy_rps_status(rps_status)
self.sDB.rps_tripped = rps_status[1]
self.sDB.rps_trip_cause = rps_status[2]
self.sDB.rps_status.high_dmg = rps_status[3]
self.sDB.rps_status.high_temp = rps_status[4]
self.sDB.rps_status.low_cool = rps_status[5]
self.sDB.rps_status.ex_waste = rps_status[6]
self.sDB.rps_status.ex_hcool = rps_status[7]
self.sDB.rps_status.no_fuel = rps_status[8]
self.sDB.rps_status.fault = rps_status[9]
self.sDB.rps_status.timeout = rps_status[10]
self.sDB.rps_status.manual = rps_status[11]
self.sDB.rps_status.automatic = rps_status[12]
self.sDB.rps_status.sys_fail = rps_status[13]
self.sDB.rps_status.force_dis = rps_status[14]
local rps = self.sDB.rps_status
self.sDB.rps_tripped = rps_status[1]
self.sDB.rps_trip_cause = rps_status[2]
rps.high_dmg = rps_status[3]
rps.high_temp = rps_status[4]
rps.low_cool = rps_status[5]
rps.ex_waste = rps_status[6]
rps.ex_hcool = rps_status[7]
rps.no_fuel = rps_status[8]
rps.fault = rps_status[9]
rps.timeout = rps_status[10]
rps.manual = rps_status[11]
rps.automatic = rps_status[12]
rps.sys_fail = rps_status[13]
rps.force_dis = rps_status[14]
end
-- copy in the reactor status
---@param mek_data table
local function _copy_status(mek_data)
local stat = self.sDB.mek_status
local struct = self.sDB.mek_struct
-- copy status information
self.sDB.mek_status.status = mek_data[1]
self.sDB.mek_status.burn_rate = mek_data[2]
self.sDB.mek_status.act_burn_rate = mek_data[3]
self.sDB.mek_status.temp = mek_data[4]
self.sDB.mek_status.damage = mek_data[5]
self.sDB.mek_status.boil_eff = mek_data[6]
self.sDB.mek_status.env_loss = mek_data[7]
stat.status = mek_data[1]
stat.burn_rate = mek_data[2]
stat.act_burn_rate = mek_data[3]
stat.temp = mek_data[4]
stat.damage = mek_data[5]
stat.boil_eff = mek_data[6]
stat.env_loss = mek_data[7]
-- copy container information
self.sDB.mek_status.fuel = mek_data[8]
self.sDB.mek_status.fuel_fill = mek_data[9]
self.sDB.mek_status.waste = mek_data[10]
self.sDB.mek_status.waste_fill = mek_data[11]
self.sDB.mek_status.ccool_type = mek_data[12]
self.sDB.mek_status.ccool_amnt = mek_data[13]
self.sDB.mek_status.ccool_fill = mek_data[14]
self.sDB.mek_status.hcool_type = mek_data[15]
self.sDB.mek_status.hcool_amnt = mek_data[16]
self.sDB.mek_status.hcool_fill = mek_data[17]
stat.fuel = mek_data[8]
stat.fuel_fill = mek_data[9]
stat.waste = mek_data[10]
stat.waste_fill = mek_data[11]
stat.ccool_type = mek_data[12]
stat.ccool_amnt = mek_data[13]
stat.ccool_fill = mek_data[14]
stat.hcool_type = mek_data[15]
stat.hcool_amnt = mek_data[16]
stat.hcool_fill = mek_data[17]
-- update computable fields if we have our structure
if self.received_struct then
self.sDB.mek_status.fuel_need = self.sDB.mek_struct.fuel_cap - self.sDB.mek_status.fuel_fill
self.sDB.mek_status.waste_need = self.sDB.mek_struct.waste_cap - self.sDB.mek_status.waste_fill
self.sDB.mek_status.cool_need = self.sDB.mek_struct.ccool_cap - self.sDB.mek_status.ccool_fill
self.sDB.mek_status.hcool_need = self.sDB.mek_struct.hcool_cap - self.sDB.mek_status.hcool_fill
stat.fuel_need = struct.fuel_cap - stat.fuel_fill
stat.waste_need = struct.waste_cap - stat.waste_fill
stat.cool_need = struct.ccool_cap - stat.ccool_fill
stat.hcool_need = struct.hcool_cap - stat.hcool_fill
end
end
-- copy in the reactor structure
---@param mek_data table
local function _copy_struct(mek_data)
self.sDB.mek_struct.length = mek_data[1]
self.sDB.mek_struct.width = mek_data[2]
self.sDB.mek_struct.height = mek_data[3]
self.sDB.mek_struct.min_pos = mek_data[4]
self.sDB.mek_struct.max_pos = mek_data[5]
self.sDB.mek_struct.heat_cap = mek_data[6]
self.sDB.mek_struct.fuel_asm = mek_data[7]
self.sDB.mek_struct.fuel_sa = mek_data[8]
self.sDB.mek_struct.fuel_cap = mek_data[9]
self.sDB.mek_struct.waste_cap = mek_data[10]
self.sDB.mek_struct.ccool_cap = mek_data[11]
self.sDB.mek_struct.hcool_cap = mek_data[12]
self.sDB.mek_struct.max_burn = mek_data[13]
local struct = self.sDB.mek_struct
struct.length = mek_data[1]
struct.width = mek_data[2]
struct.height = mek_data[3]
struct.min_pos = mek_data[4]
struct.max_pos = mek_data[5]
struct.heat_cap = mek_data[6]
struct.fuel_asm = mek_data[7]
struct.fuel_sa = mek_data[8]
struct.fuel_cap = mek_data[9]
struct.waste_cap = mek_data[10]
struct.ccool_cap = mek_data[11]
struct.hcool_cap = mek_data[12]
struct.max_burn = mek_data[13]
end
-- handle a reactor status packet
---@param pkt rplc_frame
local function _handle_status(pkt)
local valid = (type(pkt.data[1]) == "number") and (type(pkt.data[2]) == "boolean") and
(type(pkt.data[3]) == "boolean") and (type(pkt.data[4]) == "boolean") and
(type(pkt.data[5]) == "number")
if valid then
self.sDB.last_status_update = pkt.data[1]
self.sDB.control_state = pkt.data[2]
self.sDB.no_reactor = pkt.data[3]
self.sDB.formed = pkt.data[4]
self.sDB.auto_ack_token = pkt.data[5]
if (not self.sDB.no_reactor) and self.sDB.formed and (type(pkt.data[6]) == "number") then
self.sDB.mek_status.heating_rate = pkt.data[6] or 0.0
-- attempt to read mek_data table
if type(pkt.data[7]) == "table" then
if #pkt.data[7] == 17 then
_copy_status(pkt.data[7])
self.received_status_cache = true
else
log.error(log_header .. "RPLC status packet reactor data length mismatch")
end
end
end
else
log.debug(log_header .. "RPLC status packet invalid")
end
end
-- mark this PLC session as closed, stop watchdog
@ -309,13 +350,11 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
---@param pkt mgmt_frame|rplc_frame
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return
else
self.r_seq_num = pkt.scada_frame.seq_num()
self.r_seq_num = pkt.scada_frame.seq_num() + 1
end
-- process packet
@ -334,48 +373,17 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
if pkt.type == RPLC_TYPE.STATUS then
-- status packet received, update data
if pkt.length >= 5 then
if (type(pkt.data[1]) == "number") and (type(pkt.data[2]) == "boolean") and (type(pkt.data[3]) == "boolean") and
(type(pkt.data[4]) == "boolean") and (type(pkt.data[5]) == "number") then
self.sDB.last_status_update = pkt.data[1]
self.sDB.control_state = pkt.data[2]
self.sDB.no_reactor = pkt.data[3]
self.sDB.formed = pkt.data[4]
self.sDB.auto_ack_token = pkt.data[5]
if (not self.sDB.no_reactor) and self.sDB.formed and (type(pkt.data[6]) == "number") then
self.sDB.mek_status.heating_rate = pkt.data[6] or 0.0
-- attempt to read mek_data table
if type(pkt.data[7]) == "table" then
local status = pcall(_copy_status, pkt.data[7])
if status then
-- copied in status data OK
self.received_status_cache = true
else
-- error copying status data
log.error(log_header .. "failed to parse status packet data")
end
end
end
else
log.debug(log_header .. "RPLC status packet invalid")
end
_handle_status(pkt)
else
log.debug(log_header .. "RPLC status packet length mismatch")
end
elseif pkt.type == RPLC_TYPE.MEK_STRUCT then
-- received reactor structure, record it
if pkt.length == 14 then
local status = pcall(_copy_struct, pkt.data)
if status then
-- copied in structure data OK
_compute_op_temps()
self.received_struct = true
out_queue.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, reactor_id)
else
-- error copying structure data
log.error(log_header .. "failed to parse struct packet data")
end
if pkt.length == 13 then
_copy_struct(pkt.data)
_compute_op_temps()
self.received_struct = true
out_queue.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, reactor_id)
else
log.debug(log_header .. "RPLC struct packet length mismatch")
end

View File

@ -30,12 +30,13 @@ local PERIODICS = {
---@nodiscard
---@param id integer session ID
---@param s_addr integer device source address
---@param i_seq_num integer initial sequence number
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
---@param facility facility facility data table
---@param fp_ok boolean if the front panel UI is running
function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility, fp_ok)
function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, facility, fp_ok)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end
@ -43,8 +44,8 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility,
local self = {
-- connection properties
seq_num = 0,
r_seq_num = nil,
seq_num = i_seq_num + 2, -- next after the establish approval was sent
r_seq_num = i_seq_num + 1,
connected = true,
conn_watchdog = util.new_watchdog(timeout),
last_rtt = 0,
@ -93,13 +94,11 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility,
---@param pkt mgmt_frame
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return
else
self.r_seq_num = pkt.scada_frame.seq_num()
self.r_seq_num = pkt.scada_frame.seq_num() + 1
end
-- feed watchdog

View File

@ -34,13 +34,14 @@ local PERIODICS = {
---@nodiscard
---@param id integer session ID
---@param s_addr integer device source address
---@param i_seq_num integer initial sequence number
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
---@param advertisement table RTU device advertisement
---@param facility facility facility data table
---@param fp_ok boolean if the front panel UI is running
function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement, facility, fp_ok)
function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, advertisement, facility, fp_ok)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end
@ -51,8 +52,8 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
advert = advertisement,
fac_units = facility.get_units(),
-- connection properties
seq_num = 0,
r_seq_num = nil,
seq_num = i_seq_num + 2, -- next after the establish approval was sent
r_seq_num = i_seq_num + 1,
connected = true,
conn_watchdog = util.new_watchdog(timeout),
last_rtt = 0,
@ -240,13 +241,11 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
---@param pkt modbus_frame|mgmt_frame
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return
else
self.r_seq_num = pkt.scada_frame.seq_num()
self.r_seq_num = pkt.scada_frame.seq_num() + 1
end
-- feed watchdog

View File

@ -273,11 +273,12 @@ end
-- establish a new PLC session
---@nodiscard
---@param source_addr integer
---@param for_reactor integer
---@param version string
---@param source_addr integer PLC computer ID
---@param i_seq_num integer initial (most recent) sequence number
---@param for_reactor integer unit ID
---@param version string PLC version
---@return integer|false session_id
function svsessions.establish_plc_session(source_addr, for_reactor, version)
function svsessions.establish_plc_session(source_addr, i_seq_num, for_reactor, version)
if svsessions.get_reactor_session(for_reactor) == nil and for_reactor >= 1 and for_reactor <= self.config.UnitCount then
---@class plc_session_struct
local plc_s = {
@ -294,7 +295,7 @@ function svsessions.establish_plc_session(source_addr, for_reactor, version)
local id = self.next_ids.plc
plc_s.instance = plc.new_session(id, source_addr, for_reactor, plc_s.in_queue, plc_s.out_queue, self.config.PLC_Timeout, self.fp_ok)
plc_s.instance = plc.new_session(id, source_addr, i_seq_num, for_reactor, plc_s.in_queue, plc_s.out_queue, self.config.PLC_Timeout, self.fp_ok)
table.insert(self.sessions.plc, plc_s)
local units = self.facility.get_units()
@ -320,13 +321,14 @@ function svsessions.establish_plc_session(source_addr, for_reactor, version)
end
end
-- establish a new RTU session
-- establish a new RTU gateway session
---@nodiscard
---@param source_addr integer
---@param advertisement table
---@param version string
---@param source_addr integer RTU gateway computer ID
---@param i_seq_num integer initial (most recent) sequence number
---@param advertisement table RTU capability advertisement
---@param version string RTU gateway version
---@return integer session_id
function svsessions.establish_rtu_session(source_addr, advertisement, version)
function svsessions.establish_rtu_session(source_addr, i_seq_num, advertisement, version)
---@class rtu_session_struct
local rtu_s = {
s_type = "rtu",
@ -341,7 +343,7 @@ function svsessions.establish_rtu_session(source_addr, advertisement, version)
local id = self.next_ids.rtu
rtu_s.instance = rtu.new_session(id, source_addr, rtu_s.in_queue, rtu_s.out_queue, self.config.RTU_Timeout, advertisement, self.facility, self.fp_ok)
rtu_s.instance = rtu.new_session(id, source_addr, i_seq_num, rtu_s.in_queue, rtu_s.out_queue, self.config.RTU_Timeout, advertisement, self.facility, self.fp_ok)
table.insert(self.sessions.rtu, rtu_s)
local mt = {
@ -362,10 +364,11 @@ end
-- establish a new coordinator session
---@nodiscard
---@param source_addr integer
---@param version string
---@param source_addr integer coordinator computer ID
---@param i_seq_num integer initial (most recent) sequence number
---@param version string coordinator version
---@return integer|false session_id
function svsessions.establish_crd_session(source_addr, version)
function svsessions.establish_crd_session(source_addr, i_seq_num, version)
if svsessions.get_crd_session() == nil then
---@class crd_session_struct
local crd_s = {
@ -381,7 +384,7 @@ function svsessions.establish_crd_session(source_addr, version)
local id = self.next_ids.crd
crd_s.instance = coordinator.new_session(id, source_addr, crd_s.in_queue, crd_s.out_queue, self.config.CRD_Timeout, self.facility, self.fp_ok)
crd_s.instance = coordinator.new_session(id, source_addr, i_seq_num, crd_s.in_queue, crd_s.out_queue, self.config.CRD_Timeout, self.facility, self.fp_ok)
table.insert(self.sessions.crd, crd_s)
local mt = {
@ -406,10 +409,11 @@ end
-- establish a new pocket diagnostics session
---@nodiscard
---@param source_addr integer
---@param version string
---@param source_addr integer pocket computer ID
---@param i_seq_num integer initial (most recent) sequence number
---@param version string pocket version
---@return integer|false session_id
function svsessions.establish_pdg_session(source_addr, version)
function svsessions.establish_pdg_session(source_addr, i_seq_num, version)
---@class pdg_session_struct
local pdg_s = {
s_type = "pkt",
@ -424,7 +428,7 @@ function svsessions.establish_pdg_session(source_addr, version)
local id = self.next_ids.pdg
pdg_s.instance = pocket.new_session(id, source_addr, pdg_s.in_queue, pdg_s.out_queue, self.config.PKT_Timeout, self.facility, self.fp_ok)
pdg_s.instance = pocket.new_session(id, source_addr, i_seq_num, pdg_s.in_queue, pdg_s.out_queue, self.config.PKT_Timeout, self.facility, self.fp_ok)
table.insert(self.sessions.pdg, pdg_s)
local mt = {

View File

@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v1.3.12"
local SUPERVISOR_VERSION = "v1.4.0"
local println = util.println
local println_ts = util.println_ts
@ -214,7 +214,7 @@ local function main()
elseif event == "modem_message" then
-- got a packet
local packet = superv_comms.parse_packet(param1, param2, param3, param4, param5)
superv_comms.handle_packet(packet)
if packet then superv_comms.handle_packet(packet) end
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
event == "double_click" then
-- handle a mouse event

View File

@ -191,283 +191,282 @@ function supervisor.comms(_version, nic, fp_ok)
end
-- handle a packet
---@param packet modbus_frame|rplc_frame|mgmt_frame|crdn_frame|nil
---@param packet modbus_frame|rplc_frame|mgmt_frame|crdn_frame
function public.handle_packet(packet)
if packet ~= nil then
local l_chan = packet.scada_frame.local_channel()
local r_chan = packet.scada_frame.remote_channel()
local src_addr = packet.scada_frame.src_addr()
local protocol = packet.scada_frame.protocol()
local l_chan = packet.scada_frame.local_channel()
local r_chan = packet.scada_frame.remote_channel()
local src_addr = packet.scada_frame.src_addr()
local protocol = packet.scada_frame.protocol()
local i_seq_num = packet.scada_frame.seq_num()
if l_chan ~= config.SVR_Channel then
log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == config.PLC_Channel then
-- look for an associated session
local session = svsessions.find_plc_session(src_addr)
if l_chan ~= config.SVR_Channel then
log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == config.PLC_Channel then
-- look for an associated session
local session = svsessions.find_plc_session(src_addr)
if protocol == PROTOCOL.RPLC then
---@cast packet rplc_frame
-- reactor PLC packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug("discarding RPLC packet without a known session")
end
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping PLC establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.PLC then
-- PLC linking request
if packet.length == 4 and type(packet.data[4]) == "number" then
local reactor_id = packet.data[4]
local plc_id = svsessions.establish_plc_session(src_addr, reactor_id, firmware_v)
if plc_id == false then
-- reactor already has a PLC assigned
if last_ack ~= ESTABLISH_ACK.COLLISION then
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION)
else
-- got an ID; assigned to a reactor successfully
println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \xbb reactor ", reactor_id, " connected"))
log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [@", src_addr, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
end
else
log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on PLC channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug("invalid establish packet (on PLC channel)")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding PLC SCADA_MGMT packet without a known session from computer ", src_addr))
end
if protocol == PROTOCOL.RPLC then
---@cast packet rplc_frame
-- reactor PLC packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
log.debug(util.c("illegal packet type ", protocol, " on PLC channel"))
-- any other packet should be session related, discard it
log.debug("discarding RPLC packet without a known session")
end
elseif r_chan == config.RTU_Channel then
-- look for an associated session
local session = svsessions.find_rtu_session(src_addr)
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
if protocol == PROTOCOL.MODBUS_TCP then
---@cast packet modbus_frame
-- MODBUS response
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug("discarding MODBUS_TCP packet without a known session")
end
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping RTU establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.RTU then
if packet.length == 4 then
-- this is an RTU advertisement for a new session
local rtu_advert = packet.data[4]
local s_id = svsessions.establish_rtu_session(src_addr, rtu_advert, firmware_v)
println(util.c("RTU (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("RTU_ESTABLISH: RTU (",firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
else
log.debug("RTU_ESTABLISH: packet length mismatch")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on RTU channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping PLC establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
else
log.debug("invalid establish packet (on RTU channel)")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding RTU SCADA_MGMT packet without a known session from computer ", src_addr))
end
else
log.debug(util.c("illegal packet type ", protocol, " on RTU channel"))
end
elseif r_chan == config.CRD_Channel then
-- look for an associated session
local session = svsessions.find_crd_session(src_addr)
if protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.PLC then
-- PLC linking request
if packet.length == 4 and type(packet.data[4]) == "number" then
local reactor_id = packet.data[4]
local plc_id = svsessions.establish_plc_session(src_addr, i_seq_num, reactor_id, firmware_v)
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping coordinator establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.CRD then
-- this is an attempt to establish a new coordinator session
local s_id = svsessions.establish_crd_session(src_addr, firmware_v)
if s_id ~= false then
println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("CRD_ESTABLISH: coordinator (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { config.UnitCount, cooling_conf })
else
if plc_id == false then
-- reactor already has a PLC assigned
if last_ack ~= ESTABLISH_ACK.COLLISION then
log.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator")
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION)
else
-- got an ID; assigned to a reactor successfully
println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \xbb reactor ", reactor_id, " connected"))
log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [@", src_addr, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
end
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on coordinator channel"))
log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug("CRD_ESTABLISH: establish packet length mismatch")
log.debug(util.c("illegal establish packet for device ", dev_type, " on PLC channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding coordinator SCADA_MGMT packet without a known session from computer ", src_addr))
end
elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
-- coordinator packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding coordinator SCADA_CRDN packet without a known session from computer ", src_addr))
log.debug("invalid establish packet (on PLC channel)")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug(util.c("illegal packet type ", protocol, " on coordinator channel"))
end
elseif r_chan == config.PKT_Channel then
-- look for an associated session
local session = svsessions.find_pdg_session(src_addr)
if protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping PDG establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.PKT then
-- this is an attempt to establish a new pocket diagnostic session
local s_id = svsessions.establish_pdg_session(src_addr, firmware_v)
println(util.c("PKT (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("PDG_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on pocket channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug("PDG_ESTABLISH: establish packet length mismatch")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding pocket SCADA_MGMT packet without a known session from computer ", src_addr))
end
elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
-- coordinator packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding pocket SCADA_CRDN packet without a known session from computer ", src_addr))
end
else
log.debug(util.c("illegal packet type ", protocol, " on pocket channel"))
-- any other packet should be session related, discard it
log.debug(util.c("discarding PLC SCADA_MGMT packet without a known session from computer ", src_addr))
end
else
log.debug("received packet for unknown channel " .. r_chan, true)
log.debug(util.c("illegal packet type ", protocol, " on PLC channel"))
end
elseif r_chan == config.RTU_Channel then
-- look for an associated session
local session = svsessions.find_rtu_session(src_addr)
if protocol == PROTOCOL.MODBUS_TCP then
---@cast packet modbus_frame
-- MODBUS response
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug("discarding MODBUS_TCP packet without a known session")
end
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping RTU establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.RTU then
if packet.length == 4 then
-- this is an RTU advertisement for a new session
local rtu_advert = packet.data[4]
local s_id = svsessions.establish_rtu_session(src_addr, i_seq_num, rtu_advert, firmware_v)
println(util.c("RTU (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("RTU_ESTABLISH: RTU (",firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
else
log.debug("RTU_ESTABLISH: packet length mismatch")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on RTU channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug("invalid establish packet (on RTU channel)")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding RTU SCADA_MGMT packet without a known session from computer ", src_addr))
end
else
log.debug(util.c("illegal packet type ", protocol, " on RTU channel"))
end
elseif r_chan == config.CRD_Channel then
-- look for an associated session
local session = svsessions.find_crd_session(src_addr)
if protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping coordinator establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.CRD then
-- this is an attempt to establish a new coordinator session
local s_id = svsessions.establish_crd_session(src_addr, i_seq_num, firmware_v)
if s_id ~= false then
println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("CRD_ESTABLISH: coordinator (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { config.UnitCount, cooling_conf })
else
if last_ack ~= ESTABLISH_ACK.COLLISION then
log.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator")
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION)
end
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on coordinator channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug("CRD_ESTABLISH: establish packet length mismatch")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding coordinator SCADA_MGMT packet without a known session from computer ", src_addr))
end
elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
-- coordinator packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding coordinator SCADA_CRDN packet without a known session from computer ", src_addr))
end
else
log.debug(util.c("illegal packet type ", protocol, " on coordinator channel"))
end
elseif r_chan == config.PKT_Channel then
-- look for an associated session
local session = svsessions.find_pdg_session(src_addr)
if protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping PDG establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.PKT then
-- this is an attempt to establish a new pocket diagnostic session
local s_id = svsessions.establish_pdg_session(src_addr, i_seq_num, firmware_v)
println(util.c("PKT (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("PDG_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on pocket channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug("PDG_ESTABLISH: establish packet length mismatch")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding pocket SCADA_MGMT packet without a known session from computer ", src_addr))
end
elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
-- coordinator packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding pocket SCADA_CRDN packet without a known session from computer ", src_addr))
end
else
log.debug(util.c("illegal packet type ", protocol, " on pocket channel"))
end
else
log.debug("received packet for unknown channel " .. r_chan, true)
end
end