Compare commits

...

105 Commits

Author SHA1 Message Date
Mikayla Fischler
82ab85daa5 updated install manifest hotfix 2023.05.23 2023-05-25 17:41:44 -04:00
Mikayla
a14ffea6f0 Merge pull request #239 from MikaylaFischler/devel
2023.05.23 Hotfix
2023-05-23 19:54:35 -04:00
Mikayla Fischler
43a0ff86d7 #238 bugfix for push button and sidebar in bounds checks 2023-05-23 19:51:48 -04:00
Mikayla
97cee58e5a Merge pull request #236 from MikaylaFischler/devel
2023.05.22 Release
2023-05-22 10:06:18 -04:00
Mikayla Fischler
b8c81e2e70 Merge branch 'graphics-rearchitect' into devel 2023-05-20 08:38:29 -04:00
Mikayla Fischler
142f2c363a #234 made debug config setting optional, defaults to false 2023-05-19 19:12:27 -04:00
Mikayla
de99169db8 Merge pull request #235 from MikaylaFischler/graphics-rearchitect
Graphics Rearchitect: Part 2
2023-05-19 18:15:11 -04:00
Mikayla Fischler
d5446f970b updated install manifest and removed early ref to listbox 2023-05-19 17:38:55 -04:00
Mikayla Fischler
792cb46ce6 resolved register simplification 2023-05-19 17:38:08 -04:00
Mikayla Fischler
86615b03ff fixed unused variable 2023-05-18 20:42:15 -04:00
Mikayla Fischler
d5fe790c86 #227 move graphics windows 2023-05-18 20:21:23 -04:00
Mikayla Fischler
beda7624f4 #233 fixed mouse enter/exit behavior via simplification 2023-05-18 10:58:42 -04:00
Mikayla Fischler
82e3fa494c #229 pocket changes for UI element register change 2023-05-14 19:13:44 -04:00
Mikayla Fischler
466902371a #229 coordinator changes for UI element register change 2023-05-14 19:13:12 -04:00
Mikayla Fischler
e763af9981 #229 PLC changes for UI element register change 2023-05-13 09:43:42 -04:00
Mikayla Fischler
b2115fd077 #229 element PSIL register/deletion, changes for RTU to use new PSIL register 2023-05-13 08:50:13 -04:00
Mikayla Fischler
36bd2c5e08 enabled debug logs on turbine modbustest 2023-05-12 13:52:42 -04:00
Mikayla Fischler
f6610489c2 #224 fix for RTU unit indexing on supervisor when virtual units were present 2023-05-11 20:54:43 -04:00
Mikayla Fischler
a81fd49604 updated manifest 2023-05-11 20:01:04 -04:00
Mikayla
b430a22f08 Merge pull request #230 from MikaylaFischler/graphics-rearchitect
Graphics Rearchitect Part 1: Mouse Events
2023-05-11 19:59:52 -04:00
Mikayla
a220713385 Merge branch 'devel' into graphics-rearchitect 2023-05-11 19:57:57 -04:00
Mikayla Fischler
fac9a8d104 updated install manifest 2023-05-11 19:56:45 -04:00
Mikayla Fischler
0783c4c01f #226 bugfixes and pocket mouse events 2023-05-11 19:55:02 -04:00
Mikayla Fischler
676dfc8c22 #226 mouse events in coordinator 2023-05-10 20:01:06 -04:00
Mikayla
50c0a4a3eb #222 added debug log enable to configs 2023-05-10 20:57:23 +00:00
Mikayla
032284e90d #224 skip virtual RTU units when parsing advertisements instead of aborting 2023-05-10 20:40:52 +00:00
Mikayla
3a0d677c16 #226 updated PLC/RTU front panels to use new mouse events 2023-05-10 19:21:54 +00:00
Mikayla Fischler
2c2f936232 #226 updated the other controls for new mouse events, added tabbar control 2023-05-10 11:46:06 -04:00
Mikayla Fischler
4ef1915137 #226 multi button updated for new graphics mouse events 2023-05-10 11:08:24 -04:00
Mikayla Fischler
40fa0de7a3 #226 hazard and push buttons updated for new graphics mouse events 2023-05-10 10:56:56 -04:00
Mikayla Fischler
b8a8da1ac4 #226 graphics core changes for mouse events 2023-05-09 20:29:07 -04:00
Mikayla
e26dc905f8 #226 updated mouse events WIP 2023-05-07 01:27:36 +00:00
Mikayla Fischler
c7edd8c487 updated install manifest after luacheck changes 2023-05-05 14:12:35 -04:00
Mikayla Fischler
d3249da102 removed check.yml comment about -a 2023-05-05 14:11:15 -04:00
Mikayla Fischler
0e1f23efe8 fixed luacheck comments 2023-05-05 14:09:50 -04:00
Mikayla Fischler
5a139c2dd6 possible luacheck fixes 2023-05-05 14:07:15 -04:00
Mikayla Fischler
30ba8bdccf luacheck fixes continued 2023-05-05 14:04:28 -04:00
Mikayla Fischler
b2e21cb6d9 luacheck fixes 2023-05-05 14:02:25 -04:00
Mikayla Fischler
8064b33a36 some luacheck fixes 2023-05-05 13:55:14 -04:00
Mikayla Fischler
7e33f22577 luacheck suppression attempt 2023-05-05 13:15:17 -04:00
Mikayla Fischler
464451c378 unused vararg suppression, re-enable unused args luacheck 2023-05-05 13:09:53 -04:00
Mikayla Fischler
0778a442b1 diagnostic suppression 2023-05-05 13:07:48 -04:00
Mikayla
d7e2884634 Merge pull request #221 from MikaylaFischler/devel
2023.04.22 Release
2023-04-22 11:03:47 -04:00
Mikayla Fischler
43e708aa0d #219 bugfixes with renderer exit handling 2023-04-21 23:43:28 -04:00
Mikayla
783c4936cc #213 strict sequence verification 2023-04-21 21:10:15 +00:00
Mikayla
c75f08a9f7 added python to devcontainer and recommendations 2023-04-21 18:56:32 +00:00
Mikayla
e1da8b59d3 #219 properly close out GUI on error on pocket and coordinator 2023-04-21 18:53:28 +00:00
Mikayla
706fb5ea74 updated devcontainer and workspace extension recommendations 2023-04-21 13:34:46 +00:00
Mikayla Fischler
419ca2e6ef #220 close ui on crash 2023-04-20 21:19:16 -04:00
Mikayla Fischler
4c8723eb32 #217 close log file on pocket too 2023-04-20 21:01:41 -04:00
Mikayla Fischler
5db517cedc #217 close log files on exit (including crash) 2023-04-20 21:00:10 -04:00
Mikayla Fischler
e9788abde7 #219 fixed PLC renderer crash handling 2023-04-20 20:47:14 -04:00
Mikayla
be077aa1fb Merge pull request #218 from MikaylaFischler/front-panels
#183 RTU front panel
2023-04-20 20:42:28 -04:00
Mikayla Fischler
d143015cc7 #183 RTU front panel 2023-04-20 20:40:28 -04:00
Mikayla
df45f6c984 Merge pull request #215 from MikaylaFischler/193-pocket-main-application
193 pocket main application
2023-04-19 23:01:39 -04:00
Mikayla
f6fe99a5fd Merge branch 'devel' into 193-pocket-main-application 2023-04-19 23:01:10 -04:00
Mikayla Fischler
a843c8eb79 fixes and cleanup 2023-04-19 23:00:27 -04:00
Mikayla Fischler
a614b97d02 cleanup to pass checks 2023-04-19 21:26:54 -04:00
Mikayla Fischler
eca303e289 #208 ui cleanup for indicating emergency coolant status 2023-04-19 21:21:19 -04:00
Mikayla Fischler
ccdc31ed87 fixed typo in check workflow 2023-04-19 20:40:09 -04:00
Mikayla Fischler
c49ad63d6a Merge branch 'devel' into 193-pocket-main-application 2023-04-19 20:37:19 -04:00
Mikayla Fischler
7929318096 #201 functional pocket comms with supervisor and coordinator, adjusted some UI element positioning, bugfixes with apisessions and svsessions 2023-04-19 20:35:42 -04:00
Mikayla
2371a75130 #214 log level cleanup 2023-04-19 13:30:17 +00:00
Mikayla
fee54db43e #203 removed log message on failed structure send, lowered some other log levels to debug 2023-04-18 22:01:35 +00:00
Mikayla Fischler
b48c956354 #201 coordinator apisessions for pocket access 2023-04-18 13:55:18 -04:00
Mikayla Fischler
449e393b73 #201 supervisor pocket diagnostics session 2023-04-18 13:49:59 -04:00
Mikayla Fischler
d295c2b3c3 #201 added pocket connecting screens 2023-04-18 13:47:06 -04:00
Mikayla Fischler
438ab55f4f updated lua diagnostics config 2023-04-18 13:46:00 -04:00
Mikayla
46607dd690 #208 indicate emergency coolant control on PLC front panel 2023-04-18 15:28:46 +00:00
Mikayla Fischler
33c570075c supervisor code cleanup 2023-04-17 19:48:03 -04:00
Mikayla Fischler
93776a0421 update luacheck args and copied lua extension configs to workspace 2023-04-17 15:40:30 -04:00
Mikayla
14dc814925 #201 removed redundant close handling 2023-04-17 00:22:47 +00:00
Mikayla
a7ba0e43e8 #201 pocket comms establishes 2023-04-16 23:50:16 +00:00
Mikayla Fischler
e9290540f5 #193 pocket main application core 2023-04-16 15:05:28 -04:00
Mikayla
b35bf98dec update devcontainer with extensions 2023-04-13 14:45:02 +00:00
Mikayla
59512bb0cf Create devcontainer.json 2023-04-13 10:43:03 -04:00
Mikayla
64449c6674 restore shields action to just main branch 2023-04-13 09:41:56 -04:00
Mikayla
5bcd885f53 shortened shields URLs after adjustment to action 2023-04-13 09:33:44 -04:00
Mikayla
ba70aa31dc test update of shields.yml 2023-04-13 09:29:16 -04:00
Mikayla Fischler
d9ec3d7825 Merge branch 'devel' into 193-pocket-main-application 2023-04-12 18:06:24 -04:00
Mikayla Fischler
9b9ce7eae1 finally got shields component versions working with github actions 2023-04-12 18:03:48 -04:00
Mikayla Fischler
e2a3252d8a possible fix for actions 10 2023-04-12 17:55:18 -04:00
Mikayla Fischler
c0547fe463 possible fix for actions 9 2023-04-12 17:54:18 -04:00
Mikayla Fischler
36b86a4825 possible fix for actions 8 2023-04-12 17:52:45 -04:00
Mikayla Fischler
37dd52b12b possible fix for actions 7 2023-04-12 17:50:43 -04:00
Mikayla Fischler
6b8b38b8cb possible fix for actions 6 2023-04-12 17:49:08 -04:00
Mikayla Fischler
2b23dac1fe possible fix for actions 4 2023-04-12 17:44:51 -04:00
Mikayla Fischler
76f6cca42d possible fix for actions 3 2023-04-12 17:44:00 -04:00
Mikayla Fischler
ab9e487a2d possible fix for actions 2 2023-04-12 17:41:06 -04:00
Mikayla Fischler
982fded31d possible fix for actions 2023-04-12 17:39:38 -04:00
Mikayla Fischler
a8e0538804 debugging actions 2023-04-12 17:37:16 -04:00
Mikayla Fischler
8c42a05bbd test for sheilds 2023-04-12 17:34:46 -04:00
Mikayla Fischler
60a3fc8c37 Merge branch 'main' into devel 2023-04-12 17:33:16 -04:00
Mikayla
83cc4d3067 pages fix 2023-04-12 17:25:16 -04:00
Mikayla
fb31afc89c Merge pull request #211 from MikaylaFischler/sheilds-pages
create shields.yml
2023-04-12 17:23:36 -04:00
Mikayla
36c8a9ccfa Create shields.yml 2023-04-12 17:22:44 -04:00
Mikayla Fischler
f108db9cfc alternate plan for shields 2023-04-12 17:21:39 -04:00
Mikayla Fischler
f48266e27c added subversions to readme 2023-04-12 17:09:53 -04:00
Mikayla Fischler
5c333c2a07 test for adding subversions to shields.io 2023-04-12 17:04:28 -04:00
Mikayla Fischler
df0ee7c4f7 updated shields readme elements 2023-04-12 16:07:15 -04:00
Mikayla
c987d14d8d added Luacheck GitHub action (#210)
* added shields.io elements
* #209 luacheck action
* #209 cleanup to pass luacheck
* added check statuses to readme
2023-04-12 16:02:29 -04:00
Mikayla Fischler
075a0280ac #193 WIP pocket initial app, sidebar added 2023-04-12 12:40:13 -04:00
Mikayla Fischler
4b1c982292 #209 luacheck action 2023-04-12 12:13:11 -04:00
Mikayla
e276a99cb3 added shields.io elements 2023-04-12 09:51:40 -04:00
Mikayla Fischler
3ae39b2455 #204 replaced util.strwrap implementation with cc.strings.wrap 2023-04-11 23:53:42 -04:00
100 changed files with 3966 additions and 1067 deletions

View 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
View 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
View 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
View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"sumneko.lua",
"jackmacwindows.vscode-computercraft",
"ms-python.python"
]
}

