Compare commits
203 Commits
v1.0.0-bet
...
v1.4.1-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13a8435f6c | ||
|
|
5d6dda5619 | ||
|
|
f8221ad0f1 | ||
|
|
193aeed6df | ||
|
|
8c87cb3e26 | ||
|
|
1548cd706d | ||
|
|
996272e108 | ||
|
|
ef673bdf1b | ||
|
|
7aa236e987 | ||
|
|
35d857a5f4 | ||
|
|
c2c87ec6c6 | ||
|
|
5ce54d78e1 | ||
|
|
c05a312f6c | ||
|
|
86325d9527 | ||
|
|
5074ca89f0 | ||
|
|
c22b048608 | ||
|
|
7ae3014e06 | ||
|
|
8fa37cc9be | ||
|
|
2c730fbdc2 | ||
|
|
5c21140025 | ||
|
|
7859e5ea4c | ||
|
|
0b5ee8eabc | ||
|
|
1decd88415 | ||
|
|
f1b1f0b75a | ||
|
|
f37f2f009f | ||
|
|
15b071378c | ||
|
|
5ba06dcdaf | ||
|
|
f4e7137eb3 | ||
|
|
cf881548d7 | ||
|
|
0a6fd35f93 | ||
|
|
671f8b55bc | ||
|
|
e16b0d237e | ||
|
|
55dab6d675 | ||
|
|
0f5ae9a756 | ||
|
|
cdff7af431 | ||
|
|
c536b823e7 | ||
|
|
b20d42ff38 | ||
|
|
63147bfab5 | ||
|
|
360609df1f | ||
|
|
9a5fc92c86 | ||
|
|
337fca7e7c | ||
|
|
38fc7189ba | ||
|
|
0b939be412 | ||
|
|
351842c9a1 | ||
|
|
8d248408d4 | ||
|
|
2427561dc5 | ||
|
|
b4932b33b6 | ||
|
|
24a7275543 | ||
|
|
529371a0fd | ||
|
|
69df5edbeb | ||
|
|
153a83e569 | ||
|
|
ef1ec220a4 | ||
|
|
8f2e9fe319 | ||
|
|
86ad2a1069 | ||
|
|
494dc437a5 | ||
|
|
deec1ff1df | ||
|
|
4c35233289 | ||
|
|
de9cb3bd3a | ||
|
|
270726e276 | ||
|
|
dbd74afbe6 | ||
|
|
37a91986e5 | ||
|
|
a892c0cf41 | ||
|
|
b7d90872d5 | ||
|
|
82ab85daa5 | ||
|
|
f9aa75a105 | ||
|
|
e313b77abc | ||
|
|
a14ffea6f0 | ||
|
|
43a0ff86d7 | ||
|
|
ece7c0fe9a | ||
|
|
97cee58e5a | ||
|
|
4aba79f232 | ||
|
|
b8c81e2e70 | ||
|
|
142f2c363a | ||
|
|
de99169db8 | ||
|
|
d5446f970b | ||
|
|
792cb46ce6 | ||
|
|
86615b03ff | ||
|
|
d5fe790c86 | ||
|
|
beda7624f4 | ||
|
|
82e3fa494c | ||
|
|
466902371a | ||
|
|
e763af9981 | ||
|
|
b2115fd077 | ||
|
|
36bd2c5e08 | ||
|
|
f6610489c2 | ||
|
|
e159dbb850 | ||
|
|
513c72ea79 | ||
|
|
a81fd49604 | ||
|
|
b430a22f08 | ||
|
|
a220713385 | ||
|
|
fac9a8d104 | ||
|
|
0783c4c01f | ||
|
|
676dfc8c22 | ||
|
|
50c0a4a3eb | ||
|
|
032284e90d | ||
|
|
3a0d677c16 | ||
|
|
2c2f936232 | ||
|
|
4ef1915137 | ||
|
|
40fa0de7a3 | ||
|
|
b8a8da1ac4 | ||
|
|
e26dc905f8 | ||
|
|
c7edd8c487 | ||
|
|
d3249da102 | ||
|
|
0e1f23efe8 | ||
|
|
5a139c2dd6 | ||
|
|
30ba8bdccf | ||
|
|
b2e21cb6d9 | ||
|
|
8064b33a36 | ||
|
|
7e33f22577 | ||
|
|
464451c378 | ||
|
|
0778a442b1 | ||
|
|
2c7b98ba42 | ||
|
|
ff9a18a019 | ||
|
|
81005d3e2c | ||
|
|
d7e2884634 | ||
|
|
43e708aa0d | ||
|
|
783c4936cc | ||
|
|
c75f08a9f7 | ||
|
|
e1da8b59d3 | ||
|
|
706fb5ea74 | ||
|
|
419ca2e6ef | ||
|
|
4c8723eb32 | ||
|
|
5db517cedc | ||
|
|
e9788abde7 | ||
|
|
be077aa1fb | ||
|
|
d143015cc7 | ||
|
|
df45f6c984 | ||
|
|
f6fe99a5fd | ||
|
|
a843c8eb79 | ||
|
|
a614b97d02 | ||
|
|
eca303e289 | ||
|
|
ccdc31ed87 | ||
|
|
c49ad63d6a | ||
|
|
7929318096 | ||
|
|
2371a75130 | ||
|
|
fee54db43e | ||
|
|
b48c956354 | ||
|
|
449e393b73 | ||
|
|
d295c2b3c3 | ||
|
|
438ab55f4f | ||
|
|
46607dd690 | ||
|
|
33c570075c | ||
|
|
93776a0421 | ||
|
|
14dc814925 | ||
|
|
a7ba0e43e8 | ||
|
|
e9290540f5 | ||
|
|
b35bf98dec | ||
|
|
59512bb0cf | ||
|
|
64449c6674 | ||
|
|
5bcd885f53 | ||
|
|
ba70aa31dc | ||
|
|
d9ec3d7825 | ||
|
|
9b9ce7eae1 | ||
|
|
e2a3252d8a | ||
|
|
c0547fe463 | ||
|
|
36b86a4825 | ||
|
|
37dd52b12b | ||
|
|
6b8b38b8cb | ||
|
|
2b23dac1fe | ||
|
|
76f6cca42d | ||
|
|
ab9e487a2d | ||
|
|
982fded31d | ||
|
|
a8e0538804 | ||
|
|
8c42a05bbd | ||
|
|
60a3fc8c37 | ||
|
|
83cc4d3067 | ||
|
|
fb31afc89c | ||
|
|
36c8a9ccfa | ||
|
|
f108db9cfc | ||
|
|
f48266e27c | ||
|
|
5c333c2a07 | ||
|
|
df0ee7c4f7 | ||
|
|
c987d14d8d | ||
|
|
075a0280ac | ||
|
|
4b1c982292 | ||
|
|
e276a99cb3 | ||
|
|
3ae39b2455 | ||
|
|
fc9d86f23e | ||
|
|
b325992a0d | ||
|
|
04d73cdcd3 | ||
|
|
0c0055d5ae | ||
|
|
4ef73a8580 | ||
|
|
fa88392438 | ||
|
|
6e95755db4 | ||
|
|
5b1f304467 | ||
|
|
a2ea6438b5 | ||
|
|
c9b67f68dd | ||
|
|
d624690b6b | ||
|
|
527f3446a1 | ||
|
|
27a697c27e | ||
|
|
6bd7dd0271 | ||
|
|
67872a1053 | ||
|
|
4aad591d3a | ||
|
|
9bc4f0f7a6 | ||
|
|
d642f28fa9 | ||
|
|
ccc0aa18ff | ||
|
|
6ea530635f | ||
|
|
c2132ea7eb | ||
|
|
40b11dbfd3 | ||
|
|
6e1edce8e7 | ||
|
|
efef4a845f | ||
|
|
91f72ace24 | ||
|
|
0f735d049e |
16
.devcontainer/devcontainer.json
Normal file
16
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/universal:2",
|
||||
"features": {
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"sumneko.lua",
|
||||
"jackmacwindows.vscode-computercraft",
|
||||
"ms-python.python",
|
||||
"Catppuccin.catppuccin-vsc-icons",
|
||||
"melishev.feather-vscode"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
31
.github/workflows/check.yml
vendored
Normal file
31
.github/workflows/check.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Lua Checks
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- latest
|
||||
- devel
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- latest
|
||||
- devel
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3.5.1
|
||||
- name: Luacheck
|
||||
uses: lunarmodules/luacheck@v1.1.0
|
||||
with:
|
||||
# Argument Explanations
|
||||
# -i 121 = Setting a read-only global variable
|
||||
# 512 = Loop can be executed at most once
|
||||
# 542 = An empty if branch
|
||||
# --no-max-line-length = Disable warnings for long line lengths
|
||||
# --exclude-files ... = Exclude lockbox library (external) and config files
|
||||
# --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os'
|
||||
args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http parallel periphemu peripheral read rs settings shell term textutils window
|
||||
95
.github/workflows/manifest.yml
vendored
Normal file
95
.github/workflows/manifest.yml
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
# Simple workflow for deploying static content to GitHub Pages
|
||||
name: Deploy Installation Manifests and Versions
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- latest
|
||||
- devel
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v3.1.3
|
||||
# Generate manifest + shields files for main branch
|
||||
- name: Checkout main
|
||||
id: checkout-main
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'main'
|
||||
clean: false
|
||||
- name: Create outputs folders
|
||||
if: success() || failure()
|
||||
shell: bash
|
||||
run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/latest deploy/manifests/devel
|
||||
- name: Generate manifest and shields for main branch
|
||||
id: manifest-main
|
||||
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
|
||||
run: python imgen.py shields
|
||||
- name: Save main's manifest
|
||||
if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }}
|
||||
run: mv install_manifest.json deploy/manifests/main
|
||||
# Generate manifest for latest branch
|
||||
- name: Checkout latest
|
||||
id: checkout-latest
|
||||
if: success() || failure()
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'latest'
|
||||
clean: false
|
||||
- name: Generate manifest for latest
|
||||
id: manifest-latest
|
||||
if: ${{ (success() || failure()) && steps.checkout-latest.outcome == 'success' }}
|
||||
run: python imgen.py
|
||||
- name: Save latest's manifest
|
||||
if: ${{ (success() || failure()) && steps.manifest-latest.outcome == 'success' }}
|
||||
run: mv install_manifest.json deploy/manifests/latest
|
||||
# Generate manifest for devel branch
|
||||
- name: Checkout devel
|
||||
id: checkout-devel
|
||||
if: success() || failure()
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'devel'
|
||||
clean: false
|
||||
- name: Generate manifest for devel
|
||||
id: manifest-devel
|
||||
if: ${{ (success() || failure()) && steps.checkout-devel.outcome == 'success' }}
|
||||
run: python imgen.py
|
||||
- name: Save devel's manifest
|
||||
if: ${{ (success() || failure()) && steps.manifest-devel.outcome == 'success' }}
|
||||
run: mv install_manifest.json deploy/manifests/devel
|
||||
# All artifacts ready now, upload deploy directory
|
||||
- name: Upload artifacts
|
||||
id: upload-artifacts
|
||||
if: ${{ (success() || failure()) && (steps.manifest-main.outcome == 'success' || steps.manifest-latest.outcome == 'success' || steps.manifest-devel.outcome == 'success') }}
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
# Upload manifest JSON
|
||||
path: 'deploy/'
|
||||
- name: Deploy to GitHub Pages
|
||||
if: ${{ (success() || failure()) && steps.upload-artifacts.outcome == 'success' }}
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"sumneko.lua",
|
||||
"jackmacwindows.vscode-computercraft",
|
||||
"ms-python.python"
|
||||
]
|
||||
}
|
||||
36
.vscode/settings.json
vendored
36
.vscode/settings.json
vendored
@@ -1,22 +1,28 @@
|
||||
{
|
||||
"Lua.diagnostics.globals": [
|
||||
"term",
|
||||
"fs",
|
||||
"peripheral",
|
||||
"rs",
|
||||
"bit",
|
||||
"parallel",
|
||||
"colors",
|
||||
"textutils",
|
||||
"shell",
|
||||
"settings",
|
||||
"window",
|
||||
"read",
|
||||
"periphemu",
|
||||
"mekanismEnergyHelper",
|
||||
"_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": [
|
||||
"duplicate-set-field"
|
||||
]
|
||||
|
||||
27
README.md
27
README.md
@@ -1,6 +1,12 @@
|
||||
# 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!
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Mod Requirements:
|
||||
- CC: Tweaked
|
||||
- Mekanism v10.1+
|
||||
@@ -12,11 +18,30 @@ 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.
|
||||
|
||||
## Released Component Versions
|
||||
|
||||
### Core
|
||||
|
||||

|
||||

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

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

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
You can install this on a ComputerCraft computer using either:
|
||||
* `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua`
|
||||
* `pastebin get iUMjgW0C ccmsi.lua`
|
||||
* `pastebin get eRz6cUNM ccmsi.lua`
|
||||
|
||||
## [SCADA](https://en.wikipedia.org/wiki/SCADA)
|
||||
> Supervisory control and data acquisition (SCADA) is a control system architecture comprising computers, networked data communications and graphical user interfaces for high-level supervision of machines and processes. It also covers sensors and other devices, such as programmable logic controllers, which interface with process plant or machinery.
|
||||
|
||||
16
ccmsi.lua
16
ccmsi.lua
@@ -3,14 +3,14 @@
|
||||
--
|
||||
|
||||
--[[
|
||||
Copyright © 2023 Mikayla Fischler
|
||||
Copyright (c) 2023 Mikayla Fischler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the “Software”), to deal in the Software without restriction,
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
@@ -20,9 +20,10 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
local function println(message) print(tostring(message)) end
|
||||
local function print(message) term.write(tostring(message)) end
|
||||
|
||||
local CCMSI_VERSION = "v1.0"
|
||||
local CCMSI_VERSION = "v1.2"
|
||||
|
||||
local install_dir = "/.install-cache"
|
||||
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
|
||||
local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/"
|
||||
|
||||
local opts = { ... }
|
||||
@@ -122,8 +123,8 @@ if mode == "check" then
|
||||
-- GET REMOTE MANIFEST --
|
||||
-------------------------
|
||||
|
||||
if opts[2] then repo_path = repo_path .. opts[2] .. "/" else repo_path = repo_path .. "main/" end
|
||||
local install_manifest = repo_path .. "install_manifest.json"
|
||||
if opts[2] then manifest_path = manifest_path .. opts[2] .. "/" else manifest_path = manifest_path .. "main/" end
|
||||
local install_manifest = manifest_path .. "install_manifest.json"
|
||||
|
||||
local response, error = http.get(install_manifest)
|
||||
|
||||
@@ -203,7 +204,8 @@ elseif mode == "install" or mode == "update" then
|
||||
-------------------------
|
||||
|
||||
if opts[3] then repo_path = repo_path .. opts[3] .. "/" else repo_path = repo_path .. "main/" end
|
||||
local install_manifest = repo_path .. "install_manifest.json"
|
||||
if opts[3] then manifest_path = manifest_path .. opts[3] .. "/" else manifest_path = manifest_path .. "main/" end
|
||||
local install_manifest = manifest_path .. "install_manifest.json"
|
||||
|
||||
local response, error = http.get(install_manifest)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -1,15 +1,16 @@
|
||||
local config = {}
|
||||
|
||||
-- port of the SCADA supervisor
|
||||
config.SCADA_SV_PORT = 16100
|
||||
-- port to listen to incoming packets from supervisor
|
||||
config.SCADA_SV_LISTEN = 16101
|
||||
-- listen port for SCADA coordinator API access
|
||||
config.SCADA_API_LISTEN = 16200
|
||||
-- supervisor comms channel
|
||||
config.SVR_CHANNEL = 16240
|
||||
-- coordinator comms channel
|
||||
config.CRD_CHANNEL = 16243
|
||||
-- pocket comms channel
|
||||
config.PKT_CHANNEL = 16244
|
||||
-- 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
|
||||
config.SV_TIMEOUT = 5
|
||||
config.API_TIMEOUT = 5
|
||||
|
||||
-- expected number of reactor units, used only to require that number of unit monitors
|
||||
config.NUM_UNITS = 4
|
||||
@@ -27,5 +28,7 @@ config.LOG_PATH = "/log.txt"
|
||||
-- 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
|
||||
|
||||
@@ -3,15 +3,15 @@ local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local apisessions = require("coordinator.apisessions")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local process = require("coordinator.process")
|
||||
|
||||
local apisessions = require("coordinator.session.apisessions")
|
||||
|
||||
local dialog = require("coordinator.ui.dialog")
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
@@ -213,19 +213,21 @@ end
|
||||
---@nodiscard
|
||||
---@param version string coordinator version
|
||||
---@param modem table modem device
|
||||
---@param sv_port integer port of configured supervisor
|
||||
---@param sv_listen integer listening port for supervisor replys
|
||||
---@param api_listen integer listening port for pocket API
|
||||
---@param crd_channel integer port of configured supervisor
|
||||
---@param svr_channel integer listening port for supervisor replys
|
||||
---@param pkt_channel integer listening port for pocket API
|
||||
---@param range integer trusted device connection range
|
||||
---@param sv_watchdog watchdog
|
||||
function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range, sv_watchdog)
|
||||
function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel, range, sv_watchdog)
|
||||
local self = {
|
||||
sv_linked = false,
|
||||
sv_addr = comms.BROADCAST,
|
||||
sv_seq_num = 0,
|
||||
sv_r_seq_num = nil,
|
||||
sv_config_err = false,
|
||||
connected = false,
|
||||
last_est_ack = ESTABLISH_ACK.ALLOW
|
||||
last_est_ack = ESTABLISH_ACK.ALLOW,
|
||||
last_api_est_acks = {}
|
||||
}
|
||||
|
||||
comms.set_trusted_range(range)
|
||||
@@ -235,18 +237,20 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
-- configure modem channels
|
||||
local function _conf_channels()
|
||||
modem.closeAll()
|
||||
modem.open(sv_listen)
|
||||
modem.open(api_listen)
|
||||
modem.open(crd_channel)
|
||||
end
|
||||
|
||||
_conf_channels()
|
||||
|
||||
-- link modem to apisessions
|
||||
apisessions.init(modem)
|
||||
|
||||
-- send a packet to the supervisor
|
||||
---@param msg_type SCADA_MGMT_TYPE|SCADA_CRDN_TYPE
|
||||
---@param msg table
|
||||
local function _send_sv(protocol, msg_type, msg)
|
||||
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
|
||||
pkt = comms.mgmt_packet()
|
||||
@@ -257,12 +261,26 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
end
|
||||
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv_seq_num, protocol, pkt.raw_sendable())
|
||||
s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable())
|
||||
|
||||
modem.transmit(sv_port, sv_listen, s_pkt.raw_sendable())
|
||||
modem.transmit(svr_channel, crd_channel, s_pkt.raw_sendable())
|
||||
self.sv_seq_num = self.sv_seq_num + 1
|
||||
end
|
||||
|
||||
-- send an API establish request response
|
||||
---@param packet scada_packet
|
||||
---@param ack ESTABLISH_ACK
|
||||
local function _send_api_establish_ack(packet, ack)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
|
||||
m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, { ack })
|
||||
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
modem.transmit(pkt_channel, crd_channel, s_pkt.raw_sendable())
|
||||
self.last_api_est_acks[packet.src_addr()] = ack
|
||||
end
|
||||
|
||||
-- attempt connection establishment
|
||||
local function _send_establish()
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRDN })
|
||||
@@ -283,13 +301,16 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
---@param new_modem table
|
||||
function public.reconnect_modem(new_modem)
|
||||
modem = new_modem
|
||||
apisessions.relink_modem(new_modem)
|
||||
_conf_channels()
|
||||
end
|
||||
|
||||
-- close the connection to the server
|
||||
function public.close()
|
||||
sv_watchdog.cancel()
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.sv_linked = false
|
||||
self.sv_r_seq_num = nil
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {})
|
||||
end
|
||||
|
||||
@@ -317,12 +338,13 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
tick_dmesg_waiting(math.max(0, timeout_s - (util.time_s() - start)))
|
||||
_send_establish()
|
||||
clock.start()
|
||||
elseif event == "timer" then
|
||||
-- keep checking watchdog timers
|
||||
apisessions.check_all_watchdogs(p1)
|
||||
elseif event == "modem_message" then
|
||||
-- handle message
|
||||
local packet = public.parse_packet(p1, p2, p3, p4, p5)
|
||||
if packet ~= nil and packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
public.handle_packet(packet)
|
||||
end
|
||||
public.handle_packet(packet)
|
||||
elseif event == "terminate" then
|
||||
terminated = true
|
||||
break
|
||||
@@ -417,23 +439,82 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
---@param packet mgmt_frame|crdn_frame|capi_frame|nil
|
||||
function public.handle_packet(packet)
|
||||
if packet ~= nil then
|
||||
local l_chan = packet.scada_frame.local_channel()
|
||||
local r_chan = packet.scada_frame.remote_channel()
|
||||
local src_addr = packet.scada_frame.src_addr()
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
local l_port = packet.scada_frame.local_port()
|
||||
|
||||
if l_port == api_listen then
|
||||
if l_chan ~= crd_channel then
|
||||
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
||||
elseif r_chan == pkt_channel then
|
||||
if protocol == PROTOCOL.COORD_API then
|
||||
---@cast packet capi_frame
|
||||
apisessions.handle_packet(packet)
|
||||
-- look for an associated session
|
||||
local session = apisessions.find_session(src_addr)
|
||||
|
||||
-- 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(src_addr)
|
||||
|
||||
-- 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
|
||||
-- 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[src_addr] ~= ESTABLISH_ACK.BAD_VERSION then
|
||||
log.info(util.c("dropping API establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
|
||||
end
|
||||
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
|
||||
elseif dev_type == DEVICE_TYPE.PKT then
|
||||
-- pocket linking request
|
||||
local id = apisessions.establish_session(src_addr, firmware_v)
|
||||
println(util.c("[API] pocket (", firmware_v, ") [@", src_addr, "] \xbb connected"))
|
||||
coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id))
|
||||
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW)
|
||||
else
|
||||
log.debug(util.c("API_ESTABLISH: illegal establish packet for device ", dev_type, " on pocket channel"))
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
|
||||
end
|
||||
else
|
||||
log.debug("invalid establish packet (on API listening channel)")
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
|
||||
end
|
||||
else
|
||||
-- any other packet should be session related, discard it
|
||||
log.debug(util.c("discarding pocket SCADA_MGMT packet without a known session from computer ", src_addr))
|
||||
end
|
||||
else
|
||||
log.debug("illegal packet type " .. protocol .. " on api listening channel", true)
|
||||
log.debug("illegal packet type " .. protocol .. " on pocket channel", true)
|
||||
end
|
||||
elseif l_port == sv_listen then
|
||||
elseif r_chan == svr_channel then
|
||||
-- check sequence number
|
||||
if self.sv_r_seq_num == nil then
|
||||
self.sv_r_seq_num = packet.scada_frame.seq_num()
|
||||
elseif self.connected and self.sv_r_seq_num >= 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())
|
||||
return
|
||||
elseif self.sv_linked and src_addr ~= self.sv_addr then
|
||||
log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?")
|
||||
return
|
||||
else
|
||||
self.sv_r_seq_num = packet.scada_frame.seq_num()
|
||||
end
|
||||
@@ -516,7 +597,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
elseif packet.type == SCADA_CRDN_TYPE.UNIT_STATUSES then
|
||||
-- update statuses
|
||||
if not iocontrol.update_unit_statuses(packet.data) then
|
||||
log.error("received invalid UNIT_STATUSES packet")
|
||||
log.debug("received invalid UNIT_STATUSES packet")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.UNIT_CMD then
|
||||
-- unit command acknowledgement
|
||||
@@ -552,7 +633,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
log.debug("SCADA_CRDN unit command ack packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.warning("received unknown SCADA_CRDN packet type " .. packet.type)
|
||||
log.debug("received unknown SCADA_CRDN packet type " .. packet.type)
|
||||
end
|
||||
else
|
||||
log.debug("discarding SCADA_CRDN packet before linked")
|
||||
@@ -584,6 +665,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
-- init io controller
|
||||
iocontrol.init(conf, public)
|
||||
|
||||
self.sv_addr = src_addr
|
||||
self.sv_linked = true
|
||||
self.sv_config_err = false
|
||||
else
|
||||
@@ -607,11 +689,11 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION 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
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
if self.last_est_ack ~= est_ack then
|
||||
log.info("supervisor comms version mismatch")
|
||||
log.warning("supervisor comms version mismatch")
|
||||
end
|
||||
else
|
||||
log.debug("SCADA_MGMT establish packet reply (len = 1) unsupported")
|
||||
@@ -629,10 +711,10 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
local trip_time = util.time() - timestamp
|
||||
|
||||
if trip_time > 750 then
|
||||
log.warning("coord KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||
log.warning("coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug("coord RTT = " .. trip_time .. "ms")
|
||||
-- log.debug("coordinator RTT = " .. trip_time .. "ms")
|
||||
|
||||
iocontrol.get_db().facility.ps.publish("sv_ping", trip_time)
|
||||
|
||||
@@ -643,7 +725,9 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
sv_watchdog.cancel()
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.sv_linked = false
|
||||
self.sv_r_seq_num = nil
|
||||
println_ts("server connection closed by remote host")
|
||||
log.info("server connection closed by remote host")
|
||||
else
|
||||
@@ -656,7 +740,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
log.debug("illegal packet type " .. protocol .. " on supervisor listening channel", true)
|
||||
end
|
||||
else
|
||||
log.debug("received packet on unconfigured channel " .. l_port, true)
|
||||
log.debug("received packet for unknown channel " .. r_chan, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,6 +18,15 @@ local iocontrol = {}
|
||||
---@class ioctl
|
||||
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
|
||||
---@param conf facility_conf configuration
|
||||
---@param comms coord_comms comms reference
|
||||
@@ -45,11 +54,11 @@ function iocontrol.init(conf, comms)
|
||||
|
||||
radiation = types.new_zero_radiation_reading(),
|
||||
|
||||
save_cfg_ack = function (success) end, ---@param success boolean
|
||||
start_ack = function (success) end, ---@param success boolean
|
||||
stop_ack = function (success) end, ---@param success boolean
|
||||
scram_ack = function (success) end, ---@param success boolean
|
||||
ack_alarms_ack = function (success) end, ---@param success boolean
|
||||
save_cfg_ack = __generic_ack,
|
||||
start_ack = __generic_ack,
|
||||
stop_ack = __generic_ack,
|
||||
scram_ack = __generic_ack,
|
||||
ack_alarms_ack = __generic_ack,
|
||||
|
||||
ps = psil.create(),
|
||||
|
||||
@@ -74,7 +83,6 @@ function iocontrol.init(conf, comms)
|
||||
|
||||
---@class ioctl_unit
|
||||
local entry = {
|
||||
---@type integer
|
||||
unit_id = i,
|
||||
|
||||
num_boilers = 0,
|
||||
@@ -85,7 +93,8 @@ function iocontrol.init(conf, comms)
|
||||
waste_control = 0,
|
||||
radiation = types.new_zero_radiation_reading(),
|
||||
|
||||
a_group = 0, -- auto control group
|
||||
-- auto control group
|
||||
a_group = 0,
|
||||
|
||||
start = function () process.start(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
|
||||
|
||||
start_ack = function (success) end, ---@param success boolean
|
||||
scram_ack = function (success) end, ---@param success boolean
|
||||
reset_rps_ack = function (success) end, ---@param success boolean
|
||||
ack_alarms_ack = function (success) end, ---@param success boolean
|
||||
set_burn_ack = function (success) end, ---@param success boolean
|
||||
set_waste_ack = function (success) end, ---@param success boolean
|
||||
start_ack = __generic_ack,
|
||||
scram_ack = __generic_ack,
|
||||
reset_rps_ack = __generic_ack,
|
||||
ack_alarms_ack = __generic_ack,
|
||||
set_burn_ack = __generic_ack,
|
||||
set_waste_ack = __generic_ack,
|
||||
|
||||
alarm_callbacks = {
|
||||
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
|
||||
},
|
||||
|
||||
annunciator = {}, ---@type annunciator
|
||||
annunciator = {}, ---@type annunciator
|
||||
|
||||
unit_ps = psil.create(),
|
||||
reactor_data = {}, ---@type reactor_db
|
||||
reactor_data = {}, ---@type reactor_db
|
||||
|
||||
boiler_ps_tbl = {},
|
||||
boiler_data_tbl = {},
|
||||
@@ -657,8 +666,8 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
if type(rtu_statuses.rad_mon) == "table" then
|
||||
if #rtu_statuses.rad_mon > 0 then
|
||||
local rad_mon = rtu_statuses.rad_mon[1]
|
||||
local rtu_faulted = rad_mon[1] ---@type boolean
|
||||
unit.radiation = rad_mon[2] ---@type number
|
||||
-- local rtu_faulted = rad_mon[1] ---@type boolean
|
||||
unit.radiation = rad_mon[2] ---@type number
|
||||
|
||||
unit.unit_ps.publish("radiation", unit.radiation)
|
||||
else
|
||||
@@ -683,23 +692,13 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.annunciator) do
|
||||
if key == "TurbineTrip" then
|
||||
-- split up turbine trip table for all turbines and a general OR combination
|
||||
local trips = val
|
||||
local any = false
|
||||
|
||||
for id = 1, #trips do
|
||||
any = any or trips[id]
|
||||
unit.turbine_ps_tbl[id].publish(key, trips[id])
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("TurbineTrip", any)
|
||||
elseif key == "BoilerOnline" or key == "HeatingRateLow" or key == "WaterLevelLow" then
|
||||
if key == "BoilerOnline" or key == "HeatingRateLow" or key == "WaterLevelLow" then
|
||||
-- split up array for all boilers
|
||||
for id = 1, #val do
|
||||
unit.boiler_ps_tbl[id].publish(key, val[id])
|
||||
end
|
||||
elseif key == "TurbineOnline" or key == "SteamDumpOpen" or key == "TurbineOverSpeed" then
|
||||
elseif key == "TurbineOnline" or key == "SteamDumpOpen" or key == "TurbineOverSpeed" or
|
||||
key == "GeneratorTrip" or key == "TurbineTrip" then
|
||||
-- split up array for all turbines
|
||||
for id = 1, #val do
|
||||
unit.turbine_ps_tbl[id].publish(key, val[id])
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
-- Graphics Rendering Control
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
local log = require("scada-common.log")
|
||||
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 unit_view = require("coordinator.ui.layout.unit_view")
|
||||
local main_view = require("coordinator.ui.layout.main_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 = {}
|
||||
|
||||
-- render engine
|
||||
local engine = {
|
||||
monitors = nil,
|
||||
dmesg_window = nil,
|
||||
ui_ready = false
|
||||
}
|
||||
|
||||
-- UI layouts
|
||||
local ui = {
|
||||
main_layout = nil,
|
||||
unit_layouts = {}
|
||||
monitors = nil, ---@type monitors_struct|nil
|
||||
dmesg_window = nil, ---@type table|nil
|
||||
ui_ready = false,
|
||||
ui = {
|
||||
main_display = nil, ---@type graphics_element|nil
|
||||
unit_displays = {}
|
||||
}
|
||||
}
|
||||
|
||||
-- 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
|
||||
return true
|
||||
else
|
||||
for i = 1, #engine.monitors.unit_displays do
|
||||
if engine.monitors.unit_displays[i] == periph then
|
||||
return true
|
||||
end
|
||||
for _, monitor in ipairs(engine.monitors.unit_displays) do
|
||||
if monitor == periph then return true end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -74,7 +72,7 @@ function renderer.init_displays()
|
||||
_init_display(engine.monitors.primary)
|
||||
|
||||
-- init unit displays
|
||||
for _, monitor in pairs(engine.monitors.unit_displays) do
|
||||
for _, monitor in ipairs(engine.monitors.unit_displays) do
|
||||
_init_display(monitor)
|
||||
end
|
||||
end
|
||||
@@ -93,7 +91,7 @@ end
|
||||
function renderer.validate_unit_display_sizes()
|
||||
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()
|
||||
if w ~= 79 or h ~= 52 then
|
||||
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()
|
||||
local disp_x, disp_y = engine.monitors.primary.getSize()
|
||||
engine.dmesg_window = window.create(engine.monitors.primary, 1, 1, disp_x, disp_y)
|
||||
|
||||
log.direct_dmesg(engine.dmesg_window)
|
||||
end
|
||||
|
||||
@@ -119,11 +116,13 @@ function renderer.start_ui()
|
||||
engine.dmesg_window.setVisible(false)
|
||||
|
||||
-- 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
|
||||
for id, monitor in pairs(engine.monitors.unit_displays) do
|
||||
table.insert(ui.unit_layouts, unit_view(monitor, id))
|
||||
for i = 1, #engine.monitors.unit_displays do
|
||||
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
|
||||
|
||||
-- start flasher callback task
|
||||
@@ -136,29 +135,22 @@ end
|
||||
|
||||
-- close out the UI
|
||||
function renderer.close_ui()
|
||||
-- report ui as not ready
|
||||
engine.ui_ready = false
|
||||
|
||||
-- stop blinking indicators
|
||||
flasher.clear()
|
||||
|
||||
if engine.ui_ready then
|
||||
-- hide to stop animation callbacks
|
||||
ui.main_layout.hide()
|
||||
for i = 1, #ui.unit_layouts do
|
||||
ui.unit_layouts[i].hide()
|
||||
engine.monitors.unit_displays[i].clear()
|
||||
end
|
||||
else
|
||||
-- clear unit displays
|
||||
for i = 1, #ui.unit_layouts do
|
||||
engine.monitors.unit_displays[i].clear()
|
||||
end
|
||||
end
|
||||
-- delete element trees
|
||||
if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end
|
||||
for _, display in ipairs(engine.ui.unit_displays) do display.delete() end
|
||||
|
||||
-- report ui as not ready
|
||||
engine.ui_ready = false
|
||||
|
||||
-- clear root UI elements
|
||||
ui.main_layout = nil
|
||||
ui.unit_layouts = {}
|
||||
engine.ui.main_display = nil
|
||||
engine.ui.unit_displays = {}
|
||||
|
||||
-- clear unit monitors
|
||||
for _, monitor in ipairs(engine.monitors.unit_displays) do monitor.clear() end
|
||||
|
||||
-- re-draw dmesg
|
||||
engine.dmesg_window.setVisible(true)
|
||||
@@ -171,15 +163,17 @@ end
|
||||
function renderer.ui_ready() return engine.ui_ready end
|
||||
|
||||
-- handle a touch event
|
||||
---@param event monitor_touch
|
||||
function renderer.handle_touch(event)
|
||||
if event.monitor == engine.monitors.primary_name then
|
||||
ui.main_layout.handle_touch(event)
|
||||
else
|
||||
for id, monitor in pairs(engine.monitors.unit_name_map) do
|
||||
if event.monitor == monitor then
|
||||
local layout = ui.unit_layouts[id] ---@type graphics_element
|
||||
layout.handle_touch(event)
|
||||
---@param event mouse_interaction|nil
|
||||
function renderer.handle_mouse(event)
|
||||
if engine.ui_ready and event ~= nil then
|
||||
if event.monitor == engine.monitors.primary_name then
|
||||
engine.ui.main_display.handle_mouse(event)
|
||||
else
|
||||
for id, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
if event.monitor == monitor then
|
||||
local layout = engine.ui.unit_displays[id] ---@type graphics_element
|
||||
layout.handle_mouse(event)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
179
coordinator/session/apisessions.lua
Normal file
179
coordinator/session/apisessions.lua
Normal file
@@ -0,0 +1,179 @@
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local config = require("coordinator.config")
|
||||
|
||||
local pocket = require("coordinator.session.pocket")
|
||||
|
||||
local apisessions = {}
|
||||
|
||||
local self = {
|
||||
modem = nil,
|
||||
next_id = 0,
|
||||
sessions = {}
|
||||
}
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- handle a session output queue
|
||||
---@param session pkt_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(config.PKT_CHANNEL, config.CRD_CHANNEL, 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("[API] offending session: ", session))
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- cleanly close a session
|
||||
---@param session pkt_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(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message.raw_sendable())
|
||||
end
|
||||
end
|
||||
|
||||
log.debug(util.c("[API] closed session ", session))
|
||||
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 source_addr integer
|
||||
---@return pkt_session_struct|nil
|
||||
function apisessions.find_session(source_addr)
|
||||
for i = 1, #self.sessions do
|
||||
if self.sessions[i].s_addr == source_addr then return self.sessions[i] end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- establish a new API session
|
||||
---@nodiscard
|
||||
---@param source_addr integer
|
||||
---@param version string
|
||||
---@return integer session_id
|
||||
function apisessions.establish_session(source_addr, version)
|
||||
---@class pkt_session_struct
|
||||
local pkt_s = {
|
||||
open = true,
|
||||
version = version,
|
||||
s_addr = source_addr,
|
||||
in_queue = mqueue.new(),
|
||||
out_queue = mqueue.new(),
|
||||
instance = nil ---@type pkt_session
|
||||
}
|
||||
|
||||
local id = self.next_id
|
||||
|
||||
pkt_s.instance = pocket.new_session(id, source_addr, pkt_s.in_queue, pkt_s.out_queue, config.API_TIMEOUT)
|
||||
table.insert(self.sessions, pkt_s)
|
||||
|
||||
local mt = {
|
||||
---@param s pkt_session_struct
|
||||
__tostring = function (s) return util.c("PKT [", id, "] (@", s.s_addr, ")") end
|
||||
}
|
||||
|
||||
setmetatable(pkt_s, mt)
|
||||
|
||||
log.debug(util.c("[API] established new session: ", pkt_s))
|
||||
|
||||
self.next_id = id + 1
|
||||
|
||||
-- success
|
||||
return pkt_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 pkt_session_struct
|
||||
if session.open then
|
||||
local triggered = session.instance.check_wd(timer_event)
|
||||
if triggered then
|
||||
log.debug(util.c("[API] watchdog closing session ", session, "..."))
|
||||
_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 pkt_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 pkt_session_struct
|
||||
local on_delete = function (session)
|
||||
log.debug(util.c("[API] free'ing closed session ", session))
|
||||
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 pkt_session_struct
|
||||
if session.open then _shutdown(session) end
|
||||
end
|
||||
|
||||
apisessions.free_all_closed()
|
||||
end
|
||||
|
||||
return apisessions
|
||||
250
coordinator/session/pocket.lua
Normal file
250
coordinator/session/pocket.lua
Normal file
@@ -0,0 +1,250 @@
|
||||
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 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 = {
|
||||
}
|
||||
|
||||
pocket.API_S_CMDS = API_S_CMDS
|
||||
pocket.API_S_DATA = API_S_DATA
|
||||
|
||||
local PERIODICS = {
|
||||
KEEP_ALIVE = 2000
|
||||
}
|
||||
|
||||
-- pocket API session
|
||||
---@nodiscard
|
||||
---@param id integer session ID
|
||||
---@param s_addr integer device source address
|
||||
---@param in_queue mqueue in message queue
|
||||
---@param out_queue mqueue out message queue
|
||||
---@param timeout number communications timeout
|
||||
function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
local log_header = "pkt_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 pkt_session
|
||||
local public = {}
|
||||
|
||||
-- mark this pocket 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(s_addr, 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
|
||||
|
||||
-- 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 .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms")
|
||||
-- log.debug(log_header .. "PKT 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 pocket 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 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
|
||||
@@ -12,10 +12,11 @@ local ALARM_STATE = types.ALARM_STATE
|
||||
---@class sounder
|
||||
local sounder = {}
|
||||
|
||||
-- note: max samples = 0x20000 (128 * 1024 samples)
|
||||
|
||||
local _2_PI = 2 * math.pi -- 2 whole pies, hope you're hungry
|
||||
local _DRATE = 48000 -- 48kHz 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 test_alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
|
||||
|
||||
@@ -4,26 +4,25 @@
|
||||
|
||||
require("/initenv").init_env()
|
||||
|
||||
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 crash = require("scada-common.crash")
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcd = require("scada-common.tcd")
|
||||
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 coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
local config = require("coordinator.config")
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
|
||||
local COORDINATOR_VERSION = "v0.12.2"
|
||||
local apisessions = require("coordinator.session.apisessions")
|
||||
|
||||
local COORDINATOR_VERSION = "v0.16.1"
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
local log_graphics = coordinator.log_graphics
|
||||
@@ -38,12 +37,14 @@ local log_comms_connecting = coordinator.log_comms_connecting
|
||||
|
||||
local cfv = util.new_validator()
|
||||
|
||||
cfv.assert_port(config.SCADA_SV_PORT)
|
||||
cfv.assert_port(config.SCADA_SV_LISTEN)
|
||||
cfv.assert_port(config.SCADA_API_LISTEN)
|
||||
cfv.assert_channel(config.SVR_CHANNEL)
|
||||
cfv.assert_channel(config.CRD_CHANNEL)
|
||||
cfv.assert_channel(config.PKT_CHANNEL)
|
||||
cfv.assert_type_int(config.TRUSTED_RANGE)
|
||||
cfv.assert_type_num(config.COMMS_TIMEOUT)
|
||||
cfv.assert_min(config.COMMS_TIMEOUT, 2)
|
||||
cfv.assert_type_num(config.SV_TIMEOUT)
|
||||
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_num(config.SOUNDER_VOLUME)
|
||||
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(config.LOG_PATH, config.LOG_MODE)
|
||||
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
|
||||
|
||||
log.info("========================================")
|
||||
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
|
||||
@@ -142,13 +143,13 @@ local function main()
|
||||
end
|
||||
|
||||
-- create connection watchdog
|
||||
local conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT)
|
||||
local conn_watchdog = util.new_watchdog(config.SV_TIMEOUT)
|
||||
conn_watchdog.cancel()
|
||||
log.debug("startup> conn watchdog created")
|
||||
|
||||
-- start comms, open all channels
|
||||
local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.SCADA_SV_PORT, config.SCADA_SV_LISTEN,
|
||||
config.SCADA_API_LISTEN, config.TRUSTED_RANGE, conn_watchdog)
|
||||
local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.CRD_CHANNEL, config.SVR_CHANNEL,
|
||||
config.PKT_CHANNEL, config.TRUSTED_RANGE, conn_watchdog)
|
||||
log.debug("startup> comms init")
|
||||
log_comms("comms initialized")
|
||||
|
||||
@@ -162,7 +163,7 @@ local function main()
|
||||
|
||||
-- attempt to connect to the supervisor or exit
|
||||
local function init_connect_sv()
|
||||
local tick_waiting, task_done = log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SCADA_SV_PORT)
|
||||
local tick_waiting, task_done = log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SVR_CHANNEL)
|
||||
|
||||
-- attempt to establish a connection with the supervisory computer
|
||||
if not coord_comms.sv_connect(60, tick_waiting, task_done) then
|
||||
@@ -287,7 +288,7 @@ local function main()
|
||||
else
|
||||
log_sys("wired modem reconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
-- elseif type == "monitor" then
|
||||
-- not supported, system will exit on loss of in-use monitors
|
||||
elseif type == "speaker" then
|
||||
local msg = "alarm sounder speaker reconnected"
|
||||
@@ -300,6 +301,9 @@ local function main()
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
|
||||
-- iterate sessions
|
||||
apisessions.iterate_all()
|
||||
|
||||
-- free any closed sessions
|
||||
apisessions.free_all_closed()
|
||||
|
||||
@@ -326,11 +330,11 @@ local function main()
|
||||
else
|
||||
-- a non-clock/main watchdog timer event
|
||||
|
||||
--check API watchdogs
|
||||
-- check API watchdogs
|
||||
apisessions.check_all_watchdogs(param1)
|
||||
|
||||
-- notify timer callback dispatcher
|
||||
tcallbackdsp.handle(param1)
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
-- got a packet
|
||||
@@ -354,7 +358,7 @@ local function main()
|
||||
end
|
||||
elseif event == "monitor_touch" then
|
||||
-- handle a monitor touch event
|
||||
renderer.handle_touch(core.events.touch(param1, param2, param3))
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
elseif event == "speaker_audio_empty" then
|
||||
-- handle speaker buffer emptied
|
||||
sounder.continue()
|
||||
@@ -385,4 +389,6 @@ if not xpcall(main, crash.handler) then
|
||||
pcall(renderer.close_ui)
|
||||
pcall(sounder.stop)
|
||||
crash.exit()
|
||||
else
|
||||
log.close()
|
||||
end
|
||||
|
||||
@@ -9,8 +9,8 @@ local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
|
||||
local cpair = core.graphics.cpair
|
||||
local border = core.graphics.border
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
-- new boiler view
|
||||
---@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 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)
|
||||
ps.subscribe("temperature", temp.update)
|
||||
ps.subscribe("boil_rate", boil_r.update)
|
||||
status.register(ps, "computed_status", status.update)
|
||||
temp.register(ps, "temperature", temp.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="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 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)
|
||||
ps.subscribe("water_fill", water.update)
|
||||
ps.subscribe("steam_fill", steam.update)
|
||||
ps.subscribe("ccool_fill", ccool.update)
|
||||
hcool.register(ps, "hcool_fill", hcool.update)
|
||||
water.register(ps, "water_fill", water.update)
|
||||
steam.register(ps, "steam_fill", steam.update)
|
||||
ccool.register(ps, "ccool_fill", ccool.update)
|
||||
end
|
||||
|
||||
return new_view
|
||||
|
||||
@@ -13,10 +13,10 @@ local PowerIndicator = require("graphics.elements.indicators.power")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
|
||||
local cpair = core.graphics.cpair
|
||||
local border = core.graphics.border
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
-- new induction matrix view
|
||||
---@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_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)
|
||||
ps.subscribe("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)
|
||||
ps.subscribe("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)
|
||||
status.register(ps, "computed_status", status.update)
|
||||
energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end)
|
||||
capacity.register(ps, "max_energy", function (val) capacity.update(util.joules_to_fe(val)) end)
|
||||
input.register(ps, "last_input", function (val) input.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)
|
||||
ps.subscribe("avg_inflow", avg_in.update)
|
||||
ps.subscribe("avg_outflow", avg_out.update)
|
||||
avg_chg.register(ps, "avg_charge", avg_chg.update)
|
||||
avg_in.register(ps, "avg_inflow", avg_in.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}
|
||||
|
||||
@@ -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}
|
||||
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)
|
||||
ps.subscribe("providers", providers.update)
|
||||
ps.subscribe("energy_fill", function (val) fill.update(val * 100) end)
|
||||
ps.subscribe("transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end)
|
||||
cells.register(ps, "cells", cells.update)
|
||||
providers.register(ps, "providers", providers.update)
|
||||
fill.register(ps, "energy_fill", function (val) fill.update(val * 100) 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 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
|
||||
|
||||
ps.subscribe("energy_fill", charge.update)
|
||||
ps.subscribe("last_input", function (val) in_cap.update(calc_saturation(val)) end)
|
||||
ps.subscribe("last_output", function (val) out_cap.update(calc_saturation(val)) end)
|
||||
charge.register(ps, "energy_fill", charge.update)
|
||||
in_cap.register(ps, "last_input", function (val) in_cap.update(calc_saturation(val)) end)
|
||||
out_cap.register(ps, "last_output", function (val) out_cap.update(calc_saturation(val)) end)
|
||||
end
|
||||
|
||||
return new_view
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
local tcd = require("scada-common.tcallbackdsp")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
@@ -21,10 +21,10 @@ local HazardButton = require("graphics.elements.controls.hazard_button")
|
||||
local RadioButton = require("graphics.elements.controls.radio_button")
|
||||
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 border = core.graphics.border
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local period = core.flasher.PERIOD
|
||||
|
||||
@@ -33,7 +33,7 @@ local period = core.flasher.PERIOD
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
local function new_view(root, x, y)
|
||||
assert(root.height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||
assert(root.get_height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
@@ -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 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)
|
||||
facility.induction_ps_tbl[1].subscribe("computed_status", function (status) ind_mat.update(status > 1) end)
|
||||
facility.ps.subscribe("rad_computed_status", rad_mon.update)
|
||||
all_ok.register(facility.ps, "all_sys_ok", all_ok.update)
|
||||
ind_mat.register(facility.induction_ps_tbl[1], "computed_status", function (status) ind_mat.update(status > 1) end)
|
||||
rad_mon.register(facility.ps, "rad_computed_status", rad_mon.update)
|
||||
|
||||
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_sat = IndicatorLight{parent=main,label="Min/Max Burn Rate",colors=cpair(colors.yellow,colors.gray)}
|
||||
|
||||
facility.ps.subscribe("auto_ready", auto_ready.update)
|
||||
facility.ps.subscribe("auto_active", auto_act.update)
|
||||
facility.ps.subscribe("auto_ramping", auto_ramp.update)
|
||||
facility.ps.subscribe("auto_saturated", auto_sat.update)
|
||||
auto_ready.register(facility.ps, "auto_ready", auto_ready.update)
|
||||
auto_act.register(facility.ps, "auto_active", auto_act.update)
|
||||
auto_ramp.register(facility.ps, "auto_ramping", auto_ramp.update)
|
||||
auto_sat.register(facility.ps, "auto_saturated", auto_sat.update)
|
||||
|
||||
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 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)
|
||||
facility.ps.subscribe("as_matrix_dc", matrix_dc.update)
|
||||
facility.ps.subscribe("as_matrix_fill", matrix_fill.update)
|
||||
facility.ps.subscribe("as_crit_alarm", unit_crit.update)
|
||||
facility.ps.subscribe("as_radiation", fac_rad_h.update)
|
||||
facility.ps.subscribe("as_gen_fault", gen_fault.update)
|
||||
auto_scram.register(facility.ps, "auto_scram", auto_scram.update)
|
||||
matrix_dc.register(facility.ps, "as_matrix_dc", matrix_dc.update)
|
||||
matrix_fill.register(facility.ps, "as_matrix_fill", matrix_fill.update)
|
||||
unit_crit.register(facility.ps, "as_crit_alarm", unit_crit.update)
|
||||
fac_rad_h.register(facility.ps, "as_radiation", fac_rad_h.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}
|
||||
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}
|
||||
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 --
|
||||
@@ -115,8 +115,8 @@ local function new_view(root, x, y)
|
||||
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)}
|
||||
|
||||
facility.ps.subscribe("process_burn_target", b_target.set_value)
|
||||
facility.ps.subscribe("burn_sum", burn_sum.update)
|
||||
b_target.register(facility.ps, "process_burn_target", b_target.set_value)
|
||||
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)}
|
||||
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"}
|
||||
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)
|
||||
facility.induction_ps_tbl[1].subscribe("energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end)
|
||||
c_target.register(facility.ps, "process_charge_target", c_target.set_value)
|
||||
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)}
|
||||
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"}
|
||||
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)
|
||||
facility.induction_ps_tbl[1].subscribe("last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end)
|
||||
g_target.register(facility.ps, "process_gen_target", g_target.set_value)
|
||||
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 --
|
||||
@@ -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}
|
||||
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)
|
||||
unit.unit_ps.subscribe("burn_limit", rate_limits[i].set_value)
|
||||
rate_limits[i].register(unit.unit_ps, "max_burn", rate_limits[i].set_max)
|
||||
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)}
|
||||
|
||||
unit.unit_ps.subscribe("act_burn_rate", cur_burn.update)
|
||||
cur_burn.register(unit.unit_ps, "act_burn_rate", cur_burn.update)
|
||||
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 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)
|
||||
unit.unit_ps.subscribe("U_AutoDegraded", degraded.update)
|
||||
ready.register(unit.unit_ps, "U_AutoReady", ready.update)
|
||||
degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update)
|
||||
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 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 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)}
|
||||
|
||||
facility.ps.subscribe("status_line_1", stat_line_1.set_value)
|
||||
facility.ps.subscribe("status_line_2", stat_line_2.set_value)
|
||||
stat_line_1.register(facility.ps, "status_line_1", stat_line_1.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)}
|
||||
|
||||
@@ -233,11 +233,14 @@ local function new_view(root, x, y)
|
||||
tcd.dispatch(0.2, function () save.on_response(ack) 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
|
||||
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
|
||||
b_target.disable()
|
||||
c_target.disable()
|
||||
@@ -246,9 +249,7 @@ local function new_view(root, x, y)
|
||||
mode.disable()
|
||||
start.disable()
|
||||
|
||||
for i = 1, #rate_limits do
|
||||
rate_limits[i].disable()
|
||||
end
|
||||
for i = 1, #rate_limits do rate_limits[i].disable() end
|
||||
else
|
||||
b_target.enable()
|
||||
c_target.enable()
|
||||
@@ -257,9 +258,7 @@ local function new_view(root, x, y)
|
||||
mode.enable()
|
||||
if facility.auto_ready then start.enable() end
|
||||
|
||||
for i = 1, #rate_limits do
|
||||
rate_limits[i].enable()
|
||||
end
|
||||
for i = 1, #rate_limits do rate_limits[i].enable() end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -11,16 +11,15 @@ local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local HorizontalBar = require("graphics.elements.indicators.hbar")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
|
||||
local cpair = core.graphics.cpair
|
||||
local border = core.graphics.border
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
-- create new reactor view
|
||||
---@param root graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param data reactor_db reactor data
|
||||
---@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 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 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)
|
||||
ps.subscribe("temp", core_temp.update)
|
||||
ps.subscribe("act_burn_rate", burn_r.update)
|
||||
ps.subscribe("heating_rate", heating_r.update)
|
||||
status.register(ps, "computed_status", status.update)
|
||||
core_temp.register(ps, "temp", core_temp.update)
|
||||
burn_r.register(ps, "act_burn_rate", burn_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}
|
||||
|
||||
@@ -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 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
|
||||
ccool.recolor(cpair(colors.lightBlue, colors.gray))
|
||||
else
|
||||
@@ -56,7 +55,7 @@ local function new_view(root, x, y, data, ps)
|
||||
end
|
||||
end)
|
||||
|
||||
ps.subscribe("hcool_type", function (type)
|
||||
hcool.register(ps, "hcool_type", function (type)
|
||||
if type == types.FLUID.SUPERHEATED_SODIUM then
|
||||
hcool.recolor(cpair(colors.orange, colors.gray))
|
||||
else
|
||||
@@ -64,10 +63,10 @@ local function new_view(root, x, y, data, ps)
|
||||
end
|
||||
end)
|
||||
|
||||
ps.subscribe("fuel_fill", fuel.update)
|
||||
ps.subscribe("ccool_fill", ccool.update)
|
||||
ps.subscribe("hcool_fill", hcool.update)
|
||||
ps.subscribe("waste_fill", waste.update)
|
||||
fuel.register(ps, "fuel_fill", fuel.update)
|
||||
ccool.register(ps, "ccool_fill", ccool.update)
|
||||
hcool.register(ps, "hcool_fill", hcool.update)
|
||||
waste.register(ps, "waste_fill", waste.update)
|
||||
end
|
||||
|
||||
return new_view
|
||||
|
||||
@@ -12,8 +12,8 @@ local PowerIndicator = require("graphics.elements.indicators.power")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
|
||||
local cpair = core.graphics.cpair
|
||||
local border = core.graphics.border
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
-- new turbine view
|
||||
---@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 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)
|
||||
ps.subscribe("prod_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end)
|
||||
ps.subscribe("flow_rate", flow_rate.update)
|
||||
status.register(ps, "computed_status", status.update)
|
||||
prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end)
|
||||
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 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="E",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||
|
||||
ps.subscribe("steam_fill", steam.update)
|
||||
ps.subscribe("energy_fill", energy.update)
|
||||
steam.register(ps, "steam_fill", steam.update)
|
||||
energy.register(ps, "energy_fill", energy.update)
|
||||
end
|
||||
|
||||
return new_view
|
||||
|
||||
@@ -26,10 +26,10 @@ local PushButton = require("graphics.elements.controls.push_button")
|
||||
local RadioButton = require("graphics.elements.controls.radio_button")
|
||||
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 border = core.graphics.border
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
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}
|
||||
u_ps.subscribe("temp", core_map.update)
|
||||
u_ps.subscribe("size", function (s) core_map.resize(s[1], s[2]) end)
|
||||
core_map.register(u_ps, "temp", core_map.update)
|
||||
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}
|
||||
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}
|
||||
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="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 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)
|
||||
u_ps.subscribe("ccool_fill", ccool.update)
|
||||
u_ps.subscribe("hcool_fill", hcool.update)
|
||||
u_ps.subscribe("waste_fill", waste.update)
|
||||
fuel.register(u_ps, "fuel_fill", fuel.update)
|
||||
ccool.register(u_ps, "ccool_fill", ccool.update)
|
||||
hcool.register(u_ps, "hcool_fill", hcool.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
|
||||
ccool.recolor(cpair(colors.lightBlue, colors.gray))
|
||||
else
|
||||
@@ -115,7 +115,7 @@ local function init(parent, id)
|
||||
end
|
||||
end)
|
||||
|
||||
u_ps.subscribe("hcool_type", function (type)
|
||||
hcool.register(u_ps, "hcool_type", function (type)
|
||||
if type == "mekanism:superheated_sodium" then
|
||||
hcool.recolor(cpair(colors.orange, colors.gray))
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
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 --
|
||||
@@ -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_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)
|
||||
u_ps.subscribe("U_StatusLine2", stat_line_2.set_value)
|
||||
stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value)
|
||||
stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value)
|
||||
|
||||
-----------------
|
||||
-- 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 rad_mon = TriIndicatorLight{parent=annunciator,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
|
||||
|
||||
u_ps.subscribe("PLCOnline", plc_online.update)
|
||||
u_ps.subscribe("PLCHeartbeat", plc_hbeat.update)
|
||||
u_ps.subscribe("RadiationMonitor", rad_mon.update)
|
||||
plc_online.register(u_ps, "PLCOnline", plc_online.update)
|
||||
plc_hbeat.register(u_ps, "PLCHeartbeat", plc_hbeat.update)
|
||||
rad_mon.register(u_ps, "RadiationMonitor", rad_mon.update)
|
||||
|
||||
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_auto = IndicatorLight{parent=annunciator,label="Automatic Control",colors=cpair(colors.white,colors.gray)}
|
||||
|
||||
u_ps.subscribe("status", r_active.update)
|
||||
u_ps.subscribe("AutoControl", r_auto.update)
|
||||
r_active.register(u_ps, "status", r_active.update)
|
||||
r_auto.register(u_ps, "AutoControl", r_auto.update)
|
||||
|
||||
-- main unit transient/warning annunciator panel
|
||||
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_hsrt = IndicatorLight{parent=annunciator,label="Startup Rate High",colors=cpair(colors.yellow,colors.gray)}
|
||||
|
||||
u_ps.subscribe("ReactorSCRAM", r_scram.update)
|
||||
u_ps.subscribe("ManualReactorSCRAM", r_mscrm.update)
|
||||
u_ps.subscribe("AutoReactorSCRAM", r_ascrm.update)
|
||||
u_ps.subscribe("RadiationWarning", rad_wrn.update)
|
||||
u_ps.subscribe("RCPTrip", r_rtrip.update)
|
||||
u_ps.subscribe("RCSFlowLow", r_cflow.update)
|
||||
u_ps.subscribe("CoolantLevelLow", r_clow.update)
|
||||
u_ps.subscribe("ReactorTempHigh", r_temp.update)
|
||||
u_ps.subscribe("ReactorHighDeltaT", r_rhdt.update)
|
||||
u_ps.subscribe("FuelInputRateLow", r_firl.update)
|
||||
u_ps.subscribe("WasteLineOcclusion", r_wloc.update)
|
||||
u_ps.subscribe("HighStartupRate", r_hsrt.update)
|
||||
r_scram.register(u_ps, "ReactorSCRAM", r_scram.update)
|
||||
r_mscrm.register(u_ps, "ManualReactorSCRAM", r_mscrm.update)
|
||||
r_ascrm.register(u_ps, "AutoReactorSCRAM", r_ascrm.update)
|
||||
rad_wrn.register(u_ps, "RadiationWarning", rad_wrn.update)
|
||||
r_rtrip.register(u_ps, "RCPTrip", r_rtrip.update)
|
||||
r_cflow.register(u_ps, "RCSFlowLow", r_cflow.update)
|
||||
r_clow.register(u_ps, "CoolantLevelLow", r_clow.update)
|
||||
r_temp.register(u_ps, "ReactorTempHigh", r_temp.update)
|
||||
r_rhdt.register(u_ps, "ReactorHighDeltaT", r_rhdt.update)
|
||||
r_firl.register(u_ps, "FuelInputRateLow", r_firl.update)
|
||||
r_wloc.register(u_ps, "WasteLineOcclusion", r_wloc.update)
|
||||
r_hsrt.register(u_ps, "HighStartupRate", r_hsrt.update)
|
||||
|
||||
-- RPS annunciator panel
|
||||
|
||||
@@ -220,23 +220,23 @@ 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_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)
|
||||
u_ps.subscribe("high_dmg", rps_dmg.update)
|
||||
u_ps.subscribe("ex_hcool", rps_exh.update)
|
||||
u_ps.subscribe("ex_waste", rps_exw.update)
|
||||
u_ps.subscribe("high_temp", rps_tmp.update)
|
||||
u_ps.subscribe("no_fuel", rps_nof.update)
|
||||
u_ps.subscribe("low_cool", rps_loc.update)
|
||||
u_ps.subscribe("fault", rps_flt.update)
|
||||
u_ps.subscribe("timeout", rps_tmo.update)
|
||||
u_ps.subscribe("sys_fail", rps_sfl.update)
|
||||
rps_trp.register(u_ps, "rps_tripped", rps_trp.update)
|
||||
rps_dmg.register(u_ps, "high_dmg", rps_dmg.update)
|
||||
rps_exh.register(u_ps, "ex_hcool", rps_exh.update)
|
||||
rps_exw.register(u_ps, "ex_waste", rps_exw.update)
|
||||
rps_tmp.register(u_ps, "high_temp", rps_tmp.update)
|
||||
rps_nof.register(u_ps, "no_fuel", rps_nof.update)
|
||||
rps_loc.register(u_ps, "low_cool", rps_loc.update)
|
||||
rps_flt.register(u_ps, "fault", rps_flt.update)
|
||||
rps_tmo.register(u_ps, "timeout", rps_tmo.update)
|
||||
rps_sfl.register(u_ps, "sys_fail", rps_sfl.update)
|
||||
|
||||
-- cooling annunciator panel
|
||||
|
||||
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=22}
|
||||
local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23}
|
||||
local rcs_annunc = Div{parent=rcs,width=27,height=23,x=2,y=1}
|
||||
local rcs_tags = Div{parent=rcs,width=2,height=14,x=29,y=9}
|
||||
local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1}
|
||||
local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7}
|
||||
|
||||
local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=colors.gray,c2=colors.white,c3=colors.green}
|
||||
@@ -244,86 +244,112 @@ local function init(parent, id)
|
||||
local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate 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_tbnt = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
|
||||
u_ps.subscribe("RCSFault", c_flt.update)
|
||||
u_ps.subscribe("EmergencyCoolant", c_emg.update)
|
||||
u_ps.subscribe("CoolantFeedMismatch", c_cfm.update)
|
||||
u_ps.subscribe("BoilRateMismatch", c_brm.update)
|
||||
u_ps.subscribe("SteamFeedMismatch", c_sfm.update)
|
||||
u_ps.subscribe("MaxWaterReturnFeed", c_mwrf.update)
|
||||
u_ps.subscribe("TurbineTrip", c_tbnt.update)
|
||||
c_flt.register(u_ps, "RCSFault", c_flt.update)
|
||||
c_emg.register(u_ps, "EmergencyCoolant", c_emg.update)
|
||||
c_cfm.register(u_ps, "CoolantFeedMismatch", c_cfm.update)
|
||||
c_brm.register(u_ps, "BoilRateMismatch", c_brm.update)
|
||||
c_sfm.register(u_ps, "SteamFeedMismatch", c_sfm.update)
|
||||
c_mwrf.register(u_ps, "MaxWaterReturnFeed", c_mwrf.update)
|
||||
|
||||
rcs_annunc.line_break()
|
||||
local available_space = 16 - (unit.num_boilers * 2 + unit.num_turbines * 4)
|
||||
|
||||
local function _add_space()
|
||||
-- if we have some extra space, add padding
|
||||
rcs_tags.line_break()
|
||||
rcs_annunc.line_break()
|
||||
end
|
||||
|
||||
-- boiler annunciator panel(s)
|
||||
|
||||
if available_space > 0 then _add_space() end
|
||||
|
||||
if unit.num_boilers > 0 then
|
||||
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)}
|
||||
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}
|
||||
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
|
||||
if unit.num_boilers > 1 then
|
||||
-- note, can't (shouldn't for sure...) have 0 turbines
|
||||
if (available_space > 2 and unit.num_turbines == 1) or
|
||||
(available_space > 3 and unit.num_turbines == 2) or
|
||||
(available_space > 4) then
|
||||
_add_space()
|
||||
end
|
||||
|
||||
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)}
|
||||
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}
|
||||
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
|
||||
|
||||
-- turbine annunciator panels
|
||||
|
||||
if unit.num_boilers == 0 then
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
else
|
||||
rcs_tags.line_break()
|
||||
rcs_annunc.line_break()
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
end
|
||||
if available_space > 1 then _add_space() end
|
||||
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
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}
|
||||
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 (available_space > 2 and unit.num_turbines == 2) or available_space > 3 then
|
||||
_add_space()
|
||||
end
|
||||
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
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}
|
||||
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
|
||||
|
||||
if unit.num_turbines > 2 then
|
||||
if available_space > 3 then _add_space() end
|
||||
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
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}
|
||||
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
|
||||
|
||||
----------------------
|
||||
@@ -339,8 +365,8 @@ local function init(parent, id)
|
||||
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}
|
||||
|
||||
u_ps.subscribe("burn_rate", burn_rate.set_value)
|
||||
u_ps.subscribe("max_burn", burn_rate.set_max)
|
||||
burn_rate.register(u_ps, "burn_rate", burn_rate.set_value)
|
||||
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 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}
|
||||
@@ -361,9 +387,12 @@ local function init(parent, id)
|
||||
end
|
||||
end
|
||||
|
||||
u_ps.subscribe("status", start_button_en_check)
|
||||
u_ps.subscribe("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, "status", start_button_en_check)
|
||||
start.register(u_ps, "rps_tripped", start_button_en_check)
|
||||
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}
|
||||
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
|
||||
@@ -371,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}
|
||||
|
||||
u_ps.subscribe("U_WasteMode", waste_mode.set_value)
|
||||
waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)
|
||||
|
||||
----------------------
|
||||
-- alarm management --
|
||||
@@ -394,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_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)
|
||||
u_ps.subscribe("Alarm_2", a_rad.update)
|
||||
u_ps.subscribe("Alarm_4", a_dmg.update)
|
||||
a_brc.register(u_ps, "Alarm_1", a_brc.update)
|
||||
a_rad.register(u_ps, "Alarm_2", a_rad.update)
|
||||
a_dmg.register(u_ps, "Alarm_4", a_dmg.update)
|
||||
|
||||
u_ps.subscribe("Alarm_3", a_rcl.update)
|
||||
u_ps.subscribe("Alarm_5", a_rcd.update)
|
||||
u_ps.subscribe("Alarm_6", a_rot.update)
|
||||
u_ps.subscribe("Alarm_7", a_rht.update)
|
||||
u_ps.subscribe("Alarm_8", a_rwl.update)
|
||||
u_ps.subscribe("Alarm_9", a_rwh.update)
|
||||
a_rcl.register(u_ps, "Alarm_3", a_rcl.update)
|
||||
a_rcd.register(u_ps, "Alarm_5", a_rcd.update)
|
||||
a_rot.register(u_ps, "Alarm_6", a_rot.update)
|
||||
a_rht.register(u_ps, "Alarm_7", a_rht.update)
|
||||
a_rwl.register(u_ps, "Alarm_8", a_rwl.update)
|
||||
a_rwh.register(u_ps, "Alarm_9", a_rwh.update)
|
||||
|
||||
u_ps.subscribe("Alarm_10", a_rps.update)
|
||||
u_ps.subscribe("Alarm_11", a_clt.update)
|
||||
u_ps.subscribe("Alarm_12", a_tbt.update)
|
||||
a_rps.register(u_ps, "Alarm_10", a_rps.update)
|
||||
a_clt.register(u_ps, "Alarm_11", a_clt.update)
|
||||
a_tbt.register(u_ps, "Alarm_12", a_tbt.update)
|
||||
|
||||
-- ack's and resets
|
||||
|
||||
@@ -461,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}
|
||||
|
||||
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()
|
||||
|
||||
@@ -473,44 +502,35 @@ local function init(parent, id)
|
||||
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}
|
||||
|
||||
u_ps.subscribe("auto_group", auto_grp.set_value)
|
||||
auto_grp.register(u_ps, "auto_group", auto_grp.set_value)
|
||||
|
||||
auto_div.line_break()
|
||||
|
||||
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}
|
||||
|
||||
u_ps.subscribe("U_AutoReady", a_rdy.update)
|
||||
a_rdy.register(u_ps, "U_AutoReady", a_rdy.update)
|
||||
|
||||
-- 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))
|
||||
end)
|
||||
|
||||
-- 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()
|
||||
|
||||
a_stb.register(u_ps, "AutoControl", function (auto_active)
|
||||
if auto_active then
|
||||
a_stb.update(unit.reactor_data.mek_status.status == false)
|
||||
else a_stb.update(false) 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
|
||||
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
|
||||
end)
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ local Div = require("graphics.elements.div")
|
||||
local PipeNetwork = require("graphics.elements.pipenet")
|
||||
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
|
||||
---@param parent graphics_element parent
|
||||
@@ -24,22 +24,21 @@ local pipe = core.graphics.pipe
|
||||
---@param y integer top left y
|
||||
---@param unit ioctl_unit unit database entry
|
||||
local function make(parent, x, y, unit)
|
||||
local height = 0
|
||||
local num_boilers = #unit.boiler_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_turbines >= 1 and num_turbines <= 3, "minimum 1 turbine, maximum 3 turbines")
|
||||
|
||||
local height = 25
|
||||
|
||||
if num_boilers == 0 and num_turbines == 1 then
|
||||
height = 9
|
||||
elseif num_boilers == 1 and num_turbines <= 2 then
|
||||
height = 17
|
||||
else
|
||||
height = 25
|
||||
end
|
||||
|
||||
assert(parent.height() >= (y + height), "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||
assert(parent.get_height() >= (y + height), "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||
|
||||
-- bounding box div
|
||||
local root = Div{parent=parent,x=x,y=y,width=80,height=height}
|
||||
@@ -51,7 +50,7 @@ local function make(parent, x, y, unit)
|
||||
-- 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
|
||||
local coolant_pipes = {}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
--
|
||||
-- Reactor Unit Waiting Spinner
|
||||
--
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local WaitingAnim = require("graphics.elements.animations.waiting")
|
||||
|
||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
||||
|
||||
local cpair = core.graphics.cpair
|
||||
|
||||
-- create a unit waiting view
|
||||
---@param parent graphics_element parent
|
||||
---@param y integer y offset
|
||||
local function init(parent, y)
|
||||
-- bounding box div
|
||||
local root = Div{parent=parent,x=1,y=y,height=5}
|
||||
|
||||
local waiting_x = math.floor(parent.width() / 2) - 2
|
||||
|
||||
TextBox{parent=root,text="Waiting for status...",alignment=TEXT_ALIGN.CENTER,y=1,height=1,fg_bg=cpair(colors.black,style.root.bkg)}
|
||||
WaitingAnim{parent=root,x=waiting_x,y=3,fg_bg=cpair(colors.blue,style.root.bkg)}
|
||||
|
||||
return root
|
||||
end
|
||||
|
||||
return init
|
||||
@@ -5,7 +5,6 @@
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local sounder = require("coordinator.sounder")
|
||||
|
||||
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 ColorMap = require("graphics.elements.colormap")
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
local Div = require("graphics.elements.div")
|
||||
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 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
|
||||
---@param monitor table main viewscreen
|
||||
local function init(monitor)
|
||||
---@param main graphics_element main displaybox
|
||||
local function init(main)
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
|
||||
local main = DisplayBox{window=monitor,fg_bg=style.root}
|
||||
|
||||
-- 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 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"
|
||||
local datetime = TextBox{parent=main,x=(header.width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
|
||||
|
||||
facility.ps.subscribe("sv_ping", ping.update)
|
||||
facility.ps.subscribe("date_time", datetime.set_value)
|
||||
ping.register(facility.ps, "sv_ping", ping.update)
|
||||
datetime.register(facility.ps, "date_time", datetime.set_value)
|
||||
|
||||
local uo_1, uo_2, uo_3, uo_4 ---@type graphics_element
|
||||
|
||||
@@ -54,12 +45,12 @@ local function init(monitor)
|
||||
-- unit overviews
|
||||
if facility.num_units >= 1 then
|
||||
uo_1 = unit_overview(main, 2, 3, units[1])
|
||||
row_1_height = uo_1.height()
|
||||
row_1_height = uo_1.get_height()
|
||||
end
|
||||
|
||||
if facility.num_units >= 2 then
|
||||
uo_2 = unit_overview(main, 84, 3, units[2])
|
||||
row_1_height = math.max(row_1_height, uo_2.height())
|
||||
row_1_height = math.max(row_1_height, uo_2.get_height())
|
||||
end
|
||||
|
||||
cnc_y_start = cnc_y_start + row_1_height + 1
|
||||
@@ -69,11 +60,11 @@ local function init(monitor)
|
||||
local row_2_offset = cnc_y_start
|
||||
|
||||
uo_3 = unit_overview(main, 2, row_2_offset, units[3])
|
||||
cnc_y_start = row_2_offset + uo_3.height() + 1
|
||||
cnc_y_start = row_2_offset + uo_3.get_height() + 1
|
||||
|
||||
if facility.num_units == 4 then
|
||||
uo_4 = unit_overview(main, 84, row_2_offset, units[4])
|
||||
cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.height() + 1)
|
||||
cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.get_height() + 1)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -82,19 +73,17 @@ local function init(monitor)
|
||||
cnc_y_start = cnc_y_start
|
||||
|
||||
-- induction matrix and process control interfaces are 24 tall + space needed for divider
|
||||
local cnc_bottom_align_start = main.height() - 26
|
||||
local cnc_bottom_align_start = main.get_height() - 26
|
||||
|
||||
assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||
|
||||
TextBox{parent=main,y=cnc_bottom_align_start,text=util.strrep("\x8c", header.width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=cpair(colors.lightGray,colors.gray)}
|
||||
TextBox{parent=main,y=cnc_bottom_align_start,text=util.strrep("\x8c", header.get_width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=cpair(colors.lightGray,colors.gray)}
|
||||
|
||||
cnc_bottom_align_start = cnc_bottom_align_start + 2
|
||||
|
||||
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])
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return init
|
||||
|
||||
@@ -2,21 +2,13 @@
|
||||
-- Reactor Unit SCADA Coordinator GUI
|
||||
--
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local unit_detail = require("coordinator.ui.components.unit_detail")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
|
||||
-- create a unit view
|
||||
---@param monitor table
|
||||
---@param main graphics_element main displaybox
|
||||
---@param id integer
|
||||
local function init(monitor, id)
|
||||
local main = DisplayBox{window=monitor,fg_bg=style.root}
|
||||
|
||||
local function init(main, id)
|
||||
unit_detail(main, id)
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return init
|
||||
|
||||
@@ -6,7 +6,7 @@ local core = require("graphics.core")
|
||||
|
||||
local style = {}
|
||||
|
||||
local cpair = core.graphics.cpair
|
||||
local cpair = core.cpair
|
||||
|
||||
-- GLOBAL --
|
||||
|
||||
|
||||
@@ -1,40 +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 flasher = require("graphics.flasher")
|
||||
|
||||
core.flasher = flasher
|
||||
|
||||
local events = {}
|
||||
|
||||
---@class monitor_touch
|
||||
---@field monitor string
|
||||
---@field x integer
|
||||
---@field y integer
|
||||
|
||||
-- create a new touch event definition
|
||||
---@nodiscard
|
||||
---@param monitor string
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return monitor_touch
|
||||
function events.touch(monitor, x, y)
|
||||
return {
|
||||
monitor = monitor,
|
||||
x = x,
|
||||
y = y
|
||||
}
|
||||
end
|
||||
|
||||
core.events = events
|
||||
|
||||
local graphics = {}
|
||||
-- Core Types
|
||||
|
||||
---@enum TEXT_ALIGN
|
||||
graphics.TEXT_ALIGN = {
|
||||
core.TEXT_ALIGN = {
|
||||
LEFT = 1,
|
||||
CENTER = 2,
|
||||
RIGHT = 3
|
||||
@@ -53,7 +32,7 @@ graphics.TEXT_ALIGN = {
|
||||
---@param color color border color
|
||||
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
|
||||
---@return graphics_border
|
||||
function graphics.border(width, color, even)
|
||||
function core.border(width, color, even)
|
||||
return {
|
||||
width = width,
|
||||
color = color,
|
||||
@@ -74,7 +53,7 @@ end
|
||||
---@param w integer
|
||||
---@param h integer
|
||||
---@return graphics_frame
|
||||
function graphics.gframe(x, y, w, h)
|
||||
function core.gframe(x, y, w, h)
|
||||
return {
|
||||
x = x,
|
||||
y = y,
|
||||
@@ -98,7 +77,7 @@ end
|
||||
---@param a color
|
||||
---@param b color
|
||||
---@return cpair
|
||||
function graphics.cpair(a, b)
|
||||
function core.cpair(a, b)
|
||||
return {
|
||||
-- color pairs
|
||||
color_a = a,
|
||||
@@ -135,7 +114,7 @@ end
|
||||
---@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
|
||||
---@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 {
|
||||
x1 = x1,
|
||||
y1 = y1,
|
||||
@@ -149,6 +128,4 @@ function graphics.pipe(x1, y1, x2, y2, color, thin, align_tr)
|
||||
}
|
||||
end
|
||||
|
||||
core.graphics = graphics
|
||||
|
||||
return core
|
||||
|
||||
@@ -12,12 +12,11 @@ local element = {}
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer next line if omitted
|
||||
---@field offset_x? integer 0 if omitted
|
||||
---@field offset_y? integer 0 if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
---@alias graphics_args graphics_args_generic
|
||||
---|waiting_args
|
||||
@@ -25,13 +24,18 @@ local element = {}
|
||||
---|multi_button_args
|
||||
---|push_button_args
|
||||
---|radio_button_args
|
||||
---|sidebar_args
|
||||
---|spinbox_args
|
||||
---|switch_button_args
|
||||
---|tabbar_args
|
||||
---|alarm_indicator_light
|
||||
---|core_map_args
|
||||
---|data_indicator_args
|
||||
---|hbar_args
|
||||
---|icon_indicator_args
|
||||
---|indicator_led_args
|
||||
---|indicator_led_pair_args
|
||||
---|indicator_led_rgb_args
|
||||
---|indicator_light_args
|
||||
---|power_indicator_args
|
||||
---|rad_indicator_args
|
||||
@@ -41,37 +45,49 @@ local element = {}
|
||||
---|colormap_args
|
||||
---|displaybox_args
|
||||
---|div_args
|
||||
---|listbox_args
|
||||
---|multipane_args
|
||||
---|pipenet_args
|
||||
---|rectangle_args
|
||||
---|textbox_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
|
||||
---@nodiscard
|
||||
---@param args graphics_args arguments
|
||||
function element.new(args)
|
||||
---@param child_offset_x? integer mouse event offset x
|
||||
---@param child_offset_y? integer mouse event offset y
|
||||
function element.new(args, child_offset_x, child_offset_y)
|
||||
local self = {
|
||||
id = -1,
|
||||
id = nil, ---@type element_id|nil
|
||||
elem_type = debug.getinfo(2).name,
|
||||
define_completed = false,
|
||||
p_window = nil, ---@type table
|
||||
position = { x = 1, y = 1 },
|
||||
child_offset = { x = 0, y = 0 },
|
||||
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1},
|
||||
p_window = nil, ---@type table
|
||||
position = { x = 1, y = 1 }, ---@type coordinate_2d
|
||||
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
|
||||
next_y = 1,
|
||||
children = {},
|
||||
subscriptions = {},
|
||||
mt = {}
|
||||
}
|
||||
|
||||
---@class graphics_template
|
||||
---@class graphics_base
|
||||
local protected = {
|
||||
enabled = true,
|
||||
value = nil, ---@type any
|
||||
window = nil, ---@type table
|
||||
fg_bg = core.graphics.cpair(colors.white, colors.black),
|
||||
frame = core.graphics.gframe(1, 1, 1, 1)
|
||||
value = nil, ---@type any
|
||||
window = nil, ---@type table
|
||||
content_window = nil, ---@type table|nil
|
||||
fg_bg = core.cpair(colors.white, colors.black),
|
||||
frame = core.gframe(1, 1, 1, 1),
|
||||
children = {}
|
||||
}
|
||||
|
||||
local name_brief = "graphics.element{" .. self.elem_type .. "}: "
|
||||
|
||||
-- element as string
|
||||
function self.mt.__tostring()
|
||||
return "graphics.element{" .. self.elem_type .. "} @ " .. tostring(self)
|
||||
@@ -87,8 +103,8 @@ function element.new(args)
|
||||
-------------------------
|
||||
|
||||
-- prepare the template
|
||||
---@param offset_x integer x offset
|
||||
---@param offset_y integer y offset
|
||||
---@param offset_x integer x offset for mouse events
|
||||
---@param offset_y integer y offset for mouse events
|
||||
---@param next_y integer next line if no y was provided
|
||||
function protected.prepare_template(offset_x, offset_y, next_y)
|
||||
-- get frame coordinates/size
|
||||
@@ -105,35 +121,23 @@ function element.new(args)
|
||||
protected.frame.h = args.height or h
|
||||
end
|
||||
|
||||
-- inner offsets
|
||||
if args.offset_x ~= nil then self.child_offset.x = args.offset_x end
|
||||
if args.offset_y ~= nil then self.child_offset.y = args.offset_y end
|
||||
|
||||
-- adjust window frame if applicable
|
||||
local f = protected.frame
|
||||
local x = f.x
|
||||
local y = f.y
|
||||
|
||||
-- apply offsets
|
||||
if args.parent ~= nil then
|
||||
-- constrain to parent inner width/height
|
||||
local w, h = self.p_window.getSize()
|
||||
f.w = math.min(f.w, w - ((2 * offset_x) + (f.x - 1)))
|
||||
f.h = math.min(f.h, h - ((2 * offset_y) + (f.y - 1)))
|
||||
|
||||
-- offset x/y
|
||||
f.x = x + offset_x
|
||||
f.y = y + offset_y
|
||||
f.w = math.min(f.w, w - (f.x - 1))
|
||||
f.h = math.min(f.h, h - (f.y - 1))
|
||||
end
|
||||
|
||||
-- check frame
|
||||
assert(f.x >= 1, "graphics.element{" .. self.elem_type .. "}: frame x not >= 1")
|
||||
assert(f.y >= 1, "graphics.element{" .. self.elem_type .. "}: frame y not >= 1")
|
||||
assert(f.w >= 1, "graphics.element{" .. self.elem_type .. "}: frame width not >= 1")
|
||||
assert(f.h >= 1, "graphics.element{" .. self.elem_type .. "}: frame height not >= 1")
|
||||
assert(f.x >= 1, name_brief .. "frame x not >= 1")
|
||||
assert(f.y >= 1, name_brief .. "frame y not >= 1")
|
||||
assert(f.w >= 1, name_brief .. "frame width not >= 1")
|
||||
assert(f.h >= 1, name_brief .. "frame height not >= 1")
|
||||
|
||||
-- create window
|
||||
protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, true)
|
||||
protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, args.hidden ~= true)
|
||||
|
||||
-- init colors
|
||||
if args.fg_bg ~= nil then
|
||||
@@ -150,16 +154,52 @@ function element.new(args)
|
||||
-- record position
|
||||
self.position.x, self.position.y = protected.window.getPosition()
|
||||
|
||||
-- calculate bounds
|
||||
-- shift per parent child offset
|
||||
self.position.x = self.position.x + offset_x
|
||||
self.position.y = self.position.y + offset_y
|
||||
|
||||
-- calculate mouse event bounds
|
||||
self.bounds.x1 = self.position.x
|
||||
self.bounds.x2 = self.position.x + f.w - 1
|
||||
self.bounds.y1 = self.position.y
|
||||
self.bounds.y2 = self.position.y + f.h - 1
|
||||
end
|
||||
|
||||
-- handle a touch event
|
||||
---@param event table monitor_touch event
|
||||
function protected.handle_touch(event)
|
||||
-- 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
|
||||
|
||||
-- handle a child element having been added
|
||||
---@param id element_id element identifier
|
||||
---@param child graphics_element child element
|
||||
function protected.on_added(id, child)
|
||||
end
|
||||
|
||||
-- handle a child element having been removed
|
||||
---@param id element_id element identifier
|
||||
function protected.on_removed(id)
|
||||
end
|
||||
|
||||
-- handle a mouse event
|
||||
---@param event mouse_interaction mouse interaction event
|
||||
function protected.handle_mouse(event)
|
||||
end
|
||||
|
||||
-- handle data value changes
|
||||
@@ -211,6 +251,9 @@ function element.new(args)
|
||||
function protected.resize(...)
|
||||
end
|
||||
|
||||
-- luacheck: pop
|
||||
---@diagnostic enable: unused-local, unused-vararg
|
||||
|
||||
-- start animations
|
||||
function protected.start_anim()
|
||||
end
|
||||
@@ -224,6 +267,14 @@ function element.new(args)
|
||||
---@return graphics_element element, element_id id
|
||||
function protected.get() return public, self.id end
|
||||
|
||||
-- report completion of element instantiation and get the public interface
|
||||
---@nodiscard
|
||||
---@return graphics_element element, element_id id
|
||||
function protected.complete()
|
||||
if args.parent ~= nil then args.parent.__child_ready(self.id, public) end
|
||||
return public, self.id
|
||||
end
|
||||
|
||||
-----------
|
||||
-- SETUP --
|
||||
-----------
|
||||
@@ -235,10 +286,11 @@ function element.new(args)
|
||||
end
|
||||
|
||||
-- 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
|
||||
if args.parent == nil then
|
||||
self.id = args.id or "__ROOT__"
|
||||
protected.prepare_template(0, 0, 1)
|
||||
else
|
||||
self.id = args.parent.__add_child(args.id, protected)
|
||||
@@ -250,52 +302,108 @@ function element.new(args)
|
||||
|
||||
-- get the window object
|
||||
---@nodiscard
|
||||
function public.window() return protected.window end
|
||||
function public.window() return protected.content_window or protected.window end
|
||||
|
||||
-- CHILD ELEMENTS --
|
||||
-- delete this element (hide and unsubscribe from PSIL)
|
||||
function public.delete()
|
||||
local fg_bg = protected.fg_bg
|
||||
|
||||
if args.parent ~= nil then
|
||||
-- grab parent fg/bg so we can clear cleanly as a child element
|
||||
fg_bg = args.parent.get_fg_bg()
|
||||
end
|
||||
|
||||
-- clear, hide, and stop animations
|
||||
protected.window.setBackgroundColor(fg_bg.bkg)
|
||||
protected.window.setTextColor(fg_bg.fgd)
|
||||
protected.window.clear()
|
||||
public.hide()
|
||||
|
||||
-- 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(protected.children) do
|
||||
v.delete()
|
||||
protected.children[k] = nil
|
||||
end
|
||||
|
||||
if args.parent ~= nil then
|
||||
-- remove self from parent
|
||||
args.parent.__remove_child(self.id)
|
||||
end
|
||||
end
|
||||
|
||||
-- ELEMENT TREE --
|
||||
|
||||
-- add a child element
|
||||
---@nodiscard
|
||||
---@param key string|nil id
|
||||
---@param child graphics_template
|
||||
---@param child graphics_base
|
||||
---@return integer|string key
|
||||
function public.__add_child(key, child)
|
||||
child.prepare_template(self.child_offset.x, self.child_offset.y, self.next_y)
|
||||
child.prepare_template(child_offset_x or 0, child_offset_y or 0, self.next_y)
|
||||
|
||||
self.next_y = child.frame.y + child.frame.h
|
||||
|
||||
local child_element = child.get()
|
||||
|
||||
if key == nil then
|
||||
table.insert(self.children, child_element)
|
||||
return #self.children
|
||||
table.insert(protected.children, child_element)
|
||||
return #protected.children
|
||||
else
|
||||
self.children[key] = child_element
|
||||
protected.children[key] = child_element
|
||||
return key
|
||||
end
|
||||
end
|
||||
|
||||
-- remove a child element
|
||||
---@param key element_id id
|
||||
function public.__remove_child(key)
|
||||
if protected.children[key] ~= nil then
|
||||
protected.on_removed(key)
|
||||
protected.children[key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- actions to take upon a child element becoming ready (initial draw/construction completed)
|
||||
---@param key element_id id
|
||||
---@param child graphics_element
|
||||
function public.__child_ready(key, child)
|
||||
protected.on_added(key, child)
|
||||
end
|
||||
|
||||
-- get a child element
|
||||
---@nodiscard
|
||||
---@param id element_id
|
||||
---@return graphics_element
|
||||
function public.get_child(key) return self.children[key] end
|
||||
function public.get_child(id) return protected.children[id] end
|
||||
|
||||
-- remove child
|
||||
---@param key string|integer
|
||||
function public.remove(key) self.children[key] = nil end
|
||||
-- remove a child element
|
||||
---@param id element_id
|
||||
function public.remove(id)
|
||||
if protected.children[id] ~= nil then
|
||||
protected.children[id].delete()
|
||||
protected.on_removed(id)
|
||||
protected.children[id] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- attempt to get a child element by ID (does not include this element itself)
|
||||
---@nodiscard
|
||||
---@param id element_id
|
||||
---@return graphics_element|nil element
|
||||
function public.get_element_by_id(id)
|
||||
if self.children[id] == nil then
|
||||
for _, child in pairs(self.children) do
|
||||
if protected.children[id] == nil then
|
||||
for _, child in pairs(protected.children) do
|
||||
local elem = child.get_element_by_id(id)
|
||||
if elem ~= nil then return elem end
|
||||
end
|
||||
else
|
||||
return self.children[id]
|
||||
return protected.children[id]
|
||||
end
|
||||
|
||||
return nil
|
||||
@@ -334,14 +442,14 @@ function element.new(args)
|
||||
-- get element width
|
||||
---@nodiscard
|
||||
---@return integer width
|
||||
function public.width()
|
||||
function public.get_width()
|
||||
return protected.frame.w
|
||||
end
|
||||
|
||||
-- get element height
|
||||
---@nodiscard
|
||||
---@return integer height
|
||||
function public.height()
|
||||
function public.get_height()
|
||||
return protected.frame.h
|
||||
end
|
||||
|
||||
@@ -394,22 +502,29 @@ function element.new(args)
|
||||
protected.resize(...)
|
||||
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 --
|
||||
|
||||
-- handle a monitor touch
|
||||
---@param event monitor_touch monitor touch event
|
||||
function public.handle_touch(event)
|
||||
local in_x = event.x >= self.bounds.x1 and event.x <= self.bounds.x2
|
||||
local in_y = event.y >= self.bounds.y1 and event.y <= self.bounds.y2
|
||||
-- handle a monitor touch or mouse click
|
||||
---@param event mouse_interaction mouse interaction event
|
||||
function public.handle_mouse(event)
|
||||
local x_ini, y_ini = event.initial.x, event.initial.y
|
||||
|
||||
if in_x and in_y then
|
||||
local event_T = core.events.touch(event.monitor, (event.x - self.position.x) + 1, (event.y - self.position.y) + 1)
|
||||
local ini_in = protected.in_window_bounds(x_ini, y_ini)
|
||||
|
||||
-- handle the touch event, transformed into the window frame
|
||||
protected.handle_touch(event_T)
|
||||
if ini_in then
|
||||
local event_T = core.events.mouse_transposed(event, self.position.x, self.position.y)
|
||||
|
||||
-- pass on touch event to children
|
||||
for _, val in pairs(self.children) do val.handle_touch(event_T) end
|
||||
-- handle the mouse event then pass to children
|
||||
protected.handle_mouse(event_T)
|
||||
for _, child in pairs(protected.children) do child.handle_mouse(event_T) end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -425,26 +540,56 @@ function element.new(args)
|
||||
protected.response_callback(result)
|
||||
end
|
||||
|
||||
-- VISIBILITY --
|
||||
-- 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
|
||||
|
||||
-- show the element
|
||||
function public.show()
|
||||
-- VISIBILITY & ANIMATIONS --
|
||||
|
||||
-- show the element and enables animations by default
|
||||
---@param animate? boolean true (default) to automatically resume animations
|
||||
function public.show(animate)
|
||||
protected.window.setVisible(true)
|
||||
protected.start_anim()
|
||||
if animate ~= false then public.animate_all() end
|
||||
end
|
||||
|
||||
for i = 1, #self.children do
|
||||
self.children[i].show()
|
||||
-- hide the element and disables animations<br>
|
||||
-- this alone does not cause an element to be fully hidden, it only prevents updates from being shown<br>
|
||||
---@see graphics_element.content_redraw
|
||||
function public.hide()
|
||||
public.freeze_all() -- stop animations for efficiency/performance
|
||||
protected.window.setVisible(false)
|
||||
end
|
||||
|
||||
-- start/resume animation(s)
|
||||
function public.animate()
|
||||
protected.start_anim()
|
||||
end
|
||||
|
||||
-- start/resume animation(s) for this element and all its children<br>
|
||||
-- only animates if a window is visible
|
||||
function public.animate_all()
|
||||
if protected.window.isVisible() then
|
||||
public.animate()
|
||||
for _, child in pairs(protected.children) do child.animate_all() end
|
||||
end
|
||||
end
|
||||
|
||||
-- hide the element
|
||||
function public.hide()
|
||||
-- freeze animation(s)
|
||||
function public.freeze()
|
||||
protected.stop_anim()
|
||||
for i = 1, #self.children do
|
||||
self.children[i].hide()
|
||||
end
|
||||
end
|
||||
|
||||
protected.window.setVisible(false)
|
||||
-- freeze animation(s) for this element and all its children
|
||||
function public.freeze_all()
|
||||
public.freeze()
|
||||
for _, child in pairs(protected.children) do child.freeze_all() end
|
||||
end
|
||||
|
||||
-- re-draw the element
|
||||
@@ -452,6 +597,14 @@ function element.new(args)
|
||||
protected.window.redraw()
|
||||
end
|
||||
|
||||
-- if a content window is set, clears it then re-draws all children
|
||||
function public.content_redraw()
|
||||
if protected.content_window ~= nil then
|
||||
protected.content_window.clear()
|
||||
for _, child in pairs(protected.children) do child.redraw() end
|
||||
end
|
||||
end
|
||||
|
||||
return protected
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- Loading/Waiting Animation Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcallbackdsp")
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
@@ -10,6 +10,7 @@ local element = require("graphics.element")
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new waiting animation element
|
||||
---@param args waiting_args
|
||||
@@ -85,7 +86,7 @@ local function waiting(args)
|
||||
if state >= 12 then state = 0 end
|
||||
|
||||
if run_animation then
|
||||
tcd.dispatch_unique(0.5, animate)
|
||||
tcd.dispatch_unique(0.15, animate)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -102,7 +103,7 @@ local function waiting(args)
|
||||
|
||||
e.start_anim()
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return waiting
|
||||
|
||||
@@ -9,6 +9,7 @@ local element = require("graphics.element")
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new color map
|
||||
---@param args colormap_args
|
||||
@@ -27,7 +28,7 @@ local function colormap(args)
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(spaces, bkg, bkg)
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return colormap
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- Hazard-bordered Button Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcallbackdsp")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
@@ -16,6 +16,7 @@ local element = require("graphics.element")
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new hazard button
|
||||
---@param args hazard_button_args
|
||||
@@ -140,26 +141,27 @@ local function hazard_button(args)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle touch
|
||||
---@param event monitor_touch monitor touch event
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function e.handle_touch(event)
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled then
|
||||
-- change text color to indicate clicked
|
||||
e.window.setTextColor(args.accent)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
if core.events.was_clicked(event.type) then
|
||||
-- change text color to indicate clicked
|
||||
e.window.setTextColor(args.accent)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
|
||||
-- abort any other callbacks
|
||||
tcd.abort(on_timeout)
|
||||
tcd.abort(on_success)
|
||||
tcd.abort(on_failure)
|
||||
-- abort any other callbacks
|
||||
tcd.abort(on_timeout)
|
||||
tcd.abort(on_success)
|
||||
tcd.abort(on_failure)
|
||||
|
||||
-- 1.5 second timeout
|
||||
tcd.dispatch(1.5, on_timeout)
|
||||
-- 1.5 second timeout
|
||||
tcd.dispatch(1.5, on_timeout)
|
||||
|
||||
-- call the touch callback
|
||||
args.callback()
|
||||
-- call the touch callback
|
||||
args.callback()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -167,18 +169,13 @@ local function hazard_button(args)
|
||||
---@param result boolean true for success, false for failure
|
||||
function e.response_callback(result)
|
||||
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
|
||||
|
||||
-- set the value (true simulates pressing the button)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_touch(core.events.touch("", 1, 1)) end
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
|
||||
end
|
||||
|
||||
-- show the button as disabled
|
||||
@@ -202,7 +199,7 @@ local function hazard_button(args)
|
||||
-- initial draw of border
|
||||
draw_border(args.accent)
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return hazard_button
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class button_option
|
||||
---@field text string
|
||||
---@field 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 _end_x integer ending touch x range (inclusive)
|
||||
|
||||
@@ -23,6 +23,7 @@ local element = require("graphics.element")
|
||||
---@field y? integer 1 if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new multi button (latch selection, exclusively one button at a time)
|
||||
---@param args multi_button_args
|
||||
@@ -62,9 +63,7 @@ local function multi_button(args)
|
||||
local next_x = 2
|
||||
for i = 1, #args.options do
|
||||
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._end_x = next_x + button_width - 1
|
||||
|
||||
@@ -92,19 +91,32 @@ local function multi_button(args)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle touch
|
||||
---@param event monitor_touch monitor touch event
|
||||
function e.handle_touch(event)
|
||||
-- determine what was pressed
|
||||
if e.enabled and event.y == 1 then
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type button_option
|
||||
-- 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
|
||||
|
||||
if event.x >= opt._start_x and event.x <= opt._end_x then
|
||||
e.value = i
|
||||
draw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- if enabled and the button row was pressed...
|
||||
if e.enabled and core.events.was_clicked(event.type) then
|
||||
-- a button may have been pressed, which one was it?
|
||||
local button_ini = which_button(event.initial.x)
|
||||
local button_cur = which_button(event.current.x)
|
||||
|
||||
-- mouse up must always have started with a mouse down on the same button to count as a click
|
||||
-- tap always has identical coordinates, so this always passes for taps
|
||||
if button_ini == button_cur and button_cur ~= nil then
|
||||
e.value = button_cur
|
||||
draw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -119,7 +131,7 @@ local function multi_button(args)
|
||||
-- initial draw
|
||||
draw()
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return multi_button
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
-- Button Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcallbackdsp")
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
|
||||
---@class push_button_args
|
||||
---@field text string button text
|
||||
---@field callback function function to call on touch
|
||||
---@field min_width? integer text length + 2 if omitted
|
||||
---@field min_width? integer text length if omitted
|
||||
---@field active_fg_bg? cpair foreground/background colors when pressed
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
---@field parent graphics_element
|
||||
@@ -17,6 +19,7 @@ local element = require("graphics.element")
|
||||
---@field y? integer 1 if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new push button
|
||||
---@param args push_button_args
|
||||
@@ -24,6 +27,8 @@ local element = require("graphics.element")
|
||||
local function push_button(args)
|
||||
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.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)
|
||||
|
||||
@@ -47,38 +52,50 @@ local function push_button(args)
|
||||
e.window.write(args.text)
|
||||
end
|
||||
|
||||
-- handle touch
|
||||
---@param event monitor_touch monitor touch event
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function e.handle_touch(event)
|
||||
-- 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
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled then
|
||||
if args.active_fg_bg ~= nil then
|
||||
-- show as pressed
|
||||
e.value = true
|
||||
e.window.setTextColor(args.active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(args.active_fg_bg.bkg)
|
||||
draw()
|
||||
|
||||
if event.type == CLICK_TYPE.TAP then
|
||||
show_pressed()
|
||||
-- show as unpressed in 0.25 seconds
|
||||
tcd.dispatch(0.25, function ()
|
||||
e.value = false
|
||||
if e.enabled then
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
end
|
||||
draw()
|
||||
end)
|
||||
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
|
||||
args.callback()
|
||||
elseif event.type == CLICK_TYPE.DOWN then
|
||||
show_pressed()
|
||||
elseif event.type == CLICK_TYPE.UP then
|
||||
show_unpressed()
|
||||
if e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
args.callback()
|
||||
end
|
||||
end
|
||||
|
||||
-- call the touch callback
|
||||
args.callback()
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value (true simulates pressing the button)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_touch(core.events.touch("", 1, 1)) end
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
|
||||
end
|
||||
|
||||
-- show butten as enabled
|
||||
@@ -104,7 +121,7 @@ local function push_button(args)
|
||||
-- initial draw
|
||||
draw()
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return push_button
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
-- Radio Button Graphics Element
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class radio_button_args
|
||||
@@ -14,6 +15,7 @@ local element = require("graphics.element")
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new radio button list (latch selection, exclusively one button at a time)
|
||||
---@param args radio_button_args
|
||||
@@ -79,13 +81,13 @@ local function radio_button(args)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle touch
|
||||
---@param event monitor_touch monitor touch event
|
||||
function e.handle_touch(event)
|
||||
-- determine what was pressed
|
||||
if e.enabled then
|
||||
if args.options[event.y] ~= nil then
|
||||
e.value = event.y
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) and (event.initial.y == event.current.y) then
|
||||
-- determine what was pressed
|
||||
if args.options[event.current.y] ~= nil then
|
||||
e.value = event.current.y
|
||||
draw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
@@ -102,7 +104,7 @@ local function radio_button(args)
|
||||
-- initial draw
|
||||
draw()
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return radio_button
|
||||
|
||||
122
graphics/elements/controls/sidebar.lua
Normal file
122
graphics/elements/controls/sidebar.lua
Normal file
@@ -0,0 +1,122 @@
|
||||
-- Sidebar Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
|
||||
---@class 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
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- 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.complete()
|
||||
end
|
||||
|
||||
return sidebar
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class spinbox_args
|
||||
@@ -17,6 +18,7 @@ local element = require("graphics.element")
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new spinbox control (minimum value is 0)
|
||||
---@param args spinbox_args
|
||||
@@ -30,8 +32,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(fr_prec), "graphics.element.controls.spinbox_numeric: fractional precision must be an integer")
|
||||
|
||||
local fmt = ""
|
||||
local fmt_init = ""
|
||||
local fmt, fmt_init ---@type string, string
|
||||
|
||||
if fr_prec > 0 then
|
||||
fmt = "%" .. (wn_prec + fr_prec + 1) .. "." .. fr_prec .. "f"
|
||||
@@ -127,23 +128,26 @@ local function spinbox(args)
|
||||
-- init with the default value
|
||||
show_num()
|
||||
|
||||
-- handle touch
|
||||
---@param event monitor_touch monitor touch event
|
||||
function e.handle_touch(event)
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- only handle if on an increment or decrement arrow
|
||||
if e.enabled and event.x ~= dec_point_x then
|
||||
local idx = util.trinary(event.x > dec_point_x, event.x - 1, event.x)
|
||||
if digits[idx] ~= nil then
|
||||
if event.y == 1 then
|
||||
-- increment
|
||||
digits[idx] = digits[idx] + 1
|
||||
elseif event.y == 3 then
|
||||
-- decrement
|
||||
digits[idx] = digits[idx] - 1
|
||||
end
|
||||
if e.enabled and core.events.was_clicked(event.type) and
|
||||
(event.current.x ~= dec_point_x) and (event.current.y ~= 2) then
|
||||
if event.current.x == event.initial.x and event.current.y == event.initial.y then
|
||||
local idx = util.trinary(event.current.x > dec_point_x, event.current.x - 1, event.current.x)
|
||||
if digits[idx] ~= nil then
|
||||
if event.current.y == 1 then
|
||||
-- increment
|
||||
digits[idx] = digits[idx] + 1
|
||||
elseif event.current.y == 3 then
|
||||
-- decrement
|
||||
digits[idx] = digits[idx] - 1
|
||||
end
|
||||
|
||||
update_value()
|
||||
show_num()
|
||||
update_value()
|
||||
show_num()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -185,7 +189,7 @@ local function spinbox(args)
|
||||
e.value = 0
|
||||
set_digits()
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return spinbox
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
-- Button Graphics Element
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class switch_button_args
|
||||
@@ -14,6 +15,7 @@ local element = require("graphics.element")
|
||||
---@field y? integer 1 if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new switch button (latch high/low)
|
||||
---@param args switch_button_args
|
||||
@@ -22,13 +24,15 @@ local function switch_button(args)
|
||||
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.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)
|
||||
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
|
||||
local e = element.new(args)
|
||||
@@ -62,11 +66,10 @@ local function switch_button(args)
|
||||
-- initial draw
|
||||
draw_state()
|
||||
|
||||
-- handle touch
|
||||
---@param event monitor_touch monitor touch event
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function e.handle_touch(event)
|
||||
if e.enabled then
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) then
|
||||
-- toggle state
|
||||
e.value = not e.value
|
||||
draw_state()
|
||||
@@ -84,7 +87,7 @@ local function switch_button(args)
|
||||
draw_state()
|
||||
end
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return switch_button
|
||||
|
||||
131
graphics/elements/controls/tabbar.lua
Normal file
131
graphics/elements/controls/tabbar.lua
Normal file
@@ -0,0 +1,131 @@
|
||||
-- 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
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- 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.complete()
|
||||
end
|
||||
|
||||
return tabbar
|
||||
@@ -4,19 +4,22 @@ local element = require("graphics.element")
|
||||
|
||||
---@class displaybox_args
|
||||
---@field window table
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new root display box
|
||||
---@nodiscard
|
||||
---@param args displaybox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function displaybox(args)
|
||||
-- create new graphics element base object
|
||||
return element.new(args).get()
|
||||
return element.new(args).complete()
|
||||
end
|
||||
|
||||
return displaybox
|
||||
|
||||
@@ -11,6 +11,7 @@ local element = require("graphics.element")
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new div element
|
||||
---@nodiscard
|
||||
@@ -18,7 +19,7 @@ local element = require("graphics.element")
|
||||
---@return graphics_element element, element_id id
|
||||
local function div(args)
|
||||
-- create new graphics element base object
|
||||
return element.new(args).get()
|
||||
return element.new(args).complete()
|
||||
end
|
||||
|
||||
return div
|
||||
|
||||
@@ -18,6 +18,7 @@ local flasher = require("graphics.flasher")
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new alarm indicator light
|
||||
---@nodiscard
|
||||
@@ -108,7 +109,7 @@ local function alarm_indicator_light(args)
|
||||
e.on_update(1)
|
||||
e.window.write(args.label)
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return alarm_indicator_light
|
||||
|
||||
@@ -26,7 +26,7 @@ local function core_map(args)
|
||||
args.height = 18
|
||||
|
||||
-- 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
|
||||
local e = element.new(args)
|
||||
@@ -73,7 +73,7 @@ local function core_map(args)
|
||||
local function draw_core(t)
|
||||
local i = 1
|
||||
local back_c = "F"
|
||||
local text_c = "8"
|
||||
local text_c ---@type string
|
||||
|
||||
-- determine fuel assembly coloring
|
||||
if t <= 300 then
|
||||
@@ -163,7 +163,7 @@ local function core_map(args)
|
||||
-- initial draw
|
||||
e.on_update(0)
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return core_map
|
||||
|
||||
@@ -17,6 +17,7 @@ local element = require("graphics.element")
|
||||
---@field y? integer 1 if omitted
|
||||
---@field width integer length
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new data indicator
|
||||
---@nodiscard
|
||||
@@ -43,8 +44,9 @@ local function data(args)
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write(args.label)
|
||||
|
||||
local label_len = string.len(args.label)
|
||||
local data_start = 1
|
||||
local value_color = e.fg_bg.fgd
|
||||
local label_len = string.len(args.label)
|
||||
local data_start = 1
|
||||
local clear_width = args.width
|
||||
|
||||
if label_len > 0 then
|
||||
@@ -64,7 +66,7 @@ local function data(args)
|
||||
-- write data
|
||||
local data_str = util.sprintf(args.format, value)
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setTextColor(value_color)
|
||||
if args.commas then
|
||||
e.window.write(util.comma_format(data_str))
|
||||
else
|
||||
@@ -84,10 +86,17 @@ local function data(args)
|
||||
---@param val any new value
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- change the foreground color of the value, or all text if no label/unit colors provided
|
||||
---@param c color
|
||||
function e.recolor(c)
|
||||
value_color = c
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial value draw
|
||||
e.on_update(args.value)
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return data
|
||||
|
||||
@@ -15,6 +15,7 @@ local element = require("graphics.element")
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new horizontal bar
|
||||
---@nodiscard
|
||||
@@ -119,7 +120,7 @@ local function hbar(args)
|
||||
-- initialize to 0
|
||||
e.on_update(0)
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return hbar
|
||||
|
||||
@@ -18,6 +18,7 @@ local element = require("graphics.element")
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new icon indicator
|
||||
---@nodiscard
|
||||
@@ -68,7 +69,7 @@ local function icon(args)
|
||||
-- initial icon draw
|
||||
e.on_update(args.value or 1)
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return icon
|
||||
|
||||
101
graphics/elements/indicators/led.lua
Normal file
101
graphics/elements/indicators/led.lua
Normal file
@@ -0,0 +1,101 @@
|
||||
-- Indicator "LED" Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
---@class indicator_led_args
|
||||
---@field label string indicator label
|
||||
---@field colors cpair on/off colors (a/b respectively)
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field flash? boolean whether to flash on true rather than stay on
|
||||
---@field period? PERIOD flash period
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new indicator LED
|
||||
---@nodiscard
|
||||
---@param args indicator_led_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.led: label is a required field")
|
||||
assert(type(args.colors) == "table", "graphics.elements.indicators.led: colors is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.led: period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
local flash_on = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
|
||||
if flash_on then
|
||||
e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
end
|
||||
|
||||
-- enable light or start flashing
|
||||
local function enable()
|
||||
if args.flash then
|
||||
flash_on = true
|
||||
flasher.start(flash_callback, args.period)
|
||||
else
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
-- disable light or stop flashing
|
||||
local function disable()
|
||||
if args.flash then
|
||||
flash_on = false
|
||||
flasher.stop(flash_callback)
|
||||
end
|
||||
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param new_state boolean indicator state
|
||||
function e.on_update(new_state)
|
||||
e.value = new_state
|
||||
if new_state then enable() else disable() end
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val boolean indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(false)
|
||||
if string.len(args.label) > 0 then
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return indicator_led
|
||||
115
graphics/elements/indicators/ledpair.lua
Normal file
115
graphics/elements/indicators/ledpair.lua
Normal file
@@ -0,0 +1,115 @@
|
||||
-- Indicator LED Pair Graphics Element (two LEDs provide: off, color_a, color_b)
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
---@class indicator_led_pair_args
|
||||
---@field label string indicator label
|
||||
---@field off color color for off
|
||||
---@field c1 color color for #1 on
|
||||
---@field c2 color color for #2 on
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field flash? boolean whether to flash when on rather than stay on
|
||||
---@field period? PERIOD flash period
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new dual LED indicator light
|
||||
---@nodiscard
|
||||
---@param args indicator_led_pair_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led_pair(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.ledpair: label is a required field")
|
||||
assert(type(args.off) == "number", "graphics.elements.indicators.ledpair: off is a required field")
|
||||
assert(type(args.c1) == "number", "graphics.elements.indicators.ledpair: c1 is a required field")
|
||||
assert(type(args.c2) == "number", "graphics.elements.indicators.ledpair: c2 is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.ledpair: period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
local flash_on = true
|
||||
|
||||
-- blit translations
|
||||
local co = colors.toBlit(args.off)
|
||||
local c1 = colors.toBlit(args.c1)
|
||||
local c2 = colors.toBlit(args.c2)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- init value for initial check in on_update
|
||||
e.value = 1
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
|
||||
if flash_on then
|
||||
if e.value == 2 then
|
||||
e.window.blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
elseif e.value == 3 then
|
||||
e.window.blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
local was_off = e.value <= 1
|
||||
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
|
||||
if args.flash then
|
||||
if was_off and (new_state > 1) then
|
||||
flash_on = true
|
||||
flasher.start(flash_callback, args.period)
|
||||
elseif new_state <= 1 then
|
||||
flash_on = false
|
||||
flasher.stop(flash_callback)
|
||||
|
||||
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
elseif new_state == 2 then
|
||||
e.window.blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
elseif new_state == 3 then
|
||||
e.window.blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(1)
|
||||
if string.len(args.label) > 0 then
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return indicator_led_pair
|
||||
60
graphics/elements/indicators/ledrgb.lua
Normal file
60
graphics/elements/indicators/ledrgb.lua
Normal file
@@ -0,0 +1,60 @@
|
||||
-- Indicator RGB LED Graphics Element
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class indicator_led_rgb_args
|
||||
---@field label string indicator label
|
||||
---@field colors table colors to use
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new RGB LED indicator light
|
||||
---@nodiscard
|
||||
---@param args indicator_led_rgb_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led_rgb(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.ledrgb: label is a required field")
|
||||
assert(type(args.colors) == "table", "graphics.elements.indicators.ledrgb: colors is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- init value for initial check in on_update
|
||||
e.value = 1
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
if type(args.colors[new_state]) == "number" then
|
||||
e.window.blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(1)
|
||||
if string.len(args.label) > 0 then
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return indicator_led_rgb
|
||||
@@ -16,6 +16,7 @@ local flasher = require("graphics.flasher")
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new indicator light
|
||||
---@nodiscard
|
||||
@@ -92,7 +93,7 @@ local function indicator_light(args)
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return indicator_light
|
||||
|
||||
@@ -16,6 +16,7 @@ local element = require("graphics.element")
|
||||
---@field y? integer 1 if omitted
|
||||
---@field width integer length
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new power indicator
|
||||
---@nodiscard
|
||||
@@ -79,7 +80,7 @@ local function power(args)
|
||||
-- initial value draw
|
||||
e.on_update(args.value)
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return power
|
||||
|
||||
@@ -17,6 +17,7 @@ local element = require("graphics.element")
|
||||
---@field y? integer 1 if omitted
|
||||
---@field width integer length
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new radiation indicator
|
||||
---@nodiscard
|
||||
@@ -84,7 +85,7 @@ local function rad(args)
|
||||
-- initial value draw
|
||||
e.on_update(types.new_zero_radiation_reading())
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return rad
|
||||
|
||||
@@ -18,6 +18,7 @@ local element = require("graphics.element")
|
||||
---@field y? integer 1 if omitted
|
||||
---@field height? integer 1 if omitted, must be an odd number
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new state indicator
|
||||
---@nodiscard
|
||||
@@ -74,7 +75,7 @@ local function state_indicator(args)
|
||||
-- initial draw
|
||||
e.on_update(args.value or 1)
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return state_indicator
|
||||
|
||||
@@ -18,6 +18,7 @@ local flasher = require("graphics.flasher")
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new tri-state indicator light
|
||||
---@nodiscard
|
||||
@@ -105,7 +106,7 @@ local function tristate_indicator_light(args)
|
||||
e.on_update(1)
|
||||
e.window.write(args.label)
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return tristate_indicator_light
|
||||
|
||||
@@ -13,6 +13,7 @@ local element = require("graphics.element")
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new vertical bar
|
||||
---@nodiscard
|
||||
@@ -99,7 +100,7 @@ local function vbar(args)
|
||||
---@param val number 0.0 to 1.0
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return vbar
|
||||
|
||||
283
graphics/elements/listbox.lua
Normal file
283
graphics/elements/listbox.lua
Normal file
@@ -0,0 +1,283 @@
|
||||
-- Scroll-able List Box Display Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
|
||||
---@class listbox_args
|
||||
---@field scroll_height integer height of internal scrolling container (must fit all elements vertically tiled)
|
||||
---@field item_pad? integer spacing (lines) between items in the list (default 0)
|
||||
---@field nav_fg_bg? cpair foreground/background colors for scroll arrows and bar area
|
||||
---@field nav_active? cpair active colors for bar held down or arrow held down
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
---@class listbox_item
|
||||
---@field id string|integer element ID
|
||||
---@field e graphics_element element
|
||||
---@field y integer y position
|
||||
---@field h integer element height
|
||||
|
||||
-- new listbox element
|
||||
---@nodiscard
|
||||
---@param args listbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function listbox(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- create content window for child elements
|
||||
local scroll_frame = window.create(e.window, 1, 1, e.frame.w - 1, args.scroll_height, false)
|
||||
e.content_window = scroll_frame
|
||||
|
||||
-- item list and scroll management
|
||||
local list = {}
|
||||
local item_pad = args.item_pad or 0
|
||||
local scroll_offset = 0
|
||||
local content_height = 0
|
||||
local max_down_scroll = 0
|
||||
-- bar control/tracking variables
|
||||
local max_bar_height = e.frame.h - 2
|
||||
local bar_height = 0 -- full height of bar
|
||||
local bar_bounds = { 0, 0 } -- top and bottom of bar
|
||||
local bar_is_scaled = false -- if the scrollbar doesn't have a 1:1 ratio with lines
|
||||
local holding_bar = false -- bar is being held by mouse
|
||||
local bar_grip_pos = 0 -- where the bar was gripped by mouse down
|
||||
local mouse_last_y = 0 -- last reported y coordinate of drag
|
||||
|
||||
-- draw scroll bar arrows, optionally showing one of them as pressed
|
||||
---@param pressed_arrow? 1|0|-1 arrow to show as pressed (1 = scroll up, 0 = neither, -1 = scroll down)
|
||||
local function draw_arrows(pressed_arrow)
|
||||
local nav_fg_bg = args.nav_fg_bg or e.fg_bg
|
||||
local active_fg_bg = args.nav_active or nav_fg_bg
|
||||
|
||||
-- draw up/down arrows
|
||||
if pressed_arrow == 1 then
|
||||
e.window.setTextColor(active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(active_fg_bg.bkg)
|
||||
e.window.setCursorPos(e.frame.w, 1)
|
||||
e.window.write("\x1e")
|
||||
e.window.setTextColor(nav_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(nav_fg_bg.bkg)
|
||||
e.window.setCursorPos(e.frame.w, e.frame.h)
|
||||
e.window.write("\x1f")
|
||||
elseif pressed_arrow == -1 then
|
||||
e.window.setTextColor(nav_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(nav_fg_bg.bkg)
|
||||
e.window.setCursorPos(e.frame.w, 1)
|
||||
e.window.write("\x1e")
|
||||
e.window.setTextColor(active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(active_fg_bg.bkg)
|
||||
e.window.setCursorPos(e.frame.w, e.frame.h)
|
||||
e.window.write("\x1f")
|
||||
else
|
||||
e.window.setTextColor(nav_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(nav_fg_bg.bkg)
|
||||
e.window.setCursorPos(e.frame.w, 1)
|
||||
e.window.write("\x1e")
|
||||
e.window.setCursorPos(e.frame.w, e.frame.h)
|
||||
e.window.write("\x1f")
|
||||
end
|
||||
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- render the scroll bar and re-cacluate height & bounds
|
||||
local function draw_bar()
|
||||
local offset = 2 + math.abs(scroll_offset)
|
||||
|
||||
bar_height = math.min(max_bar_height + max_down_scroll, max_bar_height)
|
||||
|
||||
if bar_height < 1 then
|
||||
bar_is_scaled = true
|
||||
-- can't do a 1:1 ratio
|
||||
-- use minimum size bar with scaled offset
|
||||
local scroll_progress = scroll_offset / max_down_scroll
|
||||
offset = 2 + math.floor(scroll_progress * (max_bar_height - 1))
|
||||
bar_height = 1
|
||||
else
|
||||
bar_is_scaled = false
|
||||
end
|
||||
|
||||
bar_bounds = { offset, (bar_height + offset) - 1 }
|
||||
|
||||
for i = 2, e.frame.h - 1 do
|
||||
if (i >= offset and i < (bar_height + offset)) and (bar_height ~= max_bar_height) then
|
||||
if args.nav_fg_bg ~= nil then
|
||||
e.window.setBackgroundColor(args.nav_fg_bg.fgd)
|
||||
else
|
||||
e.window.setBackgroundColor(e.fg_bg.fgd)
|
||||
end
|
||||
else
|
||||
if args.nav_fg_bg ~= nil then
|
||||
e.window.setBackgroundColor(args.nav_fg_bg.bkg)
|
||||
else
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
end
|
||||
end
|
||||
|
||||
e.window.setCursorPos(e.frame.w, i)
|
||||
e.window.write(" ")
|
||||
end
|
||||
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- update item y positions and move elements
|
||||
local function update_positions()
|
||||
local next_y = 1
|
||||
|
||||
scroll_frame.setVisible(false)
|
||||
scroll_frame.setBackgroundColor(e.fg_bg.bkg)
|
||||
scroll_frame.setTextColor(e.fg_bg.fgd)
|
||||
scroll_frame.clear()
|
||||
|
||||
for i = 1, #list do
|
||||
local item = list[i] ---@type listbox_item
|
||||
item.y = next_y
|
||||
next_y = next_y + item.h + item_pad
|
||||
item.e.reposition(1, item.y)
|
||||
item.e.show()
|
||||
end
|
||||
|
||||
content_height = next_y
|
||||
max_down_scroll = math.min(-1 * (content_height - (e.frame.h + 1 + item_pad)), 0)
|
||||
if scroll_offset < max_down_scroll then scroll_offset = max_down_scroll end
|
||||
|
||||
scroll_frame.reposition(1, 1 + scroll_offset)
|
||||
scroll_frame.setVisible(true)
|
||||
|
||||
draw_bar()
|
||||
end
|
||||
|
||||
-- determine where to scroll to based on a scrollbar being dragged without a 1:1 relationship
|
||||
---@param direction -1|1 negative 1 to scroll up by one, positive 1 to scroll down by one
|
||||
local function scaled_bar_scroll(direction)
|
||||
local scroll_progress = scroll_offset / max_down_scroll
|
||||
local bar_position = math.floor(scroll_progress * (max_bar_height - 1))
|
||||
|
||||
-- check what moving the scroll bar up or down would mean for the scroll progress
|
||||
scroll_progress = (bar_position + direction) / (max_bar_height - 1)
|
||||
|
||||
return math.max(math.floor(scroll_progress * max_down_scroll), max_down_scroll)
|
||||
end
|
||||
|
||||
-- scroll down the list
|
||||
local function scroll_down(scaled)
|
||||
if scroll_offset > max_down_scroll then
|
||||
if scaled then
|
||||
scroll_offset = scaled_bar_scroll(1)
|
||||
else
|
||||
scroll_offset = scroll_offset - 1
|
||||
end
|
||||
|
||||
update_positions()
|
||||
end
|
||||
end
|
||||
|
||||
-- scroll up the list
|
||||
local function scroll_up(scaled)
|
||||
if scroll_offset < 0 then
|
||||
if scaled then
|
||||
scroll_offset = scaled_bar_scroll(-1)
|
||||
else
|
||||
scroll_offset = scroll_offset + 1
|
||||
end
|
||||
|
||||
update_positions()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle a child element having been added to the list
|
||||
---@param id element_id element identifier
|
||||
---@param child graphics_element child element
|
||||
function e.on_added(id, child)
|
||||
table.insert(list, { id = id, e = child, y = 0, h = child.get_height() })
|
||||
update_positions()
|
||||
end
|
||||
|
||||
-- handle a child element having been removed from the list
|
||||
---@param id element_id element identifier
|
||||
function e.on_removed(id)
|
||||
for idx, elem in ipairs(list) do
|
||||
if elem.id == id then
|
||||
table.remove(list, idx)
|
||||
update_positions()
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled then
|
||||
if event.type == CLICK_TYPE.TAP then
|
||||
if event.current.x == e.frame.w then
|
||||
if event.current.y == 1 or event.current.y < bar_bounds[1] then
|
||||
draw_arrows(1)
|
||||
scroll_up()
|
||||
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
|
||||
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
|
||||
draw_arrows(-1)
|
||||
scroll_down()
|
||||
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
|
||||
end
|
||||
end
|
||||
elseif event.type == CLICK_TYPE.DOWN then
|
||||
if event.current.x == e.frame.w then
|
||||
if event.current.y == 1 or event.current.y < bar_bounds[1] then
|
||||
draw_arrows(1)
|
||||
scroll_up()
|
||||
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
|
||||
draw_arrows(-1)
|
||||
scroll_down()
|
||||
else
|
||||
-- clicked on bar
|
||||
holding_bar = true
|
||||
bar_grip_pos = event.current.y - bar_bounds[1]
|
||||
mouse_last_y = event.current.y
|
||||
end
|
||||
end
|
||||
elseif event.type == CLICK_TYPE.UP then
|
||||
holding_bar = false
|
||||
draw_arrows(0)
|
||||
elseif event.type == CLICK_TYPE.DRAG then
|
||||
if holding_bar then
|
||||
-- if mouse is within vertical frame, including the grip point
|
||||
if event.current.y > (1 + bar_grip_pos) and event.current.y <= ((e.frame.h - bar_height) + bar_grip_pos) then
|
||||
if event.current.y < mouse_last_y then
|
||||
scroll_up(bar_is_scaled)
|
||||
elseif event.current.y > mouse_last_y then
|
||||
scroll_down(bar_is_scaled)
|
||||
end
|
||||
|
||||
mouse_last_y = event.current.y
|
||||
end
|
||||
end
|
||||
elseif event.type == CLICK_TYPE.SCROLL_DOWN then
|
||||
scroll_down()
|
||||
elseif event.type == CLICK_TYPE.SCROLL_UP then
|
||||
scroll_up()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
draw_arrows(0)
|
||||
draw_bar()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return listbox
|
||||
43
graphics/elements/multipane.lua
Normal file
43
graphics/elements/multipane.lua
Normal file
@@ -0,0 +1,43 @@
|
||||
-- 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
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- 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.complete()
|
||||
end
|
||||
|
||||
return multipane
|
||||
@@ -12,6 +12,7 @@ local element = require("graphics.element")
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new pipe network
|
||||
---@param args pipenet_args
|
||||
@@ -37,7 +38,7 @@ local function pipenet(args)
|
||||
args.y = args.y or 1
|
||||
|
||||
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
|
||||
|
||||
-- create new graphics element base object
|
||||
@@ -55,7 +56,7 @@ local function pipenet(args)
|
||||
|
||||
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
|
||||
-- cross width then height
|
||||
@@ -141,7 +142,7 @@ local function pipenet(args)
|
||||
|
||||
end
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return pipenet
|
||||
|
||||
@@ -7,6 +7,7 @@ local element = require("graphics.element")
|
||||
---@class rectangle_args
|
||||
---@field border? graphics_border
|
||||
---@field thin? boolean true to use extra thin even borders
|
||||
---@field even_inner? boolean true to make the inner area of a border even
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
@@ -15,6 +16,7 @@ local element = require("graphics.element")
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new rectangle
|
||||
---@param args rectangle_args
|
||||
@@ -29,27 +31,35 @@ local function rectangle(args)
|
||||
end
|
||||
|
||||
-- offset children
|
||||
local offset_x = 0
|
||||
local offset_y = 0
|
||||
if args.border ~= nil then
|
||||
args.offset_x = args.border.width
|
||||
args.offset_y = args.border.width
|
||||
offset_x = args.border.width
|
||||
offset_y = args.border.width
|
||||
|
||||
-- slightly different y offset if the border is set to even
|
||||
if args.border.even then
|
||||
local width_x2 = (2 * args.border.width)
|
||||
args.offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0)
|
||||
offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args, offset_x, offset_y)
|
||||
|
||||
-- create content window for child elements
|
||||
e.content_window = window.create(e.window, 1 + offset_x, 1 + offset_y, e.frame.w - (2 * offset_x), e.frame.h - (2 * offset_y))
|
||||
e.content_window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.content_window.setTextColor(e.fg_bg.fgd)
|
||||
e.content_window.clear()
|
||||
|
||||
-- draw bordered box if requested
|
||||
-- element constructor will have drawn basic colored rectangle regardless
|
||||
if args.border ~= nil then
|
||||
e.window.setCursorPos(1, 1)
|
||||
|
||||
local border_width = args.offset_x
|
||||
local border_height = args.offset_y
|
||||
local border_width = offset_x
|
||||
local border_height = offset_y
|
||||
local border_blit = colors.toBlit(args.border.color)
|
||||
local width_x2 = border_width * 2
|
||||
local inner_width = e.frame.w - width_x2
|
||||
@@ -66,14 +76,27 @@ local function rectangle(args)
|
||||
local blit_bg_top_bot = util.strrep(border_blit, e.frame.w)
|
||||
|
||||
-- partial bars
|
||||
local p_a = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width)
|
||||
local p_b = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width)
|
||||
local p_s = spaces
|
||||
|
||||
local p_a, p_b, p_s
|
||||
if args.thin == true then
|
||||
p_a = "\x97" .. util.strrep("\x83", inner_width) .. "\x94"
|
||||
p_b = "\x8a" .. util.strrep("\x8f", inner_width) .. "\x85"
|
||||
if args.even_inner == true then
|
||||
p_a = "\x9c" .. util.strrep("\x8c", inner_width) .. "\x93"
|
||||
p_b = "\x8d" .. util.strrep("\x8c", inner_width) .. "\x8e"
|
||||
else
|
||||
p_a = "\x97" .. util.strrep("\x83", inner_width) .. "\x94"
|
||||
p_b = "\x8a" .. util.strrep("\x8f", inner_width) .. "\x85"
|
||||
end
|
||||
|
||||
p_s = "\x95" .. util.spaces(inner_width) .. "\x95"
|
||||
else
|
||||
if args.even_inner == true then
|
||||
p_a = util.strrep("\x83", inner_width + width_x2)
|
||||
p_b = util.strrep("\x8f", inner_width + width_x2)
|
||||
else
|
||||
p_a = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width)
|
||||
p_b = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width)
|
||||
end
|
||||
|
||||
p_s = spaces
|
||||
end
|
||||
|
||||
local p_inv_fg = util.strrep(border_blit, border_width) .. util.strrep(e.fg_bg.blit_bkg, inner_width) ..
|
||||
@@ -112,10 +135,13 @@ local function rectangle(args)
|
||||
if args.thin == true then
|
||||
e.window.blit(p_a, p_inv_bg, p_inv_fg)
|
||||
else
|
||||
local _fg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), p_inv_bg)
|
||||
local _bg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
|
||||
|
||||
if width_x2 % 3 == 1 then
|
||||
e.window.blit(p_b, p_inv_bg, p_inv_fg)
|
||||
e.window.blit(p_b, _fg, _bg)
|
||||
elseif width_x2 % 3 == 2 then
|
||||
e.window.blit(p_a, p_inv_bg, p_inv_fg)
|
||||
e.window.blit(p_a, _fg, _bg)
|
||||
else
|
||||
-- skip line
|
||||
e.window.blit(spaces, blit_fg, blit_bg_sides)
|
||||
@@ -129,12 +155,19 @@ local function rectangle(args)
|
||||
-- partial pixel fill
|
||||
if args.border.even and y == ((e.frame.h - border_width) + 1) then
|
||||
if args.thin == true then
|
||||
e.window.blit(p_b, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
|
||||
if args.even_inner == true then
|
||||
e.window.blit(p_b, blit_bg_top_bot, util.strrep(e.fg_bg.blit_bkg, e.frame.w))
|
||||
else
|
||||
e.window.blit(p_b, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
|
||||
end
|
||||
else
|
||||
local _fg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
|
||||
local _bg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
|
||||
|
||||
if width_x2 % 3 == 1 then
|
||||
e.window.blit(p_a, p_inv_fg, blit_bg_top_bot)
|
||||
elseif width_x2 % 3 == 2 or (args.thin == true) then
|
||||
e.window.blit(p_b, p_inv_fg, blit_bg_top_bot)
|
||||
e.window.blit(p_a, _fg, _bg)
|
||||
elseif width_x2 % 3 == 2 then
|
||||
e.window.blit(p_b, _fg, _bg)
|
||||
else
|
||||
-- skip line
|
||||
e.window.blit(spaces, blit_fg, blit_bg_sides)
|
||||
@@ -153,7 +186,7 @@ local function rectangle(args)
|
||||
end
|
||||
end
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return rectangle
|
||||
|
||||
@@ -5,7 +5,7 @@ local util = require("scada-common.util")
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
---@class textbox_args
|
||||
---@field text string text to show
|
||||
@@ -18,6 +18,7 @@ local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new text box
|
||||
---@param args textbox_args
|
||||
@@ -64,7 +65,7 @@ local function textbox(args)
|
||||
display_text(val)
|
||||
end
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return textbox
|
||||
|
||||
@@ -16,6 +16,7 @@ local element = require("graphics.element")
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new tiling box
|
||||
---@param args tiling_args
|
||||
@@ -81,7 +82,7 @@ local function tiling(args)
|
||||
if inner_width % 2 == 0 then alternator = not alternator end
|
||||
end
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return tiling
|
||||
|
||||
161
graphics/events.lua
Normal file
161
graphics/events.lua
Normal file
@@ -0,0 +1,161 @@
|
||||
--
|
||||
-- Graphics Events and Event Handlers
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local events = {}
|
||||
|
||||
---@enum CLICK_BUTTON
|
||||
events.CLICK_BUTTON = {
|
||||
GENERIC = 0,
|
||||
LEFT_BUTTON = 1,
|
||||
RIGHT_BUTTON = 2,
|
||||
MID_BUTTON = 3
|
||||
}
|
||||
|
||||
---@enum CLICK_TYPE
|
||||
events.CLICK_TYPE = {
|
||||
TAP = 1, -- screen tap (complete click)
|
||||
DOWN = 2, -- button down
|
||||
UP = 3, -- button up (completed a click)
|
||||
DRAG = 4, -- mouse dragged
|
||||
SCROLL_DOWN = 5, -- scroll down
|
||||
SCROLL_UP = 6 -- scroll up
|
||||
}
|
||||
|
||||
-- create a new 2D coordinate
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return coordinate_2d
|
||||
local function _coord2d(x, y) return { x = x, y = y } end
|
||||
|
||||
---@class mouse_interaction
|
||||
---@field monitor string
|
||||
---@field button CLICK_BUTTON
|
||||
---@field type CLICK_TYPE
|
||||
---@field initial coordinate_2d
|
||||
---@field current coordinate_2d
|
||||
|
||||
local handler = {
|
||||
-- left, right, middle button down tracking
|
||||
button_down = {
|
||||
_coord2d(0, 0),
|
||||
_coord2d(0, 0),
|
||||
_coord2d(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
-- create a new monitor touch mouse interaction event
|
||||
---@nodiscard
|
||||
---@param monitor string
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return mouse_interaction
|
||||
local function _monitor_touch(monitor, x, y)
|
||||
return {
|
||||
monitor = monitor,
|
||||
button = events.CLICK_BUTTON.GENERIC,
|
||||
type = events.CLICK_TYPE.TAP,
|
||||
initial = _coord2d(x, y),
|
||||
current = _coord2d(x, y)
|
||||
}
|
||||
end
|
||||
|
||||
-- create a new mouse button mouse interaction event
|
||||
---@nodiscard
|
||||
---@param button CLICK_BUTTON mouse button
|
||||
---@param type CLICK_TYPE click type
|
||||
---@param x1 integer initial x
|
||||
---@param y1 integer initial y
|
||||
---@param x2 integer current x
|
||||
---@param y2 integer current y
|
||||
---@return mouse_interaction
|
||||
local function _mouse_event(button, type, x1, y1, x2, y2)
|
||||
return {
|
||||
monitor = "terminal",
|
||||
button = button,
|
||||
type = type,
|
||||
initial = _coord2d(x1, y1),
|
||||
current = _coord2d(x2, y2)
|
||||
}
|
||||
end
|
||||
|
||||
-- create a new generic mouse interaction event
|
||||
---@nodiscard
|
||||
---@param type CLICK_TYPE
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return mouse_interaction
|
||||
function events.mouse_generic(type, x, y)
|
||||
return {
|
||||
monitor = "",
|
||||
button = events.CLICK_BUTTON.GENERIC,
|
||||
type = type,
|
||||
initial = _coord2d(x, y),
|
||||
current = _coord2d(x, y)
|
||||
}
|
||||
end
|
||||
|
||||
-- create a new transposed mouse interaction event using the event's monitor/button fields
|
||||
---@nodiscard
|
||||
---@param event mouse_interaction
|
||||
---@param elem_pos_x integer element's x position: new x = (event x - element x) + 1
|
||||
---@param elem_pos_y integer element's y position: new y = (event y - element y) + 1
|
||||
---@return mouse_interaction
|
||||
function events.mouse_transposed(event, elem_pos_x, elem_pos_y)
|
||||
return {
|
||||
monitor = event.monitor,
|
||||
button = event.button,
|
||||
type = event.type,
|
||||
initial = _coord2d((event.initial.x - elem_pos_x) + 1, (event.initial.y - elem_pos_y) + 1),
|
||||
current = _coord2d((event.current.x - elem_pos_x) + 1, (event.current.y - elem_pos_y) + 1)
|
||||
}
|
||||
end
|
||||
|
||||
-- check if an event qualifies as a click (tap or up)
|
||||
---@nodiscard
|
||||
---@param t CLICK_TYPE
|
||||
function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events.CLICK_TYPE.UP end
|
||||
|
||||
-- create a new mouse event to pass onto graphics renderer<br>
|
||||
-- supports: mouse_click, mouse_up, mouse_drag, mouse_scroll, and monitor_touch
|
||||
---@param event_type os_event OS event to handle
|
||||
---@param opt integer|string button, scroll direction, or monitor for monitor touch
|
||||
---@param x integer x coordinate
|
||||
---@param y integer y coordinate
|
||||
---@return mouse_interaction|nil
|
||||
function events.new_mouse_event(event_type, opt, x, y)
|
||||
if event_type == "mouse_click" then
|
||||
---@cast opt 1|2|3
|
||||
handler.button_down[opt] = _coord2d(x, y)
|
||||
return _mouse_event(opt, events.CLICK_TYPE.DOWN, x, y, x, y)
|
||||
elseif event_type == "mouse_up" then
|
||||
---@cast opt 1|2|3
|
||||
local initial = handler.button_down[opt] ---@type coordinate_2d
|
||||
return _mouse_event(opt, events.CLICK_TYPE.UP, initial.x, initial.y, x, y)
|
||||
elseif event_type == "monitor_touch" then
|
||||
---@cast opt string
|
||||
return _monitor_touch(opt, x, y)
|
||||
elseif event_type == "mouse_drag" then
|
||||
---@cast opt 1|2|3
|
||||
local initial = handler.button_down[opt] ---@type coordinate_2d
|
||||
return _mouse_event(opt, events.CLICK_TYPE.DRAG, initial.x, initial.y, x, y)
|
||||
elseif event_type == "mouse_scroll" then
|
||||
---@cast opt 1|-1
|
||||
local scroll_direction = util.trinary(opt == 1, events.CLICK_TYPE.SCROLL_DOWN, events.CLICK_TYPE.SCROLL_UP)
|
||||
return _mouse_event(events.CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y)
|
||||
end
|
||||
end
|
||||
|
||||
-- create a new key event to pass onto graphics renderer<br>
|
||||
-- supports: char, key, and key_up
|
||||
---@param event_type os_event
|
||||
function events.new_key_event(event_type)
|
||||
if event_type == "char" then
|
||||
elseif event_type == "key" then
|
||||
elseif event_type == "key_up" then
|
||||
end
|
||||
end
|
||||
|
||||
return events
|
||||
@@ -2,7 +2,7 @@
|
||||
-- Indicator Light Flasher
|
||||
--
|
||||
|
||||
local tcd = require("scada-common.tcallbackdsp")
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local flasher = {}
|
||||
|
||||
|
||||
32
imgen.py
32
imgen.py
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# list files in a directory
|
||||
def list_files(path):
|
||||
@@ -7,7 +8,7 @@ def list_files(path):
|
||||
|
||||
for (root, dirs, files) in os.walk(path):
|
||||
for f in files:
|
||||
list.append(root[2:] + "/" + f)
|
||||
list.append((root[2:] + "/" + f).replace('\\','/'))
|
||||
|
||||
return list
|
||||
|
||||
@@ -68,8 +69,8 @@ def make_manifest(size):
|
||||
"pocket" : list_files("./pocket"),
|
||||
},
|
||||
"depends" : {
|
||||
"reactor-plc" : [ "system", "common" ],
|
||||
"rtu" : [ "system", "common" ],
|
||||
"reactor-plc" : [ "system", "common", "graphics" ],
|
||||
"rtu" : [ "system", "common", "graphics" ],
|
||||
"supervisor" : [ "system", "common" ],
|
||||
"coordinator" : [ "system", "common", "graphics" ],
|
||||
"pocket" : [ "system", "common", "graphics" ]
|
||||
@@ -100,7 +101,30 @@ f.close()
|
||||
|
||||
manifest_size = os.path.getsize("install_manifest.json")
|
||||
|
||||
final_manifest = make_manifest(manifest_size)
|
||||
|
||||
# calculate file size then regenerate with embedded size
|
||||
f = open("install_manifest.json", "w")
|
||||
json.dump(make_manifest(manifest_size), f)
|
||||
json.dump(final_manifest, f)
|
||||
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("./deploy/" + key + ".json", "w")
|
||||
|
||||
if version.find("alpha") >= 0:
|
||||
color = "yellow"
|
||||
elif version.find("beta") >= 0:
|
||||
color = "orange"
|
||||
else:
|
||||
color = "blue"
|
||||
|
||||
json.dump({
|
||||
"schemaVersion": 1,
|
||||
"label": key,
|
||||
"message": "" + version,
|
||||
"color": color
|
||||
}, f)
|
||||
|
||||
f.close()
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,23 @@
|
||||
local config = {}
|
||||
|
||||
-- supervisor comms channel
|
||||
config.SVR_CHANNEL = 16240
|
||||
-- coordinator comms channel
|
||||
config.CRD_CHANNEL = 16243
|
||||
-- pocket comms channel
|
||||
config.PKT_CHANNEL = 16244
|
||||
-- max trusted modem message distance (0 to disable check)
|
||||
config.TRUSTED_RANGE = 0
|
||||
-- time in seconds (>= 2) before assuming a remote device is no longer active
|
||||
config.COMMS_TIMEOUT = 5
|
||||
|
||||
-- log path
|
||||
config.LOG_PATH = "/log.txt"
|
||||
-- log mode
|
||||
-- 0 = APPEND (adds to existing file on start)
|
||||
-- 1 = NEW (replaces existing file on start)
|
||||
config.LOG_MODE = 0
|
||||
-- true to log verbose debug messages
|
||||
config.LOG_DEBUG = false
|
||||
|
||||
return config
|
||||
|
||||
35
pocket/coreio.lua
Normal file
35
pocket/coreio.lua
Normal file
@@ -0,0 +1,35 @@
|
||||
--
|
||||
-- Core I/O - Pocket Central I/O Management
|
||||
--
|
||||
|
||||
local psil = require("scada-common.psil")
|
||||
|
||||
local coreio = {}
|
||||
|
||||
---@class pocket_core_io
|
||||
local io = {
|
||||
ps = psil.create()
|
||||
}
|
||||
|
||||
---@enum POCKET_LINK_STATE
|
||||
local LINK_STATE = {
|
||||
UNLINKED = 0,
|
||||
SV_LINK_ONLY = 1,
|
||||
API_LINK_ONLY = 2,
|
||||
LINKED = 3
|
||||
}
|
||||
|
||||
coreio.LINK_STATE = LINK_STATE
|
||||
|
||||
-- get the core PSIL
|
||||
function coreio.core_ps()
|
||||
return io.ps
|
||||
end
|
||||
|
||||
-- set network link state
|
||||
---@param state POCKET_LINK_STATE
|
||||
function coreio.report_link_state(state)
|
||||
io.ps.publish("link_state", state)
|
||||
end
|
||||
|
||||
return coreio
|
||||
429
pocket/pocket.lua
Normal file
429
pocket/pocket.lua
Normal file
@@ -0,0 +1,429 @@
|
||||
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 pkt_channel integer pocket comms channel
|
||||
---@param svr_channel integer supervisor access channel
|
||||
---@param crd_channel integer coordinator access channel
|
||||
---@param range integer trusted device connection range
|
||||
---@param sv_watchdog watchdog
|
||||
---@param api_watchdog watchdog
|
||||
function pocket.comms(version, modem, pkt_channel, svr_channel, crd_channel, range, sv_watchdog, api_watchdog)
|
||||
local self = {
|
||||
sv = {
|
||||
linked = false,
|
||||
addr = comms.BROADCAST,
|
||||
seq_num = 0,
|
||||
r_seq_num = nil, ---@type nil|integer
|
||||
last_est_ack = ESTABLISH_ACK.ALLOW
|
||||
},
|
||||
api = {
|
||||
linked = false,
|
||||
addr = comms.BROADCAST,
|
||||
seq_num = 0,
|
||||
r_seq_num = nil, ---@type nil|integer
|
||||
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(pkt_channel)
|
||||
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.addr, self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
||||
|
||||
modem.transmit(svr_channel, pkt_channel, 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.addr, self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
||||
|
||||
modem.transmit(crd_channel, pkt_channel, 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.addr, self.api.seq_num, PROTOCOL.COORD_API, pkt.raw_sendable())
|
||||
|
||||
-- modem.transmit(crd_channel, pkt_channel, 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
|
||||
self.sv.r_seq_num = nil
|
||||
self.sv.addr = comms.BROADCAST
|
||||
_send_sv(SCADA_MGMT_TYPE.CLOSE, {})
|
||||
end
|
||||
|
||||
-- close connection to coordinator API server
|
||||
function public.close_api()
|
||||
api_watchdog.cancel()
|
||||
self.api.linked = false
|
||||
self.api.r_seq_num = nil
|
||||
self.api.addr = comms.BROADCAST
|
||||
_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_chan = packet.scada_frame.local_channel()
|
||||
local r_chan = packet.scada_frame.remote_channel()
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
local src_addr = packet.scada_frame.src_addr()
|
||||
|
||||
if l_chan ~= pkt_channel then
|
||||
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
||||
elseif r_chan == crd_channel then
|
||||
-- check sequence number
|
||||
if self.api.r_seq_num == nil then
|
||||
self.api.r_seq_num = packet.scada_frame.seq_num()
|
||||
elseif self.connected and ((self.api.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
|
||||
log.warning("sequence out-of-order (API): last = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
return
|
||||
elseif self.api.linked and (src_addr ~= self.api.addr) then
|
||||
log.debug("received packet from unknown computer " .. src_addr .. " while linked (API expected " .. self.api.addr ..
|
||||
"); channel in use by another system?")
|
||||
return
|
||||
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
|
||||
self.api.addr = src_addr
|
||||
|
||||
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
|
||||
self.api.r_seq_num = nil
|
||||
self.api.addr = comms.BROADCAST
|
||||
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_chan == svr_channel then
|
||||
-- check sequence number
|
||||
if self.sv.r_seq_num == nil then
|
||||
self.sv.r_seq_num = packet.scada_frame.seq_num()
|
||||
elseif self.connected and ((self.sv.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
|
||||
log.warning("sequence out-of-order (SVR): last = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
return
|
||||
elseif self.sv.linked and (src_addr ~= self.sv.addr) then
|
||||
log.debug("received packet from unknown computer " .. src_addr .. " while linked (SVR expected " .. self.sv.addr ..
|
||||
"); channel in use by another system?")
|
||||
return
|
||||
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
|
||||
self.sv.addr = src_addr
|
||||
|
||||
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
|
||||
self.sv.r_seq_num = nil
|
||||
self.sv.addr = comms.BROADCAST
|
||||
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_chan, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- check if we are still linked with the supervisor
|
||||
---@nodiscard
|
||||
function public.is_sv_linked() return self.sv.linked end
|
||||
|
||||
-- check if we are still linked with the coordinator
|
||||
---@nodiscard
|
||||
function public.is_api_linked() return self.api.linked end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
|
||||
return pocket
|
||||
80
pocket/renderer.lua
Normal file
80
pocket/renderer.lua
Normal file
@@ -0,0 +1,80 @@
|
||||
--
|
||||
-- Graphics Rendering Control
|
||||
--
|
||||
|
||||
local main_view = require("pocket.ui.main")
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
|
||||
local renderer = {}
|
||||
|
||||
local ui = {
|
||||
display = nil
|
||||
}
|
||||
|
||||
-- start the pocket GUI
|
||||
function renderer.start_ui()
|
||||
if ui.display == nil then
|
||||
-- reset screen
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.colors do
|
||||
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||
end
|
||||
|
||||
-- init front panel view
|
||||
ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||
main_view(ui.display)
|
||||
|
||||
-- start flasher callback task
|
||||
flasher.run()
|
||||
end
|
||||
end
|
||||
|
||||
-- close out the UI
|
||||
function renderer.close_ui()
|
||||
if ui.display ~= nil then
|
||||
-- stop blinking indicators
|
||||
flasher.clear()
|
||||
|
||||
-- hide to stop animation callbacks
|
||||
ui.display.hide()
|
||||
|
||||
-- clear root UI elements
|
||||
ui.display = nil
|
||||
|
||||
-- restore colors
|
||||
for i = 1, #style.colors do
|
||||
local r, g, b = term.nativePaletteColor(style.colors[i].c)
|
||||
term.setPaletteColor(style.colors[i].c, r, g, b)
|
||||
end
|
||||
|
||||
-- reset terminal
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- is the UI ready?
|
||||
---@nodiscard
|
||||
---@return boolean ready
|
||||
function renderer.ui_ready() return ui.display ~= nil end
|
||||
|
||||
-- handle a mouse event
|
||||
---@param event mouse_interaction|nil
|
||||
function renderer.handle_mouse(event)
|
||||
if ui.display ~= nil and event ~= nil then
|
||||
ui.display.handle_mouse(event)
|
||||
end
|
||||
end
|
||||
|
||||
return renderer
|
||||
@@ -1,16 +1,180 @@
|
||||
--
|
||||
-- SCADA Coordinator Access on a Pocket Computer
|
||||
-- SCADA System Access on a Pocket Computer
|
||||
--
|
||||
|
||||
require("/initenv").init_env()
|
||||
|
||||
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 tcd = require("scada-common.tcd")
|
||||
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.4.5"
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
println("Sorry, this isn't written yet :(")
|
||||
----------------------------------------
|
||||
-- config validation
|
||||
----------------------------------------
|
||||
|
||||
local cfv = util.new_validator()
|
||||
|
||||
cfv.assert_channel(config.SVR_CHANNEL)
|
||||
cfv.assert_channel(config.CRD_CHANNEL)
|
||||
cfv.assert_channel(config.PKT_CHANNEL)
|
||||
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.PKT_CHANNEL, config.SVR_CHANNEL,
|
||||
config.CRD_CHANNEL, 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")
|
||||
|
||||
-- main event loop
|
||||
while true 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
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
-- got a packet
|
||||
local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
pocket_comms.handle_packet(packet)
|
||||
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then
|
||||
-- handle a monitor touch event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
log.info("terminate requested, closing server connections...")
|
||||
pocket_comms.close()
|
||||
log.info("connections closed")
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
renderer.close_ui()
|
||||
end
|
||||
|
||||
println_ts("exited")
|
||||
log.info("exited")
|
||||
end
|
||||
|
||||
if not xpcall(main, crash.handler) then
|
||||
pcall(renderer.close_ui)
|
||||
crash.exit()
|
||||
else
|
||||
log.close()
|
||||
end
|
||||
|
||||
41
pocket/ui/components/conn_waiting.lua
Normal file
41
pocket/ui/components/conn_waiting.lua
Normal file
@@ -0,0 +1,41 @@
|
||||
--
|
||||
-- Connection Waiting Spinner
|
||||
--
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local WaitingAnim = require("graphics.elements.animations.waiting")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
-- create a waiting view
|
||||
---@param parent graphics_element parent
|
||||
---@param y integer y offset
|
||||
local function init(parent, y, is_api)
|
||||
-- root div
|
||||
local root = Div{parent=parent,x=1,y=1}
|
||||
|
||||
-- bounding box div
|
||||
local box = Div{parent=root,x=1,y=y,height=5}
|
||||
|
||||
local waiting_x = math.floor(parent.get_width() / 2) - 1
|
||||
|
||||
if is_api then
|
||||
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)}
|
||||
TextBox{parent=box,text="Connecting to API",alignment=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
|
||||
99
pocket/ui/main.lua
Normal file
99
pocket/ui/main.lua
Normal file
@@ -0,0 +1,99 @@
|
||||
--
|
||||
-- Pocket GUI Root
|
||||
--
|
||||
|
||||
local coreio = require("pocket.coreio")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
local conn_waiting = require("pocket.ui.components.conn_waiting")
|
||||
|
||||
local home_page = require("pocket.ui.pages.home_page")
|
||||
local unit_page = require("pocket.ui.pages.unit_page")
|
||||
local reactor_page = require("pocket.ui.pages.reactor_page")
|
||||
local boiler_page = require("pocket.ui.pages.boiler_page")
|
||||
local turbine_page = require("pocket.ui.pages.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
|
||||
22
pocket/ui/pages/boiler_page.lua
Normal file
22
pocket/ui/pages/boiler_page.lua
Normal file
@@ -0,0 +1,22 @@
|
||||
-- local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
-- local cpair = core.cpair
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
-- new boiler page view
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
TextBox{parent=main,text="BOILERS",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
22
pocket/ui/pages/home_page.lua
Normal file
22
pocket/ui/pages/home_page.lua
Normal file
@@ -0,0 +1,22 @@
|
||||
-- local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
-- local cpair = core.cpair
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
-- new home page view
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
TextBox{parent=main,text="HOME",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
22
pocket/ui/pages/reactor_page.lua
Normal file
22
pocket/ui/pages/reactor_page.lua
Normal file
@@ -0,0 +1,22 @@
|
||||
-- local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
-- local cpair = core.cpair
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
-- new reactor page view
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
TextBox{parent=main,text="REACTOR",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
22
pocket/ui/pages/turbine_page.lua
Normal file
22
pocket/ui/pages/turbine_page.lua
Normal file
@@ -0,0 +1,22 @@
|
||||
-- local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
-- local cpair = core.cpair
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
-- new turbine page view
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
TextBox{parent=main,text="TURBINES",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
22
pocket/ui/pages/unit_page.lua
Normal file
22
pocket/ui/pages/unit_page.lua
Normal file
@@ -0,0 +1,22 @@
|
||||
-- local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
-- local cpair = core.cpair
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
-- new unit page view
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
TextBox{parent=main,text="UNITS",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
158
pocket/ui/style.lua
Normal file
158
pocket/ui/style.lua
Normal file
@@ -0,0 +1,158 @@
|
||||
--
|
||||
-- Graphics Style Options
|
||||
--
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local style = {}
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
-- GLOBAL --
|
||||
|
||||
style.root = cpair(colors.white, colors.black)
|
||||
style.header = cpair(colors.white, colors.gray)
|
||||
style.label = cpair(colors.gray, colors.lightGray)
|
||||
|
||||
style.colors = {
|
||||
{ c = colors.red, hex = 0xdf4949 },
|
||||
{ c = colors.orange, hex = 0xffb659 },
|
||||
{ c = colors.yellow, hex = 0xfffc79 },
|
||||
{ c = colors.lime, hex = 0x80ff80 },
|
||||
{ c = colors.green, hex = 0x4aee8a },
|
||||
{ c = colors.cyan, hex = 0x34bac8 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.purple, hex = 0xb156ee },
|
||||
{ c = colors.pink, hex = 0xf26ba2 },
|
||||
{ c = colors.magenta, hex = 0xf9488a },
|
||||
-- { c = colors.white, hex = 0xf0f0f0 },
|
||||
{ c = colors.lightGray, hex = 0xcacaca },
|
||||
{ c = colors.gray, hex = 0x575757 },
|
||||
-- { c = colors.black, hex = 0x191919 },
|
||||
-- { c = colors.brown, hex = 0x7f664c }
|
||||
}
|
||||
|
||||
-- MAIN LAYOUT --
|
||||
|
||||
style.reactor = {
|
||||
-- reactor states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "PLC OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "PLC FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "DISABLED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ACTIVE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.red),
|
||||
text = "SCRAMMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.red),
|
||||
text = "FORCE DISABLED"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style.boiler = {
|
||||
-- boiler states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "IDLE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ACTIVE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style.turbine = {
|
||||
-- turbine states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "IDLE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ACTIVE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.red),
|
||||
text = "TRIP"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style.imatrix = {
|
||||
-- induction matrix states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ONLINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "LOW CHARGE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "HIGH CHARGE"
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return style
|
||||
@@ -5,10 +5,14 @@ config.NETWORKED = true
|
||||
-- unique reactor ID
|
||||
config.REACTOR_ID = 1
|
||||
|
||||
-- port to send packets TO server
|
||||
config.SERVER_PORT = 16000
|
||||
-- port to listen to incoming packets FROM server
|
||||
config.LISTEN_PORT = 14001
|
||||
-- for offline mode, this redstone interface will turn off (open a valve)
|
||||
-- when emergency coolant is needed due to low coolant
|
||||
-- config.EMERGENCY_COOL = { side = "right", color = nil }
|
||||
|
||||
-- supervisor comms channel
|
||||
config.SVR_CHANNEL = 16240
|
||||
-- PLC comms channel
|
||||
config.PLC_CHANNEL = 16241
|
||||
-- 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
|
||||
@@ -20,5 +24,7 @@ config.LOG_PATH = "/log.txt"
|
||||
-- 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
|
||||
|
||||
105
reactor-plc/databus.lua
Normal file
105
reactor-plc/databus.lua
Normal file
@@ -0,0 +1,105 @@
|
||||
--
|
||||
-- Data Bus - Central Communication Linking for PLC Front Panel
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local psil = require("scada-common.psil")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local databus = {}
|
||||
|
||||
-- databus PSIL
|
||||
databus.ps = psil.create()
|
||||
|
||||
local dbus_iface = {
|
||||
rps_scram = function () log.debug("DBUS: unset rps_scram() called") end,
|
||||
rps_reset = function () log.debug("DBUS: unset rps_reset() called") end
|
||||
}
|
||||
|
||||
-- call to toggle heartbeat signal
|
||||
function databus.heartbeat() databus.ps.toggle("heartbeat") end
|
||||
|
||||
-- link RPS command functions
|
||||
---@param scram function reactor SCRAM function
|
||||
---@param reset function RPS reset function
|
||||
function databus.link_rps(scram, reset)
|
||||
dbus_iface.rps_scram = scram
|
||||
dbus_iface.rps_reset = reset
|
||||
end
|
||||
|
||||
-- transmit a command to the RPS to SCRAM
|
||||
function databus.rps_scram() dbus_iface.rps_scram() end
|
||||
|
||||
-- transmit a command to the RPS to reset
|
||||
function databus.rps_reset() dbus_iface.rps_reset() end
|
||||
|
||||
-- transmit firmware versions across the bus
|
||||
---@param plc_v string PLC version
|
||||
---@param comms_v string comms version
|
||||
function databus.tx_versions(plc_v, comms_v)
|
||||
databus.ps.publish("version", plc_v)
|
||||
databus.ps.publish("comms_version", comms_v)
|
||||
end
|
||||
|
||||
-- transmit unit ID across the bus
|
||||
---@param id integer unit ID
|
||||
function databus.tx_id(id)
|
||||
databus.ps.publish("unit_id", id)
|
||||
end
|
||||
|
||||
-- transmit hardware status across the bus
|
||||
---@param plc_state plc_state
|
||||
function databus.tx_hw_status(plc_state)
|
||||
databus.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2)))
|
||||
databus.ps.publish("has_modem", not plc_state.no_modem)
|
||||
databus.ps.publish("degraded", plc_state.degraded)
|
||||
databus.ps.publish("init_ok", plc_state.init_ok)
|
||||
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
|
||||
|
||||
-- transmit reactor enable state across the bus
|
||||
---@param active boolean reactor active
|
||||
function databus.tx_reactor_state(active)
|
||||
databus.ps.publish("reactor_active", active)
|
||||
end
|
||||
|
||||
-- transmit RPS data across the bus
|
||||
---@param tripped boolean RPS tripped
|
||||
---@param status table RPS status
|
||||
---@param emer_cool_active boolean RPS activated the emergency coolant
|
||||
function databus.tx_rps(tripped, status, emer_cool_active)
|
||||
databus.ps.publish("rps_scram", tripped)
|
||||
databus.ps.publish("rps_damage", status[1])
|
||||
databus.ps.publish("rps_high_temp", status[2])
|
||||
databus.ps.publish("rps_low_ccool", status[3])
|
||||
databus.ps.publish("rps_high_waste", status[4])
|
||||
databus.ps.publish("rps_high_hcool", status[5])
|
||||
databus.ps.publish("rps_no_fuel", status[6])
|
||||
databus.ps.publish("rps_fault", status[7])
|
||||
databus.ps.publish("rps_timeout", status[8])
|
||||
databus.ps.publish("rps_manual", status[9])
|
||||
databus.ps.publish("rps_automatic", status[10])
|
||||
databus.ps.publish("rps_sysfail", status[11])
|
||||
databus.ps.publish("emer_cool", emer_cool_active)
|
||||
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
|
||||
147
reactor-plc/panel/front_panel.lua
Normal file
147
reactor-plc/panel/front_panel.lua
Normal file
@@ -0,0 +1,147 @@
|
||||
--
|
||||
-- Main SCADA Coordinator GUI
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local config = require("reactor-plc.config")
|
||||
local databus = require("reactor-plc.databus")
|
||||
|
||||
local style = require("reactor-plc.panel.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
|
||||
local LED = require("graphics.elements.indicators.led")
|
||||
local LEDPair = require("graphics.elements.indicators.ledpair")
|
||||
local RGBLED = require("graphics.elements.indicators.ledrgb")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
-- create new main view
|
||||
---@param panel graphics_element main displaybox
|
||||
local function init(panel)
|
||||
local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
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 init_ok = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
|
||||
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)}
|
||||
system.line_break()
|
||||
|
||||
init_ok.register(databus.ps, "init_ok", init_ok.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 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(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
system.line_break()
|
||||
|
||||
reactor.register(databus.ps, "reactor_dev_state", reactor.update)
|
||||
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_rps = LED{parent=system,label="RT RPS",colors=cpair(colors.green,colors.green_off)}
|
||||
local rt_cmtx = LED{parent=system,label="RT COMMS TX",colors=cpair(colors.green,colors.green_off)}
|
||||
local rt_cmrx = LED{parent=system,label="RT COMMS RX",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()
|
||||
|
||||
rt_main.register(databus.ps, "routine__main", rt_main.update)
|
||||
rt_rps.register(databus.ps, "routine__rps", rt_rps.update)
|
||||
rt_cmtx.register(databus.ps, "routine__comms_tx", rt_cmtx.update)
|
||||
rt_cmrx.register(databus.ps, "routine__comms_rx", rt_cmrx.update)
|
||||
rt_sctl.register(databus.ps, "routine__spctl", rt_sctl.update)
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local comp_id = util.sprintf("(%d)", os.getComputerID())
|
||||
TextBox{parent=system,x=9,y=5,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)}
|
||||
|
||||
--
|
||||
-- status & controls
|
||||
--
|
||||
|
||||
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)}
|
||||
|
||||
-- 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 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,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)}
|
||||
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)}
|
||||
|
||||
active.register(databus.ps, "reactor_active", active.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 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}
|
||||
|
||||
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)
|
||||
|
||||
--
|
||||
-- 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_man = LED{parent=rps,label="MANUAL",colors=cpair(colors.red,colors.red_off)}
|
||||
local rps_auto = LED{parent=rps,label="AUTOMATIC",colors=cpair(colors.red,colors.red_off)}
|
||||
local rps_tmo = LED{parent=rps,label="TIMEOUT",colors=cpair(colors.red,colors.red_off)}
|
||||
local rps_flt = LED{parent=rps,label="PLC FAULT",colors=cpair(colors.red,colors.red_off)}
|
||||
local rps_fail = LED{parent=rps,label="RCT FAULT",colors=cpair(colors.red,colors.red_off)}
|
||||
rps.line_break()
|
||||
local rps_dmg = LED{parent=rps,label="HI DAMAGE",colors=cpair(colors.red,colors.red_off)}
|
||||
local rps_tmp = LED{parent=rps,label="HI TEMP",colors=cpair(colors.red,colors.red_off)}
|
||||
rps.line_break()
|
||||
local rps_nof = LED{parent=rps,label="LO FUEL",colors=cpair(colors.red,colors.red_off)}
|
||||
local rps_wst = LED{parent=rps,label="HI WASTE",colors=cpair(colors.red,colors.red_off)}
|
||||
rps.line_break()
|
||||
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)}
|
||||
|
||||
rps_man.register(databus.ps, "rps_manual", rps_man.update)
|
||||
rps_auto.register(databus.ps, "rps_automatic", rps_auto.update)
|
||||
rps_tmo.register(databus.ps, "rps_timeout", rps_tmo.update)
|
||||
rps_flt.register(databus.ps, "rps_fault", rps_flt.update)
|
||||
rps_fail.register(databus.ps, "rps_sysfail", rps_fail.update)
|
||||
rps_dmg.register(databus.ps, "rps_damage", rps_dmg.update)
|
||||
rps_tmp.register(databus.ps, "rps_high_temp", rps_tmp.update)
|
||||
rps_nof.register(databus.ps, "rps_no_fuel", rps_nof.update)
|
||||
rps_wst.register(databus.ps, "rps_high_waste", rps_wst.update)
|
||||
rps_ccl.register(databus.ps, "rps_low_ccool", rps_ccl.update)
|
||||
rps_hcl.register(databus.ps, "rps_high_hcool", rps_hcl.update)
|
||||
end
|
||||
|
||||
return init
|
||||
41
reactor-plc/panel/style.lua
Normal file
41
reactor-plc/panel/style.lua
Normal file
@@ -0,0 +1,41 @@
|
||||
--
|
||||
-- Graphics Style Options
|
||||
--
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local style = {}
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
-- GLOBAL --
|
||||
|
||||
-- remap global colors
|
||||
colors.ivory = colors.pink
|
||||
colors.red_off = colors.brown
|
||||
colors.yellow_off = colors.magenta
|
||||
colors.green_off = colors.lime
|
||||
|
||||
style.root = cpair(colors.black, colors.ivory)
|
||||
style.header = cpair(colors.black, colors.lightGray)
|
||||
|
||||
style.colors = {
|
||||
{ c = colors.red, hex = 0xdf4949 }, -- RED ON
|
||||
{ c = colors.orange, hex = 0xffb659 },
|
||||
{ c = colors.yellow, hex = 0xf9fb53 }, -- YELLOW ON
|
||||
{ c = colors.lime, hex = 0x16665a }, -- GREEN OFF
|
||||
{ c = colors.green, hex = 0x6be551 }, -- GREEN ON
|
||||
{ c = colors.cyan, hex = 0x34bac8 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.purple, hex = 0xb156ee },
|
||||
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
|
||||
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
|
||||
-- { c = colors.white, hex = 0xdcd9ca },
|
||||
{ c = colors.lightGray, hex = 0xb1b8b3 },
|
||||
{ c = colors.gray, hex = 0x575757 },
|
||||
-- { c = colors.black, hex = 0x191919 },
|
||||
{ c = colors.brown, hex = 0x672223 } -- RED OFF
|
||||
}
|
||||
|
||||
return style
|
||||
@@ -1,9 +1,12 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local const = require("scada-common.constants")
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
local comms = require("scada-common.comms")
|
||||
local const = require("scada-common.constants")
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local rsio = require("scada-common.rsio")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local databus = require("reactor-plc.databus")
|
||||
|
||||
local plc = {}
|
||||
|
||||
@@ -18,11 +21,6 @@ local AUTO_ACK = comms.PLC_AUTO_ACK
|
||||
|
||||
local RPS_LIMITS = const.RPS_LIMITS
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
-- I sure hope the devs don't change this error message, not that it would have safety implications
|
||||
-- I wish they didn't change it to be like this
|
||||
local PCALL_SCRAM_MSG = "pcall: Scram requires the reactor to be active."
|
||||
@@ -34,7 +32,8 @@ local PCALL_START_MSG = "pcall: Reactor is already active."
|
||||
---@nodiscard
|
||||
---@param reactor table
|
||||
---@param is_formed boolean
|
||||
function plc.rps_init(reactor, is_formed)
|
||||
---@param emer_cool nil|table emergency coolant configuration
|
||||
function plc.rps_init(reactor, is_formed, emer_cool)
|
||||
local state_keys = {
|
||||
high_dmg = 1,
|
||||
high_temp = 2,
|
||||
@@ -54,6 +53,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
state = { false, false, false, false, false, false, false, false, false, false, false, false },
|
||||
reactor_enabled = false,
|
||||
enabled_at = 0,
|
||||
emer_cool_active = nil, ---@type boolean
|
||||
formed = is_formed,
|
||||
force_disabled = false,
|
||||
tripped = false,
|
||||
@@ -69,9 +69,39 @@ function plc.rps_init(reactor, is_formed)
|
||||
end
|
||||
end
|
||||
|
||||
-- clear reactor access fault flag
|
||||
local function _clear_fault()
|
||||
self.state[state_keys.fault] = false
|
||||
-- set emergency coolant control (if configured)
|
||||
---@param state boolean true to enable emergency coolant, false to disable
|
||||
local function _set_emer_cool(state)
|
||||
-- check if this was configured: if it's a table, fields have already been validated.
|
||||
if type(emer_cool) == "table" then
|
||||
local level = rsio.digital_write_active(rsio.IO.U_EMER_COOL, state)
|
||||
|
||||
if level ~= false then
|
||||
if rsio.is_color(emer_cool.color) then
|
||||
local output = rs.getBundledOutput(emer_cool.side)
|
||||
|
||||
if rsio.digital_write(level) then
|
||||
output = colors.combine(output, emer_cool.color)
|
||||
else
|
||||
output = colors.subtract(output, emer_cool.color)
|
||||
end
|
||||
|
||||
rs.setBundledOutput(emer_cool.side, output)
|
||||
else
|
||||
rs.setOutput(emer_cool.side, rsio.digital_write(level))
|
||||
end
|
||||
|
||||
if state ~= self.emer_cool_active then
|
||||
if state then
|
||||
log.info("RPS: emergency coolant valve OPENED")
|
||||
else
|
||||
log.info("RPS: emergency coolant valve CLOSED")
|
||||
end
|
||||
|
||||
self.emer_cool_active = state
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- check if the reactor is formed
|
||||
@@ -348,6 +378,12 @@ function plc.rps_init(reactor, is_formed)
|
||||
end
|
||||
end
|
||||
|
||||
-- update emergency coolant control if configured
|
||||
_set_emer_cool(self.state[state_keys.low_coolant])
|
||||
|
||||
-- report RPS status
|
||||
databus.tx_rps(self.tripped, self.state, self.emer_cool_active)
|
||||
|
||||
return self.tripped, status, first_trip
|
||||
end
|
||||
|
||||
@@ -358,6 +394,8 @@ function plc.rps_init(reactor, is_formed)
|
||||
function public.is_tripped() return self.tripped end
|
||||
---@nodiscard
|
||||
function public.get_trip_cause() return self.trip_cause end
|
||||
---@nodiscard
|
||||
function public.is_low_coolant() return self.states[state_keys.low_coolant] end
|
||||
|
||||
---@nodiscard
|
||||
function public.is_active() return self.reactor_enabled end
|
||||
@@ -397,6 +435,9 @@ function plc.rps_init(reactor, is_formed)
|
||||
end
|
||||
end
|
||||
|
||||
-- link functions with databus
|
||||
databus.link_rps(public.trip_manual, public.reset)
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
@@ -405,14 +446,15 @@ end
|
||||
---@param id integer reactor ID
|
||||
---@param version string PLC version
|
||||
---@param modem table modem device
|
||||
---@param local_port integer local listening port
|
||||
---@param server_port integer remote server port
|
||||
---@param plc_channel integer PLC comms channel
|
||||
---@param svr_channel integer supervisor server channel
|
||||
---@param range integer trusted device connection range
|
||||
---@param reactor table reactor device
|
||||
---@param rps rps RPS reference
|
||||
---@param conn_watchdog watchdog watchdog reference
|
||||
function plc.comms(id, version, modem, local_port, server_port, range, reactor, rps, conn_watchdog)
|
||||
function plc.comms(id, version, modem, plc_channel, svr_channel, range, reactor, rps, conn_watchdog)
|
||||
local self = {
|
||||
sv_addr = comms.BROADCAST,
|
||||
seq_num = 0,
|
||||
r_seq_num = nil,
|
||||
scrammed = false,
|
||||
@@ -431,7 +473,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
-- configure modem channels
|
||||
local function _conf_channels()
|
||||
modem.closeAll()
|
||||
modem.open(local_port)
|
||||
modem.open(plc_channel)
|
||||
end
|
||||
|
||||
_conf_channels()
|
||||
@@ -444,9 +486,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
local r_pkt = comms.rplc_packet()
|
||||
|
||||
r_pkt.make(id, msg_type, msg)
|
||||
s_pkt.make(self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable())
|
||||
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable())
|
||||
|
||||
modem.transmit(server_port, local_port, s_pkt.raw_sendable())
|
||||
modem.transmit(svr_channel, plc_channel, s_pkt.raw_sendable())
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
@@ -458,9 +500,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
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())
|
||||
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
modem.transmit(server_port, local_port, s_pkt.raw_sendable())
|
||||
modem.transmit(svr_channel, plc_channel, s_pkt.raw_sendable())
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
@@ -600,8 +642,6 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
if not reactor.__p_is_faulted() then
|
||||
_send(RPLC_TYPE.MEK_STRUCT, mek_data)
|
||||
self.resend_build = false
|
||||
else
|
||||
log.error("failed to send structure: PPM fault")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -628,9 +668,11 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
|
||||
-- unlink from the server
|
||||
function public.unlink()
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.linked = false
|
||||
self.r_seq_num = nil
|
||||
self.status_cache = nil
|
||||
databus.tx_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
end
|
||||
|
||||
-- close the connection to the server
|
||||
@@ -692,7 +734,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
end
|
||||
end
|
||||
|
||||
-- parse an RPLC packet
|
||||
-- parse a packet
|
||||
---@nodiscard
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
@@ -721,25 +763,37 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
pkt = mgmt_pkt.get()
|
||||
end
|
||||
else
|
||||
log.error("illegal packet type " .. s_pkt.protocol(), true)
|
||||
log.debug("unsupported packet type " .. s_pkt.protocol(), true)
|
||||
end
|
||||
end
|
||||
|
||||
return pkt
|
||||
end
|
||||
|
||||
-- handle an RPLC packet
|
||||
-- handle RPLC and MGMT packets
|
||||
---@param packet rplc_frame|mgmt_frame packet frame
|
||||
---@param plc_state plc_state PLC state
|
||||
---@param setpoints setpoints setpoint control table
|
||||
function public.handle_packet(packet, plc_state, setpoints)
|
||||
if packet.scada_frame.local_port() == local_port then
|
||||
-- print a log message to the terminal as long as the UI isn't running
|
||||
local function println_ts(message) if not plc_state.fp_ok then util.println_ts(message) end end
|
||||
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
local l_chan = packet.scada_frame.local_channel()
|
||||
local src_addr = packet.scada_frame.src_addr()
|
||||
|
||||
-- handle packets now that we have prints setup
|
||||
if l_chan == plc_channel then
|
||||
-- check sequence number
|
||||
if self.r_seq_num == nil then
|
||||
self.r_seq_num = packet.scada_frame.seq_num()
|
||||
elseif self.linked and self.r_seq_num >= 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())
|
||||
return
|
||||
elseif self.linked and (src_addr ~= self.sv_addr) then
|
||||
log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr ..
|
||||
"); channel in use by another system?")
|
||||
return
|
||||
else
|
||||
self.r_seq_num = packet.scada_frame.seq_num()
|
||||
end
|
||||
@@ -747,11 +801,10 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
-- feed the watchdog first so it doesn't uhh...eat our packets :)
|
||||
conn_watchdog.feed()
|
||||
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
|
||||
-- handle packet
|
||||
if protocol == PROTOCOL.RPLC then
|
||||
---@cast packet rplc_frame
|
||||
-- if linked, only accept packets from configured supervisor
|
||||
if self.linked then
|
||||
if packet.type == RPLC_TYPE.STATUS then
|
||||
-- request of full status, clear cache first
|
||||
@@ -881,13 +934,14 @@ 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")
|
||||
end
|
||||
else
|
||||
log.warning("received unknown RPLC packet type " .. packet.type)
|
||||
log.debug("received unknown RPLC packet type " .. packet.type)
|
||||
end
|
||||
else
|
||||
log.debug("discarding RPLC packet before linked")
|
||||
end
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
-- if linked, only accept packets from configured supervisor
|
||||
if self.linked then
|
||||
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
-- link request confirmation
|
||||
@@ -900,25 +954,32 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
self.status_cache = nil
|
||||
_send_struct()
|
||||
public.send_status(plc_state.no_reactor, plc_state.reactor_formed)
|
||||
log.debug("re-sent initial status data")
|
||||
elseif est_ack == ESTABLISH_ACK.DENY then
|
||||
println_ts("received unsolicited link denial, unlinking")
|
||||
log.info("unsolicited establish request denied")
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
println_ts("received unsolicited link collision, unlinking")
|
||||
log.warning("unsolicited establish request collision")
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
println_ts("received unsolicited link version mismatch, unlinking")
|
||||
log.warning("unsolicited establish request version mismatch")
|
||||
log.debug("re-sent initial status data due to re-establish")
|
||||
else
|
||||
println_ts("invalid unsolicited link response")
|
||||
log.error("unsolicited unknown establish request response")
|
||||
end
|
||||
if est_ack == ESTABLISH_ACK.DENY then
|
||||
println_ts("received unsolicited link denial, unlinking")
|
||||
log.warning("unsolicited establish request denied")
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
println_ts("received unsolicited link collision, unlinking")
|
||||
log.warning("unsolicited establish request collision")
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
println_ts("received unsolicited link version mismatch, unlinking")
|
||||
log.warning("unsolicited establish request version mismatch")
|
||||
else
|
||||
println_ts("invalid unsolicited link response")
|
||||
log.debug("unsolicited unknown establish request response")
|
||||
end
|
||||
|
||||
self.linked = est_ack == ESTABLISH_ACK.ALLOW
|
||||
-- unlink
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.linked = false
|
||||
end
|
||||
|
||||
-- clear this since this is for something that was unsolicited
|
||||
self.last_est_ack = ESTABLISH_ACK.ALLOW
|
||||
|
||||
-- report link state
|
||||
databus.tx_link_state(est_ack + 1)
|
||||
else
|
||||
log.debug("SCADA_MGMT establish packet length mismatch")
|
||||
end
|
||||
@@ -932,7 +993,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
log.warning("PLC KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug("RPLC RTT = " .. trip_time .. "ms")
|
||||
-- log.debug("PLC RTT = " .. trip_time .. "ms")
|
||||
|
||||
_send_keep_alive_ack(timestamp)
|
||||
else
|
||||
@@ -945,7 +1006,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
println_ts("server connection closed by remote host")
|
||||
log.warning("server connection closed by remote host")
|
||||
else
|
||||
log.warning("received unsupported SCADA_MGMT packet type " .. packet.type)
|
||||
log.debug("received unsupported SCADA_MGMT packet type " .. packet.type)
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
-- link request confirmation
|
||||
@@ -954,9 +1015,11 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
|
||||
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||
println_ts("linked!")
|
||||
log.info("supervisor establish request approved, PLC is linked")
|
||||
log.info("supervisor establish request approved, linked to SV (CID#" .. src_addr .. ")")
|
||||
|
||||
-- reset remote sequence number and cache
|
||||
-- link + reset remote sequence number and cache
|
||||
self.sv_addr = src_addr
|
||||
self.linked = true
|
||||
self.r_seq_num = nil
|
||||
self.status_cache = nil
|
||||
|
||||
@@ -964,24 +1027,32 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
public.send_status(plc_state.no_reactor, plc_state.reactor_formed)
|
||||
|
||||
log.debug("sent initial status data")
|
||||
elseif self.last_est_ack ~= est_ack then
|
||||
if est_ack == ESTABLISH_ACK.DENY then
|
||||
println_ts("link request denied, retrying...")
|
||||
log.info("supervisor establish request denied, retrying")
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
println_ts("reactor PLC ID collision (check config), retrying...")
|
||||
log.warning("establish request collision, retrying")
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
println_ts("supervisor version mismatch (try updating), retrying...")
|
||||
log.warning("establish request version mismatch, retrying")
|
||||
else
|
||||
println_ts("invalid link response, bad channel? retrying...")
|
||||
log.error("unknown establish request response, retrying")
|
||||
else
|
||||
if self.last_est_ack ~= est_ack then
|
||||
if est_ack == ESTABLISH_ACK.DENY then
|
||||
println_ts("link request denied, retrying...")
|
||||
log.info("supervisor establish request denied, retrying")
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
println_ts("reactor PLC ID collision (check config), retrying...")
|
||||
log.warning("establish request collision, retrying")
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
println_ts("supervisor version mismatch (try updating), retrying...")
|
||||
log.warning("establish request version mismatch, retrying")
|
||||
else
|
||||
println_ts("invalid link response, bad channel? retrying...")
|
||||
log.error("unknown establish request response, retrying")
|
||||
end
|
||||
end
|
||||
|
||||
-- unlink
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.linked = false
|
||||
end
|
||||
|
||||
self.linked = est_ack == ESTABLISH_ACK.ALLOW
|
||||
self.last_est_ack = est_ack
|
||||
|
||||
-- report link state
|
||||
databus.tx_link_state(est_ack + 1)
|
||||
else
|
||||
log.debug("SCADA_MGMT establish packet length mismatch")
|
||||
end
|
||||
@@ -992,6 +1063,8 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
-- should be unreachable assuming packet is from parse_packet()
|
||||
log.error("illegal packet type " .. protocol, true)
|
||||
end
|
||||
else
|
||||
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
78
reactor-plc/renderer.lua
Normal file
78
reactor-plc/renderer.lua
Normal file
@@ -0,0 +1,78 @@
|
||||
--
|
||||
-- Graphics Rendering Control
|
||||
--
|
||||
|
||||
local panel_view = require("reactor-plc.panel.front_panel")
|
||||
local style = require("reactor-plc.panel.style")
|
||||
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
|
||||
local renderer = {}
|
||||
|
||||
local ui = {
|
||||
display = nil
|
||||
}
|
||||
|
||||
-- start the UI
|
||||
function renderer.start_ui()
|
||||
if ui.display == nil then
|
||||
-- reset terminal
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.colors do
|
||||
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||
end
|
||||
|
||||
-- init front panel view
|
||||
ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||
panel_view(ui.display)
|
||||
|
||||
-- start flasher callback task
|
||||
flasher.run()
|
||||
end
|
||||
end
|
||||
|
||||
-- close out the UI
|
||||
function renderer.close_ui()
|
||||
if ui.display ~= nil then
|
||||
-- stop blinking indicators
|
||||
flasher.clear()
|
||||
|
||||
-- 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
|
||||
@@ -4,21 +4,23 @@
|
||||
|
||||
require("/initenv").init_env()
|
||||
|
||||
local crash = require("scada-common.crash")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
local comms = require("scada-common.comms")
|
||||
local crash = require("scada-common.crash")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local rsio = require("scada-common.rsio")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local config = require("reactor-plc.config")
|
||||
local plc = require("reactor-plc.plc")
|
||||
local threads = require("reactor-plc.threads")
|
||||
local config = require("reactor-plc.config")
|
||||
local databus = require("reactor-plc.databus")
|
||||
local plc = require("reactor-plc.plc")
|
||||
local renderer = require("reactor-plc.renderer")
|
||||
local threads = require("reactor-plc.threads")
|
||||
|
||||
local R_PLC_VERSION = "v1.0.0"
|
||||
local R_PLC_VERSION = "v1.4.6"
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
----------------------------------------
|
||||
@@ -29,8 +31,8 @@ local cfv = util.new_validator()
|
||||
|
||||
cfv.assert_type_bool(config.NETWORKED)
|
||||
cfv.assert_type_int(config.REACTOR_ID)
|
||||
cfv.assert_port(config.SERVER_PORT)
|
||||
cfv.assert_port(config.LISTEN_PORT)
|
||||
cfv.assert_channel(config.SVR_CHANNEL)
|
||||
cfv.assert_channel(config.PLC_CHANNEL)
|
||||
cfv.assert_type_int(config.TRUSTED_RANGE)
|
||||
cfv.assert_type_num(config.COMMS_TIMEOUT)
|
||||
cfv.assert_min(config.COMMS_TIMEOUT, 2)
|
||||
@@ -39,11 +41,20 @@ cfv.assert_type_int(config.LOG_MODE)
|
||||
|
||||
assert(cfv.valid(), "bad config file: missing/invalid fields")
|
||||
|
||||
-- check emergency coolant configuration
|
||||
if type(config.EMERGENCY_COOL) == "table" then
|
||||
if not rsio.is_valid_side(config.EMERGENCY_COOL.side) then
|
||||
assert(false, "bad config file: emergency coolant side unrecognized")
|
||||
elseif config.EMERGENCY_COOL.color ~= nil and not rsio.is_color(config.EMERGENCY_COOL.color) then
|
||||
assert(false, "bad config file: emergency coolant invalid redstone channel color provided")
|
||||
end
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
-- 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("BOOTING reactor-plc.startup " .. R_PLC_VERSION)
|
||||
@@ -61,6 +72,10 @@ local function main()
|
||||
-- startup
|
||||
----------------------------------------
|
||||
|
||||
-- record firmware versions and ID
|
||||
databus.tx_versions(R_PLC_VERSION, comms.version)
|
||||
databus.tx_id(config.REACTOR_ID)
|
||||
|
||||
-- mount connected devices
|
||||
ppm.mount_all()
|
||||
|
||||
@@ -74,6 +89,7 @@ local function main()
|
||||
---@class plc_state
|
||||
plc_state = {
|
||||
init_ok = true,
|
||||
fp_ok = false,
|
||||
shutdown = false,
|
||||
degraded = false,
|
||||
reactor_formed = true,
|
||||
@@ -145,17 +161,34 @@ local function main()
|
||||
plc_state.no_modem = true
|
||||
end
|
||||
|
||||
-- print a log message to the terminal as long as the UI isn't running
|
||||
local function _println_no_fp(message) if not plc_state.fp_ok then println(message) end end
|
||||
|
||||
-- PLC init<br>
|
||||
--- EVENT_CONSUMER: this function consumes events
|
||||
local function init()
|
||||
if plc_state.init_ok then
|
||||
-- just booting up, no fission allowed (neutrons stay put thanks)
|
||||
if plc_state.reactor_formed and smem_dev.reactor.getStatus() then
|
||||
smem_dev.reactor.scram()
|
||||
end
|
||||
-- just booting up, no fission allowed (neutrons stay put thanks)
|
||||
if (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
|
||||
smem_dev.reactor.scram()
|
||||
end
|
||||
|
||||
-- front panel time!
|
||||
if not renderer.ui_ready() then
|
||||
local message
|
||||
plc_state.fp_ok, message = pcall(renderer.start_ui)
|
||||
|
||||
if not plc_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
|
||||
end
|
||||
|
||||
if plc_state.init_ok then
|
||||
-- init reactor protection system
|
||||
smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed)
|
||||
smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed, config.EMERGENCY_COOL)
|
||||
log.debug("init> rps init")
|
||||
|
||||
if __shared_memory.networked then
|
||||
@@ -164,22 +197,30 @@ local function main()
|
||||
log.debug("init> conn watchdog started")
|
||||
|
||||
-- start comms
|
||||
smem_sys.plc_comms = plc.comms(config.REACTOR_ID, R_PLC_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT,
|
||||
smem_sys.plc_comms = plc.comms(config.REACTOR_ID, R_PLC_VERSION, smem_dev.modem, config.PLC_CHANNEL, config.SVR_CHANNEL,
|
||||
config.TRUSTED_RANGE, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
|
||||
log.debug("init> comms init")
|
||||
else
|
||||
println("init> starting in offline mode")
|
||||
_println_no_fp("init> starting in offline mode")
|
||||
log.info("init> running without networking")
|
||||
end
|
||||
|
||||
-- notify user of emergency coolant configuration status
|
||||
if config.EMERGENCY_COOL ~= nil then
|
||||
println("init> emergency coolant control ready")
|
||||
log.info("init> running with emergency coolant control available")
|
||||
end
|
||||
|
||||
util.push_event("clock_start")
|
||||
|
||||
println("init> completed")
|
||||
_println_no_fp("init> completed")
|
||||
log.info("init> startup completed")
|
||||
else
|
||||
println("init> system in degraded state, awaiting devices...")
|
||||
_println_no_fp("init> system in degraded state, awaiting devices...")
|
||||
log.warning("init> started in a degraded state, awaiting peripheral connections...")
|
||||
end
|
||||
|
||||
databus.tx_hw_status(plc_state)
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
@@ -217,8 +258,15 @@ local function main()
|
||||
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec)
|
||||
end
|
||||
|
||||
renderer.close_ui()
|
||||
|
||||
println_ts("exited")
|
||||
log.info("exited")
|
||||
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
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local databus = require("reactor-plc.databus")
|
||||
local renderer = require("reactor-plc.renderer")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local threads = {}
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
|
||||
local RPS_SLEEP = 250 -- (250ms, 5 ticks)
|
||||
local COMMS_SLEEP = 150 -- (150ms, 3 ticks)
|
||||
@@ -32,11 +33,15 @@ local MQ__COMM_CMD = {
|
||||
---@param smem plc_shared_memory
|
||||
---@param init function
|
||||
function threads.thread__main(smem, init)
|
||||
-- print a log message to the terminal as long as the UI isn't running
|
||||
local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
|
||||
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("main", true)
|
||||
log.debug("main thread init, clock inactive")
|
||||
|
||||
-- send status updates at 2Hz (every 10 server ticks) (every loop tick)
|
||||
@@ -61,6 +66,9 @@ function threads.thread__main(smem, init)
|
||||
|
||||
-- handle event
|
||||
if event == "timer" and loop_clock.is_clock(param1) then
|
||||
-- blink heartbeat indicator
|
||||
databus.heartbeat()
|
||||
|
||||
-- core clock tick
|
||||
if networked then
|
||||
-- start next clock timer
|
||||
@@ -133,6 +141,9 @@ function threads.thread__main(smem, init)
|
||||
-- reactor no longer formed
|
||||
plc_state.reactor_formed = false
|
||||
end
|
||||
|
||||
-- update indicators
|
||||
databus.tx_hw_status(plc_state)
|
||||
elseif event == "modem_message" and networked and plc_state.init_ok and not plc_state.no_modem then
|
||||
-- got a packet
|
||||
local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
@@ -144,6 +155,9 @@ function threads.thread__main(smem, init)
|
||||
-- haven't heard from server recently? shutdown reactor
|
||||
plc_comms.unlink()
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT)
|
||||
elseif event == "timer" then
|
||||
-- notify timer callback dispatcher if no other timer case claimed this event
|
||||
tcd.handle(param1)
|
||||
elseif event == "peripheral_detach" then
|
||||
-- peripheral disconnect
|
||||
local type, device = ppm.handle_unmount(param1)
|
||||
@@ -174,6 +188,9 @@ function threads.thread__main(smem, init)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- update indicators
|
||||
databus.tx_hw_status(plc_state)
|
||||
elseif event == "peripheral" then
|
||||
-- peripheral connect
|
||||
local type, device = ppm.mount(param1)
|
||||
@@ -237,6 +254,12 @@ function threads.thread__main(smem, init)
|
||||
plc_state.init_ok = true
|
||||
init()
|
||||
end
|
||||
|
||||
-- update indicators
|
||||
databus.tx_hw_status(plc_state)
|
||||
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))
|
||||
elseif event == "clock_start" then
|
||||
-- start loop clock
|
||||
loop_clock.start()
|
||||
@@ -263,6 +286,8 @@ function threads.thread__main(smem, init)
|
||||
log.fatal(util.strval(result))
|
||||
end
|
||||
|
||||
databus.tx_rt_status("main", false)
|
||||
|
||||
-- if status is true, then we are probably exiting, so this won't matter
|
||||
-- if not, we need to restart the clock
|
||||
-- this thread cannot be slept because it will miss events (namely "terminate" otherwise)
|
||||
@@ -280,11 +305,15 @@ end
|
||||
---@nodiscard
|
||||
---@param smem plc_shared_memory
|
||||
function threads.thread__rps(smem)
|
||||
-- print a log message to the terminal as long as the UI isn't running
|
||||
local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
|
||||
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("rps", true)
|
||||
log.debug("rps thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
@@ -314,15 +343,20 @@ function threads.thread__rps(smem)
|
||||
rps.trip_timeout()
|
||||
end
|
||||
else
|
||||
-- would do elseif not networked but there is no reason to do that extra operation
|
||||
was_linked = true
|
||||
end
|
||||
|
||||
-- if we tried to SCRAM but failed, keep trying
|
||||
-- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check)
|
||||
if (not plc_state.no_reactor) and rps.is_formed() then
|
||||
-- check reactor status
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
if (not plc_state.no_reactor) and rps.is_formed() and rps.is_tripped() and reactor.getStatus() then
|
||||
rps.scram()
|
||||
local reactor_status = reactor.getStatus()
|
||||
databus.tx_reactor_state(reactor_status)
|
||||
|
||||
-- if we tried to SCRAM but failed, keep trying
|
||||
-- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check)
|
||||
if rps.is_tripped() and reactor_status then
|
||||
rps.scram()
|
||||
end
|
||||
end
|
||||
|
||||
-- if we are in standalone mode, continuously reset RPS
|
||||
@@ -406,6 +440,8 @@ function threads.thread__rps(smem)
|
||||
log.fatal(util.strval(result))
|
||||
end
|
||||
|
||||
databus.tx_rt_status("rps", false)
|
||||
|
||||
if not plc_state.shutdown then
|
||||
if plc_state.init_ok then smem.plc_sys.rps.scram() end
|
||||
log.info("rps thread restarting in 5 seconds...")
|
||||
@@ -426,6 +462,7 @@ function threads.thread__comms_tx(smem)
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("comms_tx", true)
|
||||
log.debug("comms tx thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
@@ -483,6 +520,8 @@ function threads.thread__comms_tx(smem)
|
||||
log.fatal(util.strval(result))
|
||||
end
|
||||
|
||||
databus.tx_rt_status("comms_tx", false)
|
||||
|
||||
if not plc_state.shutdown then
|
||||
log.info("comms tx thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
@@ -502,6 +541,7 @@ function threads.thread__comms_rx(smem)
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("comms_rx", true)
|
||||
log.debug("comms rx thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
@@ -559,6 +599,8 @@ function threads.thread__comms_rx(smem)
|
||||
log.fatal(util.strval(result))
|
||||
end
|
||||
|
||||
databus.tx_rt_status("comms_rx", false)
|
||||
|
||||
if not plc_state.shutdown then
|
||||
log.info("comms rx thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
@@ -578,6 +620,7 @@ function threads.thread__setpoint_control(smem)
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("spctl", true)
|
||||
log.debug("setpoint control thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
@@ -637,7 +680,7 @@ function threads.thread__setpoint_control(smem)
|
||||
-- we yielded, check enable again
|
||||
if setpoints.burn_rate_en and (type(current_burn_rate) == "number") and (current_burn_rate ~= setpoints.burn_rate) then
|
||||
-- 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
|
||||
-- need to ramp up
|
||||
@@ -692,6 +735,8 @@ function threads.thread__setpoint_control(smem)
|
||||
log.fatal(util.strval(result))
|
||||
end
|
||||
|
||||
databus.tx_rt_status("spctl", false)
|
||||
|
||||
if not plc_state.shutdown then
|
||||
log.info("setpoint control thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
|
||||
@@ -2,11 +2,11 @@ local rsio = require("scada-common.rsio")
|
||||
|
||||
local config = {}
|
||||
|
||||
-- port to send packets TO server
|
||||
config.SERVER_PORT = 16000
|
||||
-- port to listen to incoming packets FROM server
|
||||
config.LISTEN_PORT = 15001
|
||||
-- max trusted modem message distance (< 1 to disable check)
|
||||
-- supervisor comms channel
|
||||
config.SVR_CHANNEL = 16240
|
||||
-- RTU/MODBUS comms channel
|
||||
config.RTU_CHANNEL = 16242
|
||||
-- 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
|
||||
@@ -17,6 +17,8 @@ config.LOG_PATH = "/log.txt"
|
||||
-- 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
|
||||
|
||||
-- RTU peripheral devices (named: side/network device name)
|
||||
config.RTU_DEVICES = {
|
||||
|
||||
74
rtu/databus.lua
Normal file
74
rtu/databus.lua
Normal file
@@ -0,0 +1,74 @@
|
||||
--
|
||||
-- Data Bus - Central Communication Linking for RTU Front Panel
|
||||
--
|
||||
|
||||
local psil = require("scada-common.psil")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local databus = {}
|
||||
|
||||
-- databus PSIL
|
||||
databus.ps = psil.create()
|
||||
|
||||
---@enum RTU_UNIT_HW_STATE
|
||||
local RTU_UNIT_HW_STATE = {
|
||||
OFFLINE = 1,
|
||||
FAULTED = 2,
|
||||
UNFORMED = 3,
|
||||
OK = 4
|
||||
}
|
||||
|
||||
databus.RTU_UNIT_HW_STATE = RTU_UNIT_HW_STATE
|
||||
|
||||
-- call to toggle heartbeat signal
|
||||
function databus.heartbeat() databus.ps.toggle("heartbeat") end
|
||||
|
||||
-- transmit firmware versions across the bus
|
||||
---@param rtu_v string RTU version
|
||||
---@param comms_v string comms version
|
||||
function databus.tx_versions(rtu_v, comms_v)
|
||||
databus.ps.publish("version", rtu_v)
|
||||
databus.ps.publish("comms_version", comms_v)
|
||||
end
|
||||
|
||||
-- transmit hardware status for modem connection state
|
||||
---@param has_modem boolean
|
||||
function databus.tx_hw_modem(has_modem)
|
||||
databus.ps.publish("has_modem", has_modem)
|
||||
end
|
||||
|
||||
-- transmit unit hardware type across the bus
|
||||
---@param uid integer unit ID
|
||||
---@param type RTU_UNIT_TYPE
|
||||
function databus.tx_unit_hw_type(uid, type)
|
||||
databus.ps.publish("unit_type_" .. uid, type)
|
||||
end
|
||||
|
||||
-- transmit unit hardware status across the bus
|
||||
---@param uid integer unit ID
|
||||
---@param status RTU_UNIT_HW_STATE
|
||||
function databus.tx_unit_hw_status(uid, status)
|
||||
databus.ps.publish("unit_hw_" .. uid, status)
|
||||
end
|
||||
|
||||
-- transmit thread (routine) statuses
|
||||
---@param thread string thread name
|
||||
---@param ok boolean thread state
|
||||
function databus.tx_rt_status(thread, ok)
|
||||
databus.ps.publish(util.c("routine__", thread), ok)
|
||||
end
|
||||
|
||||
-- transmit supervisor link state across the bus
|
||||
---@param state integer
|
||||
function databus.tx_link_state(state)
|
||||
databus.ps.publish("link_state", state)
|
||||
end
|
||||
|
||||
-- link a function to receive data from the bus
|
||||
---@param field string field name
|
||||
---@param func function function to link
|
||||
function databus.rx_field(field, func)
|
||||
databus.ps.subscribe(field, func)
|
||||
end
|
||||
|
||||
return databus
|
||||
@@ -5,9 +5,13 @@ local boilerv_rtu = {}
|
||||
-- create new boiler (mek 10.1+) device
|
||||
---@nodiscard
|
||||
---@param boiler table
|
||||
---@return rtu_device interface, boolean faulted
|
||||
function boilerv_rtu.new(boiler)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- disable auto fault clearing
|
||||
boiler.__p_disable_afc()
|
||||
|
||||
-- discrete inputs --
|
||||
unit.connect_di(boiler.isFormed)
|
||||
|
||||
@@ -50,7 +54,12 @@ function boilerv_rtu.new(boiler)
|
||||
-- holding registers --
|
||||
-- none
|
||||
|
||||
return unit.interface()
|
||||
-- check if any calls faulted
|
||||
local faulted = boiler.__p_is_faulted()
|
||||
boiler.__p_clear_fault()
|
||||
boiler.__p_enable_afc()
|
||||
|
||||
return unit.interface(), faulted
|
||||
end
|
||||
|
||||
return boilerv_rtu
|
||||
|
||||
@@ -5,9 +5,13 @@ local envd_rtu = {}
|
||||
-- create new environment detector device
|
||||
---@nodiscard
|
||||
---@param envd table
|
||||
---@return rtu_device interface, boolean faulted
|
||||
function envd_rtu.new(envd)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- disable auto fault clearing
|
||||
envd.__p_disable_afc()
|
||||
|
||||
-- discrete inputs --
|
||||
-- none
|
||||
|
||||
@@ -21,7 +25,12 @@ function envd_rtu.new(envd)
|
||||
-- holding registers --
|
||||
-- none
|
||||
|
||||
return unit.interface()
|
||||
-- check if any calls faulted
|
||||
local faulted = envd.__p_is_faulted()
|
||||
envd.__p_clear_fault()
|
||||
envd.__p_enable_afc()
|
||||
|
||||
return unit.interface(), faulted
|
||||
end
|
||||
|
||||
return envd_rtu
|
||||
|
||||
@@ -5,9 +5,13 @@ local imatrix_rtu = {}
|
||||
-- create new induction matrix (mek 10.1+) device
|
||||
---@nodiscard
|
||||
---@param imatrix table
|
||||
---@return rtu_device interface, boolean faulted
|
||||
function imatrix_rtu.new(imatrix)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- disable auto fault clearing
|
||||
imatrix.__p_disable_afc()
|
||||
|
||||
-- discrete inputs --
|
||||
unit.connect_di(imatrix.isFormed)
|
||||
|
||||
@@ -37,7 +41,12 @@ function imatrix_rtu.new(imatrix)
|
||||
-- holding registers --
|
||||
-- none
|
||||
|
||||
return unit.interface()
|
||||
-- check if any calls faulted
|
||||
local faulted = imatrix.__p_is_faulted()
|
||||
imatrix.__p_clear_fault()
|
||||
imatrix.__p_enable_afc()
|
||||
|
||||
return unit.interface(), faulted
|
||||
end
|
||||
|
||||
return imatrix_rtu
|
||||
|
||||
@@ -11,6 +11,7 @@ local digital_write = rsio.digital_write
|
||||
|
||||
-- create new redstone device
|
||||
---@nodiscard
|
||||
---@return rtu_rs_device interface, boolean faulted
|
||||
function redstone_rtu.new()
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
@@ -33,7 +34,7 @@ function redstone_rtu.new()
|
||||
---@param side string
|
||||
---@param color integer
|
||||
function public.link_di(side, color)
|
||||
local f_read = nil
|
||||
local f_read ---@type function
|
||||
|
||||
if color then
|
||||
f_read = function ()
|
||||
@@ -52,8 +53,8 @@ function redstone_rtu.new()
|
||||
---@param side string
|
||||
---@param color integer
|
||||
function public.link_do(side, color)
|
||||
local f_read = nil
|
||||
local f_write = nil
|
||||
local f_read ---@type function
|
||||
local f_write ---@type function
|
||||
|
||||
if color then
|
||||
f_read = function ()
|
||||
@@ -111,7 +112,7 @@ function redstone_rtu.new()
|
||||
)
|
||||
end
|
||||
|
||||
return public
|
||||
return public, false
|
||||
end
|
||||
|
||||
return redstone_rtu
|
||||
|
||||
@@ -5,9 +5,13 @@ local sna_rtu = {}
|
||||
-- create new solar neutron activator (SNA) device
|
||||
---@nodiscard
|
||||
---@param sna table
|
||||
---@return rtu_device interface, boolean faulted
|
||||
function sna_rtu.new(sna)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- disable auto fault clearing
|
||||
sna.__p_disable_afc()
|
||||
|
||||
-- discrete inputs --
|
||||
-- none
|
||||
|
||||
@@ -32,7 +36,12 @@ function sna_rtu.new(sna)
|
||||
-- holding registers --
|
||||
-- none
|
||||
|
||||
return unit.interface()
|
||||
-- check if any calls faulted
|
||||
local faulted = sna.__p_is_faulted()
|
||||
sna.__p_clear_fault()
|
||||
sna.__p_enable_afc()
|
||||
|
||||
return unit.interface(), faulted
|
||||
end
|
||||
|
||||
return sna_rtu
|
||||
|
||||
@@ -5,9 +5,13 @@ local sps_rtu = {}
|
||||
-- create new super-critical phase shifter (SPS) device
|
||||
---@nodiscard
|
||||
---@param sps table
|
||||
---@return rtu_device interface, boolean faulted
|
||||
function sps_rtu.new(sps)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- disable auto fault clearing
|
||||
sps.__p_disable_afc()
|
||||
|
||||
-- discrete inputs --
|
||||
unit.connect_di(sps.isFormed)
|
||||
|
||||
@@ -42,7 +46,12 @@ function sps_rtu.new(sps)
|
||||
-- holding registers --
|
||||
-- none
|
||||
|
||||
return unit.interface()
|
||||
-- check if any calls faulted
|
||||
local faulted = sps.__p_is_faulted()
|
||||
sps.__p_clear_fault()
|
||||
sps.__p_enable_afc()
|
||||
|
||||
return unit.interface(), faulted
|
||||
end
|
||||
|
||||
return sps_rtu
|
||||
|
||||
@@ -5,9 +5,13 @@ local turbinev_rtu = {}
|
||||
-- create new turbine (mek 10.1+) device
|
||||
---@nodiscard
|
||||
---@param turbine table
|
||||
---@return rtu_device interface, boolean faulted
|
||||
function turbinev_rtu.new(turbine)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- disable auto fault clearing
|
||||
turbine.__p_disable_afc()
|
||||
|
||||
-- discrete inputs --
|
||||
unit.connect_di(turbine.isFormed)
|
||||
|
||||
@@ -49,7 +53,12 @@ function turbinev_rtu.new(turbine)
|
||||
-- holding registers --
|
||||
unit.connect_holding_reg(turbine.getDumpingMode, turbine.setDumpingMode)
|
||||
|
||||
return unit.interface()
|
||||
-- check if any calls faulted
|
||||
local faulted = turbine.__p_is_faulted()
|
||||
turbine.__p_clear_fault()
|
||||
turbine.__p_enable_afc()
|
||||
|
||||
return unit.interface(), faulted
|
||||
end
|
||||
|
||||
return turbinev_rtu
|
||||
|
||||
@@ -347,11 +347,9 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
response = { MODBUS_EXCODE.NEG_ACKNOWLEDGE }
|
||||
end
|
||||
|
||||
-- default is to echo back
|
||||
local func_code = packet.func_code
|
||||
|
||||
-- echo back with error flag, on success the "error" will be acknowledgement
|
||||
func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
|
||||
-- default is to echo back<br>
|
||||
-- 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)
|
||||
|
||||
-- create reply
|
||||
local reply = comms.modbus_packet()
|
||||
@@ -365,8 +363,8 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
---@param packet modbus_frame
|
||||
---@return boolean return_code, modbus_packet reply
|
||||
function public.handle_packet(packet)
|
||||
local return_code = true
|
||||
local response = nil
|
||||
local return_code ---@type boolean
|
||||
local response ---@type table|MODBUS_EXCODE
|
||||
|
||||
if packet.length >= 2 then
|
||||
-- handle by function code
|
||||
|
||||
126
rtu/panel/front_panel.lua
Normal file
126
rtu/panel/front_panel.lua
Normal file
@@ -0,0 +1,126 @@
|
||||
--
|
||||
-- Main SCADA Coordinator GUI
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
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="STATUS",colors=cpair(colors.green,colors.red)}
|
||||
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)}
|
||||
on.update(true)
|
||||
system.line_break()
|
||||
|
||||
heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
|
||||
|
||||
local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)}
|
||||
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
|
||||
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
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)
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local comp_id = util.sprintf("(%d)", os.getComputerID())
|
||||
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)}
|
||||
|
||||
--
|
||||
-- about label
|
||||
--
|
||||
|
||||
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=cpair(colors.lightGray,colors.ivory)}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
|
||||
|
||||
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
|
||||
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
|
||||
|
||||
--
|
||||
-- unit status list
|
||||
--
|
||||
|
||||
local threads = Div{parent=panel,width=8,height=18,x=17,y=3}
|
||||
|
||||
-- display up to 16 units
|
||||
local list_length = math.min(#units, 16)
|
||||
|
||||
-- show routine statuses
|
||||
for i = 1, list_length do
|
||||
TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i),height=1}
|
||||
local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=cpair(colors.green,colors.green_off)}
|
||||
rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update)
|
||||
end
|
||||
|
||||
local unit_hw_statuses = Div{parent=panel,height=18,x=25,y=3}
|
||||
|
||||
-- show hardware statuses
|
||||
for i = 1, list_length do
|
||||
local unit = units[i] ---@type rtu_unit_registry_entry
|
||||
|
||||
-- hardware status
|
||||
local unit_hw = RGBLED{parent=unit_hw_statuses,y=i,label="",colors={colors.red,colors.orange,colors.yellow,colors.green}}
|
||||
|
||||
unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update)
|
||||
|
||||
-- unit name identifier (type + index)
|
||||
local name = util.c(UNIT_TYPE_LABELS[unit.type + 1], " ", unit.index)
|
||||
local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=name,height=1}
|
||||
|
||||
name_box.register(databus.ps, "unit_type_" .. i, function (t)
|
||||
name_box.set_value(util.c(UNIT_TYPE_LABELS[t + 1], " ", unit.index))
|
||||
end)
|
||||
|
||||
-- assignment (unit # or facility)
|
||||
local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor)
|
||||
TextBox{parent=unit_hw_statuses,y=i,x=19,text=for_unit,height=1,fg_bg=cpair(colors.lightGray,colors.ivory)}
|
||||
end
|
||||
end
|
||||
|
||||
return init
|
||||
41
rtu/panel/style.lua
Normal file
41
rtu/panel/style.lua
Normal file
@@ -0,0 +1,41 @@
|
||||
--
|
||||
-- Graphics Style Options
|
||||
--
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local style = {}
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
-- GLOBAL --
|
||||
|
||||
-- remap global colors
|
||||
colors.ivory = colors.pink
|
||||
colors.red_off = colors.brown
|
||||
colors.yellow_off = colors.magenta
|
||||
colors.green_off = colors.lime
|
||||
|
||||
style.root = cpair(colors.black, colors.ivory)
|
||||
style.header = cpair(colors.black, colors.lightGray)
|
||||
|
||||
style.colors = {
|
||||
{ c = colors.red, hex = 0xdf4949 }, -- RED ON
|
||||
{ c = colors.orange, hex = 0xffb659 },
|
||||
{ c = colors.yellow, hex = 0xf9fb53 }, -- YELLOW ON
|
||||
{ c = colors.lime, hex = 0x16665a }, -- GREEN OFF
|
||||
{ c = colors.green, hex = 0x6be551 }, -- GREEN ON
|
||||
{ c = colors.cyan, hex = 0x34bac8 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.purple, hex = 0xb156ee },
|
||||
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
|
||||
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
|
||||
-- { c = colors.white, hex = 0xdcd9ca },
|
||||
{ c = colors.lightGray, hex = 0xb1b8b3 },
|
||||
{ c = colors.gray, hex = 0x575757 },
|
||||
-- { c = colors.black, hex = 0x191919 },
|
||||
{ c = colors.brown, hex = 0x672223 } -- RED OFF
|
||||
}
|
||||
|
||||
return style
|
||||
79
rtu/renderer.lua
Normal file
79
rtu/renderer.lua
Normal file
@@ -0,0 +1,79 @@
|
||||
--
|
||||
-- Graphics Rendering Control
|
||||
--
|
||||
|
||||
local panel_view = require("rtu.panel.front_panel")
|
||||
local style = require("rtu.panel.style")
|
||||
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
|
||||
local renderer = {}
|
||||
|
||||
local ui = {
|
||||
display = nil
|
||||
}
|
||||
|
||||
-- start the UI
|
||||
---@param units table RTU units
|
||||
function renderer.start_ui(units)
|
||||
if ui.display == nil then
|
||||
-- reset terminal
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.colors do
|
||||
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||
end
|
||||
|
||||
-- start flasher callback task
|
||||
flasher.run()
|
||||
|
||||
-- init front panel view
|
||||
ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||
panel_view(ui.display, units)
|
||||
end
|
||||
end
|
||||
|
||||
-- close out the UI
|
||||
function renderer.close_ui()
|
||||
if ui.display ~= nil then
|
||||
-- stop blinking indicators
|
||||
flasher.clear()
|
||||
|
||||
-- delete element tree
|
||||
ui.display.delete()
|
||||
ui.display = nil
|
||||
|
||||
-- restore colors
|
||||
for i = 1, #style.colors do
|
||||
local r, g, b = term.nativePaletteColor(style.colors[i].c)
|
||||
term.setPaletteColor(style.colors[i].c, r, g, b)
|
||||
end
|
||||
|
||||
-- reset terminal
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- is the UI ready?
|
||||
---@nodiscard
|
||||
---@return boolean ready
|
||||
function renderer.ui_ready() return ui.display ~= nil end
|
||||
|
||||
-- handle a mouse event
|
||||
---@param event mouse_interaction|nil
|
||||
function renderer.handle_mouse(event)
|
||||
if ui.display ~= nil and event ~= nil then
|
||||
ui.display.handle_mouse(event)
|
||||
end
|
||||
end
|
||||
|
||||
return renderer
|
||||
71
rtu/rtu.lua
71
rtu/rtu.lua
@@ -1,10 +1,11 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
local comms = require("scada-common.comms")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local modbus = require("rtu.modbus")
|
||||
local databus = require("rtu.databus")
|
||||
local modbus = require("rtu.modbus")
|
||||
|
||||
local rtu = {}
|
||||
|
||||
@@ -14,11 +15,6 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_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
|
||||
---@nodiscard
|
||||
function rtu.init_unit()
|
||||
@@ -163,12 +159,13 @@ end
|
||||
---@nodiscard
|
||||
---@param version string RTU version
|
||||
---@param modem table modem device
|
||||
---@param local_port integer local listening port
|
||||
---@param server_port integer remote server port
|
||||
---@param rtu_channel integer PLC comms channel
|
||||
---@param svr_channel integer supervisor server channel
|
||||
---@param range integer trusted device connection range
|
||||
---@param conn_watchdog watchdog watchdog reference
|
||||
function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog)
|
||||
function rtu.comms(version, modem, rtu_channel, svr_channel, range, conn_watchdog)
|
||||
local self = {
|
||||
sv_addr = comms.BROADCAST,
|
||||
seq_num = 0,
|
||||
r_seq_num = nil,
|
||||
txn_id = 0,
|
||||
@@ -184,7 +181,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
-- configure modem channels
|
||||
local function _conf_channels()
|
||||
modem.closeAll()
|
||||
modem.open(local_port)
|
||||
modem.open(rtu_channel)
|
||||
end
|
||||
|
||||
_conf_channels()
|
||||
@@ -197,9 +194,9 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
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())
|
||||
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
modem.transmit(server_port, local_port, s_pkt.raw_sendable())
|
||||
modem.transmit(svr_channel, rtu_channel, s_pkt.raw_sendable())
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
@@ -242,8 +239,8 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
---@param m_pkt modbus_packet
|
||||
function public.send_modbus(m_pkt)
|
||||
local s_pkt = comms.scada_packet()
|
||||
s_pkt.make(self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
|
||||
modem.transmit(server_port, local_port, s_pkt.raw_sendable())
|
||||
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
|
||||
modem.transmit(svr_channel, rtu_channel, s_pkt.raw_sendable())
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
@@ -258,7 +255,9 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
---@param rtu_state rtu_state
|
||||
function public.unlink(rtu_state)
|
||||
rtu_state.linked = false
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.r_seq_num = nil
|
||||
databus.tx_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
end
|
||||
|
||||
-- close the connection to the server
|
||||
@@ -316,7 +315,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
pkt = mgmt_pkt.get()
|
||||
end
|
||||
else
|
||||
log.error("illegal packet type " .. s_pkt.protocol(), true)
|
||||
log.debug("illegal packet type " .. s_pkt.protocol(), true)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -328,13 +327,24 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
---@param units table RTU units
|
||||
---@param rtu_state rtu_state
|
||||
function public.handle_packet(packet, units, rtu_state)
|
||||
if packet.scada_frame.local_port() == local_port then
|
||||
-- print a log message to the terminal as long as the UI isn't running
|
||||
local function println_ts(message) if not rtu_state.fp_ok then util.println_ts(message) end end
|
||||
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
local l_chan = packet.scada_frame.local_channel()
|
||||
local src_addr = packet.scada_frame.src_addr()
|
||||
|
||||
if l_chan == rtu_channel then
|
||||
-- check sequence number
|
||||
if self.r_seq_num == nil then
|
||||
self.r_seq_num = packet.scada_frame.seq_num()
|
||||
elseif rtu_state.linked and self.r_seq_num >= 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())
|
||||
return
|
||||
elseif rtu_state.linked and (src_addr ~= self.sv_addr) then
|
||||
log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr ..
|
||||
"); channel in use by another system?")
|
||||
return
|
||||
else
|
||||
self.r_seq_num = packet.scada_frame.seq_num()
|
||||
end
|
||||
@@ -342,13 +352,12 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
-- feed watchdog on valid sequence number
|
||||
conn_watchdog.feed()
|
||||
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
|
||||
-- handle packet
|
||||
if protocol == PROTOCOL.MODBUS_TCP then
|
||||
---@cast packet modbus_frame
|
||||
if rtu_state.linked then
|
||||
local return_code = false
|
||||
local reply = modbus.reply__neg_ack(packet)
|
||||
local return_code ---@type boolean
|
||||
local reply ---@type modbus_packet
|
||||
|
||||
-- handle MODBUS instruction
|
||||
if packet.unit_id <= #units then
|
||||
@@ -382,7 +391,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
else
|
||||
-- unit ID out of range?
|
||||
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
|
||||
|
||||
public.send_modbus(reply)
|
||||
@@ -399,6 +408,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||
-- establish allowed
|
||||
rtu_state.linked = true
|
||||
self.sv_addr = packet.scada_frame.src_addr()
|
||||
self.r_seq_num = nil
|
||||
println_ts("supervisor connection established")
|
||||
log.info("supervisor connection established")
|
||||
@@ -419,6 +429,9 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
end
|
||||
|
||||
self.last_est_ack = est_ack
|
||||
|
||||
-- report link state
|
||||
databus.tx_link_state(est_ack + 1)
|
||||
else
|
||||
log.debug("SCADA_MGMT establish packet length mismatch")
|
||||
end
|
||||
@@ -450,7 +463,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
public.send_advertisement(units)
|
||||
else
|
||||
-- not supported
|
||||
log.warning("received unsupported SCADA_MGMT message type " .. packet.type)
|
||||
log.debug("received unsupported SCADA_MGMT message type " .. packet.type)
|
||||
end
|
||||
else
|
||||
log.debug("discarding non-link SCADA_MGMT packet before linked")
|
||||
@@ -459,6 +472,8 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
-- should be unreachable assuming packet is from parse_packet()
|
||||
log.error("illegal packet type " .. protocol, true)
|
||||
end
|
||||
else
|
||||
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user