Merge pull request #254 from MikaylaFischler/devel

2023.06.07 Release
This commit is contained in:
Mikayla 2023-06-07 17:46:50 -04:00 committed by GitHub
commit 193aeed6df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
90 changed files with 2221 additions and 838 deletions

95
.github/workflows/manifest.yml vendored Normal file
View File

@ -0,0 +1,95 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy Installation Manifests and Versions
on:
workflow_dispatch:
push:
branches:
- main
- latest
- devel
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Setup Python
uses: actions/setup-python@v3.1.3
# Generate manifest + shields files for main branch
- name: Checkout main
id: checkout-main
uses: actions/checkout@v3
with:
ref: 'main'
clean: false
- name: Create outputs folders
if: success() || failure()
shell: bash
run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/latest deploy/manifests/devel
- name: Generate manifest and shields for main branch
id: manifest-main
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
run: python imgen.py shields
- name: Save main's manifest
if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/main
# Generate manifest for latest branch
- name: Checkout latest
id: checkout-latest
if: success() || failure()
uses: actions/checkout@v3
with:
ref: 'latest'
clean: false
- name: Generate manifest for latest
id: manifest-latest
if: ${{ (success() || failure()) && steps.checkout-latest.outcome == 'success' }}
run: python imgen.py
- name: Save latest's manifest
if: ${{ (success() || failure()) && steps.manifest-latest.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/latest
# Generate manifest for devel branch
- name: Checkout devel
id: checkout-devel
if: success() || failure()
uses: actions/checkout@v3
with:
ref: 'devel'
clean: false
- name: Generate manifest for devel
id: manifest-devel
if: ${{ (success() || failure()) && steps.checkout-devel.outcome == 'success' }}
run: python imgen.py
- name: Save devel's manifest
if: ${{ (success() || failure()) && steps.manifest-devel.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/devel
# All artifacts ready now, upload deploy directory
- name: Upload artifacts
id: upload-artifacts
if: ${{ (success() || failure()) && (steps.manifest-main.outcome == 'success' || steps.manifest-latest.outcome == 'success' || steps.manifest-devel.outcome == 'success') }}
uses: actions/upload-pages-artifact@v1
with:
# Upload manifest JSON
path: 'deploy/'
- name: Deploy to GitHub Pages
if: ${{ (success() || failure()) && steps.upload-artifacts.outcome == 'success' }}
id: deployment
uses: actions/deploy-pages@v2

View File

@ -1,47 +0,0 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy Component Versions
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Setup Python
uses: actions/setup-python@v3.1.3
- run: mkdir shields
- run: python imgen.py shields
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
# Upload shields JSON
path: 'shields/'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2

View File

@ -20,9 +20,10 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
local function println(message) print(tostring(message)) end local function println(message) print(tostring(message)) end
local function print(message) term.write(tostring(message)) end local function print(message) term.write(tostring(message)) end
local CCMSI_VERSION = "v1.0" local CCMSI_VERSION = "v1.2"
local install_dir = "/.install-cache" local install_dir = "/.install-cache"
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/" local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/"
local opts = { ... } local opts = { ... }
@ -122,8 +123,8 @@ if mode == "check" then
-- GET REMOTE MANIFEST -- -- GET REMOTE MANIFEST --
------------------------- -------------------------
if opts[2] then repo_path = repo_path .. opts[2] .. "/" else repo_path = repo_path .. "main/" end if opts[2] then manifest_path = manifest_path .. opts[2] .. "/" else manifest_path = manifest_path .. "main/" end
local install_manifest = repo_path .. "install_manifest.json" local install_manifest = manifest_path .. "install_manifest.json"
local response, error = http.get(install_manifest) local response, error = http.get(install_manifest)
@ -203,7 +204,8 @@ elseif mode == "install" or mode == "update" then
------------------------- -------------------------
if opts[3] then repo_path = repo_path .. opts[3] .. "/" else repo_path = repo_path .. "main/" end if opts[3] then repo_path = repo_path .. opts[3] .. "/" else repo_path = repo_path .. "main/" end
local install_manifest = repo_path .. "install_manifest.json" if opts[3] then manifest_path = manifest_path .. opts[3] .. "/" else manifest_path = manifest_path .. "main/" end
local install_manifest = manifest_path .. "install_manifest.json"
local response, error = http.get(install_manifest) local response, error = http.get(install_manifest)

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
@ -335,12 +338,13 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
tick_dmesg_waiting(math.max(0, timeout_s - (util.time_s() - start))) tick_dmesg_waiting(math.max(0, timeout_s - (util.time_s() - start)))
_send_establish() _send_establish()
clock.start() clock.start()
elseif event == "timer" then
-- keep checking watchdog timers
apisessions.check_all_watchdogs(p1)
elseif event == "modem_message" then elseif event == "modem_message" then
-- handle message -- handle message
local packet = public.parse_packet(p1, p2, p3, p4, p5) local packet = public.parse_packet(p1, p2, p3, p4, p5)
if packet ~= nil and packet.type == SCADA_MGMT_TYPE.ESTABLISH then public.handle_packet(packet)
public.handle_packet(packet)
end
elseif event == "terminate" then elseif event == "terminate" then
terminated = true terminated = true
break break
@ -435,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
@ -456,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
@ -464,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]
@ -473,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
@ -659,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
@ -704,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)
@ -718,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
@ -731,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
@ -117,8 +118,6 @@ function api.new_session(id, in_queue, out_queue, timeout)
-- process packet -- process packet
if pkt.scada_frame.protocol() == PROTOCOL.COORD_API then if pkt.scada_frame.protocol() == PROTOCOL.COORD_API then
---@cast pkt capi_frame ---@cast pkt capi_frame
-- feed watchdog
self.conn_watchdog.feed()
-- handle packet by type -- handle packet by type
if pkt.type == nil then if pkt.type == nil then
@ -136,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
@ -173,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
@ -212,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
@ -248,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

@ -4,23 +4,23 @@
require("/initenv").init_env() require("/initenv").init_env()
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
local log = require("scada-common.log") local log = require("scada-common.log")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local tcallbackdsp = require("scada-common.tcallbackdsp") local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local config = require("coordinator.config") local config = require("coordinator.config")
local coordinator = require("coordinator.coordinator") local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local renderer = require("coordinator.renderer") local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder") local sounder = require("coordinator.sounder")
local apisessions = require("coordinator.session.apisessions") local apisessions = require("coordinator.session.apisessions")
local COORDINATOR_VERSION = "v0.15.2" 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
@ -334,7 +334,7 @@ local function main()
apisessions.check_all_watchdogs(param1) apisessions.check_all_watchdogs(param1)
-- notify timer callback dispatcher -- notify timer callback dispatcher
tcallbackdsp.handle(param1) tcd.handle(param1)
end end
elseif event == "modem_message" then elseif event == "modem_message" then
-- got a packet -- got a packet

View File

@ -1,4 +1,4 @@
local tcd = require("scada-common.tcallbackdsp") local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
@ -33,7 +33,7 @@ local period = core.flasher.PERIOD
---@param x integer top left x ---@param x integer top left x
---@param y integer top left y ---@param y integer top left y
local function new_view(root, x, y) local function new_view(root, x, y)
assert(root.height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)") assert(root.get_height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
local facility = iocontrol.get_db().facility local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units local units = iocontrol.get_db().units

View File

@ -38,7 +38,7 @@ local function make(parent, x, y, unit)
height = 17 height = 17
end end
assert(parent.height() >= (y + height), "main display not of sufficient vertical resolution (add an additional row of monitors)") assert(parent.get_height() >= (y + height), "main display not of sufficient vertical resolution (add an additional row of monitors)")
-- bounding box div -- bounding box div
local root = Div{parent=parent,x=x,y=y,width=80,height=height} local root = Div{parent=parent,x=x,y=y,width=80,height=height}

View File

@ -32,7 +32,7 @@ local function init(main)
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=cpair(colors.lightGray, colors.white),width=12,fg_bg=style.header} local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=cpair(colors.lightGray, colors.white),width=12,fg_bg=style.header}
-- max length example: "01:23:45 AM - Wednesday, September 28 2022" -- max length example: "01:23:45 AM - Wednesday, September 28 2022"
local datetime = TextBox{parent=main,x=(header.width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header} local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
ping.register(facility.ps, "sv_ping", ping.update) ping.register(facility.ps, "sv_ping", ping.update)
datetime.register(facility.ps, "date_time", datetime.set_value) datetime.register(facility.ps, "date_time", datetime.set_value)
@ -45,12 +45,12 @@ local function init(main)
-- unit overviews -- unit overviews
if facility.num_units >= 1 then if facility.num_units >= 1 then
uo_1 = unit_overview(main, 2, 3, units[1]) uo_1 = unit_overview(main, 2, 3, units[1])
row_1_height = uo_1.height() row_1_height = uo_1.get_height()
end end
if facility.num_units >= 2 then if facility.num_units >= 2 then
uo_2 = unit_overview(main, 84, 3, units[2]) uo_2 = unit_overview(main, 84, 3, units[2])
row_1_height = math.max(row_1_height, uo_2.height()) row_1_height = math.max(row_1_height, uo_2.get_height())
end end
cnc_y_start = cnc_y_start + row_1_height + 1 cnc_y_start = cnc_y_start + row_1_height + 1
@ -60,11 +60,11 @@ local function init(main)
local row_2_offset = cnc_y_start local row_2_offset = cnc_y_start
uo_3 = unit_overview(main, 2, row_2_offset, units[3]) uo_3 = unit_overview(main, 2, row_2_offset, units[3])
cnc_y_start = row_2_offset + uo_3.height() + 1 cnc_y_start = row_2_offset + uo_3.get_height() + 1
if facility.num_units == 4 then if facility.num_units == 4 then
uo_4 = unit_overview(main, 84, row_2_offset, units[4]) uo_4 = unit_overview(main, 84, row_2_offset, units[4])
cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.height() + 1) cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.get_height() + 1)
end end
end end
@ -73,11 +73,11 @@ local function init(main)
cnc_y_start = cnc_y_start cnc_y_start = cnc_y_start
-- induction matrix and process control interfaces are 24 tall + space needed for divider -- induction matrix and process control interfaces are 24 tall + space needed for divider
local cnc_bottom_align_start = main.height() - 26 local cnc_bottom_align_start = main.get_height() - 26
assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)") assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)")
TextBox{parent=main,y=cnc_bottom_align_start,text=util.strrep("\x8c", header.width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=cpair(colors.lightGray,colors.gray)} TextBox{parent=main,y=cnc_bottom_align_start,text=util.strrep("\x8c", header.get_width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=cpair(colors.lightGray,colors.gray)}
cnc_bottom_align_start = cnc_bottom_align_start + 2 cnc_bottom_align_start = cnc_bottom_align_start + 2

View File

@ -12,12 +12,11 @@ local element = {}
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer next line if omitted ---@field y? integer next line if omitted
---@field offset_x? integer 0 if omitted
---@field offset_y? integer 0 if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
---@alias graphics_args graphics_args_generic ---@alias graphics_args graphics_args_generic
---|waiting_args ---|waiting_args
@ -46,6 +45,7 @@ local element = {}
---|colormap_args ---|colormap_args
---|displaybox_args ---|displaybox_args
---|div_args ---|div_args
---|listbox_args
---|multipane_args ---|multipane_args
---|pipenet_args ---|pipenet_args
---|rectangle_args ---|rectangle_args
@ -62,26 +62,26 @@ local element = {}
---@param args graphics_args arguments ---@param args graphics_args arguments
function element.new(args) function element.new(args)
local self = { local self = {
id = -1, id = nil, ---@type element_id|nil
elem_type = debug.getinfo(2).name, elem_type = debug.getinfo(2).name,
define_completed = false, define_completed = false,
p_window = nil, ---@type table p_window = nil, ---@type table
position = { x = 1, y = 1 }, ---@type coordinate_2d position = { x = 1, y = 1 }, ---@type coordinate_2d
child_offset = { x = 0, y = 0 },
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
next_y = 1, next_y = 1,
children = {},
subscriptions = {}, subscriptions = {},
mt = {} mt = {}
} }
---@class graphics_template ---@class graphics_base
local protected = { local protected = {
enabled = true, enabled = true,
value = nil, ---@type any value = nil, ---@type any
window = nil, ---@type table window = nil, ---@type table
content_window = nil, ---@type table|nil
fg_bg = core.cpair(colors.white, colors.black), fg_bg = core.cpair(colors.white, colors.black),
frame = core.gframe(1, 1, 1, 1) frame = core.gframe(1, 1, 1, 1),
children = {}
} }
local name_brief = "graphics.element{" .. self.elem_type .. "}: " local name_brief = "graphics.element{" .. self.elem_type .. "}: "
@ -101,10 +101,8 @@ function element.new(args)
------------------------- -------------------------
-- prepare the template -- prepare the template
---@param offset_x integer x offset
---@param offset_y integer y offset
---@param next_y integer next line if no y was provided ---@param next_y integer next line if no y was provided
function protected.prepare_template(offset_x, offset_y, next_y) function protected.prepare_template(next_y)
-- get frame coordinates/size -- get frame coordinates/size
if args.gframe ~= nil then if args.gframe ~= nil then
protected.frame.x = args.gframe.x protected.frame.x = args.gframe.x
@ -114,36 +112,18 @@ function element.new(args)
else else
local w, h = self.p_window.getSize() local w, h = self.p_window.getSize()
protected.frame.x = args.x or 1 protected.frame.x = args.x or 1
protected.frame.y = args.y or next_y
if args.parent ~= nil then
protected.frame.y = args.y or (next_y - offset_y)
else
protected.frame.y = args.y or next_y
end
protected.frame.w = args.width or w protected.frame.w = args.width or w
protected.frame.h = args.height or h protected.frame.h = args.height or h
end end
-- inner offsets
if args.offset_x ~= nil then self.child_offset.x = args.offset_x end
if args.offset_y ~= nil then self.child_offset.y = args.offset_y end
-- adjust window frame if applicable -- adjust window frame if applicable
local f = protected.frame local f = protected.frame
local x = f.x
local y = f.y
-- apply offsets
if args.parent ~= nil then if args.parent ~= nil then
-- constrain to parent inner width/height -- constrain to parent inner width/height
local w, h = self.p_window.getSize() local w, h = self.p_window.getSize()
f.w = math.min(f.w, w - ((2 * offset_x) + (f.x - 1))) f.w = math.min(f.w, w - (f.x - 1))
f.h = math.min(f.h, h - ((2 * offset_y) + (f.y - 1))) f.h = math.min(f.h, h - (f.y - 1))
-- offset x/y
f.x = x + offset_x
f.y = y + offset_y
end end
-- check frame -- check frame
@ -153,7 +133,7 @@ function element.new(args)
assert(f.h >= 1, name_brief .. "frame height not >= 1") assert(f.h >= 1, name_brief .. "frame height not >= 1")
-- create window -- create window
protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, true) protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, args.hidden ~= true)
-- init colors -- init colors
if args.fg_bg ~= nil then if args.fg_bg ~= nil then
@ -198,15 +178,15 @@ function element.new(args)
-- luacheck: push ignore -- luacheck: push ignore
---@diagnostic disable: unused-local, unused-vararg ---@diagnostic disable: unused-local, unused-vararg
-- dynamically insert a child element -- handle a child element having been added
---@param id string|integer element identifier ---@param id element_id element identifier
---@param elem graphics_element element ---@param child graphics_element child element
function protected.insert(id, elem) function protected.on_added(id, child)
end end
-- dynamically remove a child element -- handle a child element having been removed
---@param id string|integer element identifier ---@param id element_id element identifier
function protected.remove(id) function protected.on_removed(id)
end end
-- handle a mouse event -- handle a mouse event
@ -279,6 +259,14 @@ function element.new(args)
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
function protected.get() return public, self.id end function protected.get() return public, self.id end
-- report completion of element instantiation and get the public interface
---@nodiscard
---@return graphics_element element, element_id id
function protected.complete()
if args.parent ~= nil then args.parent.__child_ready(self.id, public) end
return public, self.id
end
----------- -----------
-- SETUP -- -- SETUP --
----------- -----------
@ -294,7 +282,8 @@ function element.new(args)
-- prepare the template -- prepare the template
if args.parent == nil then if args.parent == nil then
protected.prepare_template(0, 0, 1) self.id = args.id or "__ROOT__"
protected.prepare_template(1)
else else
self.id = args.parent.__add_child(args.id, protected) self.id = args.parent.__add_child(args.id, protected)
end end
@ -305,11 +294,21 @@ function element.new(args)
-- get the window object -- get the window object
---@nodiscard ---@nodiscard
function public.window() return protected.window end function public.window() return protected.content_window or protected.window end
-- delete this element (hide and unsubscribe from PSIL) -- delete this element (hide and unsubscribe from PSIL)
function public.delete() function public.delete()
-- hide + stop animations local fg_bg = protected.fg_bg
if args.parent ~= nil then
-- grab parent fg/bg so we can clear cleanly as a child element
fg_bg = args.parent.get_fg_bg()
end
-- clear, hide, and stop animations
protected.window.setBackgroundColor(fg_bg.bkg)
protected.window.setTextColor(fg_bg.fgd)
protected.window.clear()
public.hide() public.hide()
-- unsubscribe from PSIL -- unsubscribe from PSIL
@ -319,9 +318,14 @@ function element.new(args)
end end
-- delete all children -- delete all children
for k, v in pairs(self.children) do for k, v in pairs(protected.children) do
v.delete() v.delete()
self.children[k] = nil protected.children[k] = nil
end
if args.parent ~= nil then
-- remove self from parent
args.parent.__remove_child(self.id)
end end
end end
@ -330,41 +334,53 @@ function element.new(args)
-- add a child element -- add a child element
---@nodiscard ---@nodiscard
---@param key string|nil id ---@param key string|nil id
---@param child graphics_template ---@param child graphics_base
---@return integer|string key ---@return integer|string key
function public.__add_child(key, child) function public.__add_child(key, child)
-- offset first automatic placement child.prepare_template(self.next_y)
if self.next_y <= self.child_offset.y then
self.next_y = self.child_offset.y + 1
end
child.prepare_template(self.child_offset.x, self.child_offset.y, self.next_y)
self.next_y = child.frame.y + child.frame.h self.next_y = child.frame.y + child.frame.h
local child_element = child.get() local child_element = child.get()
if key == nil then if key == nil then
table.insert(self.children, child_element) table.insert(protected.children, child_element)
return #self.children return #protected.children
else else
self.children[key] = child_element protected.children[key] = child_element
return key return key
end end
end end
-- remove a child element
---@param key element_id id
function public.__remove_child(key)
if protected.children[key] ~= nil then
protected.on_removed(key)
protected.children[key] = nil
end
end
-- actions to take upon a child element becoming ready (initial draw/construction completed)
---@param key element_id id
---@param child graphics_element
function public.__child_ready(key, child)
protected.on_added(key, child)
end
-- get a child element -- get a child element
---@nodiscard ---@nodiscard
---@param id element_id ---@param id element_id
---@return graphics_element ---@return graphics_element
function public.get_child(id) return self.children[id] end function public.get_child(id) return protected.children[id] end
-- remove a child element -- remove a child element
---@param id element_id ---@param id element_id
function public.remove(id) function public.remove(id)
if self.children[id] ~= nil then if protected.children[id] ~= nil then
self.children[id].delete() protected.children[id].delete()
self.children[id] = nil protected.on_removed(id)
protected.children[id] = nil
end end
end end
@ -373,37 +389,18 @@ function element.new(args)
---@param id element_id ---@param id element_id
---@return graphics_element|nil element ---@return graphics_element|nil element
function public.get_element_by_id(id) function public.get_element_by_id(id)
if self.children[id] == nil then if protected.children[id] == nil then
for _, child in pairs(self.children) do for _, child in pairs(protected.children) do
local elem = child.get_element_by_id(id) local elem = child.get_element_by_id(id)
if elem ~= nil then return elem end if elem ~= nil then return elem end
end end
else else
return self.children[id] return protected.children[id]
end end
return nil return nil
end end
-- DYNAMIC CHILD ELEMENTS --
-- insert an element as a contained child<br>
-- this is intended to be used dynamically, and depends on the target element type.<br>
-- not all elements support dynamic children.
---@param id string|integer element identifier
---@param elem graphics_element element
function public.insert_element(id, elem)
protected.insert(id, elem)
end
-- remove an element from contained children<br>
-- this is intended to be used dynamically, and depends on the target element type.<br>
-- not all elements support dynamic children.
---@param id string|integer element identifier
function public.remove_element(id)
protected.remove(id)
end
-- AUTO-PLACEMENT -- -- AUTO-PLACEMENT --
-- skip a line for automatically placed elements -- skip a line for automatically placed elements
@ -437,14 +434,14 @@ function element.new(args)
-- get element width -- get element width
---@nodiscard ---@nodiscard
---@return integer width ---@return integer width
function public.width() function public.get_width()
return protected.frame.w return protected.frame.w
end end
-- get element height -- get element height
---@nodiscard ---@nodiscard
---@return integer height ---@return integer height
function public.height() function public.get_height()
return protected.frame.h return protected.frame.h
end end
@ -519,7 +516,7 @@ function element.new(args)
-- handle the mouse event then pass to children -- handle the mouse event then pass to children
protected.handle_mouse(event_T) protected.handle_mouse(event_T)
for _, child in pairs(self.children) do child.handle_mouse(event_T) end for _, child in pairs(protected.children) do child.handle_mouse(event_T) end
end end
end end
@ -545,27 +542,61 @@ function element.new(args)
ps.subscribe(key, func) ps.subscribe(key, func)
end end
-- VISIBILITY -- -- VISIBILITY & ANIMATIONS --
-- show the element -- show the element and enables animations by default
function public.show() ---@param animate? boolean true (default) to automatically resume animations
function public.show(animate)
protected.window.setVisible(true) protected.window.setVisible(true)
protected.start_anim() if animate ~= false then public.animate_all() end
for _, child in pairs(self.children) do child.show() end
end end
-- hide the element -- hide the element and disables animations<br>
-- this alone does not cause an element to be fully hidden, it only prevents updates from being shown<br>
---@see graphics_element.content_redraw
function public.hide() function public.hide()
protected.stop_anim() public.freeze_all() -- stop animations for efficiency/performance
for _, child in pairs(self.children) do child.hide() end
protected.window.setVisible(false) protected.window.setVisible(false)
end end
-- start/resume animation(s)
function public.animate()
protected.start_anim()
end
-- start/resume animation(s) for this element and all its children<br>
-- only animates if a window is visible
function public.animate_all()
if protected.window.isVisible() then
public.animate()
for _, child in pairs(protected.children) do child.animate_all() end
end
end
-- freeze animation(s)
function public.freeze()
protected.stop_anim()
end
-- freeze animation(s) for this element and all its children
function public.freeze_all()
public.freeze()
for _, child in pairs(protected.children) do child.freeze_all() end
end
-- re-draw the element -- re-draw the element
function public.redraw() function public.redraw()
protected.window.redraw() protected.window.redraw()
end end
-- if a content window is set, clears it then re-draws all children
function public.content_redraw()
if protected.content_window ~= nil then
protected.content_window.clear()
for _, child in pairs(protected.children) do child.redraw() end
end
end
return protected return protected
end end

