mirror of
https://github.com/MikaylaFischler/cc-mek-scada.git
synced 2024-08-30 18:22:34 +00:00
commit
193aeed6df
95
.github/workflows/manifest.yml
vendored
Normal file
95
.github/workflows/manifest.yml
vendored
Normal 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
|
47
.github/workflows/shields.yml
vendored
47
.github/workflows/shields.yml
vendored
@ -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
|
|
10
ccmsi.lua
10
ccmsi.lua
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
283
graphics/elements/listbox.lua
Normal file
283
graphics/elements/listbox.lua
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 = {}
|
||||||
|
|
||||||
|
2
imgen.py
2
imgen.py
@ -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
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)}
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
--
|
--
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
--
|
--
|
||||||
|
35
rtu/rtu.lua
35
rtu/rtu.lua
@ -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
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
179
supervisor/databus.lua
Normal 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
|
@ -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
|
||||||
|
46
supervisor/panel/components/pdg_entry.lua
Normal file
46
supervisor/panel/components/pdg_entry.lua
Normal 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
|
50
supervisor/panel/components/rtu_entry.lua
Normal file
50
supervisor/panel/components/rtu_entry.lua
Normal 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
|
164
supervisor/panel/front_panel.lua
Normal file
164
supervisor/panel/front_panel.lua
Normal 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
93
supervisor/panel/pgi.lua
Normal 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
|
42
supervisor/panel/style.lua
Normal file
42
supervisor/panel/style.lua
Normal 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
84
supervisor/renderer.lua
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 })
|
||||||
|
Loading…
Reference in New Issue
Block a user