Compare commits
105 Commits
v1.1.0-bet
...
v1.3.1-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82ab85daa5 | ||
|
|
a14ffea6f0 | ||
|
|
43a0ff86d7 | ||
|
|
97cee58e5a | ||
|
|
b8c81e2e70 | ||
|
|
142f2c363a | ||
|
|
de99169db8 | ||
|
|
d5446f970b | ||
|
|
792cb46ce6 | ||
|
|
86615b03ff | ||
|
|
d5fe790c86 | ||
|
|
beda7624f4 | ||
|
|
82e3fa494c | ||
|
|
466902371a | ||
|
|
e763af9981 | ||
|
|
b2115fd077 | ||
|
|
36bd2c5e08 | ||
|
|
f6610489c2 | ||
|
|
a81fd49604 | ||
|
|
b430a22f08 | ||
|
|
a220713385 | ||
|
|
fac9a8d104 | ||
|
|
0783c4c01f | ||
|
|
676dfc8c22 | ||
|
|
50c0a4a3eb | ||
|
|
032284e90d | ||
|
|
3a0d677c16 | ||
|
|
2c2f936232 | ||
|
|
4ef1915137 | ||
|
|
40fa0de7a3 | ||
|
|
b8a8da1ac4 | ||
|
|
e26dc905f8 | ||
|
|
c7edd8c487 | ||
|
|
d3249da102 | ||
|
|
0e1f23efe8 | ||
|
|
5a139c2dd6 | ||
|
|
30ba8bdccf | ||
|
|
b2e21cb6d9 | ||
|
|
8064b33a36 | ||
|
|
7e33f22577 | ||
|
|
464451c378 | ||
|
|
0778a442b1 | ||
|
|
d7e2884634 | ||
|
|
43e708aa0d | ||
|
|
783c4936cc | ||
|
|
c75f08a9f7 | ||
|
|
e1da8b59d3 | ||
|
|
706fb5ea74 | ||
|
|
419ca2e6ef | ||
|
|
4c8723eb32 | ||
|
|
5db517cedc | ||
|
|
e9788abde7 | ||
|
|
be077aa1fb | ||
|
|
d143015cc7 | ||
|
|
df45f6c984 | ||
|
|
f6fe99a5fd | ||
|
|
a843c8eb79 | ||
|
|
a614b97d02 | ||
|
|
eca303e289 | ||
|
|
ccdc31ed87 | ||
|
|
c49ad63d6a | ||
|
|
7929318096 | ||
|
|
2371a75130 | ||
|
|
fee54db43e | ||
|
|
b48c956354 | ||
|
|
449e393b73 | ||
|
|
d295c2b3c3 | ||
|
|
438ab55f4f | ||
|
|
46607dd690 | ||
|
|
33c570075c | ||
|
|
93776a0421 | ||
|
|
14dc814925 | ||
|
|
a7ba0e43e8 | ||
|
|
e9290540f5 | ||
|
|
b35bf98dec | ||
|
|
59512bb0cf | ||
|
|
64449c6674 | ||
|
|
5bcd885f53 | ||
|
|
ba70aa31dc | ||
|
|
d9ec3d7825 | ||
|
|
9b9ce7eae1 | ||
|
|
e2a3252d8a | ||
|
|
c0547fe463 | ||
|
|
36b86a4825 | ||
|
|
37dd52b12b | ||
|
|
6b8b38b8cb | ||
|
|
2b23dac1fe | ||
|
|
76f6cca42d | ||
|
|
ab9e487a2d | ||
|
|
982fded31d | ||
|
|
a8e0538804 | ||
|
|
8c42a05bbd | ||
|
|
60a3fc8c37 | ||
|
|
83cc4d3067 | ||
|
|
fb31afc89c | ||
|
|
36c8a9ccfa | ||
|
|
f108db9cfc | ||
|
|
f48266e27c | ||
|
|
5c333c2a07 | ||
|
|
df0ee7c4f7 | ||
|
|
c987d14d8d | ||
|
|
075a0280ac | ||
|
|
4b1c982292 | ||
|
|
e276a99cb3 | ||
|
|
3ae39b2455 |
16
.devcontainer/devcontainer.json
Normal file
16
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/universal:2",
|
||||||
|
"features": {
|
||||||
|
},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"sumneko.lua",
|
||||||
|
"jackmacwindows.vscode-computercraft",
|
||||||
|
"ms-python.python",
|
||||||
|
"Catppuccin.catppuccin-vsc-icons",
|
||||||
|
"melishev.feather-vscode"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
.github/workflows/check.yml
vendored
Normal file
31
.github/workflows/check.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: Lua Checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- latest
|
||||||
|
- devel
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- latest
|
||||||
|
- devel
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v3.5.1
|
||||||
|
- name: Luacheck
|
||||||
|
uses: lunarmodules/luacheck@v1.1.0
|
||||||
|
with:
|
||||||
|
# Argument Explanations
|
||||||
|
# -i 121 = Setting a read-only global variable
|
||||||
|
# 512 = Loop can be executed at most once
|
||||||
|
# 542 = An empty if branch
|
||||||
|
# --no-max-line-length = Disable warnings for long line lengths
|
||||||
|
# --exclude-files ... = Exclude lockbox library (external) and config files
|
||||||
|
# --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os'
|
||||||
|
args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http parallel periphemu peripheral read rs settings shell term textutils window
|
||||||
47
.github/workflows/shields.yml
vendored
Normal file
47
.github/workflows/shields.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# 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
|
||||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"sumneko.lua",
|
||||||
|
"jackmacwindows.vscode-computercraft",
|
||||||
|
"ms-python.python"
|
||||||
|
]
|
||||||
|
}
|
||||||
36
.vscode/settings.json
vendored
36
.vscode/settings.json
vendored
@@ -1,22 +1,28 @@
|
|||||||
{
|
{
|
||||||
"Lua.diagnostics.globals": [
|
"Lua.diagnostics.globals": [
|
||||||
"term",
|
|
||||||
"fs",
|
|
||||||
"peripheral",
|
|
||||||
"rs",
|
|
||||||
"bit",
|
|
||||||
"parallel",
|
|
||||||
"colors",
|
|
||||||
"textutils",
|
|
||||||
"shell",
|
|
||||||
"settings",
|
|
||||||
"window",
|
|
||||||
"read",
|
|
||||||
"periphemu",
|
|
||||||
"mekanismEnergyHelper",
|
|
||||||
"_HOST",
|
"_HOST",
|
||||||
"http"
|
"bit",
|
||||||
|
"colors",
|
||||||
|
"fs",
|
||||||
|
"http",
|
||||||
|
"parallel",
|
||||||
|
"periphemu",
|
||||||
|
"peripheral",
|
||||||
|
"read",
|
||||||
|
"rs",
|
||||||
|
"settings",
|
||||||
|
"shell",
|
||||||
|
"term",
|
||||||
|
"textutils",
|
||||||
|
"window"
|
||||||
],
|
],
|
||||||
|
"Lua.diagnostics.severity": {
|
||||||
|
"unused-local": "Information",
|
||||||
|
"unused-vararg": "Information",
|
||||||
|
"unused-function": "Warning",
|
||||||
|
"unused-label": "Information"
|
||||||
|
},
|
||||||
|
"Lua.hint.setType": true,
|
||||||
"Lua.diagnostics.disable": [
|
"Lua.diagnostics.disable": [
|
||||||
"duplicate-set-field"
|
"duplicate-set-field"
|
||||||
]
|
]
|
||||||
|
|||||||
25
README.md
25
README.md
@@ -1,6 +1,12 @@
|
|||||||
# cc-mek-scada
|
# cc-mek-scada
|
||||||
Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fission reactors with a GUI, automatic safety features, waste processing control, and more!
|
Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fission reactors with a GUI, automatic safety features, waste processing control, and more!
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
Mod Requirements:
|
Mod Requirements:
|
||||||
- CC: Tweaked
|
- CC: Tweaked
|
||||||
- Mekanism v10.1+
|
- Mekanism v10.1+
|
||||||
@@ -12,6 +18,25 @@ v10.1+ is required due the complete support of CC:Tweaked added in Mekanism v10.
|
|||||||
|
|
||||||
There was also an apparent bug with boilers disconnecting and reconnecting when active in my test world on 10.0.24, so it may not even have been an option to fully implement this with support for 10.0.
|
There was also an apparent bug with boilers disconnecting and reconnecting when active in my test world on 10.0.24, so it may not even have been an option to fully implement this with support for 10.0.
|
||||||
|
|
||||||
|
## Released Component Versions
|
||||||
|
|
||||||
|
### Core
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
### Utilities
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Applications
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
You can install this on a ComputerCraft computer using either:
|
You can install this on a ComputerCraft computer using either:
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
local apisessions = {}
|
|
||||||
|
|
||||||
---@param packet capi_frame
|
|
||||||
function apisessions.handle_packet(packet)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- attempt to identify which session's watchdog timer fired
|
|
||||||
---@param timer_event number
|
|
||||||
function apisessions.check_all_watchdogs(timer_event)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- delete all closed sessions
|
|
||||||
function apisessions.free_all_closed()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- close all open connections
|
|
||||||
function apisessions.close_all()
|
|
||||||
end
|
|
||||||
|
|
||||||
return apisessions
|
|
||||||
@@ -3,13 +3,14 @@ local config = {}
|
|||||||
-- port of the SCADA supervisor
|
-- port of the SCADA supervisor
|
||||||
config.SCADA_SV_PORT = 16100
|
config.SCADA_SV_PORT = 16100
|
||||||
-- port to listen to incoming packets from supervisor
|
-- port to listen to incoming packets from supervisor
|
||||||
config.SCADA_SV_LISTEN = 16101
|
config.SCADA_SV_CTL_LISTEN = 16101
|
||||||
-- listen port for SCADA coordinator API access
|
-- listen port for SCADA coordinator API access
|
||||||
config.SCADA_API_LISTEN = 16200
|
config.SCADA_API_LISTEN = 16200
|
||||||
-- 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
|
||||||
config.COMMS_TIMEOUT = 5
|
config.SV_TIMEOUT = 5
|
||||||
|
config.API_TIMEOUT = 5
|
||||||
|
|
||||||
-- expected number of reactor units, used only to require that number of unit monitors
|
-- expected number of reactor units, used only to require that number of unit monitors
|
||||||
config.NUM_UNITS = 4
|
config.NUM_UNITS = 4
|
||||||
@@ -27,5 +28,7 @@ config.LOG_PATH = "/log.txt"
|
|||||||
-- 0 = APPEND (adds to existing file on start)
|
-- 0 = APPEND (adds to existing file on start)
|
||||||
-- 1 = NEW (replaces existing file on start)
|
-- 1 = NEW (replaces existing file on start)
|
||||||
config.LOG_MODE = 0
|
config.LOG_MODE = 0
|
||||||
|
-- true to log verbose debug messages
|
||||||
|
config.LOG_DEBUG = false
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ local log = require("scada-common.log")
|
|||||||
local ppm = require("scada-common.ppm")
|
local ppm = require("scada-common.ppm")
|
||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
local apisessions = require("coordinator.apisessions")
|
|
||||||
local iocontrol = require("coordinator.iocontrol")
|
local iocontrol = require("coordinator.iocontrol")
|
||||||
local process = require("coordinator.process")
|
local process = require("coordinator.process")
|
||||||
|
|
||||||
|
local apisessions = require("coordinator.session.apisessions")
|
||||||
|
|
||||||
local dialog = require("coordinator.ui.dialog")
|
local dialog = require("coordinator.ui.dialog")
|
||||||
|
|
||||||
local print = util.print
|
local print = util.print
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local print_ts = util.print_ts
|
|
||||||
local println_ts = util.println_ts
|
local println_ts = util.println_ts
|
||||||
|
|
||||||
local PROTOCOL = comms.PROTOCOL
|
local PROTOCOL = comms.PROTOCOL
|
||||||
@@ -225,7 +225,8 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
|||||||
sv_r_seq_num = nil,
|
sv_r_seq_num = nil,
|
||||||
sv_config_err = false,
|
sv_config_err = false,
|
||||||
connected = false,
|
connected = false,
|
||||||
last_est_ack = ESTABLISH_ACK.ALLOW
|
last_est_ack = ESTABLISH_ACK.ALLOW,
|
||||||
|
last_api_est_acks = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
comms.set_trusted_range(range)
|
comms.set_trusted_range(range)
|
||||||
@@ -241,12 +242,15 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
|||||||
|
|
||||||
_conf_channels()
|
_conf_channels()
|
||||||
|
|
||||||
|
-- link modem to apisessions
|
||||||
|
apisessions.init(modem)
|
||||||
|
|
||||||
-- send a packet to the supervisor
|
-- send a packet to the supervisor
|
||||||
---@param msg_type SCADA_MGMT_TYPE|SCADA_CRDN_TYPE
|
---@param msg_type SCADA_MGMT_TYPE|SCADA_CRDN_TYPE
|
||||||
---@param msg table
|
---@param msg table
|
||||||
local function _send_sv(protocol, msg_type, msg)
|
local function _send_sv(protocol, msg_type, msg)
|
||||||
local s_pkt = comms.scada_packet()
|
local s_pkt = comms.scada_packet()
|
||||||
local pkt = nil ---@type mgmt_packet|crdn_packet
|
local pkt ---@type mgmt_packet|crdn_packet
|
||||||
|
|
||||||
if protocol == PROTOCOL.SCADA_MGMT then
|
if protocol == PROTOCOL.SCADA_MGMT then
|
||||||
pkt = comms.mgmt_packet()
|
pkt = comms.mgmt_packet()
|
||||||
@@ -263,6 +267,19 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
|||||||
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
|
||||||
|
---@param dest integer
|
||||||
|
---@param msg table
|
||||||
|
local function _send_api_establish_ack(seq_id, dest, msg)
|
||||||
|
local s_pkt = comms.scada_packet()
|
||||||
|
local m_pkt = comms.mgmt_packet()
|
||||||
|
|
||||||
|
m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg)
|
||||||
|
s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||||
|
|
||||||
|
modem.transmit(dest, api_listen, s_pkt.raw_sendable())
|
||||||
|
end
|
||||||
|
|
||||||
-- attempt connection establishment
|
-- attempt connection establishment
|
||||||
local function _send_establish()
|
local function _send_establish()
|
||||||
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRDN })
|
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRDN })
|
||||||
@@ -283,6 +300,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
|||||||
---@param new_modem table
|
---@param new_modem table
|
||||||
function public.reconnect_modem(new_modem)
|
function public.reconnect_modem(new_modem)
|
||||||
modem = new_modem
|
modem = new_modem
|
||||||
|
apisessions.relink_modem(new_modem)
|
||||||
_conf_channels()
|
_conf_channels()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -417,13 +435,70 @@ 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 protocol = packet.scada_frame.protocol()
|
|
||||||
local l_port = packet.scada_frame.local_port()
|
local l_port = packet.scada_frame.local_port()
|
||||||
|
local r_port = packet.scada_frame.remote_port()
|
||||||
|
local protocol = packet.scada_frame.protocol()
|
||||||
|
|
||||||
if l_port == api_listen then
|
if l_port == api_listen then
|
||||||
if protocol == PROTOCOL.COORD_API then
|
if protocol == PROTOCOL.COORD_API then
|
||||||
---@cast packet capi_frame
|
---@cast packet capi_frame
|
||||||
apisessions.handle_packet(packet)
|
-- look for an associated session
|
||||||
|
local session = apisessions.find_session(r_port)
|
||||||
|
|
||||||
|
-- API packet
|
||||||
|
if session ~= nil then
|
||||||
|
-- pass the packet onto the session handler
|
||||||
|
session.in_queue.push_packet(packet)
|
||||||
|
else
|
||||||
|
-- any other packet should be session related, discard it
|
||||||
|
log.debug("discarding COORD_API packet without a known session")
|
||||||
|
end
|
||||||
|
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||||
|
---@cast packet mgmt_frame
|
||||||
|
-- look for an associated session
|
||||||
|
local session = apisessions.find_session(r_port)
|
||||||
|
|
||||||
|
-- SCADA management packet
|
||||||
|
if session ~= nil then
|
||||||
|
-- pass the packet onto the session handler
|
||||||
|
session.in_queue.push_packet(packet)
|
||||||
|
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||||
|
-- establish a new session
|
||||||
|
local next_seq_id = packet.scada_frame.seq_num() + 1
|
||||||
|
|
||||||
|
-- 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 self.last_api_est_acks[r_port] ~= ESTABLISH_ACK.BAD_VERSION then
|
||||||
|
log.info(util.c("dropping API establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
|
||||||
|
self.last_api_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION
|
||||||
|
end
|
||||||
|
|
||||||
|
_send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION })
|
||||||
|
elseif dev_type == DEVICE_TYPE.PKT then
|
||||||
|
-- pocket linking request
|
||||||
|
local id = apisessions.establish_session(l_port, r_port, firmware_v)
|
||||||
|
println(util.c("API: pocket (", firmware_v, ") [:", r_port, "] connected with session ID ", id))
|
||||||
|
coordinator.log_comms(util.c("API: pocket (", firmware_v, ") [:", r_port, "] connected with session ID ", id))
|
||||||
|
|
||||||
|
_send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW })
|
||||||
|
self.last_api_est_acks[r_port] = ESTABLISH_ACK.ALLOW
|
||||||
|
else
|
||||||
|
log.debug(util.c("illegal establish packet for device ", dev_type, " on API listening channel"))
|
||||||
|
_send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.DENY })
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug("invalid establish packet (on API listening channel)")
|
||||||
|
_send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.DENY })
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- any other packet should be session related, discard it
|
||||||
|
log.debug(util.c(r_port, "->", l_port, ": discarding SCADA_MGMT packet without a known session"))
|
||||||
|
end
|
||||||
else
|
else
|
||||||
log.debug("illegal packet type " .. protocol .. " on api listening channel", true)
|
log.debug("illegal packet type " .. protocol .. " on api listening channel", true)
|
||||||
end
|
end
|
||||||
@@ -431,7 +506,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
|||||||
-- 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 >= 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
|
||||||
else
|
else
|
||||||
@@ -516,7 +591,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
|||||||
elseif packet.type == SCADA_CRDN_TYPE.UNIT_STATUSES then
|
elseif packet.type == SCADA_CRDN_TYPE.UNIT_STATUSES then
|
||||||
-- update statuses
|
-- update statuses
|
||||||
if not iocontrol.update_unit_statuses(packet.data) then
|
if not iocontrol.update_unit_statuses(packet.data) then
|
||||||
log.error("received invalid UNIT_STATUSES packet")
|
log.debug("received invalid UNIT_STATUSES packet")
|
||||||
end
|
end
|
||||||
elseif packet.type == SCADA_CRDN_TYPE.UNIT_CMD then
|
elseif packet.type == SCADA_CRDN_TYPE.UNIT_CMD then
|
||||||
-- unit command acknowledgement
|
-- unit command acknowledgement
|
||||||
@@ -552,7 +627,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
|||||||
log.debug("SCADA_CRDN unit command ack packet length mismatch")
|
log.debug("SCADA_CRDN unit command ack packet length mismatch")
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
log.warning("received unknown SCADA_CRDN packet type " .. packet.type)
|
log.debug("received unknown SCADA_CRDN packet type " .. packet.type)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
log.debug("discarding SCADA_CRDN packet before linked")
|
log.debug("discarding SCADA_CRDN packet before linked")
|
||||||
@@ -607,11 +682,11 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
|||||||
end
|
end
|
||||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||||
if self.last_est_ack ~= est_ack then
|
if self.last_est_ack ~= est_ack then
|
||||||
log.info("supervisor connection denied due to collision")
|
log.warning("supervisor connection denied due to collision")
|
||||||
end
|
end
|
||||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||||
if self.last_est_ack ~= est_ack then
|
if self.last_est_ack ~= est_ack then
|
||||||
log.info("supervisor comms version mismatch")
|
log.warning("supervisor comms version mismatch")
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
log.debug("SCADA_MGMT establish packet reply (len = 1) unsupported")
|
log.debug("SCADA_MGMT establish packet reply (len = 1) unsupported")
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ local iocontrol = {}
|
|||||||
---@class ioctl
|
---@class ioctl
|
||||||
local io = {}
|
local io = {}
|
||||||
|
|
||||||
|
-- luacheck: no unused args
|
||||||
|
|
||||||
|
-- placeholder acknowledge function for type hinting
|
||||||
|
---@param success boolean
|
||||||
|
---@diagnostic disable-next-line: unused-local
|
||||||
|
local function __generic_ack(success) end
|
||||||
|
|
||||||
|
-- luacheck: unused args
|
||||||
|
|
||||||
-- initialize the coordinator IO controller
|
-- initialize the coordinator IO controller
|
||||||
---@param conf facility_conf configuration
|
---@param conf facility_conf configuration
|
||||||
---@param comms coord_comms comms reference
|
---@param comms coord_comms comms reference
|
||||||
@@ -45,11 +54,11 @@ function iocontrol.init(conf, comms)
|
|||||||
|
|
||||||
radiation = types.new_zero_radiation_reading(),
|
radiation = types.new_zero_radiation_reading(),
|
||||||
|
|
||||||
save_cfg_ack = function (success) end, ---@param success boolean
|
save_cfg_ack = __generic_ack,
|
||||||
start_ack = function (success) end, ---@param success boolean
|
start_ack = __generic_ack,
|
||||||
stop_ack = function (success) end, ---@param success boolean
|
stop_ack = __generic_ack,
|
||||||
scram_ack = function (success) end, ---@param success boolean
|
scram_ack = __generic_ack,
|
||||||
ack_alarms_ack = function (success) end, ---@param success boolean
|
ack_alarms_ack = __generic_ack,
|
||||||
|
|
||||||
ps = psil.create(),
|
ps = psil.create(),
|
||||||
|
|
||||||
@@ -74,7 +83,6 @@ function iocontrol.init(conf, comms)
|
|||||||
|
|
||||||
---@class ioctl_unit
|
---@class ioctl_unit
|
||||||
local entry = {
|
local entry = {
|
||||||
---@type integer
|
|
||||||
unit_id = i,
|
unit_id = i,
|
||||||
|
|
||||||
num_boilers = 0,
|
num_boilers = 0,
|
||||||
@@ -85,7 +93,8 @@ function iocontrol.init(conf, comms)
|
|||||||
waste_control = 0,
|
waste_control = 0,
|
||||||
radiation = types.new_zero_radiation_reading(),
|
radiation = types.new_zero_radiation_reading(),
|
||||||
|
|
||||||
a_group = 0, -- auto control group
|
-- auto control group
|
||||||
|
a_group = 0,
|
||||||
|
|
||||||
start = function () process.start(i) end,
|
start = function () process.start(i) end,
|
||||||
scram = function () process.scram(i) end,
|
scram = function () process.scram(i) end,
|
||||||
@@ -96,12 +105,12 @@ function iocontrol.init(conf, comms)
|
|||||||
|
|
||||||
set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0
|
set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0
|
||||||
|
|
||||||
start_ack = function (success) end, ---@param success boolean
|
start_ack = __generic_ack,
|
||||||
scram_ack = function (success) end, ---@param success boolean
|
scram_ack = __generic_ack,
|
||||||
reset_rps_ack = function (success) end, ---@param success boolean
|
reset_rps_ack = __generic_ack,
|
||||||
ack_alarms_ack = function (success) end, ---@param success boolean
|
ack_alarms_ack = __generic_ack,
|
||||||
set_burn_ack = function (success) end, ---@param success boolean
|
set_burn_ack = __generic_ack,
|
||||||
set_waste_ack = function (success) end, ---@param success boolean
|
set_waste_ack = __generic_ack,
|
||||||
|
|
||||||
alarm_callbacks = {
|
alarm_callbacks = {
|
||||||
c_breach = { ack = function () ack(1) end, reset = function () reset(1) end },
|
c_breach = { ack = function () ack(1) end, reset = function () reset(1) end },
|
||||||
@@ -134,10 +143,10 @@ function iocontrol.init(conf, comms)
|
|||||||
ALARM_STATE.INACTIVE -- turbine trip
|
ALARM_STATE.INACTIVE -- turbine trip
|
||||||
},
|
},
|
||||||
|
|
||||||
annunciator = {}, ---@type annunciator
|
annunciator = {}, ---@type annunciator
|
||||||
|
|
||||||
unit_ps = psil.create(),
|
unit_ps = psil.create(),
|
||||||
reactor_data = {}, ---@type reactor_db
|
reactor_data = {}, ---@type reactor_db
|
||||||
|
|
||||||
boiler_ps_tbl = {},
|
boiler_ps_tbl = {},
|
||||||
boiler_data_tbl = {},
|
boiler_data_tbl = {},
|
||||||
@@ -657,8 +666,8 @@ function iocontrol.update_unit_statuses(statuses)
|
|||||||
if type(rtu_statuses.rad_mon) == "table" then
|
if type(rtu_statuses.rad_mon) == "table" then
|
||||||
if #rtu_statuses.rad_mon > 0 then
|
if #rtu_statuses.rad_mon > 0 then
|
||||||
local rad_mon = rtu_statuses.rad_mon[1]
|
local rad_mon = rtu_statuses.rad_mon[1]
|
||||||
local rtu_faulted = rad_mon[1] ---@type boolean
|
-- local rtu_faulted = rad_mon[1] ---@type boolean
|
||||||
unit.radiation = rad_mon[2] ---@type number
|
unit.radiation = rad_mon[2] ---@type number
|
||||||
|
|
||||||
unit.unit_ps.publish("radiation", unit.radiation)
|
unit.unit_ps.publish("radiation", unit.radiation)
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -2,29 +2,29 @@
|
|||||||
-- Graphics Rendering Control
|
-- Graphics Rendering Control
|
||||||
--
|
--
|
||||||
|
|
||||||
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 style = require("coordinator.ui.style")
|
local style = require("coordinator.ui.style")
|
||||||
|
|
||||||
local main_view = require("coordinator.ui.layout.main_view")
|
local main_view = require("coordinator.ui.layout.main_view")
|
||||||
local unit_view = require("coordinator.ui.layout.unit_view")
|
local unit_view = require("coordinator.ui.layout.unit_view")
|
||||||
|
|
||||||
local flasher = require("graphics.flasher")
|
local flasher = require("graphics.flasher")
|
||||||
|
|
||||||
|
local DisplayBox = require("graphics.elements.displaybox")
|
||||||
|
|
||||||
local renderer = {}
|
local renderer = {}
|
||||||
|
|
||||||
-- render engine
|
-- render engine
|
||||||
local engine = {
|
local engine = {
|
||||||
monitors = nil,
|
monitors = nil, ---@type monitors_struct|nil
|
||||||
dmesg_window = nil,
|
dmesg_window = nil, ---@type table|nil
|
||||||
ui_ready = false
|
ui_ready = false,
|
||||||
}
|
ui = {
|
||||||
|
main_display = nil, ---@type graphics_element|nil
|
||||||
-- UI layouts
|
unit_displays = {}
|
||||||
local ui = {
|
}
|
||||||
main_layout = nil,
|
|
||||||
unit_layouts = {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- init a display to the "default", but set text scale to 0.5
|
-- init a display to the "default", but set text scale to 0.5
|
||||||
@@ -57,10 +57,8 @@ function renderer.is_monitor_used(periph)
|
|||||||
if engine.monitors.primary == periph then
|
if engine.monitors.primary == periph then
|
||||||
return true
|
return true
|
||||||
else
|
else
|
||||||
for i = 1, #engine.monitors.unit_displays do
|
for _, monitor in ipairs(engine.monitors.unit_displays) do
|
||||||
if engine.monitors.unit_displays[i] == periph then
|
if monitor == periph then return true end
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -74,7 +72,7 @@ function renderer.init_displays()
|
|||||||
_init_display(engine.monitors.primary)
|
_init_display(engine.monitors.primary)
|
||||||
|
|
||||||
-- init unit displays
|
-- init unit displays
|
||||||
for _, monitor in pairs(engine.monitors.unit_displays) do
|
for _, monitor in ipairs(engine.monitors.unit_displays) do
|
||||||
_init_display(monitor)
|
_init_display(monitor)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -93,7 +91,7 @@ end
|
|||||||
function renderer.validate_unit_display_sizes()
|
function renderer.validate_unit_display_sizes()
|
||||||
local valid = true
|
local valid = true
|
||||||
|
|
||||||
for id, monitor in pairs(engine.monitors.unit_displays) do
|
for id, monitor in ipairs(engine.monitors.unit_displays) do
|
||||||
local w, h = monitor.getSize()
|
local w, h = monitor.getSize()
|
||||||
if w ~= 79 or h ~= 52 then
|
if w ~= 79 or h ~= 52 then
|
||||||
log.warning(util.c("RENDERER: unit ", id, " display resolution not 79 wide by 52 tall: ", w, ", ", h))
|
log.warning(util.c("RENDERER: unit ", id, " display resolution not 79 wide by 52 tall: ", w, ", ", h))
|
||||||
@@ -108,7 +106,6 @@ end
|
|||||||
function renderer.init_dmesg()
|
function renderer.init_dmesg()
|
||||||
local disp_x, disp_y = engine.monitors.primary.getSize()
|
local disp_x, disp_y = engine.monitors.primary.getSize()
|
||||||
engine.dmesg_window = window.create(engine.monitors.primary, 1, 1, disp_x, disp_y)
|
engine.dmesg_window = window.create(engine.monitors.primary, 1, 1, disp_x, disp_y)
|
||||||
|
|
||||||
log.direct_dmesg(engine.dmesg_window)
|
log.direct_dmesg(engine.dmesg_window)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -119,11 +116,13 @@ function renderer.start_ui()
|
|||||||
engine.dmesg_window.setVisible(false)
|
engine.dmesg_window.setVisible(false)
|
||||||
|
|
||||||
-- show main view on main monitor
|
-- show main view on main monitor
|
||||||
ui.main_layout = main_view(engine.monitors.primary)
|
engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root}
|
||||||
|
main_view(engine.ui.main_display)
|
||||||
|
|
||||||
-- show unit views on unit displays
|
-- show unit views on unit displays
|
||||||
for id, monitor in pairs(engine.monitors.unit_displays) do
|
for i = 1, #engine.monitors.unit_displays do
|
||||||
table.insert(ui.unit_layouts, unit_view(monitor, id))
|
engine.ui.unit_displays[i] = DisplayBox{window=engine.monitors.unit_displays[i],fg_bg=style.root}
|
||||||
|
unit_view(engine.ui.unit_displays[i], i)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- start flasher callback task
|
-- start flasher callback task
|
||||||
@@ -136,29 +135,22 @@ end
|
|||||||
|
|
||||||
-- close out the UI
|
-- close out the UI
|
||||||
function renderer.close_ui()
|
function renderer.close_ui()
|
||||||
-- report ui as not ready
|
|
||||||
engine.ui_ready = false
|
|
||||||
|
|
||||||
-- stop blinking indicators
|
-- stop blinking indicators
|
||||||
flasher.clear()
|
flasher.clear()
|
||||||
|
|
||||||
if engine.ui_ready then
|
-- delete element trees
|
||||||
-- hide to stop animation callbacks
|
if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end
|
||||||
ui.main_layout.hide()
|
for _, display in ipairs(engine.ui.unit_displays) do display.delete() end
|
||||||
for i = 1, #ui.unit_layouts do
|
|
||||||
ui.unit_layouts[i].hide()
|
-- report ui as not ready
|
||||||
engine.monitors.unit_displays[i].clear()
|
engine.ui_ready = false
|
||||||
end
|
|
||||||
else
|
|
||||||
-- clear unit displays
|
|
||||||
for i = 1, #ui.unit_layouts do
|
|
||||||
engine.monitors.unit_displays[i].clear()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- clear root UI elements
|
-- clear root UI elements
|
||||||
ui.main_layout = nil
|
engine.ui.main_display = nil
|
||||||
ui.unit_layouts = {}
|
engine.ui.unit_displays = {}
|
||||||
|
|
||||||
|
-- clear unit monitors
|
||||||
|
for _, monitor in ipairs(engine.monitors.unit_displays) do monitor.clear() end
|
||||||
|
|
||||||
-- re-draw dmesg
|
-- re-draw dmesg
|
||||||
engine.dmesg_window.setVisible(true)
|
engine.dmesg_window.setVisible(true)
|
||||||
@@ -171,15 +163,17 @@ end
|
|||||||
function renderer.ui_ready() return engine.ui_ready end
|
function renderer.ui_ready() return engine.ui_ready end
|
||||||
|
|
||||||
-- handle a touch event
|
-- handle a touch event
|
||||||
---@param event mouse_interaction
|
---@param event mouse_interaction|nil
|
||||||
function renderer.handle_mouse(event)
|
function renderer.handle_mouse(event)
|
||||||
if event.monitor == engine.monitors.primary_name then
|
if engine.ui_ready and event ~= nil then
|
||||||
ui.main_layout.handle_mouse(event)
|
if event.monitor == engine.monitors.primary_name then
|
||||||
else
|
engine.ui.main_display.handle_mouse(event)
|
||||||
for id, monitor in pairs(engine.monitors.unit_name_map) do
|
else
|
||||||
if event.monitor == monitor then
|
for id, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||||
local layout = ui.unit_layouts[id] ---@type graphics_element
|
if event.monitor == monitor then
|
||||||
layout.handle_mouse(event)
|
local layout = engine.ui.unit_displays[id] ---@type graphics_element
|
||||||
|
layout.handle_mouse(event)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
251
coordinator/session/api.lua
Normal file
251
coordinator/session/api.lua
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
local comms = require("scada-common.comms")
|
||||||
|
local log = require("scada-common.log")
|
||||||
|
local mqueue = require("scada-common.mqueue")
|
||||||
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
|
local api = {}
|
||||||
|
|
||||||
|
local PROTOCOL = comms.PROTOCOL
|
||||||
|
-- local CAPI_TYPE = comms.CAPI_TYPE
|
||||||
|
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||||
|
|
||||||
|
local println = util.println
|
||||||
|
|
||||||
|
-- retry time constants in ms
|
||||||
|
-- local INITIAL_WAIT = 1500
|
||||||
|
-- local RETRY_PERIOD = 1000
|
||||||
|
|
||||||
|
local API_S_CMDS = {
|
||||||
|
}
|
||||||
|
|
||||||
|
local API_S_DATA = {
|
||||||
|
}
|
||||||
|
|
||||||
|
api.API_S_CMDS = API_S_CMDS
|
||||||
|
api.API_S_DATA = API_S_DATA
|
||||||
|
|
||||||
|
local PERIODICS = {
|
||||||
|
KEEP_ALIVE = 2000
|
||||||
|
}
|
||||||
|
|
||||||
|
-- pocket API session
|
||||||
|
---@nodiscard
|
||||||
|
---@param id integer session ID
|
||||||
|
---@param in_queue mqueue in message queue
|
||||||
|
---@param out_queue mqueue out message queue
|
||||||
|
---@param timeout number communications timeout
|
||||||
|
function api.new_session(id, in_queue, out_queue, timeout)
|
||||||
|
local log_header = "api_session(" .. id .. "): "
|
||||||
|
|
||||||
|
local self = {
|
||||||
|
-- connection properties
|
||||||
|
seq_num = 0,
|
||||||
|
r_seq_num = nil,
|
||||||
|
connected = true,
|
||||||
|
conn_watchdog = util.new_watchdog(timeout),
|
||||||
|
last_rtt = 0,
|
||||||
|
-- periodic messages
|
||||||
|
periodics = {
|
||||||
|
last_update = 0,
|
||||||
|
keep_alive = 0
|
||||||
|
},
|
||||||
|
-- when to next retry one of these requests
|
||||||
|
retry_times = {
|
||||||
|
},
|
||||||
|
-- command acknowledgements
|
||||||
|
acks = {
|
||||||
|
},
|
||||||
|
-- session database
|
||||||
|
---@class api_db
|
||||||
|
sDB = {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
---@class api_session
|
||||||
|
local public = {}
|
||||||
|
|
||||||
|
-- mark this API session as closed, stop watchdog
|
||||||
|
local function _close()
|
||||||
|
self.conn_watchdog.cancel()
|
||||||
|
self.connected = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- send a CAPI packet
|
||||||
|
-----@param msg_type CAPI_TYPE
|
||||||
|
-----@param msg table
|
||||||
|
-- local function _send(msg_type, msg)
|
||||||
|
-- local s_pkt = comms.scada_packet()
|
||||||
|
-- local c_pkt = comms.capi_packet()
|
||||||
|
|
||||||
|
-- c_pkt.make(msg_type, msg)
|
||||||
|
-- s_pkt.make(self.seq_num, PROTOCOL.COORD_API, c_pkt.raw_sendable())
|
||||||
|
|
||||||
|
-- out_queue.push_packet(s_pkt)
|
||||||
|
-- self.seq_num = self.seq_num + 1
|
||||||
|
-- end
|
||||||
|
|
||||||
|
-- send a SCADA management packet
|
||||||
|
---@param msg_type SCADA_MGMT_TYPE
|
||||||
|
---@param msg table
|
||||||
|
local function _send_mgmt(msg_type, msg)
|
||||||
|
local s_pkt = comms.scada_packet()
|
||||||
|
local m_pkt = comms.mgmt_packet()
|
||||||
|
|
||||||
|
m_pkt.make(msg_type, msg)
|
||||||
|
s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||||
|
|
||||||
|
out_queue.push_packet(s_pkt)
|
||||||
|
self.seq_num = self.seq_num + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- handle a packet
|
||||||
|
---@param pkt mgmt_frame|capi_frame
|
||||||
|
local function _handle_packet(pkt)
|
||||||
|
-- check sequence number
|
||||||
|
if self.r_seq_num == nil then
|
||||||
|
self.r_seq_num = pkt.scada_frame.seq_num()
|
||||||
|
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
|
||||||
|
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||||
|
return
|
||||||
|
else
|
||||||
|
self.r_seq_num = pkt.scada_frame.seq_num()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- feed watchdog
|
||||||
|
self.conn_watchdog.feed()
|
||||||
|
|
||||||
|
-- process packet
|
||||||
|
if pkt.scada_frame.protocol() == PROTOCOL.COORD_API then
|
||||||
|
---@cast pkt capi_frame
|
||||||
|
-- feed watchdog
|
||||||
|
self.conn_watchdog.feed()
|
||||||
|
|
||||||
|
-- handle packet by type
|
||||||
|
if pkt.type == nil then
|
||||||
|
else
|
||||||
|
log.debug(log_header .. "handler received unsupported CAPI packet type " .. pkt.type)
|
||||||
|
end
|
||||||
|
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||||
|
---@cast pkt mgmt_frame
|
||||||
|
if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||||
|
-- keep alive reply
|
||||||
|
if pkt.length == 2 then
|
||||||
|
local srv_start = pkt.data[1]
|
||||||
|
-- local api_send = pkt.data[2]
|
||||||
|
local srv_now = util.time()
|
||||||
|
self.last_rtt = srv_now - srv_start
|
||||||
|
|
||||||
|
if self.last_rtt > 750 then
|
||||||
|
log.warning(log_header .. "API KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- log.debug(log_header .. "API RTT = " .. self.last_rtt .. "ms")
|
||||||
|
-- log.debug(log_header .. "API TT = " .. (srv_now - api_send) .. "ms")
|
||||||
|
else
|
||||||
|
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||||
|
end
|
||||||
|
elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then
|
||||||
|
-- close the session
|
||||||
|
_close()
|
||||||
|
else
|
||||||
|
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- PUBLIC FUNCTIONS --
|
||||||
|
|
||||||
|
-- get the session ID
|
||||||
|
---@nodiscard
|
||||||
|
function public.get_id() return id end
|
||||||
|
|
||||||
|
-- get the session database
|
||||||
|
---@nodiscard
|
||||||
|
function public.get_db() return self.sDB end
|
||||||
|
|
||||||
|
-- check if a timer matches this session's watchdog
|
||||||
|
---@nodiscard
|
||||||
|
function public.check_wd(timer)
|
||||||
|
return self.conn_watchdog.is_timer(timer) and self.connected
|
||||||
|
end
|
||||||
|
|
||||||
|
-- close the connection
|
||||||
|
function public.close()
|
||||||
|
_close()
|
||||||
|
_send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
|
||||||
|
println("connection to API session " .. id .. " closed by server")
|
||||||
|
log.info(log_header .. "session closed by server")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- iterate the session
|
||||||
|
---@nodiscard
|
||||||
|
---@return boolean connected
|
||||||
|
function public.iterate()
|
||||||
|
if self.connected then
|
||||||
|
------------------
|
||||||
|
-- handle queue --
|
||||||
|
------------------
|
||||||
|
|
||||||
|
local handle_start = util.time()
|
||||||
|
|
||||||
|
while in_queue.ready() and self.connected do
|
||||||
|
-- get a new message to process
|
||||||
|
local message = in_queue.pop()
|
||||||
|
|
||||||
|
if message ~= nil then
|
||||||
|
if message.qtype == mqueue.TYPE.PACKET then
|
||||||
|
-- handle a packet
|
||||||
|
_handle_packet(message.message)
|
||||||
|
elseif message.qtype == mqueue.TYPE.COMMAND then
|
||||||
|
-- handle instruction
|
||||||
|
elseif message.qtype == mqueue.TYPE.DATA then
|
||||||
|
-- instruction with body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- max 100ms spent processing queue
|
||||||
|
if util.time() - handle_start > 100 then
|
||||||
|
log.warning(log_header .. "exceeded 100ms queue process limit")
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- exit if connection was closed
|
||||||
|
if not self.connected then
|
||||||
|
println("connection to API session " .. id .. " closed by remote host")
|
||||||
|
log.info(log_header .. "session closed by remote host")
|
||||||
|
return self.connected
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------
|
||||||
|
-- update periodics --
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
local elapsed = util.time() - self.periodics.last_update
|
||||||
|
|
||||||
|
local periodics = self.periodics
|
||||||
|
|
||||||
|
-- keep alive
|
||||||
|
|
||||||
|
periodics.keep_alive = periodics.keep_alive + elapsed
|
||||||
|
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
|
||||||
|
_send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() })
|
||||||
|
periodics.keep_alive = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
self.periodics.last_update = util.time()
|
||||||
|
|
||||||
|
---------------------
|
||||||
|
-- attempt retries --
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
-- local rtimes = self.retry_times
|
||||||
|
end
|
||||||
|
|
||||||
|
return self.connected
|
||||||
|
end
|
||||||
|
|
||||||
|
return public
|
||||||
|
end
|
||||||
|
|
||||||
|
return api
|
||||||
174
coordinator/session/apisessions.lua
Normal file
174
coordinator/session/apisessions.lua
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
|
||||||
|
local log = require("scada-common.log")
|
||||||
|
local mqueue = require("scada-common.mqueue")
|
||||||
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
|
local config = require("coordinator.config")
|
||||||
|
|
||||||
|
local api = require("coordinator.session.api")
|
||||||
|
|
||||||
|
local apisessions = {}
|
||||||
|
|
||||||
|
local self = {
|
||||||
|
modem = nil,
|
||||||
|
next_id = 0,
|
||||||
|
sessions = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
-- PRIVATE FUNCTIONS --
|
||||||
|
|
||||||
|
-- handle a session output queue
|
||||||
|
---@param session api_session_struct
|
||||||
|
local function _api_handle_outq(session)
|
||||||
|
-- record handler start time
|
||||||
|
local handle_start = util.time()
|
||||||
|
|
||||||
|
-- process output queue
|
||||||
|
while session.out_queue.ready() do
|
||||||
|
-- get a new message to process
|
||||||
|
local msg = session.out_queue.pop()
|
||||||
|
|
||||||
|
if msg ~= nil then
|
||||||
|
if msg.qtype == mqueue.TYPE.PACKET then
|
||||||
|
-- handle a packet to be sent
|
||||||
|
self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable())
|
||||||
|
elseif msg.qtype == mqueue.TYPE.COMMAND then
|
||||||
|
-- handle instruction/notification
|
||||||
|
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||||
|
-- instruction/notification with body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- max 100ms spent processing queue
|
||||||
|
if util.time() - handle_start > 100 then
|
||||||
|
log.warning("API out queue handler exceeded 100ms queue process limit")
|
||||||
|
log.warning(util.c("offending session: port ", session.r_port))
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- cleanly close a session
|
||||||
|
---@param session api_session_struct
|
||||||
|
local function _shutdown(session)
|
||||||
|
session.open = false
|
||||||
|
session.instance.close()
|
||||||
|
|
||||||
|
-- send packets in out queue (namely the close packet)
|
||||||
|
while session.out_queue.ready() do
|
||||||
|
local msg = session.out_queue.pop()
|
||||||
|
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then
|
||||||
|
self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
log.debug(util.c("closed API session ", session.instance.get_id(), " on remote port ", session.r_port))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- PUBLIC FUNCTIONS --
|
||||||
|
|
||||||
|
-- initialize apisessions
|
||||||
|
---@param modem table
|
||||||
|
function apisessions.init(modem)
|
||||||
|
self.modem = modem
|
||||||
|
end
|
||||||
|
|
||||||
|
-- re-link the modem
|
||||||
|
---@param modem table
|
||||||
|
function apisessions.relink_modem(modem)
|
||||||
|
self.modem = modem
|
||||||
|
end
|
||||||
|
|
||||||
|
-- find a session by remote port
|
||||||
|
---@nodiscard
|
||||||
|
---@param port integer
|
||||||
|
---@return api_session_struct|nil
|
||||||
|
function apisessions.find_session(port)
|
||||||
|
for i = 1, #self.sessions do
|
||||||
|
if self.sessions[i].r_port == port then return self.sessions[i] end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- establish a new API session
|
||||||
|
---@nodiscard
|
||||||
|
---@param local_port integer
|
||||||
|
---@param remote_port integer
|
||||||
|
---@param version string
|
||||||
|
---@return integer session_id
|
||||||
|
function apisessions.establish_session(local_port, remote_port, version)
|
||||||
|
---@class api_session_struct
|
||||||
|
local api_s = {
|
||||||
|
open = true,
|
||||||
|
version = version,
|
||||||
|
l_port = local_port,
|
||||||
|
r_port = remote_port,
|
||||||
|
in_queue = mqueue.new(),
|
||||||
|
out_queue = mqueue.new(),
|
||||||
|
instance = nil ---@type api_session
|
||||||
|
}
|
||||||
|
|
||||||
|
api_s.instance = api.new_session(self.next_id, api_s.in_queue, api_s.out_queue, config.API_TIMEOUT)
|
||||||
|
table.insert(self.sessions, api_s)
|
||||||
|
|
||||||
|
log.debug(util.c("established new API session to ", remote_port, " with ID ", self.next_id))
|
||||||
|
|
||||||
|
self.next_id = self.next_id + 1
|
||||||
|
|
||||||
|
-- success
|
||||||
|
return api_s.instance.get_id()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- attempt to identify which session's watchdog timer fired
|
||||||
|
---@param timer_event number
|
||||||
|
function apisessions.check_all_watchdogs(timer_event)
|
||||||
|
for i = 1, #self.sessions do
|
||||||
|
local session = self.sessions[i] ---@type api_session_struct
|
||||||
|
if session.open then
|
||||||
|
local triggered = session.instance.check_wd(timer_event)
|
||||||
|
if triggered then
|
||||||
|
log.debug(util.c("watchdog closing API session ", session.instance.get_id(),
|
||||||
|
" on remote port ", session.r_port, "..."))
|
||||||
|
_shutdown(session)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- iterate all the API sessions
|
||||||
|
function apisessions.iterate_all()
|
||||||
|
for i = 1, #self.sessions do
|
||||||
|
local session = self.sessions[i] ---@type api_session_struct
|
||||||
|
|
||||||
|
if session.open and session.instance.iterate() then
|
||||||
|
_api_handle_outq(session)
|
||||||
|
else
|
||||||
|
session.open = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- delete all closed sessions
|
||||||
|
function apisessions.free_all_closed()
|
||||||
|
local f = function (session) return session.open end
|
||||||
|
|
||||||
|
---@param session api_session_struct
|
||||||
|
local on_delete = function (session)
|
||||||
|
log.debug(util.c("free'ing closed API session ", session.instance.get_id(),
|
||||||
|
" on remote port ", session.r_port))
|
||||||
|
end
|
||||||
|
|
||||||
|
util.filter_table(self.sessions, f, on_delete)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- close all open connections
|
||||||
|
function apisessions.close_all()
|
||||||
|
for i = 1, #self.sessions do
|
||||||
|
local session = self.sessions[i] ---@type api_session_struct
|
||||||
|
if session.open then _shutdown(session) end
|
||||||
|
end
|
||||||
|
|
||||||
|
apisessions.free_all_closed()
|
||||||
|
end
|
||||||
|
|
||||||
|
return apisessions
|
||||||
@@ -12,10 +12,11 @@ local ALARM_STATE = types.ALARM_STATE
|
|||||||
---@class sounder
|
---@class sounder
|
||||||
local sounder = {}
|
local sounder = {}
|
||||||
|
|
||||||
|
-- note: max samples = 0x20000 (128 * 1024 samples)
|
||||||
|
|
||||||
local _2_PI = 2 * math.pi -- 2 whole pies, hope you're hungry
|
local _2_PI = 2 * math.pi -- 2 whole pies, hope you're hungry
|
||||||
local _DRATE = 48000 -- 48kHz audio
|
local _DRATE = 48000 -- 48kHz audio
|
||||||
local _MAX_VAL = 127 / 2 -- max signed integer in this 8-bit audio
|
local _MAX_VAL = 127 / 2 -- max signed integer in this 8-bit audio
|
||||||
local _MAX_SAMPLES = 0x20000 -- 128 * 1024 samples
|
|
||||||
local _05s_SAMPLES = 24000 -- half a second worth of samples
|
local _05s_SAMPLES = 24000 -- half a second worth of samples
|
||||||
|
|
||||||
local test_alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
|
local test_alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
|
||||||
|
|||||||
@@ -12,18 +12,17 @@ local util = require("scada-common.util")
|
|||||||
|
|
||||||
local core = require("graphics.core")
|
local core = require("graphics.core")
|
||||||
|
|
||||||
local apisessions = require("coordinator.apisessions")
|
|
||||||
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 COORDINATOR_VERSION = "v0.12.5"
|
local apisessions = require("coordinator.session.apisessions")
|
||||||
|
|
||||||
|
local COORDINATOR_VERSION = "v0.15.2"
|
||||||
|
|
||||||
local print = util.print
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local print_ts = util.print_ts
|
|
||||||
local println_ts = util.println_ts
|
local println_ts = util.println_ts
|
||||||
|
|
||||||
local log_graphics = coordinator.log_graphics
|
local log_graphics = coordinator.log_graphics
|
||||||
@@ -39,11 +38,13 @@ 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_port(config.SCADA_SV_PORT)
|
||||||
cfv.assert_port(config.SCADA_SV_LISTEN)
|
cfv.assert_port(config.SCADA_SV_CTL_LISTEN)
|
||||||
cfv.assert_port(config.SCADA_API_LISTEN)
|
cfv.assert_port(config.SCADA_API_LISTEN)
|
||||||
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.SV_TIMEOUT)
|
||||||
cfv.assert_min(config.COMMS_TIMEOUT, 2)
|
cfv.assert_min(config.SV_TIMEOUT, 2)
|
||||||
|
cfv.assert_type_num(config.API_TIMEOUT)
|
||||||
|
cfv.assert_min(config.API_TIMEOUT, 2)
|
||||||
cfv.assert_type_int(config.NUM_UNITS)
|
cfv.assert_type_int(config.NUM_UNITS)
|
||||||
cfv.assert_type_num(config.SOUNDER_VOLUME)
|
cfv.assert_type_num(config.SOUNDER_VOLUME)
|
||||||
cfv.assert_type_bool(config.TIME_24_HOUR)
|
cfv.assert_type_bool(config.TIME_24_HOUR)
|
||||||
@@ -56,7 +57,7 @@ assert(cfv.valid(), "bad config file: missing/invalid fields")
|
|||||||
-- log init
|
-- log init
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
log.init(config.LOG_PATH, config.LOG_MODE)
|
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
|
||||||
|
|
||||||
log.info("========================================")
|
log.info("========================================")
|
||||||
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
|
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
|
||||||
@@ -142,12 +143,12 @@ local function main()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- create connection watchdog
|
-- create connection watchdog
|
||||||
local conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT)
|
local conn_watchdog = util.new_watchdog(config.SV_TIMEOUT)
|
||||||
conn_watchdog.cancel()
|
conn_watchdog.cancel()
|
||||||
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_LISTEN,
|
local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.SCADA_SV_PORT, config.SCADA_SV_CTL_LISTEN,
|
||||||
config.SCADA_API_LISTEN, config.TRUSTED_RANGE, conn_watchdog)
|
config.SCADA_API_LISTEN, config.TRUSTED_RANGE, conn_watchdog)
|
||||||
log.debug("startup> comms init")
|
log.debug("startup> comms init")
|
||||||
log_comms("comms initialized")
|
log_comms("comms initialized")
|
||||||
@@ -287,7 +288,7 @@ local function main()
|
|||||||
else
|
else
|
||||||
log_sys("wired modem reconnected")
|
log_sys("wired modem reconnected")
|
||||||
end
|
end
|
||||||
elseif type == "monitor" then
|
-- elseif type == "monitor" then
|
||||||
-- not supported, system will exit on loss of in-use monitors
|
-- not supported, system will exit on loss of in-use monitors
|
||||||
elseif type == "speaker" then
|
elseif type == "speaker" then
|
||||||
local msg = "alarm sounder speaker reconnected"
|
local msg = "alarm sounder speaker reconnected"
|
||||||
@@ -300,6 +301,9 @@ local function main()
|
|||||||
if loop_clock.is_clock(param1) then
|
if loop_clock.is_clock(param1) then
|
||||||
-- main loop tick
|
-- main loop tick
|
||||||
|
|
||||||
|
-- iterate sessions
|
||||||
|
apisessions.iterate_all()
|
||||||
|
|
||||||
-- free any closed sessions
|
-- free any closed sessions
|
||||||
apisessions.free_all_closed()
|
apisessions.free_all_closed()
|
||||||
|
|
||||||
@@ -326,7 +330,7 @@ local function main()
|
|||||||
else
|
else
|
||||||
-- a non-clock/main watchdog timer event
|
-- a non-clock/main watchdog timer event
|
||||||
|
|
||||||
--check API watchdogs
|
-- check API watchdogs
|
||||||
apisessions.check_all_watchdogs(param1)
|
apisessions.check_all_watchdogs(param1)
|
||||||
|
|
||||||
-- notify timer callback dispatcher
|
-- notify timer callback dispatcher
|
||||||
@@ -354,7 +358,7 @@ local function main()
|
|||||||
end
|
end
|
||||||
elseif event == "monitor_touch" then
|
elseif event == "monitor_touch" then
|
||||||
-- handle a monitor touch event
|
-- handle a monitor touch event
|
||||||
renderer.handle_mouse(core.events.touch(param1, param2, param3))
|
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||||
elseif event == "speaker_audio_empty" then
|
elseif event == "speaker_audio_empty" then
|
||||||
-- handle speaker buffer emptied
|
-- handle speaker buffer emptied
|
||||||
sounder.continue()
|
sounder.continue()
|
||||||
@@ -385,4 +389,6 @@ if not xpcall(main, crash.handler) then
|
|||||||
pcall(renderer.close_ui)
|
pcall(renderer.close_ui)
|
||||||
pcall(sounder.stop)
|
pcall(sounder.stop)
|
||||||
crash.exit()
|
crash.exit()
|
||||||
|
else
|
||||||
|
log.close()
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ local DataIndicator = require("graphics.elements.indicators.data")
|
|||||||
local StateIndicator = require("graphics.elements.indicators.state")
|
local StateIndicator = require("graphics.elements.indicators.state")
|
||||||
local VerticalBar = require("graphics.elements.indicators.vbar")
|
local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||||
|
|
||||||
local cpair = core.graphics.cpair
|
local cpair = core.cpair
|
||||||
local border = core.graphics.border
|
local border = core.border
|
||||||
|
|
||||||
-- new boiler view
|
-- new boiler view
|
||||||
---@param root graphics_element parent
|
---@param root graphics_element parent
|
||||||
@@ -27,9 +27,9 @@ local function new_view(root, x, y, ps)
|
|||||||
local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg}
|
local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg}
|
||||||
local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg}
|
local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg}
|
||||||
|
|
||||||
ps.subscribe("computed_status", status.update)
|
status.register(ps, "computed_status", status.update)
|
||||||
ps.subscribe("temperature", temp.update)
|
temp.register(ps, "temperature", temp.update)
|
||||||
ps.subscribe("boil_rate", boil_r.update)
|
boil_r.register(ps, "boil_rate", boil_r.update)
|
||||||
|
|
||||||
TextBox{parent=boiler,text="H",x=2,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
TextBox{parent=boiler,text="H",x=2,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||||
TextBox{parent=boiler,text="W",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
TextBox{parent=boiler,text="W",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||||
@@ -41,10 +41,10 @@ local function new_view(root, x, y, ps)
|
|||||||
local steam = VerticalBar{parent=boiler,x=27,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
|
local steam = VerticalBar{parent=boiler,x=27,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
|
||||||
local ccool = VerticalBar{parent=boiler,x=28,y=1,fg_bg=cpair(colors.lightBlue,colors.gray),height=4,width=1}
|
local ccool = VerticalBar{parent=boiler,x=28,y=1,fg_bg=cpair(colors.lightBlue,colors.gray),height=4,width=1}
|
||||||
|
|
||||||
ps.subscribe("hcool_fill", hcool.update)
|
hcool.register(ps, "hcool_fill", hcool.update)
|
||||||
ps.subscribe("water_fill", water.update)
|
water.register(ps, "water_fill", water.update)
|
||||||
ps.subscribe("steam_fill", steam.update)
|
steam.register(ps, "steam_fill", steam.update)
|
||||||
ps.subscribe("ccool_fill", ccool.update)
|
ccool.register(ps, "ccool_fill", ccool.update)
|
||||||
end
|
end
|
||||||
|
|
||||||
return new_view
|
return new_view
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ local PowerIndicator = require("graphics.elements.indicators.power")
|
|||||||
local StateIndicator = require("graphics.elements.indicators.state")
|
local StateIndicator = require("graphics.elements.indicators.state")
|
||||||
local VerticalBar = require("graphics.elements.indicators.vbar")
|
local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||||
|
|
||||||
local cpair = core.graphics.cpair
|
local cpair = core.cpair
|
||||||
local border = core.graphics.border
|
local border = core.border
|
||||||
|
|
||||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
-- new induction matrix view
|
-- new induction matrix view
|
||||||
---@param root graphics_element parent
|
---@param root graphics_element parent
|
||||||
@@ -50,15 +50,15 @@ local function new_view(root, x, y, data, ps, id)
|
|||||||
local avg_in = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="Avg. In: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
|
local avg_in = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="Avg. In: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
|
||||||
local avg_out = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Avg. Out:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
|
local avg_out = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Avg. Out:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
|
||||||
|
|
||||||
ps.subscribe("computed_status", status.update)
|
status.register(ps, "computed_status", status.update)
|
||||||
ps.subscribe("energy", function (val) energy.update(util.joules_to_fe(val)) end)
|
energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end)
|
||||||
ps.subscribe("max_energy", function (val) capacity.update(util.joules_to_fe(val)) end)
|
capacity.register(ps, "max_energy", function (val) capacity.update(util.joules_to_fe(val)) end)
|
||||||
ps.subscribe("last_input", function (val) input.update(util.joules_to_fe(val)) end)
|
input.register(ps, "last_input", function (val) input.update(util.joules_to_fe(val)) end)
|
||||||
ps.subscribe("last_output", function (val) output.update(util.joules_to_fe(val)) end)
|
output.register(ps, "last_output", function (val) output.update(util.joules_to_fe(val)) end)
|
||||||
|
|
||||||
ps.subscribe("avg_charge", avg_chg.update)
|
avg_chg.register(ps, "avg_charge", avg_chg.update)
|
||||||
ps.subscribe("avg_inflow", avg_in.update)
|
avg_in.register(ps, "avg_inflow", avg_in.update)
|
||||||
ps.subscribe("avg_outflow", avg_out.update)
|
avg_out.register(ps, "avg_outflow", avg_out.update)
|
||||||
|
|
||||||
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill:",unit="%",format="%8.2f",value=0,width=18,fg_bg=text_fg_bg}
|
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill:",unit="%",format="%8.2f",value=0,width=18,fg_bg=text_fg_bg}
|
||||||
|
|
||||||
@@ -68,10 +68,10 @@ local function new_view(root, x, y, data, ps, id)
|
|||||||
TextBox{parent=rect,text="Transfer Capacity",x=11,y=17,height=1,width=17,fg_bg=label_fg_bg}
|
TextBox{parent=rect,text="Transfer Capacity",x=11,y=17,height=1,width=17,fg_bg=label_fg_bg}
|
||||||
local trans_cap = PowerIndicator{parent=rect,x=19,y=18,lu_colors=lu_col,label="",format="%5.2f",rate=true,value=0,width=12,fg_bg=text_fg_bg}
|
local trans_cap = PowerIndicator{parent=rect,x=19,y=18,lu_colors=lu_col,label="",format="%5.2f",rate=true,value=0,width=12,fg_bg=text_fg_bg}
|
||||||
|
|
||||||
ps.subscribe("cells", cells.update)
|
cells.register(ps, "cells", cells.update)
|
||||||
ps.subscribe("providers", providers.update)
|
providers.register(ps, "providers", providers.update)
|
||||||
ps.subscribe("energy_fill", function (val) fill.update(val * 100) end)
|
fill.register(ps, "energy_fill", function (val) fill.update(val * 100) end)
|
||||||
ps.subscribe("transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end)
|
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end)
|
||||||
|
|
||||||
local charge = VerticalBar{parent=rect,x=2,y=2,fg_bg=cpair(colors.green,colors.gray),height=17,width=4}
|
local charge = VerticalBar{parent=rect,x=2,y=2,fg_bg=cpair(colors.green,colors.gray),height=17,width=4}
|
||||||
local in_cap = VerticalBar{parent=rect,x=7,y=12,fg_bg=cpair(colors.red,colors.gray),height=7,width=1}
|
local in_cap = VerticalBar{parent=rect,x=7,y=12,fg_bg=cpair(colors.red,colors.gray),height=7,width=1}
|
||||||
@@ -88,9 +88,9 @@ local function new_view(root, x, y, data, ps, id)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
ps.subscribe("energy_fill", charge.update)
|
charge.register(ps, "energy_fill", charge.update)
|
||||||
ps.subscribe("last_input", function (val) in_cap.update(calc_saturation(val)) end)
|
in_cap.register(ps, "last_input", function (val) in_cap.update(calc_saturation(val)) end)
|
||||||
ps.subscribe("last_output", function (val) out_cap.update(calc_saturation(val)) end)
|
out_cap.register(ps, "last_output", function (val) out_cap.update(calc_saturation(val)) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return new_view
|
return new_view
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ local HazardButton = require("graphics.elements.controls.hazard_button")
|
|||||||
local RadioButton = require("graphics.elements.controls.radio_button")
|
local RadioButton = require("graphics.elements.controls.radio_button")
|
||||||
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
|
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
|
||||||
|
|
||||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
local cpair = core.graphics.cpair
|
local cpair = core.cpair
|
||||||
local border = core.graphics.border
|
local border = core.border
|
||||||
|
|
||||||
local period = core.flasher.PERIOD
|
local period = core.flasher.PERIOD
|
||||||
|
|
||||||
@@ -55,9 +55,9 @@ local function new_view(root, x, y)
|
|||||||
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=cpair(colors.green,colors.gray)}
|
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=cpair(colors.green,colors.gray)}
|
||||||
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
|
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
|
||||||
|
|
||||||
facility.ps.subscribe("all_sys_ok", all_ok.update)
|
all_ok.register(facility.ps, "all_sys_ok", all_ok.update)
|
||||||
facility.induction_ps_tbl[1].subscribe("computed_status", function (status) ind_mat.update(status > 1) end)
|
ind_mat.register(facility.induction_ps_tbl[1], "computed_status", function (status) ind_mat.update(status > 1) end)
|
||||||
facility.ps.subscribe("rad_computed_status", rad_mon.update)
|
rad_mon.register(facility.ps, "rad_computed_status", rad_mon.update)
|
||||||
|
|
||||||
main.line_break()
|
main.line_break()
|
||||||
|
|
||||||
@@ -66,10 +66,10 @@ local function new_view(root, x, y)
|
|||||||
local auto_ramp = IndicatorLight{parent=main,label="Process Ramping",colors=cpair(colors.white,colors.gray),flash=true,period=period.BLINK_250_MS}
|
local auto_ramp = IndicatorLight{parent=main,label="Process Ramping",colors=cpair(colors.white,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||||
local auto_sat = IndicatorLight{parent=main,label="Min/Max Burn Rate",colors=cpair(colors.yellow,colors.gray)}
|
local auto_sat = IndicatorLight{parent=main,label="Min/Max Burn Rate",colors=cpair(colors.yellow,colors.gray)}
|
||||||
|
|
||||||
facility.ps.subscribe("auto_ready", auto_ready.update)
|
auto_ready.register(facility.ps, "auto_ready", auto_ready.update)
|
||||||
facility.ps.subscribe("auto_active", auto_act.update)
|
auto_act.register(facility.ps, "auto_active", auto_act.update)
|
||||||
facility.ps.subscribe("auto_ramping", auto_ramp.update)
|
auto_ramp.register(facility.ps, "auto_ramping", auto_ramp.update)
|
||||||
facility.ps.subscribe("auto_saturated", auto_sat.update)
|
auto_sat.register(facility.ps, "auto_saturated", auto_sat.update)
|
||||||
|
|
||||||
main.line_break()
|
main.line_break()
|
||||||
|
|
||||||
@@ -80,20 +80,20 @@ local function new_view(root, x, y)
|
|||||||
local fac_rad_h = IndicatorLight{parent=main,label="Facility Radiation High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
local fac_rad_h = IndicatorLight{parent=main,label="Facility Radiation High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||||
local gen_fault = IndicatorLight{parent=main,label="Gen. Control Fault",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
|
local gen_fault = IndicatorLight{parent=main,label="Gen. Control Fault",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||||
|
|
||||||
facility.ps.subscribe("auto_scram", auto_scram.update)
|
auto_scram.register(facility.ps, "auto_scram", auto_scram.update)
|
||||||
facility.ps.subscribe("as_matrix_dc", matrix_dc.update)
|
matrix_dc.register(facility.ps, "as_matrix_dc", matrix_dc.update)
|
||||||
facility.ps.subscribe("as_matrix_fill", matrix_fill.update)
|
matrix_fill.register(facility.ps, "as_matrix_fill", matrix_fill.update)
|
||||||
facility.ps.subscribe("as_crit_alarm", unit_crit.update)
|
unit_crit.register(facility.ps, "as_crit_alarm", unit_crit.update)
|
||||||
facility.ps.subscribe("as_radiation", fac_rad_h.update)
|
fac_rad_h.register(facility.ps, "as_radiation", fac_rad_h.update)
|
||||||
facility.ps.subscribe("as_gen_fault", gen_fault.update)
|
gen_fault.register(facility.ps, "as_gen_fault", gen_fault.update)
|
||||||
|
|
||||||
TextBox{parent=main,y=23,text="Radiation",height=1,width=13,fg_bg=style.label}
|
TextBox{parent=main,y=23,text="Radiation",height=1,width=13,fg_bg=style.label}
|
||||||
local radiation = RadIndicator{parent=main,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
|
local radiation = RadIndicator{parent=main,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
|
||||||
facility.ps.subscribe("radiation", radiation.update)
|
radiation.register(facility.ps, "radiation", radiation.update)
|
||||||
|
|
||||||
TextBox{parent=main,x=15,y=23,text="Linked RTUs",height=1,width=11,fg_bg=style.label}
|
TextBox{parent=main,x=15,y=23,text="Linked RTUs",height=1,width=11,fg_bg=style.label}
|
||||||
local rtu_count = DataIndicator{parent=main,x=15,y=24,label="",format="%11d",value=0,lu_colors=lu_cpair,width=11,fg_bg=bw_fg_bg}
|
local rtu_count = DataIndicator{parent=main,x=15,y=24,label="",format="%11d",value=0,lu_colors=lu_cpair,width=11,fg_bg=bw_fg_bg}
|
||||||
facility.ps.subscribe("rtu_count", rtu_count.update)
|
rtu_count.register(facility.ps, "rtu_count", rtu_count.update)
|
||||||
|
|
||||||
---------------------
|
---------------------
|
||||||
-- process control --
|
-- process control --
|
||||||
@@ -115,8 +115,8 @@ local function new_view(root, x, y)
|
|||||||
TextBox{parent=burn_target,x=18,y=2,text="mB/t"}
|
TextBox{parent=burn_target,x=18,y=2,text="mB/t"}
|
||||||
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
|
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
|
||||||
|
|
||||||
facility.ps.subscribe("process_burn_target", b_target.set_value)
|
b_target.register(facility.ps, "process_burn_target", b_target.set_value)
|
||||||
facility.ps.subscribe("burn_sum", burn_sum.update)
|
burn_sum.register(facility.ps, "burn_sum", burn_sum.update)
|
||||||
|
|
||||||
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
|
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
|
||||||
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
|
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
|
||||||
@@ -126,8 +126,8 @@ local function new_view(root, x, y)
|
|||||||
TextBox{parent=chg_target,x=18,y=2,text="MFE"}
|
TextBox{parent=chg_target,x=18,y=2,text="MFE"}
|
||||||
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
|
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
|
||||||
|
|
||||||
facility.ps.subscribe("process_charge_target", c_target.set_value)
|
c_target.register(facility.ps, "process_charge_target", c_target.set_value)
|
||||||
facility.induction_ps_tbl[1].subscribe("energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end)
|
cur_charge.register(facility.induction_ps_tbl[1], "energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end)
|
||||||
|
|
||||||
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
|
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
|
||||||
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
|
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
|
||||||
@@ -137,8 +137,8 @@ local function new_view(root, x, y)
|
|||||||
TextBox{parent=gen_target,x=18,y=2,text="kFE/t"}
|
TextBox{parent=gen_target,x=18,y=2,text="kFE/t"}
|
||||||
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
|
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
|
||||||
|
|
||||||
facility.ps.subscribe("process_gen_target", g_target.set_value)
|
g_target.register(facility.ps, "process_gen_target", g_target.set_value)
|
||||||
facility.induction_ps_tbl[1].subscribe("last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end)
|
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end)
|
||||||
|
|
||||||
-----------------
|
-----------------
|
||||||
-- unit limits --
|
-- unit limits --
|
||||||
@@ -160,12 +160,12 @@ local function new_view(root, x, y)
|
|||||||
rate_limits[i] = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
|
rate_limits[i] = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
|
||||||
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1}
|
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1}
|
||||||
|
|
||||||
unit.unit_ps.subscribe("max_burn", rate_limits[i].set_max)
|
rate_limits[i].register(unit.unit_ps, "max_burn", rate_limits[i].set_max)
|
||||||
unit.unit_ps.subscribe("burn_limit", rate_limits[i].set_value)
|
rate_limits[i].register(unit.unit_ps, "burn_limit", rate_limits[i].set_value)
|
||||||
|
|
||||||
local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(colors.black,colors.black),width=14,fg_bg=cpair(colors.black,colors.brown)}
|
local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(colors.black,colors.black),width=14,fg_bg=cpair(colors.black,colors.brown)}
|
||||||
|
|
||||||
unit.unit_ps.subscribe("act_burn_rate", cur_burn.update)
|
cur_burn.register(unit.unit_ps, "act_burn_rate", cur_burn.update)
|
||||||
end
|
end
|
||||||
|
|
||||||
-------------------
|
-------------------
|
||||||
@@ -186,8 +186,8 @@ local function new_view(root, x, y)
|
|||||||
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(colors.green,colors.gray)}
|
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(colors.green,colors.gray)}
|
||||||
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||||
|
|
||||||
unit.unit_ps.subscribe("U_AutoReady", ready.update)
|
ready.register(unit.unit_ps, "U_AutoReady", ready.update)
|
||||||
unit.unit_ps.subscribe("U_AutoDegraded", degraded.update)
|
degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update)
|
||||||
end
|
end
|
||||||
|
|
||||||
-------------------------
|
-------------------------
|
||||||
@@ -197,14 +197,14 @@ local function new_view(root, x, y)
|
|||||||
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
|
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
|
||||||
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.purple,colors.black),radio_bg=colors.gray}
|
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.purple,colors.black),radio_bg=colors.gray}
|
||||||
|
|
||||||
facility.ps.subscribe("process_mode", mode.set_value)
|
mode.register(facility.ps, "process_mode", mode.set_value)
|
||||||
|
|
||||||
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
|
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
|
||||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
|
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
|
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
|
||||||
|
|
||||||
facility.ps.subscribe("status_line_1", stat_line_1.set_value)
|
stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value)
|
||||||
facility.ps.subscribe("status_line_2", stat_line_2.set_value)
|
stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value)
|
||||||
|
|
||||||
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=cpair(colors.gray,colors.white)}
|
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=cpair(colors.gray,colors.white)}
|
||||||
|
|
||||||
@@ -233,11 +233,14 @@ local function new_view(root, x, y)
|
|||||||
tcd.dispatch(0.2, function () save.on_response(ack) end)
|
tcd.dispatch(0.2, function () save.on_response(ack) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
facility.ps.subscribe("auto_ready", function (ready)
|
start.register(facility.ps, "auto_ready", function (ready)
|
||||||
if ready and (not facility.auto_active) then start.enable() else start.disable() end
|
if ready and (not facility.auto_active) then start.enable() else start.disable() end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
facility.ps.subscribe("auto_active", function (active)
|
-- REGISTER_NOTE: for optimization/brevity, due to not deleting anything but the whole element tree when it comes
|
||||||
|
-- to the process control display and coordinator GUI as a whole, child elements will not directly be registered here
|
||||||
|
-- (preventing garbage collection until the parent 'proc' is deleted)
|
||||||
|
proc.register(facility.ps, "auto_active", function (active)
|
||||||
if active then
|
if active then
|
||||||
b_target.disable()
|
b_target.disable()
|
||||||
c_target.disable()
|
c_target.disable()
|
||||||
@@ -246,9 +249,7 @@ local function new_view(root, x, y)
|
|||||||
mode.disable()
|
mode.disable()
|
||||||
start.disable()
|
start.disable()
|
||||||
|
|
||||||
for i = 1, #rate_limits do
|
for i = 1, #rate_limits do rate_limits[i].disable() end
|
||||||
rate_limits[i].disable()
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
b_target.enable()
|
b_target.enable()
|
||||||
c_target.enable()
|
c_target.enable()
|
||||||
@@ -257,9 +258,7 @@ local function new_view(root, x, y)
|
|||||||
mode.enable()
|
mode.enable()
|
||||||
if facility.auto_ready then start.enable() end
|
if facility.auto_ready then start.enable() end
|
||||||
|
|
||||||
for i = 1, #rate_limits do
|
for i = 1, #rate_limits do rate_limits[i].enable() end
|
||||||
rate_limits[i].enable()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,16 +11,15 @@ local DataIndicator = require("graphics.elements.indicators.data")
|
|||||||
local HorizontalBar = require("graphics.elements.indicators.hbar")
|
local HorizontalBar = require("graphics.elements.indicators.hbar")
|
||||||
local StateIndicator = require("graphics.elements.indicators.state")
|
local StateIndicator = require("graphics.elements.indicators.state")
|
||||||
|
|
||||||
local cpair = core.graphics.cpair
|
local cpair = core.cpair
|
||||||
local border = core.graphics.border
|
local border = core.border
|
||||||
|
|
||||||
-- create new reactor view
|
-- create new reactor view
|
||||||
---@param root graphics_element parent
|
---@param root graphics_element parent
|
||||||
---@param x integer top left x
|
---@param x integer top left x
|
||||||
---@param y integer top left y
|
---@param y integer top left y
|
||||||
---@param data reactor_db reactor data
|
|
||||||
---@param ps psil ps interface
|
---@param ps psil ps interface
|
||||||
local function new_view(root, x, y, data, ps)
|
local function new_view(root, x, y, ps)
|
||||||
local reactor = Rectangle{parent=root,border=border(1, colors.gray, true),width=30,height=7,x=x,y=y}
|
local reactor = Rectangle{parent=root,border=border(1, colors.gray, true),width=30,height=7,x=x,y=y}
|
||||||
|
|
||||||
local text_fg_bg = cpair(colors.black, colors.lightGray)
|
local text_fg_bg = cpair(colors.black, colors.lightGray)
|
||||||
@@ -31,10 +30,10 @@ local function new_view(root, x, y, data, ps)
|
|||||||
local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg}
|
local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||||
local heating_r = DataIndicator{parent=reactor,x=2,y=5,lu_colors=lu_col,label="Heating:",unit="mB/t",format="%12.0f",value=0,commas=true,width=26,fg_bg=text_fg_bg}
|
local heating_r = DataIndicator{parent=reactor,x=2,y=5,lu_colors=lu_col,label="Heating:",unit="mB/t",format="%12.0f",value=0,commas=true,width=26,fg_bg=text_fg_bg}
|
||||||
|
|
||||||
ps.subscribe("computed_status", status.update)
|
status.register(ps, "computed_status", status.update)
|
||||||
ps.subscribe("temp", core_temp.update)
|
core_temp.register(ps, "temp", core_temp.update)
|
||||||
ps.subscribe("act_burn_rate", burn_r.update)
|
burn_r.register(ps, "act_burn_rate", burn_r.update)
|
||||||
ps.subscribe("heating_rate", heating_r.update)
|
heating_r.register(ps, "heating_rate", heating_r.update)
|
||||||
|
|
||||||
local reactor_fills = Rectangle{parent=root,border=border(1, colors.gray, true),width=24,height=7,x=(x + 29),y=y}
|
local reactor_fills = Rectangle{parent=root,border=border(1, colors.gray, true),width=24,height=7,x=(x + 29),y=y}
|
||||||
|
|
||||||
@@ -48,7 +47,7 @@ local function new_view(root, x, y, data, ps)
|
|||||||
local hcool = HorizontalBar{parent=reactor_fills,x=8,y=4,show_percent=true,bar_fg_bg=cpair(colors.white,colors.gray),height=1,width=14}
|
local hcool = HorizontalBar{parent=reactor_fills,x=8,y=4,show_percent=true,bar_fg_bg=cpair(colors.white,colors.gray),height=1,width=14}
|
||||||
local waste = HorizontalBar{parent=reactor_fills,x=8,y=5,show_percent=true,bar_fg_bg=cpair(colors.brown,colors.gray),height=1,width=14}
|
local waste = HorizontalBar{parent=reactor_fills,x=8,y=5,show_percent=true,bar_fg_bg=cpair(colors.brown,colors.gray),height=1,width=14}
|
||||||
|
|
||||||
ps.subscribe("ccool_type", function (type)
|
ccool.register(ps, "ccool_type", function (type)
|
||||||
if type == types.FLUID.SODIUM then
|
if type == types.FLUID.SODIUM then
|
||||||
ccool.recolor(cpair(colors.lightBlue, colors.gray))
|
ccool.recolor(cpair(colors.lightBlue, colors.gray))
|
||||||
else
|
else
|
||||||
@@ -56,7 +55,7 @@ local function new_view(root, x, y, data, ps)
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ps.subscribe("hcool_type", function (type)
|
hcool.register(ps, "hcool_type", function (type)
|
||||||
if type == types.FLUID.SUPERHEATED_SODIUM then
|
if type == types.FLUID.SUPERHEATED_SODIUM then
|
||||||
hcool.recolor(cpair(colors.orange, colors.gray))
|
hcool.recolor(cpair(colors.orange, colors.gray))
|
||||||
else
|
else
|
||||||
@@ -64,10 +63,10 @@ local function new_view(root, x, y, data, ps)
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ps.subscribe("fuel_fill", fuel.update)
|
fuel.register(ps, "fuel_fill", fuel.update)
|
||||||
ps.subscribe("ccool_fill", ccool.update)
|
ccool.register(ps, "ccool_fill", ccool.update)
|
||||||
ps.subscribe("hcool_fill", hcool.update)
|
hcool.register(ps, "hcool_fill", hcool.update)
|
||||||
ps.subscribe("waste_fill", waste.update)
|
waste.register(ps, "waste_fill", waste.update)
|
||||||
end
|
end
|
||||||
|
|
||||||
return new_view
|
return new_view
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ local PowerIndicator = require("graphics.elements.indicators.power")
|
|||||||
local StateIndicator = require("graphics.elements.indicators.state")
|
local StateIndicator = require("graphics.elements.indicators.state")
|
||||||
local VerticalBar = require("graphics.elements.indicators.vbar")
|
local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||||
|
|
||||||
local cpair = core.graphics.cpair
|
local cpair = core.cpair
|
||||||
local border = core.graphics.border
|
local border = core.border
|
||||||
|
|
||||||
-- new turbine view
|
-- new turbine view
|
||||||
---@param root graphics_element parent
|
---@param root graphics_element parent
|
||||||
@@ -30,9 +30,9 @@ local function new_view(root, x, y, ps)
|
|||||||
local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg_bg}
|
local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg_bg}
|
||||||
local flow_rate = DataIndicator{parent=turbine,x=5,y=4,lu_colors=lu_col,label="",unit="mB/t",format="%10.0f",value=0,commas=true,width=16,fg_bg=text_fg_bg}
|
local flow_rate = DataIndicator{parent=turbine,x=5,y=4,lu_colors=lu_col,label="",unit="mB/t",format="%10.0f",value=0,commas=true,width=16,fg_bg=text_fg_bg}
|
||||||
|
|
||||||
ps.subscribe("computed_status", status.update)
|
status.register(ps, "computed_status", status.update)
|
||||||
ps.subscribe("prod_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end)
|
prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end)
|
||||||
ps.subscribe("flow_rate", flow_rate.update)
|
flow_rate.register(ps, "flow_rate", flow_rate.update)
|
||||||
|
|
||||||
local steam = VerticalBar{parent=turbine,x=2,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
|
local steam = VerticalBar{parent=turbine,x=2,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
|
||||||
local energy = VerticalBar{parent=turbine,x=3,y=1,fg_bg=cpair(colors.green,colors.gray),height=4,width=1}
|
local energy = VerticalBar{parent=turbine,x=3,y=1,fg_bg=cpair(colors.green,colors.gray),height=4,width=1}
|
||||||
@@ -40,8 +40,8 @@ local function new_view(root, x, y, ps)
|
|||||||
TextBox{parent=turbine,text="S",x=2,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
TextBox{parent=turbine,text="S",x=2,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||||
TextBox{parent=turbine,text="E",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
TextBox{parent=turbine,text="E",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||||
|
|
||||||
ps.subscribe("steam_fill", steam.update)
|
steam.register(ps, "steam_fill", steam.update)
|
||||||
ps.subscribe("energy_fill", energy.update)
|
energy.register(ps, "energy_fill", energy.update)
|
||||||
end
|
end
|
||||||
|
|
||||||
return new_view
|
return new_view
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ local PushButton = require("graphics.elements.controls.push_button")
|
|||||||
local RadioButton = require("graphics.elements.controls.radio_button")
|
local RadioButton = require("graphics.elements.controls.radio_button")
|
||||||
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
|
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
|
||||||
|
|
||||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
local cpair = core.graphics.cpair
|
local cpair = core.cpair
|
||||||
local border = core.graphics.border
|
local border = core.border
|
||||||
|
|
||||||
local period = core.flasher.PERIOD
|
local period = core.flasher.PERIOD
|
||||||
|
|
||||||
@@ -79,16 +79,16 @@ local function init(parent, id)
|
|||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
local core_map = CoreMap{parent=main,x=2,y=3,reactor_l=18,reactor_w=18}
|
local core_map = CoreMap{parent=main,x=2,y=3,reactor_l=18,reactor_w=18}
|
||||||
u_ps.subscribe("temp", core_map.update)
|
core_map.register(u_ps, "temp", core_map.update)
|
||||||
u_ps.subscribe("size", function (s) core_map.resize(s[1], s[2]) end)
|
core_map.register(u_ps, "size", function (s) core_map.resize(s[1], s[2]) end)
|
||||||
|
|
||||||
TextBox{parent=main,x=12,y=22,text="Heating Rate",height=1,width=12,fg_bg=style.label}
|
TextBox{parent=main,x=12,y=22,text="Heating Rate",height=1,width=12,fg_bg=style.label}
|
||||||
local heating_r = DataIndicator{parent=main,x=12,label="",format="%14.0f",value=0,unit="mB/t",commas=true,lu_colors=lu_cpair,width=19,fg_bg=bw_fg_bg}
|
local heating_r = DataIndicator{parent=main,x=12,label="",format="%14.0f",value=0,unit="mB/t",commas=true,lu_colors=lu_cpair,width=19,fg_bg=bw_fg_bg}
|
||||||
u_ps.subscribe("heating_rate", heating_r.update)
|
heating_r.register(u_ps, "heating_rate", heating_r.update)
|
||||||
|
|
||||||
TextBox{parent=main,x=12,y=25,text="Commanded Burn Rate",height=1,width=19,fg_bg=style.label}
|
TextBox{parent=main,x=12,y=25,text="Commanded Burn Rate",height=1,width=19,fg_bg=style.label}
|
||||||
local burn_r = DataIndicator{parent=main,x=12,label="",format="%14.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=19,fg_bg=bw_fg_bg}
|
local burn_r = DataIndicator{parent=main,x=12,label="",format="%14.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=19,fg_bg=bw_fg_bg}
|
||||||
u_ps.subscribe("burn_rate", burn_r.update)
|
burn_r.register(u_ps, "burn_rate", burn_r.update)
|
||||||
|
|
||||||
TextBox{parent=main,text="F",x=2,y=22,width=1,height=1,fg_bg=style.label}
|
TextBox{parent=main,text="F",x=2,y=22,width=1,height=1,fg_bg=style.label}
|
||||||
TextBox{parent=main,text="C",x=4,y=22,width=1,height=1,fg_bg=style.label}
|
TextBox{parent=main,text="C",x=4,y=22,width=1,height=1,fg_bg=style.label}
|
||||||
@@ -102,12 +102,12 @@ local function init(parent, id)
|
|||||||
local hcool = VerticalBar{parent=main,x=8,y=23,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
|
local hcool = VerticalBar{parent=main,x=8,y=23,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
|
||||||
local waste = VerticalBar{parent=main,x=10,y=23,fg_bg=cpair(colors.brown,colors.gray),height=4,width=1}
|
local waste = VerticalBar{parent=main,x=10,y=23,fg_bg=cpair(colors.brown,colors.gray),height=4,width=1}
|
||||||
|
|
||||||
u_ps.subscribe("fuel_fill", fuel.update)
|
fuel.register(u_ps, "fuel_fill", fuel.update)
|
||||||
u_ps.subscribe("ccool_fill", ccool.update)
|
ccool.register(u_ps, "ccool_fill", ccool.update)
|
||||||
u_ps.subscribe("hcool_fill", hcool.update)
|
hcool.register(u_ps, "hcool_fill", hcool.update)
|
||||||
u_ps.subscribe("waste_fill", waste.update)
|
waste.register(u_ps, "waste_fill", waste.update)
|
||||||
|
|
||||||
u_ps.subscribe("ccool_type", function (type)
|
ccool.register(u_ps, "ccool_type", function (type)
|
||||||
if type == "mekanism:sodium" then
|
if type == "mekanism:sodium" then
|
||||||
ccool.recolor(cpair(colors.lightBlue, colors.gray))
|
ccool.recolor(cpair(colors.lightBlue, colors.gray))
|
||||||
else
|
else
|
||||||
@@ -115,7 +115,7 @@ local function init(parent, id)
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
u_ps.subscribe("hcool_type", function (type)
|
hcool.register(u_ps, "hcool_type", function (type)
|
||||||
if type == "mekanism:superheated_sodium" then
|
if type == "mekanism:superheated_sodium" then
|
||||||
hcool.recolor(cpair(colors.orange, colors.gray))
|
hcool.recolor(cpair(colors.orange, colors.gray))
|
||||||
else
|
else
|
||||||
@@ -125,19 +125,19 @@ local function init(parent, id)
|
|||||||
|
|
||||||
TextBox{parent=main,x=32,y=22,text="Core Temp",height=1,width=9,fg_bg=style.label}
|
TextBox{parent=main,x=32,y=22,text="Core Temp",height=1,width=9,fg_bg=style.label}
|
||||||
local core_temp = DataIndicator{parent=main,x=32,label="",format="%11.2f",value=0,unit="K",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
|
local core_temp = DataIndicator{parent=main,x=32,label="",format="%11.2f",value=0,unit="K",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
|
||||||
u_ps.subscribe("temp", core_temp.update)
|
core_temp.register(u_ps, "temp", core_temp.update)
|
||||||
|
|
||||||
TextBox{parent=main,x=32,y=25,text="Burn Rate",height=1,width=9,fg_bg=style.label}
|
TextBox{parent=main,x=32,y=25,text="Burn Rate",height=1,width=9,fg_bg=style.label}
|
||||||
local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
|
local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
|
||||||
u_ps.subscribe("act_burn_rate", act_burn_r.update)
|
act_burn_r.register(u_ps, "act_burn_rate", act_burn_r.update)
|
||||||
|
|
||||||
TextBox{parent=main,x=32,y=28,text="Damage",height=1,width=6,fg_bg=style.label}
|
TextBox{parent=main,x=32,y=28,text="Damage",height=1,width=6,fg_bg=style.label}
|
||||||
local damage_p = DataIndicator{parent=main,x=32,label="",format="%11.0f",value=0,unit="%",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
|
local damage_p = DataIndicator{parent=main,x=32,label="",format="%11.0f",value=0,unit="%",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
|
||||||
u_ps.subscribe("damage", damage_p.update)
|
damage_p.register(u_ps, "damage", damage_p.update)
|
||||||
|
|
||||||
TextBox{parent=main,x=32,y=31,text="Radiation",height=1,width=21,fg_bg=style.label}
|
TextBox{parent=main,x=32,y=31,text="Radiation",height=1,width=21,fg_bg=style.label}
|
||||||
local radiation = RadIndicator{parent=main,x=32,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
|
local radiation = RadIndicator{parent=main,x=32,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
|
||||||
u_ps.subscribe("radiation", radiation.update)
|
radiation.register(u_ps, "radiation", radiation.update)
|
||||||
|
|
||||||
-------------------
|
-------------------
|
||||||
-- system status --
|
-- system status --
|
||||||
@@ -147,8 +147,8 @@ local function init(parent, id)
|
|||||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
|
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
|
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
|
||||||
|
|
||||||
u_ps.subscribe("U_StatusLine1", stat_line_1.set_value)
|
stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value)
|
||||||
u_ps.subscribe("U_StatusLine2", stat_line_2.set_value)
|
stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value)
|
||||||
|
|
||||||
-----------------
|
-----------------
|
||||||
-- annunciator --
|
-- annunciator --
|
||||||
@@ -163,9 +163,9 @@ local function init(parent, id)
|
|||||||
local plc_hbeat = IndicatorLight{parent=annunciator,label="PLC Heartbeat",colors=cpair(colors.white,colors.gray)}
|
local plc_hbeat = IndicatorLight{parent=annunciator,label="PLC Heartbeat",colors=cpair(colors.white,colors.gray)}
|
||||||
local rad_mon = TriIndicatorLight{parent=annunciator,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
|
local rad_mon = TriIndicatorLight{parent=annunciator,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
|
||||||
|
|
||||||
u_ps.subscribe("PLCOnline", plc_online.update)
|
plc_online.register(u_ps, "PLCOnline", plc_online.update)
|
||||||
u_ps.subscribe("PLCHeartbeat", plc_hbeat.update)
|
plc_hbeat.register(u_ps, "PLCHeartbeat", plc_hbeat.update)
|
||||||
u_ps.subscribe("RadiationMonitor", rad_mon.update)
|
rad_mon.register(u_ps, "RadiationMonitor", rad_mon.update)
|
||||||
|
|
||||||
annunciator.line_break()
|
annunciator.line_break()
|
||||||
|
|
||||||
@@ -173,8 +173,8 @@ local function init(parent, id)
|
|||||||
local r_active = IndicatorLight{parent=annunciator,label="Active",colors=cpair(colors.green,colors.gray)}
|
local r_active = IndicatorLight{parent=annunciator,label="Active",colors=cpair(colors.green,colors.gray)}
|
||||||
local r_auto = IndicatorLight{parent=annunciator,label="Automatic Control",colors=cpair(colors.white,colors.gray)}
|
local r_auto = IndicatorLight{parent=annunciator,label="Automatic Control",colors=cpair(colors.white,colors.gray)}
|
||||||
|
|
||||||
u_ps.subscribe("status", r_active.update)
|
r_active.register(u_ps, "status", r_active.update)
|
||||||
u_ps.subscribe("AutoControl", r_auto.update)
|
r_auto.register(u_ps, "AutoControl", r_auto.update)
|
||||||
|
|
||||||
-- main unit transient/warning annunciator panel
|
-- main unit transient/warning annunciator panel
|
||||||
local r_scram = IndicatorLight{parent=annunciator,label="Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
|
local r_scram = IndicatorLight{parent=annunciator,label="Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
|
||||||
@@ -190,18 +190,18 @@ local function init(parent, id)
|
|||||||
local r_wloc = IndicatorLight{parent=annunciator,label="Waste Line Occlusion",colors=cpair(colors.yellow,colors.gray)}
|
local r_wloc = IndicatorLight{parent=annunciator,label="Waste Line Occlusion",colors=cpair(colors.yellow,colors.gray)}
|
||||||
local r_hsrt = IndicatorLight{parent=annunciator,label="Startup Rate High",colors=cpair(colors.yellow,colors.gray)}
|
local r_hsrt = IndicatorLight{parent=annunciator,label="Startup Rate High",colors=cpair(colors.yellow,colors.gray)}
|
||||||
|
|
||||||
u_ps.subscribe("ReactorSCRAM", r_scram.update)
|
r_scram.register(u_ps, "ReactorSCRAM", r_scram.update)
|
||||||
u_ps.subscribe("ManualReactorSCRAM", r_mscrm.update)
|
r_mscrm.register(u_ps, "ManualReactorSCRAM", r_mscrm.update)
|
||||||
u_ps.subscribe("AutoReactorSCRAM", r_ascrm.update)
|
r_ascrm.register(u_ps, "AutoReactorSCRAM", r_ascrm.update)
|
||||||
u_ps.subscribe("RadiationWarning", rad_wrn.update)
|
rad_wrn.register(u_ps, "RadiationWarning", rad_wrn.update)
|
||||||
u_ps.subscribe("RCPTrip", r_rtrip.update)
|
r_rtrip.register(u_ps, "RCPTrip", r_rtrip.update)
|
||||||
u_ps.subscribe("RCSFlowLow", r_cflow.update)
|
r_cflow.register(u_ps, "RCSFlowLow", r_cflow.update)
|
||||||
u_ps.subscribe("CoolantLevelLow", r_clow.update)
|
r_clow.register(u_ps, "CoolantLevelLow", r_clow.update)
|
||||||
u_ps.subscribe("ReactorTempHigh", r_temp.update)
|
r_temp.register(u_ps, "ReactorTempHigh", r_temp.update)
|
||||||
u_ps.subscribe("ReactorHighDeltaT", r_rhdt.update)
|
r_rhdt.register(u_ps, "ReactorHighDeltaT", r_rhdt.update)
|
||||||
u_ps.subscribe("FuelInputRateLow", r_firl.update)
|
r_firl.register(u_ps, "FuelInputRateLow", r_firl.update)
|
||||||
u_ps.subscribe("WasteLineOcclusion", r_wloc.update)
|
r_wloc.register(u_ps, "WasteLineOcclusion", r_wloc.update)
|
||||||
u_ps.subscribe("HighStartupRate", r_hsrt.update)
|
r_hsrt.register(u_ps, "HighStartupRate", r_hsrt.update)
|
||||||
|
|
||||||
-- RPS annunciator panel
|
-- RPS annunciator panel
|
||||||
|
|
||||||
@@ -220,16 +220,16 @@ local function init(parent, id)
|
|||||||
local rps_tmo = IndicatorLight{parent=rps_annunc,label="Connection Timeout",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
|
local rps_tmo = IndicatorLight{parent=rps_annunc,label="Connection Timeout",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||||
local rps_sfl = IndicatorLight{parent=rps_annunc,label="System Failure",colors=cpair(colors.orange,colors.gray),flash=true,period=period.BLINK_500_MS}
|
local rps_sfl = IndicatorLight{parent=rps_annunc,label="System Failure",colors=cpair(colors.orange,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||||
|
|
||||||
u_ps.subscribe("rps_tripped", rps_trp.update)
|
rps_trp.register(u_ps, "rps_tripped", rps_trp.update)
|
||||||
u_ps.subscribe("high_dmg", rps_dmg.update)
|
rps_dmg.register(u_ps, "high_dmg", rps_dmg.update)
|
||||||
u_ps.subscribe("ex_hcool", rps_exh.update)
|
rps_exh.register(u_ps, "ex_hcool", rps_exh.update)
|
||||||
u_ps.subscribe("ex_waste", rps_exw.update)
|
rps_exw.register(u_ps, "ex_waste", rps_exw.update)
|
||||||
u_ps.subscribe("high_temp", rps_tmp.update)
|
rps_tmp.register(u_ps, "high_temp", rps_tmp.update)
|
||||||
u_ps.subscribe("no_fuel", rps_nof.update)
|
rps_nof.register(u_ps, "no_fuel", rps_nof.update)
|
||||||
u_ps.subscribe("low_cool", rps_loc.update)
|
rps_loc.register(u_ps, "low_cool", rps_loc.update)
|
||||||
u_ps.subscribe("fault", rps_flt.update)
|
rps_flt.register(u_ps, "fault", rps_flt.update)
|
||||||
u_ps.subscribe("timeout", rps_tmo.update)
|
rps_tmo.register(u_ps, "timeout", rps_tmo.update)
|
||||||
u_ps.subscribe("sys_fail", rps_sfl.update)
|
rps_sfl.register(u_ps, "sys_fail", rps_sfl.update)
|
||||||
|
|
||||||
-- cooling annunciator panel
|
-- cooling annunciator panel
|
||||||
|
|
||||||
@@ -245,12 +245,12 @@ local function init(parent, id)
|
|||||||
local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
|
local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
|
||||||
local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)}
|
local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)}
|
||||||
|
|
||||||
u_ps.subscribe("RCSFault", c_flt.update)
|
c_flt.register(u_ps, "RCSFault", c_flt.update)
|
||||||
u_ps.subscribe("EmergencyCoolant", c_emg.update)
|
c_emg.register(u_ps, "EmergencyCoolant", c_emg.update)
|
||||||
u_ps.subscribe("CoolantFeedMismatch", c_cfm.update)
|
c_cfm.register(u_ps, "CoolantFeedMismatch", c_cfm.update)
|
||||||
u_ps.subscribe("BoilRateMismatch", c_brm.update)
|
c_brm.register(u_ps, "BoilRateMismatch", c_brm.update)
|
||||||
u_ps.subscribe("SteamFeedMismatch", c_sfm.update)
|
c_sfm.register(u_ps, "SteamFeedMismatch", c_sfm.update)
|
||||||
u_ps.subscribe("MaxWaterReturnFeed", c_mwrf.update)
|
c_mwrf.register(u_ps, "MaxWaterReturnFeed", c_mwrf.update)
|
||||||
|
|
||||||
local available_space = 16 - (unit.num_boilers * 2 + unit.num_turbines * 4)
|
local available_space = 16 - (unit.num_boilers * 2 + unit.num_turbines * 4)
|
||||||
|
|
||||||
@@ -267,11 +267,11 @@ local function init(parent, id)
|
|||||||
if unit.num_boilers > 0 then
|
if unit.num_boilers > 0 then
|
||||||
TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=cpair(colors.red,colors.gray)}
|
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=cpair(colors.red,colors.gray)}
|
||||||
b_ps[1].subscribe("WasterLevelLow", b1_wll.update)
|
b1_wll.register(b_ps[1], "WasterLevelLow", b1_wll.update)
|
||||||
|
|
||||||
TextBox{parent=rcs_tags,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
|
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
|
||||||
b_ps[1].subscribe("HeatingRateLow", b1_hr.update)
|
b1_hr.register(b_ps[1], "HeatingRateLow", b1_hr.update)
|
||||||
end
|
end
|
||||||
if unit.num_boilers > 1 then
|
if unit.num_boilers > 1 then
|
||||||
-- note, can't (shouldn't for sure...) have 0 turbines
|
-- note, can't (shouldn't for sure...) have 0 turbines
|
||||||
@@ -283,11 +283,11 @@ local function init(parent, id)
|
|||||||
|
|
||||||
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=cpair(colors.red,colors.gray)}
|
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=cpair(colors.red,colors.gray)}
|
||||||
b_ps[2].subscribe("WasterLevelLow", b2_wll.update)
|
b2_wll.register(b_ps[2], "WasterLevelLow", b2_wll.update)
|
||||||
|
|
||||||
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
|
local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
|
||||||
b_ps[2].subscribe("HeatingRateLow", b2_hr.update)
|
b2_hr.register(b_ps[2], "HeatingRateLow", b2_hr.update)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- turbine annunciator panels
|
-- turbine annunciator panels
|
||||||
@@ -296,19 +296,19 @@ local function init(parent, id)
|
|||||||
|
|
||||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
||||||
t_ps[1].subscribe("SteamDumpOpen", t1_sdo.update)
|
t1_sdo.register(t_ps[1], "SteamDumpOpen", t1_sdo.update)
|
||||||
|
|
||||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||||
t_ps[1].subscribe("TurbineOverSpeed", t1_tos.update)
|
t1_tos.register(t_ps[1], "TurbineOverSpeed", t1_tos.update)
|
||||||
|
|
||||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||||
t_ps[1].subscribe("GeneratorTrip", t1_gtrp.update)
|
t1_gtrp.register(t_ps[1], "GeneratorTrip", t1_gtrp.update)
|
||||||
|
|
||||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||||
t_ps[1].subscribe("TurbineTrip", t1_trp.update)
|
t1_trp.register(t_ps[1], "TurbineTrip", t1_trp.update)
|
||||||
|
|
||||||
if unit.num_turbines > 1 then
|
if unit.num_turbines > 1 then
|
||||||
if (available_space > 2 and unit.num_turbines == 2) or available_space > 3 then
|
if (available_space > 2 and unit.num_turbines == 2) or available_space > 3 then
|
||||||
@@ -317,19 +317,19 @@ local function init(parent, id)
|
|||||||
|
|
||||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
||||||
t_ps[2].subscribe("SteamDumpOpen", t2_sdo.update)
|
t2_sdo.register(t_ps[2], "SteamDumpOpen", t2_sdo.update)
|
||||||
|
|
||||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||||
t_ps[2].subscribe("TurbineOverSpeed", t2_tos.update)
|
t2_tos.register(t_ps[2], "TurbineOverSpeed", t2_tos.update)
|
||||||
|
|
||||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||||
t_ps[2].subscribe("GeneratorTrip", t2_gtrp.update)
|
t2_gtrp.register(t_ps[2], "GeneratorTrip", t2_gtrp.update)
|
||||||
|
|
||||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||||
t_ps[2].subscribe("TurbineTrip", t2_trp.update)
|
t2_trp.register(t_ps[2], "TurbineTrip", t2_trp.update)
|
||||||
end
|
end
|
||||||
|
|
||||||
if unit.num_turbines > 2 then
|
if unit.num_turbines > 2 then
|
||||||
@@ -337,19 +337,19 @@ local function init(parent, id)
|
|||||||
|
|
||||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
||||||
t_ps[3].subscribe("SteamDumpOpen", t3_sdo.update)
|
t3_sdo.register(t_ps[3], "SteamDumpOpen", t3_sdo.update)
|
||||||
|
|
||||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||||
t_ps[3].subscribe("TurbineOverSpeed", t3_tos.update)
|
t3_tos.register(t_ps[3], "TurbineOverSpeed", t3_tos.update)
|
||||||
|
|
||||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||||
t_ps[3].subscribe("GeneratorTrip", t3_gtrp.update)
|
t3_gtrp.register(t_ps[3], "GeneratorTrip", t3_gtrp.update)
|
||||||
|
|
||||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||||
t_ps[3].subscribe("TurbineTrip", t3_trp.update)
|
t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update)
|
||||||
end
|
end
|
||||||
|
|
||||||
----------------------
|
----------------------
|
||||||
@@ -365,8 +365,8 @@ local function init(parent, id)
|
|||||||
local set_burn = function () unit.set_burn(burn_rate.get_value()) end
|
local set_burn = function () unit.set_burn(burn_rate.get_value()) end
|
||||||
local set_burn_btn = PushButton{parent=burn_control,x=14,y=2,text="SET",min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),dis_fg_bg=dis_colors,callback=set_burn}
|
local set_burn_btn = PushButton{parent=burn_control,x=14,y=2,text="SET",min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),dis_fg_bg=dis_colors,callback=set_burn}
|
||||||
|
|
||||||
u_ps.subscribe("burn_rate", burn_rate.set_value)
|
burn_rate.register(u_ps, "burn_rate", burn_rate.set_value)
|
||||||
u_ps.subscribe("max_burn", burn_rate.set_max)
|
burn_rate.register(u_ps, "max_burn", burn_rate.set_max)
|
||||||
|
|
||||||
local start = HazardButton{parent=main,x=2,y=28,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=unit.start,fg_bg=hzd_fg_bg}
|
local start = HazardButton{parent=main,x=2,y=28,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=unit.start,fg_bg=hzd_fg_bg}
|
||||||
local ack_a = HazardButton{parent=main,x=12,y=32,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=unit.ack_alarms,fg_bg=hzd_fg_bg}
|
local ack_a = HazardButton{parent=main,x=12,y=32,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=unit.ack_alarms,fg_bg=hzd_fg_bg}
|
||||||
@@ -387,9 +387,12 @@ local function init(parent, id)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
u_ps.subscribe("status", start_button_en_check)
|
start.register(u_ps, "status", start_button_en_check)
|
||||||
u_ps.subscribe("rps_tripped", start_button_en_check)
|
start.register(u_ps, "rps_tripped", start_button_en_check)
|
||||||
u_ps.subscribe("rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
|
start.register(u_ps, "auto_group_id", start_button_en_check)
|
||||||
|
start.register(u_ps, "AutoControl", start_button_en_check)
|
||||||
|
|
||||||
|
reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
|
||||||
|
|
||||||
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=48}
|
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=48}
|
||||||
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
|
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
|
||||||
@@ -397,7 +400,7 @@ local function init(parent, id)
|
|||||||
|
|
||||||
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=waste_opts,callback=unit.set_waste,min_width=6}
|
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=waste_opts,callback=unit.set_waste,min_width=6}
|
||||||
|
|
||||||
u_ps.subscribe("U_WasteMode", waste_mode.set_value)
|
waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)
|
||||||
|
|
||||||
----------------------
|
----------------------
|
||||||
-- alarm management --
|
-- alarm management --
|
||||||
@@ -420,20 +423,20 @@ local function init(parent, id)
|
|||||||
local a_clt = AlarmLight{parent=alarm_panel,x=6,label="RCS Transient",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS}
|
local a_clt = AlarmLight{parent=alarm_panel,x=6,label="RCS Transient",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS}
|
||||||
local a_tbt = AlarmLight{parent=alarm_panel,x=6,label="Turbine Trip",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
local a_tbt = AlarmLight{parent=alarm_panel,x=6,label="Turbine Trip",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||||
|
|
||||||
u_ps.subscribe("Alarm_1", a_brc.update)
|
a_brc.register(u_ps, "Alarm_1", a_brc.update)
|
||||||
u_ps.subscribe("Alarm_2", a_rad.update)
|
a_rad.register(u_ps, "Alarm_2", a_rad.update)
|
||||||
u_ps.subscribe("Alarm_4", a_dmg.update)
|
a_dmg.register(u_ps, "Alarm_4", a_dmg.update)
|
||||||
|
|
||||||
u_ps.subscribe("Alarm_3", a_rcl.update)
|
a_rcl.register(u_ps, "Alarm_3", a_rcl.update)
|
||||||
u_ps.subscribe("Alarm_5", a_rcd.update)
|
a_rcd.register(u_ps, "Alarm_5", a_rcd.update)
|
||||||
u_ps.subscribe("Alarm_6", a_rot.update)
|
a_rot.register(u_ps, "Alarm_6", a_rot.update)
|
||||||
u_ps.subscribe("Alarm_7", a_rht.update)
|
a_rht.register(u_ps, "Alarm_7", a_rht.update)
|
||||||
u_ps.subscribe("Alarm_8", a_rwl.update)
|
a_rwl.register(u_ps, "Alarm_8", a_rwl.update)
|
||||||
u_ps.subscribe("Alarm_9", a_rwh.update)
|
a_rwh.register(u_ps, "Alarm_9", a_rwh.update)
|
||||||
|
|
||||||
u_ps.subscribe("Alarm_10", a_rps.update)
|
a_rps.register(u_ps, "Alarm_10", a_rps.update)
|
||||||
u_ps.subscribe("Alarm_11", a_clt.update)
|
a_clt.register(u_ps, "Alarm_11", a_clt.update)
|
||||||
u_ps.subscribe("Alarm_12", a_tbt.update)
|
a_tbt.register(u_ps, "Alarm_12", a_tbt.update)
|
||||||
|
|
||||||
-- ack's and resets
|
-- ack's and resets
|
||||||
|
|
||||||
@@ -487,7 +490,7 @@ local function init(parent, id)
|
|||||||
|
|
||||||
local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.blue,colors.white),radio_bg=colors.gray}
|
local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.blue,colors.white),radio_bg=colors.gray}
|
||||||
|
|
||||||
u_ps.subscribe("auto_group_id", function (gid) group.set_value(gid + 1) end)
|
group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end)
|
||||||
|
|
||||||
auto_div.line_break()
|
auto_div.line_break()
|
||||||
|
|
||||||
@@ -499,44 +502,35 @@ local function init(parent, id)
|
|||||||
TextBox{parent=auto_div,text="Prio. Group",height=1,width=11,fg_bg=style.label}
|
TextBox{parent=auto_div,text="Prio. Group",height=1,width=11,fg_bg=style.label}
|
||||||
local auto_grp = TextBox{parent=auto_div,text="Manual",height=1,width=11,fg_bg=bw_fg_bg}
|
local auto_grp = TextBox{parent=auto_div,text="Manual",height=1,width=11,fg_bg=bw_fg_bg}
|
||||||
|
|
||||||
u_ps.subscribe("auto_group", auto_grp.set_value)
|
auto_grp.register(u_ps, "auto_group", auto_grp.set_value)
|
||||||
|
|
||||||
auto_div.line_break()
|
auto_div.line_break()
|
||||||
|
|
||||||
local a_rdy = IndicatorLight{parent=auto_div,label="Ready",x=2,colors=cpair(colors.green,colors.gray)}
|
local a_rdy = IndicatorLight{parent=auto_div,label="Ready",x=2,colors=cpair(colors.green,colors.gray)}
|
||||||
local a_stb = IndicatorLight{parent=auto_div,label="Standby",x=2,colors=cpair(colors.white,colors.gray),flash=true,period=period.BLINK_1000_MS}
|
local a_stb = IndicatorLight{parent=auto_div,label="Standby",x=2,colors=cpair(colors.white,colors.gray),flash=true,period=period.BLINK_1000_MS}
|
||||||
|
|
||||||
u_ps.subscribe("U_AutoReady", a_rdy.update)
|
a_rdy.register(u_ps, "U_AutoReady", a_rdy.update)
|
||||||
|
|
||||||
-- update standby indicator
|
-- update standby indicator
|
||||||
u_ps.subscribe("status", function (active)
|
a_stb.register(u_ps, "status", function (active)
|
||||||
a_stb.update(unit.annunciator.AutoControl and (not active))
|
a_stb.update(unit.annunciator.AutoControl and (not active))
|
||||||
end)
|
end)
|
||||||
|
a_stb.register(u_ps, "AutoControl", function (auto_active)
|
||||||
-- enable and disable controls based on group assignment
|
|
||||||
u_ps.subscribe("auto_group_id", function (gid)
|
|
||||||
start_button_en_check()
|
|
||||||
|
|
||||||
if gid == 0 then
|
|
||||||
burn_rate.enable()
|
|
||||||
set_burn_btn.enable()
|
|
||||||
else
|
|
||||||
burn_rate.disable()
|
|
||||||
set_burn_btn.disable()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- enable and disable controls based on auto control state (start button is handled separately)
|
|
||||||
u_ps.subscribe("AutoControl", function (auto_active)
|
|
||||||
start_button_en_check()
|
|
||||||
|
|
||||||
if auto_active then
|
if auto_active then
|
||||||
a_stb.update(unit.reactor_data.mek_status.status == false)
|
a_stb.update(unit.reactor_data.mek_status.status == false)
|
||||||
else a_stb.update(false) end
|
else a_stb.update(false) end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
-- enable/disable controls based on group assignment (start button is separate)
|
||||||
|
burn_rate.register(u_ps, "auto_group_id", function (gid)
|
||||||
|
if gid == 0 then burn_rate.enable() else burn_rate.disable() end
|
||||||
|
end)
|
||||||
|
set_burn_btn.register(u_ps, "auto_group_id", function (gid)
|
||||||
|
if gid == 0 then set_burn_btn.enable() else set_burn_btn.disable() end
|
||||||
|
end)
|
||||||
|
|
||||||
-- can't change group if auto is engaged regardless of if this unit is part of auto control
|
-- can't change group if auto is engaged regardless of if this unit is part of auto control
|
||||||
f_ps.subscribe("auto_active", function (auto_active)
|
set_grp_btn.register(f_ps, "auto_active", function (auto_active)
|
||||||
if auto_active then set_grp_btn.disable() else set_grp_btn.enable() end
|
if auto_active then set_grp_btn.disable() else set_grp_btn.enable() end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ local Div = require("graphics.elements.div")
|
|||||||
local PipeNetwork = require("graphics.elements.pipenet")
|
local PipeNetwork = require("graphics.elements.pipenet")
|
||||||
local TextBox = require("graphics.elements.textbox")
|
local TextBox = require("graphics.elements.textbox")
|
||||||
|
|
||||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
local pipe = core.graphics.pipe
|
local pipe = core.pipe
|
||||||
|
|
||||||
-- make a new unit overview window
|
-- make a new unit overview window
|
||||||
---@param parent graphics_element parent
|
---@param parent graphics_element parent
|
||||||
@@ -24,19 +24,18 @@ local pipe = core.graphics.pipe
|
|||||||
---@param y integer top left y
|
---@param y integer top left y
|
||||||
---@param unit ioctl_unit unit database entry
|
---@param unit ioctl_unit unit database entry
|
||||||
local function make(parent, x, y, unit)
|
local function make(parent, x, y, unit)
|
||||||
local height = 0
|
|
||||||
local num_boilers = #unit.boiler_data_tbl
|
local num_boilers = #unit.boiler_data_tbl
|
||||||
local num_turbines = #unit.turbine_data_tbl
|
local num_turbines = #unit.turbine_data_tbl
|
||||||
|
|
||||||
assert(num_boilers >= 0 and num_boilers <= 2, "minimum 0 boilers, maximum 2 boilers")
|
assert(num_boilers >= 0 and num_boilers <= 2, "minimum 0 boilers, maximum 2 boilers")
|
||||||
assert(num_turbines >= 1 and num_turbines <= 3, "minimum 1 turbine, maximum 3 turbines")
|
assert(num_turbines >= 1 and num_turbines <= 3, "minimum 1 turbine, maximum 3 turbines")
|
||||||
|
|
||||||
|
local height = 25
|
||||||
|
|
||||||
if num_boilers == 0 and num_turbines == 1 then
|
if num_boilers == 0 and num_turbines == 1 then
|
||||||
height = 9
|
height = 9
|
||||||
elseif num_boilers == 1 and num_turbines <= 2 then
|
elseif num_boilers == 1 and num_turbines <= 2 then
|
||||||
height = 17
|
height = 17
|
||||||
else
|
|
||||||
height = 25
|
|
||||||
end
|
end
|
||||||
|
|
||||||
assert(parent.height() >= (y + height), "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
assert(parent.height() >= (y + height), "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||||
@@ -51,7 +50,7 @@ local function make(parent, x, y, unit)
|
|||||||
-- REACTOR --
|
-- REACTOR --
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
reactor_view(root, 1, 3, unit.reactor_data, unit.unit_ps)
|
reactor_view(root, 1, 3, unit.unit_ps)
|
||||||
|
|
||||||
if num_boilers > 0 then
|
if num_boilers > 0 then
|
||||||
local coolant_pipes = {}
|
local coolant_pipes = {}
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
--
|
|
||||||
-- Reactor Unit Waiting Spinner
|
|
||||||
--
|
|
||||||
|
|
||||||
local style = require("coordinator.ui.style")
|
|
||||||
|
|
||||||
local core = require("graphics.core")
|
|
||||||
|
|
||||||
local Div = require("graphics.elements.div")
|
|
||||||
local TextBox = require("graphics.elements.textbox")
|
|
||||||
|
|
||||||
local WaitingAnim = require("graphics.elements.animations.waiting")
|
|
||||||
|
|
||||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
|
||||||
|
|
||||||
local cpair = core.graphics.cpair
|
|
||||||
|
|
||||||
-- create a unit waiting view
|
|
||||||
---@param parent graphics_element parent
|
|
||||||
---@param y integer y offset
|
|
||||||
local function init(parent, y)
|
|
||||||
-- bounding box div
|
|
||||||
local root = Div{parent=parent,x=1,y=y,height=5}
|
|
||||||
|
|
||||||
local waiting_x = math.floor(parent.width() / 2) - 2
|
|
||||||
|
|
||||||
TextBox{parent=root,text="Waiting for status...",alignment=TEXT_ALIGN.CENTER,y=1,height=1,fg_bg=cpair(colors.black,style.root.bkg)}
|
|
||||||
WaitingAnim{parent=root,x=waiting_x,y=3,fg_bg=cpair(colors.blue,style.root.bkg)}
|
|
||||||
|
|
||||||
return root
|
|
||||||
end
|
|
||||||
|
|
||||||
return init
|
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
local iocontrol = require("coordinator.iocontrol")
|
local iocontrol = require("coordinator.iocontrol")
|
||||||
local sounder = require("coordinator.sounder")
|
|
||||||
|
|
||||||
local style = require("coordinator.ui.style")
|
local style = require("coordinator.ui.style")
|
||||||
|
|
||||||
@@ -15,36 +14,28 @@ local unit_overview = require("coordinator.ui.components.unit_overview")
|
|||||||
|
|
||||||
local core = require("graphics.core")
|
local core = require("graphics.core")
|
||||||
|
|
||||||
local ColorMap = require("graphics.elements.colormap")
|
|
||||||
local DisplayBox = require("graphics.elements.displaybox")
|
|
||||||
local Div = require("graphics.elements.div")
|
|
||||||
local TextBox = require("graphics.elements.textbox")
|
local TextBox = require("graphics.elements.textbox")
|
||||||
|
|
||||||
local PushButton = require("graphics.elements.controls.push_button")
|
|
||||||
local SwitchButton = require("graphics.elements.controls.switch_button")
|
|
||||||
|
|
||||||
local DataIndicator = require("graphics.elements.indicators.data")
|
local DataIndicator = require("graphics.elements.indicators.data")
|
||||||
|
|
||||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
local cpair = core.graphics.cpair
|
local cpair = core.cpair
|
||||||
|
|
||||||
-- create new main view
|
-- create new main view
|
||||||
---@param monitor table main viewscreen
|
---@param main graphics_element main displaybox
|
||||||
local function init(monitor)
|
local function init(main)
|
||||||
local facility = iocontrol.get_db().facility
|
local facility = iocontrol.get_db().facility
|
||||||
local units = iocontrol.get_db().units
|
local units = iocontrol.get_db().units
|
||||||
|
|
||||||
local main = DisplayBox{window=monitor,fg_bg=style.root}
|
|
||||||
|
|
||||||
-- window header message
|
-- window header message
|
||||||
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.width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
|
||||||
|
|
||||||
facility.ps.subscribe("sv_ping", ping.update)
|
ping.register(facility.ps, "sv_ping", ping.update)
|
||||||
facility.ps.subscribe("date_time", datetime.set_value)
|
datetime.register(facility.ps, "date_time", datetime.set_value)
|
||||||
|
|
||||||
local uo_1, uo_2, uo_3, uo_4 ---@type graphics_element
|
local uo_1, uo_2, uo_3, uo_4 ---@type graphics_element
|
||||||
|
|
||||||
@@ -93,8 +84,6 @@ local function init(monitor)
|
|||||||
process_ctl(main, 2, cnc_bottom_align_start)
|
process_ctl(main, 2, cnc_bottom_align_start)
|
||||||
|
|
||||||
imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1])
|
imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1])
|
||||||
|
|
||||||
return main
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return init
|
return init
|
||||||
|
|||||||
@@ -2,21 +2,13 @@
|
|||||||
-- Reactor Unit SCADA Coordinator GUI
|
-- Reactor Unit SCADA Coordinator GUI
|
||||||
--
|
--
|
||||||
|
|
||||||
local style = require("coordinator.ui.style")
|
|
||||||
|
|
||||||
local unit_detail = require("coordinator.ui.components.unit_detail")
|
local unit_detail = require("coordinator.ui.components.unit_detail")
|
||||||
|
|
||||||
local DisplayBox = require("graphics.elements.displaybox")
|
|
||||||
|
|
||||||
-- create a unit view
|
-- create a unit view
|
||||||
---@param monitor table
|
---@param main graphics_element main displaybox
|
||||||
---@param id integer
|
---@param id integer
|
||||||
local function init(monitor, id)
|
local function init(main, id)
|
||||||
local main = DisplayBox{window=monitor,fg_bg=style.root}
|
|
||||||
|
|
||||||
unit_detail(main, id)
|
unit_detail(main, id)
|
||||||
|
|
||||||
return main
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return init
|
return init
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ local core = require("graphics.core")
|
|||||||
|
|
||||||
local style = {}
|
local style = {}
|
||||||
|
|
||||||
local cpair = core.graphics.cpair
|
local cpair = core.cpair
|
||||||
|
|
||||||
-- GLOBAL --
|
-- GLOBAL --
|
||||||
|
|
||||||
|
|||||||
@@ -1,96 +1,19 @@
|
|||||||
--
|
--
|
||||||
-- Graphics Core Functions and Objects
|
-- Graphics Core Types, Checks, and Constructors
|
||||||
--
|
--
|
||||||
|
|
||||||
|
local events = require("graphics.events")
|
||||||
|
local flasher = require("graphics.flasher")
|
||||||
|
|
||||||
local core = {}
|
local core = {}
|
||||||
|
|
||||||
local flasher = require("graphics.flasher")
|
|
||||||
|
|
||||||
core.flasher = flasher
|
core.flasher = flasher
|
||||||
|
|
||||||
local events = {}
|
|
||||||
|
|
||||||
---@enum click_type
|
|
||||||
events.click_type = {
|
|
||||||
VIRTUAL = 0,
|
|
||||||
LEFT_BUTTON = 1,
|
|
||||||
RIGHT_BUTTON = 2,
|
|
||||||
MID_BUTTON = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
---@class mouse_interaction
|
|
||||||
---@field monitor string
|
|
||||||
---@field button integer
|
|
||||||
---@field x integer
|
|
||||||
---@field y integer
|
|
||||||
|
|
||||||
-- create a new monitor touch mouse interaction event
|
|
||||||
---@nodiscard
|
|
||||||
---@param monitor string
|
|
||||||
---@param x integer
|
|
||||||
---@param y integer
|
|
||||||
---@return mouse_interaction
|
|
||||||
function events.touch(monitor, x, y)
|
|
||||||
return {
|
|
||||||
monitor = monitor,
|
|
||||||
button = events.click_type.LEFT_BUTTON,
|
|
||||||
x = x,
|
|
||||||
y = y
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- create a new mouse click mouse interaction event
|
|
||||||
---@nodiscard
|
|
||||||
---@param button click_type
|
|
||||||
---@param x integer
|
|
||||||
---@param y integer
|
|
||||||
---@return mouse_interaction
|
|
||||||
function events.click(button, x, y)
|
|
||||||
return {
|
|
||||||
monitor = "terminal",
|
|
||||||
button = button,
|
|
||||||
x = x,
|
|
||||||
y = y
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- create a new transposed mouse interaction event using the event's monitor/button fields
|
|
||||||
---@nodiscard
|
|
||||||
---@param event mouse_interaction
|
|
||||||
---@param new_x integer
|
|
||||||
---@param new_y integer
|
|
||||||
---@return mouse_interaction
|
|
||||||
function events.mouse_transposed(event, new_x, new_y)
|
|
||||||
return {
|
|
||||||
monitor = event.monitor,
|
|
||||||
button = event.button,
|
|
||||||
x = new_x,
|
|
||||||
y = new_y
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- create a new generic mouse interaction event
|
|
||||||
---@nodiscard
|
|
||||||
---@param monitor string
|
|
||||||
---@param button click_type
|
|
||||||
---@param x integer
|
|
||||||
---@param y integer
|
|
||||||
---@return mouse_interaction
|
|
||||||
function events.mouse_generic(monitor, button, x, y)
|
|
||||||
return {
|
|
||||||
monitor = monitor,
|
|
||||||
button = button,
|
|
||||||
x = x,
|
|
||||||
y = y
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
core.events = events
|
core.events = events
|
||||||
|
|
||||||
local graphics = {}
|
-- Core Types
|
||||||
|
|
||||||
---@enum TEXT_ALIGN
|
---@enum TEXT_ALIGN
|
||||||
graphics.TEXT_ALIGN = {
|
core.TEXT_ALIGN = {
|
||||||
LEFT = 1,
|
LEFT = 1,
|
||||||
CENTER = 2,
|
CENTER = 2,
|
||||||
RIGHT = 3
|
RIGHT = 3
|
||||||
@@ -109,7 +32,7 @@ graphics.TEXT_ALIGN = {
|
|||||||
---@param color color border color
|
---@param color color border color
|
||||||
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
|
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
|
||||||
---@return graphics_border
|
---@return graphics_border
|
||||||
function graphics.border(width, color, even)
|
function core.border(width, color, even)
|
||||||
return {
|
return {
|
||||||
width = width,
|
width = width,
|
||||||
color = color,
|
color = color,
|
||||||
@@ -130,7 +53,7 @@ end
|
|||||||
---@param w integer
|
---@param w integer
|
||||||
---@param h integer
|
---@param h integer
|
||||||
---@return graphics_frame
|
---@return graphics_frame
|
||||||
function graphics.gframe(x, y, w, h)
|
function core.gframe(x, y, w, h)
|
||||||
return {
|
return {
|
||||||
x = x,
|
x = x,
|
||||||
y = y,
|
y = y,
|
||||||
@@ -154,7 +77,7 @@ end
|
|||||||
---@param a color
|
---@param a color
|
||||||
---@param b color
|
---@param b color
|
||||||
---@return cpair
|
---@return cpair
|
||||||
function graphics.cpair(a, b)
|
function core.cpair(a, b)
|
||||||
return {
|
return {
|
||||||
-- color pairs
|
-- color pairs
|
||||||
color_a = a,
|
color_a = a,
|
||||||
@@ -191,7 +114,7 @@ end
|
|||||||
---@param thin? boolean true for 1 subpixel, false (default) for 2
|
---@param thin? boolean true for 1 subpixel, false (default) for 2
|
||||||
---@param align_tr? boolean false to align bottom left (default), true to align top right
|
---@param align_tr? boolean false to align bottom left (default), true to align top right
|
||||||
---@return pipe
|
---@return pipe
|
||||||
function graphics.pipe(x1, y1, x2, y2, color, thin, align_tr)
|
function core.pipe(x1, y1, x2, y2, color, thin, align_tr)
|
||||||
return {
|
return {
|
||||||
x1 = x1,
|
x1 = x1,
|
||||||
y1 = y1,
|
y1 = y1,
|
||||||
@@ -205,6 +128,4 @@ function graphics.pipe(x1, y1, x2, y2, color, thin, align_tr)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
core.graphics = graphics
|
|
||||||
|
|
||||||
return core
|
return core
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ local element = {}
|
|||||||
---|multi_button_args
|
---|multi_button_args
|
||||||
---|push_button_args
|
---|push_button_args
|
||||||
---|radio_button_args
|
---|radio_button_args
|
||||||
|
---|sidebar_args
|
||||||
---|spinbox_args
|
---|spinbox_args
|
||||||
---|switch_button_args
|
---|switch_button_args
|
||||||
|
---|tabbar_args
|
||||||
---|alarm_indicator_light
|
---|alarm_indicator_light
|
||||||
---|core_map_args
|
---|core_map_args
|
||||||
---|data_indicator_args
|
---|data_indicator_args
|
||||||
@@ -44,11 +46,17 @@ local element = {}
|
|||||||
---|colormap_args
|
---|colormap_args
|
||||||
---|displaybox_args
|
---|displaybox_args
|
||||||
---|div_args
|
---|div_args
|
||||||
|
---|multipane_args
|
||||||
---|pipenet_args
|
---|pipenet_args
|
||||||
---|rectangle_args
|
---|rectangle_args
|
||||||
---|textbox_args
|
---|textbox_args
|
||||||
---|tiling_args
|
---|tiling_args
|
||||||
|
|
||||||
|
---@class element_subscription
|
||||||
|
---@field ps psil ps used
|
||||||
|
---@field key string data key
|
||||||
|
---@field func function callback
|
||||||
|
|
||||||
-- a base graphics element, should not be created on its own
|
-- a base graphics element, should not be created on its own
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param args graphics_args arguments
|
---@param args graphics_args arguments
|
||||||
@@ -57,12 +65,13 @@ function element.new(args)
|
|||||||
id = -1,
|
id = -1,
|
||||||
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 },
|
position = { x = 1, y = 1 }, ---@type coordinate_2d
|
||||||
child_offset = { x = 0, y = 0 },
|
child_offset = { x = 0, y = 0 },
|
||||||
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1},
|
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
|
||||||
next_y = 1,
|
next_y = 1,
|
||||||
children = {},
|
children = {},
|
||||||
|
subscriptions = {},
|
||||||
mt = {}
|
mt = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,10 +80,12 @@ function element.new(args)
|
|||||||
enabled = true,
|
enabled = true,
|
||||||
value = nil, ---@type any
|
value = nil, ---@type any
|
||||||
window = nil, ---@type table
|
window = nil, ---@type table
|
||||||
fg_bg = core.graphics.cpair(colors.white, colors.black),
|
fg_bg = core.cpair(colors.white, colors.black),
|
||||||
frame = core.graphics.gframe(1, 1, 1, 1)
|
frame = core.gframe(1, 1, 1, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local name_brief = "graphics.element{" .. self.elem_type .. "}: "
|
||||||
|
|
||||||
-- element as string
|
-- element as string
|
||||||
function self.mt.__tostring()
|
function self.mt.__tostring()
|
||||||
return "graphics.element{" .. self.elem_type .. "} @ " .. tostring(self)
|
return "graphics.element{" .. self.elem_type .. "} @ " .. tostring(self)
|
||||||
@@ -136,10 +147,10 @@ function element.new(args)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- check frame
|
-- check frame
|
||||||
assert(f.x >= 1, "graphics.element{" .. self.elem_type .. "}: frame x not >= 1")
|
assert(f.x >= 1, name_brief .. "frame x not >= 1")
|
||||||
assert(f.y >= 1, "graphics.element{" .. self.elem_type .. "}: frame y not >= 1")
|
assert(f.y >= 1, name_brief .. "frame y not >= 1")
|
||||||
assert(f.w >= 1, "graphics.element{" .. self.elem_type .. "}: frame width not >= 1")
|
assert(f.w >= 1, name_brief .. "frame width not >= 1")
|
||||||
assert(f.h >= 1, "graphics.element{" .. self.elem_type .. "}: 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, true)
|
||||||
@@ -166,6 +177,38 @@ function element.new(args)
|
|||||||
self.bounds.y2 = self.position.y + f.h - 1
|
self.bounds.y2 = self.position.y + f.h - 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- check if a coordinate relative to the parent is within the bounds of this element
|
||||||
|
---@param x integer
|
||||||
|
---@param y integer
|
||||||
|
function protected.in_window_bounds(x, y)
|
||||||
|
local in_x = x >= self.bounds.x1 and x <= self.bounds.x2
|
||||||
|
local in_y = y >= self.bounds.y1 and y <= self.bounds.y2
|
||||||
|
return in_x and in_y
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check if a coordinate relative to this window is within the bounds of this element
|
||||||
|
---@param x integer
|
||||||
|
---@param y integer
|
||||||
|
function protected.in_frame_bounds(x, y)
|
||||||
|
local in_x = x >= 1 and x <= protected.frame.w
|
||||||
|
local in_y = y >= 1 and y <= protected.frame.h
|
||||||
|
return in_x and in_y
|
||||||
|
end
|
||||||
|
|
||||||
|
-- luacheck: push ignore
|
||||||
|
---@diagnostic disable: unused-local, unused-vararg
|
||||||
|
|
||||||
|
-- dynamically insert a child element
|
||||||
|
---@param id string|integer element identifier
|
||||||
|
---@param elem graphics_element element
|
||||||
|
function protected.insert(id, elem)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- dynamically remove a child element
|
||||||
|
---@param id string|integer element identifier
|
||||||
|
function protected.remove(id)
|
||||||
|
end
|
||||||
|
|
||||||
-- handle a mouse event
|
-- handle a mouse event
|
||||||
---@param event mouse_interaction mouse interaction event
|
---@param event mouse_interaction mouse interaction event
|
||||||
function protected.handle_mouse(event)
|
function protected.handle_mouse(event)
|
||||||
@@ -220,6 +263,9 @@ function element.new(args)
|
|||||||
function protected.resize(...)
|
function protected.resize(...)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- luacheck: pop
|
||||||
|
---@diagnostic enable: unused-local, unused-vararg
|
||||||
|
|
||||||
-- start animations
|
-- start animations
|
||||||
function protected.start_anim()
|
function protected.start_anim()
|
||||||
end
|
end
|
||||||
@@ -244,7 +290,7 @@ function element.new(args)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- check window
|
-- check window
|
||||||
assert(self.p_window, "graphics.element{" .. self.elem_type .. "}: no parent window provided")
|
assert(self.p_window, name_brief .. "no parent window provided")
|
||||||
|
|
||||||
-- prepare the template
|
-- prepare the template
|
||||||
if args.parent == nil then
|
if args.parent == nil then
|
||||||
@@ -261,7 +307,25 @@ function element.new(args)
|
|||||||
---@nodiscard
|
---@nodiscard
|
||||||
function public.window() return protected.window end
|
function public.window() return protected.window end
|
||||||
|
|
||||||
-- CHILD ELEMENTS --
|
-- delete this element (hide and unsubscribe from PSIL)
|
||||||
|
function public.delete()
|
||||||
|
-- hide + stop animations
|
||||||
|
public.hide()
|
||||||
|
|
||||||
|
-- unsubscribe from PSIL
|
||||||
|
for i = 1, #self.subscriptions do
|
||||||
|
local s = self.subscriptions[i] ---@type element_subscription
|
||||||
|
s.ps.unsubscribe(s.key, s.func)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- delete all children
|
||||||
|
for k, v in pairs(self.children) do
|
||||||
|
v.delete()
|
||||||
|
self.children[k] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ELEMENT TREE --
|
||||||
|
|
||||||
-- add a child element
|
-- add a child element
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
@@ -291,12 +355,18 @@ function element.new(args)
|
|||||||
|
|
||||||
-- get a child element
|
-- get a child element
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
|
---@param id element_id
|
||||||
---@return graphics_element
|
---@return graphics_element
|
||||||
function public.get_child(key) return self.children[key] end
|
function public.get_child(id) return self.children[id] end
|
||||||
|
|
||||||
-- remove child
|
-- remove a child element
|
||||||
---@param key string|integer
|
---@param id element_id
|
||||||
function public.remove(key) self.children[key] = nil end
|
function public.remove(id)
|
||||||
|
if self.children[id] ~= nil then
|
||||||
|
self.children[id].delete()
|
||||||
|
self.children[id] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- attempt to get a child element by ID (does not include this element itself)
|
-- attempt to get a child element by ID (does not include this element itself)
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
@@ -315,6 +385,25 @@ function element.new(args)
|
|||||||
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
|
||||||
@@ -408,22 +497,29 @@ function element.new(args)
|
|||||||
protected.resize(...)
|
protected.resize(...)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- reposition the element window<br>
|
||||||
|
-- offsets relative to parent frame are where (1, 1) would be on top of the parent's top left corner
|
||||||
|
---@param x integer x position relative to parent frame
|
||||||
|
---@param y integer y position relative to parent frame
|
||||||
|
function public.reposition(x, y)
|
||||||
|
protected.window.reposition(x, y)
|
||||||
|
end
|
||||||
|
|
||||||
-- FUNCTION CALLBACKS --
|
-- FUNCTION CALLBACKS --
|
||||||
|
|
||||||
-- handle a monitor touch or mouse click
|
-- handle a monitor touch or mouse click
|
||||||
---@param event mouse_interaction mouse interaction event
|
---@param event mouse_interaction mouse interaction event
|
||||||
function public.handle_mouse(event)
|
function public.handle_mouse(event)
|
||||||
local in_x = event.x >= self.bounds.x1 and event.x <= self.bounds.x2
|
local x_ini, y_ini = event.initial.x, event.initial.y
|
||||||
local in_y = event.y >= self.bounds.y1 and event.y <= self.bounds.y2
|
|
||||||
|
|
||||||
if in_x and in_y then
|
local ini_in = protected.in_window_bounds(x_ini, y_ini)
|
||||||
local event_T = core.events.mouse_transposed(event, (event.x - self.position.x) + 1, (event.y - self.position.y) + 1)
|
|
||||||
|
|
||||||
-- handle the touch event, transformed into the window frame
|
if ini_in then
|
||||||
|
local event_T = core.events.mouse_transposed(event, self.position.x, self.position.y)
|
||||||
|
|
||||||
|
-- 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
|
||||||
-- pass on touch event to children
|
|
||||||
for _, val in pairs(self.children) do val.handle_mouse(event_T) end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -439,25 +535,29 @@ function element.new(args)
|
|||||||
protected.response_callback(result)
|
protected.response_callback(result)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- register a callback with a PSIL, allowing for automatic unregister on delete<br>
|
||||||
|
-- do not use graphics elements directly with PSIL subscribe()
|
||||||
|
---@param ps psil PSIL to subscribe to
|
||||||
|
---@param key string key to subscribe to
|
||||||
|
---@param func function function to link
|
||||||
|
function public.register(ps, key, func)
|
||||||
|
table.insert(self.subscriptions, { ps = ps, key = key, func = func })
|
||||||
|
ps.subscribe(key, func)
|
||||||
|
end
|
||||||
|
|
||||||
-- VISIBILITY --
|
-- VISIBILITY --
|
||||||
|
|
||||||
-- show the element
|
-- show the element
|
||||||
function public.show()
|
function public.show()
|
||||||
protected.window.setVisible(true)
|
protected.window.setVisible(true)
|
||||||
protected.start_anim()
|
protected.start_anim()
|
||||||
|
for _, child in pairs(self.children) do child.show() end
|
||||||
for i = 1, #self.children do
|
|
||||||
self.children[i].show()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- hide the element
|
-- hide the element
|
||||||
function public.hide()
|
function public.hide()
|
||||||
protected.stop_anim()
|
protected.stop_anim()
|
||||||
for i = 1, #self.children do
|
for _, child in pairs(self.children) do child.hide() end
|
||||||
self.children[i].hide()
|
|
||||||
end
|
|
||||||
|
|
||||||
protected.window.setVisible(false)
|
protected.window.setVisible(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ local function waiting(args)
|
|||||||
if state >= 12 then state = 0 end
|
if state >= 12 then state = 0 end
|
||||||
|
|
||||||
if run_animation then
|
if run_animation then
|
||||||
tcd.dispatch_unique(0.5, animate)
|
tcd.dispatch_unique(0.15, animate)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -142,24 +142,25 @@ local function hazard_button(args)
|
|||||||
|
|
||||||
-- handle mouse interaction
|
-- handle mouse interaction
|
||||||
---@param event mouse_interaction mouse event
|
---@param event mouse_interaction mouse event
|
||||||
---@diagnostic disable-next-line: unused-local
|
|
||||||
function e.handle_mouse(event)
|
function e.handle_mouse(event)
|
||||||
if e.enabled then
|
if e.enabled then
|
||||||
-- change text color to indicate clicked
|
if core.events.was_clicked(event.type) then
|
||||||
e.window.setTextColor(args.accent)
|
-- change text color to indicate clicked
|
||||||
e.window.setCursorPos(3, 2)
|
e.window.setTextColor(args.accent)
|
||||||
e.window.write(args.text)
|
e.window.setCursorPos(3, 2)
|
||||||
|
e.window.write(args.text)
|
||||||
|
|
||||||
-- abort any other callbacks
|
-- abort any other callbacks
|
||||||
tcd.abort(on_timeout)
|
tcd.abort(on_timeout)
|
||||||
tcd.abort(on_success)
|
tcd.abort(on_success)
|
||||||
tcd.abort(on_failure)
|
tcd.abort(on_failure)
|
||||||
|
|
||||||
-- 1.5 second timeout
|
-- 1.5 second timeout
|
||||||
tcd.dispatch(1.5, on_timeout)
|
tcd.dispatch(1.5, on_timeout)
|
||||||
|
|
||||||
-- call the touch callback
|
-- call the touch callback
|
||||||
args.callback()
|
args.callback()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -167,18 +168,13 @@ local function hazard_button(args)
|
|||||||
---@param result boolean true for success, false for failure
|
---@param result boolean true for success, false for failure
|
||||||
function e.response_callback(result)
|
function e.response_callback(result)
|
||||||
tcd.abort(on_timeout)
|
tcd.abort(on_timeout)
|
||||||
|
if result then on_success() else on_failure(0) end
|
||||||
if result then
|
|
||||||
on_success()
|
|
||||||
else
|
|
||||||
on_failure(0)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- set the value (true simulates pressing the button)
|
-- set the value (true simulates pressing the button)
|
||||||
---@param val boolean new value
|
---@param val boolean new value
|
||||||
function e.set_value(val)
|
function e.set_value(val)
|
||||||
if val then e.handle_mouse(core.events.mouse_generic("", core.events.click_type.VIRTUAL, 1, 1)) end
|
if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- show the button as disabled
|
-- show the button as disabled
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
local element = require("graphics.element")
|
local element = require("graphics.element")
|
||||||
|
|
||||||
---@class button_option
|
---@class button_option
|
||||||
---@field text string
|
---@field text string
|
||||||
---@field fg_bg cpair
|
---@field fg_bg cpair
|
||||||
---@field active_fg_bg cpair
|
---@field active_fg_bg cpair
|
||||||
---@field _lpad integer automatically calculated left pad
|
|
||||||
---@field _start_x integer starting touch x range (inclusive)
|
---@field _start_x integer starting touch x range (inclusive)
|
||||||
---@field _end_x integer ending touch x range (inclusive)
|
---@field _end_x integer ending touch x range (inclusive)
|
||||||
|
|
||||||
@@ -62,9 +62,7 @@ local function multi_button(args)
|
|||||||
local next_x = 2
|
local next_x = 2
|
||||||
for i = 1, #args.options do
|
for i = 1, #args.options do
|
||||||
local opt = args.options[i] ---@type button_option
|
local opt = args.options[i] ---@type button_option
|
||||||
local w = string.len(opt.text)
|
|
||||||
|
|
||||||
opt._lpad = math.floor((e.frame.w - w) / 2)
|
|
||||||
opt._start_x = next_x
|
opt._start_x = next_x
|
||||||
opt._end_x = next_x + button_width - 1
|
opt._end_x = next_x + button_width - 1
|
||||||
|
|
||||||
@@ -92,20 +90,32 @@ local function multi_button(args)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- check which button a given x is within
|
||||||
|
---@return integer|nil button index or nil if not within a button
|
||||||
|
local function which_button(x)
|
||||||
|
for i = 1, #args.options do
|
||||||
|
local opt = args.options[i] ---@type button_option
|
||||||
|
if x >= opt._start_x and x <= opt._end_x then return i end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
-- handle mouse interaction
|
-- handle mouse interaction
|
||||||
---@param event mouse_interaction mouse event
|
---@param event mouse_interaction mouse event
|
||||||
---@diagnostic disable-next-line: unused-local
|
|
||||||
function e.handle_mouse(event)
|
function e.handle_mouse(event)
|
||||||
-- determine what was pressed
|
-- if enabled and the button row was pressed...
|
||||||
if e.enabled and event.y == 1 then
|
if e.enabled and core.events.was_clicked(event.type) then
|
||||||
for i = 1, #args.options do
|
-- a button may have been pressed, which one was it?
|
||||||
local opt = args.options[i] ---@type button_option
|
local button_ini = which_button(event.initial.x)
|
||||||
|
local button_cur = which_button(event.current.x)
|
||||||
|
|
||||||
if event.x >= opt._start_x and event.x <= opt._end_x then
|
-- mouse up must always have started with a mouse down on the same button to count as a click
|
||||||
e.value = i
|
-- tap always has identical coordinates, so this always passes for taps
|
||||||
draw()
|
if button_ini == button_cur and button_cur ~= nil then
|
||||||
args.callback(e.value)
|
e.value = button_cur
|
||||||
end
|
draw()
|
||||||
|
args.callback(e.value)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ local tcd = require("scada-common.tcallbackdsp")
|
|||||||
local core = require("graphics.core")
|
local core = require("graphics.core")
|
||||||
local element = require("graphics.element")
|
local element = require("graphics.element")
|
||||||
|
|
||||||
|
local CLICK_TYPE = core.events.CLICK_TYPE
|
||||||
|
|
||||||
---@class push_button_args
|
---@class push_button_args
|
||||||
---@field text string button text
|
---@field text string button text
|
||||||
---@field callback function function to call on touch
|
---@field callback function function to call on touch
|
||||||
@@ -24,6 +26,8 @@ local element = require("graphics.element")
|
|||||||
local function push_button(args)
|
local function push_button(args)
|
||||||
assert(type(args.text) == "string", "graphics.elements.controls.push_button: text is a required field")
|
assert(type(args.text) == "string", "graphics.elements.controls.push_button: text is a required field")
|
||||||
assert(type(args.callback) == "function", "graphics.elements.controls.push_button: callback is a required field")
|
assert(type(args.callback) == "function", "graphics.elements.controls.push_button: callback is a required field")
|
||||||
|
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||||
|
"graphics.elements.controls.push_button: min_width must be nil or a number > 0")
|
||||||
|
|
||||||
local text_width = string.len(args.text)
|
local text_width = string.len(args.text)
|
||||||
|
|
||||||
@@ -47,38 +51,50 @@ local function push_button(args)
|
|||||||
e.window.write(args.text)
|
e.window.write(args.text)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- draw the button as pressed (if active_fg_bg set)
|
||||||
|
local function show_pressed()
|
||||||
|
if e.enabled and args.active_fg_bg ~= nil then
|
||||||
|
e.value = true
|
||||||
|
e.window.setTextColor(args.active_fg_bg.fgd)
|
||||||
|
e.window.setBackgroundColor(args.active_fg_bg.bkg)
|
||||||
|
draw()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- draw the button as unpressed (if active_fg_bg set)
|
||||||
|
local function show_unpressed()
|
||||||
|
if e.enabled and args.active_fg_bg ~= nil then
|
||||||
|
e.value = false
|
||||||
|
e.window.setTextColor(e.fg_bg.fgd)
|
||||||
|
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||||
|
draw()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- handle mouse interaction
|
-- handle mouse interaction
|
||||||
---@param event mouse_interaction mouse event
|
---@param event mouse_interaction mouse event
|
||||||
---@diagnostic disable-next-line: unused-local
|
|
||||||
function e.handle_mouse(event)
|
function e.handle_mouse(event)
|
||||||
if e.enabled then
|
if e.enabled then
|
||||||
if args.active_fg_bg ~= nil then
|
if event.type == CLICK_TYPE.TAP then
|
||||||
-- show as pressed
|
show_pressed()
|
||||||
e.value = true
|
|
||||||
e.window.setTextColor(args.active_fg_bg.fgd)
|
|
||||||
e.window.setBackgroundColor(args.active_fg_bg.bkg)
|
|
||||||
draw()
|
|
||||||
|
|
||||||
-- show as unpressed in 0.25 seconds
|
-- show as unpressed in 0.25 seconds
|
||||||
tcd.dispatch(0.25, function ()
|
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
|
||||||
e.value = false
|
args.callback()
|
||||||
if e.enabled then
|
elseif event.type == CLICK_TYPE.DOWN then
|
||||||
e.window.setTextColor(e.fg_bg.fgd)
|
show_pressed()
|
||||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
elseif event.type == CLICK_TYPE.UP then
|
||||||
end
|
show_unpressed()
|
||||||
draw()
|
if e.in_frame_bounds(event.current.x, event.current.y) then
|
||||||
end)
|
args.callback()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- call the touch callback
|
|
||||||
args.callback()
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- set the value (true simulates pressing the button)
|
-- set the value (true simulates pressing the button)
|
||||||
---@param val boolean new value
|
---@param val boolean new value
|
||||||
function e.set_value(val)
|
function e.set_value(val)
|
||||||
if val then e.handle_mouse(core.events.mouse_generic("", core.events.click_type.VIRTUAL, 1, 1)) end
|
if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- show butten as enabled
|
-- show butten as enabled
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
-- Radio Button Graphics Element
|
-- Radio Button Graphics Element
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
local element = require("graphics.element")
|
local element = require("graphics.element")
|
||||||
|
|
||||||
---@class radio_button_args
|
---@class radio_button_args
|
||||||
@@ -82,10 +83,10 @@ local function radio_button(args)
|
|||||||
-- handle mouse interaction
|
-- handle mouse interaction
|
||||||
---@param event mouse_interaction mouse event
|
---@param event mouse_interaction mouse event
|
||||||
function e.handle_mouse(event)
|
function e.handle_mouse(event)
|
||||||
-- determine what was pressed
|
if e.enabled and core.events.was_clicked(event.type) and (event.initial.y == event.current.y) then
|
||||||
if e.enabled then
|
-- determine what was pressed
|
||||||
if args.options[event.y] ~= nil then
|
if args.options[event.current.y] ~= nil then
|
||||||
e.value = event.y
|
e.value = event.current.y
|
||||||
draw()
|
draw()
|
||||||
args.callback(e.value)
|
args.callback(e.value)
|
||||||
end
|
end
|
||||||
|
|||||||
121
graphics/elements/controls/sidebar.lua
Normal file
121
graphics/elements/controls/sidebar.lua
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
-- Sidebar Graphics Element
|
||||||
|
|
||||||
|
local tcd = require("scada-common.tcallbackdsp")
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
|
local element = require("graphics.element")
|
||||||
|
|
||||||
|
local CLICK_TYPE = core.events.CLICK_TYPE
|
||||||
|
|
||||||
|
---@class sidebar_tab
|
||||||
|
---@field char string character identifier
|
||||||
|
---@field color cpair tab colors (fg/bg)
|
||||||
|
|
||||||
|
---@class sidebar_args
|
||||||
|
---@field tabs table sidebar tab options
|
||||||
|
---@field callback function function to call on tab change
|
||||||
|
---@field parent graphics_element
|
||||||
|
---@field id? string element id
|
||||||
|
---@field x? integer 1 if omitted
|
||||||
|
---@field y? integer 1 if omitted
|
||||||
|
---@field height? integer parent height if omitted
|
||||||
|
---@field fg_bg? cpair foreground/background colors
|
||||||
|
|
||||||
|
-- new sidebar tab selector
|
||||||
|
---@param args sidebar_args
|
||||||
|
---@return graphics_element element, element_id id
|
||||||
|
local function sidebar(args)
|
||||||
|
assert(type(args.tabs) == "table", "graphics.elements.controls.sidebar: tabs is a required field")
|
||||||
|
assert(#args.tabs > 0, "graphics.elements.controls.sidebar: at least one tab is required")
|
||||||
|
assert(type(args.callback) == "function", "graphics.elements.controls.sidebar: callback is a required field")
|
||||||
|
|
||||||
|
-- always 3 wide
|
||||||
|
args.width = 3
|
||||||
|
|
||||||
|
-- create new graphics element base object
|
||||||
|
local e = element.new(args)
|
||||||
|
|
||||||
|
assert(e.frame.h >= (#args.tabs * 3), "graphics.elements.controls.sidebar: height insufficent to display all tabs")
|
||||||
|
|
||||||
|
-- default to 1st tab
|
||||||
|
e.value = 1
|
||||||
|
|
||||||
|
-- show the button state
|
||||||
|
---@param pressed boolean if the currently selected tab should appear as actively pressed
|
||||||
|
---@param pressed_idx? integer optional index to show as held (that is not yet selected)
|
||||||
|
local function draw(pressed, pressed_idx)
|
||||||
|
pressed_idx = pressed_idx or e.value
|
||||||
|
|
||||||
|
for i = 1, #args.tabs do
|
||||||
|
local tab = args.tabs[i] ---@type sidebar_tab
|
||||||
|
|
||||||
|
local y = ((i - 1) * 3) + 1
|
||||||
|
|
||||||
|
e.window.setCursorPos(1, y)
|
||||||
|
|
||||||
|
if pressed and i == pressed_idx then
|
||||||
|
e.window.setTextColor(e.fg_bg.fgd)
|
||||||
|
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||||
|
else
|
||||||
|
e.window.setTextColor(tab.color.fgd)
|
||||||
|
e.window.setBackgroundColor(tab.color.bkg)
|
||||||
|
end
|
||||||
|
|
||||||
|
e.window.write(" ")
|
||||||
|
e.window.setCursorPos(1, y + 1)
|
||||||
|
if e.value == i then
|
||||||
|
-- show as selected
|
||||||
|
e.window.write(" " .. tab.char .. "\x10")
|
||||||
|
else
|
||||||
|
-- show as unselected
|
||||||
|
e.window.write(" " .. tab.char .. " ")
|
||||||
|
end
|
||||||
|
e.window.setCursorPos(1, y + 2)
|
||||||
|
e.window.write(" ")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- handle mouse interaction
|
||||||
|
---@param event mouse_interaction mouse event
|
||||||
|
function e.handle_mouse(event)
|
||||||
|
-- determine what was pressed
|
||||||
|
if e.enabled then
|
||||||
|
local cur_idx = math.ceil(event.current.y / 3)
|
||||||
|
local ini_idx = math.ceil(event.initial.y / 3)
|
||||||
|
|
||||||
|
if args.tabs[cur_idx] ~= nil then
|
||||||
|
if event.type == CLICK_TYPE.TAP then
|
||||||
|
e.value = cur_idx
|
||||||
|
draw(true)
|
||||||
|
-- show as unpressed in 0.25 seconds
|
||||||
|
tcd.dispatch(0.25, function () draw(false) end)
|
||||||
|
args.callback(e.value)
|
||||||
|
elseif event.type == CLICK_TYPE.DOWN then
|
||||||
|
draw(true, cur_idx)
|
||||||
|
elseif event.type == CLICK_TYPE.UP then
|
||||||
|
if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||||
|
e.value = cur_idx
|
||||||
|
draw(false)
|
||||||
|
args.callback(e.value)
|
||||||
|
else draw(false) end
|
||||||
|
end
|
||||||
|
elseif event.type == CLICK_TYPE.UP then
|
||||||
|
draw(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- set the value
|
||||||
|
---@param val integer new value
|
||||||
|
function e.set_value(val)
|
||||||
|
e.value = val
|
||||||
|
draw(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- initial draw
|
||||||
|
draw(false)
|
||||||
|
|
||||||
|
return e.get()
|
||||||
|
end
|
||||||
|
|
||||||
|
return sidebar
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
local element = require("graphics.element")
|
local element = require("graphics.element")
|
||||||
|
|
||||||
---@class spinbox_args
|
---@class spinbox_args
|
||||||
@@ -30,8 +31,7 @@ local function spinbox(args)
|
|||||||
assert(util.is_int(wn_prec), "graphics.element.controls.spinbox_numeric: whole number precision must be an integer")
|
assert(util.is_int(wn_prec), "graphics.element.controls.spinbox_numeric: whole number precision must be an integer")
|
||||||
assert(util.is_int(fr_prec), "graphics.element.controls.spinbox_numeric: fractional precision must be an integer")
|
assert(util.is_int(fr_prec), "graphics.element.controls.spinbox_numeric: fractional precision must be an integer")
|
||||||
|
|
||||||
local fmt = ""
|
local fmt, fmt_init ---@type string, string
|
||||||
local fmt_init = ""
|
|
||||||
|
|
||||||
if fr_prec > 0 then
|
if fr_prec > 0 then
|
||||||
fmt = "%" .. (wn_prec + fr_prec + 1) .. "." .. fr_prec .. "f"
|
fmt = "%" .. (wn_prec + fr_prec + 1) .. "." .. fr_prec .. "f"
|
||||||
@@ -131,19 +131,22 @@ local function spinbox(args)
|
|||||||
---@param event mouse_interaction mouse event
|
---@param event mouse_interaction mouse event
|
||||||
function e.handle_mouse(event)
|
function e.handle_mouse(event)
|
||||||
-- only handle if on an increment or decrement arrow
|
-- only handle if on an increment or decrement arrow
|
||||||
if e.enabled and event.x ~= dec_point_x then
|
if e.enabled and core.events.was_clicked(event.type) and
|
||||||
local idx = util.trinary(event.x > dec_point_x, event.x - 1, event.x)
|
(event.current.x ~= dec_point_x) and (event.current.y ~= 2) then
|
||||||
if digits[idx] ~= nil then
|
if event.current.x == event.initial.x and event.current.y == event.initial.y then
|
||||||
if event.y == 1 then
|
local idx = util.trinary(event.current.x > dec_point_x, event.current.x - 1, event.current.x)
|
||||||
-- increment
|
if digits[idx] ~= nil then
|
||||||
digits[idx] = digits[idx] + 1
|
if event.current.y == 1 then
|
||||||
elseif event.y == 3 then
|
-- increment
|
||||||
-- decrement
|
digits[idx] = digits[idx] + 1
|
||||||
digits[idx] = digits[idx] - 1
|
elseif event.current.y == 3 then
|
||||||
end
|
-- decrement
|
||||||
|
digits[idx] = digits[idx] - 1
|
||||||
|
end
|
||||||
|
|
||||||
update_value()
|
update_value()
|
||||||
show_num()
|
show_num()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
-- Button Graphics Element
|
-- Button Graphics Element
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
local element = require("graphics.element")
|
local element = require("graphics.element")
|
||||||
|
|
||||||
---@class switch_button_args
|
---@class switch_button_args
|
||||||
@@ -22,13 +23,15 @@ local function switch_button(args)
|
|||||||
assert(type(args.text) == "string", "graphics.elements.controls.switch_button: text is a required field")
|
assert(type(args.text) == "string", "graphics.elements.controls.switch_button: text is a required field")
|
||||||
assert(type(args.callback) == "function", "graphics.elements.controls.switch_button: callback is a required field")
|
assert(type(args.callback) == "function", "graphics.elements.controls.switch_button: callback is a required field")
|
||||||
assert(type(args.active_fg_bg) == "table", "graphics.elements.controls.switch_button: active_fg_bg is a required field")
|
assert(type(args.active_fg_bg) == "table", "graphics.elements.controls.switch_button: active_fg_bg is a required field")
|
||||||
|
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||||
|
"graphics.elements.controls.switch_button: min_width must be nil or a number > 0")
|
||||||
|
|
||||||
-- single line
|
|
||||||
args.height = 1
|
|
||||||
|
|
||||||
-- determine widths
|
|
||||||
local text_width = string.len(args.text)
|
local text_width = string.len(args.text)
|
||||||
args.width = math.max(text_width + 2, args.min_width)
|
|
||||||
|
-- single line height, calculate width
|
||||||
|
args.height = 1
|
||||||
|
args.min_width = args.min_width or 0
|
||||||
|
args.width = math.max(text_width, args.min_width)
|
||||||
|
|
||||||
-- create new graphics element base object
|
-- create new graphics element base object
|
||||||
local e = element.new(args)
|
local e = element.new(args)
|
||||||
@@ -64,9 +67,8 @@ local function switch_button(args)
|
|||||||
|
|
||||||
-- handle mouse interaction
|
-- handle mouse interaction
|
||||||
---@param event mouse_interaction mouse event
|
---@param event mouse_interaction mouse event
|
||||||
---@diagnostic disable-next-line: unused-local
|
|
||||||
function e.handle_mouse(event)
|
function e.handle_mouse(event)
|
||||||
if e.enabled then
|
if e.enabled and core.events.was_clicked(event.type) then
|
||||||
-- toggle state
|
-- toggle state
|
||||||
e.value = not e.value
|
e.value = not e.value
|
||||||
draw_state()
|
draw_state()
|
||||||
|
|||||||
130
graphics/elements/controls/tabbar.lua
Normal file
130
graphics/elements/controls/tabbar.lua
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
-- Tab Bar Graphics Element
|
||||||
|
|
||||||
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
|
local element = require("graphics.element")
|
||||||
|
|
||||||
|
---@class tabbar_tab
|
||||||
|
---@field name string tab name
|
||||||
|
---@field color cpair tab colors (fg/bg)
|
||||||
|
---@field _start_x integer starting touch x range (inclusive)
|
||||||
|
---@field _end_x integer ending touch x range (inclusive)
|
||||||
|
|
||||||
|
---@class tabbar_args
|
||||||
|
---@field tabs table tab options
|
||||||
|
---@field callback function function to call on tab change
|
||||||
|
---@field min_width? integer text length + 2 if omitted
|
||||||
|
---@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 fg_bg? cpair foreground/background colors
|
||||||
|
|
||||||
|
-- new tab selector
|
||||||
|
---@param args tabbar_args
|
||||||
|
---@return graphics_element element, element_id id
|
||||||
|
local function tabbar(args)
|
||||||
|
assert(type(args.tabs) == "table", "graphics.elements.controls.tabbar: tabs is a required field")
|
||||||
|
assert(#args.tabs > 0, "graphics.elements.controls.tabbar: at least one tab is required")
|
||||||
|
assert(type(args.callback) == "function", "graphics.elements.controls.tabbar: callback is a required field")
|
||||||
|
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||||
|
"graphics.elements.controls.tabbar: min_width must be nil or a number > 0")
|
||||||
|
|
||||||
|
-- always 1 tall
|
||||||
|
args.height = 1
|
||||||
|
|
||||||
|
-- determine widths
|
||||||
|
local max_width = 1
|
||||||
|
for i = 1, #args.tabs do
|
||||||
|
local opt = args.tabs[i] ---@type tabbar_tab
|
||||||
|
if string.len(opt.name) > max_width then
|
||||||
|
max_width = string.len(opt.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local button_width = math.max(max_width, args.min_width or 0)
|
||||||
|
|
||||||
|
-- create new graphics element base object
|
||||||
|
local e = element.new(args)
|
||||||
|
|
||||||
|
assert(e.frame.w >= (button_width * #args.tabs), "graphics.elements.controls.tabbar: width insufficent to display all tabs")
|
||||||
|
|
||||||
|
-- default to 1st tab
|
||||||
|
e.value = 1
|
||||||
|
|
||||||
|
-- calculate required tab dimension information
|
||||||
|
local next_x = 1
|
||||||
|
for i = 1, #args.tabs do
|
||||||
|
local tab = args.tabs[i] ---@type tabbar_tab
|
||||||
|
|
||||||
|
tab._start_x = next_x
|
||||||
|
tab._end_x = next_x + button_width - 1
|
||||||
|
|
||||||
|
next_x = next_x + button_width
|
||||||
|
end
|
||||||
|
|
||||||
|
-- show the tab state
|
||||||
|
local function draw()
|
||||||
|
for i = 1, #args.tabs do
|
||||||
|
local tab = args.tabs[i] ---@type tabbar_tab
|
||||||
|
|
||||||
|
e.window.setCursorPos(tab._start_x, 1)
|
||||||
|
|
||||||
|
if e.value == i then
|
||||||
|
e.window.setTextColor(tab.color.fgd)
|
||||||
|
e.window.setBackgroundColor(tab.color.bkg)
|
||||||
|
else
|
||||||
|
e.window.setTextColor(e.fg_bg.fgd)
|
||||||
|
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||||
|
end
|
||||||
|
|
||||||
|
e.window.write(util.pad(tab.name, button_width))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check which tab a given x is within
|
||||||
|
---@return integer|nil button index or nil if not within a tab
|
||||||
|
local function which_tab(x)
|
||||||
|
for i = 1, #args.tabs do
|
||||||
|
local tab = args.tabs[i] ---@type tabbar_tab
|
||||||
|
if x >= tab._start_x and x <= tab._end_x then return i end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- handle mouse interaction
|
||||||
|
---@param event mouse_interaction mouse event
|
||||||
|
function e.handle_mouse(event)
|
||||||
|
-- determine what was pressed
|
||||||
|
if e.enabled and core.events.was_clicked(event.type) then
|
||||||
|
-- a button may have been pressed, which one was it?
|
||||||
|
local tab_ini = which_tab(event.initial.x)
|
||||||
|
local tab_cur = which_tab(event.current.x)
|
||||||
|
|
||||||
|
-- mouse up must always have started with a mouse down on the same tab to count as a click
|
||||||
|
-- tap always has identical coordinates, so this always passes for taps
|
||||||
|
if tab_ini == tab_cur and tab_cur ~= nil then
|
||||||
|
e.value = tab_cur
|
||||||
|
draw()
|
||||||
|
args.callback(e.value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- set the value
|
||||||
|
---@param val integer new value
|
||||||
|
function e.set_value(val)
|
||||||
|
e.value = val
|
||||||
|
draw()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- initial draw
|
||||||
|
draw()
|
||||||
|
|
||||||
|
return e.get()
|
||||||
|
end
|
||||||
|
|
||||||
|
return tabbar
|
||||||
@@ -26,7 +26,7 @@ local function core_map(args)
|
|||||||
args.height = 18
|
args.height = 18
|
||||||
|
|
||||||
-- inherit only foreground color
|
-- inherit only foreground color
|
||||||
args.fg_bg = core.graphics.cpair(args.parent.get_fg_bg().fgd, colors.gray)
|
args.fg_bg = core.cpair(args.parent.get_fg_bg().fgd, colors.gray)
|
||||||
|
|
||||||
-- create new graphics element base object
|
-- create new graphics element base object
|
||||||
local e = element.new(args)
|
local e = element.new(args)
|
||||||
@@ -73,7 +73,7 @@ local function core_map(args)
|
|||||||
local function draw_core(t)
|
local function draw_core(t)
|
||||||
local i = 1
|
local i = 1
|
||||||
local back_c = "F"
|
local back_c = "F"
|
||||||
local text_c = "8"
|
local text_c ---@type string
|
||||||
|
|
||||||
-- determine fuel assembly coloring
|
-- determine fuel assembly coloring
|
||||||
if t <= 300 then
|
if t <= 300 then
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ local function indicator_led(args)
|
|||||||
args.height = 1
|
args.height = 1
|
||||||
|
|
||||||
-- determine width
|
-- determine width
|
||||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||||
|
|
||||||
-- flasher state
|
-- flasher state
|
||||||
local flash_on = true
|
local flash_on = true
|
||||||
@@ -89,8 +89,10 @@ local function indicator_led(args)
|
|||||||
|
|
||||||
-- write label and initial indicator light
|
-- write label and initial indicator light
|
||||||
e.on_update(false)
|
e.on_update(false)
|
||||||
e.window.setCursorPos(3, 1)
|
if string.len(args.label) > 0 then
|
||||||
e.window.write(args.label)
|
e.window.setCursorPos(3, 1)
|
||||||
|
e.window.write(args.label)
|
||||||
|
end
|
||||||
|
|
||||||
return e.get()
|
return e.get()
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ local function indicator_led_pair(args)
|
|||||||
args.height = 1
|
args.height = 1
|
||||||
|
|
||||||
-- determine width
|
-- determine width
|
||||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||||
|
|
||||||
-- flasher state
|
-- flasher state
|
||||||
local flash_on = true
|
local flash_on = true
|
||||||
@@ -103,8 +103,10 @@ local function indicator_led_pair(args)
|
|||||||
|
|
||||||
-- write label and initial indicator light
|
-- write label and initial indicator light
|
||||||
e.on_update(1)
|
e.on_update(1)
|
||||||
e.window.setCursorPos(3, 1)
|
if string.len(args.label) > 0 then
|
||||||
e.window.write(args.label)
|
e.window.setCursorPos(3, 1)
|
||||||
|
e.window.write(args.label)
|
||||||
|
end
|
||||||
|
|
||||||
return e.get()
|
return e.get()
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ local function indicator_led_rgb(args)
|
|||||||
args.height = 1
|
args.height = 1
|
||||||
|
|
||||||
-- determine width
|
-- determine width
|
||||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||||
|
|
||||||
-- create new graphics element base object
|
-- create new graphics element base object
|
||||||
local e = element.new(args)
|
local e = element.new(args)
|
||||||
@@ -38,7 +38,7 @@ local function indicator_led_rgb(args)
|
|||||||
e.value = new_state
|
e.value = new_state
|
||||||
e.window.setCursorPos(1, 1)
|
e.window.setCursorPos(1, 1)
|
||||||
if type(args.colors[new_state]) == "number" then
|
if type(args.colors[new_state]) == "number" then
|
||||||
e.window.blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg)
|
e.window.blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -48,8 +48,10 @@ local function indicator_led_rgb(args)
|
|||||||
|
|
||||||
-- write label and initial indicator light
|
-- write label and initial indicator light
|
||||||
e.on_update(1)
|
e.on_update(1)
|
||||||
e.window.setCursorPos(3, 1)
|
if string.len(args.label) > 0 then
|
||||||
e.window.write(args.label)
|
e.window.setCursorPos(3, 1)
|
||||||
|
e.window.write(args.label)
|
||||||
|
end
|
||||||
|
|
||||||
return e.get()
|
return e.get()
|
||||||
end
|
end
|
||||||
|
|||||||
42
graphics/elements/multipane.lua
Normal file
42
graphics/elements/multipane.lua
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
-- Multi-Pane Display Graphics Element
|
||||||
|
|
||||||
|
local element = require("graphics.element")
|
||||||
|
|
||||||
|
---@class multipane_args
|
||||||
|
---@field panes table panes to swap between
|
||||||
|
---@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
|
||||||
|
|
||||||
|
-- new multipane element
|
||||||
|
---@nodiscard
|
||||||
|
---@param args multipane_args
|
||||||
|
---@return graphics_element element, element_id id
|
||||||
|
local function multipane(args)
|
||||||
|
assert(type(args.panes) == "table", "graphics.elements.multipane: panes is a required field")
|
||||||
|
|
||||||
|
-- create new graphics element base object
|
||||||
|
local e = element.new(args)
|
||||||
|
|
||||||
|
-- select which pane is shown
|
||||||
|
---@param value integer pane to show
|
||||||
|
function e.set_value(value)
|
||||||
|
if (e.value ~= value) and (value > 0) and (value <= #args.panes) then
|
||||||
|
e.value = value
|
||||||
|
|
||||||
|
for i = 1, #args.panes do args.panes[i].hide() end
|
||||||
|
args.panes[value].show()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
e.set_value(1)
|
||||||
|
|
||||||
|
return e.get()
|
||||||
|
end
|
||||||
|
|
||||||
|
return multipane
|
||||||
@@ -37,7 +37,7 @@ local function pipenet(args)
|
|||||||
args.y = args.y or 1
|
args.y = args.y or 1
|
||||||
|
|
||||||
if args.bg ~= nil then
|
if args.bg ~= nil then
|
||||||
args.fg_bg = core.graphics.cpair(args.bg, args.bg)
|
args.fg_bg = core.cpair(args.bg, args.bg)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- create new graphics element base object
|
-- create new graphics element base object
|
||||||
@@ -55,7 +55,7 @@ local function pipenet(args)
|
|||||||
|
|
||||||
e.window.setCursorPos(x, y)
|
e.window.setCursorPos(x, y)
|
||||||
|
|
||||||
local c = core.graphics.cpair(pipe.color, e.fg_bg.bkg)
|
local c = core.cpair(pipe.color, e.fg_bg.bkg)
|
||||||
|
|
||||||
if pipe.align_tr then
|
if pipe.align_tr then
|
||||||
-- cross width then height
|
-- cross width then height
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ local util = require("scada-common.util")
|
|||||||
local core = require("graphics.core")
|
local core = require("graphics.core")
|
||||||
local element = require("graphics.element")
|
local element = require("graphics.element")
|
||||||
|
|
||||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
---@class textbox_args
|
---@class textbox_args
|
||||||
---@field text string text to show
|
---@field text string text to show
|
||||||
|
|||||||
161
graphics/events.lua
Normal file
161
graphics/events.lua
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
--
|
||||||
|
-- Graphics Events and Event Handlers
|
||||||
|
--
|
||||||
|
|
||||||
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
|
local events = {}
|
||||||
|
|
||||||
|
---@enum CLICK_BUTTON
|
||||||
|
events.CLICK_BUTTON = {
|
||||||
|
GENERIC = 0,
|
||||||
|
LEFT_BUTTON = 1,
|
||||||
|
RIGHT_BUTTON = 2,
|
||||||
|
MID_BUTTON = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
---@enum CLICK_TYPE
|
||||||
|
events.CLICK_TYPE = {
|
||||||
|
TAP = 1, -- screen tap (complete click)
|
||||||
|
DOWN = 2, -- button down
|
||||||
|
UP = 3, -- button up (completed a click)
|
||||||
|
DRAG = 4, -- mouse dragged
|
||||||
|
SCROLL_DOWN = 5, -- scroll down
|
||||||
|
SCROLL_UP = 6 -- scroll up
|
||||||
|
}
|
||||||
|
|
||||||
|
-- create a new 2D coordinate
|
||||||
|
---@param x integer
|
||||||
|
---@param y integer
|
||||||
|
---@return coordinate_2d
|
||||||
|
local function _coord2d(x, y) return { x = x, y = y } end
|
||||||
|
|
||||||
|
---@class mouse_interaction
|
||||||
|
---@field monitor string
|
||||||
|
---@field button CLICK_BUTTON
|
||||||
|
---@field type CLICK_TYPE
|
||||||
|
---@field initial coordinate_2d
|
||||||
|
---@field current coordinate_2d
|
||||||
|
|
||||||
|
local handler = {
|
||||||
|
-- left, right, middle button down tracking
|
||||||
|
button_down = {
|
||||||
|
_coord2d(0, 0),
|
||||||
|
_coord2d(0, 0),
|
||||||
|
_coord2d(0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-- create a new monitor touch mouse interaction event
|
||||||
|
---@nodiscard
|
||||||
|
---@param monitor string
|
||||||
|
---@param x integer
|
||||||
|
---@param y integer
|
||||||
|
---@return mouse_interaction
|
||||||
|
local function _monitor_touch(monitor, x, y)
|
||||||
|
return {
|
||||||
|
monitor = monitor,
|
||||||
|
button = events.CLICK_BUTTON.GENERIC,
|
||||||
|
type = events.CLICK_TYPE.TAP,
|
||||||
|
initial = _coord2d(x, y),
|
||||||
|
current = _coord2d(x, y)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- create a new mouse button mouse interaction event
|
||||||
|
---@nodiscard
|
||||||
|
---@param button CLICK_BUTTON mouse button
|
||||||
|
---@param type CLICK_TYPE click type
|
||||||
|
---@param x1 integer initial x
|
||||||
|
---@param y1 integer initial y
|
||||||
|
---@param x2 integer current x
|
||||||
|
---@param y2 integer current y
|
||||||
|
---@return mouse_interaction
|
||||||
|
local function _mouse_event(button, type, x1, y1, x2, y2)
|
||||||
|
return {
|
||||||
|
monitor = "terminal",
|
||||||
|
button = button,
|
||||||
|
type = type,
|
||||||
|
initial = _coord2d(x1, y1),
|
||||||
|
current = _coord2d(x2, y2)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- create a new generic mouse interaction event
|
||||||
|
---@nodiscard
|
||||||
|
---@param type CLICK_TYPE
|
||||||
|
---@param x integer
|
||||||
|
---@param y integer
|
||||||
|
---@return mouse_interaction
|
||||||
|
function events.mouse_generic(type, x, y)
|
||||||
|
return {
|
||||||
|
monitor = "",
|
||||||
|
button = events.CLICK_BUTTON.GENERIC,
|
||||||
|
type = type,
|
||||||
|
initial = _coord2d(x, y),
|
||||||
|
current = _coord2d(x, y)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- create a new transposed mouse interaction event using the event's monitor/button fields
|
||||||
|
---@nodiscard
|
||||||
|
---@param event mouse_interaction
|
||||||
|
---@param elem_pos_x integer element's x position: new x = (event x - element x) + 1
|
||||||
|
---@param elem_pos_y integer element's y position: new y = (event y - element y) + 1
|
||||||
|
---@return mouse_interaction
|
||||||
|
function events.mouse_transposed(event, elem_pos_x, elem_pos_y)
|
||||||
|
return {
|
||||||
|
monitor = event.monitor,
|
||||||
|
button = event.button,
|
||||||
|
type = event.type,
|
||||||
|
initial = _coord2d((event.initial.x - elem_pos_x) + 1, (event.initial.y - elem_pos_y) + 1),
|
||||||
|
current = _coord2d((event.current.x - elem_pos_x) + 1, (event.current.y - elem_pos_y) + 1)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check if an event qualifies as a click (tap or up)
|
||||||
|
---@nodiscard
|
||||||
|
---@param t CLICK_TYPE
|
||||||
|
function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events.CLICK_TYPE.UP end
|
||||||
|
|
||||||
|
-- create a new mouse event to pass onto graphics renderer<br>
|
||||||
|
-- supports: mouse_click, mouse_up, mouse_drag, mouse_scroll, and monitor_touch
|
||||||
|
---@param event_type os_event OS event to handle
|
||||||
|
---@param opt integer|string button, scroll direction, or monitor for monitor touch
|
||||||
|
---@param x integer x coordinate
|
||||||
|
---@param y integer y coordinate
|
||||||
|
---@return mouse_interaction|nil
|
||||||
|
function events.new_mouse_event(event_type, opt, x, y)
|
||||||
|
if event_type == "mouse_click" then
|
||||||
|
---@cast opt 1|2|3
|
||||||
|
handler.button_down[opt] = _coord2d(x, y)
|
||||||
|
return _mouse_event(opt, events.CLICK_TYPE.DOWN, x, y, x, y)
|
||||||
|
elseif event_type == "mouse_up" then
|
||||||
|
---@cast opt 1|2|3
|
||||||
|
local initial = handler.button_down[opt] ---@type coordinate_2d
|
||||||
|
return _mouse_event(opt, events.CLICK_TYPE.UP, initial.x, initial.y, x, y)
|
||||||
|
elseif event_type == "monitor_touch" then
|
||||||
|
---@cast opt string
|
||||||
|
return _monitor_touch(opt, x, y)
|
||||||
|
elseif event_type == "mouse_drag" then
|
||||||
|
---@cast opt 1|2|3
|
||||||
|
local initial = handler.button_down[opt] ---@type coordinate_2d
|
||||||
|
return _mouse_event(opt, events.CLICK_TYPE.DRAG, initial.x, initial.y, x, y)
|
||||||
|
elseif event_type == "mouse_scroll" then
|
||||||
|
---@cast opt 1|-1
|
||||||
|
local scroll_direction = util.trinary(opt == 1, events.CLICK_TYPE.SCROLL_DOWN, events.CLICK_TYPE.SCROLL_UP)
|
||||||
|
return _mouse_event(events.CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- create a new key event to pass onto graphics renderer<br>
|
||||||
|
-- supports: char, key, and key_up
|
||||||
|
---@param event_type os_event
|
||||||
|
function events.new_key_event(event_type)
|
||||||
|
if event_type == "char" then
|
||||||
|
elseif event_type == "key" then
|
||||||
|
elseif event_type == "key_up" then
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return events
|
||||||
28
imgen.py
28
imgen.py
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
# list files in a directory
|
# list files in a directory
|
||||||
def list_files(path):
|
def list_files(path):
|
||||||
@@ -69,7 +70,7 @@ def make_manifest(size):
|
|||||||
},
|
},
|
||||||
"depends" : {
|
"depends" : {
|
||||||
"reactor-plc" : [ "system", "common", "graphics" ],
|
"reactor-plc" : [ "system", "common", "graphics" ],
|
||||||
"rtu" : [ "system", "common" ],
|
"rtu" : [ "system", "common", "graphics" ],
|
||||||
"supervisor" : [ "system", "common" ],
|
"supervisor" : [ "system", "common" ],
|
||||||
"coordinator" : [ "system", "common", "graphics" ],
|
"coordinator" : [ "system", "common", "graphics" ],
|
||||||
"pocket" : [ "system", "common", "graphics" ]
|
"pocket" : [ "system", "common", "graphics" ]
|
||||||
@@ -100,7 +101,30 @@ f.close()
|
|||||||
|
|
||||||
manifest_size = os.path.getsize("install_manifest.json")
|
manifest_size = os.path.getsize("install_manifest.json")
|
||||||
|
|
||||||
|
final_manifest = make_manifest(manifest_size)
|
||||||
|
|
||||||
# calculate file size then regenerate with embedded size
|
# calculate file size then regenerate with embedded size
|
||||||
f = open("install_manifest.json", "w")
|
f = open("install_manifest.json", "w")
|
||||||
json.dump(make_manifest(manifest_size), f)
|
json.dump(final_manifest, f)
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == "shields":
|
||||||
|
# write all the JSON files for shields.io
|
||||||
|
for key, version in final_manifest["versions"].items():
|
||||||
|
f = open("./shields/" + key + ".json", "w")
|
||||||
|
|
||||||
|
if version.find("alpha") >= 0:
|
||||||
|
color = "yellow"
|
||||||
|
elif version.find("beta") >= 0:
|
||||||
|
color = "orange"
|
||||||
|
else:
|
||||||
|
color = "blue"
|
||||||
|
|
||||||
|
json.dump({
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"label": key,
|
||||||
|
"message": "" + version,
|
||||||
|
"color": color
|
||||||
|
}, f)
|
||||||
|
|
||||||
|
f.close()
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,23 @@
|
|||||||
|
local config = {}
|
||||||
|
|
||||||
|
-- port of the SCADA supervisor
|
||||||
|
config.SCADA_SV_PORT = 16100
|
||||||
|
-- port for SCADA coordinator API access
|
||||||
|
config.SCADA_API_PORT = 16200
|
||||||
|
-- port to listen to incoming packets FROM servers
|
||||||
|
config.LISTEN_PORT = 16201
|
||||||
|
-- max trusted modem message distance (0 to disable check)
|
||||||
|
config.TRUSTED_RANGE = 0
|
||||||
|
-- time in seconds (>= 2) before assuming a remote device is no longer active
|
||||||
|
config.COMMS_TIMEOUT = 5
|
||||||
|
|
||||||
|
-- log path
|
||||||
|
config.LOG_PATH = "/log.txt"
|
||||||
|
-- log mode
|
||||||
|
-- 0 = APPEND (adds to existing file on start)
|
||||||
|
-- 1 = NEW (replaces existing file on start)
|
||||||
|
config.LOG_MODE = 0
|
||||||
|
-- true to log verbose debug messages
|
||||||
|
config.LOG_DEBUG = false
|
||||||
|
|
||||||
|
return config
|
||||||
|
|||||||
35
pocket/coreio.lua
Normal file
35
pocket/coreio.lua
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
--
|
||||||
|
-- Core I/O - Pocket Central I/O Management
|
||||||
|
--
|
||||||
|
|
||||||
|
local psil = require("scada-common.psil")
|
||||||
|
|
||||||
|
local coreio = {}
|
||||||
|
|
||||||
|
---@class pocket_core_io
|
||||||
|
local io = {
|
||||||
|
ps = psil.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
---@enum POCKET_LINK_STATE
|
||||||
|
local LINK_STATE = {
|
||||||
|
UNLINKED = 0,
|
||||||
|
SV_LINK_ONLY = 1,
|
||||||
|
API_LINK_ONLY = 2,
|
||||||
|
LINKED = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
coreio.LINK_STATE = LINK_STATE
|
||||||
|
|
||||||
|
-- get the core PSIL
|
||||||
|
function coreio.core_ps()
|
||||||
|
return io.ps
|
||||||
|
end
|
||||||
|
|
||||||
|
-- set network link state
|
||||||
|
---@param state POCKET_LINK_STATE
|
||||||
|
function coreio.report_link_state(state)
|
||||||
|
io.ps.publish("link_state", state)
|
||||||
|
end
|
||||||
|
|
||||||
|
return coreio
|
||||||
408
pocket/pocket.lua
Normal file
408
pocket/pocket.lua
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
local comms = require("scada-common.comms")
|
||||||
|
local log = require("scada-common.log")
|
||||||
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
|
local coreio = require("pocket.coreio")
|
||||||
|
|
||||||
|
local PROTOCOL = comms.PROTOCOL
|
||||||
|
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||||
|
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||||
|
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||||
|
-- local CAPI_TYPE = comms.CAPI_TYPE
|
||||||
|
|
||||||
|
local LINK_STATE = coreio.LINK_STATE
|
||||||
|
|
||||||
|
local pocket = {}
|
||||||
|
|
||||||
|
-- pocket coordinator + supervisor communications
|
||||||
|
---@nodiscard
|
||||||
|
---@param version string pocket version
|
||||||
|
---@param modem table modem device
|
||||||
|
---@param local_port integer local pocket port
|
||||||
|
---@param sv_port integer port of supervisor
|
||||||
|
---@param api_port integer port of coordinator API
|
||||||
|
---@param range integer trusted device connection range
|
||||||
|
---@param sv_watchdog watchdog
|
||||||
|
---@param api_watchdog watchdog
|
||||||
|
function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_watchdog, api_watchdog)
|
||||||
|
local self = {
|
||||||
|
sv = {
|
||||||
|
linked = false,
|
||||||
|
seq_num = 0,
|
||||||
|
r_seq_num = nil, ---@type nil|integer
|
||||||
|
last_est_ack = ESTABLISH_ACK.ALLOW
|
||||||
|
},
|
||||||
|
api = {
|
||||||
|
linked = false,
|
||||||
|
seq_num = 0,
|
||||||
|
r_seq_num = nil, ---@type nil|integer
|
||||||
|
last_est_ack = ESTABLISH_ACK.ALLOW
|
||||||
|
},
|
||||||
|
establish_delay_counter = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
comms.set_trusted_range(range)
|
||||||
|
|
||||||
|
-- PRIVATE FUNCTIONS --
|
||||||
|
|
||||||
|
-- configure modem channels
|
||||||
|
local function _conf_channels()
|
||||||
|
modem.closeAll()
|
||||||
|
modem.open(local_port)
|
||||||
|
end
|
||||||
|
|
||||||
|
_conf_channels()
|
||||||
|
|
||||||
|
-- send a management packet to the supervisor
|
||||||
|
---@param msg_type SCADA_MGMT_TYPE
|
||||||
|
---@param msg table
|
||||||
|
local function _send_sv(msg_type, msg)
|
||||||
|
local s_pkt = comms.scada_packet()
|
||||||
|
local pkt = comms.mgmt_packet()
|
||||||
|
|
||||||
|
pkt.make(msg_type, msg)
|
||||||
|
s_pkt.make(self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
||||||
|
|
||||||
|
modem.transmit(sv_port, local_port, s_pkt.raw_sendable())
|
||||||
|
self.sv.seq_num = self.sv.seq_num + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- send a management packet to the coordinator
|
||||||
|
---@param msg_type SCADA_MGMT_TYPE
|
||||||
|
---@param msg table
|
||||||
|
local function _send_crd(msg_type, msg)
|
||||||
|
local s_pkt = comms.scada_packet()
|
||||||
|
local pkt = comms.mgmt_packet()
|
||||||
|
|
||||||
|
pkt.make(msg_type, msg)
|
||||||
|
s_pkt.make(self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
||||||
|
|
||||||
|
modem.transmit(api_port, local_port, s_pkt.raw_sendable())
|
||||||
|
self.api.seq_num = self.api.seq_num + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- send a packet to the coordinator API
|
||||||
|
-----@param msg_type CAPI_TYPE
|
||||||
|
-----@param msg table
|
||||||
|
-- local function _send_api(msg_type, msg)
|
||||||
|
-- local s_pkt = comms.scada_packet()
|
||||||
|
-- local pkt = comms.capi_packet()
|
||||||
|
|
||||||
|
-- pkt.make(msg_type, msg)
|
||||||
|
-- s_pkt.make(self.api.seq_num, PROTOCOL.COORD_API, pkt.raw_sendable())
|
||||||
|
|
||||||
|
-- modem.transmit(api_port, local_port, s_pkt.raw_sendable())
|
||||||
|
-- self.api.seq_num = self.api.seq_num + 1
|
||||||
|
-- end
|
||||||
|
|
||||||
|
-- attempt supervisor connection establishment
|
||||||
|
local function _send_sv_establish()
|
||||||
|
_send_sv(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- attempt coordinator API connection establishment
|
||||||
|
local function _send_api_establish()
|
||||||
|
_send_crd(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- keep alive ack to supervisor
|
||||||
|
---@param srv_time integer
|
||||||
|
local function _send_sv_keep_alive_ack(srv_time)
|
||||||
|
_send_sv(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- keep alive ack to coordinator
|
||||||
|
---@param srv_time integer
|
||||||
|
local function _send_api_keep_alive_ack(srv_time)
|
||||||
|
_send_crd(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- PUBLIC FUNCTIONS --
|
||||||
|
|
||||||
|
---@class pocket_comms
|
||||||
|
local public = {}
|
||||||
|
|
||||||
|
-- reconnect a newly connected modem
|
||||||
|
---@param new_modem table
|
||||||
|
function public.reconnect_modem(new_modem)
|
||||||
|
modem = new_modem
|
||||||
|
_conf_channels()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- close connection to the supervisor
|
||||||
|
function public.close_sv()
|
||||||
|
sv_watchdog.cancel()
|
||||||
|
self.sv.linked = false
|
||||||
|
_send_sv(SCADA_MGMT_TYPE.CLOSE, {})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- close connection to coordinator API server
|
||||||
|
function public.close_api()
|
||||||
|
api_watchdog.cancel()
|
||||||
|
self.api.linked = false
|
||||||
|
_send_crd(SCADA_MGMT_TYPE.CLOSE, {})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- close the connections to the servers
|
||||||
|
function public.close()
|
||||||
|
public.close_sv()
|
||||||
|
public.close_api()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- attempt to re-link if any of the dependent links aren't active
|
||||||
|
function public.link_update()
|
||||||
|
if not self.sv.linked then
|
||||||
|
coreio.report_link_state(util.trinary(self.api.linked, LINK_STATE.API_LINK_ONLY, LINK_STATE.UNLINKED))
|
||||||
|
|
||||||
|
if self.establish_delay_counter <= 0 then
|
||||||
|
_send_sv_establish()
|
||||||
|
self.establish_delay_counter = 4
|
||||||
|
else
|
||||||
|
self.establish_delay_counter = self.establish_delay_counter - 1
|
||||||
|
end
|
||||||
|
elseif not self.api.linked then
|
||||||
|
coreio.report_link_state(LINK_STATE.SV_LINK_ONLY)
|
||||||
|
|
||||||
|
if self.establish_delay_counter <= 0 then
|
||||||
|
_send_api_establish()
|
||||||
|
self.establish_delay_counter = 4
|
||||||
|
else
|
||||||
|
self.establish_delay_counter = self.establish_delay_counter - 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- linked, all good!
|
||||||
|
coreio.report_link_state(LINK_STATE.LINKED)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- parse a packet
|
||||||
|
---@param side string
|
||||||
|
---@param sender integer
|
||||||
|
---@param reply_to integer
|
||||||
|
---@param message any
|
||||||
|
---@param distance integer
|
||||||
|
---@return mgmt_frame|capi_frame|nil packet
|
||||||
|
function public.parse_packet(side, sender, reply_to, message, distance)
|
||||||
|
local pkt = nil
|
||||||
|
local s_pkt = comms.scada_packet()
|
||||||
|
|
||||||
|
-- parse packet as generic SCADA packet
|
||||||
|
s_pkt.receive(side, sender, reply_to, message, distance)
|
||||||
|
|
||||||
|
if s_pkt.is_valid() then
|
||||||
|
-- get as SCADA management packet
|
||||||
|
if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
|
||||||
|
local mgmt_pkt = comms.mgmt_packet()
|
||||||
|
if mgmt_pkt.decode(s_pkt) then
|
||||||
|
pkt = mgmt_pkt.get()
|
||||||
|
end
|
||||||
|
-- get as coordinator API packet
|
||||||
|
elseif s_pkt.protocol() == PROTOCOL.COORD_API then
|
||||||
|
local capi_pkt = comms.capi_packet()
|
||||||
|
if capi_pkt.decode(s_pkt) then
|
||||||
|
pkt = capi_pkt.get()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return pkt
|
||||||
|
end
|
||||||
|
|
||||||
|
-- handle a packet
|
||||||
|
---@param packet mgmt_frame|capi_frame|nil
|
||||||
|
function public.handle_packet(packet)
|
||||||
|
if packet ~= nil then
|
||||||
|
local l_port = packet.scada_frame.local_port()
|
||||||
|
local r_port = packet.scada_frame.remote_port()
|
||||||
|
local protocol = packet.scada_frame.protocol()
|
||||||
|
|
||||||
|
if l_port ~= local_port then
|
||||||
|
log.debug("received packet on unconfigured channel " .. l_port, true)
|
||||||
|
elseif r_port == api_port then
|
||||||
|
-- check sequence number
|
||||||
|
if self.api.r_seq_num == nil then
|
||||||
|
self.api.r_seq_num = packet.scada_frame.seq_num()
|
||||||
|
elseif self.connected and ((self.api.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
|
||||||
|
log.warning("sequence out-of-order: last = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||||
|
return
|
||||||
|
else
|
||||||
|
self.api.r_seq_num = packet.scada_frame.seq_num()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- feed watchdog on valid sequence number
|
||||||
|
api_watchdog.feed()
|
||||||
|
|
||||||
|
if protocol == PROTOCOL.COORD_API then
|
||||||
|
---@cast packet capi_frame
|
||||||
|
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||||
|
---@cast packet mgmt_frame
|
||||||
|
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||||
|
-- connection with coordinator established
|
||||||
|
if packet.length == 1 then
|
||||||
|
local est_ack = packet.data[1]
|
||||||
|
|
||||||
|
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||||
|
log.info("coordinator connection established")
|
||||||
|
self.establish_delay_counter = 0
|
||||||
|
self.api.linked = true
|
||||||
|
|
||||||
|
if self.sv.linked then
|
||||||
|
coreio.report_link_state(LINK_STATE.LINKED)
|
||||||
|
else
|
||||||
|
coreio.report_link_state(LINK_STATE.API_LINK_ONLY)
|
||||||
|
end
|
||||||
|
elseif est_ack == ESTABLISH_ACK.DENY then
|
||||||
|
if self.api.last_est_ack ~= est_ack then
|
||||||
|
log.info("coordinator connection denied")
|
||||||
|
end
|
||||||
|
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||||
|
if self.api.last_est_ack ~= est_ack then
|
||||||
|
log.info("coordinator connection denied due to collision")
|
||||||
|
end
|
||||||
|
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||||
|
if self.api.last_est_ack ~= est_ack then
|
||||||
|
log.info("coordinator comms version mismatch")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug("coordinator SCADA_MGMT establish packet reply unsupported")
|
||||||
|
end
|
||||||
|
|
||||||
|
self.api.last_est_ack = est_ack
|
||||||
|
else
|
||||||
|
log.debug("coordinator SCADA_MGMT establish packet length mismatch")
|
||||||
|
end
|
||||||
|
elseif self.api.linked then
|
||||||
|
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||||
|
-- keep alive request received, echo back
|
||||||
|
if packet.length == 1 then
|
||||||
|
local timestamp = packet.data[1]
|
||||||
|
local trip_time = util.time() - timestamp
|
||||||
|
|
||||||
|
if trip_time > 750 then
|
||||||
|
log.warning("pocket coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- log.debug("pocket coordinator RTT = " .. trip_time .. "ms")
|
||||||
|
|
||||||
|
_send_api_keep_alive_ack(timestamp)
|
||||||
|
else
|
||||||
|
log.debug("coordinator SCADA keep alive packet length mismatch")
|
||||||
|
end
|
||||||
|
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
|
||||||
|
-- handle session close
|
||||||
|
api_watchdog.cancel()
|
||||||
|
self.api.linked = false
|
||||||
|
log.info("coordinator server connection closed by remote host")
|
||||||
|
else
|
||||||
|
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug("discarding coordinator non-link SCADA_MGMT packet before linked")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug("illegal packet type " .. protocol .. " from coordinator", true)
|
||||||
|
end
|
||||||
|
elseif r_port == sv_port then
|
||||||
|
-- check sequence number
|
||||||
|
if self.sv.r_seq_num == nil then
|
||||||
|
self.sv.r_seq_num = packet.scada_frame.seq_num()
|
||||||
|
elseif self.connected and ((self.sv.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
|
||||||
|
log.warning("sequence out-of-order: last = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||||
|
return
|
||||||
|
else
|
||||||
|
self.sv.r_seq_num = packet.scada_frame.seq_num()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- feed watchdog on valid sequence number
|
||||||
|
sv_watchdog.feed()
|
||||||
|
|
||||||
|
-- handle packet
|
||||||
|
if protocol == PROTOCOL.SCADA_MGMT then
|
||||||
|
---@cast packet mgmt_frame
|
||||||
|
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||||
|
-- connection with supervisor established
|
||||||
|
if packet.length == 1 then
|
||||||
|
local est_ack = packet.data[1]
|
||||||
|
|
||||||
|
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||||
|
log.info("supervisor connection established")
|
||||||
|
self.establish_delay_counter = 0
|
||||||
|
self.sv.linked = true
|
||||||
|
|
||||||
|
if self.api.linked then
|
||||||
|
coreio.report_link_state(LINK_STATE.LINKED)
|
||||||
|
else
|
||||||
|
coreio.report_link_state(LINK_STATE.SV_LINK_ONLY)
|
||||||
|
end
|
||||||
|
elseif est_ack == ESTABLISH_ACK.DENY then
|
||||||
|
if self.sv.last_est_ack ~= est_ack then
|
||||||
|
log.info("supervisor connection denied")
|
||||||
|
end
|
||||||
|
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||||
|
if self.sv.last_est_ack ~= est_ack then
|
||||||
|
log.info("supervisor connection denied due to collision")
|
||||||
|
end
|
||||||
|
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||||
|
if self.sv.last_est_ack ~= est_ack then
|
||||||
|
log.info("supervisor comms version mismatch")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug("supervisor SCADA_MGMT establish packet reply unsupported")
|
||||||
|
end
|
||||||
|
|
||||||
|
self.sv.last_est_ack = est_ack
|
||||||
|
else
|
||||||
|
log.debug("supervisor SCADA_MGMT establish packet length mismatch")
|
||||||
|
end
|
||||||
|
elseif self.sv.linked then
|
||||||
|
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||||
|
-- keep alive request received, echo back
|
||||||
|
if packet.length == 1 then
|
||||||
|
local timestamp = packet.data[1]
|
||||||
|
local trip_time = util.time() - timestamp
|
||||||
|
|
||||||
|
if trip_time > 750 then
|
||||||
|
log.warning("pocket supervisor KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- log.debug("pocket supervisor RTT = " .. trip_time .. "ms")
|
||||||
|
|
||||||
|
_send_sv_keep_alive_ack(timestamp)
|
||||||
|
else
|
||||||
|
log.debug("supervisor SCADA keep alive packet length mismatch")
|
||||||
|
end
|
||||||
|
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
|
||||||
|
-- handle session close
|
||||||
|
sv_watchdog.cancel()
|
||||||
|
self.sv.linked = false
|
||||||
|
log.info("supervisor server connection closed by remote host")
|
||||||
|
else
|
||||||
|
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug("discarding supervisor non-link SCADA_MGMT packet before linked")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug("illegal packet type " .. protocol .. " from supervisor", true)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug("received packet from unconfigured channel " .. r_port, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check if we are still linked with the supervisor
|
||||||
|
---@nodiscard
|
||||||
|
function public.is_sv_linked() return self.sv.linked end
|
||||||
|
|
||||||
|
-- check if we are still linked with the coordinator
|
||||||
|
---@nodiscard
|
||||||
|
function public.is_api_linked() return self.api.linked end
|
||||||
|
|
||||||
|
return public
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return pocket
|
||||||
80
pocket/renderer.lua
Normal file
80
pocket/renderer.lua
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
--
|
||||||
|
-- Graphics Rendering Control
|
||||||
|
--
|
||||||
|
|
||||||
|
local main_view = require("pocket.ui.main")
|
||||||
|
local style = require("pocket.ui.style")
|
||||||
|
|
||||||
|
local flasher = require("graphics.flasher")
|
||||||
|
|
||||||
|
local DisplayBox = require("graphics.elements.displaybox")
|
||||||
|
|
||||||
|
local renderer = {}
|
||||||
|
|
||||||
|
local ui = {
|
||||||
|
display = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
-- start the pocket GUI
|
||||||
|
function renderer.start_ui()
|
||||||
|
if ui.display == nil then
|
||||||
|
-- reset screen
|
||||||
|
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}
|
||||||
|
main_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()
|
||||||
|
|
||||||
|
-- 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
|
||||||
@@ -1,16 +1,180 @@
|
|||||||
--
|
--
|
||||||
-- SCADA Coordinator Access on a Pocket Computer
|
-- SCADA System Access on a Pocket Computer
|
||||||
--
|
--
|
||||||
|
|
||||||
require("/initenv").init_env()
|
require("/initenv").init_env()
|
||||||
|
|
||||||
local util = require("scada-common.util")
|
local crash = require("scada-common.crash")
|
||||||
|
local log = require("scada-common.log")
|
||||||
|
local ppm = require("scada-common.ppm")
|
||||||
|
local tcallbackdsp = require("scada-common.tcallbackdsp")
|
||||||
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
local POCKET_VERSION = "alpha-v0.0.0"
|
local core = require("graphics.core")
|
||||||
|
|
||||||
|
local config = require("pocket.config")
|
||||||
|
local coreio = require("pocket.coreio")
|
||||||
|
local pocket = require("pocket.pocket")
|
||||||
|
local renderer = require("pocket.renderer")
|
||||||
|
|
||||||
|
local POCKET_VERSION = "alpha-v0.3.2"
|
||||||
|
|
||||||
local print = util.print
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local print_ts = util.print_ts
|
|
||||||
local println_ts = util.println_ts
|
local println_ts = util.println_ts
|
||||||
|
|
||||||
println("Sorry, this isn't written yet :(")
|
----------------------------------------
|
||||||
|
-- config validation
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
local cfv = util.new_validator()
|
||||||
|
|
||||||
|
cfv.assert_port(config.SCADA_SV_PORT)
|
||||||
|
cfv.assert_port(config.SCADA_API_PORT)
|
||||||
|
cfv.assert_port(config.LISTEN_PORT)
|
||||||
|
cfv.assert_type_int(config.TRUSTED_RANGE)
|
||||||
|
cfv.assert_type_num(config.COMMS_TIMEOUT)
|
||||||
|
cfv.assert_min(config.COMMS_TIMEOUT, 2)
|
||||||
|
cfv.assert_type_str(config.LOG_PATH)
|
||||||
|
cfv.assert_type_int(config.LOG_MODE)
|
||||||
|
|
||||||
|
assert(cfv.valid(), "bad config file: missing/invalid fields")
|
||||||
|
|
||||||
|
----------------------------------------
|
||||||
|
-- log init
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
|
||||||
|
|
||||||
|
log.info("========================================")
|
||||||
|
log.info("BOOTING pocket.startup " .. POCKET_VERSION)
|
||||||
|
log.info("========================================")
|
||||||
|
|
||||||
|
crash.set_env("pocket", POCKET_VERSION)
|
||||||
|
|
||||||
|
----------------------------------------
|
||||||
|
-- main application
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
local function main()
|
||||||
|
----------------------------------------
|
||||||
|
-- system startup
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
-- mount connected devices
|
||||||
|
ppm.mount_all()
|
||||||
|
|
||||||
|
----------------------------------------
|
||||||
|
-- setup communications & clocks
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
coreio.report_link_state(coreio.LINK_STATE.UNLINKED)
|
||||||
|
|
||||||
|
-- get the communications modem
|
||||||
|
local modem = ppm.get_wireless_modem()
|
||||||
|
if modem == nil then
|
||||||
|
println("startup> wireless modem not found: please craft the pocket computer with a wireless modem")
|
||||||
|
log.fatal("startup> no wireless modem on startup")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- create connection watchdogs
|
||||||
|
local conn_wd = {
|
||||||
|
sv = util.new_watchdog(config.COMMS_TIMEOUT),
|
||||||
|
api = util.new_watchdog(config.COMMS_TIMEOUT)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn_wd.sv.cancel()
|
||||||
|
conn_wd.api.cancel()
|
||||||
|
|
||||||
|
log.debug("startup> conn watchdogs created")
|
||||||
|
|
||||||
|
-- start comms, open all channels
|
||||||
|
local pocket_comms = pocket.comms(POCKET_VERSION, modem, config.LISTEN_PORT, config.SCADA_SV_PORT,
|
||||||
|
config.SCADA_API_PORT, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api)
|
||||||
|
log.debug("startup> comms init")
|
||||||
|
|
||||||
|
-- base loop clock (2Hz, 10 ticks)
|
||||||
|
local MAIN_CLOCK = 0.5
|
||||||
|
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||||
|
|
||||||
|
----------------------------------------
|
||||||
|
-- start the UI
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
local ui_ok, message = pcall(renderer.start_ui)
|
||||||
|
if not ui_ok then
|
||||||
|
renderer.close_ui()
|
||||||
|
println(util.c("UI error: ", message))
|
||||||
|
log.error(util.c("startup> GUI crashed with error ", message))
|
||||||
|
else
|
||||||
|
-- start clock
|
||||||
|
loop_clock.start()
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------
|
||||||
|
-- main event loop
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
if ui_ok then
|
||||||
|
-- start connection watchdogs
|
||||||
|
conn_wd.sv.feed()
|
||||||
|
conn_wd.api.feed()
|
||||||
|
log.debug("startup> conn watchdog started")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- main event loop
|
||||||
|
while ui_ok do
|
||||||
|
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||||
|
|
||||||
|
-- handle event
|
||||||
|
if event == "timer" then
|
||||||
|
if loop_clock.is_clock(param1) then
|
||||||
|
-- main loop tick
|
||||||
|
|
||||||
|
-- relink if necessary
|
||||||
|
pocket_comms.link_update()
|
||||||
|
|
||||||
|
loop_clock.start()
|
||||||
|
elseif conn_wd.sv.is_timer(param1) then
|
||||||
|
-- supervisor watchdog timeout
|
||||||
|
log.info("supervisor server timeout")
|
||||||
|
pocket_comms.close_sv()
|
||||||
|
elseif conn_wd.api.is_timer(param1) then
|
||||||
|
-- coordinator watchdog timeout
|
||||||
|
log.info("coordinator api server timeout")
|
||||||
|
pocket_comms.close_api()
|
||||||
|
else
|
||||||
|
-- a non-clock/main watchdog timer event
|
||||||
|
-- notify timer callback dispatcher
|
||||||
|
tcallbackdsp.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
|
||||||
|
|
||||||
|
renderer.close_ui()
|
||||||
|
|
||||||
|
println_ts("exited")
|
||||||
|
log.info("exited")
|
||||||
|
end
|
||||||
|
|
||||||
|
if not xpcall(main, crash.handler) then
|
||||||
|
pcall(renderer.close_ui)
|
||||||
|
crash.exit()
|
||||||
|
else
|
||||||
|
log.close()
|
||||||
|
end
|
||||||
|
|||||||
22
pocket/ui/components/boiler_page.lua
Normal file
22
pocket/ui/components/boiler_page.lua
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- local style = require("pocket.ui.style")
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
|
|
||||||
|
local Div = require("graphics.elements.div")
|
||||||
|
local TextBox = require("graphics.elements.textbox")
|
||||||
|
|
||||||
|
-- local cpair = core.cpair
|
||||||
|
|
||||||
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
|
-- new boiler page view
|
||||||
|
---@param root graphics_element parent
|
||||||
|
local function new_view(root)
|
||||||
|
local main = Div{parent=root,x=1,y=1}
|
||||||
|
|
||||||
|
TextBox{parent=main,text="BOILERS",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER}
|
||||||
|
|
||||||
|
return main
|
||||||
|
end
|
||||||
|
|
||||||
|
return new_view
|
||||||
41
pocket/ui/components/conn_waiting.lua
Normal file
41
pocket/ui/components/conn_waiting.lua
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
--
|
||||||
|
-- Connection Waiting Spinner
|
||||||
|
--
|
||||||
|
|
||||||
|
local style = require("pocket.ui.style")
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
|
|
||||||
|
local Div = require("graphics.elements.div")
|
||||||
|
local TextBox = require("graphics.elements.textbox")
|
||||||
|
|
||||||
|
local WaitingAnim = require("graphics.elements.animations.waiting")
|
||||||
|
|
||||||
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
|
local cpair = core.cpair
|
||||||
|
|
||||||
|
-- create a waiting view
|
||||||
|
---@param parent graphics_element parent
|
||||||
|
---@param y integer y offset
|
||||||
|
local function init(parent, y, is_api)
|
||||||
|
-- root div
|
||||||
|
local root = Div{parent=parent,x=1,y=1}
|
||||||
|
|
||||||
|
-- bounding box div
|
||||||
|
local box = Div{parent=root,x=1,y=y,height=5}
|
||||||
|
|
||||||
|
local waiting_x = math.floor(parent.width() / 2) - 1
|
||||||
|
|
||||||
|
if is_api then
|
||||||
|
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)}
|
||||||
|
TextBox{parent=box,text="Connecting to API",alignment=TEXT_ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)}
|
||||||
|
else
|
||||||
|
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.green,style.root.bkg)}
|
||||||
|
TextBox{parent=box,text="Connecting to Supervisor",alignment=TEXT_ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)}
|
||||||
|
end
|
||||||
|
|
||||||
|
return root
|
||||||
|
end
|
||||||
|
|
||||||
|
return init
|
||||||
22
pocket/ui/components/home_page.lua
Normal file
22
pocket/ui/components/home_page.lua
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- local style = require("pocket.ui.style")
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
|
|
||||||
|
local Div = require("graphics.elements.div")
|
||||||
|
local TextBox = require("graphics.elements.textbox")
|
||||||
|
|
||||||
|
-- local cpair = core.cpair
|
||||||
|
|
||||||
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
|
-- new home page view
|
||||||
|
---@param root graphics_element parent
|
||||||
|
local function new_view(root)
|
||||||
|
local main = Div{parent=root,x=1,y=1}
|
||||||
|
|
||||||
|
TextBox{parent=main,text="HOME",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER}
|
||||||
|
|
||||||
|
return main
|
||||||
|
end
|
||||||
|
|
||||||
|
return new_view
|
||||||
22
pocket/ui/components/reactor_page.lua
Normal file
22
pocket/ui/components/reactor_page.lua
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- local style = require("pocket.ui.style")
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
|
|
||||||
|
local Div = require("graphics.elements.div")
|
||||||
|
local TextBox = require("graphics.elements.textbox")
|
||||||
|
|
||||||
|
-- local cpair = core.cpair
|
||||||
|
|
||||||
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
|
-- new reactor page view
|
||||||
|
---@param root graphics_element parent
|
||||||
|
local function new_view(root)
|
||||||
|
local main = Div{parent=root,x=1,y=1}
|
||||||
|
|
||||||
|
TextBox{parent=main,text="REACTOR",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER}
|
||||||
|
|
||||||
|
return main
|
||||||
|
end
|
||||||
|
|
||||||
|
return new_view
|
||||||
22
pocket/ui/components/turbine_page.lua
Normal file
22
pocket/ui/components/turbine_page.lua
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- local style = require("pocket.ui.style")
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
|
|
||||||
|
local Div = require("graphics.elements.div")
|
||||||
|
local TextBox = require("graphics.elements.textbox")
|
||||||
|
|
||||||
|
-- local cpair = core.cpair
|
||||||
|
|
||||||
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
|
-- new turbine page view
|
||||||
|
---@param root graphics_element parent
|
||||||
|
local function new_view(root)
|
||||||
|
local main = Div{parent=root,x=1,y=1}
|
||||||
|
|
||||||
|
TextBox{parent=main,text="TURBINES",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER}
|
||||||
|
|
||||||
|
return main
|
||||||
|
end
|
||||||
|
|
||||||
|
return new_view
|
||||||
22
pocket/ui/components/unit_page.lua
Normal file
22
pocket/ui/components/unit_page.lua
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- local style = require("pocket.ui.style")
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
|
|
||||||
|
local Div = require("graphics.elements.div")
|
||||||
|
local TextBox = require("graphics.elements.textbox")
|
||||||
|
|
||||||
|
-- local cpair = core.cpair
|
||||||
|
|
||||||
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
|
-- new unit page view
|
||||||
|
---@param root graphics_element parent
|
||||||
|
local function new_view(root)
|
||||||
|
local main = Div{parent=root,x=1,y=1}
|
||||||
|
|
||||||
|
TextBox{parent=main,text="UNITS",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER}
|
||||||
|
|
||||||
|
return main
|
||||||
|
end
|
||||||
|
|
||||||
|
return new_view
|
||||||
99
pocket/ui/main.lua
Normal file
99
pocket/ui/main.lua
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
--
|
||||||
|
-- Pocket GUI Root
|
||||||
|
--
|
||||||
|
|
||||||
|
local coreio = require("pocket.coreio")
|
||||||
|
|
||||||
|
local style = require("pocket.ui.style")
|
||||||
|
|
||||||
|
local conn_waiting = require("pocket.ui.components.conn_waiting")
|
||||||
|
|
||||||
|
local home_page = require("pocket.ui.components.home_page")
|
||||||
|
local unit_page = require("pocket.ui.components.unit_page")
|
||||||
|
local reactor_page = require("pocket.ui.components.reactor_page")
|
||||||
|
local boiler_page = require("pocket.ui.components.boiler_page")
|
||||||
|
local turbine_page = require("pocket.ui.components.turbine_page")
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
|
|
||||||
|
local Div = require("graphics.elements.div")
|
||||||
|
local MultiPane = require("graphics.elements.multipane")
|
||||||
|
local TextBox = require("graphics.elements.textbox")
|
||||||
|
|
||||||
|
local Sidebar = require("graphics.elements.controls.sidebar")
|
||||||
|
|
||||||
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
|
local cpair = core.cpair
|
||||||
|
|
||||||
|
-- create new main view
|
||||||
|
---@param main graphics_element main displaybox
|
||||||
|
local function init(main)
|
||||||
|
-- window header message
|
||||||
|
TextBox{parent=main,y=1,text="",alignment=TEXT_ALIGN.LEFT,height=1,fg_bg=style.header}
|
||||||
|
|
||||||
|
--
|
||||||
|
-- root panel panes (connection screens + main screen)
|
||||||
|
--
|
||||||
|
|
||||||
|
local root_pane_div = Div{parent=main,x=1,y=2}
|
||||||
|
|
||||||
|
local conn_sv_wait = conn_waiting(root_pane_div, 6, false)
|
||||||
|
local conn_api_wait = conn_waiting(root_pane_div, 6, true)
|
||||||
|
local main_pane = Div{parent=main,x=1,y=2}
|
||||||
|
local root_panes = { conn_sv_wait, conn_api_wait, main_pane }
|
||||||
|
|
||||||
|
local root_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes=root_panes}
|
||||||
|
|
||||||
|
root_pane.register(coreio.core_ps(), "link_state", function (state)
|
||||||
|
if state == coreio.LINK_STATE.UNLINKED or state == coreio.LINK_STATE.API_LINK_ONLY then
|
||||||
|
root_pane.set_value(1)
|
||||||
|
elseif state == coreio.LINK_STATE.SV_LINK_ONLY then
|
||||||
|
root_pane.set_value(2)
|
||||||
|
else
|
||||||
|
root_pane.set_value(3)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
--
|
||||||
|
-- main page panel panes & sidebar
|
||||||
|
--
|
||||||
|
|
||||||
|
local page_div = Div{parent=main_pane,x=4,y=1}
|
||||||
|
|
||||||
|
local sidebar_tabs = {
|
||||||
|
{
|
||||||
|
char = "#",
|
||||||
|
color = cpair(colors.black,colors.green)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
char = "U",
|
||||||
|
color = cpair(colors.black,colors.yellow)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
char = "R",
|
||||||
|
color = cpair(colors.black,colors.cyan)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
char = "B",
|
||||||
|
color = cpair(colors.black,colors.lightGray)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
char = "T",
|
||||||
|
color = cpair(colors.black,colors.white)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
local pane_1 = home_page(page_div)
|
||||||
|
local pane_2 = unit_page(page_div)
|
||||||
|
local pane_3 = reactor_page(page_div)
|
||||||
|
local pane_4 = boiler_page(page_div)
|
||||||
|
local pane_5 = turbine_page(page_div)
|
||||||
|
local panes = { pane_1, pane_2, pane_3, pane_4, pane_5 }
|
||||||
|
|
||||||
|
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||||
|
|
||||||
|
Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=page_pane.set_value}
|
||||||
|
end
|
||||||
|
|
||||||
|
return init
|
||||||
158
pocket/ui/style.lua
Normal file
158
pocket/ui/style.lua
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
--
|
||||||
|
-- Graphics Style Options
|
||||||
|
--
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
|
|
||||||
|
local style = {}
|
||||||
|
|
||||||
|
local cpair = core.cpair
|
||||||
|
|
||||||
|
-- GLOBAL --
|
||||||
|
|
||||||
|
style.root = cpair(colors.white, colors.black)
|
||||||
|
style.header = cpair(colors.white, colors.gray)
|
||||||
|
style.label = cpair(colors.gray, colors.lightGray)
|
||||||
|
|
||||||
|
style.colors = {
|
||||||
|
{ c = colors.red, hex = 0xdf4949 },
|
||||||
|
{ c = colors.orange, hex = 0xffb659 },
|
||||||
|
{ c = colors.yellow, hex = 0xfffc79 },
|
||||||
|
{ c = colors.lime, hex = 0x80ff80 },
|
||||||
|
{ c = colors.green, hex = 0x4aee8a },
|
||||||
|
{ c = colors.cyan, hex = 0x34bac8 },
|
||||||
|
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||||
|
{ c = colors.blue, hex = 0x0096ff },
|
||||||
|
{ c = colors.purple, hex = 0xb156ee },
|
||||||
|
{ c = colors.pink, hex = 0xf26ba2 },
|
||||||
|
{ c = colors.magenta, hex = 0xf9488a },
|
||||||
|
-- { c = colors.white, hex = 0xf0f0f0 },
|
||||||
|
{ c = colors.lightGray, hex = 0xcacaca },
|
||||||
|
{ c = colors.gray, hex = 0x575757 },
|
||||||
|
-- { c = colors.black, hex = 0x191919 },
|
||||||
|
-- { c = colors.brown, hex = 0x7f664c }
|
||||||
|
}
|
||||||
|
|
||||||
|
-- MAIN LAYOUT --
|
||||||
|
|
||||||
|
style.reactor = {
|
||||||
|
-- reactor states
|
||||||
|
states = {
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.yellow),
|
||||||
|
text = "PLC OFF-LINE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.orange),
|
||||||
|
text = "NOT FORMED"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.orange),
|
||||||
|
text = "PLC FAULT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.white, colors.gray),
|
||||||
|
text = "DISABLED"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.green),
|
||||||
|
text = "ACTIVE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.red),
|
||||||
|
text = "SCRAMMED"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.red),
|
||||||
|
text = "FORCE DISABLED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
style.boiler = {
|
||||||
|
-- boiler states
|
||||||
|
states = {
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.yellow),
|
||||||
|
text = "OFF-LINE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.orange),
|
||||||
|
text = "NOT FORMED"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.orange),
|
||||||
|
text = "RTU FAULT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.white, colors.gray),
|
||||||
|
text = "IDLE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.green),
|
||||||
|
text = "ACTIVE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
style.turbine = {
|
||||||
|
-- turbine states
|
||||||
|
states = {
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.yellow),
|
||||||
|
text = "OFF-LINE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.orange),
|
||||||
|
text = "NOT FORMED"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.orange),
|
||||||
|
text = "RTU FAULT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.white, colors.gray),
|
||||||
|
text = "IDLE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.green),
|
||||||
|
text = "ACTIVE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.red),
|
||||||
|
text = "TRIP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
style.imatrix = {
|
||||||
|
-- induction matrix states
|
||||||
|
states = {
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.yellow),
|
||||||
|
text = "OFF-LINE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.orange),
|
||||||
|
text = "NOT FORMED"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.orange),
|
||||||
|
text = "RTU FAULT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.green),
|
||||||
|
text = "ONLINE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.yellow),
|
||||||
|
text = "LOW CHARGE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color = cpair(colors.black, colors.yellow),
|
||||||
|
text = "HIGH CHARGE"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return style
|
||||||
@@ -24,5 +24,7 @@ config.LOG_PATH = "/log.txt"
|
|||||||
-- 0 = APPEND (adds to existing file on start)
|
-- 0 = APPEND (adds to existing file on start)
|
||||||
-- 1 = NEW (replaces existing file on start)
|
-- 1 = NEW (replaces existing file on start)
|
||||||
config.LOG_MODE = 0
|
config.LOG_MODE = 0
|
||||||
|
-- true to log verbose debug messages
|
||||||
|
config.LOG_DEBUG = false
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ local util = require("scada-common.util")
|
|||||||
|
|
||||||
local databus = {}
|
local databus = {}
|
||||||
|
|
||||||
|
-- databus PSIL
|
||||||
|
databus.ps = psil.create()
|
||||||
|
|
||||||
local dbus_iface = {
|
local dbus_iface = {
|
||||||
ps = psil.create(),
|
|
||||||
rps_scram = function () log.debug("DBUS: unset rps_scram() called") end,
|
rps_scram = function () log.debug("DBUS: unset rps_scram() called") end,
|
||||||
rps_reset = function () log.debug("DBUS: unset rps_reset() called") end
|
rps_reset = function () log.debug("DBUS: unset rps_reset() called") end
|
||||||
}
|
}
|
||||||
|
|
||||||
-- call to toggle heartbeat signal
|
-- call to toggle heartbeat signal
|
||||||
function databus.heartbeat() dbus_iface.ps.toggle("heartbeat") end
|
function databus.heartbeat() databus.ps.toggle("heartbeat") end
|
||||||
|
|
||||||
-- link RPS command functions
|
-- link RPS command functions
|
||||||
---@param scram function reactor SCRAM function
|
---@param scram function reactor SCRAM function
|
||||||
@@ -35,67 +37,69 @@ function databus.rps_reset() dbus_iface.rps_reset() end
|
|||||||
---@param plc_v string PLC version
|
---@param plc_v string PLC version
|
||||||
---@param comms_v string comms version
|
---@param comms_v string comms version
|
||||||
function databus.tx_versions(plc_v, comms_v)
|
function databus.tx_versions(plc_v, comms_v)
|
||||||
dbus_iface.ps.publish("version", plc_v)
|
databus.ps.publish("version", plc_v)
|
||||||
dbus_iface.ps.publish("comms_version", comms_v)
|
databus.ps.publish("comms_version", comms_v)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- transmit unit ID across the bus
|
-- transmit unit ID across the bus
|
||||||
---@param id integer unit ID
|
---@param id integer unit ID
|
||||||
function databus.tx_id(id)
|
function databus.tx_id(id)
|
||||||
dbus_iface.ps.publish("unit_id", id)
|
databus.ps.publish("unit_id", id)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- transmit hardware status across the bus
|
-- transmit hardware status across the bus
|
||||||
---@param plc_state plc_state
|
---@param plc_state plc_state
|
||||||
function databus.tx_hw_status(plc_state)
|
function databus.tx_hw_status(plc_state)
|
||||||
dbus_iface.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2)))
|
databus.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2)))
|
||||||
dbus_iface.ps.publish("has_modem", not plc_state.no_modem)
|
databus.ps.publish("has_modem", not plc_state.no_modem)
|
||||||
dbus_iface.ps.publish("degraded", plc_state.degraded)
|
databus.ps.publish("degraded", plc_state.degraded)
|
||||||
dbus_iface.ps.publish("init_ok", plc_state.init_ok)
|
databus.ps.publish("init_ok", plc_state.init_ok)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- transmit thread (routine) statuses
|
-- transmit thread (routine) statuses
|
||||||
---@param thread string thread name
|
---@param thread string thread name
|
||||||
---@param ok boolean thread state
|
---@param ok boolean thread state
|
||||||
function databus.tx_rt_status(thread, ok)
|
function databus.tx_rt_status(thread, ok)
|
||||||
dbus_iface.ps.publish(util.c("routine__", thread), ok)
|
databus.ps.publish(util.c("routine__", thread), ok)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- transmit supervisor link state across the bus
|
-- transmit supervisor link state across the bus
|
||||||
---@param state integer
|
---@param state integer
|
||||||
function databus.tx_link_state(state)
|
function databus.tx_link_state(state)
|
||||||
dbus_iface.ps.publish("link_state", state)
|
databus.ps.publish("link_state", state)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- transmit reactor enable state across the bus
|
-- transmit reactor enable state across the bus
|
||||||
---@param active boolean reactor active
|
---@param active boolean reactor active
|
||||||
function databus.tx_reactor_state(active)
|
function databus.tx_reactor_state(active)
|
||||||
dbus_iface.ps.publish("reactor_active", active)
|
databus.ps.publish("reactor_active", active)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- transmit RPS data across the bus
|
-- transmit RPS data across the bus
|
||||||
---@param tripped boolean RPS tripped
|
---@param tripped boolean RPS tripped
|
||||||
---@param status table RPS status
|
---@param status table RPS status
|
||||||
function databus.tx_rps(tripped, status)
|
---@param emer_cool_active boolean RPS activated the emergency coolant
|
||||||
dbus_iface.ps.publish("rps_scram", tripped)
|
function databus.tx_rps(tripped, status, emer_cool_active)
|
||||||
dbus_iface.ps.publish("rps_damage", status[1])
|
databus.ps.publish("rps_scram", tripped)
|
||||||
dbus_iface.ps.publish("rps_high_temp", status[2])
|
databus.ps.publish("rps_damage", status[1])
|
||||||
dbus_iface.ps.publish("rps_low_ccool", status[3])
|
databus.ps.publish("rps_high_temp", status[2])
|
||||||
dbus_iface.ps.publish("rps_high_waste", status[4])
|
databus.ps.publish("rps_low_ccool", status[3])
|
||||||
dbus_iface.ps.publish("rps_high_hcool", status[5])
|
databus.ps.publish("rps_high_waste", status[4])
|
||||||
dbus_iface.ps.publish("rps_no_fuel", status[6])
|
databus.ps.publish("rps_high_hcool", status[5])
|
||||||
dbus_iface.ps.publish("rps_fault", status[7])
|
databus.ps.publish("rps_no_fuel", status[6])
|
||||||
dbus_iface.ps.publish("rps_timeout", status[8])
|
databus.ps.publish("rps_fault", status[7])
|
||||||
dbus_iface.ps.publish("rps_manual", status[9])
|
databus.ps.publish("rps_timeout", status[8])
|
||||||
dbus_iface.ps.publish("rps_automatic", status[10])
|
databus.ps.publish("rps_manual", status[9])
|
||||||
dbus_iface.ps.publish("rps_sysfail", status[11])
|
databus.ps.publish("rps_automatic", status[10])
|
||||||
|
databus.ps.publish("rps_sysfail", status[11])
|
||||||
|
databus.ps.publish("emer_cool", emer_cool_active)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- link a function to receive data from the bus
|
-- link a function to receive data from the bus
|
||||||
---@param field string field name
|
---@param field string field name
|
||||||
---@param func function function to link
|
---@param func function function to link
|
||||||
function databus.rx_field(field, func)
|
function databus.rx_field(field, func)
|
||||||
dbus_iface.ps.subscribe(field, func)
|
databus.ps.subscribe(field, func)
|
||||||
end
|
end
|
||||||
|
|
||||||
return databus
|
return databus
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
|
local config = require("reactor-plc.config")
|
||||||
local databus = require("reactor-plc.databus")
|
local databus = require("reactor-plc.databus")
|
||||||
|
|
||||||
local style = require("reactor-plc.panel.style")
|
local style = require("reactor-plc.panel.style")
|
||||||
@@ -11,7 +12,6 @@ local style = require("reactor-plc.panel.style")
|
|||||||
local core = require("graphics.core")
|
local core = require("graphics.core")
|
||||||
local flasher = require("graphics.flasher")
|
local flasher = require("graphics.flasher")
|
||||||
|
|
||||||
local DisplayBox = require("graphics.elements.displaybox")
|
|
||||||
local Div = require("graphics.elements.div")
|
local Div = require("graphics.elements.div")
|
||||||
local Rectangle = require("graphics.elements.rectangle")
|
local Rectangle = require("graphics.elements.rectangle")
|
||||||
local TextBox = require("graphics.elements.textbox")
|
local TextBox = require("graphics.elements.textbox")
|
||||||
@@ -22,18 +22,20 @@ local LED = require("graphics.elements.indicators.led")
|
|||||||
local LEDPair = require("graphics.elements.indicators.ledpair")
|
local LEDPair = require("graphics.elements.indicators.ledpair")
|
||||||
local RGBLED = require("graphics.elements.indicators.ledrgb")
|
local RGBLED = require("graphics.elements.indicators.ledrgb")
|
||||||
|
|
||||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
local cpair = core.graphics.cpair
|
local cpair = core.cpair
|
||||||
local border = core.graphics.border
|
local border = core.border
|
||||||
|
|
||||||
-- create new main view
|
-- create new main view
|
||||||
---@param monitor table main viewscreen
|
---@param panel graphics_element main displaybox
|
||||||
local function init(monitor)
|
local function init(panel)
|
||||||
local panel = DisplayBox{window=monitor,fg_bg=style.root}
|
|
||||||
|
|
||||||
local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||||
databus.rx_field("unit_id", function (id) header.set_value(util.c("REACTOR PLC - UNIT ", id)) end)
|
header.register(databus.ps, "unit_id", function (id) header.set_value(util.c("REACTOR PLC - UNIT ", id)) end)
|
||||||
|
|
||||||
|
--
|
||||||
|
-- system indicators
|
||||||
|
--
|
||||||
|
|
||||||
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}
|
||||||
|
|
||||||
@@ -41,8 +43,8 @@ local function init(monitor)
|
|||||||
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)}
|
||||||
system.line_break()
|
system.line_break()
|
||||||
|
|
||||||
databus.rx_field("init_ok", init_ok.update)
|
init_ok.register(databus.ps, "init_ok", init_ok.update)
|
||||||
databus.rx_field("heartbeat", heartbeat.update)
|
heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
|
||||||
|
|
||||||
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)}
|
||||||
@@ -50,9 +52,9 @@ local function init(monitor)
|
|||||||
network.update(5)
|
network.update(5)
|
||||||
system.line_break()
|
system.line_break()
|
||||||
|
|
||||||
databus.rx_field("reactor_dev_state", reactor.update)
|
reactor.register(databus.ps, "reactor_dev_state", reactor.update)
|
||||||
databus.rx_field("has_modem", modem.update)
|
modem.register(databus.ps, "has_modem", modem.update)
|
||||||
databus.rx_field("link_state", network.update)
|
network.register(databus.ps, "link_state", network.update)
|
||||||
|
|
||||||
local rt_main = LED{parent=system,label="RT MAIN",colors=cpair(colors.green,colors.green_off)}
|
local rt_main = LED{parent=system,label="RT MAIN",colors=cpair(colors.green,colors.green_off)}
|
||||||
local rt_rps = LED{parent=system,label="RT RPS",colors=cpair(colors.green,colors.green_off)}
|
local rt_rps = LED{parent=system,label="RT RPS",colors=cpair(colors.green,colors.green_off)}
|
||||||
@@ -61,34 +63,52 @@ local function init(monitor)
|
|||||||
local rt_sctl = LED{parent=system,label="RT SPCTL",colors=cpair(colors.green,colors.green_off)}
|
local rt_sctl = LED{parent=system,label="RT SPCTL",colors=cpair(colors.green,colors.green_off)}
|
||||||
system.line_break()
|
system.line_break()
|
||||||
|
|
||||||
databus.rx_field("routine__main", rt_main.update)
|
rt_main.register(databus.ps, "routine__main", rt_main.update)
|
||||||
databus.rx_field("routine__rps", rt_rps.update)
|
rt_rps.register(databus.ps, "routine__rps", rt_rps.update)
|
||||||
databus.rx_field("routine__comms_tx", rt_cmtx.update)
|
rt_cmtx.register(databus.ps, "routine__comms_tx", rt_cmtx.update)
|
||||||
databus.rx_field("routine__comms_rx", rt_cmrx.update)
|
rt_cmrx.register(databus.ps, "routine__comms_rx", rt_cmrx.update)
|
||||||
databus.rx_field("routine__spctl", rt_sctl.update)
|
rt_sctl.register(databus.ps, "routine__spctl", rt_sctl.update)
|
||||||
|
|
||||||
|
--
|
||||||
|
-- status & controls
|
||||||
|
--
|
||||||
|
|
||||||
local status = Div{parent=panel,width=19,height=18,x=17,y=3}
|
local status = Div{parent=panel,width=19,height=18,x=17,y=3}
|
||||||
|
|
||||||
local active = LED{parent=status,x=2,width=12,label="RCT ACTIVE",colors=cpair(colors.green,colors.green_off)}
|
local active = LED{parent=status,x=2,width=12,label="RCT ACTIVE",colors=cpair(colors.green,colors.green_off)}
|
||||||
|
|
||||||
local status_trip_rct = Rectangle{parent=status,width=20,height=3,x=1,y=2,border=border(1,colors.lightGray,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)}
|
-- only show emergency coolant LED if emergency coolant is configured for this device
|
||||||
|
if type(config.EMERGENCY_COOL) == "table" then
|
||||||
|
local emer_cool = LED{parent=status,x=2,width=14,label="EMER COOLANT",colors=cpair(colors.yellow,colors.yellow_off)}
|
||||||
|
emer_cool.register(databus.ps, "emer_cool", emer_cool.update)
|
||||||
|
end
|
||||||
|
|
||||||
|
local status_trip_rct = Rectangle{parent=status,width=20,height=3,x=1,border=border(1,colors.lightGray,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)}
|
||||||
local status_trip = Div{parent=status_trip_rct,width=18,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
|
local status_trip = Div{parent=status_trip_rct,width=18,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
|
||||||
local scram = LED{parent=status_trip,width=10,label="RPS TRIP",colors=cpair(colors.red,colors.red_off),flash=true,period=flasher.PERIOD.BLINK_250_MS}
|
local scram = LED{parent=status_trip,width=10,label="RPS TRIP",colors=cpair(colors.red,colors.red_off),flash=true,period=flasher.PERIOD.BLINK_250_MS}
|
||||||
|
|
||||||
local controls_rct = Rectangle{parent=status,width=17,height=3,x=1,y=5,border=border(1,colors.white,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)}
|
local controls_rct = Rectangle{parent=status,width=17,height=3,x=1,border=border(1,colors.white,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)}
|
||||||
local controls = Div{parent=controls_rct,width=15,height=1,fg_bg=cpair(colors.black,colors.white)}
|
local controls = Div{parent=controls_rct,width=15,height=1,fg_bg=cpair(colors.black,colors.white)}
|
||||||
PushButton{parent=controls,x=1,y=1,min_width=7,text="SCRAM",callback=databus.rps_scram,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.black,colors.red_off)}
|
PushButton{parent=controls,x=1,y=1,min_width=7,text="SCRAM",callback=databus.rps_scram,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.black,colors.red_off)}
|
||||||
PushButton{parent=controls,x=9,y=1,min_width=7,text="RESET",callback=databus.rps_reset,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.black,colors.yellow_off)}
|
PushButton{parent=controls,x=9,y=1,min_width=7,text="RESET",callback=databus.rps_reset,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.black,colors.yellow_off)}
|
||||||
|
|
||||||
databus.rx_field("reactor_active", active.update)
|
active.register(databus.ps, "reactor_active", active.update)
|
||||||
databus.rx_field("rps_scram", scram.update)
|
scram.register(databus.ps, "rps_scram", scram.update)
|
||||||
|
|
||||||
|
--
|
||||||
|
-- about footer
|
||||||
|
--
|
||||||
|
|
||||||
local about = Rectangle{parent=panel,width=32,height=3,x=2,y=16,border=border(1,colors.ivory),thin=true,fg_bg=cpair(colors.black,colors.white)}
|
local about = Rectangle{parent=panel,width=32,height=3,x=2,y=16,border=border(1,colors.ivory),thin=true,fg_bg=cpair(colors.black,colors.white)}
|
||||||
local fw_v = TextBox{parent=about,x=2,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
|
local fw_v = TextBox{parent=about,x=2,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
|
||||||
local comms_v = TextBox{parent=about,x=17,y=1,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
|
local comms_v = TextBox{parent=about,x=17,y=1,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
|
||||||
|
|
||||||
databus.rx_field("version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
|
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
|
||||||
databus.rx_field("comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
|
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
|
||||||
|
|
||||||
|
--
|
||||||
|
-- rps list
|
||||||
|
--
|
||||||
|
|
||||||
local rps = Rectangle{parent=panel,width=16,height=16,x=36,y=3,border=border(1,colors.lightGray),thin=true,fg_bg=cpair(colors.black,colors.lightGray)}
|
local rps = Rectangle{parent=panel,width=16,height=16,x=36,y=3,border=border(1,colors.lightGray),thin=true,fg_bg=cpair(colors.black,colors.lightGray)}
|
||||||
local rps_man = LED{parent=rps,label="MANUAL",colors=cpair(colors.red,colors.red_off)}
|
local rps_man = LED{parent=rps,label="MANUAL",colors=cpair(colors.red,colors.red_off)}
|
||||||
@@ -106,19 +126,17 @@ local function init(monitor)
|
|||||||
local rps_ccl = LED{parent=rps,label="LO CCOOLANT",colors=cpair(colors.red,colors.red_off)}
|
local rps_ccl = LED{parent=rps,label="LO CCOOLANT",colors=cpair(colors.red,colors.red_off)}
|
||||||
local rps_hcl = LED{parent=rps,label="HI HCOOLANT",colors=cpair(colors.red,colors.red_off)}
|
local rps_hcl = LED{parent=rps,label="HI HCOOLANT",colors=cpair(colors.red,colors.red_off)}
|
||||||
|
|
||||||
databus.rx_field("rps_manual", rps_man.update)
|
rps_man.register(databus.ps, "rps_manual", rps_man.update)
|
||||||
databus.rx_field("rps_automatic", rps_auto.update)
|
rps_auto.register(databus.ps, "rps_automatic", rps_auto.update)
|
||||||
databus.rx_field("rps_timeout", rps_tmo.update)
|
rps_tmo.register(databus.ps, "rps_timeout", rps_tmo.update)
|
||||||
databus.rx_field("rps_fault", rps_flt.update)
|
rps_flt.register(databus.ps, "rps_fault", rps_flt.update)
|
||||||
databus.rx_field("rps_sysfail", rps_fail.update)
|
rps_fail.register(databus.ps, "rps_sysfail", rps_fail.update)
|
||||||
databus.rx_field("rps_damage", rps_dmg.update)
|
rps_dmg.register(databus.ps, "rps_damage", rps_dmg.update)
|
||||||
databus.rx_field("rps_high_temp", rps_tmp.update)
|
rps_tmp.register(databus.ps, "rps_high_temp", rps_tmp.update)
|
||||||
databus.rx_field("rps_no_fuel", rps_nof.update)
|
rps_nof.register(databus.ps, "rps_no_fuel", rps_nof.update)
|
||||||
databus.rx_field("rps_high_waste", rps_wst.update)
|
rps_wst.register(databus.ps, "rps_high_waste", rps_wst.update)
|
||||||
databus.rx_field("rps_low_ccool", rps_ccl.update)
|
rps_ccl.register(databus.ps, "rps_low_ccool", rps_ccl.update)
|
||||||
databus.rx_field("rps_high_hcool", rps_hcl.update)
|
rps_hcl.register(databus.ps, "rps_high_hcool", rps_hcl.update)
|
||||||
|
|
||||||
return panel
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return init
|
return init
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ local core = require("graphics.core")
|
|||||||
|
|
||||||
local style = {}
|
local style = {}
|
||||||
|
|
||||||
local cpair = core.graphics.cpair
|
local cpair = core.cpair
|
||||||
|
|
||||||
-- GLOBAL --
|
-- GLOBAL --
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ style.header = cpair(colors.black, colors.lightGray)
|
|||||||
style.colors = {
|
style.colors = {
|
||||||
{ c = colors.red, hex = 0xdf4949 }, -- RED ON
|
{ c = colors.red, hex = 0xdf4949 }, -- RED ON
|
||||||
{ c = colors.orange, hex = 0xffb659 },
|
{ c = colors.orange, hex = 0xffb659 },
|
||||||
{ c = colors.yellow, hex = 0xf9fb53 },
|
{ c = colors.yellow, hex = 0xf9fb53 }, -- YELLOW ON
|
||||||
{ c = colors.lime, hex = 0x16665a }, -- GREEN OFF
|
{ c = colors.lime, hex = 0x16665a }, -- GREEN OFF
|
||||||
{ c = colors.green, hex = 0x6be551 }, -- GREEN ON
|
{ c = colors.green, hex = 0x6be551 }, -- GREEN ON
|
||||||
{ c = colors.cyan, hex = 0x34bac8 },
|
{ c = colors.cyan, hex = 0x34bac8 },
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
local comms = require("scada-common.comms")
|
local comms = require("scada-common.comms")
|
||||||
local const = require("scada-common.constants")
|
local const = require("scada-common.constants")
|
||||||
local databus = require("reactor-plc.databus")
|
|
||||||
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 rsio = require("scada-common.rsio")
|
local rsio = require("scada-common.rsio")
|
||||||
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("reactor-plc.databus")
|
||||||
|
|
||||||
local plc = {}
|
local plc = {}
|
||||||
|
|
||||||
local RPS_TRIP_CAUSE = types.RPS_TRIP_CAUSE
|
local RPS_TRIP_CAUSE = types.RPS_TRIP_CAUSE
|
||||||
@@ -68,11 +69,6 @@ function plc.rps_init(reactor, is_formed, emer_cool)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- clear reactor access fault flag
|
|
||||||
local function _clear_fault()
|
|
||||||
self.state[state_keys.fault] = false
|
|
||||||
end
|
|
||||||
|
|
||||||
-- set emergency coolant control (if configured)
|
-- set emergency coolant control (if configured)
|
||||||
---@param state boolean true to enable emergency coolant, false to disable
|
---@param state boolean true to enable emergency coolant, false to disable
|
||||||
local function _set_emer_cool(state)
|
local function _set_emer_cool(state)
|
||||||
@@ -386,7 +382,7 @@ function plc.rps_init(reactor, is_formed, emer_cool)
|
|||||||
_set_emer_cool(self.state[state_keys.low_coolant])
|
_set_emer_cool(self.state[state_keys.low_coolant])
|
||||||
|
|
||||||
-- report RPS status
|
-- report RPS status
|
||||||
databus.tx_rps(self.tripped, self.state)
|
databus.tx_rps(self.tripped, self.state, self.emer_cool_active)
|
||||||
|
|
||||||
return self.tripped, status, first_trip
|
return self.tripped, status, first_trip
|
||||||
end
|
end
|
||||||
@@ -645,8 +641,6 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
|||||||
if not reactor.__p_is_faulted() then
|
if not reactor.__p_is_faulted() then
|
||||||
_send(RPLC_TYPE.MEK_STRUCT, mek_data)
|
_send(RPLC_TYPE.MEK_STRUCT, mek_data)
|
||||||
self.resend_build = false
|
self.resend_build = false
|
||||||
else
|
|
||||||
log.error("failed to send structure: PPM fault")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -766,7 +760,7 @@ 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.error("illegal packet type " .. s_pkt.protocol(), true)
|
log.debug("illegal packet type " .. s_pkt.protocol(), true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -779,15 +773,16 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
|||||||
---@param setpoints setpoints setpoint control table
|
---@param setpoints setpoints setpoint control table
|
||||||
function public.handle_packet(packet, plc_state, setpoints)
|
function public.handle_packet(packet, plc_state, setpoints)
|
||||||
-- print a log message to the terminal as long as the UI isn't running
|
-- print a log message to the terminal as long as the UI isn't running
|
||||||
local function println(message) if not plc_state.fp_ok then util.println(message) end end
|
|
||||||
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()
|
||||||
|
|
||||||
-- handle packets now that we have prints setup
|
-- handle packets now that we have prints setup
|
||||||
if packet.scada_frame.local_port() == local_port then
|
if l_port == local_port 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 >= 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
|
||||||
else
|
else
|
||||||
@@ -931,7 +926,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
|||||||
log.debug("RPLC set automatic burn rate packet length mismatch or non-numeric burn rate")
|
log.debug("RPLC set automatic burn rate packet length mismatch or non-numeric burn rate")
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
log.warning("received unknown RPLC packet type " .. packet.type)
|
log.debug("received unknown RPLC packet type " .. packet.type)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
log.debug("discarding RPLC packet before linked")
|
log.debug("discarding RPLC packet before linked")
|
||||||
@@ -953,7 +948,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
|||||||
log.debug("re-sent initial status data")
|
log.debug("re-sent initial status data")
|
||||||
elseif est_ack == ESTABLISH_ACK.DENY then
|
elseif est_ack == ESTABLISH_ACK.DENY then
|
||||||
println_ts("received unsolicited link denial, unlinking")
|
println_ts("received unsolicited link denial, unlinking")
|
||||||
log.info("unsolicited establish request denied")
|
log.warning("unsolicited establish request denied")
|
||||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||||
println_ts("received unsolicited link collision, unlinking")
|
println_ts("received unsolicited link collision, unlinking")
|
||||||
log.warning("unsolicited establish request collision")
|
log.warning("unsolicited establish request collision")
|
||||||
@@ -962,7 +957,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
|||||||
log.warning("unsolicited establish request version mismatch")
|
log.warning("unsolicited establish request version mismatch")
|
||||||
else
|
else
|
||||||
println_ts("invalid unsolicited link response")
|
println_ts("invalid unsolicited link response")
|
||||||
log.error("unsolicited unknown establish request response")
|
log.debug("unsolicited unknown establish request response")
|
||||||
end
|
end
|
||||||
|
|
||||||
self.linked = est_ack == ESTABLISH_ACK.ALLOW
|
self.linked = est_ack == ESTABLISH_ACK.ALLOW
|
||||||
@@ -998,7 +993,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
|||||||
println_ts("server connection closed by remote host")
|
println_ts("server connection closed by remote host")
|
||||||
log.warning("server connection closed by remote host")
|
log.warning("server connection closed by remote host")
|
||||||
else
|
else
|
||||||
log.warning("received unsupported SCADA_MGMT packet type " .. packet.type)
|
log.debug("received unsupported SCADA_MGMT packet type " .. packet.type)
|
||||||
end
|
end
|
||||||
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||||
-- link request confirmation
|
-- link request confirmation
|
||||||
@@ -1048,6 +1043,8 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
|||||||
-- 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_port, true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -2,20 +2,22 @@
|
|||||||
-- Graphics Rendering Control
|
-- Graphics Rendering Control
|
||||||
--
|
--
|
||||||
|
|
||||||
local style = require("reactor-plc.panel.style")
|
|
||||||
local panel_view = require("reactor-plc.panel.front_panel")
|
local panel_view = require("reactor-plc.panel.front_panel")
|
||||||
|
local style = require("reactor-plc.panel.style")
|
||||||
|
|
||||||
local flasher = require("graphics.flasher")
|
local flasher = require("graphics.flasher")
|
||||||
|
|
||||||
|
local DisplayBox = require("graphics.elements.displaybox")
|
||||||
|
|
||||||
local renderer = {}
|
local renderer = {}
|
||||||
|
|
||||||
local ui = {
|
local ui = {
|
||||||
view = nil
|
display = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
-- start the UI
|
-- start the UI
|
||||||
function renderer.start_ui()
|
function renderer.start_ui()
|
||||||
if ui.view == nil then
|
if ui.display == nil then
|
||||||
-- reset terminal
|
-- reset terminal
|
||||||
term.setTextColor(colors.white)
|
term.setTextColor(colors.white)
|
||||||
term.setBackgroundColor(colors.black)
|
term.setBackgroundColor(colors.black)
|
||||||
@@ -27,49 +29,50 @@ function renderer.start_ui()
|
|||||||
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- init front panel view
|
||||||
|
ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||||
|
panel_view(ui.display)
|
||||||
|
|
||||||
-- start flasher callback task
|
-- start flasher callback task
|
||||||
flasher.run()
|
flasher.run()
|
||||||
|
|
||||||
-- init front panel view
|
|
||||||
ui.view = panel_view(term.current())
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- close out the UI
|
-- close out the UI
|
||||||
function renderer.close_ui()
|
function renderer.close_ui()
|
||||||
-- stop blinking indicators
|
if ui.display ~= nil then
|
||||||
flasher.clear()
|
-- stop blinking indicators
|
||||||
|
flasher.clear()
|
||||||
|
|
||||||
if ui.view ~= nil then
|
-- delete element tree
|
||||||
-- hide to stop animation callbacks
|
ui.display.delete()
|
||||||
ui.view.hide()
|
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
|
||||||
|
|
||||||
-- clear root UI elements
|
|
||||||
ui.view = 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?
|
-- is the UI ready?
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@return boolean ready
|
---@return boolean ready
|
||||||
function renderer.ui_ready() return ui.view ~= nil end
|
function renderer.ui_ready() return ui.display ~= nil end
|
||||||
|
|
||||||
-- handle a mouse event
|
-- handle a mouse event
|
||||||
---@param event mouse_interaction
|
---@param event mouse_interaction|nil
|
||||||
function renderer.handle_mouse(event)
|
function renderer.handle_mouse(event)
|
||||||
ui.view.handle_mouse(event)
|
if ui.display ~= nil and event ~= nil then
|
||||||
|
ui.display.handle_mouse(event)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return renderer
|
return renderer
|
||||||
|
|||||||
@@ -18,11 +18,9 @@ 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.1.4"
|
local R_PLC_VERSION = "v1.3.2"
|
||||||
|
|
||||||
local print = util.print
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local print_ts = util.print_ts
|
|
||||||
local println_ts = util.println_ts
|
local println_ts = util.println_ts
|
||||||
|
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
@@ -56,7 +54,7 @@ end
|
|||||||
-- log init
|
-- log init
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
log.init(config.LOG_PATH, config.LOG_MODE)
|
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
|
||||||
|
|
||||||
log.info("========================================")
|
log.info("========================================")
|
||||||
log.info("BOOTING reactor-plc.startup " .. R_PLC_VERSION)
|
log.info("BOOTING reactor-plc.startup " .. R_PLC_VERSION)
|
||||||
@@ -176,8 +174,9 @@ local function main()
|
|||||||
|
|
||||||
-- front panel time!
|
-- front panel time!
|
||||||
if not renderer.ui_ready() then
|
if not renderer.ui_ready() then
|
||||||
local message = nil
|
local message
|
||||||
plc_state.fp_ok, message = pcall(renderer.start_ui)
|
plc_state.fp_ok, message = pcall(renderer.start_ui)
|
||||||
|
|
||||||
if not plc_state.fp_ok then
|
if not plc_state.fp_ok then
|
||||||
renderer.close_ui()
|
renderer.close_ui()
|
||||||
println_ts(util.c("UI error: ", message))
|
println_ts(util.c("UI error: ", message))
|
||||||
@@ -265,4 +264,9 @@ local function main()
|
|||||||
log.info("exited")
|
log.info("exited")
|
||||||
end
|
end
|
||||||
|
|
||||||
if not xpcall(main, crash.handler) then crash.exit() end
|
if not xpcall(main, crash.handler) then
|
||||||
|
pcall(renderer.close_ui)
|
||||||
|
crash.exit()
|
||||||
|
else
|
||||||
|
log.close()
|
||||||
|
end
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ local MQ__COMM_CMD = {
|
|||||||
---@param init function
|
---@param init function
|
||||||
function threads.thread__main(smem, init)
|
function threads.thread__main(smem, init)
|
||||||
-- print a log message to the terminal as long as the UI isn't running
|
-- print a log message to the terminal as long as the UI isn't running
|
||||||
local function println(message) if not smem.plc_state.fp_ok then util.println(message) end end
|
|
||||||
local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
|
local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
|
||||||
|
|
||||||
---@class parallel_thread
|
---@class parallel_thread
|
||||||
@@ -258,9 +257,9 @@ function threads.thread__main(smem, init)
|
|||||||
|
|
||||||
-- update indicators
|
-- update indicators
|
||||||
databus.tx_hw_status(plc_state)
|
databus.tx_hw_status(plc_state)
|
||||||
elseif event == "mouse_click" then
|
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then
|
||||||
-- handle a monitor touch event
|
-- handle a mouse event
|
||||||
renderer.handle_mouse(core.events.click(param1, param2, param3))
|
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||||
elseif event == "clock_start" then
|
elseif event == "clock_start" then
|
||||||
-- start loop clock
|
-- start loop clock
|
||||||
loop_clock.start()
|
loop_clock.start()
|
||||||
@@ -307,7 +306,6 @@ end
|
|||||||
---@param smem plc_shared_memory
|
---@param smem plc_shared_memory
|
||||||
function threads.thread__rps(smem)
|
function threads.thread__rps(smem)
|
||||||
-- print a log message to the terminal as long as the UI isn't running
|
-- print a log message to the terminal as long as the UI isn't running
|
||||||
local function println(message) if not smem.plc_state.fp_ok then util.println(message) end end
|
|
||||||
local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
|
local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
|
||||||
|
|
||||||
---@class parallel_thread
|
---@class parallel_thread
|
||||||
@@ -682,7 +680,7 @@ function threads.thread__setpoint_control(smem)
|
|||||||
-- we yielded, check enable again
|
-- we yielded, check enable again
|
||||||
if setpoints.burn_rate_en and (type(current_burn_rate) == "number") and (current_burn_rate ~= setpoints.burn_rate) then
|
if setpoints.burn_rate_en and (type(current_burn_rate) == "number") and (current_burn_rate ~= setpoints.burn_rate) then
|
||||||
-- calculate new burn rate
|
-- calculate new burn rate
|
||||||
local new_burn_rate = current_burn_rate
|
local new_burn_rate ---@type number
|
||||||
|
|
||||||
if setpoints.burn_rate > current_burn_rate then
|
if setpoints.burn_rate > current_burn_rate then
|
||||||
-- need to ramp up
|
-- need to ramp up
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ config.LOG_PATH = "/log.txt"
|
|||||||
-- 0 = APPEND (adds to existing file on start)
|
-- 0 = APPEND (adds to existing file on start)
|
||||||
-- 1 = NEW (replaces existing file on start)
|
-- 1 = NEW (replaces existing file on start)
|
||||||
config.LOG_MODE = 0
|
config.LOG_MODE = 0
|
||||||
|
-- true to log verbose debug messages
|
||||||
|
config.LOG_DEBUG = false
|
||||||
|
|
||||||
-- RTU peripheral devices (named: side/network device name)
|
-- RTU peripheral devices (named: side/network device name)
|
||||||
config.RTU_DEVICES = {
|
config.RTU_DEVICES = {
|
||||||
|
|||||||
74
rtu/databus.lua
Normal file
74
rtu/databus.lua
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
--
|
||||||
|
-- Data Bus - Central Communication Linking for RTU Front Panel
|
||||||
|
--
|
||||||
|
|
||||||
|
local psil = require("scada-common.psil")
|
||||||
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
|
local databus = {}
|
||||||
|
|
||||||
|
-- databus PSIL
|
||||||
|
databus.ps = psil.create()
|
||||||
|
|
||||||
|
---@enum RTU_UNIT_HW_STATE
|
||||||
|
local RTU_UNIT_HW_STATE = {
|
||||||
|
OFFLINE = 1,
|
||||||
|
FAULTED = 2,
|
||||||
|
UNFORMED = 3,
|
||||||
|
OK = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
databus.RTU_UNIT_HW_STATE = RTU_UNIT_HW_STATE
|
||||||
|
|
||||||
|
-- call to toggle heartbeat signal
|
||||||
|
function databus.heartbeat() databus.ps.toggle("heartbeat") end
|
||||||
|
|
||||||
|
-- transmit firmware versions across the bus
|
||||||
|
---@param rtu_v string RTU version
|
||||||
|
---@param comms_v string comms version
|
||||||
|
function databus.tx_versions(rtu_v, comms_v)
|
||||||
|
databus.ps.publish("version", rtu_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 unit hardware type across the bus
|
||||||
|
---@param uid integer unit ID
|
||||||
|
---@param type RTU_UNIT_TYPE
|
||||||
|
function databus.tx_unit_hw_type(uid, type)
|
||||||
|
databus.ps.publish("unit_type_" .. uid, type)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- transmit unit hardware status across the bus
|
||||||
|
---@param uid integer unit ID
|
||||||
|
---@param status RTU_UNIT_HW_STATE
|
||||||
|
function databus.tx_unit_hw_status(uid, status)
|
||||||
|
databus.ps.publish("unit_hw_" .. uid, status)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- transmit thread (routine) statuses
|
||||||
|
---@param thread string thread name
|
||||||
|
---@param ok boolean thread state
|
||||||
|
function databus.tx_rt_status(thread, ok)
|
||||||
|
databus.ps.publish(util.c("routine__", thread), ok)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- transmit supervisor link state across the bus
|
||||||
|
---@param state integer
|
||||||
|
function databus.tx_link_state(state)
|
||||||
|
databus.ps.publish("link_state", state)
|
||||||
|
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
|
||||||
@@ -34,7 +34,7 @@ function redstone_rtu.new()
|
|||||||
---@param side string
|
---@param side string
|
||||||
---@param color integer
|
---@param color integer
|
||||||
function public.link_di(side, color)
|
function public.link_di(side, color)
|
||||||
local f_read = nil
|
local f_read ---@type function
|
||||||
|
|
||||||
if color then
|
if color then
|
||||||
f_read = function ()
|
f_read = function ()
|
||||||
@@ -53,8 +53,8 @@ function redstone_rtu.new()
|
|||||||
---@param side string
|
---@param side string
|
||||||
---@param color integer
|
---@param color integer
|
||||||
function public.link_do(side, color)
|
function public.link_do(side, color)
|
||||||
local f_read = nil
|
local f_read ---@type function
|
||||||
local f_write = nil
|
local f_write ---@type function
|
||||||
|
|
||||||
if color then
|
if color then
|
||||||
f_read = function ()
|
f_read = function ()
|
||||||
|
|||||||
@@ -347,11 +347,9 @@ function modbus.new(rtu_dev, use_parallel_read)
|
|||||||
response = { MODBUS_EXCODE.NEG_ACKNOWLEDGE }
|
response = { MODBUS_EXCODE.NEG_ACKNOWLEDGE }
|
||||||
end
|
end
|
||||||
|
|
||||||
-- default is to echo back
|
-- default is to echo back<br>
|
||||||
local func_code = packet.func_code
|
-- but here we echo back with error flag, on success the "error" will be acknowledgement
|
||||||
|
local func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
|
||||||
-- echo back with error flag, on success the "error" will be acknowledgement
|
|
||||||
func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
|
|
||||||
|
|
||||||
-- create reply
|
-- create reply
|
||||||
local reply = comms.modbus_packet()
|
local reply = comms.modbus_packet()
|
||||||
@@ -365,8 +363,8 @@ function modbus.new(rtu_dev, use_parallel_read)
|
|||||||
---@param packet modbus_frame
|
---@param packet modbus_frame
|
||||||
---@return boolean return_code, modbus_packet reply
|
---@return boolean return_code, modbus_packet reply
|
||||||
function public.handle_packet(packet)
|
function public.handle_packet(packet)
|
||||||
local return_code = true
|
local return_code ---@type boolean
|
||||||
local response = nil
|
local response ---@type table|MODBUS_EXCODE
|
||||||
|
|
||||||
if packet.length >= 2 then
|
if packet.length >= 2 then
|
||||||
-- handle by function code
|
-- handle by function code
|
||||||
|
|||||||
121
rtu/panel/front_panel.lua
Normal file
121
rtu/panel/front_panel.lua
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
--
|
||||||
|
-- Main SCADA Coordinator GUI
|
||||||
|
--
|
||||||
|
|
||||||
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
|
local databus = require("rtu.databus")
|
||||||
|
|
||||||
|
local style = require("rtu.panel.style")
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
|
|
||||||
|
local Div = require("graphics.elements.div")
|
||||||
|
local TextBox = require("graphics.elements.textbox")
|
||||||
|
|
||||||
|
local LED = require("graphics.elements.indicators.led")
|
||||||
|
local RGBLED = require("graphics.elements.indicators.ledrgb")
|
||||||
|
|
||||||
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
|
local cpair = core.cpair
|
||||||
|
|
||||||
|
local UNIT_TYPE_LABELS = {
|
||||||
|
"UNKNOWN",
|
||||||
|
"REDSTONE",
|
||||||
|
"BOILER",
|
||||||
|
"TURBINE",
|
||||||
|
"IND MATRIX",
|
||||||
|
"SPS",
|
||||||
|
"SNA",
|
||||||
|
"ENV DETECTOR"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
-- create new main view
|
||||||
|
---@param panel graphics_element main displaybox
|
||||||
|
---@param units table unit list
|
||||||
|
local function init(panel, units)
|
||||||
|
TextBox{parent=panel,y=1,text="RTU GATEWAY",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||||
|
|
||||||
|
--
|
||||||
|
-- system indicators
|
||||||
|
--
|
||||||
|
|
||||||
|
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 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)}
|
||||||
|
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
|
||||||
|
network.update(5)
|
||||||
|
system.line_break()
|
||||||
|
|
||||||
|
modem.register(databus.ps, "has_modem", modem.update)
|
||||||
|
network.register(databus.ps, "link_state", network.update)
|
||||||
|
|
||||||
|
local rt_main = LED{parent=system,label="RT MAIN",colors=cpair(colors.green,colors.green_off)}
|
||||||
|
local rt_comm = LED{parent=system,label="RT COMMS",colors=cpair(colors.green,colors.green_off)}
|
||||||
|
system.line_break()
|
||||||
|
|
||||||
|
rt_main.register(databus.ps, "routine__main", rt_main.update)
|
||||||
|
rt_comm.register(databus.ps, "routine__comms", rt_comm.update)
|
||||||
|
|
||||||
|
--
|
||||||
|
-- about label
|
||||||
|
--
|
||||||
|
|
||||||
|
local about = Div{parent=panel,width=15,height=3,x=1,y=18,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)
|
||||||
|
|
||||||
|
--
|
||||||
|
-- unit status list
|
||||||
|
--
|
||||||
|
|
||||||
|
local threads = Div{parent=panel,width=8,height=18,x=17,y=3}
|
||||||
|
|
||||||
|
-- display up to 16 units
|
||||||
|
local list_length = math.min(#units, 16)
|
||||||
|
|
||||||
|
-- show routine statuses
|
||||||
|
for i = 1, list_length do
|
||||||
|
TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i),height=1}
|
||||||
|
local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=cpair(colors.green,colors.green_off)}
|
||||||
|
rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update)
|
||||||
|
end
|
||||||
|
|
||||||
|
local unit_hw_statuses = Div{parent=panel,height=18,x=25,y=3}
|
||||||
|
|
||||||
|
-- show hardware statuses
|
||||||
|
for i = 1, list_length do
|
||||||
|
local unit = units[i] ---@type rtu_unit_registry_entry
|
||||||
|
|
||||||
|
-- hardware status
|
||||||
|
local unit_hw = RGBLED{parent=unit_hw_statuses,y=i,label="",colors={colors.red,colors.orange,colors.yellow,colors.green}}
|
||||||
|
|
||||||
|
unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update)
|
||||||
|
|
||||||
|
-- unit name identifier (type + index)
|
||||||
|
local name = util.c(UNIT_TYPE_LABELS[unit.type + 1], " ", unit.index)
|
||||||
|
local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=name,height=1}
|
||||||
|
|
||||||
|
name_box.register(databus.ps, "unit_type_" .. i, function (t)
|
||||||
|
name_box.set_value(util.c(UNIT_TYPE_LABELS[t + 1], " ", unit.index))
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- assignment (unit # or facility)
|
||||||
|
local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor)
|
||||||
|
TextBox{parent=unit_hw_statuses,y=i,x=19,text=for_unit,height=1,fg_bg=cpair(colors.lightGray,colors.ivory)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return init
|
||||||
41
rtu/panel/style.lua
Normal file
41
rtu/panel/style.lua
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
--
|
||||||
|
-- Graphics Style Options
|
||||||
|
--
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
|
|
||||||
|
local style = {}
|
||||||
|
|
||||||
|
local cpair = core.cpair
|
||||||
|
|
||||||
|
-- GLOBAL --
|
||||||
|
|
||||||
|
-- remap global colors
|
||||||
|
colors.ivory = colors.pink
|
||||||
|
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 = 0x0096ff },
|
||||||
|
{ c = colors.purple, hex = 0xb156ee },
|
||||||
|
{ 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
|
||||||
79
rtu/renderer.lua
Normal file
79
rtu/renderer.lua
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
--
|
||||||
|
-- Graphics Rendering Control
|
||||||
|
--
|
||||||
|
|
||||||
|
local panel_view = require("rtu.panel.front_panel")
|
||||||
|
local style = require("rtu.panel.style")
|
||||||
|
|
||||||
|
local flasher = require("graphics.flasher")
|
||||||
|
|
||||||
|
local DisplayBox = require("graphics.elements.displaybox")
|
||||||
|
|
||||||
|
local renderer = {}
|
||||||
|
|
||||||
|
local ui = {
|
||||||
|
display = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
-- start the UI
|
||||||
|
---@param units table RTU units
|
||||||
|
function renderer.start_ui(units)
|
||||||
|
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
|
||||||
|
|
||||||
|
-- start flasher callback task
|
||||||
|
flasher.run()
|
||||||
|
|
||||||
|
-- init front panel view
|
||||||
|
ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||||
|
panel_view(ui.display, units)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- close out the UI
|
||||||
|
function renderer.close_ui()
|
||||||
|
if ui.display ~= nil then
|
||||||
|
-- stop blinking indicators
|
||||||
|
flasher.clear()
|
||||||
|
|
||||||
|
-- delete element tree
|
||||||
|
ui.display.delete()
|
||||||
|
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
|
||||||
36
rtu/rtu.lua
36
rtu/rtu.lua
@@ -1,10 +1,11 @@
|
|||||||
local comms = require("scada-common.comms")
|
local comms = require("scada-common.comms")
|
||||||
local ppm = require("scada-common.ppm")
|
local ppm = require("scada-common.ppm")
|
||||||
local log = require("scada-common.log")
|
local log = require("scada-common.log")
|
||||||
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 modbus = require("rtu.modbus")
|
local databus = require("rtu.databus")
|
||||||
|
local modbus = require("rtu.modbus")
|
||||||
|
|
||||||
local rtu = {}
|
local rtu = {}
|
||||||
|
|
||||||
@@ -14,11 +15,6 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
|||||||
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 print = util.print
|
|
||||||
local println = util.println
|
|
||||||
local print_ts = util.print_ts
|
|
||||||
local println_ts = util.println_ts
|
|
||||||
|
|
||||||
-- create a new RTU unit
|
-- create a new RTU unit
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
function rtu.init_unit()
|
function rtu.init_unit()
|
||||||
@@ -316,7 +312,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
|||||||
pkt = mgmt_pkt.get()
|
pkt = mgmt_pkt.get()
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
log.error("illegal packet type " .. s_pkt.protocol(), true)
|
log.debug("illegal packet type " .. s_pkt.protocol(), true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -328,11 +324,14 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
|||||||
---@param units table RTU units
|
---@param units table RTU units
|
||||||
---@param rtu_state rtu_state
|
---@param rtu_state rtu_state
|
||||||
function public.handle_packet(packet, units, rtu_state)
|
function public.handle_packet(packet, units, rtu_state)
|
||||||
|
-- print a log message to the terminal as long as the UI isn't running
|
||||||
|
local function println_ts(message) if not rtu_state.fp_ok then util.println_ts(message) end end
|
||||||
|
|
||||||
if packet.scada_frame.local_port() == local_port then
|
if packet.scada_frame.local_port() == local_port 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 >= 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
|
||||||
else
|
else
|
||||||
@@ -347,8 +346,8 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
|||||||
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
|
||||||
local return_code = false
|
local return_code ---@type boolean
|
||||||
local reply = modbus.reply__neg_ack(packet)
|
local reply ---@type modbus_packet
|
||||||
|
|
||||||
-- handle MODBUS instruction
|
-- handle MODBUS instruction
|
||||||
if packet.unit_id <= #units then
|
if packet.unit_id <= #units then
|
||||||
@@ -382,7 +381,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
|||||||
else
|
else
|
||||||
-- unit ID out of range?
|
-- unit ID out of range?
|
||||||
reply = modbus.reply__gw_unavailable(packet)
|
reply = modbus.reply__gw_unavailable(packet)
|
||||||
log.error("received MODBUS packet for non-existent unit")
|
log.debug("received MODBUS packet for non-existent unit")
|
||||||
end
|
end
|
||||||
|
|
||||||
public.send_modbus(reply)
|
public.send_modbus(reply)
|
||||||
@@ -419,6 +418,9 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
|||||||
end
|
end
|
||||||
|
|
||||||
self.last_est_ack = est_ack
|
self.last_est_ack = est_ack
|
||||||
|
|
||||||
|
-- report link state
|
||||||
|
databus.tx_link_state(est_ack + 1)
|
||||||
else
|
else
|
||||||
log.debug("SCADA_MGMT establish packet length mismatch")
|
log.debug("SCADA_MGMT establish packet length mismatch")
|
||||||
end
|
end
|
||||||
@@ -450,7 +452,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
|||||||
public.send_advertisement(units)
|
public.send_advertisement(units)
|
||||||
else
|
else
|
||||||
-- not supported
|
-- not supported
|
||||||
log.warning("received unsupported SCADA_MGMT message type " .. packet.type)
|
log.debug("received unsupported SCADA_MGMT message type " .. packet.type)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
log.debug("discarding non-link SCADA_MGMT packet before linked")
|
log.debug("discarding non-link SCADA_MGMT packet before linked")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
require("/initenv").init_env()
|
require("/initenv").init_env()
|
||||||
|
|
||||||
|
local comms = require("scada-common.comms")
|
||||||
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 mqueue = require("scada-common.mqueue")
|
local mqueue = require("scada-common.mqueue")
|
||||||
@@ -13,7 +14,9 @@ local types = require("scada-common.types")
|
|||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
local config = require("rtu.config")
|
local config = require("rtu.config")
|
||||||
|
local databus = require("rtu.databus")
|
||||||
local modbus = require("rtu.modbus")
|
local modbus = require("rtu.modbus")
|
||||||
|
local renderer = require("rtu.renderer")
|
||||||
local rtu = require("rtu.rtu")
|
local rtu = require("rtu.rtu")
|
||||||
local threads = require("rtu.threads")
|
local threads = require("rtu.threads")
|
||||||
|
|
||||||
@@ -25,13 +28,12 @@ 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 = "v0.13.2"
|
local RTU_VERSION = "v1.2.2"
|
||||||
|
|
||||||
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 print = util.print
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local print_ts = util.print_ts
|
|
||||||
local println_ts = util.println_ts
|
local println_ts = util.println_ts
|
||||||
|
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
@@ -55,7 +57,7 @@ assert(cfv.valid(), "bad config file: missing/invalid fields")
|
|||||||
-- log init
|
-- log init
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
log.init(config.LOG_PATH, config.LOG_MODE)
|
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
|
||||||
|
|
||||||
log.info("========================================")
|
log.info("========================================")
|
||||||
log.info("BOOTING rtu.startup " .. RTU_VERSION)
|
log.info("BOOTING rtu.startup " .. RTU_VERSION)
|
||||||
@@ -73,6 +75,9 @@ local function main()
|
|||||||
-- startup
|
-- startup
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
|
-- record firmware versions and ID
|
||||||
|
databus.tx_versions(RTU_VERSION, comms.version)
|
||||||
|
|
||||||
-- mount connected devices
|
-- mount connected devices
|
||||||
ppm.mount_all()
|
ppm.mount_all()
|
||||||
|
|
||||||
@@ -81,6 +86,7 @@ local function main()
|
|||||||
-- RTU system state flags
|
-- RTU system state flags
|
||||||
---@class rtu_state
|
---@class rtu_state
|
||||||
rtu_state = {
|
rtu_state = {
|
||||||
|
fp_ok = false,
|
||||||
linked = false,
|
linked = false,
|
||||||
shutdown = false
|
shutdown = false
|
||||||
},
|
},
|
||||||
@@ -113,6 +119,8 @@ local function main()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
databus.tx_hw_modem(true)
|
||||||
|
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
-- interpret config and init units
|
-- interpret config and init units
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
@@ -252,6 +260,8 @@ local function main()
|
|||||||
log.info(util.c("configure> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message))
|
log.info(util.c("configure> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message))
|
||||||
|
|
||||||
unit.uid = #units
|
unit.uid = #units
|
||||||
|
|
||||||
|
databus.tx_unit_hw_status(unit.uid, RTU_UNIT_HW_STATE.OK)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -287,9 +297,9 @@ local function main()
|
|||||||
|
|
||||||
local device = ppm.get_periph(name)
|
local device = ppm.get_periph(name)
|
||||||
|
|
||||||
local type = nil ---@type string|nil
|
local type ---@type string|nil
|
||||||
local rtu_iface = nil ---@type rtu_device
|
local rtu_iface ---@type rtu_device
|
||||||
local rtu_type = nil ---@type RTU_UNIT_TYPE
|
local rtu_type ---@type RTU_UNIT_TYPE
|
||||||
local is_multiblock = false ---@type boolean
|
local is_multiblock = false ---@type boolean
|
||||||
local formed = nil ---@type boolean|nil
|
local formed = nil ---@type boolean|nil
|
||||||
local faulted = nil ---@type boolean|nil
|
local faulted = nil ---@type boolean|nil
|
||||||
@@ -356,11 +366,11 @@ local function main()
|
|||||||
elseif type == "solarNeutronActivator" then
|
elseif type == "solarNeutronActivator" then
|
||||||
-- SNA
|
-- SNA
|
||||||
rtu_type = RTU_UNIT_TYPE.SNA
|
rtu_type = RTU_UNIT_TYPE.SNA
|
||||||
rtu_iface, _ = sna_rtu.new(device)
|
rtu_iface, faulted = sna_rtu.new(device)
|
||||||
elseif type == "environmentDetector" then
|
elseif type == "environmentDetector" then
|
||||||
-- advanced peripherals environment detector
|
-- advanced peripherals environment detector
|
||||||
rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
|
rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
|
||||||
rtu_iface, _ = envd_rtu.new(device)
|
rtu_iface, faulted = envd_rtu.new(device)
|
||||||
elseif type == ppm.VIRTUAL_DEVICE_TYPE then
|
elseif type == ppm.VIRTUAL_DEVICE_TYPE then
|
||||||
-- placeholder device
|
-- placeholder device
|
||||||
rtu_type = RTU_UNIT_TYPE.VIRTUAL
|
rtu_type = RTU_UNIT_TYPE.VIRTUAL
|
||||||
@@ -411,6 +421,20 @@ local function main()
|
|||||||
log.info(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ") [", index, "] for ", for_message))
|
log.info(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ") [", index, "] for ", for_message))
|
||||||
|
|
||||||
rtu_unit.uid = #units
|
rtu_unit.uid = #units
|
||||||
|
|
||||||
|
-- report hardware status
|
||||||
|
if rtu_unit.type == RTU_UNIT_TYPE.VIRTUAL then
|
||||||
|
databus.tx_unit_hw_status(rtu_unit.uid, RTU_UNIT_HW_STATE.OFFLINE)
|
||||||
|
else
|
||||||
|
if rtu_unit.is_multiblock then
|
||||||
|
databus.tx_unit_hw_status(rtu_unit.uid, util.trinary(rtu_unit.formed == true, RTU_UNIT_HW_STATE.OK, RTU_UNIT_HW_STATE.UNFORMED))
|
||||||
|
elseif faulted then
|
||||||
|
databus.tx_unit_hw_status(rtu_unit.uid, RTU_UNIT_HW_STATE.FAULTED)
|
||||||
|
else
|
||||||
|
databus.tx_unit_hw_status(rtu_unit.uid, RTU_UNIT_HW_STATE.OK)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- we made it through all that trusting-user-to-write-a-config-file chaos
|
-- we made it through all that trusting-user-to-write-a-config-file chaos
|
||||||
@@ -421,9 +445,23 @@ local function main()
|
|||||||
-- start system
|
-- start system
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
|
local rtu_state = __shared_memory.rtu_state
|
||||||
|
|
||||||
log.debug("boot> running configure()")
|
log.debug("boot> running configure()")
|
||||||
|
|
||||||
if configure() then
|
if configure() then
|
||||||
|
-- start UI
|
||||||
|
local message
|
||||||
|
rtu_state.fp_ok, message = pcall(renderer.start_ui, units)
|
||||||
|
|
||||||
|
if not rtu_state.fp_ok then
|
||||||
|
renderer.close_ui()
|
||||||
|
println_ts(util.c("UI error: ", message))
|
||||||
|
println("init> running without front panel")
|
||||||
|
log.error(util.c("GUI crashed with error ", message))
|
||||||
|
log.info("init> running in headless mode without front panel")
|
||||||
|
end
|
||||||
|
|
||||||
-- start connection watchdog
|
-- start connection watchdog
|
||||||
smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT)
|
smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT)
|
||||||
log.debug("startup> conn watchdog started")
|
log.debug("startup> conn watchdog started")
|
||||||
@@ -453,8 +491,15 @@ local function main()
|
|||||||
println("configuration failed, exiting...")
|
println("configuration failed, exiting...")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
renderer.close_ui()
|
||||||
|
|
||||||
println_ts("exited")
|
println_ts("exited")
|
||||||
log.info("exited")
|
log.info("exited")
|
||||||
end
|
end
|
||||||
|
|
||||||
if not xpcall(main, crash.handler) then crash.exit() end
|
if not xpcall(main, crash.handler) then
|
||||||
|
pcall(renderer.close_ui)
|
||||||
|
crash.exit()
|
||||||
|
else
|
||||||
|
log.close()
|
||||||
|
end
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ local ppm = require("scada-common.ppm")
|
|||||||
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("rtu.databus")
|
||||||
|
local modbus = require("rtu.modbus")
|
||||||
|
local renderer = require("rtu.renderer")
|
||||||
|
|
||||||
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
|
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
|
||||||
local envd_rtu = require("rtu.dev.envd_rtu")
|
local envd_rtu = require("rtu.dev.envd_rtu")
|
||||||
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
|
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
|
||||||
@@ -11,16 +15,12 @@ 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 modbus = require("rtu.modbus")
|
local core = require("graphics.core")
|
||||||
|
|
||||||
local threads = {}
|
local threads = {}
|
||||||
|
|
||||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||||
|
local UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
|
||||||
local print = util.print
|
|
||||||
local println = util.println
|
|
||||||
local print_ts = util.print_ts
|
|
||||||
local println_ts = util.println_ts
|
|
||||||
|
|
||||||
local MAIN_CLOCK = 2 -- (2Hz, 40 ticks)
|
local MAIN_CLOCK = 2 -- (2Hz, 40 ticks)
|
||||||
local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
|
local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
|
||||||
@@ -29,11 +29,15 @@ local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
|
|||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param smem rtu_shared_memory
|
---@param smem rtu_shared_memory
|
||||||
function threads.thread__main(smem)
|
function threads.thread__main(smem)
|
||||||
|
-- print a log message to the terminal as long as the UI isn't running
|
||||||
|
local function println_ts(message) if not smem.rtu_state.fp_ok then util.println_ts(message) end end
|
||||||
|
|
||||||
---@class parallel_thread
|
---@class parallel_thread
|
||||||
local public = {}
|
local public = {}
|
||||||
|
|
||||||
-- execute thread
|
-- execute thread
|
||||||
function public.exec()
|
function public.exec()
|
||||||
|
databus.tx_rt_status("main", true)
|
||||||
log.debug("main thread start")
|
log.debug("main thread start")
|
||||||
|
|
||||||
-- main loop clock
|
-- main loop clock
|
||||||
@@ -57,6 +61,9 @@ function threads.thread__main(smem)
|
|||||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||||
|
|
||||||
if event == "timer" and loop_clock.is_clock(param1) then
|
if event == "timer" and loop_clock.is_clock(param1) then
|
||||||
|
-- blink heartbeat indicator
|
||||||
|
databus.heartbeat()
|
||||||
|
|
||||||
-- start next clock timer
|
-- start next clock timer
|
||||||
loop_clock.start()
|
loop_clock.start()
|
||||||
|
|
||||||
@@ -85,6 +92,8 @@ function threads.thread__main(smem)
|
|||||||
if device == rtu_dev.modem then
|
if device == rtu_dev.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
|
||||||
@@ -94,10 +103,11 @@ function threads.thread__main(smem)
|
|||||||
if units[i].device == device then
|
if units[i].device == device then
|
||||||
-- we are going to let the PPM prevent crashes
|
-- we are going to let the PPM prevent crashes
|
||||||
-- return fault flags/codes to MODBUS queries
|
-- return fault flags/codes to MODBUS queries
|
||||||
local unit = units[i]
|
local unit = units[i] ---@type rtu_unit_registry_entry
|
||||||
local type_name = types.rtu_type_to_string(unit.type)
|
local type_name = types.rtu_type_to_string(unit.type)
|
||||||
println_ts(util.c("lost the ", type_name, " on interface ", unit.name))
|
println_ts(util.c("lost the ", type_name, " on interface ", unit.name))
|
||||||
log.warning(util.c("lost the ", type_name, " unit peripheral on interface ", unit.name))
|
log.warning(util.c("lost the ", type_name, " unit peripheral on interface ", unit.name))
|
||||||
|
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OFFLINE)
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -116,6 +126,8 @@ function threads.thread__main(smem)
|
|||||||
|
|
||||||
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
|
||||||
@@ -156,34 +168,49 @@ function threads.thread__main(smem)
|
|||||||
resend_advert = false
|
resend_advert = false
|
||||||
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
|
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
databus.tx_unit_hw_type(unit.uid, unit.type)
|
||||||
end
|
end
|
||||||
|
|
||||||
if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
|
if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
|
||||||
unit.rtu = boilerv_rtu.new(device)
|
unit.rtu = boilerv_rtu.new(device)
|
||||||
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
|
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
|
||||||
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
|
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
|
||||||
|
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED)
|
||||||
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
|
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
|
||||||
unit.rtu = turbinev_rtu.new(device)
|
unit.rtu = turbinev_rtu.new(device)
|
||||||
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
|
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
|
||||||
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
|
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
|
||||||
|
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED)
|
||||||
elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
|
elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
|
||||||
unit.rtu = imatrix_rtu.new(device)
|
unit.rtu = imatrix_rtu.new(device)
|
||||||
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
|
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
|
||||||
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
|
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
|
||||||
|
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED)
|
||||||
elseif unit.type == RTU_UNIT_TYPE.SPS then
|
elseif unit.type == RTU_UNIT_TYPE.SPS then
|
||||||
unit.rtu = sps_rtu.new(device)
|
unit.rtu = sps_rtu.new(device)
|
||||||
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
|
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
|
||||||
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
|
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
|
||||||
|
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED)
|
||||||
elseif unit.type == RTU_UNIT_TYPE.SNA then
|
elseif unit.type == RTU_UNIT_TYPE.SNA then
|
||||||
unit.rtu = sna_rtu.new(device)
|
unit.rtu = sna_rtu.new(device)
|
||||||
|
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK)
|
||||||
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
|
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
|
||||||
unit.rtu = envd_rtu.new(device)
|
unit.rtu = envd_rtu.new(device)
|
||||||
|
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK)
|
||||||
else
|
else
|
||||||
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
|
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
|
||||||
end
|
end
|
||||||
|
|
||||||
if unit.is_multiblock and (unit.formed == false) then
|
if unit.is_multiblock then
|
||||||
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
|
if (unit.formed == false) then
|
||||||
|
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
|
||||||
|
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED)
|
||||||
|
end
|
||||||
|
elseif device.__p_is_faulted() then
|
||||||
|
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.FAULTED)
|
||||||
|
else
|
||||||
|
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK)
|
||||||
end
|
end
|
||||||
|
|
||||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||||
@@ -196,12 +223,15 @@ function threads.thread__main(smem)
|
|||||||
if resend_advert then
|
if resend_advert then
|
||||||
rtu_comms.send_advertisement(units)
|
rtu_comms.send_advertisement(units)
|
||||||
else
|
else
|
||||||
rtu_comms.send_remounted(unit.uid)
|
rtu_comms.send_remounted(unit.uid)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
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
|
||||||
@@ -223,6 +253,8 @@ function threads.thread__main(smem)
|
|||||||
log.fatal(util.strval(result))
|
log.fatal(util.strval(result))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
databus.tx_rt_status("main", false)
|
||||||
|
|
||||||
if not rtu_state.shutdown then
|
if not rtu_state.shutdown then
|
||||||
log.info("main thread restarting in 5 seconds...")
|
log.info("main thread restarting in 5 seconds...")
|
||||||
util.psleep(5)
|
util.psleep(5)
|
||||||
@@ -242,6 +274,7 @@ function threads.thread__comms(smem)
|
|||||||
|
|
||||||
-- execute thread
|
-- execute thread
|
||||||
function public.exec()
|
function public.exec()
|
||||||
|
databus.tx_rt_status("comms", true)
|
||||||
log.debug("comms thread start")
|
log.debug("comms thread start")
|
||||||
|
|
||||||
-- load in from shared memory
|
-- load in from shared memory
|
||||||
@@ -297,6 +330,8 @@ function threads.thread__comms(smem)
|
|||||||
log.fatal(util.strval(result))
|
log.fatal(util.strval(result))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
databus.tx_rt_status("comms", false)
|
||||||
|
|
||||||
if not rtu_state.shutdown then
|
if not rtu_state.shutdown then
|
||||||
log.info("comms thread restarting in 5 seconds...")
|
log.info("comms thread restarting in 5 seconds...")
|
||||||
util.psleep(5)
|
util.psleep(5)
|
||||||
@@ -317,7 +352,8 @@ function threads.thread__unit_comms(smem, unit)
|
|||||||
|
|
||||||
-- execute thread
|
-- execute thread
|
||||||
function public.exec()
|
function public.exec()
|
||||||
log.debug(util.c("rtu unit thread start -> ", types.rtu_type_to_string(unit.type), "(", unit.name, ")"))
|
databus.tx_rt_status("unit_" .. unit.uid, true)
|
||||||
|
log.debug(util.c("rtu unit thread start -> ", types.rtu_type_to_string(unit.type), " (", unit.name, ")"))
|
||||||
|
|
||||||
-- load in from shared memory
|
-- load in from shared memory
|
||||||
local rtu_state = smem.rtu_state
|
local rtu_state = smem.rtu_state
|
||||||
@@ -351,6 +387,13 @@ function threads.thread__unit_comms(smem, unit)
|
|||||||
-- received a packet
|
-- received a packet
|
||||||
local _, reply = unit.modbus_io.handle_packet(msg.message)
|
local _, reply = unit.modbus_io.handle_packet(msg.message)
|
||||||
rtu_comms.send_modbus(reply)
|
rtu_comms.send_modbus(reply)
|
||||||
|
|
||||||
|
-- check if there was a problem and update the hardware state if so
|
||||||
|
local frame = reply.get()
|
||||||
|
if unit.formed and (bit.band(frame.func_code, types.MODBUS_FCODE.ERROR_FLAG) ~= 0) and
|
||||||
|
(frame.data[1] == types.MODBUS_EXCODE.SERVER_DEVICE_FAIL) then
|
||||||
|
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.FAULTED)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -364,7 +407,14 @@ function threads.thread__unit_comms(smem, unit)
|
|||||||
|
|
||||||
last_f_check = util.time_ms()
|
last_f_check = util.time_ms()
|
||||||
|
|
||||||
if unit.formed == nil then unit.formed = is_formed end
|
if unit.formed == nil then
|
||||||
|
unit.formed = is_formed
|
||||||
|
if is_formed then databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK) end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not unit.formed then
|
||||||
|
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED)
|
||||||
|
end
|
||||||
|
|
||||||
if (not unit.formed) and is_formed then
|
if (not unit.formed) and is_formed then
|
||||||
-- newly re-formed
|
-- newly re-formed
|
||||||
@@ -403,21 +453,25 @@ function threads.thread__unit_comms(smem, unit)
|
|||||||
unit.formed = device.isFormed()
|
unit.formed = device.isFormed()
|
||||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||||
else
|
else
|
||||||
log.error("illegal remount of non-multiblock RTU attempted for " .. short_name, true)
|
log.error("illegal remount of non-multiblock RTU or type change attempted for " .. short_name, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
if unit.formed and faulted then
|
if unit.formed and faulted then
|
||||||
-- something is still wrong = can't mark as formed yet
|
-- something is still wrong = can't mark as formed yet
|
||||||
unit.formed = false
|
unit.formed = false
|
||||||
|
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
|
||||||
|
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED)
|
||||||
else
|
else
|
||||||
rtu_comms.send_remounted(unit.uid)
|
rtu_comms.send_remounted(unit.uid)
|
||||||
|
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local type_name = types.rtu_type_to_string(unit.type)
|
||||||
|
log.info(util.c("reconnected the ", type_name, " on interface ", unit.name))
|
||||||
else
|
else
|
||||||
-- fully lost the peripheral now :(
|
-- fully lost the peripheral now :(
|
||||||
log.error(util.c(unit.name, " lost (failed reconnect)"))
|
log.error(util.c(unit.name, " lost (failed reconnect)"))
|
||||||
end
|
end
|
||||||
|
|
||||||
log.info(util.c("reconnected the ", unit.type, " on interface ", unit.name))
|
|
||||||
else
|
else
|
||||||
log.error("failed to get interface of previously connected RTU unit " .. detail_name, true)
|
log.error("failed to get interface of previously connected RTU unit " .. detail_name, true)
|
||||||
end
|
end
|
||||||
@@ -447,8 +501,10 @@ function threads.thread__unit_comms(smem, unit)
|
|||||||
log.fatal(util.strval(result))
|
log.fatal(util.strval(result))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
databus.tx_rt_status("unit_" .. unit.uid, false)
|
||||||
|
|
||||||
if not rtu_state.shutdown then
|
if not rtu_state.shutdown then
|
||||||
log.info(util.c("rtu unit thread ", types.rtu_type_to_string(unit.type), "(", unit.name, " restarting in 5 seconds..."))
|
log.info(util.c("rtu unit thread ", types.rtu_type_to_string(unit.type), " (", unit.name, ") restarting in 5 seconds..."))
|
||||||
util.psleep(5)
|
util.psleep(5)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
-- Communications
|
-- Communications
|
||||||
--
|
--
|
||||||
|
|
||||||
local log = require("scada-common.log")
|
local log = require("scada-common.log")
|
||||||
|
|
||||||
---@class comms
|
---@class comms
|
||||||
local comms = {}
|
local comms = {}
|
||||||
@@ -11,7 +11,7 @@ local insert = table.insert
|
|||||||
|
|
||||||
local max_distance = nil
|
local max_distance = nil
|
||||||
|
|
||||||
comms.version = "1.4.0"
|
comms.version = "1.4.1"
|
||||||
|
|
||||||
---@enum PROTOCOL
|
---@enum PROTOCOL
|
||||||
local PROTOCOL = {
|
local PROTOCOL = {
|
||||||
@@ -74,7 +74,8 @@ local DEVICE_TYPE = {
|
|||||||
PLC = 0, -- PLC device type for establish
|
PLC = 0, -- PLC device type for establish
|
||||||
RTU = 1, -- RTU device type for establish
|
RTU = 1, -- RTU device type for establish
|
||||||
SV = 2, -- supervisor device type for establish
|
SV = 2, -- supervisor device type for establish
|
||||||
CRDN = 3 -- coordinator device type for establish
|
CRDN = 3, -- coordinator device type for establish
|
||||||
|
PKT = 4 -- pocket device type for establish
|
||||||
}
|
}
|
||||||
|
|
||||||
---@enum PLC_AUTO_ACK
|
---@enum PLC_AUTO_ACK
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ end
|
|||||||
|
|
||||||
-- final error print on failed xpcall, app exits here
|
-- final error print on failed xpcall, app exits here
|
||||||
function crash.exit()
|
function crash.exit()
|
||||||
|
log.close()
|
||||||
util.println("fatal error occured in main application:")
|
util.println("fatal error occured in main application:")
|
||||||
error(err, 0)
|
error(err, 0)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
local aes128 = require("lockbox.cipher.aes128")
|
local aes128 = require("lockbox.cipher.aes128")
|
||||||
local ctr_mode = require("lockbox.cipher.mode.ctr")
|
local ctr_mode = require("lockbox.cipher.mode.ctr")
|
||||||
local sha1 = require("lockbox.digest.sha1")
|
local sha1 = require("lockbox.digest.sha1")
|
||||||
local sha2_224 = require("lockbox.digest.sha2_224")
|
|
||||||
local sha2_256 = require("lockbox.digest.sha2_256")
|
local sha2_256 = require("lockbox.digest.sha2_256")
|
||||||
local pbkdf2 = require("lockbox.kdf.pbkdf2")
|
local pbkdf2 = require("lockbox.kdf.pbkdf2")
|
||||||
local hmac = require("lockbox.mac.hmac")
|
local hmac = require("lockbox.mac.hmac")
|
||||||
@@ -157,10 +156,6 @@ end
|
|||||||
-- wrap a modem as a secure modem to send encrypted traffic
|
-- wrap a modem as a secure modem to send encrypted traffic
|
||||||
---@param modem table modem to wrap
|
---@param modem table modem to wrap
|
||||||
function crypto.secure_modem(modem)
|
function crypto.secure_modem(modem)
|
||||||
local self = {
|
|
||||||
modem = modem
|
|
||||||
}
|
|
||||||
|
|
||||||
---@class secure_modem
|
---@class secure_modem
|
||||||
---@field open function
|
---@field open function
|
||||||
---@field isOpen function
|
---@field isOpen function
|
||||||
@@ -177,17 +172,17 @@ function crypto.secure_modem(modem)
|
|||||||
local public = {}
|
local public = {}
|
||||||
|
|
||||||
-- wrap a modem
|
-- wrap a modem
|
||||||
---@param modem table
|
---@param reconnected_modem table
|
||||||
---@diagnostic disable-next-line: redefined-local
|
---@diagnostic disable-next-line: redefined-local
|
||||||
function public.wrap(modem)
|
function public.wrap(reconnected_modem)
|
||||||
self.modem = modem
|
modem = reconnected_modem
|
||||||
for key, func in pairs(self.modem) do
|
for key, func in pairs(modem) do
|
||||||
public[key] = func
|
public[key] = func
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- wrap modem functions, then we replace transmit
|
-- wrap modem functions, then we replace transmit
|
||||||
public.wrap(self.modem)
|
public.wrap(modem)
|
||||||
|
|
||||||
-- send a packet with encryption
|
-- send a packet with encryption
|
||||||
---@param channel integer
|
---@param channel integer
|
||||||
@@ -198,9 +193,9 @@ function crypto.secure_modem(modem)
|
|||||||
|
|
||||||
local iv, ciphertext = crypto.encrypt(plaintext)
|
local iv, ciphertext = crypto.encrypt(plaintext)
|
||||||
---@diagnostic disable-next-line: redefined-local
|
---@diagnostic disable-next-line: redefined-local
|
||||||
local hmac = crypto.hmac(iv .. ciphertext)
|
local computed_hmac = crypto.hmac(iv .. ciphertext)
|
||||||
|
|
||||||
self.modem.transmit(channel, reply_channel, { hmac, iv, ciphertext })
|
modem.transmit(channel, reply_channel, { computed_hmac, iv, ciphertext })
|
||||||
end
|
end
|
||||||
|
|
||||||
-- parse in a modem message as a network packet
|
-- parse in a modem message as a network packet
|
||||||
@@ -217,13 +212,13 @@ function crypto.secure_modem(modem)
|
|||||||
if type(message) == "table" then
|
if type(message) == "table" then
|
||||||
if #message == 3 then
|
if #message == 3 then
|
||||||
---@diagnostic disable-next-line: redefined-local
|
---@diagnostic disable-next-line: redefined-local
|
||||||
local hmac = message[1]
|
local rx_hmac = message[1]
|
||||||
local iv = message[2]
|
local iv = message[2]
|
||||||
local ciphertext = message[3]
|
local ciphertext = message[3]
|
||||||
|
|
||||||
local computed_hmac = crypto.hmac(iv .. ciphertext)
|
local computed_hmac = crypto.hmac(iv .. ciphertext)
|
||||||
|
|
||||||
if hmac == computed_hmac then
|
if rx_hmac == computed_hmac then
|
||||||
-- message intact
|
-- message intact
|
||||||
local plaintext = crypto.decrypt(iv, ciphertext)
|
local plaintext = crypto.decrypt(iv, ciphertext)
|
||||||
body = textutils.unserialize(plaintext)
|
body = textutils.unserialize(plaintext)
|
||||||
|
|||||||
@@ -15,12 +15,10 @@ local MODE = {
|
|||||||
|
|
||||||
log.MODE = MODE
|
log.MODE = MODE
|
||||||
|
|
||||||
-- whether to log debug messages or not
|
local logger = {
|
||||||
local LOG_DEBUG = false
|
|
||||||
|
|
||||||
local log_sys = {
|
|
||||||
path = "/log.txt",
|
path = "/log.txt",
|
||||||
mode = MODE.APPEND,
|
mode = MODE.APPEND,
|
||||||
|
debug = false,
|
||||||
file = nil,
|
file = nil,
|
||||||
dmesg_out = nil
|
dmesg_out = nil
|
||||||
}
|
}
|
||||||
@@ -28,30 +26,9 @@ local log_sys = {
|
|||||||
---@type function
|
---@type function
|
||||||
local free_space = fs.getFreeSpace
|
local free_space = fs.getFreeSpace
|
||||||
|
|
||||||
-- initialize logger
|
-----------------------
|
||||||
---@param path string file path
|
-- PRIVATE FUNCTIONS --
|
||||||
---@param write_mode MODE
|
-----------------------
|
||||||
---@param dmesg_redirect? table terminal/window to direct dmesg to
|
|
||||||
function log.init(path, write_mode, dmesg_redirect)
|
|
||||||
log_sys.path = path
|
|
||||||
log_sys.mode = write_mode
|
|
||||||
|
|
||||||
if log_sys.mode == MODE.APPEND then
|
|
||||||
log_sys.file = fs.open(path, "a")
|
|
||||||
else
|
|
||||||
log_sys.file = fs.open(path, "w")
|
|
||||||
end
|
|
||||||
|
|
||||||
if dmesg_redirect then
|
|
||||||
log_sys.dmesg_out = dmesg_redirect
|
|
||||||
else
|
|
||||||
log_sys.dmesg_out = term.current()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- direct dmesg output to a monitor/window
|
|
||||||
---@param window table window or terminal reference
|
|
||||||
function log.direct_dmesg(window) log_sys.dmesg_out = window end
|
|
||||||
|
|
||||||
-- private log write function
|
-- private log write function
|
||||||
---@param msg string
|
---@param msg string
|
||||||
@@ -62,8 +39,8 @@ local function _log(msg)
|
|||||||
|
|
||||||
-- attempt to write log
|
-- attempt to write log
|
||||||
local status, result = pcall(function ()
|
local status, result = pcall(function ()
|
||||||
log_sys.file.writeLine(stamped)
|
logger.file.writeLine(stamped)
|
||||||
log_sys.file.flush()
|
logger.file.flush()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- if we don't have space, we need to create a new log file
|
-- if we don't have space, we need to create a new log file
|
||||||
@@ -78,21 +55,57 @@ local function _log(msg)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if out_of_space or (free_space(log_sys.path) < 100) then
|
if out_of_space or (free_space(logger.path) < 100) then
|
||||||
-- delete the old log file before opening a new one
|
-- delete the old log file before opening a new one
|
||||||
log_sys.file.close()
|
logger.file.close()
|
||||||
fs.delete(log_sys.path)
|
fs.delete(logger.path)
|
||||||
|
|
||||||
-- re-init logger and pass dmesg_out so that it doesn't change
|
-- re-init logger and pass dmesg_out so that it doesn't change
|
||||||
log.init(log_sys.path, log_sys.mode, log_sys.dmesg_out)
|
log.init(logger.path, logger.mode, logger.debug, logger.dmesg_out)
|
||||||
|
|
||||||
-- leave a message
|
-- leave a message
|
||||||
log_sys.file.writeLine(time_stamp .. "recycled log file")
|
logger.file.writeLine(time_stamp .. "recycled log file")
|
||||||
log_sys.file.writeLine(stamped)
|
logger.file.writeLine(stamped)
|
||||||
log_sys.file.flush()
|
logger.file.flush()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
----------------------
|
||||||
|
-- PUBLIC FUNCTIONS --
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
-- initialize logger
|
||||||
|
---@param path string file path
|
||||||
|
---@param write_mode MODE file write mode
|
||||||
|
---@param include_debug boolean whether or not to include debug logs
|
||||||
|
---@param dmesg_redirect? table terminal/window to direct dmesg to
|
||||||
|
function log.init(path, write_mode, include_debug, dmesg_redirect)
|
||||||
|
logger.path = path
|
||||||
|
logger.mode = write_mode
|
||||||
|
logger.debug = include_debug
|
||||||
|
|
||||||
|
if logger.mode == MODE.APPEND then
|
||||||
|
logger.file = fs.open(path, "a")
|
||||||
|
else
|
||||||
|
logger.file = fs.open(path, "w")
|
||||||
|
end
|
||||||
|
|
||||||
|
if dmesg_redirect then
|
||||||
|
logger.dmesg_out = dmesg_redirect
|
||||||
|
else
|
||||||
|
logger.dmesg_out = term.current()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- close the log file handle
|
||||||
|
function log.close()
|
||||||
|
logger.file.close()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- direct dmesg output to a monitor/window
|
||||||
|
---@param window table window or terminal reference
|
||||||
|
function log.direct_dmesg(window) logger.dmesg_out = window end
|
||||||
|
|
||||||
-- dmesg style logging for boot because I like linux-y things
|
-- dmesg style logging for boot because I like linux-y things
|
||||||
---@param msg string message
|
---@param msg string message
|
||||||
---@param tag? string log tag
|
---@param tag? string log tag
|
||||||
@@ -107,7 +120,7 @@ function log.dmesg(msg, tag, tag_color)
|
|||||||
tag = util.strval(tag)
|
tag = util.strval(tag)
|
||||||
|
|
||||||
local t_stamp = string.format("%12.2f", os.clock())
|
local t_stamp = string.format("%12.2f", os.clock())
|
||||||
local out = log_sys.dmesg_out
|
local out = logger.dmesg_out
|
||||||
|
|
||||||
if out ~= nil then
|
if out ~= nil then
|
||||||
local out_w, out_h = out.getSize()
|
local out_w, out_h = out.getSize()
|
||||||
@@ -203,7 +216,7 @@ end
|
|||||||
function log.dmesg_working(msg, tag, tag_color)
|
function log.dmesg_working(msg, tag, tag_color)
|
||||||
local ts_coord = log.dmesg(msg, tag, tag_color)
|
local ts_coord = log.dmesg(msg, tag, tag_color)
|
||||||
|
|
||||||
local out = log_sys.dmesg_out
|
local out = logger.dmesg_out
|
||||||
local width = (ts_coord.x2 - ts_coord.x1) + 1
|
local width = (ts_coord.x2 - ts_coord.x1) + 1
|
||||||
|
|
||||||
if out ~= nil then
|
if out ~= nil then
|
||||||
@@ -262,7 +275,7 @@ end
|
|||||||
---@param msg string message
|
---@param msg string message
|
||||||
---@param trace? boolean include file trace
|
---@param trace? boolean include file trace
|
||||||
function log.debug(msg, trace)
|
function log.debug(msg, trace)
|
||||||
if LOG_DEBUG then
|
if logger.debug then
|
||||||
local dbg_info = ""
|
local dbg_info = ""
|
||||||
|
|
||||||
if trace then
|
if trace then
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
-- Publisher-Subscriber Interconnect Layer
|
-- Publisher-Subscriber Interconnect Layer
|
||||||
--
|
--
|
||||||
|
|
||||||
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
local psil = {}
|
local psil = {}
|
||||||
|
|
||||||
-- instantiate a new PSI layer
|
-- instantiate a new PSI layer
|
||||||
@@ -36,6 +38,15 @@ function psil.create()
|
|||||||
table.insert(self.ic[key].subscribers, { notify = func })
|
table.insert(self.ic[key].subscribers, { notify = func })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- unsubscribe a function from a given key
|
||||||
|
---@param key string data key
|
||||||
|
---@param func function function to unsubscribe
|
||||||
|
function public.unsubscribe(key, func)
|
||||||
|
if self.ic[key] ~= nil then
|
||||||
|
util.filter_table(self.ic[key].subscribers, function (s) return s.notify ~= func end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- publish data to a given key, passing it to all subscribers if it has changed
|
-- publish data to a given key, passing it to all subscribers if it has changed
|
||||||
---@param key string data key
|
---@param key string data key
|
||||||
---@param value any data value
|
---@param value any data value
|
||||||
@@ -64,6 +75,9 @@ function psil.create()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- clear the contents of the interconnect
|
||||||
|
function public.purge() self.ic = nil end
|
||||||
|
|
||||||
return public
|
return public
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ function types.new_radiation_reading(r, u) return { radiation = r, unit = u } en
|
|||||||
---@return radiation_reading
|
---@return radiation_reading
|
||||||
function types.new_zero_radiation_reading() return { radiation = 0, unit = "nSv" } end
|
function types.new_zero_radiation_reading() return { radiation = 0, unit = "nSv" } end
|
||||||
|
|
||||||
|
---@class coordinate_2d
|
||||||
|
---@field x integer
|
||||||
|
---@field y integer
|
||||||
|
|
||||||
---@class coordinate
|
---@class coordinate
|
||||||
---@field x integer
|
---@field x integer
|
||||||
---@field y integer
|
---@field y integer
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
-- Utility Functions
|
-- Utility Functions
|
||||||
--
|
--
|
||||||
|
|
||||||
|
local cc_strings = require("cc.strings")
|
||||||
|
|
||||||
---@class util
|
---@class util
|
||||||
local util = {}
|
local util = {}
|
||||||
|
|
||||||
@@ -104,58 +106,20 @@ function util.pad(str, n)
|
|||||||
return util.spaces(lpad) .. str .. util.spaces(rpad)
|
return util.spaces(lpad) .. str .. util.spaces(rpad)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- wrap a string into a table of lines, supporting single dash splits
|
-- wrap a string into a table of lines
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param str string
|
---@param str string
|
||||||
---@param limit integer line limit
|
---@param limit integer line limit
|
||||||
---@return table lines
|
---@return table lines
|
||||||
function util.strwrap(str, limit)
|
function util.strwrap(str, limit) return cc_strings.wrap(str, limit) end
|
||||||
local lines = {}
|
|
||||||
local ln_start = 1
|
|
||||||
|
|
||||||
local first_break = str:find("([%-%s]+)")
|
-- luacheck: no unused args
|
||||||
|
|
||||||
if first_break ~= nil then
|
|
||||||
lines[1] = string.sub(str, 1, first_break - 1)
|
|
||||||
else
|
|
||||||
lines[1] = str
|
|
||||||
end
|
|
||||||
|
|
||||||
---@diagnostic disable-next-line: discard-returns
|
|
||||||
str:gsub("(%s+)()(%S+)()",
|
|
||||||
function(space, start, word, stop)
|
|
||||||
-- support splitting SINGLE DASH words
|
|
||||||
word:gsub("(%S+)(%-)()(%S+)()",
|
|
||||||
function (pre, dash, d_start, post, d_stop)
|
|
||||||
if (stop + d_stop) - ln_start <= limit then
|
|
||||||
-- do nothing, it will entirely fit
|
|
||||||
elseif ((start + d_start) + 1) - ln_start <= limit then
|
|
||||||
-- we can fit including the dash
|
|
||||||
lines[#lines] = lines[#lines] .. space .. pre .. dash
|
|
||||||
-- drop the space and replace the word with the post
|
|
||||||
space = ""
|
|
||||||
word = post
|
|
||||||
-- force a wrap
|
|
||||||
stop = limit + 1 + ln_start
|
|
||||||
-- change start position for new line start
|
|
||||||
start = start + d_start - 1
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
-- can we append this or do we have to start a new line?
|
|
||||||
if stop - ln_start > limit then
|
|
||||||
-- starting new line
|
|
||||||
ln_start = start
|
|
||||||
lines[#lines + 1] = word
|
|
||||||
else lines[#lines] = lines[#lines] .. space .. word end
|
|
||||||
end)
|
|
||||||
|
|
||||||
return lines
|
|
||||||
end
|
|
||||||
|
|
||||||
-- concatenation with built-in to string
|
-- concatenation with built-in to string
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@vararg any
|
---@vararg any
|
||||||
---@return string
|
---@return string
|
||||||
|
---@diagnostic disable-next-line: unused-vararg
|
||||||
function util.concat(...)
|
function util.concat(...)
|
||||||
local str = ""
|
local str = ""
|
||||||
for _, v in ipairs(arg) do str = str .. util.strval(v) end
|
for _, v in ipairs(arg) do str = str .. util.strval(v) end
|
||||||
@@ -169,10 +133,13 @@ util.c = util.concat
|
|||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param format string
|
---@param format string
|
||||||
---@vararg any
|
---@vararg any
|
||||||
|
---@diagnostic disable-next-line: unused-vararg
|
||||||
function util.sprintf(format, ...)
|
function util.sprintf(format, ...)
|
||||||
return string.format(format, table.unpack(arg))
|
return string.format(format, table.unpack(arg))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- luacheck: unused args
|
||||||
|
|
||||||
-- format a number string with commas as the thousands separator<br>
|
-- format a number string with commas as the thousands separator<br>
|
||||||
-- subtracts from spaces at the start if present for each comma used
|
-- subtracts from spaces at the start if present for each comma used
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ local println_ts = util.println_ts
|
|||||||
|
|
||||||
println("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION)
|
println("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION)
|
||||||
|
|
||||||
local exit_code = false
|
local exit_code ---@type boolean
|
||||||
|
|
||||||
println_ts("BOOT> SCANNING FOR APPLICATIONS...")
|
println_ts("BOOT> SCANNING FOR APPLICATIONS...")
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ local config = {}
|
|||||||
|
|
||||||
-- scada network listen for PLC's and RTU's
|
-- scada network listen for PLC's and RTU's
|
||||||
config.SCADA_DEV_LISTEN = 16000
|
config.SCADA_DEV_LISTEN = 16000
|
||||||
-- listen port for SCADA supervisor access by coordinators
|
-- listen port for SCADA supervisor access
|
||||||
config.SCADA_SV_LISTEN = 16100
|
config.SCADA_SV_CTL_LISTEN = 16100
|
||||||
-- 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
|
||||||
config.PLC_TIMEOUT = 5
|
config.PLC_TIMEOUT = 5
|
||||||
config.RTU_TIMEOUT = 5
|
config.RTU_TIMEOUT = 5
|
||||||
config.CRD_TIMEOUT = 5
|
config.CRD_TIMEOUT = 5
|
||||||
|
config.PKT_TIMEOUT = 5
|
||||||
|
|
||||||
-- expected number of reactors
|
-- expected number of reactors
|
||||||
config.NUM_REACTORS = 4
|
config.NUM_REACTORS = 4
|
||||||
@@ -27,5 +28,7 @@ config.LOG_PATH = "/log.txt"
|
|||||||
-- 0 = APPEND (adds to existing file on start)
|
-- 0 = APPEND (adds to existing file on start)
|
||||||
-- 1 = NEW (replaces existing file on start)
|
-- 1 = NEW (replaces existing file on start)
|
||||||
config.LOG_MODE = 0
|
config.LOG_MODE = 0
|
||||||
|
-- true to log verbose debug messages
|
||||||
|
config.LOG_DEBUG = false
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -16,16 +16,12 @@ local FAC_COMMAND = comms.FAC_COMMAND
|
|||||||
|
|
||||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||||
|
|
||||||
local SV_Q_CMDS = svqtypes.SV_Q_CMDS
|
|
||||||
local SV_Q_DATA = svqtypes.SV_Q_DATA
|
local SV_Q_DATA = svqtypes.SV_Q_DATA
|
||||||
|
|
||||||
local print = util.print
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local print_ts = util.print_ts
|
|
||||||
local println_ts = util.println_ts
|
|
||||||
|
|
||||||
-- 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
|
||||||
local PARTIAL_RETRY_PERIOD = 2000
|
local PARTIAL_RETRY_PERIOD = 2000
|
||||||
|
|
||||||
@@ -177,12 +173,12 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- handle a packet
|
-- handle a packet
|
||||||
---@param pkt crdn_frame
|
---@param pkt mgmt_frame|crdn_frame
|
||||||
local function _handle_packet(pkt)
|
local function _handle_packet(pkt)
|
||||||
-- check sequence number
|
-- check sequence number
|
||||||
if self.r_seq_num == nil then
|
if self.r_seq_num == nil then
|
||||||
self.r_seq_num = pkt.scada_frame.seq_num()
|
self.r_seq_num = pkt.scada_frame.seq_num()
|
||||||
elseif self.r_seq_num >= pkt.scada_frame.seq_num() then
|
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
|
||||||
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||||
return
|
return
|
||||||
else
|
else
|
||||||
@@ -194,11 +190,12 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
|||||||
|
|
||||||
-- process packet
|
-- process packet
|
||||||
if pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
if pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||||
|
---@cast pkt mgmt_frame
|
||||||
if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||||
-- 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 coord_send = pkt.data[2]
|
-- local coord_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
|
||||||
|
|
||||||
@@ -218,6 +215,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
|||||||
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||||
end
|
end
|
||||||
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
|
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
|
||||||
|
---@cast pkt crdn_frame
|
||||||
if pkt.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then
|
if pkt.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then
|
||||||
-- acknowledgement to coordinator receiving builds
|
-- acknowledgement to coordinator receiving builds
|
||||||
self.acks.builds = true
|
self.acks.builds = true
|
||||||
@@ -414,7 +412,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
|||||||
_send(SCADA_CRDN_TYPE.FAC_BUILDS, { facility.get_build(cmd.val.type == RTU_UNIT_TYPE.IMATRIX) })
|
_send(SCADA_CRDN_TYPE.FAC_BUILDS, { facility.get_build(cmd.val.type == RTU_UNIT_TYPE.IMATRIX) })
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
log.warning(log_header .. "unsupported data command received in in_queue (this is a bug)")
|
log.error(log_header .. "unsupported data command received in in_queue (this is a bug)", true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ 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 print = util.print
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local print_ts = util.print_ts
|
|
||||||
local println_ts = util.println_ts
|
|
||||||
|
|
||||||
-- retry time constants in ms
|
-- retry time constants in ms
|
||||||
local INITIAL_WAIT = 1500
|
local INITIAL_WAIT = 1500
|
||||||
@@ -67,7 +64,7 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
|
|||||||
connected = true,
|
connected = true,
|
||||||
received_struct = false,
|
received_struct = false,
|
||||||
received_status_cache = false,
|
received_status_cache = false,
|
||||||
plc_conn_watchdog = util.new_watchdog(timeout),
|
conn_watchdog = util.new_watchdog(timeout),
|
||||||
last_rtt = 0,
|
last_rtt = 0,
|
||||||
-- periodic messages
|
-- periodic messages
|
||||||
periodics = {
|
periodics = {
|
||||||
@@ -236,7 +233,7 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
|
|||||||
|
|
||||||
-- mark this PLC session as closed, stop watchdog
|
-- mark this PLC session as closed, stop watchdog
|
||||||
local function _close()
|
local function _close()
|
||||||
self.plc_conn_watchdog.cancel()
|
self.conn_watchdog.cancel()
|
||||||
self.connected = false
|
self.connected = false
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -276,18 +273,18 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
|
|||||||
if pkt.length == 1 then
|
if pkt.length == 1 then
|
||||||
return pkt.data[1]
|
return pkt.data[1]
|
||||||
else
|
else
|
||||||
log.warning(log_header .. "RPLC ACK length mismatch")
|
log.debug(log_header .. "RPLC ACK length mismatch")
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- handle a packet
|
-- handle a packet
|
||||||
---@param pkt rplc_frame
|
---@param pkt mgmt_frame|rplc_frame
|
||||||
local function _handle_packet(pkt)
|
local function _handle_packet(pkt)
|
||||||
-- check sequence number
|
-- check sequence number
|
||||||
if self.r_seq_num == nil then
|
if self.r_seq_num == nil then
|
||||||
self.r_seq_num = pkt.scada_frame.seq_num()
|
self.r_seq_num = pkt.scada_frame.seq_num()
|
||||||
elseif self.r_seq_num >= pkt.scada_frame.seq_num() then
|
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
|
||||||
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||||
return
|
return
|
||||||
else
|
else
|
||||||
@@ -296,14 +293,15 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
|
|||||||
|
|
||||||
-- process packet
|
-- process packet
|
||||||
if pkt.scada_frame.protocol() == PROTOCOL.RPLC then
|
if pkt.scada_frame.protocol() == PROTOCOL.RPLC then
|
||||||
|
---@cast pkt rplc_frame
|
||||||
-- check reactor ID
|
-- check reactor ID
|
||||||
if pkt.id ~= reactor_id then
|
if pkt.id ~= reactor_id then
|
||||||
log.warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. reactor_id .. " != " .. pkt.id)
|
log.warning(log_header .. "discarding RPLC packet with ID not matching reactor ID: reactor " .. reactor_id .. " != " .. pkt.id)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- feed watchdog
|
-- feed watchdog
|
||||||
self.plc_conn_watchdog.feed()
|
self.conn_watchdog.feed()
|
||||||
|
|
||||||
-- handle packet by type
|
-- handle packet by type
|
||||||
if pkt.type == RPLC_TYPE.STATUS then
|
if pkt.type == RPLC_TYPE.STATUS then
|
||||||
@@ -472,11 +470,12 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
|
|||||||
log.debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type)
|
log.debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type)
|
||||||
end
|
end
|
||||||
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||||
|
---@cast pkt mgmt_frame
|
||||||
if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||||
-- 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 plc_send = pkt.data[2]
|
-- local plc_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
|
||||||
|
|
||||||
@@ -577,7 +576,7 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
|
|||||||
-- check if a timer matches this session's watchdog
|
-- check if a timer matches this session's watchdog
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
function public.check_wd(timer)
|
function public.check_wd(timer)
|
||||||
return self.plc_conn_watchdog.is_timer(timer) and self.connected
|
return self.conn_watchdog.is_timer(timer) and self.connected
|
||||||
end
|
end
|
||||||
|
|
||||||
-- close the connection
|
-- close the connection
|
||||||
@@ -636,7 +635,7 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
|
|||||||
_send(RPLC_TYPE.RPS_AUTO_RESET, {})
|
_send(RPLC_TYPE.RPS_AUTO_RESET, {})
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
log.warning(log_header .. "unsupported command received in in_queue (this is a bug)")
|
log.error(log_header .. "unsupported command received in in_queue (this is a bug)", true)
|
||||||
end
|
end
|
||||||
elseif message.qtype == mqueue.TYPE.DATA then
|
elseif message.qtype == mqueue.TYPE.DATA then
|
||||||
-- instruction with body
|
-- instruction with body
|
||||||
@@ -683,7 +682,7 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
log.warning(log_header .. "unsupported data command received in in_queue (this is a bug)")
|
log.error(log_header .. "unsupported data command received in in_queue (this is a bug)", true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
226
supervisor/session/pocket.lua
Normal file
226
supervisor/session/pocket.lua
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
local comms = require("scada-common.comms")
|
||||||
|
local log = require("scada-common.log")
|
||||||
|
local mqueue = require("scada-common.mqueue")
|
||||||
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
|
local pocket = {}
|
||||||
|
|
||||||
|
local PROTOCOL = comms.PROTOCOL
|
||||||
|
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||||
|
|
||||||
|
local println = util.println
|
||||||
|
|
||||||
|
-- retry time constants in ms
|
||||||
|
-- local INITIAL_WAIT = 1500
|
||||||
|
-- local RETRY_PERIOD = 1000
|
||||||
|
|
||||||
|
local POCKET_S_CMDS = {
|
||||||
|
}
|
||||||
|
|
||||||
|
local POCKET_S_DATA = {
|
||||||
|
}
|
||||||
|
|
||||||
|
pocket.POCKET_S_CMDS = POCKET_S_CMDS
|
||||||
|
pocket.POCKET_S_DATA = POCKET_S_DATA
|
||||||
|
|
||||||
|
local PERIODICS = {
|
||||||
|
KEEP_ALIVE = 2000
|
||||||
|
}
|
||||||
|
|
||||||
|
-- pocket diagnostics session
|
||||||
|
---@nodiscard
|
||||||
|
---@param id integer session ID
|
||||||
|
---@param in_queue mqueue in message queue
|
||||||
|
---@param out_queue mqueue out message queue
|
||||||
|
---@param timeout number communications timeout
|
||||||
|
function pocket.new_session(id, in_queue, out_queue, timeout)
|
||||||
|
local log_header = "diag_session(" .. id .. "): "
|
||||||
|
|
||||||
|
local self = {
|
||||||
|
-- connection properties
|
||||||
|
seq_num = 0,
|
||||||
|
r_seq_num = nil,
|
||||||
|
connected = true,
|
||||||
|
conn_watchdog = util.new_watchdog(timeout),
|
||||||
|
last_rtt = 0,
|
||||||
|
-- periodic messages
|
||||||
|
periodics = {
|
||||||
|
last_update = 0,
|
||||||
|
keep_alive = 0
|
||||||
|
},
|
||||||
|
-- when to next retry one of these requests
|
||||||
|
retry_times = {
|
||||||
|
},
|
||||||
|
-- command acknowledgements
|
||||||
|
acks = {
|
||||||
|
},
|
||||||
|
-- session database
|
||||||
|
---@class diag_db
|
||||||
|
sDB = {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
---@class diag_session
|
||||||
|
local public = {}
|
||||||
|
|
||||||
|
-- mark this diagnostics session as closed, stop watchdog
|
||||||
|
local function _close()
|
||||||
|
self.conn_watchdog.cancel()
|
||||||
|
self.connected = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- send a SCADA management packet
|
||||||
|
---@param msg_type SCADA_MGMT_TYPE
|
||||||
|
---@param msg table
|
||||||
|
local function _send_mgmt(msg_type, msg)
|
||||||
|
local s_pkt = comms.scada_packet()
|
||||||
|
local m_pkt = comms.mgmt_packet()
|
||||||
|
|
||||||
|
m_pkt.make(msg_type, msg)
|
||||||
|
s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||||
|
|
||||||
|
out_queue.push_packet(s_pkt)
|
||||||
|
self.seq_num = self.seq_num + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- handle a packet
|
||||||
|
---@param pkt mgmt_frame
|
||||||
|
local function _handle_packet(pkt)
|
||||||
|
-- check sequence number
|
||||||
|
if self.r_seq_num == nil then
|
||||||
|
self.r_seq_num = pkt.scada_frame.seq_num()
|
||||||
|
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
|
||||||
|
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||||
|
return
|
||||||
|
else
|
||||||
|
self.r_seq_num = pkt.scada_frame.seq_num()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- feed watchdog
|
||||||
|
self.conn_watchdog.feed()
|
||||||
|
|
||||||
|
-- process packet
|
||||||
|
if pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||||
|
---@cast pkt mgmt_frame
|
||||||
|
if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||||
|
-- keep alive reply
|
||||||
|
if pkt.length == 2 then
|
||||||
|
local srv_start = pkt.data[1]
|
||||||
|
-- local diag_send = pkt.data[2]
|
||||||
|
local srv_now = util.time()
|
||||||
|
self.last_rtt = srv_now - srv_start
|
||||||
|
|
||||||
|
if self.last_rtt > 750 then
|
||||||
|
log.warning(log_header .. "DIAG KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- log.debug(log_header .. "DIAG RTT = " .. self.last_rtt .. "ms")
|
||||||
|
-- log.debug(log_header .. "DIAG TT = " .. (srv_now - diag_send) .. "ms")
|
||||||
|
else
|
||||||
|
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||||
|
end
|
||||||
|
elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then
|
||||||
|
-- close the session
|
||||||
|
_close()
|
||||||
|
else
|
||||||
|
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- PUBLIC FUNCTIONS --
|
||||||
|
|
||||||
|
-- get the session ID
|
||||||
|
---@nodiscard
|
||||||
|
function public.get_id() return id end
|
||||||
|
|
||||||
|
-- get the session database
|
||||||
|
---@nodiscard
|
||||||
|
function public.get_db() return self.sDB end
|
||||||
|
|
||||||
|
-- check if a timer matches this session's watchdog
|
||||||
|
---@nodiscard
|
||||||
|
function public.check_wd(timer)
|
||||||
|
return self.conn_watchdog.is_timer(timer) and self.connected
|
||||||
|
end
|
||||||
|
|
||||||
|
-- close the connection
|
||||||
|
function public.close()
|
||||||
|
_close()
|
||||||
|
_send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
|
||||||
|
println("connection to pocket diag session " .. id .. " closed by server")
|
||||||
|
log.info(log_header .. "session closed by server")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- iterate the session
|
||||||
|
---@nodiscard
|
||||||
|
---@return boolean connected
|
||||||
|
function public.iterate()
|
||||||
|
if self.connected then
|
||||||
|
------------------
|
||||||
|
-- handle queue --
|
||||||
|
------------------
|
||||||
|
|
||||||
|
local handle_start = util.time()
|
||||||
|
|
||||||
|
while in_queue.ready() and self.connected do
|
||||||
|
-- get a new message to process
|
||||||
|
local message = in_queue.pop()
|
||||||
|
|
||||||
|
if message ~= nil then
|
||||||
|
if message.qtype == mqueue.TYPE.PACKET then
|
||||||
|
-- handle a packet
|
||||||
|
_handle_packet(message.message)
|
||||||
|
elseif message.qtype == mqueue.TYPE.COMMAND then
|
||||||
|
-- handle instruction
|
||||||
|
elseif message.qtype == mqueue.TYPE.DATA then
|
||||||
|
-- instruction with body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- max 100ms spent processing queue
|
||||||
|
if util.time() - handle_start > 100 then
|
||||||
|
log.warning(log_header .. "exceeded 100ms queue process limit")
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- exit if connection was closed
|
||||||
|
if not self.connected then
|
||||||
|
println("connection to pocket diag session " .. id .. " closed by remote host")
|
||||||
|
log.info(log_header .. "session closed by remote host")
|
||||||
|
return self.connected
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------
|
||||||
|
-- update periodics --
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
local elapsed = util.time() - self.periodics.last_update
|
||||||
|
|
||||||
|
local periodics = self.periodics
|
||||||
|
|
||||||
|
-- keep alive
|
||||||
|
|
||||||
|
periodics.keep_alive = periodics.keep_alive + elapsed
|
||||||
|
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
|
||||||
|
_send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() })
|
||||||
|
periodics.keep_alive = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
self.periodics.last_update = util.time()
|
||||||
|
|
||||||
|
---------------------
|
||||||
|
-- attempt retries --
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
-- local rtimes = self.retry_times
|
||||||
|
end
|
||||||
|
|
||||||
|
return self.connected
|
||||||
|
end
|
||||||
|
|
||||||
|
return public
|
||||||
|
end
|
||||||
|
|
||||||
|
return pocket
|
||||||
@@ -22,10 +22,7 @@ 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 print = util.print
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local print_ts = util.print_ts
|
|
||||||
local println_ts = util.println_ts
|
|
||||||
|
|
||||||
local PERIODICS = {
|
local PERIODICS = {
|
||||||
KEEP_ALIVE = 2000
|
KEEP_ALIVE = 2000
|
||||||
@@ -50,7 +47,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
|||||||
seq_num = 0,
|
seq_num = 0,
|
||||||
r_seq_num = nil,
|
r_seq_num = nil,
|
||||||
connected = true,
|
connected = true,
|
||||||
rtu_conn_watchdog = util.new_watchdog(timeout),
|
conn_watchdog = util.new_watchdog(timeout),
|
||||||
last_rtt = 0,
|
last_rtt = 0,
|
||||||
-- periodic messages
|
-- periodic messages
|
||||||
periodics = {
|
periodics = {
|
||||||
@@ -78,9 +75,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
|||||||
end
|
end
|
||||||
|
|
||||||
for i = 1, #self.advert do
|
for i = 1, #self.advert do
|
||||||
local unit = nil ---@type unit_session|nil
|
local unit = nil ---@type unit_session|nil
|
||||||
local rs_in_q = nil ---@type mqueue|nil
|
|
||||||
local tbv_in_q = nil ---@type mqueue|nil
|
|
||||||
|
|
||||||
---@type rtu_advertisement
|
---@type rtu_advertisement
|
||||||
local unit_advert = {
|
local unit_advert = {
|
||||||
@@ -123,26 +118,31 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
|||||||
if unit_advert.reactor > 0 then
|
if unit_advert.reactor > 0 then
|
||||||
local target_unit = self.fac_units[unit_advert.reactor] ---@type reactor_unit
|
local target_unit = self.fac_units[unit_advert.reactor] ---@type reactor_unit
|
||||||
|
|
||||||
|
-- unit RTUs
|
||||||
if u_type == RTU_UNIT_TYPE.REDSTONE then
|
if u_type == RTU_UNIT_TYPE.REDSTONE then
|
||||||
-- redstone
|
-- redstone
|
||||||
unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q)
|
unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q)
|
||||||
if type(unit) ~= "nil" then target_unit.add_redstone(unit) end
|
if type(unit) ~= "nil" then target_unit.add_redstone(unit) end
|
||||||
elseif u_type == RTU_UNIT_TYPE.BOILER_VALVE then
|
elseif u_type == RTU_UNIT_TYPE.BOILER_VALVE then
|
||||||
-- boiler (Mekanism 10.1+)
|
-- boiler
|
||||||
unit = svrs_boilerv.new(id, i, unit_advert, self.modbus_q)
|
unit = svrs_boilerv.new(id, i, unit_advert, self.modbus_q)
|
||||||
if type(unit) ~= "nil" then target_unit.add_boiler(unit) end
|
if type(unit) ~= "nil" then target_unit.add_boiler(unit) end
|
||||||
elseif u_type == RTU_UNIT_TYPE.TURBINE_VALVE then
|
elseif u_type == RTU_UNIT_TYPE.TURBINE_VALVE then
|
||||||
-- turbine (Mekanism 10.1+)
|
-- turbine
|
||||||
unit = svrs_turbinev.new(id, i, unit_advert, self.modbus_q)
|
unit = svrs_turbinev.new(id, i, unit_advert, self.modbus_q)
|
||||||
if type(unit) ~= "nil" then target_unit.add_turbine(unit) end
|
if type(unit) ~= "nil" then target_unit.add_turbine(unit) end
|
||||||
elseif u_type == RTU_UNIT_TYPE.ENV_DETECTOR then
|
elseif u_type == RTU_UNIT_TYPE.ENV_DETECTOR then
|
||||||
-- environment detector
|
-- environment detector
|
||||||
unit = svrs_envd.new(id, i, unit_advert, self.modbus_q)
|
unit = svrs_envd.new(id, i, unit_advert, self.modbus_q)
|
||||||
if type(unit) ~= "nil" then target_unit.add_envd(unit) end
|
if type(unit) ~= "nil" then target_unit.add_envd(unit) end
|
||||||
|
elseif u_type == RTU_UNIT_TYPE.VIRTUAL then
|
||||||
|
-- skip virtual units
|
||||||
|
log.debug(util.c(log_header, "skipping virtual RTU unit #", i))
|
||||||
else
|
else
|
||||||
log.error(util.c(log_header, "bad advertisement: encountered unsupported reactor-specific RTU type ", type_string))
|
log.error(util.c(log_header, "bad advertisement: encountered unsupported reactor-specific RTU type ", type_string))
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
-- facility RTUs
|
||||||
if u_type == RTU_UNIT_TYPE.REDSTONE then
|
if u_type == RTU_UNIT_TYPE.REDSTONE then
|
||||||
-- redstone
|
-- redstone
|
||||||
unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q)
|
unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q)
|
||||||
@@ -161,6 +161,9 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
|||||||
-- environment detector
|
-- environment detector
|
||||||
unit = svrs_envd.new(id, i, unit_advert, self.modbus_q)
|
unit = svrs_envd.new(id, i, unit_advert, self.modbus_q)
|
||||||
if type(unit) ~= "nil" then facility.add_envd(unit) end
|
if type(unit) ~= "nil" then facility.add_envd(unit) end
|
||||||
|
elseif u_type == RTU_UNIT_TYPE.VIRTUAL then
|
||||||
|
-- skip virtual units
|
||||||
|
log.debug(util.c(log_header, "skipping virtual RTU unit #", i))
|
||||||
else
|
else
|
||||||
log.error(util.c(log_header, "bad advertisement: encountered unsupported reactor-independent RTU type ", type_string))
|
log.error(util.c(log_header, "bad advertisement: encountered unsupported reactor-independent RTU type ", type_string))
|
||||||
end
|
end
|
||||||
@@ -168,8 +171,8 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
|||||||
end
|
end
|
||||||
|
|
||||||
if unit ~= nil then
|
if unit ~= nil then
|
||||||
table.insert(self.units, unit)
|
self.units[i] = unit
|
||||||
else
|
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
|
||||||
@@ -179,13 +182,11 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
|||||||
|
|
||||||
-- mark this RTU session as closed, stop watchdog
|
-- mark this RTU session as closed, stop watchdog
|
||||||
local function _close()
|
local function _close()
|
||||||
self.rtu_conn_watchdog.cancel()
|
self.conn_watchdog.cancel()
|
||||||
self.connected = false
|
self.connected = false
|
||||||
|
|
||||||
-- 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 i = 1, #self.units do
|
for _, unit in pairs(self.units) do unit.close() end
|
||||||
self.units[i].close()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- send a MODBUS packet
|
-- send a MODBUS packet
|
||||||
@@ -219,7 +220,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
|||||||
-- check sequence number
|
-- check sequence number
|
||||||
if self.r_seq_num == nil then
|
if self.r_seq_num == nil then
|
||||||
self.r_seq_num = pkt.scada_frame.seq_num()
|
self.r_seq_num = pkt.scada_frame.seq_num()
|
||||||
elseif self.r_seq_num >= pkt.scada_frame.seq_num() then
|
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
|
||||||
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||||
return
|
return
|
||||||
else
|
else
|
||||||
@@ -227,22 +228,23 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- feed watchdog
|
-- feed watchdog
|
||||||
self.rtu_conn_watchdog.feed()
|
self.conn_watchdog.feed()
|
||||||
|
|
||||||
-- process packet
|
-- process packet
|
||||||
if pkt.scada_frame.protocol() == PROTOCOL.MODBUS_TCP then
|
if pkt.scada_frame.protocol() == PROTOCOL.MODBUS_TCP then
|
||||||
|
---@cast pkt modbus_frame
|
||||||
if self.units[pkt.unit_id] ~= nil then
|
if self.units[pkt.unit_id] ~= nil then
|
||||||
local unit = self.units[pkt.unit_id] ---@type unit_session
|
local unit = self.units[pkt.unit_id] ---@type unit_session
|
||||||
---@diagnostic disable-next-line: param-type-mismatch
|
|
||||||
unit.handle_packet(pkt)
|
unit.handle_packet(pkt)
|
||||||
end
|
end
|
||||||
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||||
|
---@cast pkt mgmt_frame
|
||||||
-- handle management packet
|
-- handle management packet
|
||||||
if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||||
-- 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 rtu_send = pkt.data[2]
|
-- local rtu_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
|
||||||
|
|
||||||
@@ -290,7 +292,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
|||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param timer number
|
---@param timer number
|
||||||
function public.check_wd(timer)
|
function public.check_wd(timer)
|
||||||
return self.rtu_conn_watchdog.is_timer(timer) and self.connected
|
return self.conn_watchdog.is_timer(timer) and self.connected
|
||||||
end
|
end
|
||||||
|
|
||||||
-- close the connection
|
-- close the connection
|
||||||
@@ -347,9 +349,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
|||||||
|
|
||||||
local time_now = util.time()
|
local time_now = util.time()
|
||||||
|
|
||||||
for i = 1, #self.units do
|
for _, unit in pairs(self.units) do unit.update(time_now) end
|
||||||
self.units[i].update(time_now)
|
|
||||||
end
|
|
||||||
|
|
||||||
----------------------
|
----------------------
|
||||||
-- update periodics --
|
-- update periodics --
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ local MODBUS_FCODE = types.MODBUS_FCODE
|
|||||||
|
|
||||||
local IO_PORT = rsio.IO
|
local IO_PORT = rsio.IO
|
||||||
local IO_LVL = rsio.IO_LVL
|
local IO_LVL = rsio.IO_LVL
|
||||||
local IO_DIR = rsio.IO_DIR
|
|
||||||
local IO_MODE = rsio.IO_MODE
|
local IO_MODE = rsio.IO_MODE
|
||||||
|
|
||||||
local TXN_READY = -1
|
local TXN_READY = -1
|
||||||
@@ -121,8 +120,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
|||||||
local io_f = {
|
local io_f = {
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[port].phy) end,
|
read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[port].phy) end,
|
||||||
---@param active boolean
|
write = function () end
|
||||||
write = function (active) end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.db.io[port] = io_f
|
self.db.io[port] = io_f
|
||||||
@@ -155,8 +153,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
|||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@return integer
|
---@return integer
|
||||||
read = function () return self.phy_io.analog_in[port].phy end,
|
read = function () return self.phy_io.analog_in[port].phy end,
|
||||||
---@param value integer
|
write = function () end
|
||||||
write = function (value) end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.db.io[port] = io_f
|
self.db.io[port] = io_f
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
|
|||||||
|
|
||||||
-- PUBLIC TEMPLATE FUNCTIONS --
|
-- PUBLIC TEMPLATE FUNCTIONS --
|
||||||
|
|
||||||
|
-- luacheck: no unused args
|
||||||
|
|
||||||
-- handle a packet
|
-- handle a packet
|
||||||
---@param m_pkt modbus_frame
|
---@param m_pkt modbus_frame
|
||||||
---@diagnostic disable-next-line: unused-local
|
---@diagnostic disable-next-line: unused-local
|
||||||
@@ -180,6 +182,8 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
|
|||||||
log.debug("template unit_session.update() called", true)
|
log.debug("template unit_session.update() called", true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- luacheck: unused args
|
||||||
|
|
||||||
-- invalidate build cache
|
-- invalidate build cache
|
||||||
function public.invalidate_cache()
|
function public.invalidate_cache()
|
||||||
log.debug("template unit_session.invalidate_cache() called", true)
|
log.debug("template unit_session.invalidate_cache() called", true)
|
||||||
|
|||||||
@@ -9,44 +9,42 @@ local svqtypes = require("supervisor.session.svqtypes")
|
|||||||
|
|
||||||
local coordinator = require("supervisor.session.coordinator")
|
local coordinator = require("supervisor.session.coordinator")
|
||||||
local plc = require("supervisor.session.plc")
|
local plc = require("supervisor.session.plc")
|
||||||
|
local pocket = require("supervisor.session.pocket")
|
||||||
local rtu = require("supervisor.session.rtu")
|
local rtu = require("supervisor.session.rtu")
|
||||||
|
|
||||||
-- Supervisor Sessions Handler
|
-- Supervisor Sessions Handler
|
||||||
|
|
||||||
local SV_Q_CMDS = svqtypes.SV_Q_CMDS
|
|
||||||
local SV_Q_DATA = svqtypes.SV_Q_DATA
|
local SV_Q_DATA = svqtypes.SV_Q_DATA
|
||||||
|
|
||||||
local PLC_S_CMDS = plc.PLC_S_CMDS
|
local PLC_S_CMDS = plc.PLC_S_CMDS
|
||||||
local PLC_S_DATA = plc.PLC_S_DATA
|
local PLC_S_DATA = plc.PLC_S_DATA
|
||||||
local CRD_S_CMDS = coordinator.CRD_S_CMDS
|
|
||||||
local CRD_S_DATA = coordinator.CRD_S_DATA
|
local CRD_S_DATA = coordinator.CRD_S_DATA
|
||||||
|
|
||||||
local svsessions = {}
|
local svsessions = {}
|
||||||
|
|
||||||
local SESSION_TYPE = {
|
local SESSION_TYPE = {
|
||||||
RTU_SESSION = 0,
|
RTU_SESSION = 0, -- RTU gateway
|
||||||
PLC_SESSION = 1,
|
PLC_SESSION = 1, -- reactor PLC
|
||||||
COORD_SESSION = 2
|
COORD_SESSION = 2, -- coordinator
|
||||||
|
DIAG_SESSION = 3 -- pocket diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
svsessions.SESSION_TYPE = SESSION_TYPE
|
svsessions.SESSION_TYPE = SESSION_TYPE
|
||||||
|
|
||||||
local self = {
|
local self = {
|
||||||
modem = nil,
|
modem = nil, ---@type table|nil
|
||||||
num_reactors = 0,
|
num_reactors = 0,
|
||||||
facility = nil, ---@type facility
|
facility = nil, ---@type facility|nil
|
||||||
rtu_sessions = {},
|
sessions = { rtu = {}, plc = {}, coord = {}, diag = {} },
|
||||||
plc_sessions = {},
|
next_ids = { rtu = 0, plc = 0, coord = 0, diag = 0 }
|
||||||
coord_sessions = {},
|
|
||||||
next_rtu_id = 0,
|
|
||||||
next_plc_id = 0,
|
|
||||||
next_coord_id = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
---@alias sv_session_structs plc_session_struct|rtu_session_struct|coord_session_struct|diag_session_struct
|
||||||
|
|
||||||
-- PRIVATE FUNCTIONS --
|
-- PRIVATE FUNCTIONS --
|
||||||
|
|
||||||
-- handle a session output queue
|
-- handle a session output queue
|
||||||
---@param session plc_session_struct|rtu_session_struct|coord_session_struct
|
---@param session sv_session_structs
|
||||||
local function _sv_handle_outq(session)
|
local function _sv_handle_outq(session)
|
||||||
-- record handler start time
|
-- record handler start time
|
||||||
local handle_start = util.time()
|
local handle_start = util.time()
|
||||||
@@ -114,7 +112,7 @@ end
|
|||||||
---@param sessions table
|
---@param sessions table
|
||||||
local function _iterate(sessions)
|
local function _iterate(sessions)
|
||||||
for i = 1, #sessions do
|
for i = 1, #sessions do
|
||||||
local session = sessions[i] ---@type plc_session_struct|rtu_session_struct|coord_session_struct
|
local session = sessions[i] ---@type sv_session_structs
|
||||||
|
|
||||||
if session.open and session.instance.iterate() then
|
if session.open and session.instance.iterate() then
|
||||||
_sv_handle_outq(session)
|
_sv_handle_outq(session)
|
||||||
@@ -125,7 +123,7 @@ local function _iterate(sessions)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- cleanly close a session
|
-- cleanly close a session
|
||||||
---@param session plc_session_struct|rtu_session_struct
|
---@param session sv_session_structs
|
||||||
local function _shutdown(session)
|
local function _shutdown(session)
|
||||||
session.open = false
|
session.open = false
|
||||||
session.instance.close()
|
session.instance.close()
|
||||||
@@ -145,10 +143,8 @@ end
|
|||||||
---@param sessions table
|
---@param sessions table
|
||||||
local function _close(sessions)
|
local function _close(sessions)
|
||||||
for i = 1, #sessions do
|
for i = 1, #sessions do
|
||||||
local session = sessions[i] ---@type plc_session_struct|rtu_session_struct
|
local session = sessions[i] ---@type sv_session_structs
|
||||||
if session.open then
|
if session.open then _shutdown(session) end
|
||||||
_shutdown(session)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -157,7 +153,7 @@ end
|
|||||||
---@param timer_event number
|
---@param timer_event number
|
||||||
local function _check_watchdogs(sessions, timer_event)
|
local function _check_watchdogs(sessions, timer_event)
|
||||||
for i = 1, #sessions do
|
for i = 1, #sessions do
|
||||||
local session = sessions[i] ---@type plc_session_struct|rtu_session_struct
|
local session = sessions[i] ---@type sv_session_structs
|
||||||
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
|
||||||
@@ -174,6 +170,7 @@ end
|
|||||||
local function _free_closed(sessions)
|
local function _free_closed(sessions)
|
||||||
local f = function (session) return session.open end
|
local f = function (session) return session.open end
|
||||||
|
|
||||||
|
---@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("free'ing closed ", session.s_type, " session ", session.instance.get_id(),
|
||||||
" on remote port ", session.r_port))
|
" on remote port ", session.r_port))
|
||||||
@@ -186,7 +183,7 @@ end
|
|||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param list table
|
---@param list table
|
||||||
---@param port integer
|
---@param port integer
|
||||||
---@return plc_session_struct|rtu_session_struct|coord_session_struct|nil
|
---@return sv_session_structs|nil
|
||||||
local function _find_session(list, port)
|
local function _find_session(list, port)
|
||||||
for i = 1, #list do
|
for i = 1, #list do
|
||||||
if list[i].r_port == port then return list[i] end
|
if list[i].r_port == port then return list[i] end
|
||||||
@@ -218,8 +215,8 @@ end
|
|||||||
---@return rtu_session_struct|nil
|
---@return rtu_session_struct|nil
|
||||||
function svsessions.find_rtu_session(remote_port)
|
function svsessions.find_rtu_session(remote_port)
|
||||||
-- check RTU sessions
|
-- check RTU sessions
|
||||||
local session = _find_session(self.rtu_sessions, remote_port)
|
local session = _find_session(self.sessions.rtu, remote_port)
|
||||||
---@cast session rtu_session_struct
|
---@cast session rtu_session_struct|nil
|
||||||
return session
|
return session
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -229,8 +226,8 @@ end
|
|||||||
---@return plc_session_struct|nil
|
---@return plc_session_struct|nil
|
||||||
function svsessions.find_plc_session(remote_port)
|
function svsessions.find_plc_session(remote_port)
|
||||||
-- check PLC sessions
|
-- check PLC sessions
|
||||||
local session = _find_session(self.plc_sessions, remote_port)
|
local session = _find_session(self.sessions.plc, remote_port)
|
||||||
---@cast session plc_session_struct
|
---@cast session plc_session_struct|nil
|
||||||
return session
|
return session
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -240,24 +237,27 @@ end
|
|||||||
---@return plc_session_struct|rtu_session_struct|nil
|
---@return plc_session_struct|rtu_session_struct|nil
|
||||||
function svsessions.find_device_session(remote_port)
|
function svsessions.find_device_session(remote_port)
|
||||||
-- check RTU sessions
|
-- check RTU sessions
|
||||||
local session = _find_session(self.rtu_sessions, remote_port)
|
local session = _find_session(self.sessions.rtu, remote_port)
|
||||||
|
|
||||||
-- check PLC sessions
|
-- check PLC sessions
|
||||||
if session == nil then session = _find_session(self.plc_sessions, remote_port) end
|
if session == nil then session = _find_session(self.sessions.plc, remote_port) end
|
||||||
---@cast session plc_session_struct|rtu_session_struct|nil
|
---@cast session plc_session_struct|rtu_session_struct|nil
|
||||||
|
|
||||||
return session
|
return session
|
||||||
end
|
end
|
||||||
|
|
||||||
-- find a coordinator session by the remote port<br>
|
-- find a coordinator or diagnostic access session by the remote port
|
||||||
-- only one coordinator is allowed, but this is kept to be consistent with all other session tables
|
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param remote_port integer
|
---@param remote_port integer
|
||||||
---@return coord_session_struct|nil
|
---@return coord_session_struct|diag_session_struct|nil
|
||||||
function svsessions.find_coord_session(remote_port)
|
function svsessions.find_svctl_session(remote_port)
|
||||||
-- check coordinator sessions
|
-- check coordinator sessions
|
||||||
local session = _find_session(self.coord_sessions, remote_port)
|
local session = _find_session(self.sessions.coord, remote_port)
|
||||||
---@cast session coord_session_struct
|
|
||||||
|
-- check diagnostic sessions
|
||||||
|
if session == nil then session = _find_session(self.sessions.diag, remote_port) end
|
||||||
|
---@cast session coord_session_struct|diag_session_struct|nil
|
||||||
|
|
||||||
return session
|
return session
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -265,7 +265,7 @@ end
|
|||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@return coord_session_struct|nil
|
---@return coord_session_struct|nil
|
||||||
function svsessions.get_coord_session()
|
function svsessions.get_coord_session()
|
||||||
return self.coord_sessions[1]
|
return self.sessions.coord[1]
|
||||||
end
|
end
|
||||||
|
|
||||||
-- get a session by reactor ID
|
-- get a session by reactor ID
|
||||||
@@ -275,9 +275,9 @@ end
|
|||||||
function svsessions.get_reactor_session(reactor)
|
function svsessions.get_reactor_session(reactor)
|
||||||
local session = nil
|
local session = nil
|
||||||
|
|
||||||
for i = 1, #self.plc_sessions do
|
for i = 1, #self.sessions.plc do
|
||||||
if self.plc_sessions[i].reactor == reactor then
|
if self.sessions.plc[i].reactor == reactor then
|
||||||
session = self.plc_sessions[i]
|
session = self.sessions.plc[i]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -306,15 +306,15 @@ function svsessions.establish_plc_session(local_port, remote_port, for_reactor,
|
|||||||
instance = nil ---@type plc_session
|
instance = nil ---@type plc_session
|
||||||
}
|
}
|
||||||
|
|
||||||
plc_s.instance = plc.new_session(self.next_plc_id, for_reactor, plc_s.in_queue, plc_s.out_queue, config.PLC_TIMEOUT)
|
plc_s.instance = plc.new_session(self.next_ids.plc, for_reactor, plc_s.in_queue, plc_s.out_queue, config.PLC_TIMEOUT)
|
||||||
table.insert(self.plc_sessions, 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_plc_id, " for reactor ", for_reactor))
|
log.debug(util.c("established new PLC session to ", remote_port, " with ID ", self.next_ids.plc, " for reactor ", for_reactor))
|
||||||
|
|
||||||
self.next_plc_id = self.next_plc_id + 1
|
self.next_ids.plc = self.next_ids.plc + 1
|
||||||
|
|
||||||
-- success
|
-- success
|
||||||
return plc_s.instance.get_id()
|
return plc_s.instance.get_id()
|
||||||
@@ -344,12 +344,12 @@ function svsessions.establish_rtu_session(local_port, remote_port, advertisement
|
|||||||
instance = nil ---@type rtu_session
|
instance = nil ---@type rtu_session
|
||||||
}
|
}
|
||||||
|
|
||||||
rtu_s.instance = rtu.new_session(self.next_rtu_id, rtu_s.in_queue, rtu_s.out_queue, config.RTU_TIMEOUT, advertisement, self.facility)
|
rtu_s.instance = rtu.new_session(self.next_ids.rtu, rtu_s.in_queue, rtu_s.out_queue, config.RTU_TIMEOUT, advertisement, self.facility)
|
||||||
table.insert(self.rtu_sessions, rtu_s)
|
table.insert(self.sessions.rtu, rtu_s)
|
||||||
|
|
||||||
log.debug("established new RTU session to " .. remote_port .. " with ID " .. self.next_rtu_id)
|
log.debug("established new RTU session to " .. remote_port .. " with ID " .. self.next_ids.rtu)
|
||||||
|
|
||||||
self.next_rtu_id = self.next_rtu_id + 1
|
self.next_ids.rtu = self.next_ids.rtu + 1
|
||||||
|
|
||||||
-- success
|
-- success
|
||||||
return rtu_s.instance.get_id()
|
return rtu_s.instance.get_id()
|
||||||
@@ -375,12 +375,12 @@ function svsessions.establish_coord_session(local_port, remote_port, version)
|
|||||||
instance = nil ---@type coord_session
|
instance = nil ---@type coord_session
|
||||||
}
|
}
|
||||||
|
|
||||||
coord_s.instance = coordinator.new_session(self.next_coord_id, coord_s.in_queue, coord_s.out_queue, config.CRD_TIMEOUT, self.facility)
|
coord_s.instance = coordinator.new_session(self.next_ids.coord, coord_s.in_queue, coord_s.out_queue, config.CRD_TIMEOUT, self.facility)
|
||||||
table.insert(self.coord_sessions, coord_s)
|
table.insert(self.sessions.coord, coord_s)
|
||||||
|
|
||||||
log.debug("established new coordinator session to " .. remote_port .. " with ID " .. self.next_coord_id)
|
log.debug("established new coordinator session to " .. remote_port .. " with ID " .. self.next_ids.coord)
|
||||||
|
|
||||||
self.next_coord_id = self.next_coord_id + 1
|
self.next_ids.coord = self.next_ids.coord + 1
|
||||||
|
|
||||||
-- success
|
-- success
|
||||||
return coord_s.instance.get_id()
|
return coord_s.instance.get_id()
|
||||||
@@ -390,32 +390,49 @@ function svsessions.establish_coord_session(local_port, remote_port, version)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- establish a new pocket diagnostics session
|
||||||
|
---@nodiscard
|
||||||
|
---@param local_port integer
|
||||||
|
---@param remote_port integer
|
||||||
|
---@param version string
|
||||||
|
---@return integer|false session_id
|
||||||
|
function svsessions.establish_diag_session(local_port, remote_port, version)
|
||||||
|
---@class diag_session_struct
|
||||||
|
local diag_s = {
|
||||||
|
s_type = "pkt",
|
||||||
|
open = true,
|
||||||
|
version = version,
|
||||||
|
l_port = local_port,
|
||||||
|
r_port = remote_port,
|
||||||
|
in_queue = mqueue.new(),
|
||||||
|
out_queue = mqueue.new(),
|
||||||
|
instance = nil ---@type diag_session
|
||||||
|
}
|
||||||
|
|
||||||
|
diag_s.instance = pocket.new_session(self.next_ids.diag, diag_s.in_queue, diag_s.out_queue, config.PKT_TIMEOUT)
|
||||||
|
table.insert(self.sessions.diag, diag_s)
|
||||||
|
|
||||||
|
log.debug("established new pocket diagnostics session to " .. remote_port .. " with ID " .. self.next_ids.diag)
|
||||||
|
|
||||||
|
self.next_ids.diag = self.next_ids.diag + 1
|
||||||
|
|
||||||
|
-- success
|
||||||
|
return diag_s.instance.get_id()
|
||||||
|
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 svsessions.check_all_watchdogs(timer_event)
|
function svsessions.check_all_watchdogs(timer_event)
|
||||||
-- check RTU session watchdogs
|
for _, list in pairs(self.sessions) do _check_watchdogs(list, timer_event) end
|
||||||
_check_watchdogs(self.rtu_sessions, timer_event)
|
|
||||||
|
|
||||||
-- check PLC session watchdogs
|
|
||||||
_check_watchdogs(self.plc_sessions, timer_event)
|
|
||||||
|
|
||||||
-- check coordinator session watchdogs
|
|
||||||
_check_watchdogs(self.coord_sessions, timer_event)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- iterate all sessions
|
-- iterate all sessions, and update facility/unit data & process control logic
|
||||||
function svsessions.iterate_all()
|
function svsessions.iterate_all()
|
||||||
-- iterate RTU sessions
|
-- iterate sessions
|
||||||
_iterate(self.rtu_sessions)
|
for _, list in pairs(self.sessions) do _iterate(list) end
|
||||||
|
|
||||||
-- iterate PLC sessions
|
|
||||||
_iterate(self.plc_sessions)
|
|
||||||
|
|
||||||
-- iterate coordinator sessions
|
|
||||||
_iterate(self.coord_sessions)
|
|
||||||
|
|
||||||
-- report RTU sessions to facility
|
-- report RTU sessions to facility
|
||||||
self.facility.report_rtus(self.rtu_sessions)
|
self.facility.report_rtus(self.sessions.rtu)
|
||||||
|
|
||||||
-- iterate facility
|
-- iterate facility
|
||||||
self.facility.update()
|
self.facility.update()
|
||||||
@@ -426,22 +443,15 @@ end
|
|||||||
|
|
||||||
-- delete all closed sessions
|
-- delete all closed sessions
|
||||||
function svsessions.free_all_closed()
|
function svsessions.free_all_closed()
|
||||||
-- free closed RTU sessions
|
for _, list in pairs(self.sessions) do _free_closed(list) end
|
||||||
_free_closed(self.rtu_sessions)
|
|
||||||
|
|
||||||
-- free closed PLC sessions
|
|
||||||
_free_closed(self.plc_sessions)
|
|
||||||
|
|
||||||
-- free closed coordinator sessions
|
|
||||||
_free_closed(self.coord_sessions)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- close all open connections
|
-- close all open connections
|
||||||
function svsessions.close_all()
|
function svsessions.close_all()
|
||||||
-- close sessions
|
-- close sessions
|
||||||
_close(self.rtu_sessions)
|
for _, list in pairs(self.sessions) do
|
||||||
_close(self.plc_sessions)
|
_close(list)
|
||||||
_close(self.coord_sessions)
|
end
|
||||||
|
|
||||||
-- free sessions
|
-- free sessions
|
||||||
svsessions.free_all_closed()
|
svsessions.free_all_closed()
|
||||||
|
|||||||
@@ -9,16 +9,14 @@ local log = require("scada-common.log")
|
|||||||
local ppm = require("scada-common.ppm")
|
local ppm = require("scada-common.ppm")
|
||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
local svsessions = require("supervisor.session.svsessions")
|
|
||||||
|
|
||||||
local config = require("supervisor.config")
|
local config = require("supervisor.config")
|
||||||
local supervisor = require("supervisor.supervisor")
|
local supervisor = require("supervisor.supervisor")
|
||||||
|
|
||||||
local SUPERVISOR_VERSION = "v0.14.3"
|
local svsessions = require("supervisor.session.svsessions")
|
||||||
|
|
||||||
|
local SUPERVISOR_VERSION = "v0.15.9"
|
||||||
|
|
||||||
local print = util.print
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local print_ts = util.print_ts
|
|
||||||
local println_ts = util.println_ts
|
local println_ts = util.println_ts
|
||||||
|
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
@@ -28,7 +26,7 @@ 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_port(config.SCADA_DEV_LISTEN)
|
||||||
cfv.assert_port(config.SCADA_SV_LISTEN)
|
cfv.assert_port(config.SCADA_SV_CTL_LISTEN)
|
||||||
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)
|
||||||
@@ -36,6 +34,8 @@ cfv.assert_type_num(config.RTU_TIMEOUT)
|
|||||||
cfv.assert_min(config.RTU_TIMEOUT, 2)
|
cfv.assert_min(config.RTU_TIMEOUT, 2)
|
||||||
cfv.assert_type_num(config.CRD_TIMEOUT)
|
cfv.assert_type_num(config.CRD_TIMEOUT)
|
||||||
cfv.assert_min(config.CRD_TIMEOUT, 2)
|
cfv.assert_min(config.CRD_TIMEOUT, 2)
|
||||||
|
cfv.assert_type_num(config.PKT_TIMEOUT)
|
||||||
|
cfv.assert_min(config.PKT_TIMEOUT, 2)
|
||||||
cfv.assert_type_int(config.NUM_REACTORS)
|
cfv.assert_type_int(config.NUM_REACTORS)
|
||||||
cfv.assert_type_table(config.REACTOR_COOLING)
|
cfv.assert_type_table(config.REACTOR_COOLING)
|
||||||
cfv.assert_type_str(config.LOG_PATH)
|
cfv.assert_type_str(config.LOG_PATH)
|
||||||
@@ -61,7 +61,7 @@ end
|
|||||||
-- log init
|
-- log init
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
log.init(config.LOG_PATH, config.LOG_MODE)
|
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
|
||||||
|
|
||||||
log.info("========================================")
|
log.info("========================================")
|
||||||
log.info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION)
|
log.info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION)
|
||||||
@@ -91,7 +91,7 @@ local function main()
|
|||||||
|
|
||||||
-- start comms, open all channels
|
-- start comms, open all channels
|
||||||
local superv_comms = supervisor.comms(SUPERVISOR_VERSION, config.NUM_REACTORS, config.REACTOR_COOLING, modem,
|
local superv_comms = supervisor.comms(SUPERVISOR_VERSION, config.NUM_REACTORS, config.REACTOR_COOLING, modem,
|
||||||
config.SCADA_DEV_LISTEN, config.SCADA_SV_LISTEN, config.TRUSTED_RANGE)
|
config.SCADA_DEV_LISTEN, config.SCADA_SV_CTL_LISTEN, config.TRUSTED_RANGE)
|
||||||
|
|
||||||
-- base loop clock (6.67Hz, 3 ticks)
|
-- base loop clock (6.67Hz, 3 ticks)
|
||||||
local MAIN_CLOCK = 0.15
|
local MAIN_CLOCK = 0.15
|
||||||
@@ -169,4 +169,4 @@ local function main()
|
|||||||
log.info("exited")
|
log.info("exited")
|
||||||
end
|
end
|
||||||
|
|
||||||
if not xpcall(main, crash.handler) then crash.exit() end
|
if not xpcall(main, crash.handler) then crash.exit() else log.close() end
|
||||||
|
|||||||
@@ -11,21 +11,19 @@ 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 print = util.print
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local print_ts = util.print_ts
|
|
||||||
local println_ts = util.println_ts
|
|
||||||
|
|
||||||
-- 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 num_reactors integer number of reactors
|
||||||
---@param cooling_conf table cooling configuration table
|
---@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 dev_listen integer listening port for PLC/RTU devices
|
||||||
---@param coord_listen integer listening port for coordinator
|
---@param svctl_listen integer listening port for supervisor access
|
||||||
---@param range integer trusted device connection range
|
---@param range integer trusted device connection range
|
||||||
function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen, coord_listen, range)
|
---@diagnostic disable-next-line: unused-local
|
||||||
|
function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_listen, svctl_listen, range)
|
||||||
local self = {
|
local self = {
|
||||||
last_est_acks = {}
|
last_est_acks = {}
|
||||||
}
|
}
|
||||||
@@ -38,7 +36,7 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
|||||||
local function _conf_channels()
|
local function _conf_channels()
|
||||||
modem.closeAll()
|
modem.closeAll()
|
||||||
modem.open(dev_listen)
|
modem.open(dev_listen)
|
||||||
modem.open(coord_listen)
|
modem.open(svctl_listen)
|
||||||
end
|
end
|
||||||
|
|
||||||
_conf_channels()
|
_conf_channels()
|
||||||
@@ -59,18 +57,18 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
|||||||
modem.transmit(dest, dev_listen, s_pkt.raw_sendable())
|
modem.transmit(dest, dev_listen, s_pkt.raw_sendable())
|
||||||
end
|
end
|
||||||
|
|
||||||
-- send coordinator connection establish response
|
-- send supervisor control access connection establish response
|
||||||
---@param seq_id integer
|
---@param seq_id integer
|
||||||
---@param dest integer
|
---@param dest integer
|
||||||
---@param msg table
|
---@param msg table
|
||||||
local function _send_crdn_establish(seq_id, dest, msg)
|
local function _send_svctl_establish(seq_id, dest, msg)
|
||||||
local s_pkt = comms.scada_packet()
|
local s_pkt = comms.scada_packet()
|
||||||
local c_pkt = comms.mgmt_packet()
|
local c_pkt = comms.mgmt_packet()
|
||||||
|
|
||||||
c_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg)
|
c_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg)
|
||||||
s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, c_pkt.raw_sendable())
|
s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, c_pkt.raw_sendable())
|
||||||
|
|
||||||
modem.transmit(dest, coord_listen, s_pkt.raw_sendable())
|
modem.transmit(dest, svctl_listen, s_pkt.raw_sendable())
|
||||||
end
|
end
|
||||||
|
|
||||||
-- PUBLIC FUNCTIONS --
|
-- PUBLIC FUNCTIONS --
|
||||||
@@ -253,9 +251,9 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
|||||||
log.debug("illegal packet type " .. protocol .. " on device listening channel")
|
log.debug("illegal packet type " .. protocol .. " on device listening channel")
|
||||||
end
|
end
|
||||||
-- coordinator listening channel
|
-- coordinator listening channel
|
||||||
elseif l_port == coord_listen then
|
elseif l_port == svctl_listen then
|
||||||
-- look for an associated session
|
-- look for an associated session
|
||||||
local session = svsessions.find_coord_session(r_port)
|
local session = svsessions.find_svctl_session(r_port)
|
||||||
|
|
||||||
if protocol == PROTOCOL.SCADA_MGMT then
|
if protocol == PROTOCOL.SCADA_MGMT then
|
||||||
---@cast packet mgmt_frame
|
---@cast packet mgmt_frame
|
||||||
@@ -279,12 +277,9 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
|||||||
self.last_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION
|
self.last_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION
|
||||||
end
|
end
|
||||||
|
|
||||||
_send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION })
|
_send_svctl_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION })
|
||||||
elseif dev_type ~= DEVICE_TYPE.CRDN then
|
elseif dev_type == DEVICE_TYPE.CRDN then
|
||||||
log.debug(util.c("illegal establish packet for device ", dev_type, " on CRDN listening channel"))
|
-- this is an attempt to establish a new coordinator session
|
||||||
_send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY })
|
|
||||||
else
|
|
||||||
-- this is an attempt to establish a new session
|
|
||||||
local s_id = svsessions.establish_coord_session(l_port, r_port, firmware_v)
|
local s_id = svsessions.establish_coord_session(l_port, r_port, firmware_v)
|
||||||
|
|
||||||
if s_id ~= false then
|
if s_id ~= false then
|
||||||
@@ -294,23 +289,35 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
|||||||
table.insert(config, cooling_conf[i].TURBINES)
|
table.insert(config, cooling_conf[i].TURBINES)
|
||||||
end
|
end
|
||||||
|
|
||||||
println(util.c("CRD (",firmware_v, ") [:", r_port, "] \xbb connected"))
|
println(util.c("CRD (", firmware_v, ") [:", r_port, "] \xbb connected"))
|
||||||
log.info(util.c("CRDN_ESTABLISH: coordinator (",firmware_v, ") [:", r_port, "] connected with session ID ", s_id))
|
log.info(util.c("SVCTL_ESTABLISH: coordinator (", firmware_v, ") [:", r_port, "] connected with session ID ", s_id))
|
||||||
|
|
||||||
_send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW, config })
|
_send_svctl_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW, config })
|
||||||
self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW
|
self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW
|
||||||
else
|
else
|
||||||
if self.last_est_acks[r_port] ~= ESTABLISH_ACK.COLLISION then
|
if self.last_est_acks[r_port] ~= ESTABLISH_ACK.COLLISION then
|
||||||
log.info("CRDN_ESTABLISH: denied new coordinator due to already being connected to another coordinator")
|
log.info("SVCTL_ESTABLISH: denied new coordinator due to already being connected to another coordinator")
|
||||||
self.last_est_acks[r_port] = ESTABLISH_ACK.COLLISION
|
self.last_est_acks[r_port] = ESTABLISH_ACK.COLLISION
|
||||||
end
|
end
|
||||||
|
|
||||||
_send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION })
|
_send_svctl_establish(next_seq_id, r_port, { 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
|
||||||
|
log.debug(util.c("illegal establish packet for device ", dev_type, " on SVCTL listening channel"))
|
||||||
|
_send_svctl_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY })
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
log.debug("CRDN_ESTABLISH: establish packet length mismatch")
|
log.debug("SVCTL_ESTABLISH: establish packet length mismatch")
|
||||||
_send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY })
|
_send_svctl_establish(next_seq_id, r_port, { 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
|
||||||
@@ -330,7 +337,7 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
|||||||
log.debug("illegal packet type " .. protocol .. " on coordinator listening channel")
|
log.debug("illegal packet type " .. protocol .. " on coordinator listening channel")
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
log.warning("received packet on unconfigured channel " .. l_port)
|
log.debug("received packet on unconfigured channel " .. l_port, true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ local pbkdf2 = require("lockbox.kdf.pbkdf2")
|
|||||||
local AES128Cipher = require("lockbox.cipher.aes128")
|
local AES128Cipher = require("lockbox.cipher.aes128")
|
||||||
local HMAC = require("lockbox.mac.hmac")
|
local HMAC = require("lockbox.mac.hmac")
|
||||||
local SHA1 = require("lockbox.digest.sha1")
|
local SHA1 = require("lockbox.digest.sha1")
|
||||||
local SHA2_224 = require("lockbox.digest.sha2_224")
|
-- local SHA2_224 = require("lockbox.digest.sha2_224")
|
||||||
local SHA2_256 = require("lockbox.digest.sha2_256")
|
local SHA2_256 = require("lockbox.digest.sha2_256")
|
||||||
local Stream = require("lockbox.util.stream")
|
local Stream = require("lockbox.util.stream")
|
||||||
local Array = require("lockbox.util.array")
|
local Array = require("lockbox.util.array")
|
||||||
|
|
||||||
local CBCMode = require("lockbox.cipher.mode.cbc")
|
-- local CBCMode = require("lockbox.cipher.mode.cbc")
|
||||||
local CFBMode = require("lockbox.cipher.mode.cfb")
|
-- local CFBMode = require("lockbox.cipher.mode.cfb")
|
||||||
local OFBMode = require("lockbox.cipher.mode.ofb")
|
-- local OFBMode = require("lockbox.cipher.mode.ofb")
|
||||||
local CTRMode = require("lockbox.cipher.mode.ctr")
|
local CTRMode = require("lockbox.cipher.mode.ctr")
|
||||||
|
|
||||||
local ZeroPadding = require("lockbox.padding.zero")
|
local ZeroPadding = require("lockbox.padding.zero")
|
||||||
@@ -35,6 +35,7 @@ util.println("pbkdf2: took " .. (util.time() - start) .. "ms")
|
|||||||
util.println(keyd.asHex())
|
util.println(keyd.asHex())
|
||||||
|
|
||||||
local pkt = comms.modbus_packet()
|
local pkt = comms.modbus_packet()
|
||||||
|
---@diagnostic disable-next-line: param-type-mismatch
|
||||||
pkt.make(1, 2, 7, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
|
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(1, 1, pkt.raw_sendable())
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ mbt.test_error__check_request(MODBUS_EXCODE.NEG_ACKNOWLEDGE)
|
|||||||
println("PASS")
|
println("PASS")
|
||||||
|
|
||||||
print("99 {1,2}: ")
|
print("99 {1,2}: ")
|
||||||
|
---@diagnostic disable-next-line: param-type-mismatch
|
||||||
mbt.pkt_set(99, {1, 2})
|
mbt.pkt_set(99, {1, 2})
|
||||||
mbt.test_error__check_request(MODBUS_EXCODE.ILLEGAL_FUNCTION)
|
mbt.test_error__check_request(MODBUS_EXCODE.ILLEGAL_FUNCTION)
|
||||||
println("PASS")
|
println("PASS")
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ local println = util.println
|
|||||||
|
|
||||||
local IO = rsio.IO
|
local IO = rsio.IO
|
||||||
local IO_LVL = rsio.IO_LVL
|
local IO_LVL = rsio.IO_LVL
|
||||||
local IO_DIR = rsio.IO_DIR
|
|
||||||
local IO_MODE = rsio.IO_MODE
|
local IO_MODE = rsio.IO_MODE
|
||||||
|
|
||||||
println("starting RSIO tester")
|
println("starting RSIO tester")
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ println("")
|
|||||||
|
|
||||||
-- RTU init --
|
-- RTU init --
|
||||||
|
|
||||||
log.init("/log.txt", log.MODE.NEW)
|
log.init("/log.txt", log.MODE.NEW, true)
|
||||||
|
|
||||||
print(">>> init turbine RTU: ")
|
print(">>> init turbine RTU: ")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user