View File

@ -1,6 +1,6 @@
-- Loading/Waiting Animation Graphics Element -- Loading/Waiting Animation Graphics Element
local tcd = require("scada-common.tcallbackdsp") local tcd = require("scada-common.tcd")
local element = require("graphics.element") local element = require("graphics.element")
@ -10,6 +10,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new waiting animation element -- new waiting animation element
---@param args waiting_args ---@param args waiting_args
@ -102,7 +103,7 @@ local function waiting(args)
e.start_anim() e.start_anim()
return e.get() return e.complete()
end end
return waiting return waiting

View File

@ -9,6 +9,7 @@ local element = require("graphics.element")
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field hidden? boolean true to hide on initial draw
-- new color map -- new color map
---@param args colormap_args ---@param args colormap_args
@ -27,7 +28,7 @@ local function colormap(args)
e.window.setCursorPos(1, 1) e.window.setCursorPos(1, 1)
e.window.blit(spaces, bkg, bkg) e.window.blit(spaces, bkg, bkg)
return e.get() return e.complete()
end end
return colormap return colormap

View File

@ -1,6 +1,6 @@
-- Hazard-bordered Button Graphics Element -- Hazard-bordered Button Graphics Element
local tcd = require("scada-common.tcallbackdsp") local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
@ -16,6 +16,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new hazard button -- new hazard button
---@param args hazard_button_args ---@param args hazard_button_args
@ -198,7 +199,7 @@ local function hazard_button(args)
-- initial draw of border -- initial draw of border
draw_border(args.accent) draw_border(args.accent)
return e.get() return e.complete()
end end
return hazard_button return hazard_button

View File

@ -23,6 +23,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new multi button (latch selection, exclusively one button at a time) -- new multi button (latch selection, exclusively one button at a time)
---@param args multi_button_args ---@param args multi_button_args
@ -130,7 +131,7 @@ local function multi_button(args)
-- initial draw -- initial draw
draw() draw()
return e.get() return e.complete()
end end
return multi_button return multi_button

View File

@ -1,6 +1,6 @@
-- Button Graphics Element -- Button Graphics Element
local tcd = require("scada-common.tcallbackdsp") local tcd = require("scada-common.tcd")
local core = require("graphics.core") local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
@ -19,6 +19,7 @@ local CLICK_TYPE = core.events.CLICK_TYPE
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new push button -- new push button
---@param args push_button_args ---@param args push_button_args
@ -120,7 +121,7 @@ local function push_button(args)
-- initial draw -- initial draw
draw() draw()
return e.get() return e.complete()
end end
return push_button return push_button

View File

@ -15,6 +15,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new radio button list (latch selection, exclusively one button at a time) -- new radio button list (latch selection, exclusively one button at a time)
---@param args radio_button_args ---@param args radio_button_args
@ -103,7 +104,7 @@ local function radio_button(args)
-- initial draw -- initial draw
draw() draw()
return e.get() return e.complete()
end end
return radio_button return radio_button

View File

@ -1,6 +1,6 @@
-- Sidebar Graphics Element -- Sidebar Graphics Element
local tcd = require("scada-common.tcallbackdsp") local tcd = require("scada-common.tcd")
local core = require("graphics.core") local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
@ -20,6 +20,7 @@ local CLICK_TYPE = core.events.CLICK_TYPE
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new sidebar tab selector -- new sidebar tab selector
---@param args sidebar_args ---@param args sidebar_args
@ -115,7 +116,7 @@ local function sidebar(args)
-- initial draw -- initial draw
draw(false) draw(false)
return e.get() return e.complete()
end end
return sidebar return sidebar