36
.vscode/settings.json vendored
View File

@@ -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"
] ]

View File

@@ -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!
![GitHub](https://img.shields.io/github/license/MikaylaFischler/cc-mek-scada)
![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/MikaylaFischler/cc-mek-scada?include_prereleases)
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=main&label=main)
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=latest&label=latest)
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=devel&label=devel)
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
![Bootloader](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fbootloader.json)
![Comms](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fcomms.json)
### Utilities
![Installer](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Finstaller.json)
### Applications
![Reactor PLC](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Freactor-plc.json)
![RTU](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Frtu.json)
![Supervisor](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fsupervisor.json)
![Coordinator](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fcoordinator.json)
![Pocket](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fpocket.json)
## Installation ## Installation
You can install this on a ComputerCraft computer using either: You can install this on a ComputerCraft computer using either:

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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
View 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

View 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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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 = {}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -6,7 +6,7 @@ local core = require("graphics.core")
local style = {} local style = {}
local cpair = core.graphics.cpair local cpair = core.cpair
-- GLOBAL -- -- GLOBAL --

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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()

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View 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

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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
View 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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 },

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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 ()

View File

@@ -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
View 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
View 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
View 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

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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...")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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 --

View File

@@ -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

View File

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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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())

View File

@@ -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")

View File

@@ -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")

View File

@@ -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: ")