View File

@ -18,6 +18,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new spinbox control (minimum value is 0) -- new spinbox control (minimum value is 0)
---@param args spinbox_args ---@param args spinbox_args
@ -188,7 +189,7 @@ local function spinbox(args)
e.value = 0 e.value = 0
set_digits() set_digits()
return e.get() return e.complete()
end end
return spinbox return spinbox

View File

@ -15,6 +15,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new switch button (latch high/low) -- new switch button (latch high/low)
---@param args switch_button_args ---@param args switch_button_args
@ -86,7 +87,7 @@ local function switch_button(args)
draw_state() draw_state()
end end
return e.get() return e.complete()
end end
return switch_button return switch_button

View File

@ -21,6 +21,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new tab selector -- new tab selector
---@param args tabbar_args ---@param args tabbar_args
@ -124,7 +125,7 @@ local function tabbar(args)
-- initial draw -- initial draw
draw() draw()
return e.get() return e.complete()
end end
return tabbar return tabbar

View File

@ -4,19 +4,22 @@ local element = require("graphics.element")
---@class displaybox_args ---@class displaybox_args
---@field window table ---@field window table
---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new root display box -- new root display box
---@nodiscard ---@nodiscard
---@param args displaybox_args ---@param args displaybox_args
---@return graphics_element element, element_id id
local function displaybox(args) local function displaybox(args)
-- create new graphics element base object -- create new graphics element base object
return element.new(args).get() return element.new(args).complete()
end end
return displaybox return displaybox

View File

@ -11,6 +11,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new div element -- new div element
---@nodiscard ---@nodiscard
@ -18,7 +19,7 @@ local element = require("graphics.element")
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function div(args) local function div(args)
-- create new graphics element base object -- create new graphics element base object
return element.new(args).get() return element.new(args).complete()
end end
return div return div

View File

@ -18,6 +18,7 @@ local flasher = require("graphics.flasher")
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new alarm indicator light -- new alarm indicator light
---@nodiscard ---@nodiscard
@ -108,7 +109,7 @@ local function alarm_indicator_light(args)
e.on_update(1) e.on_update(1)
e.window.write(args.label) e.window.write(args.label)
return e.get() return e.complete()
end end
return alarm_indicator_light return alarm_indicator_light

View File

@ -163,7 +163,7 @@ local function core_map(args)
-- initial draw -- initial draw
e.on_update(0) e.on_update(0)
return e.get() return e.complete()
end end
return core_map return core_map

View File

@ -17,6 +17,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field width integer length ---@field width integer length
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new data indicator -- new data indicator
---@nodiscard ---@nodiscard
@ -43,8 +44,9 @@ local function data(args)
e.window.setCursorPos(1, 1) e.window.setCursorPos(1, 1)
e.window.write(args.label) e.window.write(args.label)
local label_len = string.len(args.label) local value_color = e.fg_bg.fgd
local data_start = 1 local label_len = string.len(args.label)
local data_start = 1
local clear_width = args.width local clear_width = args.width
if label_len > 0 then if label_len > 0 then
@ -64,7 +66,7 @@ local function data(args)
-- write data -- write data
local data_str = util.sprintf(args.format, value) local data_str = util.sprintf(args.format, value)
e.window.setCursorPos(data_start, 1) e.window.setCursorPos(data_start, 1)
e.window.setTextColor(e.fg_bg.fgd) e.window.setTextColor(value_color)
if args.commas then if args.commas then
e.window.write(util.comma_format(data_str)) e.window.write(util.comma_format(data_str))
else else
@ -84,10 +86,17 @@ local function data(args)
---@param val any new value ---@param val any new value
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- change the foreground color of the value, or all text if no label/unit colors provided
---@param c color
function e.recolor(c)
value_color = c
e.on_update(e.value)
end
-- initial value draw -- initial value draw
e.on_update(args.value) e.on_update(args.value)
return e.get() return e.complete()
end end
return data return data

View File

@ -15,6 +15,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new horizontal bar -- new horizontal bar
---@nodiscard ---@nodiscard
@ -119,7 +120,7 @@ local function hbar(args)
-- initialize to 0 -- initialize to 0
e.on_update(0) e.on_update(0)
return e.get() return e.complete()
end end
return hbar return hbar

View File

@ -18,6 +18,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new icon indicator -- new icon indicator
---@nodiscard ---@nodiscard
@ -68,7 +69,7 @@ local function icon(args)
-- initial icon draw -- initial icon draw
e.on_update(args.value or 1) e.on_update(args.value or 1)
return e.get() return e.complete()
end end
return icon return icon

View File

@ -16,6 +16,7 @@ local flasher = require("graphics.flasher")
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new indicator LED -- new indicator LED
---@nodiscard ---@nodiscard
@ -94,7 +95,7 @@ local function indicator_led(args)
e.window.write(args.label) e.window.write(args.label)
end end
return e.get() return e.complete()
end end
return indicator_led return indicator_led

View File

@ -18,6 +18,7 @@ local flasher = require("graphics.flasher")
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new dual LED indicator light -- new dual LED indicator light
---@nodiscard ---@nodiscard
@ -108,7 +109,7 @@ local function indicator_led_pair(args)
e.window.write(args.label) e.window.write(args.label)
end end
return e.get() return e.complete()
end end
return indicator_led_pair return indicator_led_pair

View File

@ -11,6 +11,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new RGB LED indicator light -- new RGB LED indicator light
---@nodiscard ---@nodiscard
@ -53,7 +54,7 @@ local function indicator_led_rgb(args)
e.window.write(args.label) e.window.write(args.label)
end end
return e.get() return e.complete()
end end
return indicator_led_rgb return indicator_led_rgb

View File

@ -16,6 +16,7 @@ local flasher = require("graphics.flasher")
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new indicator light -- new indicator light
---@nodiscard ---@nodiscard
@ -92,7 +93,7 @@ local function indicator_light(args)
e.window.setCursorPos(3, 1) e.window.setCursorPos(3, 1)
e.window.write(args.label) e.window.write(args.label)
return e.get() return e.complete()
end end
return indicator_light return indicator_light

View File

@ -16,6 +16,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field width integer length ---@field width integer length
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new power indicator -- new power indicator
---@nodiscard ---@nodiscard
@ -79,7 +80,7 @@ local function power(args)
-- initial value draw -- initial value draw
e.on_update(args.value) e.on_update(args.value)
return e.get() return e.complete()
end end
return power return power

View File

@ -17,6 +17,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field width integer length ---@field width integer length
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new radiation indicator -- new radiation indicator
---@nodiscard ---@nodiscard
@ -84,7 +85,7 @@ local function rad(args)
-- initial value draw -- initial value draw
e.on_update(types.new_zero_radiation_reading()) e.on_update(types.new_zero_radiation_reading())
return e.get() return e.complete()
end end
return rad return rad

View File

@ -18,6 +18,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field height? integer 1 if omitted, must be an odd number ---@field height? integer 1 if omitted, must be an odd number
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new state indicator -- new state indicator
---@nodiscard ---@nodiscard
@ -74,7 +75,7 @@ local function state_indicator(args)
-- initial draw -- initial draw
e.on_update(args.value or 1) e.on_update(args.value or 1)
return e.get() return e.complete()
end end
return state_indicator return state_indicator

View File

@ -18,6 +18,7 @@ local flasher = require("graphics.flasher")
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new tri-state indicator light -- new tri-state indicator light
---@nodiscard ---@nodiscard
@ -105,7 +106,7 @@ local function tristate_indicator_light(args)
e.on_update(1) e.on_update(1)
e.window.write(args.label) e.window.write(args.label)
return e.get() return e.complete()
end end
return tristate_indicator_light return tristate_indicator_light

View File

@ -13,6 +13,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new vertical bar -- new vertical bar
---@nodiscard ---@nodiscard
@ -99,7 +100,7 @@ local function vbar(args)
---@param val number 0.0 to 1.0 ---@param val number 0.0 to 1.0
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
return e.get() return e.complete()
end end
return vbar return vbar

View File

@ -0,0 +1,283 @@
-- Scroll-able List Box Display Graphics Element
local tcd = require("scada-common.tcd")
local core = require("graphics.core")
local element = require("graphics.element")
local CLICK_TYPE = core.events.CLICK_TYPE
---@class listbox_args
---@field scroll_height integer height of internal scrolling container (must fit all elements vertically tiled)
---@field item_pad? integer spacing (lines) between items in the list (default 0)
---@field nav_fg_bg? cpair foreground/background colors for scroll arrows and bar area
---@field nav_active? cpair active colors for bar held down or arrow held down
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
---@class listbox_item
---@field id string|integer element ID
---@field e graphics_element element
---@field y integer y position
---@field h integer element height
-- new listbox element
---@nodiscard
---@param args listbox_args
---@return graphics_element element, element_id id
local function listbox(args)
-- create new graphics element base object
local e = element.new(args)
-- create content window for child elements
local scroll_frame = window.create(e.window, 1, 1, e.frame.w - 1, args.scroll_height, false)
e.content_window = scroll_frame
-- item list and scroll management
local list = {}
local item_pad = args.item_pad or 0
local scroll_offset = 0
local content_height = 0
local max_down_scroll = 0
-- bar control/tracking variables
local max_bar_height = e.frame.h - 2
local bar_height = 0 -- full height of bar
local bar_bounds = { 0, 0 } -- top and bottom of bar
local bar_is_scaled = false -- if the scrollbar doesn't have a 1:1 ratio with lines
local holding_bar = false -- bar is being held by mouse
local bar_grip_pos = 0 -- where the bar was gripped by mouse down
local mouse_last_y = 0 -- last reported y coordinate of drag
-- draw scroll bar arrows, optionally showing one of them as pressed
---@param pressed_arrow? 1|0|-1 arrow to show as pressed (1 = scroll up, 0 = neither, -1 = scroll down)
local function draw_arrows(pressed_arrow)
local nav_fg_bg = args.nav_fg_bg or e.fg_bg
local active_fg_bg = args.nav_active or nav_fg_bg
-- draw up/down arrows
if pressed_arrow == 1 then
e.window.setTextColor(active_fg_bg.fgd)
e.window.setBackgroundColor(active_fg_bg.bkg)
e.window.setCursorPos(e.frame.w, 1)
e.window.write("\x1e")
e.window.setTextColor(nav_fg_bg.fgd)
e.window.setBackgroundColor(nav_fg_bg.bkg)
e.window.setCursorPos(e.frame.w, e.frame.h)
e.window.write("\x1f")
elseif pressed_arrow == -1 then
e.window.setTextColor(nav_fg_bg.fgd)
e.window.setBackgroundColor(nav_fg_bg.bkg)
e.window.setCursorPos(e.frame.w, 1)
e.window.write("\x1e")
e.window.setTextColor(active_fg_bg.fgd)
e.window.setBackgroundColor(active_fg_bg.bkg)
e.window.setCursorPos(e.frame.w, e.frame.h)
e.window.write("\x1f")
else
e.window.setTextColor(nav_fg_bg.fgd)
e.window.setBackgroundColor(nav_fg_bg.bkg)
e.window.setCursorPos(e.frame.w, 1)
e.window.write("\x1e")
e.window.setCursorPos(e.frame.w, e.frame.h)
e.window.write("\x1f")
end
e.window.setTextColor(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg)
end
-- render the scroll bar and re-cacluate height & bounds
local function draw_bar()
local offset = 2 + math.abs(scroll_offset)
bar_height = math.min(max_bar_height + max_down_scroll, max_bar_height)
if bar_height < 1 then
bar_is_scaled = true
-- can't do a 1:1 ratio
-- use minimum size bar with scaled offset
local scroll_progress = scroll_offset / max_down_scroll
offset = 2 + math.floor(scroll_progress * (max_bar_height - 1))
bar_height = 1
else
bar_is_scaled = false
end
bar_bounds = { offset, (bar_height + offset) - 1 }
for i = 2, e.frame.h - 1 do
if (i >= offset and i < (bar_height + offset)) and (bar_height ~= max_bar_height) then
if args.nav_fg_bg ~= nil then
e.window.setBackgroundColor(args.nav_fg_bg.fgd)
else
e.window.setBackgroundColor(e.fg_bg.fgd)
end
else
if args.nav_fg_bg ~= nil then
e.window.setBackgroundColor(args.nav_fg_bg.bkg)
else
e.window.setBackgroundColor(e.fg_bg.bkg)
end
end
e.window.setCursorPos(e.frame.w, i)
e.window.write(" ")
end
e.window.setBackgroundColor(e.fg_bg.bkg)
end
-- update item y positions and move elements
local function update_positions()
local next_y = 1
scroll_frame.setVisible(false)
scroll_frame.setBackgroundColor(e.fg_bg.bkg)
scroll_frame.setTextColor(e.fg_bg.fgd)
scroll_frame.clear()
for i = 1, #list do
local item = list[i] ---@type listbox_item
item.y = next_y
next_y = next_y + item.h + item_pad
item.e.reposition(1, item.y)
item.e.show()
end
content_height = next_y
max_down_scroll = math.min(-1 * (content_height - (e.frame.h + 1 + item_pad)), 0)
if scroll_offset < max_down_scroll then scroll_offset = max_down_scroll end
scroll_frame.reposition(1, 1 + scroll_offset)
scroll_frame.setVisible(true)
draw_bar()
end
-- determine where to scroll to based on a scrollbar being dragged without a 1:1 relationship
---@param direction -1|1 negative 1 to scroll up by one, positive 1 to scroll down by one
local function scaled_bar_scroll(direction)
local scroll_progress = scroll_offset / max_down_scroll
local bar_position = math.floor(scroll_progress * (max_bar_height - 1))
-- check what moving the scroll bar up or down would mean for the scroll progress
scroll_progress = (bar_position + direction) / (max_bar_height - 1)
return math.max(math.floor(scroll_progress * max_down_scroll), max_down_scroll)
end
-- scroll down the list
local function scroll_down(scaled)
if scroll_offset > max_down_scroll then
if scaled then
scroll_offset = scaled_bar_scroll(1)
else
scroll_offset = scroll_offset - 1
end
update_positions()
end
end
-- scroll up the list
local function scroll_up(scaled)
if scroll_offset < 0 then
if scaled then
scroll_offset = scaled_bar_scroll(-1)
else
scroll_offset = scroll_offset + 1
end
update_positions()
end
end
-- handle a child element having been added to the list
---@param id element_id element identifier
---@param child graphics_element child element
function e.on_added(id, child)
table.insert(list, { id = id, e = child, y = 0, h = child.get_height() })
update_positions()
end
-- handle a child element having been removed from the list
---@param id element_id element identifier
function e.on_removed(id)
for idx, elem in ipairs(list) do
if elem.id == id then
table.remove(list, idx)
update_positions()
return
end
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled then
if event.type == CLICK_TYPE.TAP then
if event.current.x == e.frame.w then
if event.current.y == 1 or event.current.y < bar_bounds[1] then
draw_arrows(1)
scroll_up()
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
draw_arrows(-1)
scroll_down()
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
end
end
elseif event.type == CLICK_TYPE.DOWN then
if event.current.x == e.frame.w then
if event.current.y == 1 or event.current.y < bar_bounds[1] then
draw_arrows(1)
scroll_up()
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
draw_arrows(-1)
scroll_down()
else
-- clicked on bar
holding_bar = true
bar_grip_pos = event.current.y - bar_bounds[1]
mouse_last_y = event.current.y
end
end
elseif event.type == CLICK_TYPE.UP then
holding_bar = false
draw_arrows(0)
elseif event.type == CLICK_TYPE.DRAG then
if holding_bar then
-- if mouse is within vertical frame, including the grip point
if event.current.y > (1 + bar_grip_pos) and event.current.y <= ((e.frame.h - bar_height) + bar_grip_pos) then
if event.current.y < mouse_last_y then
scroll_up(bar_is_scaled)
elseif event.current.y > mouse_last_y then
scroll_down(bar_is_scaled)
end
mouse_last_y = event.current.y
end
end
elseif event.type == CLICK_TYPE.SCROLL_DOWN then
scroll_down()
elseif event.type == CLICK_TYPE.SCROLL_UP then
scroll_up()
end
end
end
draw_arrows(0)
draw_bar()
return e.complete()
end
return listbox

View File

@ -12,6 +12,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new multipane element -- new multipane element
---@nodiscard ---@nodiscard
@ -36,7 +37,7 @@ local function multipane(args)
e.set_value(1) e.set_value(1)
return e.get() return e.complete()
end end
return multipane return multipane

View File

@ -12,6 +12,7 @@ local element = require("graphics.element")
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field hidden? boolean true to hide on initial draw
-- new pipe network -- new pipe network
---@param args pipenet_args ---@param args pipenet_args
@ -141,7 +142,7 @@ local function pipenet(args)
end end
return e.get() return e.complete()
end end
return pipenet return pipenet

View File

@ -16,6 +16,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new rectangle -- new rectangle
---@param args rectangle_args ---@param args rectangle_args
@ -30,27 +31,35 @@ local function rectangle(args)
end end
-- offset children -- offset children
local offset_x = 0
local offset_y = 0
if args.border ~= nil then if args.border ~= nil then
args.offset_x = args.border.width offset_x = args.border.width
args.offset_y = args.border.width offset_y = args.border.width
-- slightly different y offset if the border is set to even -- slightly different y offset if the border is set to even
if args.border.even then if args.border.even then
local width_x2 = (2 * args.border.width) local width_x2 = (2 * args.border.width)
args.offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0) offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0)
end end
end end
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- create content window for child elements
e.content_window = window.create(e.window, 1 + offset_x, 1 + offset_y, e.frame.w - (2 * offset_x), e.frame.h - (2 * offset_y))
e.content_window.setBackgroundColor(e.fg_bg.bkg)
e.content_window.setTextColor(e.fg_bg.fgd)
e.content_window.clear()
-- draw bordered box if requested -- draw bordered box if requested
-- element constructor will have drawn basic colored rectangle regardless -- element constructor will have drawn basic colored rectangle regardless
if args.border ~= nil then if args.border ~= nil then
e.window.setCursorPos(1, 1) e.window.setCursorPos(1, 1)
local border_width = args.offset_x local border_width = offset_x
local border_height = args.offset_y local border_height = offset_y
local border_blit = colors.toBlit(args.border.color) local border_blit = colors.toBlit(args.border.color)
local width_x2 = border_width * 2 local width_x2 = border_width * 2
local inner_width = e.frame.w - width_x2 local inner_width = e.frame.w - width_x2
@ -177,7 +186,7 @@ local function rectangle(args)
end end
end end
return e.get() return e.complete()
end end
return rectangle return rectangle

View File

@ -18,6 +18,7 @@ local TEXT_ALIGN = core.TEXT_ALIGN
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new text box -- new text box
---@param args textbox_args ---@param args textbox_args
@ -64,7 +65,7 @@ local function textbox(args)
display_text(val) display_text(val)
end end
return e.get() return e.complete()
end end
return textbox return textbox

View File

@ -16,6 +16,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new tiling box -- new tiling box
---@param args tiling_args ---@param args tiling_args
@ -81,7 +82,7 @@ local function tiling(args)
if inner_width % 2 == 0 then alternator = not alternator end if inner_width % 2 == 0 then alternator = not alternator end
end end
return e.get() return e.complete()
end end
return tiling return tiling

View File

@ -2,7 +2,7 @@
-- Indicator Light Flasher -- Indicator Light Flasher
-- --
local tcd = require("scada-common.tcallbackdsp") local tcd = require("scada-common.tcd")
local flasher = {} local flasher = {}

View File

@ -111,7 +111,7 @@ f.close()
if len(sys.argv) > 1 and sys.argv[1] == "shields": if len(sys.argv) > 1 and sys.argv[1] == "shields":
# write all the JSON files for shields.io # write all the JSON files for shields.io
for key, version in final_manifest["versions"].items(): for key, version in final_manifest["versions"].items():
f = open("./shields/" + key + ".json", "w") f = open("./deploy/" + key + ".json", "w")
if version.find("alpha") >= 0: if version.find("alpha") >= 0:
color = "yellow" color = "yellow"

File diff suppressed because one or more lines are too long

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

@ -4,20 +4,20 @@
require("/initenv").init_env() require("/initenv").init_env()
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
local log = require("scada-common.log") local log = require("scada-common.log")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local tcallbackdsp = require("scada-common.tcallbackdsp") local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local config = require("pocket.config") local config = require("pocket.config")
local coreio = require("pocket.coreio") 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.2" 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)
@ -120,54 +120,54 @@ local function main()
conn_wd.sv.feed() conn_wd.sv.feed()
conn_wd.api.feed() conn_wd.api.feed()
log.debug("startup> conn watchdog started") log.debug("startup> conn watchdog started")
end
-- main event loop -- main event loop
while ui_ok do while true do
local event, param1, param2, param3, param4, param5 = util.pull_event() local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event -- handle event
if event == "timer" then if event == "timer" then
if loop_clock.is_clock(param1) then if loop_clock.is_clock(param1) then
-- main loop tick -- main loop tick
-- relink if necessary -- relink if necessary
pocket_comms.link_update() pocket_comms.link_update()
loop_clock.start() loop_clock.start()
elseif conn_wd.sv.is_timer(param1) then elseif conn_wd.sv.is_timer(param1) then
-- supervisor watchdog timeout -- supervisor watchdog timeout
log.info("supervisor server timeout") log.info("supervisor server timeout")
pocket_comms.close_sv() pocket_comms.close_sv()
elseif conn_wd.api.is_timer(param1) then elseif conn_wd.api.is_timer(param1) then
-- coordinator watchdog timeout -- coordinator watchdog timeout
log.info("coordinator api server timeout") log.info("coordinator api server timeout")
pocket_comms.close_api() pocket_comms.close_api()
else else
-- a non-clock/main watchdog timer event -- a non-clock/main watchdog timer event
-- notify timer callback dispatcher -- notify timer callback dispatcher
tcallbackdsp.handle(param1) tcd.handle(param1)
end
elseif event == "modem_message" then
-- got a packet
local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
pocket_comms.handle_packet(packet)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then
-- handle a monitor touch event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
end
-- check for termination request
if event == "terminate" or ppm.should_terminate() then
log.info("terminate requested, closing server connections...")
pocket_comms.close()
log.info("connections closed")
break
end end
elseif event == "modem_message" then
-- got a packet
local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
pocket_comms.handle_packet(packet)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then
-- handle a monitor touch event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
end end
-- check for termination request renderer.close_ui()
if event == "terminate" or ppm.should_terminate() then
log.info("terminate requested, closing server connections...")
pocket_comms.close()
log.info("connections closed")
break
end
end end
renderer.close_ui()
println_ts("exited") println_ts("exited")
log.info("exited") log.info("exited")
end end

View File

@ -25,7 +25,7 @@ local function init(parent, y, is_api)
-- bounding box div -- bounding box div
local box = Div{parent=root,x=1,y=y,height=5} local box = Div{parent=root,x=1,y=y,height=5}
local waiting_x = math.floor(parent.width() / 2) - 1 local waiting_x = math.floor(parent.get_width() / 2) - 1
if is_api then if is_api then
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)} WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)}

View File

@ -8,11 +8,11 @@ local style = require("pocket.ui.style")
local conn_waiting = require("pocket.ui.components.conn_waiting") local conn_waiting = require("pocket.ui.components.conn_waiting")
local home_page = require("pocket.ui.components.home_page") local home_page = require("pocket.ui.pages.home_page")
local unit_page = require("pocket.ui.components.unit_page") local unit_page = require("pocket.ui.pages.unit_page")
local reactor_page = require("pocket.ui.components.reactor_page") local reactor_page = require("pocket.ui.pages.reactor_page")
local boiler_page = require("pocket.ui.components.boiler_page") local boiler_page = require("pocket.ui.pages.boiler_page")
local turbine_page = require("pocket.ui.components.turbine_page") local turbine_page = require("pocket.ui.pages.turbine_page")
local core = require("graphics.core") local core = require("graphics.core")

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,22 +954,26 @@ 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
println_ts("received unsolicited link denial, unlinking")
log.warning("unsolicited establish request denied")
elseif est_ack == ESTABLISH_ACK.COLLISION then
println_ts("received unsolicited link collision, unlinking")
log.warning("unsolicited establish request collision")
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
println_ts("received unsolicited link version mismatch, unlinking")
log.warning("unsolicited establish request version mismatch")
else else
println_ts("invalid unsolicited link response") if est_ack == ESTABLISH_ACK.DENY then
log.debug("unsolicited unknown establish request response") println_ts("received unsolicited link denial, unlinking")
end log.warning("unsolicited establish request denied")
elseif est_ack == ESTABLISH_ACK.COLLISION then
println_ts("received unsolicited link collision, unlinking")
log.warning("unsolicited establish request collision")
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
println_ts("received unsolicited link version mismatch, unlinking")
log.warning("unsolicited establish request version mismatch")
else
println_ts("invalid unsolicited link response")
log.debug("unsolicited unknown establish request response")
end
self.linked = est_ack == ESTABLISH_ACK.ALLOW -- unlink
self.sv_addr = comms.BROADCAST
self.linked = false
end
-- clear this since this is for something that was unsolicited -- 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,23 +1027,28 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
public.send_status(plc_state.no_reactor, plc_state.reactor_formed) 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 est_ack == ESTABLISH_ACK.DENY then if self.last_est_ack ~= est_ack then
println_ts("link request denied, retrying...") if est_ack == ESTABLISH_ACK.DENY then
log.info("supervisor establish request denied, retrying") println_ts("link request denied, retrying...")
elseif est_ack == ESTABLISH_ACK.COLLISION then log.info("supervisor establish request denied, retrying")
println_ts("reactor PLC ID collision (check config), retrying...") elseif est_ack == ESTABLISH_ACK.COLLISION then
log.warning("establish request collision, retrying") println_ts("reactor PLC ID collision (check config), retrying...")
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then log.warning("establish request collision, retrying")
println_ts("supervisor version mismatch (try updating), retrying...") elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
log.warning("establish request version mismatch, retrying") println_ts("supervisor version mismatch (try updating), retrying...")
else log.warning("establish request version mismatch, retrying")
println_ts("invalid link response, bad channel? retrying...") else
log.error("unknown establish request response, retrying") println_ts("invalid link response, bad channel? retrying...")
log.error("unknown establish request response, retrying")
end
end end
-- unlink
self.sv_addr = comms.BROADCAST
self.linked = false
end end
self.linked = est_ack == ESTABLISH_ACK.ALLOW
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.2" 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

@ -1,13 +1,13 @@
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local tcallbackdsp = require("scada-common.tcallbackdsp") local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
local databus = require("reactor-plc.databus") local databus = require("reactor-plc.databus")
local renderer = require("reactor-plc.renderer") local renderer = require("reactor-plc.renderer")
local core = require("graphics.core") local core = require("graphics.core")
local threads = {} local threads = {}
@ -157,7 +157,7 @@ function threads.thread__main(smem, init)
smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT) smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT)
elseif event == "timer" then elseif event == "timer" then
-- notify timer callback dispatcher if no other timer case claimed this event -- notify timer callback dispatcher if no other timer case claimed this event
tcallbackdsp.handle(param1) tcd.handle(param1)
elseif event == "peripheral_detach" then elseif event == "peripheral_detach" then
-- peripheral disconnect -- peripheral disconnect
local type, device = ppm.handle_unmount(param1) local type, device = ppm.handle_unmount(param1)

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")
@ -44,7 +45,7 @@ local function init(panel, units)
local system = Div{parent=panel,width=14,height=18,x=2,y=3} local system = Div{parent=panel,width=14,height=18,x=2,y=3}
local on = LED{parent=system,label="POWER",colors=cpair(colors.green,colors.red)} local on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)} local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)}
on.update(true) on.update(true)
system.line_break() system.line_break()
@ -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.2" 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)
@ -457,9 +457,9 @@ local function main()
if not rtu_state.fp_ok then if not rtu_state.fp_ok then
renderer.close_ui() renderer.close_ui()
println_ts(util.c("UI error: ", message)) println_ts(util.c("UI error: ", message))
println("init> running without front panel") println("startup> running without front panel")
log.error(util.c("GUI crashed with error ", message)) log.error(util.c("GUI crashed with error ", message))
log.info("init> running in headless mode without front panel") log.info("startup> running in headless mode without front panel")
end end
-- start connection watchdog -- start connection watchdog
@ -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

@ -1,6 +1,7 @@
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
@ -82,6 +83,9 @@ function threads.thread__main(smem)
elseif event == "timer" and conn_watchdog.is_timer(param1) then elseif event == "timer" and conn_watchdog.is_timer(param1) then
-- haven't heard from server recently? unlink -- haven't heard from server recently? unlink
rtu_comms.unlink(rtu_state) rtu_comms.unlink(rtu_state)
elseif event == "timer" then
-- notify timer callback dispatcher if no other timer case claimed this event
tcd.handle(param1)
elseif event == "peripheral_detach" then elseif event == "peripheral_detach" then
-- handle loss of a device -- handle loss of a device
local type, device = ppm.handle_unmount(param1) local type, device = ppm.handle_unmount(param1)

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

@ -5,14 +5,14 @@
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 tcallbackdsp = {} local tcd = {}
local registry = {} local registry = {}
-- request a function to be called after the specified time -- request a function to be called after the specified time
---@param time number seconds ---@param time number seconds
---@param f function callback function ---@param f function callback function
function tcallbackdsp.dispatch(time, f) function tcd.dispatch(time, f)
local timer = util.start_timer(time) local timer = util.start_timer(time)
registry[timer] = { registry[timer] = {
callback = f, callback = f,
@ -24,7 +24,7 @@ end
-- request a function to be called after the specified time, aborting any registered instances of that function reference -- request a function to be called after the specified time, aborting any registered instances of that function reference
---@param time number seconds ---@param time number seconds
---@param f function callback function ---@param f function callback function
function tcallbackdsp.dispatch_unique(time, f) function tcd.dispatch_unique(time, f)
-- cancel if already registered -- cancel if already registered
for timer, entry in pairs(registry) do for timer, entry in pairs(registry) do
if entry.callback == f then if entry.callback == f then
@ -47,7 +47,7 @@ end
-- abort a requested callback -- abort a requested callback
---@param f function callback function ---@param f function callback function
function tcallbackdsp.abort(f) function tcd.abort(f)
for timer, entry in pairs(registry) do for timer, entry in pairs(registry) do
if entry.callback == f then if entry.callback == f then
-- cancel event and remove from registry (even if it fires it won't call) -- cancel event and remove from registry (even if it fires it won't call)
@ -59,7 +59,7 @@ end
-- lookup a timer event and execute the callback if found -- lookup a timer event and execute the callback if found
---@param event integer timer event timer ID ---@param event integer timer event timer ID
function tcallbackdsp.handle(event) function tcd.handle(event)
if registry[event] ~= nil then if registry[event] ~= nil then
local callback = registry[event].callback local callback = registry[event].callback
-- clear first so that dispatch_unique call from inside callback won't throw a debug message -- clear first so that dispatch_unique call from inside callback won't throw a debug message
@ -70,7 +70,7 @@ end
-- identify any overdo callbacks<br> -- identify any overdo callbacks<br>
-- prints to log debug output -- prints to log debug output
function tcallbackdsp.diagnostics() function tcd.diagnostics()
for timer, entry in pairs(registry) do for timer, entry in pairs(registry) do
if entry.expiry < util.time_s() then if entry.expiry < util.time_s() then
local overtime = util.time_s() - entry.expiry local overtime = util.time_s() - entry.expiry
@ -82,4 +82,4 @@ function tcallbackdsp.diagnostics()
end end
end end
return tcallbackdsp return tcd

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

179
supervisor/databus.lua Normal file
View File

@ -0,0 +1,179 @@
--
-- Data Bus - Central Communication Linking for Supervisor Front Panel
--
local psil = require("scada-common.psil")
local util = require("scada-common.util")
local pgi = require("supervisor.panel.pgi")
-- nominal RTT is ping (0ms to 10ms usually) + 150ms for SV main loop tick
local WARN_RTT = 300 -- 2x as long as expected w/ 0 ping
local HIGH_RTT = 500 -- 3.33x as long as expected w/ 0 ping
local databus = {}
-- databus PSIL
databus.ps = psil.create()
-- call to toggle heartbeat signal
function databus.heartbeat() databus.ps.toggle("heartbeat") end
-- transmit firmware versions across the bus
---@param sv_v string supervisor version
---@param comms_v string comms version
function databus.tx_versions(sv_v, comms_v)
databus.ps.publish("version", sv_v)
databus.ps.publish("comms_version", comms_v)
end
-- transmit hardware status for modem connection state
---@param has_modem boolean
function databus.tx_hw_modem(has_modem)
databus.ps.publish("has_modem", has_modem)
end
-- transmit PLC firmware version and session connection state
---@param reactor_id integer reactor unit ID
---@param fw string firmware version
---@param s_addr integer PLC computer ID
function databus.tx_plc_connected(reactor_id, fw, s_addr)
databus.ps.publish("plc_" .. reactor_id .. "_fw", fw)
databus.ps.publish("plc_" .. reactor_id .. "_conn", true)
databus.ps.publish("plc_" .. reactor_id .. "_addr", util.sprintf("@% 4d", s_addr))
end
-- transmit PLC disconnected
---@param reactor_id integer reactor unit ID
function databus.tx_plc_disconnected(reactor_id)
databus.ps.publish("plc_" .. reactor_id .. "_fw", " ------- ")
databus.ps.publish("plc_" .. reactor_id .. "_conn", false)
databus.ps.publish("plc_" .. reactor_id .. "_addr", " --- ")
databus.ps.publish("plc_" .. reactor_id .. "_rtt", 0)
databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.lightGray)
end
-- transmit PLC session RTT
---@param reactor_id integer reactor unit ID
---@param rtt integer round trip time
function databus.tx_plc_rtt(reactor_id, rtt)
databus.ps.publish("plc_" .. reactor_id .. "_rtt", rtt)
if rtt > HIGH_RTT then
databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.red)
elseif rtt > WARN_RTT then
databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.yellow_hc)
else
databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.green)
end
end
-- transmit RTU firmware version and session connection state
---@param session_id integer RTU session
---@param fw string firmware version
---@param s_addr integer RTU computer ID
function databus.tx_rtu_connected(session_id, fw, s_addr)
databus.ps.publish("rtu_" .. session_id .. "_fw", fw)
databus.ps.publish("rtu_" .. session_id .. "_addr", util.sprintf("@ C% 3d", s_addr))
pgi.create_rtu_entry(session_id)
end
-- transmit RTU disconnected
---@param session_id integer RTU session
function databus.tx_rtu_disconnected(session_id)
pgi.delete_rtu_entry(session_id)
end
-- transmit RTU session RTT
---@param session_id integer RTU session
---@param rtt integer round trip time
function databus.tx_rtu_rtt(session_id, rtt)
databus.ps.publish("rtu_" .. session_id .. "_rtt", rtt)
if rtt > HIGH_RTT then
databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.red)
elseif rtt > WARN_RTT then
databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.yellow_hc)
else
databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.green)
end
end
-- transmit RTU session unit count
---@param session_id integer RTU session
---@param units integer unit count
function databus.tx_rtu_units(session_id, units)
databus.ps.publish("rtu_" .. session_id .. "_units", units)
end
-- transmit coordinator firmware version and session connection state
---@param fw string firmware version
---@param s_addr integer coordinator computer ID
function databus.tx_crd_connected(fw, s_addr)
databus.ps.publish("crd_fw", fw)
databus.ps.publish("crd_conn", true)
databus.ps.publish("crd_addr", tostring(s_addr))
end
-- transmit coordinator disconnected
function databus.tx_crd_disconnected()
databus.ps.publish("crd_fw", " ------- ")
databus.ps.publish("crd_conn", false)
databus.ps.publish("crd_addr", "---")
databus.ps.publish("crd_rtt", 0)
databus.ps.publish("crd_rtt_color", colors.lightGray)
end
-- transmit coordinator session RTT
---@param rtt integer round trip time
function databus.tx_crd_rtt(rtt)
databus.ps.publish("crd_rtt", rtt)
if rtt > HIGH_RTT then
databus.ps.publish("crd_rtt_color", colors.red)
elseif rtt > WARN_RTT then
databus.ps.publish("crd_rtt_color", colors.yellow_hc)
else
databus.ps.publish("crd_rtt_color", colors.green)
end
end
-- transmit PKT firmware version and PDG session connection state
---@param session_id integer PDG session
---@param fw string firmware version
---@param s_addr integer PDG computer ID
function databus.tx_pdg_connected(session_id, fw, s_addr)
databus.ps.publish("pdg_" .. session_id .. "_fw", fw)
databus.ps.publish("pdg_" .. session_id .. "_addr", util.sprintf("@ C% 3d", s_addr))
pgi.create_pdg_entry(session_id)
end
-- transmit PDG session disconnected
---@param session_id integer PDG session
function databus.tx_pdg_disconnected(session_id)
pgi.delete_pdg_entry(session_id)
end
-- transmit PDG session RTT
---@param session_id integer PDG session
---@param rtt integer round trip time
function databus.tx_pdg_rtt(session_id, rtt)
databus.ps.publish("pdg_" .. session_id .. "_rtt", rtt)
if rtt > HIGH_RTT then
databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.red)
elseif rtt > WARN_RTT then
databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.yellow_hc)
else
databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.green)
end
end
-- link a function to receive data from the bus
---@param field string field name
---@param func function function to link
function databus.rx_field(field, func)
databus.ps.subscribe(field, func)
end
return databus

View File

@ -128,7 +128,7 @@ function facility.new(num_reactors, cooling_conf)
for i = 1, #self.prio_defs do for i = 1, #self.prio_defs do
local units = self.prio_defs[i] local units = self.prio_defs[i]
for u = 1, #units do for u = 1, #units do
all_ramped = all_ramped and units[u].a_ramp_complete() all_ramped = all_ramped and units[u].auto_ramp_complete()
end end
end end
@ -159,7 +159,7 @@ function facility.new(num_reactors, cooling_conf)
local u = units[id] ---@type reactor_unit local u = units[id] ---@type reactor_unit
local ctl = u.get_control_inf() local ctl = u.get_control_inf()
local lim_br100 = u.a_get_effective_limit() local lim_br100 = u.auto_get_effective_limit()
if abort_on_fault and (lim_br100 ~= ctl.lim_br100) then if abort_on_fault and (lim_br100 ~= ctl.lim_br100) then
-- effective limit differs from set limit, unit is degraded -- effective limit differs from set limit, unit is degraded
@ -183,7 +183,7 @@ function facility.new(num_reactors, cooling_conf)
unallocated = math.max(0, unallocated - ctl.br100) unallocated = math.max(0, unallocated - ctl.br100)
if last ~= ctl.br100 then u.a_commit_br100(ramp) end if last ~= ctl.br100 then u.auto_commit_br100(ramp) end
end end
end end
end end
@ -320,7 +320,7 @@ function facility.new(num_reactors, cooling_conf)
self.start_fail = START_STATUS.BLADE_MISMATCH self.start_fail = START_STATUS.BLADE_MISMATCH
end end
if self.start_fail == START_STATUS.OK then u.a_engage() end if self.start_fail == START_STATUS.OK then u.auto_engage() end
self.max_burn_combined = self.max_burn_combined + (u.get_control_inf().lim_br100 / 100.0) self.max_burn_combined = self.max_burn_combined + (u.get_control_inf().lim_br100 / 100.0)
end end
@ -340,7 +340,7 @@ function facility.new(num_reactors, cooling_conf)
-- use manual SCRAM since inactive was requested, and automatic SCRAM trips an alarm -- use manual SCRAM since inactive was requested, and automatic SCRAM trips an alarm
for _, u in pairs(self.prio_defs[i]) do for _, u in pairs(self.prio_defs[i]) do
u.scram() u.scram()
u.a_disengage() u.auto_disengage()
end end
end end
@ -601,7 +601,7 @@ function facility.new(num_reactors, cooling_conf)
-- SCRAM all units -- SCRAM all units
for i = 1, #self.prio_defs do for i = 1, #self.prio_defs do
for _, u in pairs(self.prio_defs[i]) do for _, u in pairs(self.prio_defs[i]) do
u.a_scram() u.auto_scram()
end end
end end
@ -653,7 +653,7 @@ function facility.new(num_reactors, cooling_conf)
-- reset PLC RPS trips if we should -- reset PLC RPS trips if we should
for i = 1, #self.units do for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit local u = self.units[i] ---@type reactor_unit
u.a_cond_rps_reset() u.auto_cond_rps_reset()
end end
end end
end end

View File

@ -0,0 +1,46 @@
--
-- Pocket Diagnostics Connection Entry
--
local databus = require("supervisor.databus")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data")
local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.cpair
-- create a pocket diagnostics list entry
---@param parent graphics_element parent
---@param id integer PDG session ID
local function init(parent, id)
-- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)}
local ps_prefix = "pdg_" .. id .. "_"
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
local pdg_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)}
TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
pdg_addr.register(databus.ps, ps_prefix .. "addr", pdg_addr.set_value)
TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1}
local pdg_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
pdg_fw_v.register(databus.ps, ps_prefix .. "fw", pdg_fw_v.set_value)
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1}
local pdg_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
pdg_rtt.register(databus.ps, ps_prefix .. "rtt", pdg_rtt.update)
pdg_rtt.register(databus.ps, ps_prefix .. "rtt_color", pdg_rtt.recolor)
return root
end
return init

View File

@ -0,0 +1,50 @@
--
-- RTU Connection Entry
--
local databus = require("supervisor.databus")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data")
local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.cpair
-- create an RTU list entry
---@param parent graphics_element parent
---@param id integer RTU session ID
local function init(parent, id)
-- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)}
local ps_prefix = "rtu_" .. id .. "_"
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
local rtu_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)}
TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
rtu_addr.register(databus.ps, ps_prefix .. "addr", rtu_addr.set_value)
TextBox{parent=entry,x=10,y=2,text="UNITS:",width=7,height=1}
local unit_count = DataIndicator{parent=entry,x=17,y=2,label="",unit="",format="%2d",value=0,width=2,fg_bg=cpair(colors.gray,colors.white)}
unit_count.register(databus.ps, ps_prefix .. "units", unit_count.set_value)
TextBox{parent=entry,x=21,y=2,text="FW:",width=3,height=1}
local rtu_fw_v = TextBox{parent=entry,x=25,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
rtu_fw_v.register(databus.ps, ps_prefix .. "fw", rtu_fw_v.set_value)
TextBox{parent=entry,x=36,y=2,text="RTT:",width=4,height=1}
local rtu_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
rtu_rtt.register(databus.ps, ps_prefix .. "rtt", rtu_rtt.update)
rtu_rtt.register(databus.ps, ps_prefix .. "rtt_color", rtu_rtt.recolor)
return root
end
return init

View File

@ -0,0 +1,164 @@
--
-- Main SCADA Coordinator GUI
--
local util = require("scada-common.util")
local config = require("supervisor.config")
local databus = require("supervisor.databus")
local pgi = require("supervisor.panel.pgi")
local style = require("supervisor.panel.style")
local pdg_entry = require("supervisor.panel.components.pdg_entry")
local rtu_entry = require("supervisor.panel.components.rtu_entry")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local TabBar = require("graphics.elements.controls.tabbar")
local LED = require("graphics.elements.indicators.led")
local DataIndicator = require("graphics.elements.indicators.data")
local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.cpair
-- create new main view
---@param panel graphics_element main displaybox
local function init(panel)
TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
local page_div = Div{parent=panel,x=1,y=3}
--
-- system indicators
--
local main_page = Div{parent=page_div,x=1,y=1}
local system = Div{parent=main_page,width=14,height=17,x=2,y=2}
local on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)}
on.update(true)
system.line_break()
heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)}
system.line_break()
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
--
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=cpair(colors.lightGray,colors.ivory)}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
--
-- page handling
--
-- plc page
local plc_page = Div{parent=page_div,x=1,y=1,hidden=true}
local plc_list = Div{parent=plc_page,x=2,y=2,width=49}
for i = 1, config.NUM_REACTORS do
local ps_prefix = "plc_" .. i .. "_"
local plc_entry = Div{parent=plc_list,height=3,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=plc_entry,x=1,y=1,text="",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)}
local conn = LED{parent=plc_entry,x=10,y=2,label="LINK",colors=cpair(colors.green,colors.green_off)}
conn.register(databus.ps, ps_prefix .. "conn", conn.update)
local plc_addr = TextBox{parent=plc_entry,x=17,y=2,text=" --- ",width=5,height=1,fg_bg=cpair(colors.gray,colors.white)}
plc_addr.register(databus.ps, ps_prefix .. "addr", plc_addr.set_value)
TextBox{parent=plc_entry,x=23,y=2,text="FW:",width=3,height=1}
local plc_fw_v = TextBox{parent=plc_entry,x=27,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
plc_fw_v.register(databus.ps, ps_prefix .. "fw", plc_fw_v.set_value)
TextBox{parent=plc_entry,x=37,y=2,text="RTT:",width=4,height=1}
local plc_rtt = DataIndicator{parent=plc_entry,x=42,y=2,label="",unit="",format="%4d",value=0,width=4,fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=plc_entry,x=47,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
plc_rtt.register(databus.ps, ps_prefix .. "rtt", plc_rtt.update)
plc_rtt.register(databus.ps, ps_prefix .. "rtt_color", plc_rtt.recolor)
plc_list.line_break()
end
-- rtu page
local rtu_page = Div{parent=page_div,x=1,y=1,hidden=true}
local rtu_list = ListBox{parent=rtu_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=rtu_list,height=1,hidden=true} -- padding
-- coordinator page
local crd_page = Div{parent=page_div,x=1,y=1,hidden=true}
local crd_box = Div{parent=crd_page,x=2,y=2,width=49,height=4,fg_bg=cpair(colors.black,colors.white)}
local crd_conn = LED{parent=crd_box,x=2,y=2,label="CONNECTION",colors=cpair(colors.green,colors.green_off)}
crd_conn.register(databus.ps, "crd_conn", crd_conn.update)
TextBox{parent=crd_box,x=4,y=3,text="COMPUTER",width=8,height=1,fg_bg=cpair(colors.gray,colors.white)}
local crd_addr = TextBox{parent=crd_box,x=13,y=3,text="---",width=5,height=1,fg_bg=cpair(colors.gray,colors.white)}
crd_addr.register(databus.ps, "crd_addr", crd_addr.set_value)
TextBox{parent=crd_box,x=22,y=2,text="FW:",width=3,height=1}
local crd_fw_v = TextBox{parent=crd_box,x=26,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
crd_fw_v.register(databus.ps, "crd_fw", crd_fw_v.set_value)
TextBox{parent=crd_box,x=36,y=2,text="RTT:",width=4,height=1}
local crd_rtt = DataIndicator{parent=crd_box,x=41,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=crd_box,x=47,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
crd_rtt.register(databus.ps, "crd_rtt", crd_rtt.update)
crd_rtt.register(databus.ps, "crd_rtt_color", crd_rtt.recolor)
-- pocket diagnostics page
local pkt_page = Div{parent=page_div,x=1,y=1,hidden=true}
local pdg_list = ListBox{parent=pkt_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=pdg_list,height=1,hidden=true} -- padding
-- assemble page panes
local panes = { main_page, plc_page, rtu_page, crd_page, pkt_page }
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
local tabs = {
{ name = "SVR", color = cpair(colors.black, colors.ivory) },
{ name = "PLC", color = cpair(colors.black, colors.ivory) },
{ name = "RTU", color = cpair(colors.black, colors.ivory) },
{ name = "CRD", color = cpair(colors.black, colors.ivory) },
{ name = "PKT", color = cpair(colors.black, colors.ivory) },
}
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=cpair(colors.black,colors.white)}
-- link RTU/PDG list management to PGI
pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry)
end
return init

93
supervisor/panel/pgi.lua Normal file
View File

@ -0,0 +1,93 @@
--
-- Protected Graphics Interface
--
local log = require("scada-common.log")
local util = require("scada-common.util")
local pgi = {}
local data = {
rtu_list = nil, ---@type nil|graphics_element
pdg_list = nil, ---@type nil|graphics_element
rtu_entry = nil, ---@type function
pdg_entry = nil, ---@type function
-- session entries
s_entries = { rtu = {}, pdg = {} }
}
-- link list boxes
---@param rtu_list graphics_element RTU list element
---@param rtu_entry function RTU entry constructor
---@param pdg_list graphics_element pocket diagnostics list element
---@param pdg_entry function pocket diagnostics entry constructor
function pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry)
data.rtu_list = rtu_list
data.pdg_list = pdg_list
data.rtu_entry = rtu_entry
data.pdg_entry = pdg_entry
end
-- unlink all fields, disabling the PGI
function pgi.unlink()
data.rtu_list = nil
data.pdg_list = nil
data.rtu_entry = nil
data.pdg_entry = nil
end
-- add an RTU entry to the RTU list
---@param session_id integer RTU session
function pgi.create_rtu_entry(session_id)
if data.rtu_list ~= nil and data.rtu_entry ~= nil then
local success, result = pcall(data.rtu_entry, data.rtu_list, session_id)
if success then
data.s_entries.rtu[session_id] = result
else
log.error(util.c("PGI: failed to create RTU entry (", result, ")"), true)
end
end
end
-- delete an RTU entry from the RTU list
---@param session_id integer RTU session
function pgi.delete_rtu_entry(session_id)
if data.s_entries.rtu[session_id] ~= nil then
local success, result = pcall(data.s_entries.rtu[session_id].delete)
data.s_entries.rtu[session_id] = nil
if not success then
log.error(util.c("PGI: failed to delete RTU entry (", result, ")"), true)
end
end
end
-- add a PDG entry to the PDG list
---@param session_id integer pocket diagnostics session
function pgi.create_pdg_entry(session_id)
if data.pdg_list ~= nil and data.pdg_entry ~= nil then
local success, result = pcall(data.pdg_entry, data.pdg_list, session_id)
if success then
data.s_entries.pdg[session_id] = result
else
log.error(util.c("PGI: failed to create PDG entry (", result, ")"), true)
end
end
end
-- delete a PDG entry from the PDG list
---@param session_id integer pocket diagnostics session
function pgi.delete_pdg_entry(session_id)
if data.s_entries.pdg[session_id] ~= nil then
local success, result = pcall(data.s_entries.pdg[session_id].delete)
data.s_entries.pdg[session_id] = nil
if not success then
log.error(util.c("PGI: failed to delete PDG entry (", result, ")"), true)
end
end
end
return pgi

View File

@ -0,0 +1,42 @@
--
-- Graphics Style Options
--
local core = require("graphics.core")
local style = {}
local cpair = core.cpair
-- GLOBAL --
-- remap global colors
colors.ivory = colors.pink
colors.yellow_hc = colors.purple
colors.red_off = colors.brown
colors.yellow_off = colors.magenta
colors.green_off = colors.lime
style.root = cpair(colors.black, colors.ivory)
style.header = cpair(colors.black, colors.lightGray)
style.colors = {
{ c = colors.red, hex = 0xdf4949 }, -- RED ON
{ c = colors.orange, hex = 0xffb659 },
{ c = colors.yellow, hex = 0xf9fb53 }, -- YELLOW ON
{ c = colors.lime, hex = 0x16665a }, -- GREEN OFF
{ c = colors.green, hex = 0x6be551 }, -- GREEN ON
{ c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0008fe }, -- LCD BLUE
{ c = colors.purple, hex = 0xe3bc2a }, -- YELLOW HIGH CONTRAST
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
-- { c = colors.white, hex = 0xdcd9ca },
{ c = colors.lightGray, hex = 0xb1b8b3 },
{ c = colors.gray, hex = 0x575757 },
-- { c = colors.black, hex = 0x191919 },
{ c = colors.brown, hex = 0x672223 } -- RED OFF
}
return style

84
supervisor/renderer.lua Normal file
View File

@ -0,0 +1,84 @@
--
-- Graphics Rendering Control
--
local panel_view = require("supervisor.panel.front_panel")
local pgi = require("supervisor.panel.pgi")
local style = require("supervisor.panel.style")
local flasher = require("graphics.flasher")
local DisplayBox = require("graphics.elements.displaybox")
local renderer = {}
local ui = {
display = nil
}
-- start the UI
function renderer.start_ui()
if ui.display == nil then
-- reset terminal
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
-- set overridden colors
for i = 1, #style.colors do
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
end
-- init front panel view
ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
panel_view(ui.display)
-- start flasher callback task
flasher.run()
end
end
-- close out the UI
function renderer.close_ui()
if ui.display ~= nil then
-- stop blinking indicators
flasher.clear()
-- disable PGI
pgi.unlink()
-- hide to stop animation callbacks
ui.display.hide()
-- clear root UI elements
ui.display = nil
-- restore colors
for i = 1, #style.colors do
local r, g, b = term.nativePaletteColor(style.colors[i].c)
term.setPaletteColor(style.colors[i].c, r, g, b)
end
-- reset terminal
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
end
end
-- is the UI ready?
---@nodiscard
---@return boolean ready
function renderer.ui_ready() return ui.display ~= nil end
-- handle a mouse event
---@param event mouse_interaction|nil
function renderer.handle_mouse(event)
if ui.display ~= nil and event ~= nil then
ui.display.handle_mouse(event)
end
end
return renderer

View File

@ -4,6 +4,8 @@ local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local databus = require("supervisor.databus")
local svqtypes = require("supervisor.session.svqtypes") local svqtypes = require("supervisor.session.svqtypes")
local coordinator = {} local coordinator = {}
@ -18,8 +20,6 @@ local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local SV_Q_DATA = svqtypes.SV_Q_DATA local SV_Q_DATA = svqtypes.SV_Q_DATA
local println = util.println
-- retry time constants in ms -- retry time constants in ms
-- local INITIAL_WAIT = 1500 -- local INITIAL_WAIT = 1500
local RETRY_PERIOD = 1000 local RETRY_PERIOD = 1000
@ -45,11 +45,16 @@ 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
function coordinator.new_session(id, in_queue, out_queue, timeout, facility) ---@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)
-- 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 log_header = "crdn_session(" .. id .. "): " local log_header = "crdn_session(" .. id .. "): "
local self = { local self = {
@ -84,6 +89,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
local function _close() local function _close()
self.conn_watchdog.cancel() self.conn_watchdog.cancel()
self.connected = false self.connected = false
databus.tx_crd_disconnected()
end end
-- send a CRDN packet -- send a CRDN packet
@ -94,7 +100,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
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
@ -108,7 +114,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
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
@ -205,6 +211,8 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
-- log.debug(log_header .. "COORD RTT = " .. self.last_rtt .. "ms") -- log.debug(log_header .. "COORD RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "COORD TT = " .. (srv_now - coord_send) .. "ms") -- log.debug(log_header .. "COORD TT = " .. (srv_now - coord_send) .. "ms")
databus.tx_crd_rtt(self.last_rtt)
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_header .. "SCADA keep alive packet length mismatch")
end end
@ -327,7 +335,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
end end
end end
---@class coord_session ---@class crd_session
local public = {} local public = {}
-- get the session ID -- get the session ID

View File

@ -4,6 +4,8 @@ local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local databus = require("supervisor.databus")
local svqtypes = require("supervisor.session.svqtypes") local svqtypes = require("supervisor.session.svqtypes")
local plc = {} local plc = {}
@ -14,8 +16,6 @@ local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local PLC_AUTO_ACK = comms.PLC_AUTO_ACK local PLC_AUTO_ACK = comms.PLC_AUTO_ACK
local UNIT_COMMAND = comms.UNIT_COMMAND local UNIT_COMMAND = comms.UNIT_COMMAND
local println = util.println
-- retry time constants in ms -- retry time constants in ms
local INITIAL_WAIT = 1500 local INITIAL_WAIT = 1500
local INITIAL_AUTO_WAIT = 1000 local INITIAL_AUTO_WAIT = 1000
@ -45,11 +45,16 @@ 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
function plc.new_session(id, reactor_id, in_queue, out_queue, 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)
-- 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 log_header = "plc_session(" .. id .. "): " local log_header = "plc_session(" .. id .. "): "
local self = { local self = {
@ -235,6 +240,7 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
local function _close() local function _close()
self.conn_watchdog.cancel() self.conn_watchdog.cancel()
self.connected = false self.connected = false
databus.tx_plc_disconnected(reactor_id)
end end
-- send an RPLC packet -- send an RPLC packet
@ -245,7 +251,7 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
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
@ -259,7 +265,7 @@ function plc.new_session(id, reactor_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
@ -485,6 +491,8 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
-- log.debug(log_header .. "PLC RTT = " .. self.last_rtt .. "ms") -- log.debug(log_header .. "PLC RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "PLC TT = " .. (srv_now - plc_send) .. "ms") -- log.debug(log_header .. "PLC TT = " .. (srv_now - plc_send) .. "ms")
databus.tx_plc_rtt(reactor_id, self.last_rtt)
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_header .. "SCADA keep alive packet length mismatch")
end end

View File

@ -1,15 +1,14 @@
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local log = require("scada-common.log") 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 databus = require("supervisor.databus")
local pocket = {} local pocket = {}
local PROTOCOL = comms.PROTOCOL local PROTOCOL = comms.PROTOCOL
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local println = util.println
-- retry time constants in ms -- retry time constants in ms
-- local INITIAL_WAIT = 1500 -- local INITIAL_WAIT = 1500
-- local RETRY_PERIOD = 1000 -- local RETRY_PERIOD = 1000
@ -30,11 +29,16 @@ 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
function pocket.new_session(id, in_queue, out_queue, timeout) ---@param fp_ok boolean if the front panel UI is running
local log_header = "diag_session(" .. id .. "): " function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, fp_ok)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end
local log_header = "pdg_session(" .. id .. "): "
local self = { local self = {
-- connection properties -- connection properties
@ -55,18 +59,19 @@ function pocket.new_session(id, in_queue, out_queue, timeout)
acks = { acks = {
}, },
-- session database -- session database
---@class diag_db ---@class pdg_db
sDB = { sDB = {
} }
} }
---@class diag_session ---@class pdg_session
local public = {} local public = {}
-- mark this diagnostics session as closed, stop watchdog -- mark this diagnostics 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
databus.tx_pdg_disconnected(id)
end end
-- send a SCADA management packet -- send a SCADA management packet
@ -77,7 +82,7 @@ function pocket.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
@ -106,16 +111,18 @@ function pocket.new_session(id, in_queue, out_queue, timeout)
-- keep alive reply -- keep alive reply
if pkt.length == 2 then if pkt.length == 2 then
local srv_start = pkt.data[1] local srv_start = pkt.data[1]
-- local diag_send = pkt.data[2] -- local pdg_send = pkt.data[2]
local srv_now = util.time() local srv_now = util.time()
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 .. "DIAG KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)") log.warning(log_header .. "PDG KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
end end
-- log.debug(log_header .. "DIAG RTT = " .. self.last_rtt .. "ms") -- log.debug(log_header .. "PDG RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "DIAG TT = " .. (srv_now - diag_send) .. "ms") -- log.debug(log_header .. "PDG TT = " .. (srv_now - pdg_send) .. "ms")
databus.tx_pdg_rtt(id, self.last_rtt)
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_header .. "SCADA keep alive packet length mismatch")
end end

View File

@ -4,6 +4,8 @@ local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local databus = require("supervisor.databus")
local svqtypes = require("supervisor.session.svqtypes") local svqtypes = require("supervisor.session.svqtypes")
-- supervisor rtu sessions (svrs) -- supervisor rtu sessions (svrs)
@ -22,8 +24,6 @@ local PROTOCOL = comms.PROTOCOL
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local println = util.println
local PERIODICS = { local PERIODICS = {
KEEP_ALIVE = 2000 KEEP_ALIVE = 2000
} }
@ -31,12 +31,17 @@ 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
function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facility) ---@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)
-- 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 log_header = "rtu_session(" .. id .. "): " local log_header = "rtu_session(" .. id .. "): "
local self = { local self = {
@ -66,6 +71,8 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
-- parse the recorded advertisement and create unit sub-sessions -- parse the recorded advertisement and create unit sub-sessions
local function _handle_advertisement() local function _handle_advertisement()
local unit_count = 0
_reset_config() _reset_config()
for i = 1, #self.fac_units do for i = 1, #self.fac_units do
@ -172,18 +179,22 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
if unit ~= nil then if unit ~= nil then
self.units[i] = unit self.units[i] = unit
unit_count = unit_count + 1
elseif u_type ~= RTU_UNIT_TYPE.VIRTUAL then elseif u_type ~= RTU_UNIT_TYPE.VIRTUAL then
_reset_config() _reset_config()
log.error(util.c(log_header, "bad advertisement: error occured while creating a unit (type is ", type_string, ")")) log.error(util.c(log_header, "bad advertisement: error occured while creating a unit (type is ", type_string, ")"))
break break
end end
end end
databus.tx_rtu_units(id, unit_count)
end end
-- mark this RTU session as closed, stop watchdog -- mark this RTU 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
databus.tx_rtu_disconnected(id)
-- mark all RTU unit sessions as closed so the reactor unit knows -- mark all RTU unit sessions as closed so the reactor unit knows
for _, unit in pairs(self.units) do unit.close() end for _, unit in pairs(self.units) do unit.close() end
@ -194,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
@ -208,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
@ -254,6 +265,8 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
-- log.debug(log_header .. "RTU RTT = " .. self.last_rtt .. "ms") -- log.debug(log_header .. "RTU RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "RTU TT = " .. (srv_now - rtu_send) .. "ms") -- log.debug(log_header .. "RTU TT = " .. (srv_now - rtu_send) .. "ms")
databus.tx_rtu_rtt(id, self.last_rtt)
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_header .. "SCADA keep alive packet length mismatch")
end end

View File

@ -3,6 +3,7 @@ local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util") local util = require("scada-common.util")
local config = require("supervisor.config") local config = require("supervisor.config")
local databus = require("supervisor.databus")
local facility = require("supervisor.facility") local facility = require("supervisor.facility")
local svqtypes = require("supervisor.session.svqtypes") local svqtypes = require("supervisor.session.svqtypes")
@ -22,24 +23,26 @@ local CRD_S_DATA = coordinator.CRD_S_DATA
local svsessions = {} local svsessions = {}
---@enum SESSION_TYPE
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
DIAG_SESSION = 3 -- pocket diagnostics PDG_SESSION = 3 -- pocket diagnostics
} }
svsessions.SESSION_TYPE = SESSION_TYPE svsessions.SESSION_TYPE = SESSION_TYPE
local self = { local self = {
modem = nil, ---@type table|nil modem = nil, ---@type table|nil
fp_ok = false,
num_reactors = 0, num_reactors = 0,
facility = nil, ---@type facility|nil facility = nil, ---@type facility|nil
sessions = { rtu = {}, plc = {}, coord = {}, diag = {} }, sessions = { rtu = {}, plc = {}, crd = {}, pdg = {} },
next_ids = { rtu = 0, plc = 0, coord = 0, diag = 0 } next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 }
} }
---@alias sv_session_structs plc_session_struct|rtu_session_struct|coord_session_struct|diag_session_struct ---@alias sv_session_structs plc_session_struct|rtu_session_struct|crd_session_struct|pdg_session_struct
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
@ -57,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
@ -78,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
@ -101,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
@ -128,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
@ -157,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
@ -172,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
@ -194,11 +195,13 @@ end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
-- initialize svsessions -- initialize svsessions
---@param modem table ---@param modem table modem device
---@param num_reactors integer ---@param fp_ok boolean front panel active
---@param cooling_conf table ---@param num_reactors integer number of reactors
function svsessions.init(modem, num_reactors, cooling_conf) ---@param cooling_conf table cooling configuration definition
function svsessions.init(modem, fp_ok, num_reactors, cooling_conf)
self.modem = modem self.modem = modem
self.fp_ok = fp_ok
self.num_reactors = num_reactors self.num_reactors = num_reactors
self.facility = facility.new(num_reactors, cooling_conf) self.facility = facility.new(num_reactors, cooling_conf)
end end
@ -209,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|diag_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.diag, remote_port) end local session = _find_session(self.sessions.pdg, source_addr)
---@cast session coord_session_struct|diag_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
@ -286,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 = {
@ -299,22 +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, config.PLC_TIMEOUT) local id = self.next_ids.plc
plc_s.instance = plc.new_session(id, source_addr, for_reactor, plc_s.in_queue, plc_s.out_queue,
config.PLC_TIMEOUT, self.fp_ok)
table.insert(self.sessions.plc, plc_s) 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, " for reactor ", for_reactor)) local mt = {
---@param s plc_session_struct
__tostring = function (s) return util.c("PLC [", s.instance.get_id(), "] for reactor #", s.reactor,
" (@", s.s_addr, ")") end
}
self.next_ids.plc = self.next_ids.plc + 1 setmetatable(plc_s, mt)
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()
@ -326,64 +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, self.facility) local id = self.next_ids.rtu
rtu_s.instance = rtu.new_session(id, source_addr, rtu_s.in_queue, rtu_s.out_queue, config.RTU_TIMEOUT,
advertisement, self.facility, self.fp_ok)
table.insert(self.sessions.rtu, rtu_s) 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
}
self.next_ids.rtu = self.next_ids.rtu + 1 setmetatable(rtu_s, mt)
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, self.facility) local id = self.next_ids.crd
table.insert(self.sessions.coord, coord_s)
log.debug("established new coordinator session to " .. remote_port .. " with ID " .. self.next_ids.coord) crd_s.instance = coordinator.new_session(id, source_addr, crd_s.in_queue, crd_s.out_queue, config.CRD_TIMEOUT,
self.facility, self.fp_ok)
table.insert(self.sessions.crd, crd_s)
self.next_ids.coord = self.next_ids.coord + 1 local mt = {
---@param s crd_session_struct
__tostring = function (s) return util.c("CRD [", s.instance.get_id(), "] (@", s.s_addr, ")") end
}
setmetatable(crd_s, mt)
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
@ -392,32 +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_diag_session(local_port, remote_port, version) function svsessions.establish_pdg_session(source_addr, version)
---@class diag_session_struct ---@class pdg_session_struct
local diag_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 diag_session instance = nil ---@type pdg_session
} }
diag_s.instance = pocket.new_session(self.next_ids.diag, diag_s.in_queue, diag_s.out_queue, config.PKT_TIMEOUT) local id = self.next_ids.pdg
table.insert(self.sessions.diag, diag_s)
log.debug("established new pocket diagnostics session to " .. remote_port .. " with ID " .. self.next_ids.diag) 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)
self.next_ids.diag = self.next_ids.diag + 1 local mt = {
---@param s pdg_session_struct
__tostring = function (s) return util.c("PDG [", s.instance.get_id(), "] (@", s.s_addr, ")") end
}
setmetatable(pdg_s, mt)
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 diag_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
@ -449,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

@ -5,16 +5,22 @@
require("/initenv").init_env() require("/initenv").init_env()
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core")
local config = require("supervisor.config") local config = require("supervisor.config")
local databus = require("supervisor.databus")
local renderer = require("supervisor.renderer")
local supervisor = require("supervisor.supervisor") local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v0.15.9" 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
@ -25,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)
@ -79,6 +88,9 @@ local function main()
-- startup -- startup
---------------------------------------- ----------------------------------------
-- record firmware versions and ID
databus.tx_versions(SUPERVISOR_VERSION, comms.version)
-- mount connected devices -- mount connected devices
ppm.mount_all() ppm.mount_all()
@ -89,9 +101,22 @@ local function main()
return return
end end
-- start comms, open all channels databus.tx_hw_modem(true)
local superv_comms = supervisor.comms(SUPERVISOR_VERSION, config.NUM_REACTORS, config.REACTOR_COOLING, modem,
config.SCADA_DEV_LISTEN, config.SCADA_SV_CTL_LISTEN, config.TRUSTED_RANGE) -- start UI
local fp_ok, message = pcall(renderer.start_ui)
if not fp_ok then
renderer.close_ui()
println_ts(util.c("UI error: ", message))
log.error(util.c("GUI crashed with error ", message))
else
-- redefine println_ts local to not print as we have the front panel running
println_ts = function (_) end
end
-- start comms
local superv_comms = supervisor.comms(SUPERVISOR_VERSION, modem, 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
@ -100,6 +125,9 @@ local function main()
-- start clock -- start clock
loop_clock.start() loop_clock.start()
-- halve the rate heartbeat LED flash
local heartbeat_toggle = true
-- event loop -- event loop
while true do while true do
local event, param1, param2, param3, param4, param5 = util.pull_event() local event, param1, param2, param3, param4, param5 = util.pull_event()
@ -114,6 +142,7 @@ local function main()
if device == modem then if device == modem then
println_ts("wireless modem disconnected!") println_ts("wireless modem disconnected!")
log.warning("comms modem disconnected") log.warning("comms modem disconnected")
databus.tx_hw_modem(false)
else else
log.warning("non-comms modem disconnected") log.warning("non-comms modem disconnected")
end end
@ -131,6 +160,8 @@ local function main()
println_ts("wireless modem reconnected.") println_ts("wireless modem reconnected.")
log.info("comms modem reconnected") log.info("comms modem reconnected")
databus.tx_hw_modem(true)
else else
log.info("wired modem reconnected") log.info("wired modem reconnected")
end end
@ -139,6 +170,9 @@ local function main()
elseif event == "timer" and loop_clock.is_clock(param1) then elseif event == "timer" and loop_clock.is_clock(param1) then
-- main loop tick -- main loop tick
if heartbeat_toggle then databus.heartbeat() end
heartbeat_toggle = not heartbeat_toggle
-- iterate sessions -- iterate sessions
svsessions.iterate_all() svsessions.iterate_all()
@ -149,10 +183,16 @@ local function main()
elseif event == "timer" then elseif event == "timer" then
-- a non-clock timer event, check watchdogs -- a non-clock timer event, check watchdogs
svsessions.check_all_watchdogs(param1) svsessions.check_all_watchdogs(param1)
-- notify timer callback dispatcher
tcd.handle(param1)
elseif event == "modem_message" then elseif event == "modem_message" then
-- got a packet -- got a packet
local packet = superv_comms.parse_packet(param1, param2, param3, param4, param5) local packet = superv_comms.parse_packet(param1, param2, param3, param4, param5)
superv_comms.handle_packet(packet) superv_comms.handle_packet(packet)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then
-- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
end end
-- check for termination request -- check for termination request
@ -165,8 +205,15 @@ local function main()
end end
end end
println_ts("exited") renderer.close_ui()
util.println_ts("exited")
log.info("exited") log.info("exited")
end end
if not xpcall(main, crash.handler) then crash.exit() else log.close() end if not xpcall(main, crash.handler) then
pcall(renderer.close_ui)
crash.exit()
else
log.close()
end

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 = {}
@ -11,64 +13,59 @@ local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK local ESTABLISH_ACK = comms.ESTABLISH_ACK
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local println = util.println
-- 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 fp_ok boolean if the front panel UI is running
---@param svctl_listen integer listening port for supervisor access
---@param range integer trusted device connection range
---@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) function supervisor.comms(_version, modem, fp_ok)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end
-- channel list from config
local svr_channel = config.SVR_CHANNEL
local plc_channel = config.PLC_CHANNEL
local rtu_channel = config.RTU_CHANNEL
local crd_channel = config.CRD_CHANNEL
local pkt_channel = config.PKT_CHANNEL
-- configuration data
local num_reactors = config.NUM_REACTORS
local cooling_conf = config.REACTOR_COOLING
local self = { 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()
-- link modem to svsessions -- pass modem, status, and config data to svsessions
svsessions.init(modem, 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 --
@ -136,17 +133,94 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste
---@param packet modbus_frame|rplc_frame|mgmt_frame|crdn_frame|nil ---@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)
elseif r_chan == plc_channel then
-- look for an associated session
local session = svsessions.find_plc_session(src_addr)
if protocol == PROTOCOL.RPLC then
---@cast packet rplc_frame
-- reactor PLC packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- unknown session, force a re-link
log.debug("PLC_ESTABLISH: no session but not an establish, forcing relink")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping PLC establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.PLC then
-- PLC linking request
if packet.length == 4 and type(packet.data[4]) == "number" then
local reactor_id = packet.data[4]
local plc_id = svsessions.establish_plc_session(src_addr, reactor_id, firmware_v)
if plc_id == false then
-- reactor already has a PLC assigned
if last_ack ~= ESTABLISH_ACK.COLLISION then
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION)
else
-- got an ID; assigned to a reactor successfully
println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \xbb reactor ", reactor_id, " connected"))
log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [@", src_addr, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
end
else
log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on PLC channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug("invalid establish packet (on PLC channel)")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding PLC SCADA_MGMT packet without a known session from computer ", src_addr))
end
else
log.debug(util.c("illegal packet type ", protocol, " on PLC channel"))
end
elseif r_chan == rtu_channel then
-- look for an associated session
local session = svsessions.find_rtu_session(src_addr)
if protocol == PROTOCOL.MODBUS_TCP then if protocol == PROTOCOL.MODBUS_TCP then
---@cast packet modbus_frame ---@cast packet modbus_frame
-- look for an associated session
local session = svsessions.find_rtu_session(r_port)
-- MODBUS response -- MODBUS response
if session ~= nil then if session ~= nil then
-- pass the packet onto the session handler -- pass the packet onto the session handler
@ -155,105 +229,59 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste
-- any other packet should be session related, discard it -- any other packet should be session related, discard it
log.debug("discarding MODBUS_TCP packet without a known session") log.debug("discarding MODBUS_TCP packet without a known session")
end end
elseif protocol == PROTOCOL.RPLC then
---@cast packet rplc_frame
-- look for an associated session
local session = svsessions.find_plc_session(r_port)
-- reactor PLC packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- unknown session, force a re-link
log.debug("PLC_ESTABLISH: no session but not an establish, forcing relink")
_send_dev_establish(packet.scada_frame.seq_num() + 1, r_port, { ESTABLISH_ACK.DENY })
end
elseif protocol == PROTOCOL.SCADA_MGMT then 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
local comms_v = packet.data[1] local comms_v = packet.data[1]
local firmware_v = packet.data[2] local firmware_v = packet.data[2]
local dev_type = packet.data[3] local dev_type = packet.data[3]
if comms_v ~= comms.version then if 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 RTU 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
-- PLC linking request
if packet.length == 4 and type(packet.data[4]) == "number" then
local reactor_id = packet.data[4]
local plc_id = svsessions.establish_plc_session(l_port, r_port, reactor_id, firmware_v)
if plc_id == false then
-- reactor already has a PLC assigned
if self.last_est_acks[r_port] ~= ESTABLISH_ACK.COLLISION then
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
self.last_est_acks[r_port] = ESTABLISH_ACK.COLLISION
end
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION })
else
-- got an ID; assigned to a reactor successfully
println(util.c("PLC (", firmware_v, ") [:", r_port, "] \xbb reactor ", reactor_id, " connected"))
log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [:", r_port, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id))
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW })
self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW
end
else
log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type")
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY })
end
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
@ -263,65 +291,53 @@ 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
local comms_v = packet.data[1] local comms_v = packet.data[1]
local firmware_v = packet.data[2] local firmware_v = packet.data[2]
local dev_type = packet.data[3] local dev_type = packet.data[3]
if comms_v ~= comms.version then if 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 end
elseif dev_type == DEVICE_TYPE.PKT then
-- this is an attempt to establish a new pocket diagnostic session
local s_id = svsessions.establish_diag_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 else
log.debug(util.c("illegal establish packet for device ", dev_type, " on SVCTL listening channel")) 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 end
else else
log.debug("SVCTL_ESTABLISH: establish packet length mismatch") log.debug("CRD_ESTABLISH: establish packet length mismatch")
_send_svctl_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(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
@ -331,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
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 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

@ -506,7 +506,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
--#region --#region
-- engage automatic control -- engage automatic control
function public.a_engage() function public.auto_engage()
self.auto_engaged = true self.auto_engaged = true
if self.plc_i ~= nil then if self.plc_i ~= nil then
self.plc_i.auto_lock(true) self.plc_i.auto_lock(true)
@ -514,7 +514,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
end end
-- disengage automatic control -- disengage automatic control
function public.a_disengage() function public.auto_disengage()
self.auto_engaged = false self.auto_engaged = false
if self.plc_i ~= nil then if self.plc_i ~= nil then
self.plc_i.auto_lock(false) self.plc_i.auto_lock(false)
@ -526,7 +526,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
-- if it is degraded or not ready, the limit will be 0 -- if it is degraded or not ready, the limit will be 0
---@nodiscard ---@nodiscard
---@return integer lim_br100 ---@return integer lim_br100
function public.a_get_effective_limit() function public.auto_get_effective_limit()
if (not self.db.control.ready) or self.db.control.degraded or self.plc_cache.rps_trip then if (not self.db.control.ready) or self.db.control.degraded or self.plc_cache.rps_trip then
self.db.control.br100 = 0 self.db.control.br100 = 0
return 0 return 0
@ -537,7 +537,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
-- set the automatic burn rate based on the last set burn rate in 100ths -- set the automatic burn rate based on the last set burn rate in 100ths
---@param ramp boolean true to ramp to rate, false to set right away ---@param ramp boolean true to ramp to rate, false to set right away
function public.a_commit_br100(ramp) function public.auto_commit_br100(ramp)
if self.auto_engaged then if self.auto_engaged then
if self.plc_i ~= nil then if self.plc_i ~= nil then
self.plc_i.auto_set_burn(self.db.control.br100 / 100, ramp) self.plc_i.auto_set_burn(self.db.control.br100 / 100, ramp)
@ -550,16 +550,16 @@ function unit.new(reactor_id, num_boilers, num_turbines)
-- check if ramping is complete (burn rate is same as target) -- check if ramping is complete (burn rate is same as target)
---@nodiscard ---@nodiscard
---@return boolean complete ---@return boolean complete
function public.a_ramp_complete() function public.auto_ramp_complete()
if self.plc_i ~= nil then if self.plc_i ~= nil then
return self.plc_i.is_ramp_complete() or return self.plc_i.is_ramp_complete() or
(self.plc_i.get_status().act_burn_rate == 0 and self.db.control.br100 == 0) or (self.plc_i.get_status().act_burn_rate == 0 and self.db.control.br100 == 0) or
public.a_get_effective_limit() == 0 public.auto_get_effective_limit() == 0
else return true end else return true end
end end
-- perform an automatic SCRAM -- perform an automatic SCRAM
function public.a_scram() function public.auto_scram()
if self.plc_s ~= nil then if self.plc_s ~= nil then
self.db.control.br100 = 0 self.db.control.br100 = 0
self.plc_s.in_queue.push_command(PLC_S_CMDS.ASCRAM) self.plc_s.in_queue.push_command(PLC_S_CMDS.ASCRAM)
@ -567,7 +567,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
end end
-- queue a command to clear timeout/auto-scram if set -- queue a command to clear timeout/auto-scram if set
function public.a_cond_rps_reset() function public.auto_cond_rps_reset()
if self.plc_s ~= nil and self.plc_i ~= nil and (not self.auto_was_alarmed) and (not self.emcool_opened) then if self.plc_s ~= nil and self.plc_i ~= nil and (not self.auto_was_alarmed) and (not self.emcool_opened) then
local rps = self.plc_i.get_rps() local rps = self.plc_i.get_rps()
if rps.timeout or rps.automatic then if rps.timeout or rps.automatic then

View File

@ -549,7 +549,7 @@ function logic.update_auto_safety(public, self)
end end
if alarmed and not self.plc_cache.rps_status.automatic then if alarmed and not self.plc_cache.rps_status.automatic then
public.a_scram() public.auto_scram()
end end
self.auto_was_alarmed = alarmed self.auto_was_alarmed = alarmed

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 })