Compare commits
308 Commits
v1.8.1-bet
...
v1.8.15-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afc38e7e7a | ||
|
|
41b7a68f3e | ||
|
|
8968ebede3 | ||
|
|
50a9168b06 | ||
|
|
76e85da9d5 | ||
|
|
be560cd532 | ||
|
|
2b0a536292 | ||
|
|
afed6f514d | ||
|
|
3e6a0a8869 | ||
|
|
b99cf19be0 | ||
|
|
7f009f9c86 | ||
|
|
6a8ed311f3 | ||
|
|
c181142f75 | ||
|
|
0cb964a177 | ||
|
|
3cd832ca20 | ||
|
|
0c6c7bf9a5 | ||
|
|
3bfcb1d83c | ||
|
|
4fe6792804 | ||
|
|
25dc47d520 | ||
|
|
f958b0e3b7 | ||
|
|
f621ff2482 | ||
|
|
eb45ff899b | ||
|
|
e91fd2fcaa | ||
|
|
d35b824458 | ||
|
|
165d1497f8 | ||
|
|
50bf057ca6 | ||
|
|
6f768ef6b3 | ||
|
|
826086951e | ||
|
|
35bf56663f | ||
|
|
7b8cea4a5c | ||
|
|
51d4a22532 | ||
|
|
fb85c2f05b | ||
|
|
712d018806 | ||
|
|
00a8d64a88 | ||
|
|
d9efd5b8d2 | ||
|
|
a786404092 | ||
|
|
1bd03c0b1a | ||
|
|
0d8d7aeb15 | ||
|
|
0b60dc9fa4 | ||
|
|
48b91ac808 | ||
|
|
4c9efc78b1 | ||
|
|
3c5857cd68 | ||
|
|
473b0dd10d | ||
|
|
d2df7db18b | ||
|
|
0ca002316f | ||
|
|
9fe34648c2 | ||
|
|
57dc20692b | ||
|
|
f22d699baf | ||
|
|
23b31e0049 | ||
|
|
2b4309afa7 | ||
|
|
89cd5a07f2 | ||
|
|
99213da760 | ||
|
|
f23f69d441 | ||
|
|
a99b7912f8 | ||
|
|
23ca5fb69e | ||
|
|
c243d064ef | ||
|
|
878c3b92e1 | ||
|
|
2aa52d2e2c | ||
|
|
dfc1ee6497 | ||
|
|
a6a1a61954 | ||
|
|
d0b50c834c | ||
|
|
612a06ba98 | ||
|
|
65d43d55c7 | ||
|
|
4fcd375ee2 | ||
|
|
a22f5562cf | ||
|
|
0365ea5e8a | ||
|
|
da300607f1 | ||
|
|
cc3174ee76 | ||
|
|
98c37caecd | ||
|
|
cc50e4a020 | ||
|
|
f734c4307b | ||
|
|
45573be8c7 | ||
|
|
eab1ffbe03 | ||
|
|
1504247b33 | ||
|
|
92a4277f73 | ||
|
|
31a663e86b | ||
|
|
5c0e2c32ee | ||
|
|
3537c59365 | ||
|
|
1d7104ae74 | ||
|
|
4629e1ba2a | ||
|
|
659644865a | ||
|
|
03d2b3f087 | ||
|
|
bea7b91ff1 | ||
|
|
b94c89f4ec | ||
|
|
55e4e5a68b | ||
|
|
e1ad76a00d | ||
|
|
93e4590947 | ||
|
|
2442e7f972 | ||
|
|
bb2c07963b | ||
|
|
44c6352a8c | ||
|
|
968b0a9122 | ||
|
|
19869416af | ||
|
|
598f6f08af | ||
|
|
ca2d8ab7da | ||
|
|
1c0f61b3e0 | ||
|
|
ecd3575643 | ||
|
|
9dc3a09f4d | ||
|
|
7a5d14d67f | ||
|
|
d6175e5cec | ||
|
|
a00a824a7f | ||
|
|
9393632428 | ||
|
|
886bd0d5d5 | ||
|
|
9c1d83fdfc | ||
|
|
bd88244681 | ||
|
|
8dae632b25 | ||
|
|
89d56d3101 | ||
|
|
1f451ff92a | ||
|
|
9d08b51f84 | ||
|
|
047ff5c203 | ||
|
|
895f768e58 | ||
|
|
b3f29566ea | ||
|
|
c0a5c8d504 | ||
|
|
bbe7b52662 | ||
|
|
d5b166dcc6 | ||
|
|
b0aa6d54ac | ||
|
|
ad240ae44c | ||
|
|
79c93f1562 | ||
|
|
5d760a0524 | ||
|
|
6c89b3134c | ||
|
|
3c7fff28c9 | ||
|
|
814043bf04 | ||
|
|
fc7896ebd3 | ||
|
|
48a8eadc55 | ||
|
|
f0f2aadf53 | ||
|
|
ce37a672a3 | ||
|
|
510995b04f | ||
|
|
560061d4ad | ||
|
|
d87e3893f0 | ||
|
|
fc198cd9d2 | ||
|
|
c714e49ad8 | ||
|
|
d1e4ea586e | ||
|
|
4e789ab92d | ||
|
|
0892a57d35 | ||
|
|
1bc4828010 | ||
|
|
fb5a9d5d9e | ||
|
|
adbf1f2f78 | ||
|
|
2f99aaeedb | ||
|
|
a318ffb283 | ||
|
|
a677e994d6 | ||
|
|
f9917b786c | ||
|
|
dbc1f41c5d | ||
|
|
d6185e0183 | ||
|
|
6ef049baa1 | ||
|
|
a4214e8a4f | ||
|
|
45881067df | ||
|
|
c40aa229bf | ||
|
|
51f2bba4d1 | ||
|
|
628a50e1bd | ||
|
|
cdd31508d9 | ||
|
|
83d62991f8 | ||
|
|
f207a950e4 | ||
|
|
0b0051dc2f | ||
|
|
f152c37ea9 | ||
|
|
372fd426d8 | ||
|
|
8347afb6d0 | ||
|
|
10d0a9763a | ||
|
|
96691d773a | ||
|
|
910509d764 | ||
|
|
158cc39b80 | ||
|
|
940f71aa35 | ||
|
|
788cef8f86 | ||
|
|
baaef862ab | ||
|
|
96db709ced | ||
|
|
c47fa5433f | ||
|
|
440b724798 | ||
|
|
126d6eb163 | ||
|
|
8ac46faf36 | ||
|
|
f112746e12 | ||
|
|
76f21e925b | ||
|
|
a330249c7e | ||
|
|
bbc64c8dc2 | ||
|
|
bb062cf397 | ||
|
|
53bb36ce8d | ||
|
|
8237113577 | ||
|
|
02906ae707 | ||
|
|
6d0e777e68 | ||
|
|
36468c4043 | ||
|
|
1cf7375311 | ||
|
|
ca55948286 | ||
|
|
195f59178f | ||
|
|
e416faf313 | ||
|
|
20bff48cfd | ||
|
|
1a9892b291 | ||
|
|
827953c0a1 | ||
|
|
56e69e3a29 | ||
|
|
1984b63837 | ||
|
|
36b12d5dea | ||
|
|
3e83c8e2c6 | ||
|
|
1fa8c03dff | ||
|
|
d6de9c266b | ||
|
|
2142c1b4f7 | ||
|
|
cafba6c67a | ||
|
|
6eccebbe39 | ||
|
|
5e9f03c900 | ||
|
|
7374bb02d1 | ||
|
|
d19794ae4f | ||
|
|
108cf75cad | ||
|
|
34cbb6be39 | ||
|
|
907f27baf8 | ||
|
|
4710fa7cee | ||
|
|
526a54903e | ||
|
|
e1a632dcc7 | ||
|
|
d5d818e625 | ||
|
|
bfa1f4d0c6 | ||
|
|
737e0d72b0 | ||
|
|
6edeb3e3b8 | ||
|
|
fb00e98a5b | ||
|
|
4f952eff83 | ||
|
|
1eede97c08 | ||
|
|
95419562ee | ||
|
|
03de90c3d8 | ||
|
|
99096e0fc9 | ||
|
|
7b85d947c4 | ||
|
|
08e670091a | ||
|
|
1348b632a8 | ||
|
|
8cd5162362 | ||
|
|
1c410a89d8 | ||
|
|
6a931fced4 | ||
|
|
622e2eeb90 | ||
|
|
42cd9fff0c | ||
|
|
2a85a438ba | ||
|
|
338b3b1615 | ||
|
|
363f164f47 | ||
|
|
739f04ece9 | ||
|
|
c6ade68ce2 | ||
|
|
7d60e259e2 | ||
|
|
cd71c6a9c1 | ||
|
|
26fe130609 | ||
|
|
95f87b1b05 | ||
|
|
aebdf3e8df | ||
|
|
0395aa95b6 | ||
|
|
c624baed2c | ||
|
|
1a40321c0f | ||
|
|
5d4fc36256 | ||
|
|
b799d785b9 | ||
|
|
d55442fa53 | ||
|
|
c870b749a4 | ||
|
|
4421cbc0c5 | ||
|
|
b6a3305f23 | ||
|
|
5680260136 | ||
|
|
bc66ea6ecb | ||
|
|
1b20218445 | ||
|
|
466e442353 | ||
|
|
9e6751f47f | ||
|
|
f868923905 | ||
|
|
5d3fd6d939 | ||
|
|
37659d687e | ||
|
|
f23b7e2c2f | ||
|
|
55ccdd63d4 | ||
|
|
5c88890ed4 | ||
|
|
fa0185c9a4 | ||
|
|
e1ed9a8e5e | ||
|
|
9f7e3bc282 | ||
|
|
4ec060ba24 | ||
|
|
b1f1753a8d | ||
|
|
94a62f8c31 | ||
|
|
e6f49f256c | ||
|
|
a048b0aa4a | ||
|
|
b6617c140c | ||
|
|
1fdf012f65 | ||
|
|
8fe0321ac0 | ||
|
|
4a2199fa13 | ||
|
|
69680a53a0 | ||
|
|
785dea6545 | ||
|
|
885932afe1 | ||
|
|
a38ccf3dcc | ||
|
|
d7b1f9cc7e | ||
|
|
6e92097544 | ||
|
|
76403b4ddc | ||
|
|
41ad8d8edb | ||
|
|
68754977b0 | ||
|
|
78ad6d5457 | ||
|
|
cb049ebf41 | ||
|
|
f2f5c3201f | ||
|
|
1ba178eae8 | ||
|
|
838f80c30c | ||
|
|
dc0408881e | ||
|
|
1b5e8cb69c | ||
|
|
9e13a3a467 | ||
|
|
32653c3b8a | ||
|
|
16258a2631 | ||
|
|
4c646249ad | ||
|
|
45c8a8d8a9 | ||
|
|
eff3444834 | ||
|
|
25f68f338c | ||
|
|
7ef363a3c2 | ||
|
|
3065e2bece | ||
|
|
1075d66122 | ||
|
|
d477b33774 | ||
|
|
7b374f8618 | ||
|
|
4869c00c0e | ||
|
|
ff4a5a68d9 | ||
|
|
d77a527b15 | ||
|
|
01caca48dc | ||
|
|
43e545b6ae | ||
|
|
8b65956dcc | ||
|
|
7b522ae120 | ||
|
|
43d134d9ad | ||
|
|
24c787f47d | ||
|
|
f95ac8be8c | ||
|
|
41442012c2 | ||
|
|
6f1195dded | ||
|
|
86e8feaabc | ||
|
|
670ca78a5b | ||
|
|
73ceed0f60 | ||
|
|
8412270772 | ||
|
|
686b47898c | ||
|
|
e03eaf2982 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ko_fi: mikayla_f
|
||||
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
@@ -5,12 +5,10 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- latest
|
||||
- devel
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- latest
|
||||
- devel
|
||||
jobs:
|
||||
check:
|
||||
|
||||
18
.github/workflows/manifest.yml
vendored
18
.github/workflows/manifest.yml
vendored
@@ -6,7 +6,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- latest
|
||||
- devel
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
@@ -43,7 +42,7 @@ jobs:
|
||||
- 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
|
||||
run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/devel
|
||||
- name: Generate manifest and shields for main branch
|
||||
id: manifest-main
|
||||
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
|
||||
@@ -51,21 +50,6 @@ jobs:
|
||||
- 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
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright © 2022 - 2023 Mikayla Fischler
|
||||
Copyright © 2022 - 2024 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
|
||||
|
||||
15
README.md
15
README.md
@@ -4,12 +4,11 @@ Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fi
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### [Join](https://discord.gg/R9NSCkhcwt) the Discord!
|
||||
### Join [the Discord](https://discord.gg/R9NSCkhcwt)!
|
||||
|
||||

|
||||

|
||||
|
||||
## Released Component Versions
|
||||
|
||||
@@ -17,6 +16,7 @@ Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fi
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
@@ -36,8 +36,9 @@ Mod Requirements:
|
||||
|
||||
Mod Recommendations:
|
||||
- Advanced Peripherals (adds the capability to detect environmental radiation levels)
|
||||
- Immersive Engineering (provides bundled redstone, though any mod containing bundled redstone will do)
|
||||
|
||||
v10.1+ is required due the complete support of CC:Tweaked added in Mekanism v10.1
|
||||
v10.1+ is required due to the complete support of CC:Tweaked added in Mekanism v10.1
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -45,6 +46,12 @@ You can install this on a ComputerCraft computer using either:
|
||||
* `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua`
|
||||
* `pastebin get sqUN6VUb ccmsi.lua`
|
||||
|
||||
## Contributing
|
||||
|
||||
Please reach out to me via Discord or email (or GitHub in some way) if you are thinking of making any contributions at this time. I started this project as a challenge for myself and have been enjoying having something I can work on in my own way.
|
||||
|
||||
Once this is out of beta I will be more open to contributions, but for now I am hoping to keep them to a minimum as the remaining challenges are ones I am looking forward to solving.
|
||||
|
||||
## [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.
|
||||
|
||||
|
||||
145
ccmsi.lua
145
ccmsi.lua
@@ -1,7 +1,7 @@
|
||||
--[[
|
||||
CC-MEK-SCADA Installer Utility
|
||||
|
||||
Copyright (c) 2023 Mikayla Fischler
|
||||
Copyright (c) 2023 - 2024 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,
|
||||
@@ -18,7 +18,7 @@ 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.11a"
|
||||
local CCMSI_VERSION = "v1.14"
|
||||
|
||||
local install_dir = "/.install-cache"
|
||||
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
|
||||
@@ -26,12 +26,13 @@ local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada
|
||||
|
||||
local opts = { ... }
|
||||
local mode, app, target
|
||||
local install_manifest = manifest_path .. "main/install_manifest.json"
|
||||
local install_manifest = manifest_path.."main/install_manifest.json"
|
||||
|
||||
local function red() term.setTextColor(colors.red) end
|
||||
local function orange() term.setTextColor(colors.orange) end
|
||||
local function yellow() term.setTextColor(colors.yellow) end
|
||||
local function green() term.setTextColor(colors.green) end
|
||||
local function cyan() term.setTextColor(colors.cyan) end
|
||||
local function blue() term.setTextColor(colors.blue) end
|
||||
local function white() term.setTextColor(colors.white) end
|
||||
local function lgray() term.setTextColor(colors.lightGray) end
|
||||
@@ -58,17 +59,17 @@ local function ask_y_n(question, default)
|
||||
end
|
||||
|
||||
-- print out a white + blue text message
|
||||
local function pkg_message(message, package) white();print(message .. " ");blue();println(package);white() end
|
||||
local function pkg_message(message, package) white();print(message.." ");blue();println(package);white() end
|
||||
|
||||
-- indicate actions to be taken based on package differences for installs/updates
|
||||
local function show_pkg_change(name, v)
|
||||
if v.v_local ~= nil then
|
||||
if v.v_local ~= v.v_remote then
|
||||
print("[" .. name .. "] updating ");blue();print(v.v_local);white();print(" \xbb ");blue();println(v.v_remote);white()
|
||||
print("["..name.."] updating ");blue();print(v.v_local);white();print(" \xbb ");blue();println(v.v_remote);white()
|
||||
elseif mode == "install" then
|
||||
pkg_message("[" .. name .. "] reinstalling", v.v_local)
|
||||
pkg_message("["..name.."] reinstalling", v.v_local)
|
||||
end
|
||||
else pkg_message("[" .. name .. "] new install of", v.v_remote) end
|
||||
else pkg_message("["..name.."] new install of", v.v_remote) end
|
||||
return v.v_local ~= v.v_remote
|
||||
end
|
||||
|
||||
@@ -89,7 +90,7 @@ local function get_remote_manifest()
|
||||
local response, error = http.get(install_manifest)
|
||||
if response == nil then
|
||||
orange();println("Failed to get installation manifest from GitHub, cannot update or install.")
|
||||
red();println("HTTP error: " .. error);white()
|
||||
red();println("HTTP error: "..error);white()
|
||||
return false, {}
|
||||
end
|
||||
|
||||
@@ -154,57 +155,55 @@ local function _clean_dir(dir, tree)
|
||||
if tree == nil then tree = {} end
|
||||
local ls = fs.list(dir)
|
||||
for _, val in pairs(ls) do
|
||||
local path = dir .. "/" .. val
|
||||
local path = dir.."/"..val
|
||||
if fs.isDir(path) then
|
||||
_clean_dir(path, tree[val])
|
||||
if #fs.list(path) == 0 then fs.delete(path);println("deleted " .. path) end
|
||||
elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then ---@fixme remove condition after migration to settings files
|
||||
if #fs.list(path) == 0 then fs.delete(path);println("deleted "..path) end
|
||||
elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then
|
||||
fs.delete(path)
|
||||
println("deleted " .. path)
|
||||
println("deleted "..path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- go through app/common directories to delete unused files
|
||||
local function clean(manifest)
|
||||
local root_ext = false
|
||||
local tree = gen_tree(manifest)
|
||||
|
||||
table.insert(tree, "install_manifest.json")
|
||||
table.insert(tree, "ccmsi.lua")
|
||||
table.insert(tree, "log.txt") ---@fixme fix after migration to settings files?
|
||||
|
||||
lgray()
|
||||
|
||||
local ls = fs.list("/")
|
||||
for _, val in pairs(ls) do
|
||||
if fs.isDir(val) then
|
||||
if tree[val] ~= nil then _clean_dir("/" .. val, tree[val]) end
|
||||
if #fs.list(val) == 0 then fs.delete(val);println("deleted " .. val) end
|
||||
if fs.isDriveRoot(val) then
|
||||
yellow();println("skipped mount '"..val.."'")
|
||||
elseif fs.isDir(val) then
|
||||
if tree[val] ~= nil then lgray();_clean_dir("/"..val, tree[val])
|
||||
else white(); if ask_y_n("delete the unused directory '"..val.."'") then lgray();_clean_dir("/"..val) end end
|
||||
if #fs.list(val) == 0 then fs.delete(val);lgray();println("deleted empty directory '"..val.."'") end
|
||||
elseif not _in_array(val, tree) and (string.find(val, ".settings") == nil) then
|
||||
root_ext = true
|
||||
yellow();println(val .. " not used")
|
||||
white();if ask_y_n("delete the unused file '"..val.."'") then fs.delete(val);lgray();println("deleted "..val) end
|
||||
end
|
||||
end
|
||||
|
||||
white()
|
||||
if root_ext then println("Files in root directory won't be automatically deleted.") end
|
||||
end
|
||||
|
||||
-- get and validate command line options
|
||||
|
||||
println("-- CC Mekanism SCADA Installer " .. CCMSI_VERSION .. " --")
|
||||
println("-- CC Mekanism SCADA Installer "..CCMSI_VERSION.." --")
|
||||
|
||||
if #opts == 0 or opts[1] == "help" then
|
||||
println("usage: ccmsi <mode> <app> <branch>")
|
||||
println("<mode>")
|
||||
lgray()
|
||||
println(" check - check latest versions avilable")
|
||||
println(" check - check latest versions available")
|
||||
yellow()
|
||||
println(" ccmsi check <branch> for target")
|
||||
lgray()
|
||||
println(" install - fresh install, overwrites config.lua")
|
||||
println(" update - update files EXCEPT for config.lua")
|
||||
println(" install - fresh install")
|
||||
println(" update - update files")
|
||||
println(" uninstall - delete files INCLUDING config/logs")
|
||||
white();println("<app>");lgray()
|
||||
println(" reactor-plc - reactor PLC firmware")
|
||||
@@ -214,7 +213,7 @@ if #opts == 0 or opts[1] == "help" then
|
||||
println(" pocket - pocket application")
|
||||
println(" installer - ccmsi installer (update only)")
|
||||
white();println("<branch>")
|
||||
lgray();println(" main (default) | latest | devel");white()
|
||||
lgray();println(" main (default) | devel");white()
|
||||
return
|
||||
else
|
||||
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
|
||||
@@ -234,14 +233,14 @@ else
|
||||
|
||||
-- determine target
|
||||
if mode == "check" then target = opts[2] else target = opts[3] end
|
||||
if (target ~= "main") and (target ~= "latest") and (target ~= "devel") then
|
||||
if (target ~= "main") and (target ~= "devel") then
|
||||
if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end
|
||||
target = "main"
|
||||
end
|
||||
|
||||
-- set paths
|
||||
install_manifest = manifest_path .. target .. "/install_manifest.json"
|
||||
repo_path = repo_path .. target .. "/"
|
||||
install_manifest = manifest_path..target.."/install_manifest.json"
|
||||
repo_path = repo_path..target.."/"
|
||||
end
|
||||
|
||||
-- run selected mode
|
||||
@@ -261,23 +260,21 @@ if mode == "check" then
|
||||
-- list all versions
|
||||
for key, value in pairs(manifest.versions) do
|
||||
term.setTextColor(colors.purple)
|
||||
print(string.format("%-14s", "[" .. key .. "]"))
|
||||
print(string.format("%-14s", "["..key.."]"))
|
||||
if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then
|
||||
blue();print(local_manifest.versions[key])
|
||||
if value ~= local_manifest.versions[key] then
|
||||
white();print(" (")
|
||||
term.setTextColor(colors.cyan)
|
||||
print(value);white();println(" available)")
|
||||
cyan();print(value);white();println(" available)")
|
||||
else green();println(" (up to date)") end
|
||||
else
|
||||
lgray();print("not installed");white();print(" (latest ")
|
||||
term.setTextColor(colors.cyan)
|
||||
print(value);white();println(")")
|
||||
cyan();print(value);white();println(")")
|
||||
end
|
||||
end
|
||||
|
||||
if manifest.versions.installer ~= local_manifest.versions.installer then
|
||||
yellow();println("\nA newer version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white()
|
||||
yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white()
|
||||
end
|
||||
elseif mode == "install" or mode == "update" then
|
||||
local update_installer = app == "installer"
|
||||
@@ -315,13 +312,13 @@ elseif mode == "install" or mode == "update" then
|
||||
end
|
||||
|
||||
if manifest.versions.installer ~= CCMSI_VERSION then
|
||||
if not update_installer then yellow();println("A newer version of the installer is available, it is recommended to update to it.");white() end
|
||||
if not update_installer then yellow();println("A different version of the installer is available, it is recommended to update to it.");white() end
|
||||
if update_installer or ask_y_n("Would you like to update now") then
|
||||
lgray();println("GET ccmsi.lua")
|
||||
local dl, err = http.get(repo_path .. "ccmsi.lua")
|
||||
local dl, err = http.get(repo_path.."ccmsi.lua")
|
||||
|
||||
if dl == nil then
|
||||
red();println("HTTP Error " .. err)
|
||||
red();println("HTTP Error "..err)
|
||||
println("Installer download failed.");white()
|
||||
else
|
||||
local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location
|
||||
@@ -345,12 +342,8 @@ elseif mode == "install" or mode == "update" then
|
||||
ver.lockbox.v_remote = manifest.versions.lockbox
|
||||
|
||||
green()
|
||||
if mode == "install" then
|
||||
println("Installing " .. app .. " files...")
|
||||
elseif mode == "update" then
|
||||
println("Updating " .. app .. " files... (keeping old config.lua)")
|
||||
end
|
||||
white()
|
||||
if mode == "install" then print("Installing ") else print("Updating ") end
|
||||
println(app.." files...");white()
|
||||
|
||||
ver.boot.changed = show_pkg_change("bootldr", ver.boot)
|
||||
ver.common.changed = show_pkg_change("common", ver.common)
|
||||
@@ -376,7 +369,6 @@ elseif mode == "install" or mode == "update" then
|
||||
local file_list = manifest.files
|
||||
local size_list = manifest.sizes
|
||||
local dependencies = manifest.depends[app]
|
||||
local config_file = app .. "/config.lua"
|
||||
|
||||
table.insert(dependencies, app)
|
||||
|
||||
@@ -423,15 +415,15 @@ elseif mode == "install" or mode == "update" then
|
||||
|
||||
local files = file_list[dependency]
|
||||
for _, file in pairs(files) do
|
||||
println("GET " .. file)
|
||||
local dl, err = http.get(repo_path .. file)
|
||||
println("GET "..file)
|
||||
local dl, err = http.get(repo_path..file)
|
||||
|
||||
if dl == nil then
|
||||
red();println("HTTP Error " .. err)
|
||||
red();println("HTTP Error "..err)
|
||||
success = false
|
||||
break
|
||||
else
|
||||
local handle = fs.open(install_dir .. "/" .. file, "w")
|
||||
local handle = fs.open(install_dir.."/"..file, "w")
|
||||
handle.write(dl.readAll())
|
||||
handle.close()
|
||||
end
|
||||
@@ -450,11 +442,9 @@ elseif mode == "install" or mode == "update" then
|
||||
|
||||
local files = file_list[dependency]
|
||||
for _, file in pairs(files) do
|
||||
if mode == "install" or file ~= config_file then
|
||||
local temp_file = install_dir .. "/" .. file
|
||||
if fs.exists(file) then fs.delete(file) end
|
||||
fs.move(temp_file, file)
|
||||
end
|
||||
local temp_file = install_dir.."/"..file
|
||||
if fs.exists(file) then fs.delete(file) end
|
||||
fs.move(temp_file, file)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -487,19 +477,17 @@ elseif mode == "install" or mode == "update" then
|
||||
|
||||
local files = file_list[dependency]
|
||||
for _, file in pairs(files) do
|
||||
if mode == "install" or file ~= config_file then
|
||||
println("GET " .. file)
|
||||
local dl, err = http.get(repo_path .. file)
|
||||
println("GET "..file)
|
||||
local dl, err = http.get(repo_path..file)
|
||||
|
||||
if dl == nil then
|
||||
red();println("HTTP Error " .. err)
|
||||
success = false
|
||||
break
|
||||
else
|
||||
local handle = fs.open("/" .. file, "w")
|
||||
handle.write(dl.readAll())
|
||||
handle.close()
|
||||
end
|
||||
if dl == nil then
|
||||
red();println("HTTP Error "..err)
|
||||
success = false
|
||||
break
|
||||
else
|
||||
local handle = fs.open("/"..file, "w")
|
||||
handle.write(dl.readAll())
|
||||
handle.close()
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -529,11 +517,11 @@ elseif mode == "uninstall" then
|
||||
end
|
||||
|
||||
if manifest.versions[app] == nil then
|
||||
red();println("Error: '" .. app .. "' is not installed.")
|
||||
red();println("Error: '"..app.."' is not installed.")
|
||||
return
|
||||
end
|
||||
|
||||
orange();println("Uninstalling all " .. app .. " files...")
|
||||
orange();println("Uninstalling all "..app.." files...")
|
||||
|
||||
-- ask for confirmation
|
||||
if not ask_y_n("Continue", false) then return end
|
||||
@@ -548,16 +536,16 @@ elseif mode == "uninstall" then
|
||||
|
||||
-- delete log file
|
||||
local log_deleted = false
|
||||
local settings_file = app .. ".settings"
|
||||
local legacy_config_file = app .. "/config.lua"
|
||||
local settings_file = app..".settings"
|
||||
local legacy_config_file = app.."/config.lua"
|
||||
|
||||
lgray()
|
||||
if fs.exists(legacy_config_file) then
|
||||
log_deleted = pcall(function ()
|
||||
local config = require(app .. ".config")
|
||||
local config = require(app..".config")
|
||||
if fs.exists(config.LOG_PATH) then
|
||||
fs.delete(config.LOG_PATH)
|
||||
println("deleted log file " .. config.LOG_PATH)
|
||||
println("deleted log file "..config.LOG_PATH)
|
||||
end
|
||||
end)
|
||||
elseif fs.exists(settings_file) and settings.load(settings_file) then
|
||||
@@ -565,7 +553,7 @@ elseif mode == "uninstall" then
|
||||
if log ~= nil and fs.exists(log) then
|
||||
log_deleted = true
|
||||
fs.delete(log)
|
||||
println("deleted log file " .. log)
|
||||
println("deleted log file "..log)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -579,7 +567,7 @@ elseif mode == "uninstall" then
|
||||
for _, dependency in pairs(dependencies) do
|
||||
local files = file_list[dependency]
|
||||
for _, file in pairs(files) do
|
||||
if fs.exists(file) then fs.delete(file);println("deleted " .. file) end
|
||||
if fs.exists(file) then fs.delete(file);println("deleted "..file) end
|
||||
end
|
||||
|
||||
local folder = files[1]
|
||||
@@ -590,13 +578,16 @@ elseif mode == "uninstall" then
|
||||
|
||||
if fs.isDir(folder) then
|
||||
fs.delete(folder)
|
||||
println("deleted directory " .. folder)
|
||||
println("deleted directory "..folder)
|
||||
end
|
||||
end
|
||||
|
||||
if fs.exists(legacy_config_file) then
|
||||
fs.delete(legacy_config_file);println("deleted "..legacy_config_file)
|
||||
end
|
||||
|
||||
if fs.exists(settings_file) then
|
||||
fs.delete(settings_file)
|
||||
println("deleted " .. settings_file)
|
||||
fs.delete(settings_file);println("deleted "..settings_file)
|
||||
end
|
||||
|
||||
fs.delete("install_manifest.json")
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
print("CONFIGURE> SCANNING FOR CONFIGURATOR...")
|
||||
|
||||
if fs.exists("reactor-plc/configure.lua") then
|
||||
require("reactor-plc.configure").configure()
|
||||
elseif fs.exists("rtu/startup.lua") then
|
||||
print("CONFIGURE> RTU CONFIGURATOR NOT YET IMPLEMENTED IN BETA")
|
||||
elseif fs.exists("supervisor/startup.lua") then
|
||||
print("CONFIGURE> SUPERVISOR CONFIGURATOR NOT YET IMPLEMENTED IN BETA")
|
||||
elseif fs.exists("coordinator/startup.lua") then
|
||||
print("CONFIGURE> COORDINATOR CONFIGURATOR NOT YET IMPLEMENTED IN BETA")
|
||||
elseif fs.exists("pocket/startup.lua") then
|
||||
print("CONFIGURE> POCKET CONFIGURATOR NOT YET IMPLEMENTED IN BETA")
|
||||
if fs.exists("reactor-plc/configure.lua") then require("reactor-plc.configure").configure()
|
||||
elseif fs.exists("rtu/configure.lua") then require("rtu.configure").configure()
|
||||
elseif fs.exists("supervisor/configure.lua") then require("supervisor.configure").configure()
|
||||
elseif fs.exists("coordinator/configure.lua") then require("coordinator.configure").configure()
|
||||
elseif fs.exists("pocket/configure.lua") then require("pocket.configure").configure()
|
||||
else
|
||||
print("CONFIGURE> NO CONFIGURATOR FOUND")
|
||||
print("CONFIGURE> EXIT")
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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.SV_TIMEOUT = 5
|
||||
config.API_TIMEOUT = 5
|
||||
-- facility authentication key (do NOT use one of your passwords)
|
||||
-- this enables verifying that messages are authentic
|
||||
-- all devices on the same network must use the same key
|
||||
-- config.AUTH_KEY = "SCADAfacility123"
|
||||
|
||||
-- expected number of reactor units, used only to require that number of unit monitors
|
||||
config.NUM_UNITS = 4
|
||||
|
||||
-- alarm sounder volume (0.0 to 3.0, 1.0 being standard max volume, this is the option given to to speaker.play())
|
||||
-- note: alarm sine waves are at half saturation, so that multiple will be required to reach full scale
|
||||
config.SOUNDER_VOLUME = 1.0
|
||||
|
||||
-- true for 24 hour time on main view screen
|
||||
config.TIME_24_HOUR = true
|
||||
|
||||
-- disable flow view (for legacy layouts)
|
||||
config.DISABLE_FLOW_VIEW = false
|
||||
|
||||
-- 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
|
||||
1491
coordinator/configure.lua
Normal file
1491
coordinator/configure.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,16 +4,13 @@ local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
local types = require("scada-common.types")
|
||||
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
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 PROTOCOL = comms.PROTOCOL
|
||||
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
@@ -26,182 +23,166 @@ local LINK_TIMEOUT = 60.0
|
||||
|
||||
local coordinator = {}
|
||||
|
||||
-- request the user to select a monitor
|
||||
---@nodiscard
|
||||
---@param names table available monitors
|
||||
---@return boolean|string|nil
|
||||
local function ask_monitor(names)
|
||||
println("available monitors:")
|
||||
for i = 1, #names do
|
||||
print(" " .. names[i])
|
||||
end
|
||||
println("")
|
||||
println("select a monitor or type c to cancel")
|
||||
---@type crd_config
|
||||
local config = {}
|
||||
|
||||
local iface = dialog.ask_options(names, "c")
|
||||
coordinator.config = config
|
||||
|
||||
if iface ~= false and iface ~= nil then
|
||||
util.filter_table(names, function (x) return x ~= iface end)
|
||||
-- load the coordinator configuration<br>
|
||||
-- status of 0 is OK, 1 is bad config, 2 is bad monitor config
|
||||
---@return 0|1|2 status, nil|monitors_struct|string monitors (or error message)
|
||||
function coordinator.load_config()
|
||||
if not settings.load("/coordinator.settings") then return 1 end
|
||||
|
||||
config.UnitCount = settings.get("UnitCount")
|
||||
config.SpeakerVolume = settings.get("SpeakerVolume")
|
||||
config.Time24Hour = settings.get("Time24Hour")
|
||||
config.TempScale = settings.get("TempScale")
|
||||
|
||||
config.DisableFlowView = settings.get("DisableFlowView")
|
||||
config.MainDisplay = settings.get("MainDisplay")
|
||||
config.FlowDisplay = settings.get("FlowDisplay")
|
||||
config.UnitDisplays = settings.get("UnitDisplays")
|
||||
|
||||
config.SVR_Channel = settings.get("SVR_Channel")
|
||||
config.CRD_Channel = settings.get("CRD_Channel")
|
||||
config.PKT_Channel = settings.get("PKT_Channel")
|
||||
config.SVR_Timeout = settings.get("SVR_Timeout")
|
||||
config.API_Timeout = settings.get("API_Timeout")
|
||||
config.TrustedRange = settings.get("TrustedRange")
|
||||
config.AuthKey = settings.get("AuthKey")
|
||||
|
||||
config.LogMode = settings.get("LogMode")
|
||||
config.LogPath = settings.get("LogPath")
|
||||
config.LogDebug = settings.get("LogDebug")
|
||||
|
||||
config.MainTheme = settings.get("MainTheme")
|
||||
config.FrontPanelTheme = settings.get("FrontPanelTheme")
|
||||
config.ColorMode = settings.get("ColorMode")
|
||||
|
||||
local cfv = util.new_validator()
|
||||
|
||||
cfv.assert_type_int(config.UnitCount)
|
||||
cfv.assert_range(config.UnitCount, 1, 4)
|
||||
cfv.assert_type_bool(config.Time24Hour)
|
||||
cfv.assert_type_int(config.TempScale)
|
||||
cfv.assert_range(config.TempScale, 1, 4)
|
||||
|
||||
cfv.assert_type_bool(config.DisableFlowView)
|
||||
cfv.assert_type_table(config.UnitDisplays)
|
||||
|
||||
cfv.assert_type_num(config.SpeakerVolume)
|
||||
cfv.assert_range(config.SpeakerVolume, 0, 3)
|
||||
|
||||
cfv.assert_channel(config.SVR_Channel)
|
||||
cfv.assert_channel(config.CRD_Channel)
|
||||
cfv.assert_channel(config.PKT_Channel)
|
||||
|
||||
cfv.assert_type_num(config.SVR_Timeout)
|
||||
cfv.assert_min(config.SVR_Timeout, 2)
|
||||
cfv.assert_type_num(config.API_Timeout)
|
||||
cfv.assert_min(config.API_Timeout, 2)
|
||||
|
||||
cfv.assert_type_num(config.TrustedRange)
|
||||
cfv.assert_min(config.TrustedRange, 0)
|
||||
cfv.assert_type_str(config.AuthKey)
|
||||
|
||||
if type(config.AuthKey) == "string" then
|
||||
local len = string.len(config.AuthKey)
|
||||
cfv.assert(len == 0 or len >= 8)
|
||||
end
|
||||
|
||||
return iface
|
||||
end
|
||||
cfv.assert_type_int(config.LogMode)
|
||||
cfv.assert_range(config.LogMode, 0, 1)
|
||||
cfv.assert_type_str(config.LogPath)
|
||||
cfv.assert_type_bool(config.LogDebug)
|
||||
|
||||
cfv.assert_type_int(config.MainTheme)
|
||||
cfv.assert_range(config.MainTheme, 1, 2)
|
||||
cfv.assert_type_int(config.FrontPanelTheme)
|
||||
cfv.assert_range(config.FrontPanelTheme, 1, 2)
|
||||
cfv.assert_type_int(config.ColorMode)
|
||||
cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
|
||||
|
||||
-- Monitor Setup
|
||||
|
||||
-- configure monitor layout
|
||||
---@param num_units integer number of units expected
|
||||
---@param disable_flow_view boolean disable flow view (legacy)
|
||||
---@return boolean success, monitors_struct? monitors
|
||||
function coordinator.configure_monitors(num_units, disable_flow_view)
|
||||
---@class monitors_struct
|
||||
local monitors = {
|
||||
primary = nil, ---@type table|nil
|
||||
primary_name = "",
|
||||
flow = nil, ---@type table|nil
|
||||
main = nil, ---@type table|nil
|
||||
main_name = "",
|
||||
flow = nil, ---@type table|nil
|
||||
flow_name = "",
|
||||
unit_displays = {},
|
||||
unit_name_map = {}
|
||||
}
|
||||
|
||||
local monitors_avail = ppm.get_monitor_list()
|
||||
local names = {}
|
||||
local available = {}
|
||||
local mon_cfv = util.new_validator()
|
||||
|
||||
-- get all interface names
|
||||
for iface, _ in pairs(monitors_avail) do
|
||||
table.insert(names, iface)
|
||||
table.insert(available, iface)
|
||||
end
|
||||
local names = {}
|
||||
for iface, _ in pairs(ppm.get_monitor_list()) do table.insert(names, iface) end
|
||||
|
||||
-- we need a certain number of monitors (1 per unit + 1 primary display + 1 flow display)
|
||||
local num_displays_needed = num_units + util.trinary(disable_flow_view, 1, 2)
|
||||
if #names < num_displays_needed then
|
||||
local message = "not enough monitors connected (need " .. num_displays_needed .. ")"
|
||||
println(message)
|
||||
log.warning(message)
|
||||
return false
|
||||
end
|
||||
local function setup_monitors()
|
||||
mon_cfv.assert_type_str(config.MainDisplay)
|
||||
if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end
|
||||
mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount)
|
||||
|
||||
-- attempt to load settings
|
||||
if not settings.load("/coord.settings") then
|
||||
log.warning("configure_monitors(): failed to load coordinator settings file (may not exist yet)")
|
||||
else
|
||||
local _primary = settings.get("PRIMARY_DISPLAY")
|
||||
local _flow = settings.get("FLOW_DISPLAY")
|
||||
local _unitd = settings.get("UNIT_DISPLAYS")
|
||||
if mon_cfv.valid() then
|
||||
local w, h, _
|
||||
|
||||
-- filter out already assigned monitors
|
||||
util.filter_table(available, function (x) return x ~= _primary end)
|
||||
util.filter_table(available, function (x) return x ~= _flow end)
|
||||
if type(_unitd) == "table" then
|
||||
util.filter_table(available, function (x) return not util.table_contains(_unitd, x) end)
|
||||
end
|
||||
end
|
||||
|
||||
---------------------
|
||||
-- PRIMARY DISPLAY --
|
||||
---------------------
|
||||
|
||||
local iface_primary_display = settings.get("PRIMARY_DISPLAY") ---@type boolean|string|nil
|
||||
|
||||
if not util.table_contains(names, iface_primary_display) then
|
||||
println("primary display is not connected")
|
||||
local response = dialog.ask_y_n("would you like to change it", true)
|
||||
if response == false then return false end
|
||||
iface_primary_display = nil
|
||||
end
|
||||
|
||||
while iface_primary_display == nil and #available > 0 do
|
||||
iface_primary_display = ask_monitor(available)
|
||||
end
|
||||
|
||||
if type(iface_primary_display) ~= "string" then return false end
|
||||
|
||||
settings.set("PRIMARY_DISPLAY", iface_primary_display)
|
||||
util.filter_table(available, function (x) return x ~= iface_primary_display end)
|
||||
|
||||
monitors.primary = ppm.get_periph(iface_primary_display)
|
||||
monitors.primary_name = iface_primary_display
|
||||
|
||||
--------------------------
|
||||
-- FLOW MONITOR DISPLAY --
|
||||
--------------------------
|
||||
|
||||
if not disable_flow_view then
|
||||
local iface_flow_display = settings.get("FLOW_DISPLAY") ---@type boolean|string|nil
|
||||
|
||||
if not util.table_contains(names, iface_flow_display) then
|
||||
println("flow monitor display is not connected")
|
||||
local response = dialog.ask_y_n("would you like to change it", true)
|
||||
if response == false then return false end
|
||||
iface_flow_display = nil
|
||||
end
|
||||
|
||||
while iface_flow_display == nil and #available > 0 do
|
||||
iface_flow_display = ask_monitor(available)
|
||||
end
|
||||
|
||||
if type(iface_flow_display) ~= "string" then return false end
|
||||
|
||||
settings.set("FLOW_DISPLAY", iface_flow_display)
|
||||
util.filter_table(available, function (x) return x ~= iface_flow_display end)
|
||||
|
||||
monitors.flow = ppm.get_periph(iface_flow_display)
|
||||
monitors.flow_name = iface_flow_display
|
||||
end
|
||||
|
||||
-------------------
|
||||
-- UNIT DISPLAYS --
|
||||
-------------------
|
||||
|
||||
local unit_displays = settings.get("UNIT_DISPLAYS")
|
||||
|
||||
if unit_displays == nil then
|
||||
unit_displays = {}
|
||||
for i = 1, num_units do
|
||||
local display = nil
|
||||
|
||||
while display == nil and #available > 0 do
|
||||
println("please select monitor for unit #" .. i)
|
||||
display = ask_monitor(available)
|
||||
if not util.table_contains(names, config.MainDisplay) then
|
||||
return 2, "Main monitor is not connected."
|
||||
end
|
||||
|
||||
if display == false then return false end
|
||||
monitors.main = ppm.get_periph(config.MainDisplay)
|
||||
monitors.main_name = config.MainDisplay
|
||||
|
||||
unit_displays[i] = display
|
||||
end
|
||||
else
|
||||
-- make sure all displays are connected
|
||||
for i = 1, num_units do
|
||||
local display = unit_displays[i]
|
||||
|
||||
if not util.table_contains(names, display) then
|
||||
println("unit #" .. i .. " display is not connected")
|
||||
local response = dialog.ask_y_n("would you like to change it", true)
|
||||
if response == false then return false end
|
||||
display = nil
|
||||
monitors.main.setTextScale(0.5)
|
||||
w, _ = ppm.monitor_block_size(monitors.main.getSize())
|
||||
if w ~= 8 then
|
||||
return 2, util.c("Main monitor width is incorrect (was ", w, ", must be 8).")
|
||||
end
|
||||
|
||||
while display == nil and #available > 0 do
|
||||
display = ask_monitor(available)
|
||||
if not config.DisableFlowView then
|
||||
if not util.table_contains(names, config.FlowDisplay) then
|
||||
return 2, "Flow monitor is not connected."
|
||||
end
|
||||
|
||||
monitors.flow = ppm.get_periph(config.FlowDisplay)
|
||||
monitors.flow_name = config.FlowDisplay
|
||||
|
||||
monitors.flow.setTextScale(0.5)
|
||||
w, _ = ppm.monitor_block_size(monitors.flow.getSize())
|
||||
if w ~= 8 then
|
||||
return 2, util.c("Flow monitor width is incorrect (was ", w, ", must be 8).")
|
||||
end
|
||||
end
|
||||
|
||||
if display == false then return false end
|
||||
for i = 1, config.UnitCount do
|
||||
local display = config.UnitDisplays[i]
|
||||
if type(display) ~= "string" or not util.table_contains(names, display) then
|
||||
return 2, "Unit " .. i .. " monitor is not connected."
|
||||
end
|
||||
|
||||
unit_displays[i] = display
|
||||
end
|
||||
monitors.unit_displays[i] = ppm.get_periph(display)
|
||||
monitors.unit_name_map[i] = display
|
||||
|
||||
monitors.unit_displays[i].setTextScale(0.5)
|
||||
w, h = ppm.monitor_block_size(monitors.unit_displays[i].getSize())
|
||||
if w ~= 4 or h ~= 4 then
|
||||
return 2, util.c("Unit ", i, " monitor size is incorrect (was ", w, " by ", h,", must be 4 by 4).")
|
||||
end
|
||||
end
|
||||
else return 2, "Monitor configuration invalid." end
|
||||
end
|
||||
|
||||
settings.set("UNIT_DISPLAYS", unit_displays)
|
||||
if not settings.save("/coord.settings") then
|
||||
log.warning("configure_monitors(): failed to save coordinator settings file")
|
||||
end
|
||||
if cfv.valid() then
|
||||
local ok, result, message = pcall(setup_monitors)
|
||||
assert(ok, util.c("fatal error while trying to verify monitors: ", result))
|
||||
if result == 2 then return 2, message end
|
||||
else return 1 end
|
||||
|
||||
for i = 1, #unit_displays do
|
||||
monitors.unit_displays[i] = ppm.get_periph(unit_displays[i])
|
||||
monitors.unit_name_map[i] = unit_displays[i]
|
||||
end
|
||||
|
||||
return true, monitors
|
||||
return 0, monitors
|
||||
end
|
||||
|
||||
-- dmesg print wrapper
|
||||
@@ -211,7 +192,7 @@ end
|
||||
---@return function? update, function? done
|
||||
local function log_dmesg(message, dmesg_tag, working)
|
||||
local colors = {
|
||||
GRAPHICS = colors.green,
|
||||
RENDER = colors.green,
|
||||
SYSTEM = colors.cyan,
|
||||
BOOT = colors.blue,
|
||||
COMMS = colors.purple,
|
||||
@@ -225,7 +206,7 @@ local function log_dmesg(message, dmesg_tag, working)
|
||||
end
|
||||
end
|
||||
|
||||
function coordinator.log_graphics(message) log_dmesg(message, "GRAPHICS") end
|
||||
function coordinator.log_render(message) log_dmesg(message, "RENDER") end
|
||||
function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end
|
||||
function coordinator.log_boot(message) log_dmesg(message, "BOOT") end
|
||||
function coordinator.log_comms(message) log_dmesg(message, "COMMS") end
|
||||
@@ -246,13 +227,8 @@ end
|
||||
---@nodiscard
|
||||
---@param version string coordinator version
|
||||
---@param nic nic network interface device
|
||||
---@param num_units integer number of configured units for number of monitors, checked against SV
|
||||
---@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, nic, num_units, crd_channel, svr_channel, pkt_channel, range, sv_watchdog)
|
||||
function coordinator.comms(version, nic, sv_watchdog)
|
||||
local self = {
|
||||
sv_linked = false,
|
||||
sv_addr = comms.BROADCAST,
|
||||
@@ -267,16 +243,16 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
||||
est_task_done = nil
|
||||
}
|
||||
|
||||
comms.set_trusted_range(range)
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
comms.set_trusted_range(config.TrustedRange)
|
||||
|
||||
-- configure network channels
|
||||
nic.closeAll()
|
||||
nic.open(crd_channel)
|
||||
nic.open(config.CRD_Channel)
|
||||
|
||||
-- link nic to apisessions
|
||||
apisessions.init(nic)
|
||||
-- pass config to apisessions
|
||||
apisessions.init(nic, config)
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- send a packet to the supervisor
|
||||
---@param msg_type MGMT_TYPE|CRDN_TYPE
|
||||
@@ -296,21 +272,22 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable())
|
||||
|
||||
nic.transmit(svr_channel, crd_channel, s_pkt)
|
||||
nic.transmit(config.SVR_Channel, config.CRD_Channel, s_pkt)
|
||||
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)
|
||||
---@param data any?
|
||||
local function _send_api_establish_ack(packet, ack, data)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
|
||||
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack })
|
||||
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data })
|
||||
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
nic.transmit(pkt_channel, crd_channel, s_pkt)
|
||||
nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt)
|
||||
self.last_api_est_acks[packet.src_addr()] = ack
|
||||
end
|
||||
|
||||
@@ -339,24 +316,24 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
||||
|
||||
if not self.sv_linked then
|
||||
if self.est_tick_waiting == nil then
|
||||
self.est_start = util.time_s()
|
||||
self.est_start = os.clock()
|
||||
self.est_last = self.est_start
|
||||
|
||||
self.est_tick_waiting, self.est_task_done =
|
||||
coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. svr_channel)
|
||||
coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SVR_Channel)
|
||||
|
||||
_send_establish()
|
||||
else
|
||||
self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (util.time_s() - self.est_start)))
|
||||
self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (os.clock() - self.est_start)))
|
||||
end
|
||||
|
||||
if abort or (util.time_s() - self.est_start) >= LINK_TIMEOUT then
|
||||
if abort or (os.clock() - self.est_start) >= LINK_TIMEOUT then
|
||||
self.est_task_done(false)
|
||||
|
||||
if abort then
|
||||
coordinator.log_comms("supervisor connection attempt cancelled by user")
|
||||
elseif self.sv_config_err then
|
||||
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
|
||||
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
|
||||
elseif not self.sv_linked then
|
||||
if self.last_est_ack == ESTABLISH_ACK.DENY then
|
||||
coordinator.log_comms("supervisor connection attempt denied")
|
||||
@@ -371,11 +348,12 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
||||
|
||||
ok = false
|
||||
elseif self.sv_config_err then
|
||||
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
|
||||
self.est_task_done(false)
|
||||
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
|
||||
ok = false
|
||||
elseif (util.time_s() - self.est_last) > 1.0 then
|
||||
elseif (os.clock() - self.est_last) > 1.0 then
|
||||
_send_establish()
|
||||
self.est_last = util.time_s()
|
||||
self.est_last = os.clock()
|
||||
end
|
||||
elseif self.est_tick_waiting ~= nil then
|
||||
self.est_task_done(true)
|
||||
@@ -405,10 +383,10 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
||||
end
|
||||
|
||||
-- send the auto process control configuration with a start command
|
||||
---@param config coord_auto_config configuration
|
||||
function public.send_auto_start(config)
|
||||
---@param auto_cfg coord_auto_config configuration
|
||||
function public.send_auto_start(auto_cfg)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
|
||||
FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits
|
||||
FAC_COMMAND.START, auto_cfg.mode, auto_cfg.burn_target, auto_cfg.charge_target, auto_cfg.gen_target, auto_cfg.limits
|
||||
})
|
||||
end
|
||||
|
||||
@@ -464,9 +442,9 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
||||
local src_addr = packet.scada_frame.src_addr()
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
|
||||
if l_chan ~= crd_channel then
|
||||
if l_chan ~= config.CRD_Channel then
|
||||
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
||||
elseif r_chan == pkt_channel then
|
||||
elseif r_chan == config.PKT_Channel then
|
||||
if not self.sv_linked then
|
||||
log.debug("discarding pocket API packet before linked to supervisor")
|
||||
elseif protocol == PROTOCOL.SCADA_CRDN then
|
||||
@@ -494,10 +472,11 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
||||
elseif packet.type == 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]
|
||||
if packet.length == 4 then
|
||||
local comms_v = util.strval(packet.data[1])
|
||||
local firmware_v = util.strval(packet.data[2])
|
||||
local dev_type = packet.data[3]
|
||||
local api_v = util.strval(packet.data[4])
|
||||
|
||||
if comms_v ~= comms.version then
|
||||
if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_VERSION then
|
||||
@@ -505,12 +484,19 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
||||
end
|
||||
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
|
||||
elseif api_v ~= comms.api_version then
|
||||
if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_API_VERSION then
|
||||
log.info(util.c("dropping API establish packet with incorrect api version v", api_v, " (expected v", comms.api_version, ")"))
|
||||
end
|
||||
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_API_VERSION)
|
||||
elseif dev_type == DEVICE_TYPE.PKT then
|
||||
-- pocket linking request
|
||||
local id = apisessions.establish_session(src_addr, firmware_v)
|
||||
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)
|
||||
local conf = iocontrol.get_db().facility.conf
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW, { conf.num_units, conf.cooling })
|
||||
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)
|
||||
@@ -526,7 +512,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
||||
else
|
||||
log.debug("illegal packet type " .. protocol .. " on pocket channel", true)
|
||||
end
|
||||
elseif r_chan == svr_channel then
|
||||
elseif r_chan == config.SVR_Channel then
|
||||
-- check sequence number
|
||||
if self.sv_r_seq_num == nil then
|
||||
self.sv_r_seq_num = packet.scada_frame.seq_num()
|
||||
@@ -699,24 +685,24 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
||||
-- connection with supervisor established
|
||||
if packet.length == 2 then
|
||||
local est_ack = packet.data[1]
|
||||
local config = packet.data[2]
|
||||
local sv_config = packet.data[2]
|
||||
|
||||
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||
-- reset to disconnected before validating
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
|
||||
if type(config) == "table" and #config == 2 then
|
||||
if type(sv_config) == "table" and #sv_config == 2 then
|
||||
-- get configuration
|
||||
|
||||
---@class facility_conf
|
||||
local conf = {
|
||||
num_units = config[1], ---@type integer
|
||||
cooling = config[2] ---@type sv_cooling_conf
|
||||
num_units = sv_config[1], ---@type integer
|
||||
cooling = sv_config[2] ---@type sv_cooling_conf
|
||||
}
|
||||
|
||||
if conf.num_units == num_units then
|
||||
if conf.num_units == config.UnitCount then
|
||||
-- init io controller
|
||||
iocontrol.init(conf, public)
|
||||
iocontrol.init(conf, public, config.TempScale)
|
||||
|
||||
self.sv_addr = src_addr
|
||||
self.sv_linked = true
|
||||
|
||||
@@ -47,10 +47,27 @@ end
|
||||
-- initialize the coordinator IO controller
|
||||
---@param conf facility_conf configuration
|
||||
---@param comms coord_comms comms reference
|
||||
function iocontrol.init(conf, comms)
|
||||
---@param temp_scale integer temperature unit (1 = K, 2 = C, 3 = F, 4 = R)
|
||||
function iocontrol.init(conf, comms, temp_scale)
|
||||
-- temperature unit label and conversion function (from Kelvin)
|
||||
if temp_scale == 2 then
|
||||
io.temp_label = "\xb0C"
|
||||
io.temp_convert = function (t) return t - 273.15 end
|
||||
elseif temp_scale == 3 then
|
||||
io.temp_label = "\xb0F"
|
||||
io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end
|
||||
elseif temp_scale == 4 then
|
||||
io.temp_label = "\xb0R"
|
||||
io.temp_convert = function (t) return 1.8 * t end
|
||||
else
|
||||
io.temp_label = "K"
|
||||
io.temp_convert = function (t) return t end
|
||||
end
|
||||
|
||||
-- facility data structure
|
||||
---@class ioctl_facility
|
||||
io.facility = {
|
||||
conf = conf,
|
||||
num_units = conf.num_units,
|
||||
tank_mode = conf.cooling.fac_tank_mode,
|
||||
tank_defs = conf.cooling.fac_tank_defs,
|
||||
@@ -75,6 +92,7 @@ function iocontrol.init(conf, comms)
|
||||
---@type WASTE_PRODUCT
|
||||
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
|
||||
auto_pu_fallback_active = false,
|
||||
auto_sps_disabled = false,
|
||||
|
||||
radiation = types.new_zero_radiation_reading(),
|
||||
|
||||
@@ -110,9 +128,9 @@ function iocontrol.init(conf, comms)
|
||||
-- determine tank information
|
||||
if io.facility.tank_mode == 0 then
|
||||
io.facility.tank_defs = {}
|
||||
-- on facility tank mode 0, setup tank defs to match unit TANK option
|
||||
-- on facility tank mode 0, setup tank defs to match unit tank option
|
||||
for i = 1, conf.num_units do
|
||||
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TANK, 1, 0)
|
||||
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TankConnection, 1, 0)
|
||||
end
|
||||
|
||||
io.facility.tank_list = { table.unpack(io.facility.tank_defs) }
|
||||
@@ -210,16 +228,21 @@ function iocontrol.init(conf, comms)
|
||||
---@class ioctl_unit
|
||||
local entry = {
|
||||
unit_id = i,
|
||||
connected = false,
|
||||
rtu_hw = { boilers = {}, turbines = {} },
|
||||
|
||||
num_boilers = 0,
|
||||
num_turbines = 0,
|
||||
num_snas = 0,
|
||||
has_tank = conf.cooling.r_cool[i].TANK,
|
||||
has_tank = conf.cooling.r_cool[i].TankConnection,
|
||||
|
||||
control_state = false,
|
||||
burn_rate_cmd = 0.0,
|
||||
radiation = types.new_zero_radiation_reading(),
|
||||
sna_prod_rate = 0.0,
|
||||
|
||||
sna_peak_rate = 0.0,
|
||||
sna_max_rate = 0.0,
|
||||
sna_out_rate = 0.0,
|
||||
|
||||
waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
|
||||
waste_product = types.WASTE_PRODUCT.PLUTONIUM,
|
||||
@@ -260,18 +283,18 @@ function iocontrol.init(conf, comms)
|
||||
|
||||
---@type alarms
|
||||
alarms = {
|
||||
ALARM_STATE.INACTIVE, -- containment breach
|
||||
ALARM_STATE.INACTIVE, -- containment radiation
|
||||
ALARM_STATE.INACTIVE, -- reactor lost
|
||||
ALARM_STATE.INACTIVE, -- damage critical
|
||||
ALARM_STATE.INACTIVE, -- reactor taking damage
|
||||
ALARM_STATE.INACTIVE, -- reactor over temperature
|
||||
ALARM_STATE.INACTIVE, -- reactor high temperature
|
||||
ALARM_STATE.INACTIVE, -- waste leak
|
||||
ALARM_STATE.INACTIVE, -- waste level high
|
||||
ALARM_STATE.INACTIVE, -- RPS transient
|
||||
ALARM_STATE.INACTIVE, -- RCS transient
|
||||
ALARM_STATE.INACTIVE -- turbine trip
|
||||
ALARM_STATE.INACTIVE, -- containment breach
|
||||
ALARM_STATE.INACTIVE, -- containment radiation
|
||||
ALARM_STATE.INACTIVE, -- reactor lost
|
||||
ALARM_STATE.INACTIVE, -- damage critical
|
||||
ALARM_STATE.INACTIVE, -- reactor taking damage
|
||||
ALARM_STATE.INACTIVE, -- reactor over temperature
|
||||
ALARM_STATE.INACTIVE, -- reactor high temperature
|
||||
ALARM_STATE.INACTIVE, -- waste leak
|
||||
ALARM_STATE.INACTIVE, -- waste level high
|
||||
ALARM_STATE.INACTIVE, -- RPS transient
|
||||
ALARM_STATE.INACTIVE, -- RCS transient
|
||||
ALARM_STATE.INACTIVE -- turbine trip
|
||||
},
|
||||
|
||||
annunciator = {}, ---@type annunciator
|
||||
@@ -295,15 +318,17 @@ function iocontrol.init(conf, comms)
|
||||
end
|
||||
|
||||
-- create boiler tables
|
||||
for _ = 1, conf.cooling.r_cool[i].BOILERS do
|
||||
for _ = 1, conf.cooling.r_cool[i].BoilerCount do
|
||||
table.insert(entry.boiler_ps_tbl, psil.create())
|
||||
table.insert(entry.boiler_data_tbl, {})
|
||||
table.insert(entry.rtu_hw.boilers, { connected = false, faulted = false })
|
||||
end
|
||||
|
||||
-- create turbine tables
|
||||
for _ = 1, conf.cooling.r_cool[i].TURBINES do
|
||||
for _ = 1, conf.cooling.r_cool[i].TurbineCount do
|
||||
table.insert(entry.turbine_ps_tbl, psil.create())
|
||||
table.insert(entry.turbine_data_tbl, {})
|
||||
table.insert(entry.rtu_hw.turbines, { connected = false, faulted = false })
|
||||
end
|
||||
|
||||
-- create tank tables
|
||||
@@ -357,6 +382,13 @@ function iocontrol.fp_monitor_state(id, connected)
|
||||
end
|
||||
end
|
||||
|
||||
-- report thread (routine) statuses
|
||||
---@param thread string thread name
|
||||
---@param ok boolean thread state
|
||||
function iocontrol.fp_rt_status(thread, ok)
|
||||
io.fp.ps.publish(util.c("routine__", thread), ok)
|
||||
end
|
||||
|
||||
-- report PKT firmware version and PKT session connection state
|
||||
---@param session_id integer PKT session
|
||||
---@param fw string firmware version
|
||||
@@ -384,7 +416,7 @@ function iocontrol.fp_pkt_rtt(session_id, rtt)
|
||||
elseif rtt > WARN_RTT then
|
||||
io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.yellow_hc)
|
||||
else
|
||||
io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.green)
|
||||
io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.green_hc)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -469,7 +501,7 @@ function iocontrol.record_unit_builds(builds)
|
||||
|
||||
-- note: if not all units and RTUs are connected, some will be nil
|
||||
for id, build in pairs(builds) do
|
||||
local unit = io.units[id] ---@type ioctl_unit
|
||||
local unit = io.units[id] ---@type ioctl_unit
|
||||
|
||||
local log_header = util.c("iocontrol.record_unit_builds[UNIT ", id, "]: ")
|
||||
|
||||
@@ -566,7 +598,7 @@ function iocontrol.update_facility_status(status)
|
||||
|
||||
local ctl_status = status[1]
|
||||
|
||||
if type(ctl_status) == "table" and #ctl_status == 16 then
|
||||
if type(ctl_status) == "table" and #ctl_status == 17 then
|
||||
fac.all_sys_ok = ctl_status[1]
|
||||
fac.auto_ready = ctl_status[2]
|
||||
|
||||
@@ -617,9 +649,11 @@ function iocontrol.update_facility_status(status)
|
||||
|
||||
fac.auto_current_waste_product = ctl_status[15]
|
||||
fac.auto_pu_fallback_active = ctl_status[16]
|
||||
fac.auto_sps_disabled = ctl_status[17]
|
||||
|
||||
fac.ps.publish("current_waste_product", fac.auto_current_waste_product)
|
||||
fac.ps.publish("pu_fallback_active", fac.auto_pu_fallback_active)
|
||||
fac.ps.publish("sps_disabled_low_power", fac.auto_sps_disabled)
|
||||
else
|
||||
log.debug(log_header .. "control status not a table or length mismatch")
|
||||
valid = false
|
||||
@@ -636,10 +670,27 @@ function iocontrol.update_facility_status(status)
|
||||
fac.rtu_count = rtu_statuses.count
|
||||
|
||||
-- power statistics
|
||||
if type(rtu_statuses.power) == "table" then
|
||||
fac.induction_ps_tbl[1].publish("avg_charge", rtu_statuses.power[1])
|
||||
fac.induction_ps_tbl[1].publish("avg_inflow", rtu_statuses.power[2])
|
||||
fac.induction_ps_tbl[1].publish("avg_outflow", rtu_statuses.power[3])
|
||||
if type(rtu_statuses.power) == "table" and #rtu_statuses.power == 4 then
|
||||
local data = fac.induction_data_tbl[1] ---@type imatrix_session_db
|
||||
local ps = fac.induction_ps_tbl[1] ---@type psil
|
||||
|
||||
local chg = tonumber(rtu_statuses.power[1])
|
||||
local in_f = tonumber(rtu_statuses.power[2])
|
||||
local out_f = tonumber(rtu_statuses.power[3])
|
||||
local eta = tonumber(rtu_statuses.power[4])
|
||||
|
||||
ps.publish("avg_charge", chg)
|
||||
ps.publish("avg_inflow", in_f)
|
||||
ps.publish("avg_outflow", out_f)
|
||||
ps.publish("eta_ms", eta)
|
||||
|
||||
ps.publish("is_charging", in_f > out_f)
|
||||
ps.publish("is_discharging", out_f > in_f)
|
||||
|
||||
if data and data.build then
|
||||
local cap = util.joules_to_fe(data.build.transfer_cap)
|
||||
ps.publish("at_max_io", in_f >= cap or out_f >= cap)
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "power statistics list not a table")
|
||||
valid = false
|
||||
@@ -694,8 +745,8 @@ function iocontrol.update_facility_status(status)
|
||||
|
||||
for id, sps in pairs(rtu_statuses.sps) do
|
||||
if type(fac.sps_data_tbl[id]) == "table" then
|
||||
local data = fac.sps_data_tbl[id] ---@type sps_session_db
|
||||
local ps = fac.sps_ps_tbl[id] ---@type psil
|
||||
local data = fac.sps_data_tbl[id] ---@type sps_session_db
|
||||
local ps = fac.sps_ps_tbl[id] ---@type psil
|
||||
|
||||
local rtu_faulted = _record_multiblock_status(sps, data, ps)
|
||||
|
||||
@@ -732,8 +783,8 @@ function iocontrol.update_facility_status(status)
|
||||
|
||||
for id, tank in pairs(rtu_statuses.tanks) do
|
||||
if type(fac.tank_data_tbl[id]) == "table" then
|
||||
local data = fac.tank_data_tbl[id] ---@type dynamicv_session_db
|
||||
local ps = fac.tank_ps_tbl[id] ---@type psil
|
||||
local data = fac.tank_data_tbl[id] ---@type dynamicv_session_db
|
||||
local ps = fac.tank_ps_tbl[id] ---@type psil
|
||||
|
||||
local rtu_faulted = _record_multiblock_status(tank, data, ps)
|
||||
|
||||
@@ -760,20 +811,34 @@ function iocontrol.update_facility_status(status)
|
||||
end
|
||||
|
||||
-- environment detector status
|
||||
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
|
||||
fac.radiation = rad_mon[2] ---@type number
|
||||
if type(rtu_statuses.envds) == "table" then
|
||||
local max_rad, max_reading, any_conn, any_faulted = 0, types.new_zero_radiation_reading(), false, false
|
||||
|
||||
fac.ps.publish("rad_computed_status", util.trinary(rtu_faulted, 2, 3))
|
||||
fac.ps.publish("radiation", fac.radiation)
|
||||
for _, envd in pairs(rtu_statuses.envds) do
|
||||
local rtu_faulted = envd[1] ---@type boolean
|
||||
local radiation = envd[2] ---@type radiation_reading
|
||||
local rad_raw = envd[3] ---@type number
|
||||
|
||||
any_conn = true
|
||||
any_faulted = any_faulted or rtu_faulted
|
||||
|
||||
if rad_raw > max_rad then
|
||||
max_rad = rad_raw
|
||||
max_reading = radiation
|
||||
end
|
||||
end
|
||||
|
||||
if any_conn then
|
||||
fac.radiation = max_reading
|
||||
fac.ps.publish("rad_computed_status", util.trinary(any_faulted, 2, 3))
|
||||
else
|
||||
fac.radiation = types.new_zero_radiation_reading()
|
||||
fac.ps.publish("rad_computed_status", 1)
|
||||
end
|
||||
|
||||
fac.ps.publish("radiation", fac.radiation)
|
||||
else
|
||||
log.debug(log_header .. "radiation monitor list not a table")
|
||||
log.debug(log_header .. "environment detector list not a table")
|
||||
valid = false
|
||||
end
|
||||
else
|
||||
@@ -836,6 +901,7 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
end
|
||||
|
||||
if #reactor_status == 0 then
|
||||
unit.connected = false
|
||||
unit.unit_ps.publish("computed_status", 1) -- disconnected
|
||||
elseif #reactor_status == 3 then
|
||||
local mek_status = reactor_status[1]
|
||||
@@ -895,6 +961,8 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
unit.unit_ps.publish(key, val)
|
||||
end
|
||||
end
|
||||
|
||||
unit.connected = true
|
||||
else
|
||||
log.debug(log_header .. "reactor status length mismatch")
|
||||
valid = false
|
||||
@@ -909,7 +977,10 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
local boil_sum = 0
|
||||
|
||||
for id = 1, #unit.boiler_ps_tbl do
|
||||
if rtu_statuses.boilers[i] == nil then
|
||||
local connected = rtu_statuses.boilers[id] ~= nil
|
||||
unit.rtu_hw.boilers[id].connected = connected
|
||||
|
||||
if not connected then
|
||||
-- disconnected
|
||||
unit.boiler_ps_tbl[id].publish("computed_status", 1)
|
||||
end
|
||||
@@ -917,10 +988,11 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
|
||||
for id, boiler in pairs(rtu_statuses.boilers) do
|
||||
if type(unit.boiler_data_tbl[id]) == "table" then
|
||||
local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
|
||||
local ps = unit.boiler_ps_tbl[id] ---@type psil
|
||||
local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
|
||||
local ps = unit.boiler_ps_tbl[id] ---@type psil
|
||||
|
||||
local rtu_faulted = _record_multiblock_status(boiler, data, ps)
|
||||
unit.rtu_hw.boilers[id].faulted = rtu_faulted
|
||||
|
||||
if rtu_faulted then
|
||||
ps.publish("computed_status", 3) -- faulted
|
||||
@@ -952,7 +1024,10 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
local flow_sum = 0
|
||||
|
||||
for id = 1, #unit.turbine_ps_tbl do
|
||||
if rtu_statuses.turbines[i] == nil then
|
||||
local connected = rtu_statuses.turbines[id] ~= nil
|
||||
unit.rtu_hw.turbines[id].connected = connected
|
||||
|
||||
if not connected then
|
||||
-- disconnected
|
||||
unit.turbine_ps_tbl[id].publish("computed_status", 1)
|
||||
end
|
||||
@@ -960,10 +1035,11 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
|
||||
for id, turbine in pairs(rtu_statuses.turbines) do
|
||||
if type(unit.turbine_data_tbl[id]) == "table" then
|
||||
local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db
|
||||
local ps = unit.turbine_ps_tbl[id] ---@type psil
|
||||
local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db
|
||||
local ps = unit.turbine_ps_tbl[id] ---@type psil
|
||||
|
||||
local rtu_faulted = _record_multiblock_status(turbine, data, ps)
|
||||
unit.rtu_hw.turbines[id].faulted = rtu_faulted
|
||||
|
||||
if rtu_faulted then
|
||||
ps.publish("computed_status", 3) -- faulted
|
||||
@@ -995,7 +1071,7 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
-- dynamic tank statuses
|
||||
if type(rtu_statuses.tanks) == "table" then
|
||||
for id = 1, #unit.tank_ps_tbl do
|
||||
if rtu_statuses.tanks[i] == nil then
|
||||
if rtu_statuses.tanks[id] == nil then
|
||||
-- disconnected
|
||||
unit.tank_ps_tbl[id].publish("computed_status", 1)
|
||||
end
|
||||
@@ -1033,13 +1109,15 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
|
||||
-- solar neutron activator status info
|
||||
if type(rtu_statuses.sna) == "table" then
|
||||
unit.num_snas = rtu_statuses.sna[1] ---@type integer
|
||||
unit.sna_prod_rate = rtu_statuses.sna[2] ---@type number
|
||||
unit.sna_peak_rate = rtu_statuses.sna[3] ---@type number
|
||||
unit.num_snas = rtu_statuses.sna[1] ---@type integer
|
||||
unit.sna_peak_rate = rtu_statuses.sna[2] ---@type number
|
||||
unit.sna_max_rate = rtu_statuses.sna[3] ---@type number
|
||||
unit.sna_out_rate = rtu_statuses.sna[4] ---@type number
|
||||
|
||||
unit.unit_ps.publish("sna_count", unit.num_snas)
|
||||
unit.unit_ps.publish("sna_prod_rate", unit.sna_prod_rate)
|
||||
unit.unit_ps.publish("sna_peak_rate", unit.sna_peak_rate)
|
||||
unit.unit_ps.publish("sna_max_rate", unit.sna_max_rate)
|
||||
unit.unit_ps.publish("sna_out_rate", unit.sna_out_rate)
|
||||
|
||||
sna_count_sum = sna_count_sum + unit.num_snas
|
||||
else
|
||||
@@ -1048,16 +1126,28 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
end
|
||||
|
||||
-- environment detector status
|
||||
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
|
||||
if type(rtu_statuses.envds) == "table" then
|
||||
local max_rad, max_reading, any_conn = 0, types.new_zero_radiation_reading(), false
|
||||
|
||||
unit.unit_ps.publish("radiation", unit.radiation)
|
||||
for _, envd in pairs(rtu_statuses.envds) do
|
||||
local radiation = envd[2] ---@type radiation_reading
|
||||
local rad_raw = envd[3] ---@type number
|
||||
|
||||
any_conn = true
|
||||
|
||||
if rad_raw > max_rad then
|
||||
max_rad = rad_raw
|
||||
max_reading = radiation
|
||||
end
|
||||
end
|
||||
|
||||
if any_conn then
|
||||
unit.radiation = max_reading
|
||||
else
|
||||
unit.radiation = types.new_zero_radiation_reading()
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("radiation", unit.radiation)
|
||||
else
|
||||
log.debug(log_header .. "radiation monitor list not a table")
|
||||
valid = false
|
||||
@@ -1175,7 +1265,7 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
|
||||
local u_spent_rate = waste_rate
|
||||
local u_pu_rate = util.trinary(is_pu, waste_rate, 0.0)
|
||||
local u_po_rate = util.trinary(not is_pu, math.min(waste_rate, unit.sna_prod_rate), 0.0)
|
||||
local u_po_rate = unit.sna_out_rate
|
||||
|
||||
unit.unit_ps.publish("pu_rate", u_pu_rate)
|
||||
unit.unit_ps.publish("po_rate", u_po_rate)
|
||||
@@ -1183,14 +1273,15 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
unit.unit_ps.publish("sna_in", util.trinary(is_pu, 0, burn_rate))
|
||||
|
||||
if unit.waste_product == types.WASTE_PRODUCT.POLONIUM then
|
||||
u_spent_rate = u_po_rate
|
||||
unit.unit_ps.publish("po_pl_rate", u_po_rate)
|
||||
unit.unit_ps.publish("po_am_rate", 0)
|
||||
po_pl_rate = po_pl_rate + u_po_rate
|
||||
elseif unit.waste_product == types.WASTE_PRODUCT.ANTI_MATTER then
|
||||
u_spent_rate = 0
|
||||
unit.unit_ps.publish("po_pl_rate", 0)
|
||||
unit.unit_ps.publish("po_am_rate", u_po_rate)
|
||||
po_am_rate = po_am_rate + u_po_rate
|
||||
u_spent_rate = 0
|
||||
else
|
||||
unit.unit_ps.publish("po_pl_rate", 0)
|
||||
unit.unit_ps.publish("po_am_rate", 0)
|
||||
|
||||
@@ -19,15 +19,21 @@ local process = {}
|
||||
local self = {
|
||||
io = nil, ---@type ioctl
|
||||
comms = nil, ---@type coord_comms
|
||||
---@class coord_auto_config
|
||||
config = {
|
||||
mode = PROCESS.INACTIVE,
|
||||
burn_target = 0.0,
|
||||
charge_target = 0.0,
|
||||
gen_target = 0.0,
|
||||
limits = {},
|
||||
waste_product = PRODUCT.PLUTONIUM,
|
||||
pu_fallback = false
|
||||
---@class coord_control_states
|
||||
control_states = {
|
||||
---@class coord_auto_config
|
||||
process = {
|
||||
mode = PROCESS.INACTIVE,
|
||||
burn_target = 0.0,
|
||||
charge_target = 0.0,
|
||||
gen_target = 0.0,
|
||||
limits = {},
|
||||
waste_product = PRODUCT.PLUTONIUM,
|
||||
pu_fallback = false,
|
||||
sps_low_power = false
|
||||
},
|
||||
waste_modes = {},
|
||||
priority_groups = {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,63 +48,67 @@ function process.init(iocontrol, coord_comms)
|
||||
self.io = iocontrol
|
||||
self.comms = coord_comms
|
||||
|
||||
local ctl_proc = self.control_states.process
|
||||
|
||||
for i = 1, self.io.facility.num_units do
|
||||
self.config.limits[i] = 0.1
|
||||
ctl_proc.limits[i] = 0.1
|
||||
end
|
||||
|
||||
-- load settings
|
||||
if not settings.load("/coord.settings") then
|
||||
log.error("process.init(): failed to load coordinator settings file")
|
||||
end
|
||||
local ctrl_states = settings.get("ControlStates", {})
|
||||
local config = ctrl_states.process ---@type coord_auto_config
|
||||
|
||||
-- facility auto control configuration
|
||||
local config = settings.get("PROCESS") ---@type coord_auto_config|nil
|
||||
if type(config) == "table" then
|
||||
self.config.mode = config.mode
|
||||
self.config.burn_target = config.burn_target
|
||||
self.config.charge_target = config.charge_target
|
||||
self.config.gen_target = config.gen_target
|
||||
self.config.limits = config.limits
|
||||
self.config.waste_product = config.waste_product
|
||||
self.config.pu_fallback = config.pu_fallback
|
||||
ctl_proc.mode = config.mode
|
||||
ctl_proc.burn_target = config.burn_target
|
||||
ctl_proc.charge_target = config.charge_target
|
||||
ctl_proc.gen_target = config.gen_target
|
||||
ctl_proc.limits = config.limits
|
||||
ctl_proc.waste_product = config.waste_product
|
||||
ctl_proc.pu_fallback = config.pu_fallback
|
||||
ctl_proc.sps_low_power = config.sps_low_power
|
||||
|
||||
self.io.facility.ps.publish("process_mode", self.config.mode)
|
||||
self.io.facility.ps.publish("process_burn_target", self.config.burn_target)
|
||||
self.io.facility.ps.publish("process_charge_target", self.config.charge_target)
|
||||
self.io.facility.ps.publish("process_gen_target", self.config.gen_target)
|
||||
self.io.facility.ps.publish("process_waste_product", self.config.waste_product)
|
||||
self.io.facility.ps.publish("process_pu_fallback", self.config.pu_fallback)
|
||||
self.io.facility.ps.publish("process_mode", ctl_proc.mode)
|
||||
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
|
||||
self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target)
|
||||
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target)
|
||||
self.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
|
||||
self.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback)
|
||||
self.io.facility.ps.publish("process_sps_low_power", ctl_proc.sps_low_power)
|
||||
|
||||
for id = 1, math.min(#self.config.limits, self.io.facility.num_units) do
|
||||
for id = 1, math.min(#ctl_proc.limits, self.io.facility.num_units) do
|
||||
local unit = self.io.units[id] ---@type ioctl_unit
|
||||
unit.unit_ps.publish("burn_limit", self.config.limits[id])
|
||||
unit.unit_ps.publish("burn_limit", ctl_proc.limits[id])
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded auto control settings from coord.settings")
|
||||
log.info("PROCESS: loaded auto control settings")
|
||||
|
||||
-- notify supervisor of auto waste config
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, self.config.waste_product)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, self.config.pu_fallback)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, ctl_proc.waste_product)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, ctl_proc.pu_fallback)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_SPS_LP, ctl_proc.sps_low_power)
|
||||
end
|
||||
|
||||
-- unit waste states
|
||||
local waste_modes = settings.get("WASTE_MODES") ---@type table|nil
|
||||
local waste_modes = ctrl_states.waste_modes ---@type table|nil
|
||||
if type(waste_modes) == "table" then
|
||||
for id, mode in pairs(waste_modes) do
|
||||
self.control_states.waste_modes[id] = mode
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded unit waste mode settings from coord.settings")
|
||||
log.info("PROCESS: loaded unit waste mode settings")
|
||||
end
|
||||
|
||||
-- unit priority groups
|
||||
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil
|
||||
local prio_groups = ctrl_states.priority_groups ---@type table|nil
|
||||
if type(prio_groups) == "table" then
|
||||
for id, group in pairs(prio_groups) do
|
||||
self.control_states.priority_groups[id] = group
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group)
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded priority groups settings from coord.settings")
|
||||
log.info("PROCESS: loaded priority groups settings")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -155,15 +165,10 @@ function process.set_unit_waste(id, mode)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
|
||||
|
||||
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil
|
||||
self.control_states.waste_modes[id] = mode
|
||||
settings.set("ControlStates", self.control_states)
|
||||
|
||||
if type(waste_mode) ~= "table" then waste_mode = {} end
|
||||
|
||||
waste_mode[id] = mode
|
||||
|
||||
settings.set("WASTE_MODES", waste_mode)
|
||||
|
||||
if not settings.save("/coord.settings") then
|
||||
if not settings.save("/coordinator.settings") then
|
||||
log.error("process.set_unit_waste(): failed to save coordinator settings file")
|
||||
end
|
||||
end
|
||||
@@ -198,15 +203,10 @@ function process.set_group(unit_id, group_id)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, unit_id, group_id)
|
||||
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
|
||||
|
||||
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil
|
||||
self.control_states.priority_groups[unit_id] = group_id
|
||||
settings.set("ControlStates", self.control_states)
|
||||
|
||||
if type(prio_groups) ~= "table" then prio_groups = {} end
|
||||
|
||||
prio_groups[unit_id] = group_id
|
||||
|
||||
settings.set("PRIORITY_GROUPS", prio_groups)
|
||||
|
||||
if not settings.save("/coord.settings") then
|
||||
if not settings.save("/coordinator.settings") then
|
||||
log.error("process.set_group(): failed to save coordinator settings file")
|
||||
end
|
||||
end
|
||||
@@ -217,20 +217,14 @@ end
|
||||
|
||||
-- write auto process control to config file
|
||||
local function _write_auto_config()
|
||||
-- attempt to load settings
|
||||
if not settings.load("/coord.settings") then
|
||||
log.warning("process._write_auto_config(): failed to load coordinator settings file")
|
||||
end
|
||||
|
||||
-- save config
|
||||
settings.set("PROCESS", self.config)
|
||||
local saved = settings.save("/coord.settings")
|
||||
|
||||
settings.set("ControlStates", self.control_states)
|
||||
local saved = settings.save("/coordinator.settings")
|
||||
if not saved then
|
||||
log.warning("process._write_auto_config(): failed to save coordinator settings file")
|
||||
end
|
||||
|
||||
return not not saved
|
||||
return saved
|
||||
end
|
||||
|
||||
-- stop automatic process control
|
||||
@@ -241,7 +235,7 @@ end
|
||||
|
||||
-- start automatic process control
|
||||
function process.start_auto()
|
||||
self.comms.send_auto_start(self.config)
|
||||
self.comms.send_auto_start(self.control_states.process)
|
||||
log.debug("PROCESS: START AUTO CTL")
|
||||
end
|
||||
|
||||
@@ -253,7 +247,7 @@ function process.set_process_waste(product)
|
||||
log.debug(util.c("PROCESS: SET WASTE ", product))
|
||||
|
||||
-- update config table and save
|
||||
self.config.waste_product = product
|
||||
self.control_states.process.waste_product = product
|
||||
_write_auto_config()
|
||||
end
|
||||
|
||||
@@ -265,7 +259,19 @@ function process.set_pu_fallback(enabled)
|
||||
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
|
||||
|
||||
-- update config table and save
|
||||
self.config.pu_fallback = enabled
|
||||
self.control_states.process.pu_fallback = enabled
|
||||
_write_auto_config()
|
||||
end
|
||||
|
||||
-- set automatic process control SPS usage at low power
|
||||
---@param enabled boolean whether to enable SPS usage at low power
|
||||
function process.set_sps_low_power(enabled)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_SPS_LP, enabled)
|
||||
|
||||
log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled))
|
||||
|
||||
-- update config table and save
|
||||
self.control_states.process.sps_low_power = enabled
|
||||
_write_auto_config()
|
||||
end
|
||||
|
||||
@@ -279,11 +285,12 @@ function process.save(mode, burn_target, charge_target, gen_target, limits)
|
||||
log.debug("PROCESS: SAVE")
|
||||
|
||||
-- update config table
|
||||
self.config.mode = mode
|
||||
self.config.burn_target = burn_target
|
||||
self.config.charge_target = charge_target
|
||||
self.config.gen_target = gen_target
|
||||
self.config.limits = limits
|
||||
local ctl_proc = self.control_states.process
|
||||
ctl_proc.mode = mode
|
||||
ctl_proc.burn_target = burn_target
|
||||
ctl_proc.charge_target = charge_target
|
||||
ctl_proc.gen_target = gen_target
|
||||
ctl_proc.limits = limits
|
||||
|
||||
-- save config
|
||||
self.io.facility.save_cfg_ack(_write_auto_config())
|
||||
@@ -294,22 +301,23 @@ end
|
||||
function process.start_ack_handle(response)
|
||||
local ack = response[1]
|
||||
|
||||
self.config.mode = response[2]
|
||||
self.config.burn_target = response[3]
|
||||
self.config.charge_target = response[4]
|
||||
self.config.gen_target = response[5]
|
||||
local ctl_proc = self.control_states.process
|
||||
ctl_proc.mode = response[2]
|
||||
ctl_proc.burn_target = response[3]
|
||||
ctl_proc.charge_target = response[4]
|
||||
ctl_proc.gen_target = response[5]
|
||||
|
||||
for i = 1, math.min(#response[6], self.io.facility.num_units) do
|
||||
self.config.limits[i] = response[6][i]
|
||||
ctl_proc.limits[i] = response[6][i]
|
||||
|
||||
local unit = self.io.units[i] ---@type ioctl_unit
|
||||
unit.unit_ps.publish("burn_limit", self.config.limits[i])
|
||||
unit.unit_ps.publish("burn_limit", ctl_proc.limits[i])
|
||||
end
|
||||
|
||||
self.io.facility.ps.publish("process_mode", self.config.mode)
|
||||
self.io.facility.ps.publish("process_burn_target", self.config.burn_target)
|
||||
self.io.facility.ps.publish("process_charge_target", self.config.charge_target)
|
||||
self.io.facility.ps.publish("process_gen_target", self.config.gen_target)
|
||||
self.io.facility.ps.publish("process_mode", ctl_proc.mode)
|
||||
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
|
||||
self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target)
|
||||
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target)
|
||||
|
||||
self.io.facility.start_ack(ack)
|
||||
end
|
||||
@@ -317,14 +325,14 @@ end
|
||||
-- record waste product state after attempting to change it
|
||||
---@param response WASTE_PRODUCT supervisor waste product state
|
||||
function process.waste_ack_handle(response)
|
||||
self.config.waste_product = response
|
||||
self.control_states.process.waste_product = response
|
||||
self.io.facility.ps.publish("process_waste_product", response)
|
||||
end
|
||||
|
||||
-- record plutonium fallback state after attempting to change it
|
||||
---@param response boolean supervisor plutonium fallback state
|
||||
function process.pu_fb_ack_handle(response)
|
||||
self.config.pu_fallback = response
|
||||
self.control_states.process.pu_fallback = response
|
||||
self.io.facility.ps.publish("process_pu_fallback", response)
|
||||
end
|
||||
|
||||
|
||||
@@ -2,29 +2,33 @@
|
||||
-- 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 iocontrol = require("coordinator.iocontrol")
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
local style = require("coordinator.ui.style")
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
|
||||
local flow_view = require("coordinator.ui.layout.flow_view")
|
||||
local panel_view = require("coordinator.ui.layout.front_panel")
|
||||
local main_view = require("coordinator.ui.layout.main_view")
|
||||
local unit_view = require("coordinator.ui.layout.unit_view")
|
||||
local flow_view = require("coordinator.ui.layout.flow_view")
|
||||
local panel_view = require("coordinator.ui.layout.front_panel")
|
||||
local main_view = require("coordinator.ui.layout.main_view")
|
||||
local unit_view = require("coordinator.ui.layout.unit_view")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local flasher = require("graphics.flasher")
|
||||
local core = require("graphics.core")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
|
||||
local log_render = coordinator.log_render
|
||||
|
||||
---@class coord_renderer
|
||||
local renderer = {}
|
||||
|
||||
-- render engine
|
||||
local engine = {
|
||||
color_mode = 1, ---@type COLOR_MODE
|
||||
monitors = nil, ---@type monitors_struct|nil
|
||||
dmesg_window = nil, ---@type table|nil
|
||||
ui_ready = false,
|
||||
@@ -48,15 +52,34 @@ local function _init_display(monitor)
|
||||
monitor.setCursorPos(1, 1)
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.colors do
|
||||
monitor.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||
for i = 1, #style.theme.colors do
|
||||
monitor.setPaletteColor(style.theme.colors[i].c, style.theme.colors[i].hex)
|
||||
end
|
||||
|
||||
-- apply color mode
|
||||
local c_mode_overrides = style.theme.color_modes[engine.color_mode]
|
||||
for i = 1, #c_mode_overrides do
|
||||
monitor.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
|
||||
end
|
||||
end
|
||||
|
||||
-- disable the flow view
|
||||
---@param disable boolean
|
||||
function renderer.legacy_disable_flow_view(disable)
|
||||
engine.disable_flow_view = disable
|
||||
-- print out that the monitor is too small
|
||||
---@param monitor table monitor
|
||||
local function _print_too_small(monitor)
|
||||
monitor.setCursorPos(1, 1)
|
||||
monitor.setBackgroundColor(colors.black)
|
||||
monitor.setTextColor(colors.red)
|
||||
monitor.clear()
|
||||
monitor.write("monitor too small")
|
||||
end
|
||||
|
||||
-- apply renderer configurations
|
||||
---@param config crd_config
|
||||
function renderer.configure(config)
|
||||
style.set_themes(config.MainTheme, config.FrontPanelTheme, config.ColorMode)
|
||||
|
||||
engine.color_mode = config.ColorMode
|
||||
engine.disable_flow_view = config.DisableFlowView
|
||||
end
|
||||
|
||||
-- link to the monitor peripherals
|
||||
@@ -65,15 +88,15 @@ function renderer.set_displays(monitors)
|
||||
engine.monitors = monitors
|
||||
|
||||
-- report to front panel as connected
|
||||
iocontrol.fp_monitor_state("main", engine.monitors.primary ~= nil)
|
||||
iocontrol.fp_monitor_state("main", engine.monitors.main ~= nil)
|
||||
iocontrol.fp_monitor_state("flow", engine.monitors.flow ~= nil)
|
||||
for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end
|
||||
end
|
||||
|
||||
-- init all displays in use by the renderer
|
||||
function renderer.init_displays()
|
||||
-- init primary and flow monitors
|
||||
_init_display(engine.monitors.primary)
|
||||
-- init main and flow monitors
|
||||
_init_display(engine.monitors.main)
|
||||
if not engine.disable_flow_view then _init_display(engine.monitors.flow) end
|
||||
|
||||
-- init unit displays
|
||||
@@ -88,48 +111,21 @@ function renderer.init_displays()
|
||||
term.setCursorPos(1, 1)
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.fp.colors do
|
||||
term.setPaletteColor(style.fp.colors[i].c, style.fp.colors[i].hex)
|
||||
end
|
||||
end
|
||||
|
||||
-- check main display width
|
||||
---@nodiscard
|
||||
---@return boolean width_okay
|
||||
function renderer.validate_main_display_width()
|
||||
local w, _ = engine.monitors.primary.getSize()
|
||||
return w == 164
|
||||
end
|
||||
|
||||
-- check flow display width
|
||||
---@nodiscard
|
||||
---@return boolean width_okay
|
||||
function renderer.validate_flow_display_width()
|
||||
local w, _ = engine.monitors.flow.getSize()
|
||||
return w == 164
|
||||
end
|
||||
|
||||
-- check display sizes
|
||||
---@nodiscard
|
||||
---@return boolean valid all unit display dimensions OK
|
||||
function renderer.validate_unit_display_sizes()
|
||||
local valid = true
|
||||
|
||||
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))
|
||||
valid = false
|
||||
end
|
||||
for i = 1, #style.fp_theme.colors do
|
||||
term.setPaletteColor(style.fp_theme.colors[i].c, style.fp_theme.colors[i].hex)
|
||||
end
|
||||
|
||||
return valid
|
||||
-- apply color mode
|
||||
local c_mode_overrides = style.fp_theme.color_modes[engine.color_mode]
|
||||
for i = 1, #c_mode_overrides do
|
||||
term.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
|
||||
end
|
||||
end
|
||||
|
||||
-- initialize the dmesg output window
|
||||
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)
|
||||
local disp_w, disp_h = engine.monitors.main.getSize()
|
||||
engine.dmesg_window = window.create(engine.monitors.main, 1, 1, disp_w, disp_h)
|
||||
log.direct_dmesg(engine.dmesg_window)
|
||||
end
|
||||
|
||||
@@ -176,9 +172,9 @@ function renderer.close_fp()
|
||||
engine.fp_ready = false
|
||||
|
||||
-- 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)
|
||||
for i = 1, #style.fp_theme.colors do
|
||||
local r, g, b = term.nativePaletteColor(style.fp_theme.colors[i].c)
|
||||
term.setPaletteColor(style.fp_theme.colors[i].c, r, g, b)
|
||||
end
|
||||
|
||||
-- reset terminal
|
||||
@@ -200,21 +196,24 @@ function renderer.try_start_ui()
|
||||
|
||||
status, msg = pcall(function ()
|
||||
-- show main view on main monitor
|
||||
if engine.monitors.primary ~= nil then
|
||||
engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root}
|
||||
if engine.monitors.main ~= nil then
|
||||
engine.ui.main_display = DisplayBox{window=engine.monitors.main,fg_bg=style.root}
|
||||
main_view(engine.ui.main_display)
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- show flow view on flow monitor
|
||||
if engine.monitors.flow ~= nil then
|
||||
engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root}
|
||||
flow_view(engine.ui.flow_display)
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- show unit views on unit displays
|
||||
for idx, display in pairs(engine.monitors.unit_displays) do
|
||||
engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
|
||||
unit_view(engine.ui.unit_displays[idx], idx)
|
||||
util.nop()
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -255,6 +254,11 @@ function renderer.close_ui()
|
||||
-- clear unit monitors
|
||||
for _, monitor in ipairs(engine.monitors.unit_displays) do monitor.clear() end
|
||||
|
||||
if not engine.disable_flow_view then
|
||||
-- clear flow monitor
|
||||
engine.monitors.flow.clear()
|
||||
end
|
||||
|
||||
-- re-draw dmesg
|
||||
engine.dmesg_window.setVisible(true)
|
||||
engine.dmesg_window.redraw()
|
||||
@@ -276,43 +280,43 @@ function renderer.ui_ready() return engine.ui_ready end
|
||||
function renderer.handle_disconnect(device)
|
||||
local is_used = false
|
||||
|
||||
if engine.monitors ~= nil then
|
||||
if engine.monitors.primary == device then
|
||||
if engine.ui.main_display ~= nil then
|
||||
-- delete element tree and clear root UI elements
|
||||
engine.ui.main_display.delete()
|
||||
end
|
||||
if not engine.monitors then return false end
|
||||
|
||||
is_used = true
|
||||
engine.monitors.primary = nil
|
||||
engine.ui.main_display = nil
|
||||
if engine.monitors.main == device then
|
||||
if engine.ui.main_display ~= nil then
|
||||
-- delete element tree and clear root UI elements
|
||||
engine.ui.main_display.delete()
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state("main", false)
|
||||
elseif engine.monitors.flow == device then
|
||||
if engine.ui.flow_display ~= nil then
|
||||
-- delete element tree and clear root UI elements
|
||||
engine.ui.flow_display.delete()
|
||||
end
|
||||
is_used = true
|
||||
engine.monitors.main = nil
|
||||
engine.ui.main_display = nil
|
||||
|
||||
is_used = true
|
||||
engine.monitors.flow = nil
|
||||
engine.ui.flow_display = nil
|
||||
iocontrol.fp_monitor_state("main", false)
|
||||
elseif engine.monitors.flow == device then
|
||||
if engine.ui.flow_display ~= nil then
|
||||
-- delete element tree and clear root UI elements
|
||||
engine.ui.flow_display.delete()
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state("flow", false)
|
||||
else
|
||||
for idx, monitor in pairs(engine.monitors.unit_displays) do
|
||||
if monitor == device then
|
||||
if engine.ui.unit_displays[idx] ~= nil then
|
||||
engine.ui.unit_displays[idx].delete()
|
||||
end
|
||||
is_used = true
|
||||
engine.monitors.flow = nil
|
||||
engine.ui.flow_display = nil
|
||||
|
||||
is_used = true
|
||||
engine.monitors.unit_displays[idx] = nil
|
||||
engine.ui.unit_displays[idx] = nil
|
||||
|
||||
iocontrol.fp_monitor_state(idx, false)
|
||||
break
|
||||
iocontrol.fp_monitor_state("flow", false)
|
||||
else
|
||||
for idx, monitor in pairs(engine.monitors.unit_displays) do
|
||||
if monitor == device then
|
||||
if engine.ui.unit_displays[idx] ~= nil then
|
||||
engine.ui.unit_displays[idx].delete()
|
||||
end
|
||||
|
||||
is_used = true
|
||||
engine.monitors.unit_displays[idx] = nil
|
||||
engine.ui.unit_displays[idx] = nil
|
||||
|
||||
iocontrol.fp_monitor_state(idx, false)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -327,52 +331,29 @@ end
|
||||
function renderer.handle_reconnect(name, device)
|
||||
local is_used = false
|
||||
|
||||
if engine.monitors ~= nil then
|
||||
if engine.monitors.primary_name == name then
|
||||
is_used = true
|
||||
_init_display(device)
|
||||
engine.monitors.primary = device
|
||||
if not engine.monitors then return false end
|
||||
|
||||
local disp_x, disp_y = engine.monitors.primary.getSize()
|
||||
engine.dmesg_window.reposition(1, 1, disp_x, disp_y, engine.monitors.primary)
|
||||
-- note: handle_resize is a more adaptive way of re-initializing a connected monitor
|
||||
-- since it can handle a monitor being reconnected that isn't the right size
|
||||
|
||||
if engine.ui_ready and (engine.ui.main_display == nil) then
|
||||
engine.dmesg_window.setVisible(false)
|
||||
if engine.monitors.main_name == name then
|
||||
is_used = true
|
||||
engine.monitors.main = device
|
||||
|
||||
engine.ui.main_display = DisplayBox{window=device,fg_bg=style.root}
|
||||
main_view(engine.ui.main_display)
|
||||
else
|
||||
engine.dmesg_window.setVisible(true)
|
||||
engine.dmesg_window.redraw()
|
||||
end
|
||||
renderer.handle_resize(name)
|
||||
elseif engine.monitors.flow_name == name then
|
||||
is_used = true
|
||||
engine.monitors.flow = device
|
||||
|
||||
iocontrol.fp_monitor_state("main", true)
|
||||
elseif engine.monitors.flow_name == name then
|
||||
is_used = true
|
||||
_init_display(device)
|
||||
engine.monitors.flow = device
|
||||
renderer.handle_resize(name)
|
||||
else
|
||||
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
if monitor == name then
|
||||
is_used = true
|
||||
engine.monitors.unit_displays[idx] = device
|
||||
|
||||
if engine.ui_ready and (engine.ui.flow_display == nil) then
|
||||
engine.ui.flow_display = DisplayBox{window=device,fg_bg=style.root}
|
||||
flow_view(engine.ui.flow_display)
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state("flow", true)
|
||||
else
|
||||
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
if monitor == name then
|
||||
is_used = true
|
||||
_init_display(device)
|
||||
engine.monitors.unit_displays[idx] = device
|
||||
|
||||
if engine.ui_ready and (engine.ui.unit_displays[idx] == nil) then
|
||||
engine.ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
|
||||
unit_view(engine.ui.unit_displays[idx], idx)
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state(idx, true)
|
||||
break
|
||||
end
|
||||
renderer.handle_resize(name)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -380,6 +361,142 @@ function renderer.handle_reconnect(name, device)
|
||||
return is_used
|
||||
end
|
||||
|
||||
-- handle a monitor being resized<br>
|
||||
-- returns if this monitor is assigned + if the assigned screen still fits
|
||||
---@param name string monitor name
|
||||
---@return boolean is_used, boolean is_ok
|
||||
function renderer.handle_resize(name)
|
||||
local is_used = false
|
||||
local is_ok = true
|
||||
local ui = engine.ui
|
||||
|
||||
if not engine.monitors then return false, false end
|
||||
|
||||
if engine.monitors.main_name == name and engine.monitors.main then
|
||||
local device = engine.monitors.main ---@type table
|
||||
|
||||
-- this is necessary if the bottom left block was broken and on reconnect
|
||||
_init_display(device)
|
||||
|
||||
is_used = true
|
||||
|
||||
-- resize dmesg window if needed, but don't make it thinner
|
||||
local disp_w, disp_h = engine.monitors.main.getSize()
|
||||
local dmsg_w, _ = engine.dmesg_window.getSize()
|
||||
engine.dmesg_window.reposition(1, 1, math.max(disp_w, dmsg_w), disp_h, engine.monitors.main)
|
||||
|
||||
if ui.main_display then
|
||||
ui.main_display.delete()
|
||||
ui.main_display = nil
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state("main", true)
|
||||
|
||||
engine.dmesg_window.setVisible(not engine.ui_ready)
|
||||
|
||||
if engine.ui_ready then
|
||||
local draw_start = util.time_ms()
|
||||
local ok = pcall(function ()
|
||||
ui.main_display = DisplayBox{window=device,fg_bg=style.root}
|
||||
main_view(ui.main_display)
|
||||
end)
|
||||
|
||||
if ok then
|
||||
log_render("main view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
|
||||
else
|
||||
if ui.main_display then
|
||||
ui.main_display.delete()
|
||||
ui.main_display = nil
|
||||
end
|
||||
|
||||
_print_too_small(device)
|
||||
|
||||
iocontrol.fp_monitor_state("main", false)
|
||||
is_ok = false
|
||||
end
|
||||
else engine.dmesg_window.redraw() end
|
||||
elseif engine.monitors.flow_name == name and engine.monitors.flow then
|
||||
local device = engine.monitors.flow ---@type table
|
||||
|
||||
-- this is necessary if the bottom left block was broken and on reconnect
|
||||
_init_display(device)
|
||||
|
||||
is_used = true
|
||||
|
||||
if ui.flow_display then
|
||||
ui.flow_display.delete()
|
||||
ui.flow_display = nil
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state("flow", true)
|
||||
|
||||
if engine.ui_ready then
|
||||
local draw_start = util.time_ms()
|
||||
local ok = pcall(function ()
|
||||
ui.flow_display = DisplayBox{window=device,fg_bg=style.root}
|
||||
flow_view(ui.flow_display)
|
||||
end)
|
||||
|
||||
if ok then
|
||||
log_render("flow view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
|
||||
else
|
||||
if ui.flow_display then
|
||||
ui.flow_display.delete()
|
||||
ui.flow_display = nil
|
||||
end
|
||||
|
||||
_print_too_small(device)
|
||||
|
||||
iocontrol.fp_monitor_state("flow", false)
|
||||
is_ok = false
|
||||
end
|
||||
end
|
||||
else
|
||||
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
local device = engine.monitors.unit_displays[idx]
|
||||
|
||||
if monitor == name and device then
|
||||
-- this is necessary if the bottom left block was broken and on reconnect
|
||||
_init_display(device)
|
||||
|
||||
is_used = true
|
||||
|
||||
if ui.unit_displays[idx] then
|
||||
ui.unit_displays[idx].delete()
|
||||
ui.unit_displays[idx] = nil
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state(idx, true)
|
||||
|
||||
if engine.ui_ready then
|
||||
local draw_start = util.time_ms()
|
||||
local ok = pcall(function ()
|
||||
ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
|
||||
unit_view(ui.unit_displays[idx], idx)
|
||||
end)
|
||||
|
||||
if ok then
|
||||
log_render("unit " .. idx .. " view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
|
||||
else
|
||||
if ui.unit_displays[idx] then
|
||||
ui.unit_displays[idx].delete()
|
||||
ui.unit_displays[idx] = nil
|
||||
end
|
||||
|
||||
_print_too_small(device)
|
||||
|
||||
iocontrol.fp_monitor_state(idx, false)
|
||||
is_ok = false
|
||||
end
|
||||
end
|
||||
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return is_used, is_ok
|
||||
end
|
||||
|
||||
-- handle a touch event
|
||||
---@param event mouse_interaction|nil
|
||||
@@ -388,16 +505,15 @@ function renderer.handle_mouse(event)
|
||||
if engine.fp_ready and event.monitor == "terminal" then
|
||||
engine.ui.front_panel.handle_mouse(event)
|
||||
elseif engine.ui_ready then
|
||||
if event.monitor == engine.monitors.primary_name then
|
||||
engine.ui.main_display.handle_mouse(event)
|
||||
if event.monitor == engine.monitors.main_name then
|
||||
if engine.ui.main_display then engine.ui.main_display.handle_mouse(event) end
|
||||
elseif event.monitor == engine.monitors.flow_name then
|
||||
engine.ui.flow_display.handle_mouse(event)
|
||||
if engine.ui.flow_display then engine.ui.flow_display.handle_mouse(event) end
|
||||
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)
|
||||
break
|
||||
local display = engine.ui.unit_displays[id]
|
||||
if event.monitor == monitor and display then
|
||||
if display then display.handle_mouse(event) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,6 @@ local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local config = require("coordinator.config")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local pocket = require("coordinator.session.pocket")
|
||||
@@ -11,7 +10,8 @@ local pocket = require("coordinator.session.pocket")
|
||||
local apisessions = {}
|
||||
|
||||
local self = {
|
||||
nic = nil,
|
||||
nic = nil, ---@type nic
|
||||
config = nil, ---@type crd_config
|
||||
next_id = 0,
|
||||
sessions = {}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ local function _api_handle_outq(session)
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- handle a packet to be sent
|
||||
self.nic.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message)
|
||||
self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message)
|
||||
elseif msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- handle instruction/notification
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
@@ -59,7 +59,7 @@ local function _shutdown(session)
|
||||
while session.out_queue.ready() do
|
||||
local msg = session.out_queue.pop()
|
||||
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then
|
||||
self.nic.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message)
|
||||
self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -69,9 +69,11 @@ end
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
-- initialize apisessions
|
||||
---@param nic nic
|
||||
function apisessions.init(nic)
|
||||
---@param nic nic network interface
|
||||
---@param config crd_config coordinator config
|
||||
function apisessions.init(nic, config)
|
||||
self.nic = nic
|
||||
self.config = config
|
||||
end
|
||||
|
||||
-- find a session by remote port
|
||||
@@ -103,7 +105,7 @@ function apisessions.establish_session(source_addr, version)
|
||||
|
||||
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)
|
||||
pkt_s.instance = pocket.new_session(id, source_addr, pkt_s.in_queue, pkt_s.out_queue, self.config.API_Timeout)
|
||||
table.insert(self.sessions, pkt_s)
|
||||
|
||||
local mt = {
|
||||
|
||||
@@ -8,7 +8,7 @@ local iocontrol = require("coordinator.iocontrol")
|
||||
local pocket = {}
|
||||
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
-- local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
|
||||
-- retry time constants in ms
|
||||
@@ -73,18 +73,18 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
end
|
||||
|
||||
-- send a CRDN packet
|
||||
-----@param msg_type CRDN_TYPE
|
||||
-----@param msg table
|
||||
-- local function _send(msg_type, msg)
|
||||
-- local s_pkt = comms.scada_packet()
|
||||
-- local c_pkt = comms.crdn_packet()
|
||||
---@param msg_type CRDN_TYPE
|
||||
---@param msg table
|
||||
local function _send(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local c_pkt = comms.crdn_packet()
|
||||
|
||||
-- c_pkt.make(msg_type, msg)
|
||||
-- s_pkt.make(self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable())
|
||||
c_pkt.make(msg_type, msg)
|
||||
s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable())
|
||||
|
||||
-- out_queue.push_packet(s_pkt)
|
||||
-- self.seq_num = self.seq_num + 1
|
||||
-- end
|
||||
out_queue.push_packet(s_pkt)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- send a SCADA management packet
|
||||
---@param msg_type MGMT_TYPE
|
||||
@@ -120,8 +120,44 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
if pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
---@cast pkt crdn_frame
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
-- handle packet by type
|
||||
if pkt.type == nil then
|
||||
if pkt.type == CRDN_TYPE.API_GET_FAC then
|
||||
local fac = db.facility
|
||||
|
||||
local data = {
|
||||
fac.all_sys_ok,
|
||||
fac.rtu_count,
|
||||
fac.radiation,
|
||||
{ fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated },
|
||||
{ fac.auto_current_waste_product, fac.auto_pu_fallback_active },
|
||||
util.table_len(fac.tank_data_tbl),
|
||||
fac.induction_data_tbl[1] ~= nil,
|
||||
fac.sps_data_tbl[1] ~= nil,
|
||||
}
|
||||
|
||||
_send(CRDN_TYPE.API_GET_FAC, data)
|
||||
elseif pkt.type == CRDN_TYPE.API_GET_UNIT then
|
||||
if pkt.length == 1 and type(pkt.data[1]) == "number" then
|
||||
local u = db.units[pkt.data[1]] ---@type ioctl_unit
|
||||
|
||||
if u then
|
||||
local data = {
|
||||
u.unit_id,
|
||||
u.connected,
|
||||
u.rtu_hw,
|
||||
u.alarms,
|
||||
u.annunciator,
|
||||
u.reactor_data,
|
||||
u.boiler_data_tbl,
|
||||
u.turbine_data_tbl,
|
||||
u.tank_data_tbl
|
||||
}
|
||||
|
||||
_send(CRDN_TYPE.API_GET_UNIT, data)
|
||||
end
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type)
|
||||
end
|
||||
|
||||
@@ -7,59 +7,92 @@ require("/initenv").init_env()
|
||||
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 network = require("scada-common.network")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local config = require("coordinator.config")
|
||||
local configure = require("coordinator.configure")
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
local threads = require("coordinator.threads")
|
||||
|
||||
local apisessions = require("coordinator.session.apisessions")
|
||||
local COORDINATOR_VERSION = "v1.4.6"
|
||||
|
||||
local COORDINATOR_VERSION = "v1.0.16"
|
||||
local CHUNK_LOAD_DELAY_S = 30.0
|
||||
|
||||
local println = util.println
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
|
||||
local log_graphics = coordinator.log_graphics
|
||||
local log_sys = coordinator.log_sys
|
||||
local log_boot = coordinator.log_boot
|
||||
local log_comms = coordinator.log_comms
|
||||
local log_render = coordinator.log_render
|
||||
local log_sys = coordinator.log_sys
|
||||
local log_boot = coordinator.log_boot
|
||||
local log_comms = coordinator.log_comms
|
||||
local log_crypto = coordinator.log_crypto
|
||||
|
||||
----------------------------------------
|
||||
-- config validation
|
||||
-- get configuration
|
||||
----------------------------------------
|
||||
|
||||
local cfv = util.new_validator()
|
||||
-- mount connected devices (required for monitor setup)
|
||||
ppm.mount_all()
|
||||
|
||||
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.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)
|
||||
cfv.assert_type_str(config.LOG_PATH)
|
||||
cfv.assert_type_int(config.LOG_MODE)
|
||||
local wait_on_load = true
|
||||
local loaded, monitors = coordinator.load_config()
|
||||
|
||||
assert(cfv.valid(), "bad config file: missing/invalid fields")
|
||||
-- if the computer just started, its chunk may have just loaded (...or the user rebooted)
|
||||
-- if monitor config failed, maybe an adjacent chunk containing all or part of a monitor has not loaded yet, so keep trying
|
||||
while wait_on_load and loaded == 2 and os.clock() < CHUNK_LOAD_DELAY_S do
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
println("There was a monitor configuration problem at boot.\n")
|
||||
println("Startup will keep trying every 2s in case of chunk load delays.\n")
|
||||
println(util.sprintf("The configurator will be started in %ds if all attempts fail.\n", math.max(0, CHUNK_LOAD_DELAY_S - os.clock())))
|
||||
println("(click to skip to the configurator)")
|
||||
|
||||
local timer_id = util.start_timer(2)
|
||||
|
||||
while true do
|
||||
local event, param1 = util.pull_event()
|
||||
if event == "timer" and param1 == timer_id then
|
||||
-- remount and re-attempt
|
||||
ppm.mount_all()
|
||||
loaded, monitors = coordinator.load_config()
|
||||
break
|
||||
elseif event == "mouse_click" or event == "terminate" then
|
||||
wait_on_load = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if loaded ~= 0 then
|
||||
-- try to reconfigure (user action)
|
||||
local success, error = configure.configure(loaded, monitors)
|
||||
if success then
|
||||
loaded, monitors = coordinator.load_config()
|
||||
if loaded ~= 0 then
|
||||
println(util.trinary(loaded == 2, "monitor configuration invalid", "failed to load a valid configuration") .. ", please reconfigure")
|
||||
return
|
||||
end
|
||||
else
|
||||
println("configuration error: " .. error)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- passed checks, good now
|
||||
---@cast monitors monitors_struct
|
||||
|
||||
local config = coordinator.config
|
||||
|
||||
----------------------------------------
|
||||
-- log init
|
||||
----------------------------------------
|
||||
|
||||
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
|
||||
log.init(config.LogPath, config.LogMode, config.LogDebug)
|
||||
|
||||
log.info("========================================")
|
||||
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
|
||||
@@ -67,6 +100,7 @@ log.info("========================================")
|
||||
println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<")
|
||||
|
||||
crash.set_env("coordinator", COORDINATOR_VERSION)
|
||||
crash.dbg_log_env()
|
||||
|
||||
----------------------------------------
|
||||
-- main application
|
||||
@@ -77,54 +111,73 @@ local function main()
|
||||
-- system startup
|
||||
----------------------------------------
|
||||
|
||||
-- mount connected devices
|
||||
ppm.mount_all()
|
||||
-- log mounts now since mounting was done before logging was ready
|
||||
ppm.log_mounts()
|
||||
|
||||
-- report versions/init fp PSIL
|
||||
iocontrol.init_fp(COORDINATOR_VERSION, comms.version)
|
||||
|
||||
-- setup monitors
|
||||
local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS, config.DISABLE_FLOW_VIEW == true)
|
||||
if not configured or monitors == nil then
|
||||
println("startup> monitor setup failed")
|
||||
log.fatal("monitor configuration failed")
|
||||
return
|
||||
end
|
||||
|
||||
-- init renderer
|
||||
renderer.legacy_disable_flow_view(config.DISABLE_FLOW_VIEW == true)
|
||||
renderer.configure(config)
|
||||
renderer.set_displays(monitors)
|
||||
renderer.init_displays()
|
||||
|
||||
if not renderer.validate_main_display_width() then
|
||||
println("startup> main display must be 8 blocks wide")
|
||||
log.fatal("main display not wide enough")
|
||||
return
|
||||
elseif (config.DISABLE_FLOW_VIEW ~= true) and not renderer.validate_flow_display_width() then
|
||||
println("startup> flow display must be 8 blocks wide")
|
||||
log.fatal("flow display not wide enough")
|
||||
return
|
||||
elseif not renderer.validate_unit_display_sizes() then
|
||||
println("startup> one or more unit display dimensions incorrect; they must be 4x4 blocks")
|
||||
log.fatal("unit display dimensions incorrect")
|
||||
return
|
||||
end
|
||||
|
||||
renderer.init_dmesg()
|
||||
|
||||
-- lets get started!
|
||||
log.info("monitors ready, dmesg output incoming...")
|
||||
|
||||
log_graphics("displays connected and reset")
|
||||
log_render("displays connected and reset")
|
||||
log_sys("system start on " .. os.date("%c"))
|
||||
log_boot("starting " .. COORDINATOR_VERSION)
|
||||
|
||||
----------------------------------------
|
||||
-- memory allocation
|
||||
----------------------------------------
|
||||
|
||||
-- shared memory across threads
|
||||
---@class crd_shared_memory
|
||||
local __shared_memory = {
|
||||
-- time and date format for display
|
||||
date_format = util.trinary(config.Time24Hour, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y"),
|
||||
|
||||
-- coordinator system state flags
|
||||
---@class crd_state
|
||||
crd_state = {
|
||||
fp_ok = false,
|
||||
ui_ok = true, -- default true, used to abort on fail
|
||||
link_fail = false,
|
||||
shutdown = false
|
||||
},
|
||||
|
||||
-- core coordinator devices
|
||||
crd_dev = {
|
||||
speaker = ppm.get_device("speaker"),
|
||||
modem = ppm.get_wireless_modem()
|
||||
},
|
||||
|
||||
-- system objects
|
||||
crd_sys = {
|
||||
nic = nil, ---@type nic
|
||||
coord_comms = nil, ---@type coord_comms
|
||||
conn_watchdog = nil ---@type watchdog
|
||||
},
|
||||
|
||||
-- message queues
|
||||
q = {
|
||||
mq_render = mqueue.new()
|
||||
}
|
||||
}
|
||||
|
||||
local smem_dev = __shared_memory.crd_dev
|
||||
local smem_sys = __shared_memory.crd_sys
|
||||
|
||||
local crd_state = __shared_memory.crd_state
|
||||
|
||||
----------------------------------------
|
||||
-- setup alarm sounder subsystem
|
||||
----------------------------------------
|
||||
|
||||
local speaker = ppm.get_device("speaker")
|
||||
if speaker == nil then
|
||||
if smem_dev.speaker == nil then
|
||||
log_boot("annunciator alarm speaker not found")
|
||||
println("startup> speaker not found")
|
||||
log.fatal("no annunciator alarm speaker found")
|
||||
@@ -132,7 +185,7 @@ local function main()
|
||||
else
|
||||
local sounder_start = util.time_ms()
|
||||
log_boot("annunciator alarm speaker connected")
|
||||
sounder.init(speaker, config.SOUNDER_VOLUME)
|
||||
sounder.init(smem_dev.speaker, config.SpeakerVolume)
|
||||
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
|
||||
log_sys("annunciator alarm configured")
|
||||
iocontrol.fp_has_speaker(true)
|
||||
@@ -143,14 +196,13 @@ local function main()
|
||||
----------------------------------------
|
||||
|
||||
-- message authentication init
|
||||
if type(config.AUTH_KEY) == "string" then
|
||||
local init_time = network.init_mac(config.AUTH_KEY)
|
||||
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
|
||||
local init_time = network.init_mac(config.AuthKey)
|
||||
log_crypto("HMAC init took " .. init_time .. "ms")
|
||||
end
|
||||
|
||||
-- get the communications modem
|
||||
local modem = ppm.get_wireless_modem()
|
||||
if modem == nil then
|
||||
if smem_dev.modem == nil then
|
||||
log_comms("wireless modem not found")
|
||||
println("startup> wireless modem not found")
|
||||
log.fatal("no wireless modem on startup")
|
||||
@@ -161,239 +213,54 @@ local function main()
|
||||
end
|
||||
|
||||
-- create connection watchdog
|
||||
local conn_watchdog = util.new_watchdog(config.SV_TIMEOUT)
|
||||
conn_watchdog.cancel()
|
||||
smem_sys.conn_watchdog = util.new_watchdog(config.SVR_Timeout)
|
||||
smem_sys.conn_watchdog.cancel()
|
||||
log.debug("startup> conn watchdog created")
|
||||
|
||||
-- create network interface then setup comms
|
||||
local nic = network.nic(modem)
|
||||
local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, config.NUM_UNITS, config.CRD_CHANNEL,
|
||||
config.SVR_CHANNEL, config.PKT_CHANNEL, config.TRUSTED_RANGE, conn_watchdog)
|
||||
smem_sys.nic = network.nic(smem_dev.modem)
|
||||
smem_sys.coord_comms = coordinator.comms(COORDINATOR_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
|
||||
log.debug("startup> comms init")
|
||||
log_comms("comms initialized")
|
||||
|
||||
-- base loop clock (2Hz, 10 ticks)
|
||||
local MAIN_CLOCK = 0.5
|
||||
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||
|
||||
----------------------------------------
|
||||
-- start front panel & UI start function
|
||||
-- start front panel
|
||||
----------------------------------------
|
||||
|
||||
log_graphics("starting front panel UI...")
|
||||
log_render("starting front panel UI...")
|
||||
|
||||
local fp_ok, fp_message = renderer.try_start_fp()
|
||||
if not fp_ok then
|
||||
log_graphics(util.c("front panel UI error: ", fp_message))
|
||||
local fp_message
|
||||
crd_state.fp_ok, fp_message = renderer.try_start_fp()
|
||||
if not crd_state.fp_ok then
|
||||
log_render(util.c("front panel UI error: ", fp_message))
|
||||
println_ts("front panel UI creation failed")
|
||||
log.fatal(util.c("front panel GUI render failed with error ", fp_message))
|
||||
return
|
||||
else log_graphics("front panel ready") end
|
||||
|
||||
-- start up the main UI
|
||||
---@return boolean ui_ok started ok
|
||||
local function start_main_ui()
|
||||
log_graphics("starting main UI...")
|
||||
|
||||
local draw_start = util.time_ms()
|
||||
|
||||
local ui_ok, ui_message = renderer.try_start_ui()
|
||||
if not ui_ok then
|
||||
log_graphics(util.c("main UI error: ", ui_message))
|
||||
log.fatal(util.c("main GUI render failed with error ", ui_message))
|
||||
else
|
||||
log_graphics("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
|
||||
end
|
||||
|
||||
return ui_ok
|
||||
end
|
||||
else log_render("front panel ready") end
|
||||
|
||||
----------------------------------------
|
||||
-- main event loop
|
||||
-- start system
|
||||
----------------------------------------
|
||||
|
||||
local link_failed = false
|
||||
local ui_ok = true
|
||||
local date_format = util.trinary(config.TIME_24_HOUR, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y")
|
||||
-- init threads
|
||||
local main_thread = threads.thread__main(__shared_memory)
|
||||
local render_thread = threads.thread__render(__shared_memory)
|
||||
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
log.info("startup> completed")
|
||||
|
||||
log_sys("system started successfully")
|
||||
|
||||
-- main event loop
|
||||
while true do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
if event == "peripheral_detach" then
|
||||
local type, device = ppm.handle_unmount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
-- we only really care if this is our wireless modem
|
||||
-- if it is another modem, handle other peripheral losses separately
|
||||
if nic.is_modem(device) then
|
||||
nic.disconnect()
|
||||
log_sys("comms modem disconnected")
|
||||
|
||||
local other_modem = ppm.get_wireless_modem()
|
||||
if other_modem then
|
||||
log_sys("found another wireless modem, using it for comms")
|
||||
nic.connect(other_modem)
|
||||
else
|
||||
-- close out main UI
|
||||
renderer.close_ui()
|
||||
|
||||
-- alert user to status
|
||||
log_sys("awaiting comms modem reconnect...")
|
||||
|
||||
iocontrol.fp_has_modem(false)
|
||||
end
|
||||
else
|
||||
log_sys("non-comms modem disconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
if renderer.handle_disconnect(device) then
|
||||
log_sys("lost a configured monitor")
|
||||
else
|
||||
log_sys("lost an unused monitor")
|
||||
end
|
||||
elseif type == "speaker" then
|
||||
log_sys("lost alarm sounder speaker")
|
||||
iocontrol.fp_has_speaker(false)
|
||||
end
|
||||
end
|
||||
elseif event == "peripheral" then
|
||||
local type, device = ppm.mount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
if device.isWireless() and not nic.is_connected() then
|
||||
-- reconnected modem
|
||||
log_sys("comms modem reconnected")
|
||||
nic.connect(device)
|
||||
iocontrol.fp_has_modem(true)
|
||||
elseif device.isWireless() then
|
||||
log.info("unused wireless modem reconnected")
|
||||
else
|
||||
log_sys("wired modem reconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
if renderer.handle_reconnect(param1, device) then
|
||||
log_sys(util.c("configured monitor ", param1, " reconnected"))
|
||||
else
|
||||
log_sys(util.c("unused monitor ", param1, " connected"))
|
||||
end
|
||||
elseif type == "speaker" then
|
||||
log_sys("alarm sounder speaker reconnected")
|
||||
sounder.reconnect(device)
|
||||
iocontrol.fp_has_speaker(true)
|
||||
end
|
||||
end
|
||||
elseif event == "timer" then
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
|
||||
-- toggle heartbeat
|
||||
iocontrol.heartbeat()
|
||||
|
||||
-- maintain connection
|
||||
if nic.is_connected() then
|
||||
local ok, start_ui = coord_comms.try_connect()
|
||||
if not ok then
|
||||
link_failed = true
|
||||
log_sys("supervisor connection failed, shutting down...")
|
||||
log.fatal("failed to connect to supervisor")
|
||||
break
|
||||
elseif start_ui then
|
||||
log_sys("supervisor connected, proceeding to main UI start")
|
||||
ui_ok = start_main_ui()
|
||||
if not ui_ok then break end
|
||||
end
|
||||
end
|
||||
|
||||
-- iterate sessions
|
||||
apisessions.iterate_all()
|
||||
|
||||
-- free any closed sessions
|
||||
apisessions.free_all_closed()
|
||||
|
||||
-- update date and time string for main display
|
||||
if coord_comms.is_linked() then
|
||||
iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format))
|
||||
end
|
||||
|
||||
loop_clock.start()
|
||||
elseif conn_watchdog.is_timer(param1) then
|
||||
-- supervisor watchdog timeout
|
||||
log_comms("supervisor server timeout")
|
||||
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
else
|
||||
-- a non-clock/main watchdog timer event
|
||||
|
||||
-- check API watchdogs
|
||||
apisessions.check_all_watchdogs(param1)
|
||||
|
||||
-- notify timer callback dispatcher
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
-- got a packet
|
||||
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
|
||||
-- handle then check if it was a disconnect
|
||||
if coord_comms.handle_packet(packet) then
|
||||
log_comms("supervisor closed connection")
|
||||
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
end
|
||||
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
|
||||
event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
|
||||
-- handle a mouse event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
elseif event == "speaker_audio_empty" then
|
||||
-- handle speaker buffer emptied
|
||||
sounder.continue()
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
-- handle supervisor connection
|
||||
coord_comms.try_connect(true)
|
||||
|
||||
if coord_comms.is_linked() then
|
||||
log_comms("terminate requested, closing supervisor connection...")
|
||||
else link_failed = true end
|
||||
|
||||
coord_comms.close()
|
||||
log_comms("supervisor connection closed")
|
||||
|
||||
-- handle API sessions
|
||||
log_comms("closing api sessions...")
|
||||
apisessions.close_all()
|
||||
log_comms("api sessions closed")
|
||||
break
|
||||
end
|
||||
end
|
||||
-- run threads
|
||||
parallel.waitForAll(main_thread.p_exec, render_thread.p_exec)
|
||||
|
||||
renderer.close_ui()
|
||||
renderer.close_fp()
|
||||
sounder.stop()
|
||||
log_sys("system shutdown")
|
||||
|
||||
if link_failed then println_ts("failed to connect to supervisor") end
|
||||
if not ui_ok then println_ts("main UI creation failed") end
|
||||
if crd_state.link_fail then println_ts("failed to connect to supervisor") end
|
||||
if not crd_state.ui_ok then println_ts("main UI creation failed") end
|
||||
|
||||
-- close on error exit (such as UI error)
|
||||
if coord_comms.is_linked() then coord_comms.close() end
|
||||
if smem_sys.coord_comms.is_linked() then smem_sys.coord_comms.close() end
|
||||
|
||||
println_ts("exited")
|
||||
log.info("exited")
|
||||
|
||||
363
coordinator/threads.lua
Normal file
363
coordinator/threads.lua
Normal file
@@ -0,0 +1,363 @@
|
||||
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 coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
|
||||
local apisessions = require("coordinator.session.apisessions")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local log_render = coordinator.log_render
|
||||
local log_sys = coordinator.log_sys
|
||||
local log_comms = coordinator.log_comms
|
||||
|
||||
local threads = {}
|
||||
|
||||
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
|
||||
local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
|
||||
|
||||
local MQ__RENDER_CMD = {
|
||||
START_MAIN_UI = 1
|
||||
}
|
||||
|
||||
local MQ__RENDER_DATA = {
|
||||
MON_CONNECT = 1,
|
||||
MON_DISCONNECT = 2,
|
||||
MON_RESIZE = 3
|
||||
}
|
||||
|
||||
-- main thread
|
||||
---@nodiscard
|
||||
---@param smem crd_shared_memory
|
||||
function threads.thread__main(smem)
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
iocontrol.fp_rt_status("main", true)
|
||||
log.debug("main thread start")
|
||||
|
||||
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
|
||||
log_sys("system started successfully")
|
||||
|
||||
-- load in from shared memory
|
||||
local crd_state = smem.crd_state
|
||||
local nic = smem.crd_sys.nic
|
||||
local coord_comms = smem.crd_sys.coord_comms
|
||||
local conn_watchdog = smem.crd_sys.conn_watchdog
|
||||
|
||||
-- event loop
|
||||
while true do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
if event == "peripheral_detach" then
|
||||
local type, device = ppm.handle_unmount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
-- we only really care if this is our wireless modem
|
||||
-- if it is another modem, handle other peripheral losses separately
|
||||
if nic.is_modem(device) then
|
||||
nic.disconnect()
|
||||
log_sys("comms modem disconnected")
|
||||
|
||||
local other_modem = ppm.get_wireless_modem()
|
||||
if other_modem then
|
||||
log_sys("found another wireless modem, using it for comms")
|
||||
nic.connect(other_modem)
|
||||
else
|
||||
-- close out main UI
|
||||
renderer.close_ui()
|
||||
|
||||
-- alert user to status
|
||||
log_sys("awaiting comms modem reconnect...")
|
||||
|
||||
iocontrol.fp_has_modem(false)
|
||||
end
|
||||
else
|
||||
log_sys("non-comms modem disconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, device)
|
||||
elseif type == "speaker" then
|
||||
log_sys("lost alarm sounder speaker")
|
||||
iocontrol.fp_has_speaker(false)
|
||||
end
|
||||
end
|
||||
elseif event == "peripheral" then
|
||||
local type, device = ppm.mount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
if device.isWireless() and not nic.is_connected() then
|
||||
-- reconnected modem
|
||||
log_sys("comms modem reconnected")
|
||||
nic.connect(device)
|
||||
iocontrol.fp_has_modem(true)
|
||||
elseif device.isWireless() then
|
||||
log.info("unused wireless modem reconnected")
|
||||
else
|
||||
log_sys("wired modem reconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, { name = param1, device = device })
|
||||
elseif type == "speaker" then
|
||||
log_sys("alarm sounder speaker reconnected")
|
||||
sounder.reconnect(device)
|
||||
iocontrol.fp_has_speaker(true)
|
||||
end
|
||||
end
|
||||
elseif event == "monitor_resize" then
|
||||
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_RESIZE, param1)
|
||||
elseif event == "timer" then
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
|
||||
-- toggle heartbeat
|
||||
iocontrol.heartbeat()
|
||||
|
||||
-- maintain connection
|
||||
if nic.is_connected() then
|
||||
local ok, start_ui = coord_comms.try_connect()
|
||||
if not ok then
|
||||
crd_state.link_fail = true
|
||||
crd_state.shutdown = true
|
||||
log_sys("supervisor connection failed, shutting down...")
|
||||
log.fatal("failed to connect to supervisor")
|
||||
break
|
||||
elseif start_ui then
|
||||
log_sys("supervisor connected, dispatching main UI start")
|
||||
smem.q.mq_render.push_command(MQ__RENDER_CMD.START_MAIN_UI)
|
||||
end
|
||||
end
|
||||
|
||||
-- iterate sessions and free any closed ones
|
||||
apisessions.iterate_all()
|
||||
apisessions.free_all_closed()
|
||||
|
||||
if renderer.ui_ready() then
|
||||
-- update clock used on main and flow monitors
|
||||
iocontrol.get_db().facility.ps.publish("date_time", os.date(smem.date_format))
|
||||
end
|
||||
|
||||
loop_clock.start()
|
||||
elseif conn_watchdog.is_timer(param1) then
|
||||
-- supervisor watchdog timeout
|
||||
log_comms("supervisor server timeout")
|
||||
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
else
|
||||
-- a non-clock/main watchdog timer event
|
||||
|
||||
-- check API watchdogs
|
||||
apisessions.check_all_watchdogs(param1)
|
||||
|
||||
-- notify timer callback dispatcher
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
-- got a packet
|
||||
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
|
||||
-- handle then check if it was a disconnect
|
||||
if coord_comms.handle_packet(packet) then
|
||||
log_comms("supervisor closed connection")
|
||||
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
end
|
||||
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
|
||||
event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
|
||||
-- handle a mouse event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
elseif event == "speaker_audio_empty" then
|
||||
-- handle speaker buffer emptied
|
||||
sounder.continue()
|
||||
end
|
||||
|
||||
-- check for termination request or UI crash
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
crd_state.shutdown = true
|
||||
log.info("terminate requested, main thread exiting")
|
||||
elseif not crd_state.ui_ok then
|
||||
crd_state.shutdown = true
|
||||
log.info("terminating due to fatal UI error")
|
||||
end
|
||||
|
||||
if crd_state.shutdown then
|
||||
-- handle closing supervisor connection
|
||||
coord_comms.try_connect(true)
|
||||
|
||||
if coord_comms.is_linked() then
|
||||
log_comms("closing supervisor connection...")
|
||||
else crd_state.link_fail = true end
|
||||
|
||||
coord_comms.close()
|
||||
log_comms("supervisor connection closed")
|
||||
|
||||
-- handle API sessions
|
||||
log_comms("closing api sessions...")
|
||||
apisessions.close_all()
|
||||
log_comms("api sessions closed")
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- execute the thread in a protected mode, retrying it on return if not shutting down
|
||||
function public.p_exec()
|
||||
local crd_state = smem.crd_state
|
||||
|
||||
while not crd_state.shutdown do
|
||||
local status, result = pcall(public.exec)
|
||||
if status == false then
|
||||
log.fatal(util.strval(result))
|
||||
end
|
||||
|
||||
iocontrol.fp_rt_status("main", false)
|
||||
|
||||
-- if status is true, then we are probably exiting, so this won't matter
|
||||
-- this thread cannot be slept because it will miss events (namely "terminate")
|
||||
if not crd_state.shutdown then
|
||||
log.info("main thread restarting now...")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- coordinator renderer thread, tasked with long duration re-draws
|
||||
---@nodiscard
|
||||
---@param smem crd_shared_memory
|
||||
function threads.thread__render(smem)
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
iocontrol.fp_rt_status("render", true)
|
||||
log.debug("render thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local crd_state = smem.crd_state
|
||||
local render_queue = smem.q.mq_render
|
||||
|
||||
local last_update = util.time()
|
||||
|
||||
-- thread loop
|
||||
while true do
|
||||
-- check for messages in the message queue
|
||||
while render_queue.ready() and not crd_state.shutdown do
|
||||
local msg = render_queue.pop()
|
||||
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- received a command
|
||||
if msg.message == MQ__RENDER_CMD.START_MAIN_UI then
|
||||
-- stop the UI if it was already started
|
||||
-- this may occur on a quick supervisor disconnect -> connect
|
||||
if renderer.ui_ready() then
|
||||
log_render("closing main UI before executing new request to start")
|
||||
renderer.close_ui()
|
||||
end
|
||||
|
||||
-- start up the main UI
|
||||
log_render("starting main UI...")
|
||||
|
||||
local draw_start = util.time_ms()
|
||||
|
||||
local ui_message
|
||||
crd_state.ui_ok, ui_message = renderer.try_start_ui()
|
||||
if not crd_state.ui_ok then
|
||||
log_render(util.c("main UI error: ", ui_message))
|
||||
log.fatal(util.c("main GUI render failed with error ", ui_message))
|
||||
else
|
||||
log_render("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
|
||||
end
|
||||
end
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
-- received data
|
||||
local cmd = msg.message ---@type queue_data
|
||||
|
||||
if cmd.key == MQ__RENDER_DATA.MON_CONNECT then
|
||||
-- monitor connected
|
||||
if renderer.handle_reconnect(cmd.val.name, cmd.val.device) then
|
||||
log_sys(util.c("configured monitor ", cmd.val.name, " reconnected"))
|
||||
else
|
||||
log_sys(util.c("unused monitor ", cmd.val.name, " connected"))
|
||||
end
|
||||
elseif cmd.key == MQ__RENDER_DATA.MON_DISCONNECT then
|
||||
-- monitor disconnected
|
||||
if renderer.handle_disconnect(cmd.val) then
|
||||
log_sys("lost a configured monitor")
|
||||
else
|
||||
log_sys("lost an unused monitor")
|
||||
end
|
||||
elseif cmd.key == MQ__RENDER_DATA.MON_RESIZE then
|
||||
-- monitor resized
|
||||
local is_used, is_ok = renderer.handle_resize(cmd.val)
|
||||
if is_used then
|
||||
log_sys(util.c("configured monitor ", cmd.val, " resized, ", util.trinary(is_ok, "display fits", "display does not fit")))
|
||||
end
|
||||
end
|
||||
elseif msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- received a packet
|
||||
end
|
||||
end
|
||||
|
||||
-- quick yield
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if crd_state.shutdown then
|
||||
log.info("render thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
-- delay before next check
|
||||
last_update = util.adaptive_delay(RENDER_SLEEP, last_update)
|
||||
end
|
||||
end
|
||||
|
||||
-- execute the thread in a protected mode, retrying it on return if not shutting down
|
||||
function public.p_exec()
|
||||
local crd_state = smem.crd_state
|
||||
|
||||
while not crd_state.shutdown do
|
||||
local status, result = pcall(public.exec)
|
||||
if status == false then
|
||||
log.fatal(util.strval(result))
|
||||
end
|
||||
|
||||
iocontrol.fp_rt_status("render", false)
|
||||
|
||||
if not crd_state.shutdown then
|
||||
log.info("render thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return threads
|
||||
@@ -1,5 +1,7 @@
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
@@ -12,28 +14,31 @@ local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local text_fg_bg = style.text_colors
|
||||
|
||||
-- new boiler view
|
||||
---@param root graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param ps psil ps interface
|
||||
local function new_view(root, x, y, ps)
|
||||
local text_fg = style.theme.text_fg
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local boiler = Rectangle{parent=root,border=border(1,colors.gray,true),width=31,height=7,x=x,y=y}
|
||||
|
||||
local status = StateIndicator{parent=boiler,x=9,y=1,states=style.boiler.states,value=1,min_width=12}
|
||||
local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=style.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=style.lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg}
|
||||
local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=lu_col,label="Temp:",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=22,fg_bg=text_fg}
|
||||
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}
|
||||
|
||||
status.register(ps, "computed_status", status.update)
|
||||
temp.register(ps, "temperature", temp.update)
|
||||
temp.register(ps, "temperature", function (t) temp.update(db.temp_convert(t)) end)
|
||||
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}
|
||||
TextBox{parent=boiler,text="S",x=27,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=boiler,text="C",x=28,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=boiler,text="H",x=2,y=5,height=1,width=1,fg_bg=text_fg}
|
||||
TextBox{parent=boiler,text="W",x=3,y=5,height=1,width=1,fg_bg=text_fg}
|
||||
TextBox{parent=boiler,text="S",x=27,y=5,height=1,width=1,fg_bg=text_fg}
|
||||
TextBox{parent=boiler,text="C",x=28,y=5,height=1,width=1,fg_bg=text_fg}
|
||||
|
||||
local hcool = VerticalBar{parent=boiler,x=2,y=1,fg_bg=cpair(colors.orange,colors.gray),height=4,width=1}
|
||||
local water = VerticalBar{parent=boiler,x=3,y=1,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1}
|
||||
|
||||
@@ -9,6 +9,7 @@ local Rectangle = require("graphics.elements.rectangle")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||
local PowerIndicator = require("graphics.elements.indicators.power")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
@@ -18,9 +19,6 @@ local border = core.border
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local text_fg_bg = style.text_colors
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
-- new induction matrix view
|
||||
---@param root graphics_element parent
|
||||
---@param x integer top left x
|
||||
@@ -29,57 +27,67 @@ local lu_col = style.lu_colors
|
||||
---@param ps psil ps interface
|
||||
---@param id number? matrix ID
|
||||
local function new_view(root, x, y, data, ps, id)
|
||||
local label_fg = style.theme.label_fg
|
||||
local text_fg = style.theme.text_fg
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
local ind_yel = style.ind_yel
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local title = "INDUCTION MATRIX"
|
||||
if type(id) == "number" then title = title .. id end
|
||||
|
||||
local matrix = Div{parent=root,fg_bg=style.root,width=33,height=24,x=x,y=y}
|
||||
|
||||
TextBox{parent=matrix,text=" ",width=33,height=1,x=1,y=1,fg_bg=style.lg_gray}
|
||||
TextBox{parent=matrix,text=title,alignment=ALIGN.CENTER,width=33,height=1,x=1,y=2,fg_bg=style.lg_gray}
|
||||
-- black has low contrast with dark gray, so if background is black use white instead
|
||||
local cutout_fg_bg = cpair(util.trinary(style.theme.bg == colors.black, colors.white, style.theme.bg), colors.gray)
|
||||
|
||||
TextBox{parent=matrix,text=" ",width=33,height=1,x=1,y=1,fg_bg=cutout_fg_bg}
|
||||
TextBox{parent=matrix,text=title,alignment=ALIGN.CENTER,width=33,height=1,x=1,y=2,fg_bg=cutout_fg_bg}
|
||||
|
||||
local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3}
|
||||
|
||||
local label_fg_bg = cpair(colors.gray, colors.lightGray)
|
||||
|
||||
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
|
||||
local energy = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
local capacity = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Capacity:",format="%8.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
local input = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="Input: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
|
||||
local output = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Output: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
|
||||
|
||||
local avg_chg = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Avg. Chg:",format="%8.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
local avg_in = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="Avg. In: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
|
||||
local avg_out = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Avg. Out:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
|
||||
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
|
||||
local capacity = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Capacity:",format="%8.2f",value=0,width=26,fg_bg=text_fg}
|
||||
local energy = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg}
|
||||
local avg_chg = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",value=0,width=26,fg_bg=text_fg}
|
||||
local input = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Input: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local avg_in = PowerIndicator{parent=rect,x=7,y=7,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local output = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Output: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local avg_out = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local trans_cap = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Max I/O: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
|
||||
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)
|
||||
|
||||
energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end)
|
||||
avg_chg.register(ps, "avg_charge", avg_chg.update)
|
||||
input.register(ps, "last_input", function (val) input.update(util.joules_to_fe(val)) end)
|
||||
avg_in.register(ps, "avg_inflow", avg_in.update)
|
||||
output.register(ps, "last_output", function (val) output.update(util.joules_to_fe(val)) end)
|
||||
avg_out.register(ps, "avg_outflow", avg_out.update)
|
||||
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end)
|
||||
|
||||
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill:",unit="%",format="%8.2f",value=0,width=18,fg_bg=text_fg_bg}
|
||||
|
||||
local cells = DataIndicator{parent=rect,x=11,y=14,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg_bg}
|
||||
local providers = DataIndicator{parent=rect,x=11,y=15,lu_colors=lu_col,label="Providers:",format="%7d",value=0,width=18,fg_bg=text_fg_bg}
|
||||
|
||||
TextBox{parent=rect,text="Transfer Capacity",x=11,y=17,height=1,width=17,fg_bg=label_fg_bg}
|
||||
local trans_cap = PowerIndicator{parent=rect,x=19,y=18,lu_colors=lu_col,label="",format="%5.2f",rate=true,value=0,width=12,fg_bg=text_fg_bg}
|
||||
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill: ",format="%7.2f",unit="%",value=0,width=20,fg_bg=text_fg}
|
||||
local cells = DataIndicator{parent=rect,x=11,y=13,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg}
|
||||
local providers = DataIndicator{parent=rect,x=11,y=14,lu_colors=lu_col,label="Providers:",format="%7d",value=0,width=18,fg_bg=text_fg}
|
||||
|
||||
fill.register(ps, "energy_fill", function (val) fill.update(val * 100) 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 chging = IndicatorLight{parent=rect,x=11,y=16,label="Charging",colors=ind_wht}
|
||||
local dischg = IndicatorLight{parent=rect,x=11,y=17,label="Discharging",colors=ind_wht}
|
||||
local max_io = IndicatorLight{parent=rect,x=11,y=18,label="Max I/O Rate",colors=ind_yel}
|
||||
|
||||
chging.register(ps, "is_charging", chging.update)
|
||||
dischg.register(ps, "is_discharging", dischg.update)
|
||||
max_io.register(ps, "at_max_io", max_io.update)
|
||||
|
||||
local charge = VerticalBar{parent=rect,x=2,y=2,fg_bg=cpair(colors.green,colors.gray),height=17,width=4}
|
||||
local in_cap = VerticalBar{parent=rect,x=7,y=12,fg_bg=cpair(colors.red,colors.gray),height=7,width=1}
|
||||
local out_cap = VerticalBar{parent=rect,x=9,y=12,fg_bg=cpair(colors.blue,colors.gray),height=7,width=1}
|
||||
|
||||
TextBox{parent=rect,text="FILL",x=2,y=20,height=1,width=4,fg_bg=text_fg_bg}
|
||||
TextBox{parent=rect,text="I/O",x=7,y=20,height=1,width=3,fg_bg=text_fg_bg}
|
||||
TextBox{parent=rect,text="FILL I/O",x=2,y=20,height=1,width=8,fg_bg=label_fg}
|
||||
|
||||
local function calc_saturation(val)
|
||||
if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then
|
||||
@@ -90,6 +98,49 @@ local function new_view(root, x, y, data, ps, id)
|
||||
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)
|
||||
|
||||
local eta = TextBox{parent=rect,x=11,y=20,width=20,height=1,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=style.theme.field_box}
|
||||
|
||||
eta.register(ps, "eta_ms", function (eta_ms)
|
||||
local str, pre = "", util.trinary(eta_ms >= 0, "Full in ", "Empty in ")
|
||||
|
||||
local seconds = math.abs(eta_ms) / 1000
|
||||
local minutes = seconds / 60
|
||||
local hours = minutes / 60
|
||||
local days = hours / 24
|
||||
|
||||
if math.abs(eta_ms) < 1000 or (eta_ms ~= eta_ms) then
|
||||
-- really small or NaN
|
||||
str = "No ETA"
|
||||
elseif days < 1000 then
|
||||
days = math.floor(days)
|
||||
hours = math.floor(hours % 24)
|
||||
minutes = math.floor(minutes % 60)
|
||||
seconds = math.floor(seconds % 60)
|
||||
|
||||
if days > 0 then
|
||||
str = days .. "d"
|
||||
elseif hours > 0 then
|
||||
str = hours .. "h " .. minutes .. "m"
|
||||
elseif minutes > 0 then
|
||||
str = minutes .. "m " .. seconds .. "s"
|
||||
elseif seconds > 0 then
|
||||
str = seconds .. "s"
|
||||
end
|
||||
|
||||
str = pre .. str
|
||||
else
|
||||
local years = math.floor(days / 365.25)
|
||||
|
||||
if years <= 99999999 then
|
||||
str = pre .. years .. "y"
|
||||
else
|
||||
str = pre .. "eras"
|
||||
end
|
||||
end
|
||||
|
||||
eta.set_value(str)
|
||||
end)
|
||||
end
|
||||
|
||||
return new_view
|
||||
|
||||
@@ -17,33 +17,35 @@ local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local text_fg_bg = style.text_colors
|
||||
local lg_wh = style.lg_white
|
||||
|
||||
-- create a pocket list entry
|
||||
---@param parent graphics_element parent
|
||||
---@param id integer PKT session ID
|
||||
local function init(parent, id)
|
||||
local s_hi_box = style.fp_theme.highlight_box
|
||||
local s_hi_bright = style.fp_theme.highlight_box_bright
|
||||
|
||||
local label_fg = style.fp.label_fg
|
||||
|
||||
local ps = iocontrol.get_db().fp.ps
|
||||
|
||||
-- root div
|
||||
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
|
||||
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.bw_fg_bg}
|
||||
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=s_hi_bright}
|
||||
|
||||
local ps_prefix = "pkt_" .. id .. "_"
|
||||
|
||||
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=text_fg_bg}
|
||||
local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,height=1,fg_bg=text_fg_bg,nav_active=cpair(colors.gray,colors.black)}
|
||||
TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=s_hi_box}
|
||||
local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,height=1,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)}
|
||||
TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=s_hi_box}
|
||||
pkt_addr.register(ps, ps_prefix .. "addr", pkt_addr.set_value)
|
||||
|
||||
TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1}
|
||||
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=lg_wh}
|
||||
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=label_fg}
|
||||
pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value)
|
||||
|
||||
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1}
|
||||
local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=lg_wh}
|
||||
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=lg_wh}
|
||||
local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
|
||||
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=label_fg}
|
||||
pkt_rtt.register(ps, ps_prefix .. "rtt", pkt_rtt.update)
|
||||
pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor)
|
||||
|
||||
|
||||
@@ -29,16 +29,6 @@ local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local lu_cpair = style.lu_colors
|
||||
local hzd_fg_bg = style.hzd_fg_bg
|
||||
local dis_colors = style.dis_colors
|
||||
|
||||
local gry_wht = style.gray_white
|
||||
|
||||
local ind_grn = style.ind_grn
|
||||
local ind_yel = style.ind_yel
|
||||
local ind_red = style.ind_red
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local period = core.flasher.PERIOD
|
||||
|
||||
@@ -47,6 +37,19 @@ local period = core.flasher.PERIOD
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
local function new_view(root, x, y)
|
||||
local s_hi_box = style.theme.highlight_box
|
||||
local s_field = style.theme.field_box
|
||||
|
||||
local lu_cpair = style.lu_colors
|
||||
local hzd_fg_bg = style.hzd_fg_bg
|
||||
local dis_colors = style.dis_colors
|
||||
local arrow_fg_bg = cpair(style.theme.label, s_hi_box.bkg)
|
||||
|
||||
local ind_grn = style.ind_grn
|
||||
local ind_yel = style.ind_yel
|
||||
local ind_red = style.ind_red
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
assert(root.get_height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||
|
||||
local black = cpair(colors.black, colors.black)
|
||||
@@ -65,7 +68,7 @@ local function new_view(root, x, y)
|
||||
facility.ack_alarms_ack = ack_a.on_response
|
||||
|
||||
local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=ind_grn}
|
||||
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
|
||||
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=style.ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd}
|
||||
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=ind_grn}
|
||||
local sps = IndicatorLight{parent=main,label="SPS Connected",colors=ind_grn}
|
||||
|
||||
@@ -103,11 +106,11 @@ local function new_view(root, x, y)
|
||||
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}
|
||||
local radiation = RadIndicator{parent=main,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=s_field}
|
||||
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}
|
||||
local rtu_count = DataIndicator{parent=main,x=15,y=24,label="",format="%11d",value=0,lu_colors=lu_cpair,width=11,fg_bg=s_field}
|
||||
rtu_count.register(facility.ps, "rtu_count", rtu_count.update)
|
||||
|
||||
---------------------
|
||||
@@ -125,9 +128,9 @@ local function new_view(root, x, y)
|
||||
local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=blk_pur}
|
||||
TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2}
|
||||
|
||||
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=gry_wht}
|
||||
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=burn_target,x=18,y=2,text="mB/t"}
|
||||
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=s_hi_box}
|
||||
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
|
||||
TextBox{parent=burn_target,x=18,y=2,text="mB/t",fg_bg=style.theme.label_fg}
|
||||
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
|
||||
|
||||
b_target.register(facility.ps, "process_burn_target", b_target.set_value)
|
||||
@@ -136,20 +139,20 @@ local function new_view(root, x, y)
|
||||
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=blk_pur}
|
||||
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
|
||||
|
||||
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=gry_wht}
|
||||
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=chg_target,x=18,y=2,text="MFE"}
|
||||
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=s_hi_box}
|
||||
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
|
||||
TextBox{parent=chg_target,x=18,y=2,text="MFE",fg_bg=style.theme.label_fg}
|
||||
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
|
||||
|
||||
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)
|
||||
cur_charge.register(facility.induction_ps_tbl[1], "avg_charge", function (fe) cur_charge.update(fe / 1000000) end)
|
||||
|
||||
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur}
|
||||
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
|
||||
|
||||
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=gry_wht}
|
||||
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=gen_target,x=18,y=2,text="kFE/t"}
|
||||
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=s_hi_box}
|
||||
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
|
||||
TextBox{parent=gen_target,x=18,y=2,text="kFE/t",fg_bg=style.theme.label_fg}
|
||||
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
|
||||
|
||||
g_target.register(facility.ps, "process_gen_target", g_target.set_value)
|
||||
@@ -165,17 +168,17 @@ local function new_view(root, x, y)
|
||||
|
||||
for i = 1, 4 do
|
||||
local unit
|
||||
local tag_fg_bg = gry_wht
|
||||
local lim_fg_bg = style.lg_white
|
||||
local ctl_fg = colors.lightGray
|
||||
local cur_fg_bg = style.lg_white
|
||||
local cur_lu = colors.lightGray
|
||||
local tag_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
|
||||
local lim_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
|
||||
local label_fg = style.theme.disabled_fg
|
||||
local cur_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
|
||||
local cur_lu = style.theme.disabled
|
||||
|
||||
if i <= facility.num_units then
|
||||
unit = units[i] ---@type ioctl_unit
|
||||
tag_fg_bg = cpair(colors.black,colors.lightBlue)
|
||||
lim_fg_bg = bw_fg_bg
|
||||
ctl_fg = colors.gray
|
||||
tag_fg_bg = cpair(colors.black, colors.lightBlue)
|
||||
lim_fg_bg = s_hi_box
|
||||
label_fg = style.theme.label_fg
|
||||
cur_fg_bg = blk_brn
|
||||
cur_lu = colors.black
|
||||
end
|
||||
@@ -185,9 +188,9 @@ local function new_view(root, x, y)
|
||||
local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
|
||||
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2}
|
||||
|
||||
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(ctl_fg,colors.white)}
|
||||
local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=lim_fg_bg}
|
||||
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1}
|
||||
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=s_hi_box}
|
||||
local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled,fg_bg=lim_fg_bg}
|
||||
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1,fg_bg=label_fg}
|
||||
|
||||
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(cur_lu,cur_lu),width=14,fg_bg=cur_fg_bg}
|
||||
|
||||
@@ -209,14 +212,14 @@ local function new_view(root, x, y)
|
||||
local stat_div = Div{parent=proc,width=22,height=24,x=57,y=6}
|
||||
|
||||
for i = 1, 4 do
|
||||
local tag_fg_bg = gry_wht
|
||||
local ind_fg_bg = style.lg_white
|
||||
local ind_off = colors.lightGray
|
||||
local tag_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
|
||||
local ind_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
|
||||
local ind_off = style.theme.disabled
|
||||
|
||||
if i <= facility.num_units then
|
||||
tag_fg_bg = cpair(colors.black, colors.cyan)
|
||||
ind_fg_bg = bw_fg_bg
|
||||
ind_off = colors.gray
|
||||
ind_fg_bg = cpair(style.theme.text, s_hi_box.bkg)
|
||||
ind_off = style.ind_hi_box_bg
|
||||
end
|
||||
|
||||
local _y = ((i - 1) * 5) + 1
|
||||
@@ -225,8 +228,8 @@ local function new_view(root, x, y)
|
||||
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Status",width=7,height=2}
|
||||
|
||||
local lights = Div{parent=stat_div,x=9,y=_y,width=14,height=4,fg_bg=ind_fg_bg}
|
||||
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(colors.green,ind_off)}
|
||||
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(colors.red,ind_off),flash=true,period=period.BLINK_250_MS}
|
||||
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(ind_grn.fgd,ind_off)}
|
||||
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(ind_red.fgd,ind_off),flash=true,period=period.BLINK_250_MS}
|
||||
|
||||
if i <= facility.num_units then
|
||||
local unit = units[i] ---@type ioctl_unit
|
||||
@@ -241,18 +244,18 @@ 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.gray,colors.white),select_color=colors.purple}
|
||||
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
|
||||
|
||||
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=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=ALIGN.CENTER,fg_bg=gry_wht}
|
||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.white)}
|
||||
|
||||
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=gry_wht}
|
||||
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=s_hi_box}
|
||||
|
||||
-- save the automatic process control configuration without starting
|
||||
local function _save_cfg()
|
||||
@@ -327,40 +330,36 @@ local function new_view(root, x, y)
|
||||
|
||||
local waste_sel = Div{parent=proc,width=21,height=24,x=81,y=1}
|
||||
|
||||
TextBox{parent=waste_sel,text=" ",width=21,height=1,x=1,y=1,fg_bg=blk_brn}
|
||||
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.brown)}
|
||||
local cutout_fg_bg = cpair(style.theme.bg, colors.brown)
|
||||
|
||||
TextBox{parent=waste_sel,text=" ",width=21,height=1,x=1,y=1,fg_bg=cutout_fg_bg}
|
||||
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,height=1,x=1,y=2,fg_bg=cutout_fg_bg}
|
||||
|
||||
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
|
||||
local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17}
|
||||
|
||||
status.register(facility.ps, "current_waste_product", status.update)
|
||||
|
||||
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.gray,colors.white),select_color=colors.brown}
|
||||
local pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.green,colors.black)}
|
||||
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.brown}
|
||||
|
||||
waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value)
|
||||
pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value)
|
||||
|
||||
local fb_active = IndicatorLight{parent=rect,x=2,y=9,label="Fallback Active",colors=ind_wht}
|
||||
local fb_active = IndicatorLight{parent=rect,x=2,y=7,label="Fallback Active",colors=ind_wht}
|
||||
local sps_disabled = IndicatorLight{parent=rect,x=2,y=8,label="SPS Disabled LC",colors=ind_yel}
|
||||
|
||||
fb_active.register(facility.ps, "pu_fallback_active", fb_active.update)
|
||||
sps_disabled.register(facility.ps, "sps_disabled_low_power", sps_disabled.update)
|
||||
|
||||
TextBox{parent=rect,x=2,y=11,text="Plutonium Rate",height=1,width=17,fg_bg=style.label}
|
||||
local pu_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
|
||||
local pu_fallback = Checkbox{parent=rect,x=2,y=10,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.brown,style.theme.checkbox_bg)}
|
||||
|
||||
TextBox{parent=rect,x=2,y=14,text="Polonium Rate",height=1,width=17,fg_bg=style.label}
|
||||
local po_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
|
||||
TextBox{parent=rect,x=2,y=12,height=3,text="Switch to Pu when SNAs cannot keep up with waste.",fg_bg=style.label}
|
||||
|
||||
TextBox{parent=rect,x=2,y=17,text="Antimatter Rate",height=1,width=17,fg_bg=style.label}
|
||||
local am_rate = DataIndicator{parent=rect,x=2,label="",unit="\xb5B/t",format="%12d",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
|
||||
local lc_sps = Checkbox{parent=rect,x=2,y=16,label="Low Charge SPS",callback=process.set_sps_low_power,box_fg_bg=cpair(colors.brown,style.theme.checkbox_bg)}
|
||||
|
||||
pu_rate.register(facility.ps, "pu_rate", pu_rate.update)
|
||||
po_rate.register(facility.ps, "po_rate", po_rate.update)
|
||||
am_rate.register(facility.ps, "am_rate", am_rate.update)
|
||||
TextBox{parent=rect,x=2,y=18,height=3,text="Use SPS at low charge, otherwise switches to Po.",fg_bg=style.label}
|
||||
|
||||
local sna_count = DataIndicator{parent=rect,x=2,y=20,label="Linked SNAs:",format="%4d",value=0,lu_colors=lu_cpair,width=17}
|
||||
|
||||
sna_count.register(facility.ps, "sna_count", sna_count.update)
|
||||
pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value)
|
||||
lc_sps.register(facility.ps, "process_sps_low_power", lc_sps.set_value)
|
||||
end
|
||||
|
||||
return new_view
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
local types = require("scada-common.types")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
@@ -14,35 +16,37 @@ local StateIndicator = require("graphics.elements.indicators.state")
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local text_fg_bg = style.text_colors
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
-- create new reactor view
|
||||
---@param root graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param ps psil ps interface
|
||||
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 = style.theme.text_fg
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local reactor = Rectangle{parent=root,border=border(1,colors.gray,true),width=30,height=7,x=x,y=y}
|
||||
|
||||
local status = StateIndicator{parent=reactor,x=6,y=1,states=style.reactor.states,value=1,min_width=16}
|
||||
local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit="K",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
local heating_r = DataIndicator{parent=reactor,x=2,y=5,lu_colors=lu_col,label="Heating:",unit="mB/t",format="%12.0f",value=0,commas=true,width=26,fg_bg=text_fg_bg}
|
||||
local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
|
||||
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}
|
||||
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}
|
||||
|
||||
status.register(ps, "computed_status", status.update)
|
||||
core_temp.register(ps, "temp", core_temp.update)
|
||||
core_temp.register(ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end)
|
||||
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}
|
||||
|
||||
TextBox{parent=reactor_fills,text="FUEL",x=2,y=1,height=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=reactor_fills,text="COOL",x=2,y=2,height=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=reactor_fills,text="HCOOL",x=2,y=4,height=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=reactor_fills,text="WASTE",x=2,y=5,height=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=reactor_fills,text="FUEL",x=2,y=1,height=1,fg_bg=text_fg}
|
||||
TextBox{parent=reactor_fills,text="COOL",x=2,y=2,height=1,fg_bg=text_fg}
|
||||
TextBox{parent=reactor_fills,text="HCOOL",x=2,y=4,height=1,fg_bg=text_fg}
|
||||
TextBox{parent=reactor_fills,text="WASTE",x=2,y=5,height=1,fg_bg=text_fg}
|
||||
|
||||
local fuel = HorizontalBar{parent=reactor_fills,x=8,y=1,show_percent=true,bar_fg_bg=cpair(colors.black,colors.gray),height=1,width=14}
|
||||
local fuel = HorizontalBar{parent=reactor_fills,x=8,y=1,show_percent=true,bar_fg_bg=cpair(style.theme.fuel_color,colors.gray),height=1,width=14}
|
||||
local ccool = HorizontalBar{parent=reactor_fills,x=8,y=2,show_percent=true,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=14}
|
||||
local hcool = HorizontalBar{parent=reactor_fills,x=8,y=4,show_percent=true,bar_fg_bg=cpair(colors.white,colors.gray),height=1,width=14}
|
||||
local waste = HorizontalBar{parent=reactor_fills,x=8,y=5,show_percent=true,bar_fg_bg=cpair(colors.brown,colors.gray),height=1,width=14}
|
||||
|
||||
@@ -15,20 +15,20 @@ local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local text_fg_bg = style.text_colors
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
-- new turbine view
|
||||
---@param root graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param ps psil ps interface
|
||||
local function new_view(root, x, y, ps)
|
||||
local text_fg = style.theme.text_fg
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
local turbine = Rectangle{parent=root,border=border(1,colors.gray,true),width=23,height=7,x=x,y=y}
|
||||
|
||||
local status = StateIndicator{parent=turbine,x=7,y=1,states=style.turbine.states,value=1,min_width=12}
|
||||
local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg_bg}
|
||||
local flow_rate = DataIndicator{parent=turbine,x=5,y=4,lu_colors=lu_col,label="",unit="mB/t",format="%10.0f",value=0,commas=true,width=16,fg_bg=text_fg_bg}
|
||||
local 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}
|
||||
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}
|
||||
|
||||
status.register(ps, "computed_status", status.update)
|
||||
prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end)
|
||||
@@ -37,8 +37,8 @@ local function new_view(root, x, y, ps)
|
||||
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}
|
||||
|
||||
TextBox{parent=turbine,text="S",x=2,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=turbine,text="E",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=turbine,text="S",x=2,y=5,height=1,width=1,fg_bg=text_fg}
|
||||
TextBox{parent=turbine,text="E",x=3,y=5,height=1,width=1,fg_bg=text_fg}
|
||||
|
||||
steam.register(ps, "steam_fill", steam.update)
|
||||
energy.register(ps, "energy_fill", energy.update)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
@@ -34,25 +35,33 @@ local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local lu_cpair = style.lu_colors
|
||||
local hzd_fg_bg = style.hzd_fg_bg
|
||||
local dis_colors = style.dis_colors
|
||||
|
||||
local gry_wht = style.gray_white
|
||||
|
||||
local ind_grn = style.ind_grn
|
||||
local ind_yel = style.ind_yel
|
||||
local ind_red = style.ind_red
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local period = core.flasher.PERIOD
|
||||
|
||||
-- create a unit view
|
||||
---@param parent graphics_element parent
|
||||
---@param id integer
|
||||
local function init(parent, id)
|
||||
local unit = iocontrol.get_db().units[id] ---@type ioctl_unit
|
||||
local f_ps = iocontrol.get_db().facility.ps
|
||||
local s_hi_box = style.theme.highlight_box
|
||||
local s_hi_bright = style.theme.highlight_box_bright
|
||||
local s_field = style.theme.field_box
|
||||
|
||||
local hc_text = style.hc_text
|
||||
local lu_cpair = style.lu_colors
|
||||
local hzd_fg_bg = style.hzd_fg_bg
|
||||
local dis_colors = style.dis_colors
|
||||
local arrow_fg_bg = cpair(style.theme.label, s_hi_box.bkg)
|
||||
|
||||
local ind_bkg = style.ind_bkg
|
||||
local ind_grn = style.ind_grn
|
||||
local ind_yel = style.ind_yel
|
||||
local ind_red = style.ind_red
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
local unit = db.units[id] ---@type ioctl_unit
|
||||
local f_ps = db.facility.ps
|
||||
|
||||
local main = Div{parent=parent,x=1,y=1}
|
||||
|
||||
@@ -62,7 +71,7 @@ local function init(parent, id)
|
||||
local b_ps = unit.boiler_ps_tbl
|
||||
local t_ps = unit.turbine_ps_tbl
|
||||
|
||||
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,height=1,fg_bg=style.theme.header}
|
||||
|
||||
-----------------------------
|
||||
-- main stats and core map --
|
||||
@@ -73,11 +82,11 @@ local function init(parent, id)
|
||||
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}
|
||||
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=s_field}
|
||||
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}
|
||||
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=s_field}
|
||||
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}
|
||||
@@ -87,7 +96,7 @@ local function init(parent, id)
|
||||
TextBox{parent=main,text="H",x=8,y=22,width=1,height=1,fg_bg=style.label}
|
||||
TextBox{parent=main,text="W",x=10,y=22,width=1,height=1,fg_bg=style.label}
|
||||
|
||||
local fuel = VerticalBar{parent=main,x=2,y=23,fg_bg=cpair(colors.black,colors.gray),height=4,width=1}
|
||||
local fuel = VerticalBar{parent=main,x=2,y=23,fg_bg=cpair(style.theme.fuel_color,colors.gray),height=4,width=1}
|
||||
local ccool = VerticalBar{parent=main,x=4,y=23,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1}
|
||||
local hcool = VerticalBar{parent=main,x=8,y=23,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
|
||||
local waste = VerticalBar{parent=main,x=10,y=23,fg_bg=cpair(colors.brown,colors.gray),height=4,width=1}
|
||||
@@ -114,19 +123,20 @@ local function init(parent, id)
|
||||
end)
|
||||
|
||||
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}
|
||||
core_temp.register(u_ps, "temp", core_temp.update)
|
||||
local fmt = util.trinary(string.len(db.temp_label) == 2, "%10.2f", "%11.2f")
|
||||
local core_temp = DataIndicator{parent=main,x=32,label="",format=fmt,value=0,commas=true,unit=db.temp_label,lu_colors=lu_cpair,width=13,fg_bg=s_field}
|
||||
core_temp.register(u_ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end)
|
||||
|
||||
TextBox{parent=main,x=32,y=25,text="Burn Rate",height=1,width=9,fg_bg=style.label}
|
||||
local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
|
||||
local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=s_field}
|
||||
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}
|
||||
local damage_p = DataIndicator{parent=main,x=32,label="",format="%11.0f",value=0,unit="%",lu_colors=lu_cpair,width=13,fg_bg=s_field}
|
||||
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}
|
||||
local radiation = RadIndicator{parent=main,x=32,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=s_field}
|
||||
radiation.register(u_ps, "radiation", radiation.update)
|
||||
|
||||
-------------------
|
||||
@@ -149,9 +159,9 @@ local function init(parent, id)
|
||||
local annunciator = Div{parent=main,width=23,height=18,x=22,y=3}
|
||||
|
||||
-- connectivity
|
||||
local plc_online = IndicatorLight{parent=annunciator,label="PLC Online",colors=cpair(colors.green,colors.red)}
|
||||
local plc_online = IndicatorLight{parent=annunciator,label="PLC Online",colors=cpair(ind_grn.fgd,ind_red.fgd)}
|
||||
local plc_hbeat = IndicatorLight{parent=annunciator,label="PLC Heartbeat",colors=ind_wht}
|
||||
local rad_mon = TriIndicatorLight{parent=annunciator,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
|
||||
local rad_mon = TriIndicatorLight{parent=annunciator,label="Radiation Monitor",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd}
|
||||
|
||||
plc_online.register(u_ps, "PLCOnline", plc_online.update)
|
||||
plc_hbeat.register(u_ps, "PLCHeartbeat", plc_hbeat.update)
|
||||
@@ -173,7 +183,7 @@ local function init(parent, id)
|
||||
local rad_wrn = IndicatorLight{parent=annunciator,label="Radiation Warning",colors=ind_yel}
|
||||
local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=ind_red}
|
||||
local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=ind_yel}
|
||||
local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=ind_yel}
|
||||
local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=ind_yel}
|
||||
local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=ind_red}
|
||||
local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=ind_yel}
|
||||
local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=ind_yel}
|
||||
@@ -208,7 +218,7 @@ local function init(parent, id)
|
||||
local rps_loc = IndicatorLight{parent=rps_annunc,label="Coolant Level Low Low",colors=ind_yel}
|
||||
local rps_flt = IndicatorLight{parent=rps_annunc,label="PPM Fault",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
|
||||
local rps_tmo = IndicatorLight{parent=rps_annunc,label="Connection Timeout",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
|
||||
local rps_sfl = IndicatorLight{parent=rps_annunc,label="System Failure",colors=cpair(colors.orange,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local rps_sfl = IndicatorLight{parent=rps_annunc,label="System Failure",colors=ind_red,flash=true,period=period.BLINK_500_MS}
|
||||
|
||||
rps_trp.register(u_ps, "rps_tripped", rps_trp.update)
|
||||
rps_dmg.register(u_ps, "high_dmg", rps_dmg.update)
|
||||
@@ -229,7 +239,7 @@ local function init(parent, id)
|
||||
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=ind_yel}
|
||||
local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=colors.gray,c2=colors.white,c3=colors.green}
|
||||
local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=ind_bkg,c2=ind_wht.fgd,c3=ind_grn.fgd}
|
||||
local c_cfm = IndicatorLight{parent=rcs_annunc,label="Coolant Feed Mismatch",colors=ind_yel}
|
||||
local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=ind_yel}
|
||||
local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=ind_yel}
|
||||
@@ -252,14 +262,14 @@ local function init(parent, id)
|
||||
|
||||
-- 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=ind_red}
|
||||
b1_wll.register(b_ps[1], "WasterLevelLow", b1_wll.update)
|
||||
if available_space > 0 then _add_space() end
|
||||
|
||||
TextBox{parent=rcs_tags,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=hc_text}
|
||||
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
|
||||
b1_wll.register(b_ps[1], "WaterLevelLow", b1_wll.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="B1",width=2,height=1,fg_bg=hc_text}
|
||||
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
|
||||
b1_hr.register(b_ps[1], "HeatingRateLow", b1_hr.update)
|
||||
end
|
||||
@@ -271,11 +281,11 @@ local function init(parent, id)
|
||||
_add_space()
|
||||
end
|
||||
|
||||
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=hc_text}
|
||||
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
|
||||
b2_wll.register(b_ps[2], "WasterLevelLow", b2_wll.update)
|
||||
b2_wll.register(b_ps[2], "WaterLevelLow", b2_wll.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=hc_text}
|
||||
local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
|
||||
b2_hr.register(b_ps[2], "HeatingRateLow", b2_hr.update)
|
||||
end
|
||||
@@ -284,19 +294,19 @@ local function init(parent, id)
|
||||
|
||||
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}
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=hc_text}
|
||||
local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
|
||||
t1_sdo.register(t_ps[1], "SteamDumpOpen", t1_sdo.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=hc_text}
|
||||
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
|
||||
t1_tos.register(t_ps[1], "TurbineOverSpeed", t1_tos.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=hc_text}
|
||||
local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,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}
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=hc_text}
|
||||
local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
t1_trp.register(t_ps[1], "TurbineTrip", t1_trp.update)
|
||||
|
||||
@@ -305,19 +315,19 @@ local function init(parent, id)
|
||||
_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}
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=hc_text}
|
||||
local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
|
||||
t2_sdo.register(t_ps[2], "SteamDumpOpen", t2_sdo.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=hc_text}
|
||||
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
|
||||
t2_tos.register(t_ps[2], "TurbineOverSpeed", t2_tos.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=hc_text}
|
||||
local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,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}
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=hc_text}
|
||||
local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
t2_trp.register(t_ps[2], "TurbineTrip", t2_trp.update)
|
||||
end
|
||||
@@ -325,30 +335,32 @@ local function init(parent, id)
|
||||
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}
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=hc_text}
|
||||
local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
|
||||
t3_sdo.register(t_ps[3], "SteamDumpOpen", t3_sdo.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=hc_text}
|
||||
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
|
||||
t3_tos.register(t_ps[3], "TurbineOverSpeed", t3_tos.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=hc_text}
|
||||
local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,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}
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=hc_text}
|
||||
local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update)
|
||||
end
|
||||
|
||||
util.nop()
|
||||
|
||||
----------------------
|
||||
-- reactor controls --
|
||||
----------------------
|
||||
|
||||
local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=gry_wht}
|
||||
local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=burn_control,x=9,y=2,text="mB/t"}
|
||||
local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=s_hi_box}
|
||||
local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
|
||||
TextBox{parent=burn_control,x=9,y=2,text="mB/t",fg_bg=style.theme.label_fg}
|
||||
|
||||
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=style.wh_gray,dis_fg_bg=dis_colors,callback=set_burn}
|
||||
@@ -394,22 +406,22 @@ local function init(parent, id)
|
||||
-- alarm management --
|
||||
----------------------
|
||||
|
||||
local alarm_panel = Div{parent=main,x=2,y=36,width=29,height=16,fg_bg=bw_fg_bg}
|
||||
local alarm_panel = Div{parent=main,x=2,y=36,width=29,height=16,fg_bg=s_hi_bright}
|
||||
|
||||
local a_brc = AlarmLight{parent=alarm_panel,x=6,y=2,label="Containment Breach",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rad = AlarmLight{parent=alarm_panel,x=6,label="Containment Radiation",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||
local a_dmg = AlarmLight{parent=alarm_panel,x=6,label="Critical Damage",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||
local a_brc = AlarmLight{parent=alarm_panel,x=6,y=2,label="Containment Breach",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rad = AlarmLight{parent=alarm_panel,x=6,label="Containment Radiation",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
|
||||
local a_dmg = AlarmLight{parent=alarm_panel,x=6,label="Critical Damage",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
|
||||
alarm_panel.line_break()
|
||||
local a_rcl = AlarmLight{parent=alarm_panel,x=6,label="Reactor Lost",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rcd = AlarmLight{parent=alarm_panel,x=6,label="Reactor Damage",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rot = AlarmLight{parent=alarm_panel,x=6,label="Reactor Over Temp",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rht = AlarmLight{parent=alarm_panel,x=6,label="Reactor High Temp",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS}
|
||||
local a_rwl = AlarmLight{parent=alarm_panel,x=6,label="Reactor Waste Leak",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rwh = AlarmLight{parent=alarm_panel,x=6,label="Reactor Waste High",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS}
|
||||
local a_rcl = AlarmLight{parent=alarm_panel,x=6,label="Reactor Lost",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rcd = AlarmLight{parent=alarm_panel,x=6,label="Reactor Damage",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rot = AlarmLight{parent=alarm_panel,x=6,label="Reactor Over Temp",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rht = AlarmLight{parent=alarm_panel,x=6,label="Reactor High Temp",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_500_MS}
|
||||
local a_rwl = AlarmLight{parent=alarm_panel,x=6,label="Reactor Waste Leak",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rwh = AlarmLight{parent=alarm_panel,x=6,label="Reactor Waste High",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_500_MS}
|
||||
alarm_panel.line_break()
|
||||
local a_rps = AlarmLight{parent=alarm_panel,x=6,label="RPS Transient",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS}
|
||||
local a_clt = AlarmLight{parent=alarm_panel,x=6,label="RCS Transient",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS}
|
||||
local a_tbt = AlarmLight{parent=alarm_panel,x=6,label="Turbine Trip",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rps = AlarmLight{parent=alarm_panel,x=6,label="RPS Transient",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_500_MS}
|
||||
local a_clt = AlarmLight{parent=alarm_panel,x=6,label="RCS Transient",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_500_MS}
|
||||
local a_tbt = AlarmLight{parent=alarm_panel,x=6,label="Turbine Trip",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
|
||||
|
||||
a_brc.register(u_ps, "Alarm_1", a_brc.update)
|
||||
a_rad.register(u_ps, "Alarm_2", a_rad.update)
|
||||
@@ -462,9 +474,9 @@ local function init(parent, id)
|
||||
|
||||
-- color tags
|
||||
|
||||
TextBox{parent=alarm_panel,x=5,y=13,text="\x95",width=1,height=1,fg_bg=cpair(colors.white,colors.cyan)}
|
||||
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,height=1,fg_bg=cpair(colors.white,colors.blue)}
|
||||
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,height=1,fg_bg=cpair(colors.white,colors.blue)}
|
||||
TextBox{parent=alarm_panel,x=5,y=13,text="\x95",width=1,height=1,fg_bg=cpair(s_hi_bright.bkg,colors.cyan)}
|
||||
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,height=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)}
|
||||
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,height=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)}
|
||||
|
||||
--------------------------------
|
||||
-- automatic control settings --
|
||||
@@ -476,7 +488,7 @@ local function init(parent, id)
|
||||
|
||||
local ctl_opts = { "Manual", "Primary", "Secondary", "Tertiary", "Backup" }
|
||||
|
||||
local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.gray,colors.white),select_color=colors.purple}
|
||||
local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
|
||||
|
||||
group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end)
|
||||
|
||||
@@ -488,7 +500,7 @@ local function init(parent, id)
|
||||
auto_div.line_break()
|
||||
|
||||
TextBox{parent=auto_div,text="Prio. Group",height=1,width=11,fg_bg=style.label}
|
||||
local auto_grp = TextBox{parent=auto_div,text="Manual",height=1,width=11,fg_bg=bw_fg_bg}
|
||||
local auto_grp = TextBox{parent=auto_div,text="Manual",height=1,width=11,fg_bg=s_field}
|
||||
|
||||
auto_grp.register(u_ps, "auto_group", auto_grp.set_value)
|
||||
|
||||
|
||||
@@ -24,17 +24,12 @@ local ALIGN = core.ALIGN
|
||||
local sprintf = util.sprintf
|
||||
|
||||
local border = core.border
|
||||
local cpair = core.cpair
|
||||
local pipe = core.pipe
|
||||
|
||||
local wh_gray = style.wh_gray
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local text_c = style.text_colors
|
||||
local lu_c = style.lu_colors
|
||||
local lg_gray = style.lg_gray
|
||||
|
||||
local ind_grn = style.ind_grn
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
-- make a new unit flow window
|
||||
---@param parent graphics_element parent
|
||||
---@param x integer top left x
|
||||
@@ -42,6 +37,15 @@ local ind_wht = style.ind_wht
|
||||
---@param wide boolean whether to render wide version
|
||||
---@param unit ioctl_unit unit database entry
|
||||
local function make(parent, x, y, wide, unit)
|
||||
local s_field = style.theme.field_box
|
||||
|
||||
local text_c = style.text_colors
|
||||
local lu_c = style.lu_colors
|
||||
local lu_c_d = style.lu_colors_dark
|
||||
|
||||
local ind_grn = style.ind_grn
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local height = 16
|
||||
|
||||
local v_start = 1 + ((unit.unit_id - 1) * 5)
|
||||
@@ -99,35 +103,35 @@ local function make(parent, x, y, wide, unit)
|
||||
table.insert(rc_pipes, pipe(_wide(92, 78), py, _wide(104, 83), py, colors.white, true))
|
||||
end
|
||||
|
||||
PipeNetwork{parent=root,x=20,y=1,pipes=rc_pipes,bg=colors.lightGray}
|
||||
PipeNetwork{parent=root,x=20,y=1,pipes=rc_pipes,bg=style.theme.bg}
|
||||
|
||||
if unit.num_boilers > 0 then
|
||||
local cc_rate = DataIndicator{parent=root,x=_wide(25,22),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
local hc_rate = DataIndicator{parent=root,x=_wide(25,22),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
local cc_rate = DataIndicator{parent=root,x=_wide(25,22),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
|
||||
local hc_rate = DataIndicator{parent=root,x=_wide(25,22),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
|
||||
|
||||
cc_rate.register(unit.unit_ps, "boiler_boil_sum", function (sum) cc_rate.update(sum * 10) end)
|
||||
hc_rate.register(unit.unit_ps, "heating_rate", hc_rate.update)
|
||||
|
||||
local boiler = Rectangle{parent=root,x=_wide(47,40),y=1,border=border(1, colors.gray, true),width=19,height=5,fg_bg=wh_gray}
|
||||
local boiler = Rectangle{parent=root,x=_wide(47,40),y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
|
||||
TextBox{parent=boiler,y=1,text="THERMO-ELECTRIC",alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=boiler,y=3,text=util.trinary(unit.num_boilers>1,"BOILERS","BOILER"),alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=root,x=_wide(47,40),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
|
||||
TextBox{parent=root,x=_wide(65,58),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
|
||||
|
||||
local wt_rate = DataIndicator{parent=root,x=_wide(71,61),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
local st_rate = DataIndicator{parent=root,x=_wide(71,61),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
local wt_rate = DataIndicator{parent=root,x=_wide(71,61),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
|
||||
local st_rate = DataIndicator{parent=root,x=_wide(71,61),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
|
||||
|
||||
wt_rate.register(unit.unit_ps, "turbine_flow_sum", wt_rate.update)
|
||||
st_rate.register(unit.unit_ps, "boiler_boil_sum", st_rate.update)
|
||||
else
|
||||
local wt_rate = DataIndicator{parent=root,x=28,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
local st_rate = DataIndicator{parent=root,x=28,y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
local wt_rate = DataIndicator{parent=root,x=28,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
|
||||
local st_rate = DataIndicator{parent=root,x=28,y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
|
||||
|
||||
wt_rate.register(unit.unit_ps, "turbine_flow_sum", wt_rate.update)
|
||||
st_rate.register(unit.unit_ps, "heating_rate", st_rate.update)
|
||||
end
|
||||
|
||||
local turbine = Rectangle{parent=root,x=_wide(93,79),y=1,border=border(1, colors.gray, true),width=19,height=5,fg_bg=wh_gray}
|
||||
local turbine = Rectangle{parent=root,x=_wide(93,79),y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
|
||||
TextBox{parent=turbine,y=1,text="STEAM TURBINE",alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=root,x=_wide(93,79),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
|
||||
@@ -135,7 +139,7 @@ local function make(parent, x, y, wide, unit)
|
||||
for i = 1, unit.num_turbines do
|
||||
local ry = 1 + (2 * (i - 1)) + prv_yo
|
||||
TextBox{parent=root,x=_wide(125,103),y=ry,text="\x10\x11\x7f",fg_bg=text_c,width=3,height=1}
|
||||
local state = TriIndicatorLight{parent=root,x=_wide(129,107),y=ry,label=v_names[i+4],c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
||||
local state = TriIndicatorLight{parent=root,x=_wide(129,107),y=ry,label=v_names[i+4],c1=style.ind_bkg,c2=style.ind_yel.fgd,c3=style.ind_red.fgd}
|
||||
state.register(unit.turbine_ps_tbl[i], "SteamDumpOpen", state.update)
|
||||
end
|
||||
|
||||
@@ -145,6 +149,8 @@ local function make(parent, x, y, wide, unit)
|
||||
|
||||
local waste = Div{parent=root,x=3,y=6}
|
||||
|
||||
local waste_c = style.theme.fuel_color
|
||||
|
||||
local waste_pipes = {
|
||||
pipe(0, 0, _wide(19, 16), 1, colors.brown, true),
|
||||
pipe(_wide(14, 13), 1, _wide(19, 17), 5, colors.brown, true),
|
||||
@@ -158,12 +164,12 @@ local function make(parent, x, y, wide, unit)
|
||||
pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.cyan, true),
|
||||
pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.cyan, true),
|
||||
|
||||
pipe(_wide(108, 94), 1, _wide(132, 110), 6, colors.black, true, true),
|
||||
pipe(_wide(108, 94), 4, _wide(111, 95), 1, colors.black, true, true),
|
||||
pipe(_wide(132, 110), 6, _wide(130, 108), 6, colors.black, true, true)
|
||||
pipe(_wide(108, 94), 1, _wide(132, 110), 6, waste_c, true, true),
|
||||
pipe(_wide(108, 94), 4, _wide(111, 95), 1, waste_c, true, true),
|
||||
pipe(_wide(132, 110), 6, _wide(130, 108), 6, waste_c, true, true)
|
||||
}
|
||||
|
||||
PipeNetwork{parent=waste,x=1,y=1,pipes=waste_pipes,bg=colors.lightGray}
|
||||
PipeNetwork{parent=waste,x=1,y=1,pipes=waste_pipes,bg=style.theme.bg}
|
||||
|
||||
local function _valve(vx, vy, n)
|
||||
TextBox{parent=waste,x=vx,y=vy,text="\x10\x11",fg_bg=text_c,width=2,height=1}
|
||||
@@ -175,16 +181,16 @@ local function make(parent, x, y, wide, unit)
|
||||
|
||||
local function _machine(mx, my, name)
|
||||
local l = string.len(name) + 2
|
||||
TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=ALIGN.CENTER,fg_bg=lg_gray,width=l,height=1}
|
||||
TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=ALIGN.CENTER,fg_bg=wh_gray,width=l,height=1}
|
||||
TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=ALIGN.CENTER,fg_bg=cpair(style.theme.bg,style.theme.header.bkg),width=l,height=1}
|
||||
TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=ALIGN.CENTER,fg_bg=style.theme.header,width=l,height=1}
|
||||
end
|
||||
|
||||
local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
local pu_rate = DataIndicator{parent=waste,x=_wide(82,70),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
local po_rate = DataIndicator{parent=waste,x=_wide(52,45),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
local popl_rate = DataIndicator{parent=waste,x=_wide(82,70),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
local poam_rate = DataIndicator{parent=waste,x=_wide(82,70),y=10,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
local spent_rate = DataIndicator{parent=waste,x=_wide(117,99),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
|
||||
local pu_rate = DataIndicator{parent=waste,x=_wide(82,70),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=s_field}
|
||||
local po_rate = DataIndicator{parent=waste,x=_wide(52,45),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
|
||||
local popl_rate = DataIndicator{parent=waste,x=_wide(82,70),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
|
||||
local poam_rate = DataIndicator{parent=waste,x=_wide(82,70),y=10,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
|
||||
local spent_rate = DataIndicator{parent=waste,x=_wide(117,98),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%8.3f",value=0,width=13,fg_bg=s_field}
|
||||
|
||||
waste_rate.register(unit.unit_ps, "act_burn_rate", waste_rate.update)
|
||||
pu_rate.register(unit.unit_ps, "pu_rate", pu_rate.update)
|
||||
@@ -204,17 +210,17 @@ local function make(parent, x, y, wide, unit)
|
||||
_machine(_wide(116, 94), 6, "SPENT WASTE \x1b")
|
||||
|
||||
TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
|
||||
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=bw_fg_bg}
|
||||
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=style.theme.highlight_box_bright}
|
||||
local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn}
|
||||
local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c,label="CNT",unit="",format="%2d",value=0,width=7}
|
||||
local sna_pk = DataIndicator{parent=sna_po,y=3,lu_colors=lu_c,label="PEAK",unit="mB/t",format="%7.2f",value=0,width=17}
|
||||
local sna_max = DataIndicator{parent=sna_po,lu_colors=lu_c,label="MAX",unit="mB/t",format="%8.2f",value=0,width=17}
|
||||
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c,label="IN",unit="mB/t",format="%9.2f",value=0,width=17}
|
||||
local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c_d,label="CNT",unit="",format="%2d",value=0,width=7}
|
||||
local sna_pk = DataIndicator{parent=sna_po,y=3,lu_colors=lu_c_d,label="PEAK",unit="mB/t",format="%7.2f",value=0,width=17}
|
||||
local sna_max = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="MAX",unit="mB/t",format="%8.2f",value=0,width=17}
|
||||
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="IN",unit="mB/t",format="%9.2f",value=0,width=17}
|
||||
|
||||
sna_act.register(unit.unit_ps, "po_rate", function (r) sna_act.update(r > 0) end)
|
||||
sna_cnt.register(unit.unit_ps, "sna_count", sna_cnt.update)
|
||||
sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.update)
|
||||
sna_max.register(unit.unit_ps, "sna_prod_rate", sna_max.update)
|
||||
sna_max.register(unit.unit_ps, "sna_max_rate", sna_max.update)
|
||||
sna_in.register(unit.unit_ps, "sna_in", sna_in.update)
|
||||
|
||||
return root
|
||||
|
||||
@@ -44,7 +44,7 @@ local function make(parent, x, y, unit)
|
||||
local root = Div{parent=parent,x=x,y=y,width=80,height=height}
|
||||
|
||||
-- unit header message
|
||||
TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,height=1,fg_bg=style.theme.header}
|
||||
|
||||
-------------
|
||||
-- REACTOR --
|
||||
@@ -66,7 +66,7 @@ local function make(parent, x, y, unit)
|
||||
table.insert(coolant_pipes, pipe(2, 0, 11, 11, colors.orange))
|
||||
end
|
||||
|
||||
PipeNetwork{parent=root,x=4,y=10,pipes=coolant_pipes,bg=colors.lightGray}
|
||||
PipeNetwork{parent=root,x=4,y=10,pipes=coolant_pipes,bg=style.theme.bg}
|
||||
end
|
||||
|
||||
-------------
|
||||
@@ -164,10 +164,10 @@ local function make(parent, x, y, unit)
|
||||
table.insert(steam_pipes_b, pipe(0, 18, 2, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
|
||||
end
|
||||
|
||||
PipeNetwork{parent=root,x=47,y=11,pipes=steam_pipes_a,bg=colors.lightGray}
|
||||
PipeNetwork{parent=root,x=47,y=11,pipes=steam_pipes_a,bg=style.theme.bg}
|
||||
end
|
||||
|
||||
PipeNetwork{parent=root,x=54,y=3,pipes=steam_pipes_b,bg=colors.lightGray}
|
||||
PipeNetwork{parent=root,x=54,y=3,pipes=steam_pipes_b,bg=style.theme.bg}
|
||||
|
||||
return root
|
||||
end
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
local completion = require("cc.completion")
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local print = util.print
|
||||
|
||||
local dialog = {}
|
||||
|
||||
-- ask the user yes or no
|
||||
---@nodiscard
|
||||
---@param question string
|
||||
---@param default boolean
|
||||
---@return boolean|nil
|
||||
function dialog.ask_y_n(question, default)
|
||||
print(question)
|
||||
|
||||
if default == true then
|
||||
print(" (Y/n)? ")
|
||||
else
|
||||
print(" (y/N)? ")
|
||||
end
|
||||
|
||||
local response = read(nil, nil)
|
||||
|
||||
if response == "" then
|
||||
return default
|
||||
elseif response == "Y" or response == "y" then
|
||||
return true
|
||||
elseif response == "N" or response == "n" then
|
||||
return false
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
-- ask the user for an input within a set of options
|
||||
---@nodiscard
|
||||
---@param options table
|
||||
---@param cancel string
|
||||
---@return boolean|string|nil
|
||||
function dialog.ask_options(options, cancel)
|
||||
print("> ")
|
||||
local response = read(nil, nil, function(text) return completion.choice(text, options) end)
|
||||
|
||||
if response == cancel then return false end
|
||||
|
||||
if util.table_contains(options, response) then
|
||||
return response
|
||||
else return nil end
|
||||
end
|
||||
|
||||
return dialog
|
||||
@@ -32,13 +32,16 @@ local border = core.border
|
||||
local pipe = core.pipe
|
||||
|
||||
local wh_gray = style.wh_gray
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local text_col = style.text_colors
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
-- create new flow view
|
||||
---@param main graphics_element main displaybox
|
||||
local function init(main)
|
||||
local s_hi_bright = style.theme.highlight_box_bright
|
||||
local s_field = style.theme.field_box
|
||||
local text_col = style.text_colors
|
||||
local lu_col = style.lu_colors
|
||||
local lu_c_d = style.lu_colors_dark
|
||||
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
|
||||
@@ -46,9 +49,9 @@ local function init(main)
|
||||
local tank_list = facility.tank_list
|
||||
|
||||
-- window header message
|
||||
local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=ALIGN.CENTER,height=1,fg_bg=style.theme.header}
|
||||
-- max length example: "01:23:45 AM - Wednesday, September 28 2022"
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=style.theme.header}
|
||||
|
||||
datetime.register(facility.ps, "date_time", datetime.set_value)
|
||||
|
||||
@@ -240,16 +243,17 @@ local function init(main)
|
||||
local flow_x = 3
|
||||
if #water_pipes > 0 then
|
||||
flow_x = 25
|
||||
PipeNetwork{parent=main,x=2,y=3,pipes=water_pipes,bg=colors.lightGray}
|
||||
PipeNetwork{parent=main,x=2,y=3,pipes=water_pipes,bg=style.theme.bg}
|
||||
end
|
||||
|
||||
for i = 1, facility.num_units do
|
||||
local y_offset = y_ofs(i)
|
||||
unit_flow(main, flow_x, 5 + y_offset, #water_pipes == 0, units[i])
|
||||
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.cyan, true, true))
|
||||
util.nop()
|
||||
end
|
||||
|
||||
PipeNetwork{parent=main,x=139,y=15,pipes=po_pipes,bg=colors.lightGray}
|
||||
PipeNetwork{parent=main,x=139,y=15,pipes=po_pipes,bg=style.theme.bg}
|
||||
|
||||
-----------------
|
||||
-- tank valves --
|
||||
@@ -297,7 +301,7 @@ local function init(main)
|
||||
|
||||
TextBox{parent=tank_box,x=2,y=3,text="Fill",height=1,width=10,fg_bg=style.label}
|
||||
local tank_pcnt = DataIndicator{parent=tank_box,x=10,y=3,label="",format="%5.2f",value=100,unit="%",lu_colors=lu_col,width=8,fg_bg=text_col}
|
||||
local tank_amnt = DataIndicator{parent=tank_box,x=2,label="",format="%13d",value=0,commas=true,unit="mB",lu_colors=lu_col,width=16,fg_bg=bw_fg_bg}
|
||||
local tank_amnt = DataIndicator{parent=tank_box,x=2,label="",format="%13d",value=0,commas=true,unit="mB",lu_colors=lu_col,width=16,fg_bg=s_field}
|
||||
|
||||
TextBox{parent=tank_box,x=2,y=6,text="Water Level",height=1,width=11,fg_bg=style.label}
|
||||
local level = HorizontalBar{parent=tank_box,x=2,y=7,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=16}
|
||||
@@ -332,6 +336,8 @@ local function init(main)
|
||||
end
|
||||
end
|
||||
|
||||
util.nop()
|
||||
|
||||
---------
|
||||
-- SPS --
|
||||
---------
|
||||
@@ -348,12 +354,12 @@ local function init(main)
|
||||
status.register(facility.sps_ps_tbl[1], "computed_status", status.update)
|
||||
|
||||
TextBox{parent=sps_box,x=2,y=3,text="Input Rate",height=1,width=10,fg_bg=style.label}
|
||||
local sps_in = DataIndicator{parent=sps_box,x=2,label="",format="%15.3f",value=0,unit="mB/t",lu_colors=lu_col,width=20,fg_bg=bw_fg_bg}
|
||||
local sps_in = DataIndicator{parent=sps_box,x=2,label="",format="%15.2f",value=0,unit="mB/t",lu_colors=lu_col,width=20,fg_bg=s_field}
|
||||
|
||||
sps_in.register(facility.ps, "po_am_rate", sps_in.update)
|
||||
|
||||
TextBox{parent=sps_box,x=2,y=6,text="Production Rate",height=1,width=15,fg_bg=style.label}
|
||||
local sps_rate = DataIndicator{parent=sps_box,x=2,label="",format="%15d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=20,fg_bg=bw_fg_bg}
|
||||
local sps_rate = DataIndicator{parent=sps_box,x=2,label="",format="%15d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=20,fg_bg=s_field}
|
||||
|
||||
sps_rate.register(facility.sps_ps_tbl[1], "process_rate", function (r) sps_rate.update(r * 1000) end)
|
||||
|
||||
@@ -362,24 +368,24 @@ local function init(main)
|
||||
----------------
|
||||
|
||||
TextBox{parent=main,x=145,y=16,text="RAW WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
|
||||
local raw_waste = Rectangle{parent=main,x=145,y=17,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg}
|
||||
local sum_raw_waste = DataIndicator{parent=raw_waste,lu_colors=lu_col,label="SUM",unit="mB/t",format="%8.2f",value=0,width=17}
|
||||
local raw_waste = Rectangle{parent=main,x=145,y=17,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=s_hi_bright}
|
||||
local sum_raw_waste = DataIndicator{parent=raw_waste,lu_colors=lu_c_d,label="SUM",unit="mB/t",format="%8.2f",value=0,width=17}
|
||||
|
||||
sum_raw_waste.register(facility.ps, "burn_sum", sum_raw_waste.update)
|
||||
|
||||
TextBox{parent=main,x=145,y=21,text="PROC. WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
|
||||
local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=bw_fg_bg}
|
||||
local pu = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17}
|
||||
local po = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Po",unit="mB/t",format="%9.3f",value=0,width=17}
|
||||
local popl = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="PoPl",unit="mB/t",format="%7.3f",value=0,width=17}
|
||||
local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=s_hi_bright}
|
||||
local pu = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17}
|
||||
local po = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="Po",unit="mB/t",format="%9.2f",value=0,width=17}
|
||||
local popl = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="PoPl",unit="mB/t",format="%7.2f",value=0,width=17}
|
||||
|
||||
pu.register(facility.ps, "pu_rate", pu.update)
|
||||
po.register(facility.ps, "po_rate", po.update)
|
||||
popl.register(facility.ps, "po_pl_rate", popl.update)
|
||||
|
||||
TextBox{parent=main,x=145,y=28,text="SPENT WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
|
||||
local sp_waste = Rectangle{parent=main,x=145,y=29,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg}
|
||||
local sum_sp_waste = DataIndicator{parent=sp_waste,lu_colors=lu_col,label="SUM",unit="mB/t",format="%8.3f",value=0,width=17}
|
||||
local sp_waste = Rectangle{parent=main,x=145,y=29,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=s_hi_bright}
|
||||
local sum_sp_waste = DataIndicator{parent=sp_waste,lu_colors=lu_c_d,label="SUM",unit="mB/t",format="%8.3f",value=0,width=17}
|
||||
|
||||
sum_sp_waste.register(facility.ps, "spent_waste_rate", sum_sp_waste.update)
|
||||
end
|
||||
|
||||
@@ -22,8 +22,11 @@ local TextBox = require("graphics.elements.textbox")
|
||||
local TabBar = require("graphics.elements.controls.tabbar")
|
||||
|
||||
local LED = require("graphics.elements.indicators.led")
|
||||
local LEDPair = require("graphics.elements.indicators.ledpair")
|
||||
local RGBLED = require("graphics.elements.indicators.ledrgb")
|
||||
|
||||
local LINK_STATE = types.PANEL_LINK_STATE
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
@@ -36,7 +39,7 @@ local led_grn = style.led_grn
|
||||
local function init(panel, num_units)
|
||||
local ps = iocontrol.get_db().fp.ps
|
||||
|
||||
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,height=1,fg_bg=style.fp.header}
|
||||
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,height=1,fg_bg=style.fp_theme.header}
|
||||
|
||||
local page_div = Div{parent=panel,x=1,y=3}
|
||||
|
||||
@@ -56,19 +59,58 @@ local function init(panel, num_units)
|
||||
heartbeat.register(ps, "heartbeat", heartbeat.update)
|
||||
|
||||
local modem = LED{parent=system,label="MODEM",colors=led_grn}
|
||||
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)
|
||||
|
||||
if not style.colorblind then
|
||||
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,style.fp_ind_bkg}}
|
||||
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
network.register(ps, "link_state", network.update)
|
||||
else
|
||||
local nt_lnk = LEDPair{parent=system,label="NT LINKED",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
|
||||
local nt_ver = LEDPair{parent=system,label="NT VERSION",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
|
||||
|
||||
nt_lnk.register(ps, "link_state", function (state)
|
||||
local value = 2
|
||||
|
||||
if state == LINK_STATE.DISCONNECTED then
|
||||
value = 1
|
||||
elseif state == LINK_STATE.LINKED then
|
||||
value = 3
|
||||
end
|
||||
|
||||
nt_lnk.update(value)
|
||||
end)
|
||||
|
||||
nt_ver.register(ps, "link_state", function (state)
|
||||
local value = 3
|
||||
|
||||
if state == LINK_STATE.BAD_VERSION then
|
||||
value = 2
|
||||
elseif state == LINK_STATE.DISCONNECTED then
|
||||
value = 1
|
||||
end
|
||||
|
||||
nt_ver.update(value)
|
||||
end)
|
||||
end
|
||||
|
||||
system.line_break()
|
||||
|
||||
modem.register(ps, "has_modem", modem.update)
|
||||
network.register(ps, "link_state", network.update)
|
||||
|
||||
local speaker = LED{parent=system,label="SPEAKER",colors=led_grn}
|
||||
speaker.register(ps, "has_speaker", speaker.update)
|
||||
|
||||
system.line_break()
|
||||
|
||||
local rt_main = LED{parent=system,label="RT MAIN",colors=led_grn}
|
||||
local rt_render = LED{parent=system,label="RT RENDER",colors=led_grn}
|
||||
|
||||
rt_main.register(ps, "routine__main", rt_main.update)
|
||||
rt_render.register(ps, "routine__render", rt_render.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=style.fp_label}
|
||||
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=style.fp.disabled_fg}
|
||||
|
||||
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
|
||||
|
||||
@@ -89,7 +131,7 @@ local function init(panel, num_units)
|
||||
-- about footer
|
||||
--
|
||||
|
||||
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp_label}
|
||||
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1}
|
||||
|
||||
@@ -103,7 +145,7 @@ local function init(panel, num_units)
|
||||
-- API page
|
||||
|
||||
local api_page = Div{parent=page_div,x=1,y=1,hidden=true}
|
||||
local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp_text,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
|
||||
local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
|
||||
local _ = Div{parent=api_list,height=1,hidden=true} -- padding
|
||||
|
||||
-- assemble page panes
|
||||
@@ -113,11 +155,11 @@ local function init(panel, num_units)
|
||||
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
|
||||
local tabs = {
|
||||
{ name = "CRD", color = style.fp_text },
|
||||
{ name = "API", color = style.fp_text },
|
||||
{ name = "CRD", color = style.fp.text },
|
||||
{ name = "API", color = style.fp.text },
|
||||
}
|
||||
|
||||
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=style.bw_fg_bg}
|
||||
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=style.fp_theme.highlight_box_bright}
|
||||
|
||||
-- link pocket API list management to PGI
|
||||
pgi.link_elements(api_list, pkt_entry)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
-- Main SCADA Coordinator GUI
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
@@ -21,14 +23,16 @@ local ALIGN = core.ALIGN
|
||||
-- create new main view
|
||||
---@param main graphics_element main displaybox
|
||||
local function init(main)
|
||||
local s_header = style.theme.header
|
||||
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
|
||||
-- window header message
|
||||
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=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=style.lg_white,width=12,fg_bg=style.header}
|
||||
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=ALIGN.CENTER,height=1,fg_bg=s_header}
|
||||
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=style.lg_white,width=12,fg_bg=s_header}
|
||||
-- max length example: "01:23:45 AM - Wednesday, September 28 2022"
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=s_header}
|
||||
|
||||
ping.register(facility.ps, "sv_ping", ping.update)
|
||||
datetime.register(facility.ps, "date_time", datetime.set_value)
|
||||
@@ -51,6 +55,8 @@ local function init(main)
|
||||
|
||||
cnc_y_start = cnc_y_start + row_1_height + 1
|
||||
|
||||
util.nop()
|
||||
|
||||
if facility.num_units >= 3 then
|
||||
-- base offset 3, spacing 1, max height of units 1 and 2
|
||||
local row_2_offset = cnc_y_start
|
||||
@@ -62,12 +68,12 @@ local function init(main)
|
||||
uo_4 = unit_overview(main, 84, row_2_offset, units[4])
|
||||
cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.get_height() + 1)
|
||||
end
|
||||
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- command & control
|
||||
|
||||
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.get_height() - 26
|
||||
|
||||
@@ -79,6 +85,8 @@ local function init(main)
|
||||
|
||||
process_ctl(main, 2, cnc_bottom_align_start)
|
||||
|
||||
util.nop()
|
||||
|
||||
imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1])
|
||||
end
|
||||
|
||||
|
||||
@@ -2,79 +2,141 @@
|
||||
-- Graphics Style Options
|
||||
--
|
||||
|
||||
local core = require("graphics.core")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
---@class crd_style
|
||||
local style = {}
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
-- GLOBAL --
|
||||
|
||||
-- add color mappings for front panel
|
||||
colors.ivory = colors.pink
|
||||
colors.yellow_hc = colors.purple
|
||||
colors.red_off = colors.brown
|
||||
colors.yellow_off = colors.magenta
|
||||
colors.green_off = colors.lime
|
||||
|
||||
-- front panel styling
|
||||
|
||||
style.fp = {}
|
||||
style.fp_theme = themes.sandstone
|
||||
style.fp = themes.get_fp_style(style.fp_theme)
|
||||
|
||||
style.fp.root = cpair(colors.black, colors.ivory)
|
||||
style.fp.header = cpair(colors.black, colors.lightGray)
|
||||
|
||||
style.fp.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 }, -- YELLOW HIGH CONTRAST
|
||||
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
|
||||
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
|
||||
-- { c = colors.white, hex = 0xdcd9ca },
|
||||
{ c = colors.lightGray, hex = 0xb1b8b3 },
|
||||
{ c = colors.gray, hex = 0x575757 },
|
||||
-- { c = colors.black, hex = 0x191919 },
|
||||
{ c = colors.brown, hex = 0x672223 } -- RED OFF
|
||||
}
|
||||
style.led_grn = cpair(colors.green, colors.green_off)
|
||||
|
||||
-- main GUI styling
|
||||
|
||||
style.root = cpair(colors.black, colors.lightGray)
|
||||
style.header = cpair(colors.white, colors.gray)
|
||||
style.label = cpair(colors.gray, colors.lightGray)
|
||||
---@class theme
|
||||
local smooth_stone = {
|
||||
text = colors.black,
|
||||
text_inv = colors.white,
|
||||
label = colors.gray,
|
||||
label_dark = colors.gray,
|
||||
disabled = colors.lightGray,
|
||||
bg = colors.lightGray,
|
||||
checkbox_bg = colors.black,
|
||||
accent_light = colors.white,
|
||||
accent_dark = colors.gray,
|
||||
|
||||
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 }
|
||||
fuel_color = colors.black,
|
||||
|
||||
header = cpair(colors.white, colors.gray),
|
||||
|
||||
text_fg = cpair(colors.black, colors._INHERIT),
|
||||
label_fg = cpair(colors.gray, colors._INHERIT),
|
||||
disabled_fg = cpair(colors.lightGray, colors._INHERIT),
|
||||
|
||||
highlight_box = cpair(colors.black, colors.white),
|
||||
highlight_box_bright = cpair(colors.black, colors.white),
|
||||
field_box = cpair(colors.black, colors.white),
|
||||
|
||||
colors = themes.smooth_stone.colors,
|
||||
|
||||
-- color re-mappings for assistive modes
|
||||
color_modes = themes.smooth_stone.color_modes
|
||||
}
|
||||
|
||||
---@type theme
|
||||
local deepslate = {
|
||||
text = colors.white,
|
||||
text_inv = colors.black,
|
||||
label = colors.lightGray,
|
||||
label_dark = colors.gray,
|
||||
disabled = colors.gray,
|
||||
bg = colors.black,
|
||||
checkbox_bg = colors.gray,
|
||||
accent_light = colors.gray,
|
||||
accent_dark = colors.lightGray,
|
||||
|
||||
fuel_color = colors.lightGray,
|
||||
|
||||
header = cpair(colors.white, colors.gray),
|
||||
|
||||
text_fg = cpair(colors.white, colors._INHERIT),
|
||||
label_fg = cpair(colors.lightGray, colors._INHERIT),
|
||||
disabled_fg = cpair(colors.gray, colors._INHERIT),
|
||||
|
||||
highlight_box = cpair(colors.white, colors.gray),
|
||||
highlight_box_bright = cpair(colors.black, colors.lightGray),
|
||||
field_box = cpair(colors.white, colors.gray),
|
||||
|
||||
colors = themes.deepslate.colors,
|
||||
|
||||
-- color re-mappings for assistive modes
|
||||
color_modes = themes.deepslate.color_modes
|
||||
}
|
||||
|
||||
style.theme = smooth_stone
|
||||
|
||||
-- set themes per configurations
|
||||
---@param main UI_THEME main UI theme
|
||||
---@param fp FP_THEME front panel theme
|
||||
---@param color_mode COLOR_MODE the color mode to use
|
||||
function style.set_themes(main, fp, color_mode)
|
||||
local colorblind = color_mode ~= themes.COLOR_MODE.STANDARD and color_mode ~= themes.COLOR_MODE.STD_ON_BLACK
|
||||
local gray_ind_off = color_mode == themes.COLOR_MODE.STANDARD or color_mode == themes.COLOR_MODE.BLUE_IND
|
||||
|
||||
style.ind_bkg = colors.gray
|
||||
style.fp_ind_bkg = util.trinary(gray_ind_off, colors.gray, colors.black)
|
||||
style.ind_hi_box_bg = util.trinary(gray_ind_off, colors.gray, colors.black)
|
||||
|
||||
if main == themes.UI_THEME.SMOOTH_STONE then
|
||||
style.theme = smooth_stone
|
||||
style.ind_bkg = util.trinary(gray_ind_off, colors.gray, colors.black)
|
||||
elseif main == themes.UI_THEME.DEEPSLATE then
|
||||
style.theme = deepslate
|
||||
style.ind_hi_box_bg = util.trinary(gray_ind_off, colors.lightGray, colors.black)
|
||||
end
|
||||
|
||||
style.colorblind = colorblind
|
||||
|
||||
style.root = cpair(style.theme.text, style.theme.bg)
|
||||
style.label = cpair(style.theme.label, style.theme.bg)
|
||||
|
||||
-- high contrast text (also tags)
|
||||
style.hc_text = cpair(style.theme.text, style.theme.text_inv)
|
||||
-- text on default background
|
||||
style.text_colors = cpair(style.theme.text, style.theme.bg)
|
||||
-- label & unit colors
|
||||
style.lu_colors = cpair(style.theme.label, style.theme.label)
|
||||
-- label & unit colors (darker if set)
|
||||
style.lu_colors_dark = cpair(style.theme.label_dark, style.theme.label_dark)
|
||||
|
||||
style.ind_grn = cpair(util.trinary(colorblind, colors.blue, colors.green), style.ind_bkg)
|
||||
style.ind_yel = cpair(colors.yellow, style.ind_bkg)
|
||||
style.ind_red = cpair(colors.red, style.ind_bkg)
|
||||
style.ind_wht = cpair(colors.white, style.ind_bkg)
|
||||
|
||||
if fp == themes.FP_THEME.SANDSTONE then
|
||||
style.fp_theme = themes.sandstone
|
||||
elseif fp == themes.FP_THEME.BASALT then
|
||||
style.fp_theme = themes.basalt
|
||||
end
|
||||
|
||||
style.fp = themes.get_fp_style(style.fp_theme)
|
||||
end
|
||||
|
||||
-- COMMON COLOR PAIRS --
|
||||
|
||||
style.wh_gray = cpair(colors.white, colors.gray)
|
||||
|
||||
style.bw_fg_bg = cpair(colors.black, colors.white)
|
||||
style.text_colors = cpair(colors.black, colors.lightGray)
|
||||
style.lu_colors = cpair(colors.gray, colors.gray)
|
||||
|
||||
style.hzd_fg_bg = style.wh_gray
|
||||
style.dis_colors = cpair(colors.white, colors.lightGray)
|
||||
|
||||
@@ -82,15 +144,6 @@ style.lg_gray = cpair(colors.lightGray, colors.gray)
|
||||
style.lg_white = cpair(colors.lightGray, colors.white)
|
||||
style.gray_white = cpair(colors.gray, colors.white)
|
||||
|
||||
style.ind_grn = cpair(colors.green, colors.gray)
|
||||
style.ind_yel = cpair(colors.yellow, colors.gray)
|
||||
style.ind_red = cpair(colors.red, colors.gray)
|
||||
style.ind_wht = style.wh_gray
|
||||
|
||||
style.fp_text = cpair(colors.black, colors.ivory)
|
||||
style.fp_label = cpair(colors.lightGray, colors.ivory)
|
||||
style.led_grn = cpair(colors.green, colors.green_off)
|
||||
|
||||
-- UI COMPONENTS --
|
||||
|
||||
style.reactor = {
|
||||
|
||||
@@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
|
||||
|
||||
local core = {}
|
||||
|
||||
core.version = "2.0.0"
|
||||
core.version = "2.2.4"
|
||||
|
||||
core.flasher = flasher
|
||||
core.events = events
|
||||
@@ -61,6 +61,9 @@ end
|
||||
---@field blit_fgd string
|
||||
---@field blit_bkg string
|
||||
|
||||
-- add inherited flag, 3 isn't a pure color so it wouldn't be used
|
||||
colors._INHERIT = 3
|
||||
|
||||
-- create a new color pair definition
|
||||
---@nodiscard
|
||||
---@param a color
|
||||
@@ -115,7 +118,7 @@ end
|
||||
|
||||
-- extract the custom element assert message, dropping the path to the element file
|
||||
function core.extract_assert_msg(msg)
|
||||
return string.sub(msg, (string.find(msg, "@") + 1) or 1)
|
||||
return string.sub(msg, (string.find(msg, "@") or 0) + 1)
|
||||
end
|
||||
|
||||
-- Interactive Field Manager
|
||||
@@ -173,7 +176,7 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
|
||||
if e.enabled then
|
||||
e.w_set_bkg(fg_bg.bkg)
|
||||
e.w_set_fgd(fg_bg.fgd)
|
||||
else
|
||||
elseif dis_fg_bg ~= nil then
|
||||
e.w_set_bkg(dis_fg_bg.bkg)
|
||||
e.w_set_fgd(dis_fg_bg.fgd)
|
||||
end
|
||||
|
||||
@@ -49,9 +49,11 @@ local element = {}
|
||||
---|indicator_light_args
|
||||
---|power_indicator_args
|
||||
---|rad_indicator_args
|
||||
---|signal_bar_args
|
||||
---|state_indicator_args
|
||||
---|tristate_indicator_light_args
|
||||
---|vbar_args
|
||||
---|app_multipane_args
|
||||
---|colormap_args
|
||||
---|displaybox_args
|
||||
---|div_args
|
||||
@@ -91,6 +93,8 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
p_window = nil, ---@type table
|
||||
position = events.new_coord_2d(1, 1),
|
||||
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
|
||||
offset_x = 0,
|
||||
offset_y = 0,
|
||||
next_y = 1, -- next child y coordinate
|
||||
next_id = 0, -- next child ID
|
||||
subscriptions = {},
|
||||
@@ -105,6 +109,7 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
value = nil, ---@type any
|
||||
window = nil, ---@type table
|
||||
content_window = nil, ---@type table|nil
|
||||
mouse_window_shift = { x = 0, y = 0 },
|
||||
fg_bg = core.cpair(colors.white, colors.black),
|
||||
frame = core.gframe(1, 1, 1, 1),
|
||||
children = {},
|
||||
@@ -193,6 +198,13 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
---@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)
|
||||
-- don't auto incrememnt y if inheriting height, that would cause an assertion
|
||||
next_y = util.trinary(args.height == nil, 1, next_y)
|
||||
|
||||
-- record offsets in case there is a reposition
|
||||
self.offset_x = offset_x
|
||||
self.offset_y = offset_y
|
||||
|
||||
-- get frame coordinates/size
|
||||
if args.gframe ~= nil then
|
||||
protected.frame.x = args.gframe.x
|
||||
@@ -217,21 +229,34 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
end
|
||||
|
||||
-- check frame
|
||||
element.assert(f.x >= 1, "frame x not >= 1", 2)
|
||||
element.assert(f.y >= 1, "frame y not >= 1", 2)
|
||||
element.assert(f.w >= 1, "frame width not >= 1", 2)
|
||||
element.assert(f.h >= 1, "frame height not >= 1", 2)
|
||||
element.assert(f.x >= 1, "frame x not >= 1", 3)
|
||||
element.assert(f.y >= 1, "frame y not >= 1", 3)
|
||||
element.assert(f.w >= 1, "frame width not >= 1", 3)
|
||||
element.assert(f.h >= 1, "frame height not >= 1", 3)
|
||||
|
||||
-- create window
|
||||
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
|
||||
protected.fg_bg = args.fg_bg
|
||||
elseif args.parent ~= nil then
|
||||
protected.fg_bg = args.parent.get_fg_bg()
|
||||
protected.fg_bg = core.cpair(args.fg_bg.fgd, args.fg_bg.bkg)
|
||||
end
|
||||
|
||||
if args.parent ~= nil then
|
||||
local p_fg_bg = args.parent.get_fg_bg()
|
||||
|
||||
if args.fg_bg == nil then
|
||||
protected.fg_bg = core.cpair(p_fg_bg.fgd, p_fg_bg.bkg)
|
||||
else
|
||||
if protected.fg_bg.fgd == colors._INHERIT then protected.fg_bg = core.cpair(p_fg_bg.fgd, protected.fg_bg.bkg) end
|
||||
if protected.fg_bg.bkg == colors._INHERIT then protected.fg_bg = core.cpair(protected.fg_bg.fgd, p_fg_bg.bkg) end
|
||||
end
|
||||
end
|
||||
|
||||
-- check colors
|
||||
element.assert(protected.fg_bg.fgd ~= colors._INHERIT, "could not determine foreground color to inherit")
|
||||
element.assert(protected.fg_bg.bkg ~= colors._INHERIT, "could not determine background color to inherit")
|
||||
|
||||
-- set colors
|
||||
protected.window.setBackgroundColor(protected.fg_bg.bkg)
|
||||
protected.window.setTextColor(protected.fg_bg.fgd)
|
||||
@@ -344,6 +369,10 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
-- handle this element having been unfocused
|
||||
function protected.on_unfocused() end
|
||||
|
||||
-- handle this element having had a child focused
|
||||
---@param child graphics_element
|
||||
function protected.on_child_focused(child) end
|
||||
|
||||
-- handle this element having been shown
|
||||
function protected.on_shown() end
|
||||
|
||||
@@ -520,6 +549,13 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
else args.parent.__focus_child(child) end
|
||||
end
|
||||
|
||||
-- a child was focused, used to make sure it is actually visible to the user in the content frame
|
||||
---@param child graphics_element
|
||||
function public.__child_focused(child)
|
||||
protected.on_child_focused(child)
|
||||
if not self.is_root then args.parent.__child_focused(public) end
|
||||
end
|
||||
|
||||
-- get a child element
|
||||
---@nodiscard
|
||||
---@param id element_id
|
||||
@@ -652,6 +688,7 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
if args.can_focus and protected.enabled and not self.focused then
|
||||
self.focused = true
|
||||
protected.on_focused()
|
||||
if not self.is_root then args.parent.__child_focused(public) end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -666,7 +703,7 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
-- unfocus this element and all its children
|
||||
function public.unfocus_all()
|
||||
public.unfocus()
|
||||
for _, child in pairs(protected.children) do child.get().unfocus() end
|
||||
for _, child in pairs(protected.children) do child.get().unfocus_all() end
|
||||
end
|
||||
|
||||
-- custom recolor command, varies by element if implemented
|
||||
@@ -681,7 +718,22 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
-- 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 public.reposition(x, y)
|
||||
protected.window.reposition(x, y)
|
||||
|
||||
-- record position
|
||||
self.position.x, self.position.y = protected.window.getPosition()
|
||||
|
||||
-- shift per parent child offset
|
||||
self.position.x = self.position.x + self.offset_x
|
||||
self.position.y = self.position.y + self.offset_y
|
||||
|
||||
-- calculate mouse event bounds
|
||||
self.bounds.x1 = self.position.x
|
||||
self.bounds.x2 = self.position.x + protected.frame.w - 1
|
||||
self.bounds.y1 = self.position.y
|
||||
self.bounds.y2 = self.position.y + protected.frame.h - 1
|
||||
end
|
||||
|
||||
-- FUNCTION CALLBACKS --
|
||||
|
||||
@@ -704,10 +756,11 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
end
|
||||
|
||||
local event_T = events.mouse_transposed(event, self.position.x, self.position.y)
|
||||
|
||||
-- handle the mouse event then pass to children
|
||||
protected.handle_mouse(event_T)
|
||||
for _, child in pairs(protected.children) do child.get().handle_mouse(event_T) end
|
||||
|
||||
-- shift child event if the content window has moved then pass to children
|
||||
local c_event_T = events.mouse_transposed(event_T, protected.mouse_window_shift.x + 1, protected.mouse_window_shift.y + 1)
|
||||
for _, child in pairs(protected.children) do child.get().handle_mouse(c_event_T) end
|
||||
elseif event.type == events.MOUSE_CLICK.DOWN or event.type == events.MOUSE_CLICK.TAP then
|
||||
-- clicked out, unfocus this element and children
|
||||
public.unfocus_all()
|
||||
@@ -808,9 +861,12 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
|
||||
-- re-draw this element and all its children
|
||||
function public.redraw()
|
||||
local bg, fg = protected.window.getBackgroundColor(), protected.window.getTextColor()
|
||||
protected.window.setBackgroundColor(protected.fg_bg.bkg)
|
||||
protected.window.setTextColor(protected.fg_bg.fgd)
|
||||
protected.window.clear()
|
||||
protected.window.setBackgroundColor(bg)
|
||||
protected.window.setTextColor(fg)
|
||||
protected.redraw()
|
||||
for _, child in pairs(protected.children) do child.get().redraw() end
|
||||
end
|
||||
|
||||
109
graphics/elements/appmultipane.lua
Normal file
109
graphics/elements/appmultipane.lua
Normal file
@@ -0,0 +1,109 @@
|
||||
-- App Page Multi-Pane Display Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
local events = require("graphics.events")
|
||||
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class app_multipane_args
|
||||
---@field panes table panes to swap between
|
||||
---@field nav_colors cpair on/off colors (a/b respectively) for page navigator
|
||||
---@field scroll_nav boolean? true to allow scrolling to change the active pane
|
||||
---@field drag_nav boolean? true to allow mouse dragging to change the active pane (on mouse up)
|
||||
---@field callback function? function to call when pane is changed by mouse interaction
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented 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 app multipane element
|
||||
---@nodiscard
|
||||
---@param args app_multipane_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function multipane(args)
|
||||
element.assert(type(args.panes) == "table", "panes is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 1
|
||||
|
||||
local nav_x_start = math.floor((e.frame.w / 2) - (#args.panes / 2)) + 1
|
||||
local nav_x_end = math.floor((e.frame.w / 2) - (#args.panes / 2)) + #args.panes
|
||||
|
||||
-- show the selected pane
|
||||
function e.redraw()
|
||||
for i = 1, #args.panes do args.panes[i].hide() end
|
||||
args.panes[e.value].show()
|
||||
|
||||
-- draw page indicator dots
|
||||
for i = 1, #args.panes do
|
||||
e.w_set_cur(nav_x_start + (i - 1), e.frame.h)
|
||||
e.w_set_fgd(util.trinary(i == e.value, args.nav_colors.color_a, args.nav_colors.color_b))
|
||||
e.w_write("\x07")
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
local initial = e.value
|
||||
|
||||
if e.enabled then
|
||||
if event.current.y == e.frame.h and event.current.x >= nav_x_start and event.current.x <= nav_x_end then
|
||||
local id = event.current.x - nav_x_start + 1
|
||||
|
||||
if event.type == MOUSE_CLICK.TAP then
|
||||
e.set_value(id)
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
e.set_value(id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if args.scroll_nav then
|
||||
if event.type == events.MOUSE_CLICK.SCROLL_DOWN then
|
||||
e.set_value(e.value + 1)
|
||||
elseif event.type == events.MOUSE_CLICK.SCROLL_UP then
|
||||
e.set_value(e.value - 1)
|
||||
end
|
||||
end
|
||||
|
||||
if args.drag_nav then
|
||||
local x1, x2 = event.initial.x, event.current.x
|
||||
if event.type == events.MOUSE_CLICK.UP and e.in_frame_bounds(x1, event.initial.y) and e.in_frame_bounds(x1, event.current.y) then
|
||||
if x2 > x1 then
|
||||
e.set_value(e.value - 1)
|
||||
elseif x2 < x1 then
|
||||
e.set_value(e.value + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if e.value ~= initial and type(args.callback) == "function" then args.callback(e.value) end
|
||||
end
|
||||
|
||||
-- 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
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return multipane
|
||||
@@ -30,7 +30,7 @@ local function app_button(args)
|
||||
element.assert(type(args.app_fg_bg) == "table", "app_fg_bg is a required field")
|
||||
|
||||
args.height = 4
|
||||
args.width = 5
|
||||
args.width = 7
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
@@ -46,7 +46,7 @@ local function app_button(args)
|
||||
end
|
||||
|
||||
-- draw icon
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_set_cur(2, 1)
|
||||
e.w_set_fgd(fgd)
|
||||
e.w_set_bkg(bkg)
|
||||
e.w_write("\x9f\x83\x83\x83")
|
||||
@@ -55,16 +55,16 @@ local function app_button(args)
|
||||
e.w_write("\x90")
|
||||
e.w_set_fgd(fgd)
|
||||
e.w_set_bkg(bkg)
|
||||
e.w_set_cur(1, 2)
|
||||
e.w_set_cur(2, 2)
|
||||
e.w_write("\x95 ")
|
||||
e.w_set_fgd(bkg)
|
||||
e.w_set_bkg(fgd)
|
||||
e.w_write("\x95")
|
||||
e.w_set_cur(1, 3)
|
||||
e.w_set_cur(2, 3)
|
||||
e.w_write("\x82\x8f\x8f\x8f\x81")
|
||||
|
||||
-- write the icon text
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_set_cur(4, 2)
|
||||
e.w_set_fgd(fgd)
|
||||
e.w_set_bkg(bkg)
|
||||
e.w_write(args.text)
|
||||
|
||||
@@ -24,7 +24,7 @@ local function checkbox(args)
|
||||
|
||||
args.can_focus = true
|
||||
args.height = 1
|
||||
args.width = 3 + string.len(args.label)
|
||||
args.width = 2 + string.len(args.label)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
@@ -138,23 +138,21 @@ local function hazard_button(args)
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled then
|
||||
if core.events.was_clicked(event.type) then
|
||||
-- change text color to indicate clicked
|
||||
e.w_set_fgd(args.accent)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
-- change text color to indicate clicked
|
||||
e.w_set_fgd(args.accent)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_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)
|
||||
|
||||
args.callback()
|
||||
end
|
||||
args.callback()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ local tcd = require("scada-common.tcd")
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
|
||||
@@ -12,6 +14,7 @@ local KEY_CLICK = core.events.KEY_CLICK
|
||||
---@field text string button text
|
||||
---@field callback function function to call on touch
|
||||
---@field min_width? integer text length if omitted
|
||||
---@field alignment? ALIGN text align if min width > length
|
||||
---@field active_fg_bg? cpair foreground/background colors when pressed
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
---@field parent graphics_element
|
||||
@@ -31,6 +34,7 @@ local function push_button(args)
|
||||
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
|
||||
|
||||
local text_width = string.len(args.text)
|
||||
local alignment = args.alignment or ALIGN.CENTER
|
||||
|
||||
-- set automatic settings
|
||||
args.can_focus = true
|
||||
@@ -41,9 +45,15 @@ local function push_button(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1
|
||||
local h_pad = 1
|
||||
local v_pad = math.floor(e.frame.h / 2) + 1
|
||||
|
||||
if alignment == ALIGN.CENTER then
|
||||
h_pad = math.floor((e.frame.w - text_width) / 2) + 1
|
||||
elseif alignment == ALIGN.RIGHT then
|
||||
h_pad = (e.frame.w - text_width) + 1
|
||||
end
|
||||
|
||||
-- draw the button
|
||||
function e.redraw()
|
||||
e.window.clear()
|
||||
|
||||
@@ -90,7 +90,8 @@ local function radio_button(args)
|
||||
-- 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
|
||||
if e.enabled and core.events.was_clicked(event.type) and
|
||||
(event.initial.y == event.current.y) and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
-- determine what was pressed
|
||||
if args.options[event.current.y] ~= nil then
|
||||
e.value = event.current.y
|
||||
|
||||
@@ -8,13 +8,7 @@ local element = require("graphics.element")
|
||||
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@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
|
||||
@@ -27,21 +21,16 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
---@param args sidebar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function sidebar(args)
|
||||
element.assert(type(args.tabs) == "table", "tabs is a required field")
|
||||
element.assert(#args.tabs > 0, "at least one tab is required")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
|
||||
args.width = 3
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
element.assert(e.frame.h >= (#args.tabs * 3), "height insufficent to display all tabs")
|
||||
|
||||
-- default to 1st tab
|
||||
e.value = 1
|
||||
|
||||
local was_pressed = false
|
||||
local tabs = {}
|
||||
|
||||
-- show the button state
|
||||
---@param pressed? boolean if the currently selected tab should appear as actively pressed
|
||||
@@ -51,10 +40,18 @@ local function sidebar(args)
|
||||
was_pressed = pressed
|
||||
pressed_idx = pressed_idx or e.value
|
||||
|
||||
for i = 1, #args.tabs do
|
||||
local tab = args.tabs[i] ---@type sidebar_tab
|
||||
-- clear
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
for y = 1, e.frame.h do
|
||||
e.w_set_cur(1, y)
|
||||
e.w_write(" ")
|
||||
end
|
||||
|
||||
local y = ((i - 1) * 3) + 1
|
||||
-- draw tabs
|
||||
for i = 1, #tabs do
|
||||
local tab = tabs[i] ---@type sidebar_tab
|
||||
local y = tab.y_start
|
||||
|
||||
e.w_set_cur(1, y)
|
||||
|
||||
@@ -66,13 +63,29 @@ local function sidebar(args)
|
||||
e.w_set_bkg(tab.color.bkg)
|
||||
end
|
||||
|
||||
e.w_write(" ")
|
||||
e.w_set_cur(1, y + 1)
|
||||
if e.value == i then
|
||||
e.w_write(" " .. tab.char .. "\x10")
|
||||
else e.w_write(" " .. tab.char .. " ") end
|
||||
e.w_set_cur(1, y + 2)
|
||||
e.w_write(" ")
|
||||
if tab.tall then
|
||||
e.w_write(" ")
|
||||
e.w_set_cur(1, y + 1)
|
||||
end
|
||||
|
||||
e.w_write(tab.label)
|
||||
|
||||
if tab.tall then
|
||||
e.w_set_cur(1, y + 2)
|
||||
e.w_write(" ")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- determine which tab was pressed
|
||||
---@param y integer y coordinate
|
||||
local function find_tab(y)
|
||||
for i = 1, #tabs do
|
||||
local tab = tabs[i] ---@type sidebar_tab
|
||||
|
||||
if y >= tab.y_start and y <= tab.y_end then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -81,23 +94,25 @@ local function sidebar(args)
|
||||
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)
|
||||
local cur_idx = find_tab(event.current.y)
|
||||
local ini_idx = find_tab(event.initial.y)
|
||||
local tab = tabs[cur_idx]
|
||||
|
||||
if args.tabs[cur_idx] ~= nil then
|
||||
-- handle press if a callback was provided
|
||||
if tab ~= nil and type(tab.callback) == "function" then
|
||||
if event.type == MOUSE_CLICK.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)
|
||||
tab.callback()
|
||||
elseif event.type == MOUSE_CLICK.DOWN then
|
||||
draw(true, cur_idx)
|
||||
elseif event.type == MOUSE_CLICK.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)
|
||||
tab.callback()
|
||||
else draw(false) end
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
@@ -113,6 +128,35 @@ local function sidebar(args)
|
||||
draw(false)
|
||||
end
|
||||
|
||||
-- update the sidebar navigation options
|
||||
---@param items table sidebar entries
|
||||
function e.on_update(items)
|
||||
local next_y = 1
|
||||
|
||||
tabs = {}
|
||||
|
||||
for i = 1, #items do
|
||||
local item = items[i]
|
||||
local height = util.trinary(item.tall, 3, 1)
|
||||
|
||||
---@class sidebar_tab
|
||||
local entry = {
|
||||
y_start = next_y, ---@type integer
|
||||
y_end = next_y + height - 1, ---@type integer
|
||||
tall = item.tall, ---@type boolean
|
||||
label = item.label, ---@type string
|
||||
color = item.color, ---@type cpair
|
||||
callback = item.callback ---@type function|nil
|
||||
}
|
||||
|
||||
next_y = next_y + height
|
||||
|
||||
tabs[i] = entry
|
||||
end
|
||||
|
||||
draw()
|
||||
end
|
||||
|
||||
-- element redraw
|
||||
e.redraw = draw
|
||||
|
||||
|
||||
@@ -127,20 +127,19 @@ local function spinbox(args)
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- only handle if on an increment or decrement arrow
|
||||
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
|
||||
digits[idx] = digits[idx] + 1
|
||||
elseif event.current.y == 3 then
|
||||
digits[idx] = digits[idx] - 1
|
||||
end
|
||||
|
||||
update_value()
|
||||
show_num()
|
||||
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) and
|
||||
(event.current.x ~= dec_point_x) and (event.current.y ~= 2) and
|
||||
(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
|
||||
digits[idx] = digits[idx] + 1
|
||||
elseif event.current.y == 3 then
|
||||
digits[idx] = digits[idx] - 1
|
||||
end
|
||||
|
||||
update_value()
|
||||
show_num()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -58,14 +58,14 @@ local function switch_button(args)
|
||||
-- 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
|
||||
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
e.value = not e.value
|
||||
e.redraw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
-- set the value (does not call the callback)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
|
||||
@@ -98,7 +98,7 @@ local function tabbar(args)
|
||||
---@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
|
||||
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) 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)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
-- Numeric Value Entry Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
@@ -8,9 +10,11 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class number_field_args
|
||||
---@field default? number default value, defaults to 0
|
||||
---@field min? number minimum, forced on unfocus
|
||||
---@field max? number maximum, forced on unfocus
|
||||
---@field max_digits? integer maximum number of digits, defaults to width
|
||||
---@field min? number minimum, enforced on unfocus
|
||||
---@field max? number maximum, enforced on unfocus
|
||||
---@field max_chars? integer maximum number of characters, defaults to width
|
||||
---@field max_int_digits? integer maximum number of integer digits, enforced on unfocus
|
||||
---@field max_frac_digits? integer maximum number of fractional digits, enforced on unfocus
|
||||
---@field allow_decimal? boolean true to allow decimals
|
||||
---@field allow_negative? boolean true to allow negative numbers
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
@@ -26,6 +30,9 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
---@param args number_field_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function number_field(args)
|
||||
element.assert(args.max_int_digits == nil or (util.is_int(args.max_int_digits) and args.max_int_digits > 0), "max_int_digits must be an integer greater than zero if supplied")
|
||||
element.assert(args.max_frac_digits == nil or (util.is_int(args.max_frac_digits) and args.max_frac_digits > 0), "max_frac_digits must be an integer greater than zero if supplied")
|
||||
|
||||
args.height = 1
|
||||
args.can_focus = true
|
||||
|
||||
@@ -34,19 +41,19 @@ local function number_field(args)
|
||||
|
||||
local has_decimal = false
|
||||
|
||||
args.max_digits = args.max_digits or e.frame.w
|
||||
args.max_chars = args.max_chars or e.frame.w
|
||||
|
||||
-- set initial value
|
||||
e.value = "" .. (args.default or 0)
|
||||
|
||||
-- make an interactive field manager
|
||||
local ifield = core.new_ifield(e, args.max_digits, args.fg_bg, args.dis_fg_bg)
|
||||
local ifield = core.new_ifield(e, args.max_chars, args.fg_bg, args.dis_fg_bg)
|
||||
|
||||
-- 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 then
|
||||
if e.enabled and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
if core.events.was_clicked(event.type) then
|
||||
e.take_focus()
|
||||
|
||||
@@ -62,7 +69,7 @@ local function number_field(args)
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == KEY_CLICK.CHAR and string.len(e.value) < args.max_digits then
|
||||
if event.type == KEY_CLICK.CHAR and string.len(e.value) < args.max_chars then
|
||||
if tonumber(event.name) then
|
||||
if e.value == 0 then e.value = "" end
|
||||
ifield.try_insert_char(event.name)
|
||||
@@ -127,12 +134,46 @@ local function number_field(args)
|
||||
local min = tonumber(args.min)
|
||||
|
||||
if type(val) == "number" then
|
||||
if args.max_int_digits or args.max_frac_digits then
|
||||
local str = e.value
|
||||
local ceil = false
|
||||
|
||||
if string.find(str, "-") then str = string.sub(e.value, 2) end
|
||||
local parts = util.strtok(str, ".")
|
||||
|
||||
if parts[1] and args.max_int_digits then
|
||||
if string.len(parts[1]) > args.max_int_digits then
|
||||
parts[1] = string.rep("9", args.max_int_digits)
|
||||
ceil = true
|
||||
end
|
||||
end
|
||||
|
||||
if args.allow_decimal and args.max_frac_digits then
|
||||
if ceil then
|
||||
parts[2] = string.rep("9", args.max_frac_digits)
|
||||
elseif parts[2] and (string.len(parts[2]) > args.max_frac_digits) then
|
||||
-- add a half of the highest precision fractional value in order to round using floor
|
||||
local scaled = math.fmod(val, 1) * (10 ^ (args.max_frac_digits))
|
||||
local value = math.floor(scaled + 0.5)
|
||||
local unscaled = value * (10 ^ (-args.max_frac_digits))
|
||||
parts[2] = string.sub(tostring(unscaled), 3) -- remove starting "0."
|
||||
end
|
||||
end
|
||||
|
||||
if parts[2] then parts[2] = "." .. parts[2] else parts[2] = "" end
|
||||
|
||||
val = tonumber((parts[1] or "") .. parts[2])
|
||||
end
|
||||
|
||||
if type(args.max) == "number" and val > max then
|
||||
e.value = "" .. max
|
||||
ifield.nav_start()
|
||||
elseif type(args.min) == "number" and val < min then
|
||||
e.value = "" .. min
|
||||
ifield.nav_start()
|
||||
else
|
||||
e.value = "" .. val
|
||||
ifield.nav_end()
|
||||
end
|
||||
else
|
||||
e.value = ""
|
||||
|
||||
@@ -41,7 +41,7 @@ local function text_field(args)
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- only handle if on an increment or decrement arrow
|
||||
if e.enabled then
|
||||
if e.enabled and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
if core.events.was_clicked(event.type) then
|
||||
e.take_focus()
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ local element = require("graphics.element")
|
||||
---@class icon_indicator_args
|
||||
---@field label string indicator label
|
||||
---@field states table state color and symbol table
|
||||
---@field value? integer default state, defaults to 1
|
||||
---@field value? integer|boolean default state, defaults to 1 (true = 2, false = 1)
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
@@ -33,6 +33,7 @@ local function icon(args)
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = args.value or 1
|
||||
if e.value == true then e.value = 2 end
|
||||
|
||||
-- state blit strings
|
||||
local state_blit_cmds = {}
|
||||
@@ -47,8 +48,11 @@ local function icon(args)
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
---@param new_state integer|boolean indicator state
|
||||
function e.on_update(new_state)
|
||||
new_state = new_state or 1
|
||||
if new_state == true then new_state = 2 end
|
||||
|
||||
local blit_cmd = state_blit_cmds[new_state]
|
||||
e.value = new_state
|
||||
e.w_set_cur(1, 1)
|
||||
@@ -56,7 +60,7 @@ local function icon(args)
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val integer indicator state
|
||||
---@param val integer|boolean indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- element redraw
|
||||
|
||||
85
graphics/elements/indicators/signal.lua
Normal file
85
graphics/elements/indicators/signal.lua
Normal file
@@ -0,0 +1,85 @@
|
||||
-- Signal Bars Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class signal_bar_args
|
||||
---@field compact? boolean true to use a single character (works better against edges that extend out colors)
|
||||
---@field colors_low_med? cpair color a for low signal quality, color b for medium signal quality
|
||||
---@field disconnect_color? color color for the 'x' on disconnect
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors (foreground is used for high signal quality)
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new signal bar
|
||||
---@nodiscard
|
||||
---@param args signal_bar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function signal_bar(args)
|
||||
args.height = 1
|
||||
args.width = util.trinary(args.compact, 1, 2)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 0
|
||||
|
||||
local blit_bkg = args.fg_bg.blit_bkg
|
||||
local blit_0, blit_1, blit_2, blit_3 = args.fg_bg.blit_fgd, args.fg_bg.blit_fgd, args.fg_bg.blit_fgd, args.fg_bg.blit_fgd
|
||||
|
||||
if type(args.colors_low_med) == "table" then
|
||||
blit_1 = args.colors_low_med.blit_a or blit_1
|
||||
blit_2 = args.colors_low_med.blit_b or blit_2
|
||||
end
|
||||
|
||||
if util.is_int(args.disconnect_color) then blit_0 = colors.toBlit(args.disconnect_color) end
|
||||
|
||||
-- on state change (0 = offline, 1 through 3 = low to high signal)
|
||||
---@param new_state integer signal state
|
||||
function e.on_update(new_state)
|
||||
e.value = new_state
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- set signal state (0 = offline, 1 through 3 = low to high signal)
|
||||
---@param val integer signal state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- draw label and signal bar
|
||||
function e.redraw()
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if args.compact then
|
||||
if e.value == 1 then
|
||||
e.w_blit("\x90", blit_1, blit_bkg)
|
||||
elseif e.value == 2 then
|
||||
e.w_blit("\x94", blit_2, blit_bkg)
|
||||
elseif e.value == 3 then
|
||||
e.w_blit("\x95", blit_3, blit_bkg)
|
||||
else
|
||||
e.w_blit("x", blit_0, blit_bkg)
|
||||
end
|
||||
else
|
||||
if e.value == 1 then
|
||||
e.w_blit("\x9f ", blit_bkg .. blit_bkg, blit_1 .. blit_bkg)
|
||||
elseif e.value == 2 then
|
||||
e.w_blit("\x9f\x94", blit_bkg .. blit_2, blit_2 .. blit_bkg)
|
||||
elseif e.value == 3 then
|
||||
e.w_blit("\x9f\x81", blit_bkg .. blit_bkg, blit_3 .. blit_3)
|
||||
else
|
||||
e.w_blit(" x", blit_0 .. blit_0, blit_bkg .. blit_bkg)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return signal_bar
|
||||
@@ -5,6 +5,7 @@ local tcd = require("scada-common.tcd")
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class listbox_args
|
||||
@@ -33,6 +34,8 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
---@param args listbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function listbox(args)
|
||||
args.can_focus = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
@@ -128,7 +131,7 @@ local function listbox(args)
|
||||
end
|
||||
|
||||
e.w_set_cur(e.frame.w, i)
|
||||
e.w_write(" ")
|
||||
if e.is_focused() then e.w_write("\x7f") else e.w_write(" ") end
|
||||
end
|
||||
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
@@ -158,6 +161,9 @@ local function listbox(args)
|
||||
scroll_frame.reposition(1, 1 + scroll_offset)
|
||||
scroll_frame.setVisible(true)
|
||||
|
||||
-- shift mouse events
|
||||
e.mouse_window_shift.y = scroll_offset
|
||||
|
||||
draw_bar()
|
||||
end
|
||||
|
||||
@@ -219,6 +225,32 @@ local function listbox(args)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle focus
|
||||
e.on_focused = draw_bar
|
||||
e.on_unfocused = draw_bar
|
||||
|
||||
-- handle a child in the list being focused, make sure it is visible
|
||||
function e.on_child_focused(child)
|
||||
for i = 1, #list do
|
||||
local item = list[i] ---@type listbox_item
|
||||
if item.e == child then
|
||||
if (item.y + scroll_offset) <= 0 then
|
||||
scroll_offset = 1 - item.y
|
||||
update_positions()
|
||||
draw_bar()
|
||||
elseif (item.y + scroll_offset) == 1 then
|
||||
-- do nothing, it's right at the top (if the bottom doesn't fit we can't easily fix that)
|
||||
elseif ((item.h + item.y - 1) + scroll_offset) > e.frame.h then
|
||||
scroll_offset = 1 - ((item.h + item.y) - e.frame.h)
|
||||
update_positions()
|
||||
draw_bar()
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
@@ -226,23 +258,27 @@ local function listbox(args)
|
||||
if event.type == MOUSE_CLICK.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
|
||||
if event.current.y == 1 then
|
||||
draw_arrows(1)
|
||||
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) 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
|
||||
if event.current.y == e.frame.h then
|
||||
draw_arrows(-1)
|
||||
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.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()
|
||||
if event.current.y == 1 then draw_arrows(1) end
|
||||
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
|
||||
draw_arrows(-1)
|
||||
scroll_down()
|
||||
if event.current.y == e.frame.h then draw_arrows(-1) end
|
||||
else
|
||||
-- clicked on bar
|
||||
holding_bar = true
|
||||
@@ -274,6 +310,24 @@ local function listbox(args)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
|
||||
if event.key == keys.up then
|
||||
scroll_up()
|
||||
elseif event.key == keys.down then
|
||||
scroll_down()
|
||||
elseif event.key == keys.home then
|
||||
scroll_offset = 0
|
||||
update_positions()
|
||||
elseif event.key == keys["end"] then
|
||||
scroll_offset = max_down_scroll
|
||||
update_positions()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
draw_arrows(0)
|
||||
|
||||
418
graphics/themes.lua
Normal file
418
graphics/themes.lua
Normal file
@@ -0,0 +1,418 @@
|
||||
--
|
||||
-- Graphics Themes
|
||||
--
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
---@class graphics_themes
|
||||
local themes = {}
|
||||
|
||||
-- add color mappings for front panels
|
||||
colors.ivory = colors.pink
|
||||
colors.green_hc = colors.cyan
|
||||
colors.yellow_hc = colors.purple
|
||||
colors.red_off = colors.brown
|
||||
colors.yellow_off = colors.magenta
|
||||
colors.green_off = colors.lime
|
||||
|
||||
--#region Types
|
||||
|
||||
---@enum UI_THEME
|
||||
themes.UI_THEME = { SMOOTH_STONE = 1, DEEPSLATE = 2 }
|
||||
themes.UI_THEME_NAMES = { "Smooth Stone", "Deepslate" }
|
||||
|
||||
-- attempts to get the string name of a main ui theme
|
||||
---@nodiscard
|
||||
---@param id any
|
||||
---@return string|nil
|
||||
function themes.ui_theme_name(id)
|
||||
if id == themes.UI_THEME.SMOOTH_STONE or
|
||||
id == themes.UI_THEME.DEEPSLATE then
|
||||
return themes.UI_THEME_NAMES[id]
|
||||
else return nil end
|
||||
end
|
||||
|
||||
---@enum FP_THEME
|
||||
themes.FP_THEME = { SANDSTONE = 1, BASALT = 2 }
|
||||
themes.FP_THEME_NAMES = { "Sandstone", "Basalt" }
|
||||
|
||||
-- attempts to get the string name of a front panel theme
|
||||
---@nodiscard
|
||||
---@param id any
|
||||
---@return string|nil
|
||||
function themes.fp_theme_name(id)
|
||||
if id == themes.FP_THEME.SANDSTONE or
|
||||
id == themes.FP_THEME.BASALT then
|
||||
return themes.FP_THEME_NAMES[id]
|
||||
else return nil end
|
||||
end
|
||||
|
||||
---@enum COLOR_MODE
|
||||
themes.COLOR_MODE = {
|
||||
STANDARD = 1,
|
||||
DEUTERANOPIA = 2,
|
||||
PROTANOPIA = 3,
|
||||
TRITANOPIA = 4,
|
||||
BLUE_IND = 5,
|
||||
STD_ON_BLACK = 6,
|
||||
BLUE_ON_BLACK = 7,
|
||||
NUM_MODES = 8
|
||||
}
|
||||
|
||||
themes.COLOR_MODE_NAMES = {
|
||||
"Standard",
|
||||
"Deuteranopia",
|
||||
"Protanopia",
|
||||
"Tritanopia",
|
||||
"Blue for 'Good'",
|
||||
"Standard + Black",
|
||||
"Blue + Black"
|
||||
}
|
||||
|
||||
-- attempts to get the string name of a color mode
|
||||
---@nodiscard
|
||||
---@param id any
|
||||
---@return string|nil
|
||||
function themes.color_mode_name(id)
|
||||
if id == themes.COLOR_MODE.STANDARD or
|
||||
id == themes.COLOR_MODE.DEUTERANOPIA or
|
||||
id == themes.COLOR_MODE.PROTANOPIA or
|
||||
id == themes.COLOR_MODE.TRITANOPIA or
|
||||
id == themes.COLOR_MODE.BLUE_IND or
|
||||
id == themes.COLOR_MODE.STD_ON_BLACK or
|
||||
id == themes.COLOR_MODE.BLUE_ON_BLACK then
|
||||
return themes.COLOR_MODE_NAMES[id]
|
||||
else return nil end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Front Panel Themes
|
||||
|
||||
---@class fp_theme
|
||||
themes.sandstone = {
|
||||
text = colors.black,
|
||||
label = colors.lightGray,
|
||||
label_dark = colors.gray,
|
||||
disabled = colors.lightGray,
|
||||
bg = colors.ivory,
|
||||
|
||||
header = cpair(colors.black, colors.lightGray),
|
||||
|
||||
highlight_box = cpair(colors.black, colors.lightGray),
|
||||
highlight_box_bright = cpair(colors.black, colors.white),
|
||||
field_box = cpair(colors.gray, colors.white),
|
||||
|
||||
colors = {
|
||||
{ c = colors.red, hex = 0xdf4949 },
|
||||
{ c = colors.orange, hex = 0xffb659 },
|
||||
{ c = colors.yellow, hex = 0xf9fb53 },
|
||||
{ c = colors.green_off, hex = 0x16665a },
|
||||
{ c = colors.green, hex = 0x6be551 },
|
||||
{ c = colors.green_hc, hex = 0x6be551 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.yellow_hc, hex = 0xe3bc2a },
|
||||
{ c = colors.ivory, hex = 0xdcd9ca },
|
||||
{ c = colors.yellow_off, hex = 0x85862c },
|
||||
{ c = colors.white, hex = 0xf0f0f0 },
|
||||
{ c = colors.lightGray, hex = 0xb1b8b3 },
|
||||
{ c = colors.gray, hex = 0x575757 },
|
||||
{ c = colors.black, hex = 0x191919 },
|
||||
{ c = colors.red_off, hex = 0x672223 }
|
||||
},
|
||||
|
||||
-- color re-mappings for assistive modes
|
||||
color_modes = {
|
||||
-- standard
|
||||
{},
|
||||
-- deuteranopia
|
||||
{
|
||||
{ c = colors.green, hex = 0x1081ff },
|
||||
{ c = colors.green_hc, hex = 0x1081ff },
|
||||
{ c = colors.green_off, hex = 0x141414 },
|
||||
{ c = colors.yellow, hex = 0xf7c311 },
|
||||
{ c = colors.yellow_off, hex = 0x141414 },
|
||||
{ c = colors.red, hex = 0xfb5615 },
|
||||
{ c = colors.red_off, hex = 0x141414 }
|
||||
},
|
||||
-- protanopia
|
||||
{
|
||||
{ c = colors.green, hex = 0x1081ff },
|
||||
{ c = colors.green_hc, hex = 0x1081ff },
|
||||
{ c = colors.green_off, hex = 0x141414 },
|
||||
{ c = colors.yellow, hex = 0xf5e633 },
|
||||
{ c = colors.yellow_off, hex = 0x141414 },
|
||||
{ c = colors.red, hex = 0xff521a },
|
||||
{ c = colors.red_off, hex = 0x141414 }
|
||||
},
|
||||
-- tritanopia
|
||||
{
|
||||
{ c = colors.green, hex = 0x40cbd7 },
|
||||
{ c = colors.green_hc, hex = 0x40cbd7 },
|
||||
{ c = colors.green_off, hex = 0x141414 },
|
||||
{ c = colors.yellow, hex = 0xffbc00 },
|
||||
{ c = colors.yellow_off, hex = 0x141414 },
|
||||
{ c = colors.red, hex = 0xff0000 },
|
||||
{ c = colors.red_off, hex = 0x141414 }
|
||||
},
|
||||
-- blue indicators
|
||||
{
|
||||
{ c = colors.green, hex = 0x1081ff },
|
||||
{ c = colors.green_hc, hex = 0x1081ff },
|
||||
{ c = colors.green_off, hex = 0x053466 },
|
||||
},
|
||||
-- standard, black backgrounds
|
||||
{
|
||||
{ c = colors.green_off, hex = 0x141414 },
|
||||
{ c = colors.yellow_off, hex = 0x141414 },
|
||||
{ c = colors.red_off, hex = 0x141414 }
|
||||
},
|
||||
-- blue indicators, black backgrounds
|
||||
{
|
||||
{ c = colors.green, hex = 0x1081ff },
|
||||
{ c = colors.green_hc, hex = 0x1081ff },
|
||||
{ c = colors.green_off, hex = 0x141414 },
|
||||
{ c = colors.yellow_off, hex = 0x141414 },
|
||||
{ c = colors.red_off, hex = 0x141414 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
---@type fp_theme
|
||||
themes.basalt = {
|
||||
text = colors.white,
|
||||
label = colors.gray,
|
||||
label_dark = colors.ivory,
|
||||
disabled = colors.lightGray,
|
||||
bg = colors.ivory,
|
||||
|
||||
header = cpair(colors.white, colors.gray),
|
||||
|
||||
highlight_box = cpair(colors.white, colors.gray),
|
||||
highlight_box_bright = cpair(colors.black, colors.lightGray),
|
||||
field_box = cpair(colors.white, colors.gray),
|
||||
|
||||
colors = {
|
||||
{ c = colors.red, hex = 0xf18486 },
|
||||
{ c = colors.orange, hex = 0xffb659 },
|
||||
{ c = colors.yellow, hex = 0xefe37c },
|
||||
{ c = colors.green_off, hex = 0x436b41 },
|
||||
{ c = colors.green, hex = 0x7ae175 },
|
||||
{ c = colors.green_hc, hex = 0x7ae175 },
|
||||
{ c = colors.lightBlue, hex = 0x7dc6f2 },
|
||||
{ c = colors.blue, hex = 0x56aae6 },
|
||||
{ c = colors.yellow_hc, hex = 0xe9cd68 },
|
||||
{ c = colors.ivory, hex = 0x4d4e52 },
|
||||
{ c = colors.yellow_off, hex = 0x757040 },
|
||||
{ c = colors.white, hex = 0xbfbfbf },
|
||||
{ c = colors.lightGray, hex = 0x848794 },
|
||||
{ c = colors.gray, hex = 0x5c5f68 },
|
||||
{ c = colors.black, hex = 0x333333 },
|
||||
{ c = colors.red_off, hex = 0x512d2d }
|
||||
},
|
||||
|
||||
color_modes = {
|
||||
-- standard
|
||||
{},
|
||||
-- deuteranopia
|
||||
{
|
||||
{ c = colors.green, hex = 0x65aeff },
|
||||
{ c = colors.green_hc, hex = 0x99c9ff },
|
||||
{ c = colors.green_off, hex = 0x333333 },
|
||||
{ c = colors.yellow, hex = 0xf7c311 },
|
||||
{ c = colors.yellow_off, hex = 0x333333 },
|
||||
{ c = colors.red, hex = 0xf18486 },
|
||||
{ c = colors.red_off, hex = 0x333333 }
|
||||
},
|
||||
-- protanopia
|
||||
{
|
||||
{ c = colors.green, hex = 0x65aeff },
|
||||
{ c = colors.green_hc, hex = 0x99c9ff },
|
||||
{ c = colors.green_off, hex = 0x333333 },
|
||||
{ c = colors.yellow, hex = 0xf5e633 },
|
||||
{ c = colors.yellow_off, hex = 0x333333 },
|
||||
{ c = colors.red, hex = 0xff8058 },
|
||||
{ c = colors.red_off, hex = 0x333333 }
|
||||
},
|
||||
-- tritanopia
|
||||
{
|
||||
{ c = colors.green, hex = 0x00ecff },
|
||||
{ c = colors.green_hc, hex = 0x00ecff },
|
||||
{ c = colors.green_off, hex = 0x333333 },
|
||||
{ c = colors.yellow, hex = 0xffbc00 },
|
||||
{ c = colors.yellow_off, hex = 0x333333 },
|
||||
{ c = colors.red, hex = 0xdf4949 },
|
||||
{ c = colors.red_off, hex = 0x333333 }
|
||||
},
|
||||
-- blue indicators
|
||||
{
|
||||
{ c = colors.green, hex = 0x65aeff },
|
||||
{ c = colors.green_hc, hex = 0x99c9ff },
|
||||
{ c = colors.green_off, hex = 0x365e8a },
|
||||
},
|
||||
-- standard, black backgrounds
|
||||
{
|
||||
{ c = colors.green_off, hex = 0x333333 },
|
||||
{ c = colors.yellow_off, hex = 0x333333 },
|
||||
{ c = colors.red_off, hex = 0x333333 }
|
||||
},
|
||||
-- blue indicators, black backgrounds
|
||||
{
|
||||
{ c = colors.green, hex = 0x65aeff },
|
||||
{ c = colors.green_hc, hex = 0x99c9ff },
|
||||
{ c = colors.green_off, hex = 0x333333 },
|
||||
{ c = colors.yellow_off, hex = 0x333333 },
|
||||
{ c = colors.red_off, hex = 0x333333 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-- get style fields for a front panel based on the provided theme
|
||||
---@param theme fp_theme
|
||||
function themes.get_fp_style(theme)
|
||||
---@class fp_style
|
||||
local style = {
|
||||
root = cpair(theme.text, theme.bg),
|
||||
|
||||
text = cpair(theme.text, theme.bg),
|
||||
text_fg = cpair(theme.text, colors._INHERIT),
|
||||
|
||||
label_fg = cpair(theme.label, colors._INHERIT),
|
||||
label_d_fg = cpair(theme.label_dark, colors._INHERIT),
|
||||
|
||||
disabled_fg = cpair(theme.disabled, colors._INHERIT)
|
||||
}
|
||||
|
||||
return style
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Main UI Color Palettes
|
||||
|
||||
---@class ui_palette
|
||||
themes.smooth_stone = {
|
||||
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 }
|
||||
},
|
||||
|
||||
-- color re-mappings for assistive modes
|
||||
color_modes = {
|
||||
-- standard
|
||||
{},
|
||||
-- deuteranopia
|
||||
{
|
||||
{ c = colors.blue, hex = 0x1081ff },
|
||||
{ c = colors.yellow, hex = 0xf7c311 },
|
||||
{ c = colors.red, hex = 0xfb5615 }
|
||||
},
|
||||
-- protanopia
|
||||
{
|
||||
{ c = colors.blue, hex = 0x1081ff },
|
||||
{ c = colors.yellow, hex = 0xf5e633 },
|
||||
{ c = colors.red, hex = 0xff521a }
|
||||
},
|
||||
-- tritanopia
|
||||
{
|
||||
{ c = colors.blue, hex = 0x40cbd7 },
|
||||
{ c = colors.yellow, hex = 0xffbc00 },
|
||||
{ c = colors.red, hex = 0xff0000 }
|
||||
},
|
||||
-- blue indicators
|
||||
{
|
||||
{ c = colors.blue, hex = 0x1081ff },
|
||||
{ c = colors.yellow, hex = 0xfffc79 },
|
||||
{ c = colors.red, hex = 0xdf4949 }
|
||||
},
|
||||
-- standard, black backgrounds
|
||||
{},
|
||||
-- blue indicators, black backgrounds
|
||||
{
|
||||
{ c = colors.blue, hex = 0x1081ff },
|
||||
{ c = colors.yellow, hex = 0xfffc79 },
|
||||
{ c = colors.red, hex = 0xdf4949 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
---@type ui_palette
|
||||
themes.deepslate = {
|
||||
colors = {
|
||||
{ c = colors.red, hex = 0xeb6a6c },
|
||||
{ c = colors.orange, hex = 0xf2b86c },
|
||||
{ c = colors.yellow, hex = 0xd9cf81 },
|
||||
{ c = colors.lime, hex = 0x80ff80 },
|
||||
{ c = colors.green, hex = 0x70e19b },
|
||||
{ c = colors.cyan, hex = 0x7ccdd0 },
|
||||
{ c = colors.lightBlue, hex = 0x99ceef },
|
||||
{ c = colors.blue, hex = 0x60bcff },
|
||||
{ c = colors.purple, hex = 0xc38aea },
|
||||
{ c = colors.pink, hex = 0xff7fb8 },
|
||||
{ c = colors.magenta, hex = 0xf980dd },
|
||||
{ c = colors.white, hex = 0xd9d9d9 },
|
||||
{ c = colors.lightGray, hex = 0x949494 },
|
||||
{ c = colors.gray, hex = 0x575757 },
|
||||
{ c = colors.black, hex = 0x262626 },
|
||||
{ c = colors.brown, hex = 0xb18f6a }
|
||||
},
|
||||
|
||||
-- color re-mappings for assistive modes
|
||||
color_modes = {
|
||||
-- standard
|
||||
{},
|
||||
-- deuteranopia
|
||||
{
|
||||
{ c = colors.blue, hex = 0x65aeff },
|
||||
{ c = colors.yellow, hex = 0xf7c311 },
|
||||
{ c = colors.red, hex = 0xfb5615 }
|
||||
},
|
||||
-- protanopia
|
||||
{
|
||||
{ c = colors.blue, hex = 0x65aeff },
|
||||
{ c = colors.yellow, hex = 0xf5e633 },
|
||||
{ c = colors.red, hex = 0xff8058 }
|
||||
},
|
||||
-- tritanopia
|
||||
{
|
||||
{ c = colors.blue, hex = 0x00ecff },
|
||||
{ c = colors.yellow, hex = 0xffbc00 },
|
||||
{ c = colors.red, hex = 0xdf4949 }
|
||||
},
|
||||
-- blue indicators
|
||||
{
|
||||
{ c = colors.blue, hex = 0x65aeff },
|
||||
{ c = colors.yellow, hex = 0xd9cf81 },
|
||||
{ c = colors.red, hex = 0xeb6a6c }
|
||||
},
|
||||
-- standard, black backgrounds
|
||||
{},
|
||||
-- blue indicators, black backgrounds
|
||||
{
|
||||
{ c = colors.blue, hex = 0x65aeff },
|
||||
{ c = colors.yellow, hex = 0xd9cf81 },
|
||||
{ c = colors.red, hex = 0xeb6a6c }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
--#endregion
|
||||
|
||||
return themes
|
||||
2
imgen.py
2
imgen.py
@@ -60,7 +60,7 @@ def make_manifest(size):
|
||||
},
|
||||
"files" : {
|
||||
# common files
|
||||
"system" : [ "initenv.lua", "startup.lua", "configure.lua" ],
|
||||
"system" : [ "initenv.lua", "startup.lua", "configure.lua", "LICENSE" ],
|
||||
"common" : list_files("./scada-common"),
|
||||
"graphics" : list_files("./graphics"),
|
||||
"lockbox" : list_files("./lockbox"),
|
||||
|
||||
@@ -2,9 +2,7 @@ return {
|
||||
-- initialize booted environment
|
||||
init_env = function ()
|
||||
local _require, _env = require("cc.require"), setmetatable({}, { __index = _ENV })
|
||||
-- overwrite require/package globals
|
||||
require, package = _require.make(_env, "/")
|
||||
-- reset terminal
|
||||
term.clear(); term.setCursorPos(1, 1)
|
||||
end
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
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
|
||||
-- facility authentication key (do NOT use one of your passwords)
|
||||
-- this enables verifying that messages are authentic
|
||||
-- all devices on the same network must use the same key
|
||||
-- config.AUTH_KEY = "SCADAfacility123"
|
||||
|
||||
-- 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
|
||||
565
pocket/configure.lua
Normal file
565
pocket/configure.lua
Normal file
@@ -0,0 +1,565 @@
|
||||
--
|
||||
-- Configuration GUI
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
local Div = require("graphics.elements.div")
|
||||
local ListBox = require("graphics.elements.listbox")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local CheckBox = require("graphics.elements.controls.checkbox")
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
local RadioButton = require("graphics.elements.controls.radio_button")
|
||||
|
||||
local NumberField = require("graphics.elements.form.number_field")
|
||||
local TextField = require("graphics.elements.form.text_field")
|
||||
|
||||
local println = util.println
|
||||
local tri = util.trinary
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local LEFT = core.ALIGN.LEFT
|
||||
local CENTER = core.ALIGN.CENTER
|
||||
local RIGHT = core.ALIGN.RIGHT
|
||||
|
||||
-- changes to the config data/format to let the user know
|
||||
local changes = {}
|
||||
|
||||
---@class pkt_configurator
|
||||
local configurator = {}
|
||||
|
||||
local style = {}
|
||||
|
||||
style.root = cpair(colors.black, colors.lightGray)
|
||||
style.header = cpair(colors.white, colors.gray)
|
||||
|
||||
style.colors = themes.smooth_stone.colors
|
||||
|
||||
local bw_fg_bg = cpair(colors.black, colors.white)
|
||||
local g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
|
||||
local nav_fg_bg = bw_fg_bg
|
||||
local btn_act_fg_bg = cpair(colors.white, colors.gray)
|
||||
local dis_fg_bg = cpair(colors.lightGray,colors.white)
|
||||
|
||||
local tool_ctl = {
|
||||
ask_config = false,
|
||||
has_config = false,
|
||||
viewing_config = false,
|
||||
importing_legacy = false,
|
||||
|
||||
view_cfg = nil, ---@type graphics_element
|
||||
settings_apply = nil, ---@type graphics_element
|
||||
|
||||
set_networked = nil, ---@type function
|
||||
bundled_emcool = nil, ---@type function
|
||||
gen_summary = nil, ---@type function
|
||||
show_current_cfg = nil, ---@type function
|
||||
load_legacy = nil, ---@type function
|
||||
|
||||
show_auth_key = nil, ---@type function
|
||||
show_key_btn = nil, ---@type graphics_element
|
||||
auth_key_textbox = nil, ---@type graphics_element
|
||||
auth_key_value = ""
|
||||
}
|
||||
|
||||
---@class pkt_config
|
||||
local tmp_cfg = {
|
||||
SVR_Channel = nil, ---@type integer
|
||||
CRD_Channel = nil, ---@type integer
|
||||
PKT_Channel = nil, ---@type integer
|
||||
ConnTimeout = nil, ---@type number
|
||||
TrustedRange = nil, ---@type number
|
||||
AuthKey = nil, ---@type string|nil
|
||||
LogMode = 0,
|
||||
LogPath = "",
|
||||
LogDebug = false,
|
||||
}
|
||||
|
||||
---@class pkt_config
|
||||
local ini_cfg = {}
|
||||
---@class pkt_config
|
||||
local settings_cfg = {}
|
||||
|
||||
-- all settings fields, their nice names, and their default values
|
||||
local fields = {
|
||||
{ "SVR_Channel", "SVR Channel", 16240 },
|
||||
{ "CRD_Channel", "CRD Channel", 16243 },
|
||||
{ "PKT_Channel", "PKT Channel", 16244 },
|
||||
{ "ConnTimeout", "Connection Timeout", 5 },
|
||||
{ "TrustedRange", "Trusted Range", 0 },
|
||||
{ "AuthKey", "Facility Auth Key" , ""},
|
||||
{ "LogMode", "Log Mode", log.MODE.APPEND },
|
||||
{ "LogPath", "Log Path", "/log.txt" },
|
||||
{ "LogDebug", "Log Debug Messages", false }
|
||||
}
|
||||
|
||||
-- load data from the settings file
|
||||
---@param target pkt_config
|
||||
---@param raw boolean? true to not use default values
|
||||
local function load_settings(target, raw)
|
||||
for _, v in pairs(fields) do settings.unset(v[1]) end
|
||||
|
||||
local loaded = settings.load("/pocket.settings")
|
||||
|
||||
for _, v in pairs(fields) do target[v[1]] = settings.get(v[1], tri(raw, nil, v[3])) end
|
||||
|
||||
return loaded
|
||||
end
|
||||
|
||||
-- create the config view
|
||||
---@param display graphics_element
|
||||
local function config_view(display)
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local function exit() os.queueEvent("terminate") end
|
||||
|
||||
TextBox{parent=display,y=1,text="Pocket Configurator",alignment=CENTER,height=1,fg_bg=style.header}
|
||||
|
||||
local root_pane_div = Div{parent=display,x=1,y=2}
|
||||
|
||||
local main_page = Div{parent=root_pane_div,x=1,y=1}
|
||||
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local summary = Div{parent=root_pane_div,x=1,y=1}
|
||||
local changelog = Div{parent=root_pane_div,x=1,y=1}
|
||||
|
||||
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,net_cfg,log_cfg,summary,changelog}}
|
||||
|
||||
-- Main Page
|
||||
|
||||
local y_start = 7
|
||||
|
||||
TextBox{parent=main_page,x=2,y=2,height=4,text="Welcome to the Pocket configurator! Please select one of the following options."}
|
||||
|
||||
if tool_ctl.ask_config then
|
||||
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Please configure before starting up.",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
y_start = y_start + 3
|
||||
end
|
||||
|
||||
local function view_config()
|
||||
tool_ctl.viewing_config = true
|
||||
tool_ctl.gen_summary(settings_cfg)
|
||||
tool_ctl.settings_apply.hide(true)
|
||||
main_pane.set_value(4)
|
||||
end
|
||||
|
||||
if fs.exists("/pocket/config.lua") then
|
||||
PushButton{parent=main_page,x=2,y=y_start,min_width=22,text="Import Legacy Config",callback=function()tool_ctl.load_legacy()end,fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=btn_act_fg_bg}
|
||||
y_start = y_start + 2
|
||||
end
|
||||
|
||||
PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure Device",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg}
|
||||
|
||||
if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end
|
||||
|
||||
PushButton{parent=main_page,x=2,y=18,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=main_page,x=14,y=18,min_width=12,text="Change Log",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#region Network
|
||||
|
||||
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||
|
||||
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
|
||||
|
||||
TextBox{parent=net_cfg,x=1,y=2,height=1,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=1,height=1,text="Set network channels."}
|
||||
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the named channels must be the same within a particular SCADA network.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=8,height=1,width=18,text="Supervisor Channel"}
|
||||
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=10,height=1,width=19,text="Coordinator Channel"}
|
||||
local crd_chan = NumberField{parent=net_c_1,x=1,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=12,height=1,width=14,text="Pocket Channel"}
|
||||
local pkt_chan = NumberField{parent=net_c_1,x=1,y=13,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=13,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local chan_err = TextBox{parent=net_c_1,x=1,y=14,height=1,width=24,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_channels()
|
||||
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
|
||||
if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then
|
||||
tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c
|
||||
net_pane.set_value(2)
|
||||
chan_err.hide(true)
|
||||
else chan_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,x=19,y=15,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=1,height=1,text="Set connection timeout."}
|
||||
TextBox{parent=net_c_2,x=1,y=3,height=7,text="You generally should not need to modify this. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=11,height=1,width=19,text="Connection Timeout"}
|
||||
local timeout = NumberField{parent=net_c_2,x=1,y=12,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=9,y=12,height=2,text="seconds\n(default 5)",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local ct_err = TextBox{parent=net_c_2,x=1,y=14,height=1,width=24,text="Please set timeout.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_timeouts()
|
||||
local timeout_val = tonumber(timeout.get_value())
|
||||
if timeout_val ~= nil then
|
||||
tmp_cfg.ConnTimeout = timeout_val
|
||||
net_pane.set_value(3)
|
||||
ct_err.hide(true)
|
||||
else ct_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_2,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,x=19,y=15,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,x=1,y=1,height=1,text="Set the trusted range."}
|
||||
TextBox{parent=net_c_3,x=1,y=3,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many blocks away.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_3,x=1,y=8,height=4,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local range = NumberField{parent=net_c_3,x=1,y=13,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
|
||||
local tr_err = TextBox{parent=net_c_3,x=1,y=14,height=1,width=24,text="Set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_tr()
|
||||
local range_val = tonumber(range.get_value())
|
||||
if range_val ~= nil then
|
||||
tmp_cfg.TrustedRange = range_val
|
||||
net_pane.set_value(4)
|
||||
tr_err.hide(true)
|
||||
else tr_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_3,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,x=19,y=15,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_4,x=1,y=1,height=4,text="Optionally, set the facility authentication key. Do NOT use one of your passwords."}
|
||||
TextBox{parent=net_c_4,x=1,y=6,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_4,x=1,y=12,height=1,text="Facility Auth Key"}
|
||||
local key, _, censor = TextField{parent=net_c_4,x=1,y=13,max_len=64,value=ini_cfg.AuthKey,width=24,height=1,fg_bg=bw_fg_bg}
|
||||
|
||||
local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
|
||||
|
||||
-- declare back first so tabbing makes sense visually
|
||||
PushButton{parent=net_c_4,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
local hide_key = CheckBox{parent=net_c_4,x=8,y=15,label="Hide Key",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
|
||||
|
||||
hide_key.set_value(true)
|
||||
censor_key(true)
|
||||
|
||||
local key_err = TextBox{parent=net_c_4,x=1,y=14,height=1,width=24,text="Length must be > 7.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_auth()
|
||||
local v = key.get_value()
|
||||
if string.len(v) == 0 or string.len(v) >= 8 then
|
||||
tmp_cfg.AuthKey = key.get_value()
|
||||
main_pane.set_value(3)
|
||||
key_err.hide(true)
|
||||
else key_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_4,x=19,y=15,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Logging
|
||||
|
||||
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=24}
|
||||
|
||||
TextBox{parent=log_cfg,x=1,y=2,height=1,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=1,height=1,text="Configure logging below."}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=3,height=1,text="Log File Mode"}
|
||||
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=7,height=1,text="Log File Path"}
|
||||
local path = TextField{parent=log_c_1,x=1,y=8,width=24,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||
|
||||
local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||
TextBox{parent=log_c_1,x=3,y=11,height=4,text="This results in much larger log files. Use only as needed.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local path_err = TextBox{parent=log_c_1,x=1,y=14,height=1,width=24,text="Provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_log()
|
||||
if path.get_value() ~= "" then
|
||||
path_err.hide(true)
|
||||
tmp_cfg.LogMode = mode.get_value() - 1
|
||||
tmp_cfg.LogPath = path.get_value()
|
||||
tmp_cfg.LogDebug = en_dbg.get_value()
|
||||
tool_ctl.gen_summary(tmp_cfg)
|
||||
tool_ctl.viewing_config = false
|
||||
tool_ctl.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
main_pane.set_value(4)
|
||||
else path_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=log_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,x=19,y=15,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Summary and Saving
|
||||
|
||||
local sum_c_1 = Div{parent=summary,x=2,y=4,width=24}
|
||||
local sum_c_2 = Div{parent=summary,x=2,y=4,width=24}
|
||||
local sum_c_3 = Div{parent=summary,x=2,y=4,width=24}
|
||||
local sum_c_4 = Div{parent=summary,x=2,y=4,width=24}
|
||||
|
||||
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
|
||||
|
||||
TextBox{parent=summary,x=1,y=2,height=1,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||
|
||||
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=11,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local function back_from_summary()
|
||||
if tool_ctl.viewing_config or tool_ctl.importing_legacy then
|
||||
main_pane.set_value(1)
|
||||
tool_ctl.viewing_config = false
|
||||
tool_ctl.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
else
|
||||
main_pane.set_value(3)
|
||||
end
|
||||
end
|
||||
|
||||
---@param element graphics_element
|
||||
---@param data any
|
||||
local function try_set(element, data)
|
||||
if data ~= nil then element.set_value(data) end
|
||||
end
|
||||
|
||||
local function save_and_continue()
|
||||
for k, v in pairs(tmp_cfg) do settings.set(k, v) end
|
||||
|
||||
if settings.save("/pocket.settings") then
|
||||
load_settings(settings_cfg, true)
|
||||
load_settings(ini_cfg)
|
||||
|
||||
try_set(svr_chan, ini_cfg.SVR_Channel)
|
||||
try_set(crd_chan, ini_cfg.CRD_Channel)
|
||||
try_set(pkt_chan, ini_cfg.PKT_Channel)
|
||||
try_set(timeout, ini_cfg.ConnTimeout)
|
||||
try_set(range, ini_cfg.TrustedRange)
|
||||
try_set(key, ini_cfg.AuthKey)
|
||||
try_set(mode, ini_cfg.LogMode)
|
||||
try_set(path, ini_cfg.LogPath)
|
||||
try_set(en_dbg, ini_cfg.LogDebug)
|
||||
|
||||
tool_ctl.view_cfg.enable()
|
||||
|
||||
if tool_ctl.importing_legacy then
|
||||
tool_ctl.importing_legacy = false
|
||||
sum_pane.set_value(3)
|
||||
else
|
||||
sum_pane.set_value(2)
|
||||
end
|
||||
else
|
||||
sum_pane.set_value(4)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_1,x=1,y=15,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=1,y=13,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg}
|
||||
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=18,y=15,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=sum_c_2,x=1,y=1,height=1,text="Settings saved!"}
|
||||
|
||||
local function go_home()
|
||||
main_pane.set_value(1)
|
||||
net_pane.set_value(1)
|
||||
sum_pane.set_value(1)
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_2,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_2,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_3,x=1,y=1,height=4,text="The old config.lua file will now be deleted, then the configurator will exit."}
|
||||
|
||||
local function delete_legacy()
|
||||
fs.delete("/pocket/config.lua")
|
||||
exit()
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_3,x=1,y=15,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_3,x=19,y=15,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_4,x=1,y=1,height=8,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=sum_c_4,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_4,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
--#endregion
|
||||
|
||||
-- Config Change Log
|
||||
|
||||
local cl = Div{parent=changelog,x=2,y=4,width=24}
|
||||
|
||||
TextBox{parent=changelog,x=1,y=2,height=1,text=" Config Change Log",fg_bg=bw_fg_bg}
|
||||
|
||||
local c_log = ListBox{parent=cl,x=1,y=1,height=13,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
for _, change in ipairs(changes) do
|
||||
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg}
|
||||
for _, v in ipairs(change[2]) do
|
||||
local e = Div{parent=c_log,height=#util.strwrap(v,21)}
|
||||
TextBox{parent=e,y=1,x=1,text="- ",height=1,fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=cl,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
-- set tool functions now that we have the elements
|
||||
|
||||
-- load a legacy config file
|
||||
function tool_ctl.load_legacy()
|
||||
local config = require("pocket.config")
|
||||
|
||||
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
|
||||
tmp_cfg.CRD_Channel = config.CRD_CHANNEL
|
||||
tmp_cfg.PKT_Channel = config.PKT_CHANNEL
|
||||
tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT
|
||||
tmp_cfg.TrustedRange = config.TRUSTED_RANGE
|
||||
tmp_cfg.AuthKey = config.AUTH_KEY or ""
|
||||
|
||||
tmp_cfg.LogMode = config.LOG_MODE
|
||||
tmp_cfg.LogPath = config.LOG_PATH
|
||||
tmp_cfg.LogDebug = config.LOG_DEBUG or false
|
||||
|
||||
tool_ctl.gen_summary(tmp_cfg)
|
||||
sum_pane.set_value(1)
|
||||
main_pane.set_value(4)
|
||||
tool_ctl.importing_legacy = true
|
||||
end
|
||||
|
||||
-- expose the auth key on the summary page
|
||||
function tool_ctl.show_auth_key()
|
||||
tool_ctl.show_key_btn.disable()
|
||||
tool_ctl.auth_key_textbox.set_value(tool_ctl.auth_key_value)
|
||||
end
|
||||
|
||||
-- generate the summary list
|
||||
---@param cfg pkt_config
|
||||
function tool_ctl.gen_summary(cfg)
|
||||
setting_list.remove_all()
|
||||
|
||||
local alternate = false
|
||||
local inner_width = setting_list.get_width() - 1
|
||||
|
||||
tool_ctl.show_key_btn.enable()
|
||||
tool_ctl.auth_key_value = cfg.AuthKey or "" -- to show auth key
|
||||
|
||||
for i = 1, #fields do
|
||||
local f = fields[i]
|
||||
local height = 1
|
||||
local label_w = string.len(f[2])
|
||||
local val_max_w = (inner_width - label_w) - 1
|
||||
local raw = cfg[f[1]]
|
||||
local val = util.strval(raw)
|
||||
|
||||
if f[1] == "AuthKey" then val = string.rep("*", string.len(val))
|
||||
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end
|
||||
|
||||
if val == "nil" then val = "<not set>" end
|
||||
|
||||
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
|
||||
alternate = not alternate
|
||||
|
||||
if string.len(val) > val_max_w then
|
||||
local lines = util.strwrap(val, inner_width)
|
||||
height = #lines + 1
|
||||
end
|
||||
|
||||
local line = Div{parent=setting_list,height=height,fg_bg=c}
|
||||
TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)}
|
||||
|
||||
local textbox
|
||||
if height > 1 then
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
|
||||
else
|
||||
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||
end
|
||||
|
||||
if f[1] == "AuthKey" then tool_ctl.auth_key_textbox = textbox end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- reset terminal screen
|
||||
local function reset_term()
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
end
|
||||
|
||||
-- run the pcoket configurator
|
||||
---@param ask_config? boolean indicate if this is being called by the startup app due to an invalid configuration
|
||||
function configurator.configure(ask_config)
|
||||
tool_ctl.ask_config = ask_config == true
|
||||
|
||||
load_settings(settings_cfg, true)
|
||||
tool_ctl.has_config = load_settings(ini_cfg)
|
||||
|
||||
reset_term()
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.colors do
|
||||
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||
end
|
||||
|
||||
local status, error = pcall(function ()
|
||||
local display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||
config_view(display)
|
||||
|
||||
while true do
|
||||
local event, param1, param2, param3 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
if event == "timer" then
|
||||
tcd.handle(param1)
|
||||
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
|
||||
local m_e = core.events.new_mouse_event(event, param1, param2, param3)
|
||||
if m_e then display.handle_mouse(m_e) end
|
||||
elseif event == "char" or event == "key" or event == "key_up" then
|
||||
local k_e = core.events.new_key_event(event, param1, param2)
|
||||
if k_e then display.handle_key(k_e) end
|
||||
elseif event == "paste" then
|
||||
display.handle_paste(param1)
|
||||
end
|
||||
|
||||
if event == "terminate" then return end
|
||||
end
|
||||
end)
|
||||
|
||||
-- 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_term()
|
||||
if not status then
|
||||
println("configurator error: " .. error)
|
||||
end
|
||||
|
||||
return status, error
|
||||
end
|
||||
|
||||
return configurator
|
||||
@@ -2,19 +2,20 @@
|
||||
-- I/O Control for Pocket Integration with Supervisor & Coordinator
|
||||
--
|
||||
|
||||
local psil = require("scada-common.psil")
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local psil = require("scada-common.psil")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local ALARM = types.ALARM
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
|
||||
---@todo nominal trip time is ping (0ms to 10ms usually)
|
||||
local WARN_TT = 40
|
||||
local HIGH_TT = 80
|
||||
|
||||
local iocontrol = {}
|
||||
|
||||
---@class pocket_ioctl
|
||||
local io = {
|
||||
ps = psil.create()
|
||||
}
|
||||
|
||||
---@enum POCKET_LINK_STATE
|
||||
local LINK_STATE = {
|
||||
UNLINKED = 0,
|
||||
@@ -23,23 +24,203 @@ local LINK_STATE = {
|
||||
LINKED = 3
|
||||
}
|
||||
|
||||
---@enum NAV_PAGE
|
||||
local NAV_PAGE = {
|
||||
HOME = 1,
|
||||
iocontrol.LINK_STATE = LINK_STATE
|
||||
|
||||
---@enum POCKET_APP_ID
|
||||
local APP_ID = {
|
||||
ROOT = 1,
|
||||
-- main app page
|
||||
UNITS = 2,
|
||||
REACTORS = 3,
|
||||
BOILERS = 4,
|
||||
TURBINES = 5,
|
||||
DIAG = 6,
|
||||
D_ALARMS = 7
|
||||
ABOUT = 3,
|
||||
-- diag app page
|
||||
ALARMS = 4,
|
||||
-- other
|
||||
DUMMY = 5,
|
||||
NUM_APPS = 5
|
||||
}
|
||||
|
||||
iocontrol.LINK_STATE = LINK_STATE
|
||||
iocontrol.NAV_PAGE = NAV_PAGE
|
||||
iocontrol.APP_ID = APP_ID
|
||||
|
||||
---@class pocket_ioctl
|
||||
local io = {
|
||||
version = "unknown",
|
||||
ps = psil.create()
|
||||
}
|
||||
|
||||
---@class nav_tree_page
|
||||
---@field _p nav_tree_page|nil page's parent
|
||||
---@field _c table page's children
|
||||
---@field nav_to function function to navigate to this page
|
||||
---@field switcher function|nil function to switch between children
|
||||
---@field tasks table tasks to run while viewing this page
|
||||
|
||||
-- allocate the page navigation system
|
||||
function iocontrol.alloc_nav()
|
||||
local self = {
|
||||
pane = nil, ---@type graphics_element
|
||||
apps = {},
|
||||
containers = {},
|
||||
cur_app = APP_ID.ROOT
|
||||
}
|
||||
|
||||
self.cur_page = self.root
|
||||
|
||||
---@class pocket_nav
|
||||
io.nav = {}
|
||||
|
||||
-- set the root pane element to switch between apps with
|
||||
---@param root_pane graphics_element
|
||||
function io.nav.set_pane(root_pane)
|
||||
self.pane = root_pane
|
||||
end
|
||||
|
||||
function io.nav.set_sidebar(sidebar)
|
||||
self.sidebar = sidebar
|
||||
end
|
||||
|
||||
-- register an app
|
||||
---@param app_id POCKET_APP_ID app ID
|
||||
---@param container graphics_element element that contains this app (usually a Div)
|
||||
---@param pane graphics_element? multipane if this is a simple paned app, then nav_to must be a number
|
||||
function io.nav.register_app(app_id, container, pane)
|
||||
---@class pocket_app
|
||||
local app = {
|
||||
loaded = false,
|
||||
load = nil,
|
||||
cur_page = nil, ---@type nav_tree_page
|
||||
pane = pane,
|
||||
paned_pages = {},
|
||||
sidebar_items = {}
|
||||
}
|
||||
|
||||
app.load = function () app.loaded = true end
|
||||
|
||||
-- delayed set of the pane if it wasn't ready at the start
|
||||
---@param root_pane graphics_element multipane
|
||||
function app.set_root_pane(root_pane)
|
||||
app.pane = root_pane
|
||||
end
|
||||
|
||||
function app.set_sidebar(items)
|
||||
app.sidebar_items = items
|
||||
if self.sidebar then self.sidebar.update(items) end
|
||||
end
|
||||
|
||||
-- function to run on initial load into memory
|
||||
---@param on_load function callback
|
||||
function app.set_on_load(on_load)
|
||||
app.load = function ()
|
||||
on_load()
|
||||
app.loaded = true
|
||||
end
|
||||
end
|
||||
|
||||
-- if a pane was provided, this will switch between numbered pages
|
||||
---@param idx integer page index
|
||||
function app.switcher(idx)
|
||||
if app.paned_pages[idx] then
|
||||
app.paned_pages[idx].nav_to()
|
||||
end
|
||||
end
|
||||
|
||||
-- create a new page entry in the app's page navigation tree
|
||||
---@param parent nav_tree_page? a parent page or nil to set this as the root
|
||||
---@param nav_to function|integer function to navigate to this page or pane index
|
||||
---@return nav_tree_page new_page this new page
|
||||
function app.new_page(parent, nav_to)
|
||||
---@type nav_tree_page
|
||||
local page = { _p = parent, _c = {}, nav_to = function () end, switcher = function () end, tasks = {} }
|
||||
|
||||
if parent == nil and app.cur_page == nil then
|
||||
app.cur_page = page
|
||||
end
|
||||
|
||||
if type(nav_to) == "number" then
|
||||
app.paned_pages[nav_to] = page
|
||||
|
||||
function page.nav_to()
|
||||
app.cur_page = page
|
||||
if app.pane then app.pane.set_value(nav_to) end
|
||||
end
|
||||
else
|
||||
function page.nav_to()
|
||||
app.cur_page = page
|
||||
nav_to()
|
||||
end
|
||||
end
|
||||
|
||||
-- switch between children
|
||||
---@param id integer child ID
|
||||
function page.switcher(id) if page._c[id] then page._c[id].nav_to() end end
|
||||
|
||||
if parent ~= nil then
|
||||
table.insert(page._p._c, page)
|
||||
end
|
||||
|
||||
return page
|
||||
end
|
||||
|
||||
-- get the currently active page
|
||||
function app.get_current_page() return app.cur_page end
|
||||
|
||||
-- attempt to navigate up the tree
|
||||
---@return boolean success true if successfully navigated up
|
||||
function app.nav_up()
|
||||
local parent = app.cur_page._p
|
||||
if parent then parent.nav_to() end
|
||||
return parent ~= nil
|
||||
end
|
||||
|
||||
self.apps[app_id] = app
|
||||
self.containers[app_id] = container
|
||||
|
||||
return app
|
||||
end
|
||||
|
||||
-- get a list of the app containers (usually Div elements)
|
||||
function io.nav.get_containers() return self.containers end
|
||||
|
||||
-- open a given app
|
||||
---@param app_id POCKET_APP_ID
|
||||
function io.nav.open_app(app_id)
|
||||
local app = self.apps[app_id] ---@type pocket_app
|
||||
if app then
|
||||
if not app.loaded then app.load() end
|
||||
|
||||
self.cur_app = app_id
|
||||
self.pane.set_value(app_id)
|
||||
|
||||
if #app.sidebar_items > 0 then
|
||||
self.sidebar.update(app.sidebar_items)
|
||||
end
|
||||
else
|
||||
log.debug("tried to open unknown app")
|
||||
end
|
||||
end
|
||||
|
||||
-- get the currently active page
|
||||
---@return nav_tree_page
|
||||
function io.nav.get_current_page()
|
||||
return self.apps[self.cur_app].get_current_page()
|
||||
end
|
||||
|
||||
-- attempt to navigate up
|
||||
function io.nav.nav_up()
|
||||
local app = self.apps[self.cur_app] ---@type pocket_app
|
||||
log.debug("attempting app nav up for app " .. self.cur_app)
|
||||
|
||||
if not app.nav_up() then
|
||||
log.debug("internal app nav up failed, going to home screen")
|
||||
io.nav.open_app(APP_ID.ROOT)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- initialize facility-independent components of pocket iocontrol
|
||||
---@param comms pocket_comms
|
||||
function iocontrol.init_core(comms)
|
||||
iocontrol.alloc_nav()
|
||||
|
||||
---@class pocket_ioctl_diag
|
||||
io.diag = {}
|
||||
|
||||
@@ -77,28 +258,514 @@ function iocontrol.init_core(comms)
|
||||
tone_indicators = {} -- indicators to update from supervisor tone states
|
||||
}
|
||||
|
||||
---@class pocket_nav
|
||||
io.nav = {
|
||||
page = NAV_PAGE.HOME, ---@type NAV_PAGE
|
||||
sub_pages = { NAV_PAGE.HOME, NAV_PAGE.UNITS, NAV_PAGE.REACTORS, NAV_PAGE.BOILERS, NAV_PAGE.TURBINES, NAV_PAGE.DIAG },
|
||||
tasks = {}
|
||||
-- API access
|
||||
---@class pocket_ioctl_api
|
||||
io.api = {
|
||||
get_unit = function (unit) comms.api__get_unit(unit) end
|
||||
}
|
||||
|
||||
-- add a task to be performed periodically while on a given page
|
||||
---@param page NAV_PAGE page to add task to
|
||||
---@param task function function to execute
|
||||
function io.nav.register_task(page, task)
|
||||
if io.nav.tasks[page] == nil then io.nav.tasks[page] = {} end
|
||||
table.insert(io.nav.tasks[page], task)
|
||||
end
|
||||
end
|
||||
|
||||
-- initialize facility-dependent components of pocket iocontrol
|
||||
function iocontrol.init_fac() end
|
||||
---@param conf facility_conf configuration
|
||||
---@param temp_scale 1|2|3|4 temperature unit (1 = K, 2 = C, 3 = F, 4 = R)
|
||||
function iocontrol.init_fac(conf, temp_scale)
|
||||
-- temperature unit label and conversion function (from Kelvin)
|
||||
if temp_scale == 2 then
|
||||
io.temp_label = "\xb0C"
|
||||
io.temp_convert = function (t) return t - 273.15 end
|
||||
elseif temp_scale == 3 then
|
||||
io.temp_label = "\xb0F"
|
||||
io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end
|
||||
elseif temp_scale == 4 then
|
||||
io.temp_label = "\xb0R"
|
||||
io.temp_convert = function (t) return 1.8 * t end
|
||||
else
|
||||
io.temp_label = "K"
|
||||
io.temp_convert = function (t) return t end
|
||||
end
|
||||
|
||||
-- facility data structure
|
||||
---@class pioctl_facility
|
||||
io.facility = {
|
||||
num_units = conf.num_units,
|
||||
tank_mode = conf.cooling.fac_tank_mode,
|
||||
tank_defs = conf.cooling.fac_tank_defs,
|
||||
all_sys_ok = false,
|
||||
rtu_count = 0,
|
||||
|
||||
auto_ready = false,
|
||||
auto_active = false,
|
||||
auto_ramping = false,
|
||||
auto_saturated = false,
|
||||
|
||||
auto_scram = false,
|
||||
---@type ascram_status
|
||||
ascram_status = {
|
||||
matrix_dc = false,
|
||||
matrix_fill = false,
|
||||
crit_alarm = false,
|
||||
radiation = false,
|
||||
gen_fault = false
|
||||
},
|
||||
|
||||
---@type WASTE_PRODUCT
|
||||
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
|
||||
auto_pu_fallback_active = false,
|
||||
|
||||
radiation = types.new_zero_radiation_reading(),
|
||||
|
||||
ps = psil.create(),
|
||||
|
||||
induction_ps_tbl = {},
|
||||
induction_data_tbl = {},
|
||||
|
||||
sps_ps_tbl = {},
|
||||
sps_data_tbl = {},
|
||||
|
||||
tank_ps_tbl = {},
|
||||
tank_data_tbl = {},
|
||||
|
||||
env_d_ps = psil.create(),
|
||||
env_d_data = {}
|
||||
}
|
||||
|
||||
-- create induction and SPS tables (currently only 1 of each is supported)
|
||||
table.insert(io.facility.induction_ps_tbl, psil.create())
|
||||
table.insert(io.facility.induction_data_tbl, {})
|
||||
table.insert(io.facility.sps_ps_tbl, psil.create())
|
||||
table.insert(io.facility.sps_data_tbl, {})
|
||||
|
||||
-- determine tank information
|
||||
if io.facility.tank_mode == 0 then
|
||||
io.facility.tank_defs = {}
|
||||
-- on facility tank mode 0, setup tank defs to match unit tank option
|
||||
for i = 1, conf.num_units do
|
||||
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TankConnection, 1, 0)
|
||||
end
|
||||
|
||||
io.facility.tank_list = { table.unpack(io.facility.tank_defs) }
|
||||
else
|
||||
-- decode the layout of tanks from the connections definitions
|
||||
local tank_mode = io.facility.tank_mode
|
||||
local tank_defs = io.facility.tank_defs
|
||||
local tank_list = { table.unpack(tank_defs) }
|
||||
|
||||
local function calc_fdef(start_idx, end_idx)
|
||||
local first = 4
|
||||
for i = start_idx, end_idx do
|
||||
if io.facility.tank_defs[i] == 2 then
|
||||
if i < first then first = i end
|
||||
end
|
||||
end
|
||||
return first
|
||||
end
|
||||
|
||||
if tank_mode == 1 then
|
||||
-- (1) 1 total facility tank (A A A A)
|
||||
local first_fdef = calc_fdef(1, #tank_defs)
|
||||
for i = 1, #tank_defs do
|
||||
if i > first_fdef and tank_defs[i] == 2 then
|
||||
tank_list[i] = 0
|
||||
end
|
||||
end
|
||||
elseif tank_mode == 2 then
|
||||
-- (2) 2 total facility tanks (A A A B)
|
||||
local first_fdef = calc_fdef(1, math.min(3, #tank_defs))
|
||||
for i = 1, #tank_defs do
|
||||
if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then
|
||||
tank_list[i] = 0
|
||||
end
|
||||
end
|
||||
elseif tank_mode == 3 then
|
||||
-- (3) 2 total facility tanks (A A B B)
|
||||
for _, a in pairs({ 1, 3 }) do
|
||||
local b = a + 1
|
||||
if (tank_defs[a] == 2) and (tank_defs[b] == 2) then
|
||||
tank_list[b] = 0
|
||||
end
|
||||
end
|
||||
elseif tank_mode == 4 then
|
||||
-- (4) 2 total facility tanks (A B B B)
|
||||
local first_fdef = calc_fdef(2, #tank_defs)
|
||||
for i = 1, #tank_defs do
|
||||
if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then
|
||||
tank_list[i] = 0
|
||||
end
|
||||
end
|
||||
elseif tank_mode == 5 then
|
||||
-- (5) 3 total facility tanks (A A B C)
|
||||
local first_fdef = calc_fdef(1, math.min(2, #tank_defs))
|
||||
for i = 1, #tank_defs do
|
||||
if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
|
||||
tank_list[i] = 0
|
||||
end
|
||||
end
|
||||
elseif tank_mode == 6 then
|
||||
-- (6) 3 total facility tanks (A B B C)
|
||||
local first_fdef = calc_fdef(2, math.min(3, #tank_defs))
|
||||
for i = 1, #tank_defs do
|
||||
if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
|
||||
tank_list[i] = 0
|
||||
end
|
||||
end
|
||||
elseif tank_mode == 7 then
|
||||
-- (7) 3 total facility tanks (A B C C)
|
||||
local first_fdef = calc_fdef(3, #tank_defs)
|
||||
for i = 1, #tank_defs do
|
||||
if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then
|
||||
tank_list[i] = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
io.facility.tank_list = tank_list
|
||||
end
|
||||
|
||||
-- create facility tank tables
|
||||
for i = 1, #io.facility.tank_list do
|
||||
if io.facility.tank_list[i] == 2 then
|
||||
table.insert(io.facility.tank_ps_tbl, psil.create())
|
||||
table.insert(io.facility.tank_data_tbl, {})
|
||||
end
|
||||
end
|
||||
|
||||
-- create unit data structures
|
||||
io.units = {}
|
||||
for i = 1, conf.num_units do
|
||||
---@class pioctl_unit
|
||||
local entry = {
|
||||
unit_id = i,
|
||||
connected = false,
|
||||
rtu_hw = {},
|
||||
|
||||
num_boilers = 0,
|
||||
num_turbines = 0,
|
||||
num_snas = 0,
|
||||
has_tank = conf.cooling.r_cool[i].TankConnection,
|
||||
|
||||
control_state = false,
|
||||
burn_rate_cmd = 0.0,
|
||||
radiation = types.new_zero_radiation_reading(),
|
||||
|
||||
sna_peak_rate = 0.0,
|
||||
sna_max_rate = 0.0,
|
||||
sna_out_rate = 0.0,
|
||||
|
||||
waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
|
||||
waste_product = types.WASTE_PRODUCT.PLUTONIUM,
|
||||
|
||||
-- auto control group
|
||||
a_group = 0,
|
||||
|
||||
---@type alarms
|
||||
alarms = { ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE },
|
||||
|
||||
annunciator = {}, ---@type annunciator
|
||||
|
||||
unit_ps = psil.create(),
|
||||
reactor_data = {}, ---@type reactor_db
|
||||
|
||||
boiler_ps_tbl = {},
|
||||
boiler_data_tbl = {},
|
||||
|
||||
turbine_ps_tbl = {},
|
||||
turbine_data_tbl = {},
|
||||
|
||||
tank_ps_tbl = {},
|
||||
tank_data_tbl = {}
|
||||
}
|
||||
|
||||
-- on other facility modes, overwrite unit TANK option with facility tank defs
|
||||
if io.facility.tank_mode ~= 0 then
|
||||
entry.has_tank = conf.cooling.fac_tank_defs[i] > 0
|
||||
end
|
||||
|
||||
-- create boiler tables
|
||||
for _ = 1, conf.cooling.r_cool[i].BoilerCount do
|
||||
table.insert(entry.boiler_ps_tbl, psil.create())
|
||||
table.insert(entry.boiler_data_tbl, {})
|
||||
end
|
||||
|
||||
-- create turbine tables
|
||||
for _ = 1, conf.cooling.r_cool[i].TurbineCount do
|
||||
table.insert(entry.turbine_ps_tbl, psil.create())
|
||||
table.insert(entry.turbine_data_tbl, {})
|
||||
end
|
||||
|
||||
-- create tank tables
|
||||
if io.facility.tank_defs[i] == 1 then
|
||||
table.insert(entry.tank_ps_tbl, psil.create())
|
||||
table.insert(entry.tank_data_tbl, {})
|
||||
end
|
||||
|
||||
entry.num_boilers = #entry.boiler_data_tbl
|
||||
entry.num_turbines = #entry.turbine_data_tbl
|
||||
|
||||
table.insert(io.units, entry)
|
||||
end
|
||||
end
|
||||
|
||||
-- set network link state
|
||||
---@param state POCKET_LINK_STATE
|
||||
function iocontrol.report_link_state(state) io.ps.publish("link_state", state) end
|
||||
---@param sv_addr integer? supervisor address if linked
|
||||
---@param api_addr integer? coordinator address if linked
|
||||
function iocontrol.report_link_state(state, sv_addr, api_addr)
|
||||
io.ps.publish("link_state", state)
|
||||
|
||||
if state == LINK_STATE.API_LINK_ONLY or state == LINK_STATE.UNLINKED then
|
||||
io.ps.publish("svr_conn_quality", 0)
|
||||
end
|
||||
|
||||
if state == LINK_STATE.SV_LINK_ONLY or state == LINK_STATE.UNLINKED then
|
||||
io.ps.publish("crd_conn_quality", 0)
|
||||
end
|
||||
|
||||
if state == LINK_STATE.LINKED then
|
||||
io.ps.publish("sv_addr", sv_addr)
|
||||
io.ps.publish("api_addr", api_addr)
|
||||
end
|
||||
end
|
||||
|
||||
-- determine supervisor connection quality (trip time)
|
||||
---@param trip_time integer
|
||||
function iocontrol.report_svr_tt(trip_time)
|
||||
local state = 3
|
||||
if trip_time > HIGH_TT then
|
||||
state = 1
|
||||
elseif trip_time > WARN_TT then
|
||||
state = 2
|
||||
end
|
||||
|
||||
io.ps.publish("svr_conn_quality", state)
|
||||
end
|
||||
|
||||
-- determine coordinator connection quality (trip time)
|
||||
---@param trip_time integer
|
||||
function iocontrol.report_crd_tt(trip_time)
|
||||
local state = 3
|
||||
if trip_time > HIGH_TT then
|
||||
state = 1
|
||||
elseif trip_time > WARN_TT then
|
||||
state = 2
|
||||
end
|
||||
|
||||
io.ps.publish("crd_conn_quality", state)
|
||||
end
|
||||
|
||||
-- populate facility data from API_GET_FAC
|
||||
---@param data table
|
||||
---@return boolean valid
|
||||
function iocontrol.record_facility_data(data)
|
||||
local valid = true
|
||||
|
||||
local fac = io.facility
|
||||
|
||||
fac.all_sys_ok = data[1]
|
||||
fac.rtu_count = data[2]
|
||||
fac.radiation = data[3]
|
||||
|
||||
-- auto control
|
||||
if type(data[4]) == "table" and #data[4] == 4 then
|
||||
fac.auto_ready = data[4][1]
|
||||
fac.auto_active = data[4][2]
|
||||
fac.auto_ramping = data[4][3]
|
||||
fac.auto_saturated = data[4][4]
|
||||
end
|
||||
|
||||
-- waste
|
||||
if type(data[5]) == "table" and #data[5] == 2 then
|
||||
fac.auto_current_waste_product = data[5][1]
|
||||
fac.auto_pu_fallback_active = data[5][2]
|
||||
end
|
||||
|
||||
fac.num_tanks = data[6]
|
||||
fac.has_imatrix = data[7]
|
||||
fac.has_sps = data[8]
|
||||
|
||||
return valid
|
||||
end
|
||||
|
||||
-- update unit status data from API_GET_UNIT
|
||||
---@param data table
|
||||
function iocontrol.record_unit_data(data)
|
||||
if type(data[1]) == "number" and io.units[data[1]] then
|
||||
local unit = io.units[data[1]] ---@type pioctl_unit
|
||||
|
||||
unit.connected = data[2]
|
||||
unit.rtu_hw = data[3]
|
||||
unit.alarms = data[4]
|
||||
|
||||
--#region Annunciator
|
||||
|
||||
unit.annunciator = data[5]
|
||||
|
||||
local rcs_disconn, rcs_warn, rcs_hazard = false, false, false
|
||||
|
||||
for key, val in pairs(unit.annunciator) do
|
||||
if key == "BoilerOnline" or key == "TurbineOnline" then
|
||||
-- split up online arrays
|
||||
local every = true
|
||||
for id = 1, #val do
|
||||
every = every and val[id]
|
||||
unit.boiler_ps_tbl[id].publish(key, val[id])
|
||||
end
|
||||
|
||||
if not every then rcs_disconn = true end
|
||||
|
||||
unit.unit_ps.publish("U_" .. key, every)
|
||||
elseif key == "HeatingRateLow" or key == "WaterLevelLow" then
|
||||
-- split up array for all boilers
|
||||
local any = false
|
||||
for id = 1, #val do
|
||||
any = any or val[id]
|
||||
unit.boiler_ps_tbl[id].publish(key, val[id])
|
||||
end
|
||||
|
||||
if key == "HeatingRateLow" and any then
|
||||
rcs_warn = true
|
||||
elseif key == "WaterLevelLow" and any then
|
||||
rcs_hazard = true
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_" .. key, any)
|
||||
elseif key == "SteamDumpOpen" or key == "TurbineOverSpeed" or key == "GeneratorTrip" or key == "TurbineTrip" then
|
||||
-- split up array for all turbines
|
||||
local any = false
|
||||
for id = 1, #val do
|
||||
any = any or val[id]
|
||||
unit.turbine_ps_tbl[id].publish(key, val[id])
|
||||
end
|
||||
|
||||
if key == "GeneratorTrip" and any then
|
||||
rcs_warn = true
|
||||
elseif (key == "TurbineOverSpeed" or key == "TurbineTrip") and any then
|
||||
rcs_hazard = true
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_" .. key, any)
|
||||
else
|
||||
-- non-table fields
|
||||
unit.unit_ps.publish(key, val)
|
||||
end
|
||||
end
|
||||
|
||||
local anc = unit.annunciator
|
||||
rcs_hazard = rcs_hazard or anc.RCPTrip
|
||||
rcs_warn = rcs_warn or anc.RCSFlowLow or anc.CoolantLevelLow or anc.RCSFault or anc.MaxWaterReturnFeed or
|
||||
anc.CoolantFeedMismatch or anc.BoilRateMismatch or anc.SteamFeedMismatch or anc.MaxWaterReturnFeed
|
||||
|
||||
local rcs_status = 4
|
||||
if rcs_hazard then
|
||||
rcs_status = 2
|
||||
elseif rcs_warn then
|
||||
rcs_status = 3
|
||||
elseif rcs_disconn then
|
||||
rcs_status = 1
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_RCS", rcs_status)
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Reactor Data
|
||||
|
||||
unit.reactor_data = data[6]
|
||||
|
||||
local control_status = 1
|
||||
local reactor_status = 1
|
||||
local rps_status = 1
|
||||
|
||||
if unit.connected then
|
||||
-- update RPS status
|
||||
if unit.reactor_data.rps_tripped then
|
||||
control_status = 2
|
||||
rps_status = util.trinary(unit.reactor_data.rps_trip_cause == "manual", 3, 2)
|
||||
else rps_status = 4 end
|
||||
|
||||
-- update reactor/control status
|
||||
if unit.reactor_data.mek_status.status then
|
||||
reactor_status = 4
|
||||
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
|
||||
else
|
||||
if unit.reactor_data.no_reactor then
|
||||
reactor_status = 2
|
||||
elseif not unit.reactor_data.formed or unit.reactor_data.rps_status.force_dis then
|
||||
reactor_status = 3
|
||||
else
|
||||
reactor_status = 4
|
||||
end
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.reactor_data) do
|
||||
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
|
||||
unit.unit_ps.publish(key, val)
|
||||
end
|
||||
end
|
||||
|
||||
if type(unit.reactor_data.rps_status) == "table" then
|
||||
for key, val in pairs(unit.reactor_data.rps_status) do
|
||||
unit.unit_ps.publish(key, val)
|
||||
end
|
||||
end
|
||||
|
||||
if type(unit.reactor_data.mek_status) == "table" then
|
||||
for key, val in pairs(unit.reactor_data.mek_status) do
|
||||
unit.unit_ps.publish(key, val)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_ControlStatus", control_status)
|
||||
unit.unit_ps.publish("U_ReactorStatus", reactor_status)
|
||||
unit.unit_ps.publish("U_RPS", rps_status)
|
||||
|
||||
--#endregion
|
||||
|
||||
unit.boiler_data_tbl = data[7]
|
||||
|
||||
for id = 1, #unit.boiler_data_tbl do
|
||||
local boiler = unit.boiler_data_tbl[id] ---@type boilerv_session_db
|
||||
local ps = unit.boiler_ps_tbl[id] ---@type psil
|
||||
|
||||
local boiler_status = 1
|
||||
|
||||
if unit.rtu_hw.boilers[id].connected then
|
||||
if unit.rtu_hw.boilers[id].faulted then
|
||||
boiler_status = 3
|
||||
elseif boiler.formed then
|
||||
boiler_status = 4
|
||||
else
|
||||
boiler_status = 2
|
||||
end
|
||||
end
|
||||
|
||||
ps.publish("BoilerStatus", boiler_status)
|
||||
end
|
||||
|
||||
unit.turbine_data_tbl = data[8]
|
||||
|
||||
for id = 1, #unit.turbine_data_tbl do
|
||||
local turbine = unit.turbine_data_tbl[id] ---@type turbinev_session_db
|
||||
local ps = unit.turbine_ps_tbl[id] ---@type psil
|
||||
|
||||
local turbine_status = 1
|
||||
|
||||
if unit.rtu_hw.turbines[id].connected then
|
||||
if unit.rtu_hw.turbines[id].faulted then
|
||||
turbine_status = 3
|
||||
elseif turbine.formed then
|
||||
turbine_status = 4
|
||||
else
|
||||
turbine_status = 2
|
||||
end
|
||||
end
|
||||
|
||||
ps.publish("TurbineStatus", turbine_status)
|
||||
end
|
||||
|
||||
unit.tank_data_tbl = data[9]
|
||||
end
|
||||
end
|
||||
|
||||
-- get the IO controller database
|
||||
function iocontrol.get_db() return io end
|
||||
|
||||
@@ -8,22 +8,63 @@ local PROTOCOL = comms.PROTOCOL
|
||||
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
local CRDN_TYPE = comms.CRDN_TYPE
|
||||
|
||||
local LINK_STATE = iocontrol.LINK_STATE
|
||||
|
||||
local pocket = {}
|
||||
|
||||
---@type pkt_config
|
||||
local config = {}
|
||||
|
||||
pocket.config = config
|
||||
|
||||
-- load the pocket configuration
|
||||
function pocket.load_config()
|
||||
if not settings.load("/pocket.settings") then return false end
|
||||
|
||||
config.SVR_Channel = settings.get("SVR_Channel")
|
||||
config.CRD_Channel = settings.get("CRD_Channel")
|
||||
config.PKT_Channel = settings.get("PKT_Channel")
|
||||
config.ConnTimeout = settings.get("ConnTimeout")
|
||||
config.TrustedRange = settings.get("TrustedRange")
|
||||
config.AuthKey = settings.get("AuthKey")
|
||||
|
||||
config.LogMode = settings.get("LogMode")
|
||||
config.LogPath = settings.get("LogPath")
|
||||
config.LogDebug = settings.get("LogDebug")
|
||||
|
||||
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_num(config.ConnTimeout)
|
||||
cfv.assert_min(config.ConnTimeout, 2)
|
||||
cfv.assert_type_num(config.TrustedRange)
|
||||
cfv.assert_min(config.TrustedRange, 0)
|
||||
cfv.assert_type_str(config.AuthKey)
|
||||
|
||||
if type(config.AuthKey) == "string" then
|
||||
local len = string.len(config.AuthKey)
|
||||
cfv.assert(len == 0 or len >= 8)
|
||||
end
|
||||
|
||||
cfv.assert_type_int(config.LogMode)
|
||||
cfv.assert_range(config.LogMode, 0, 1)
|
||||
cfv.assert_type_str(config.LogPath)
|
||||
cfv.assert_type_bool(config.LogDebug)
|
||||
|
||||
return cfv.valid()
|
||||
end
|
||||
|
||||
-- pocket coordinator + supervisor communications
|
||||
---@nodiscard
|
||||
---@param version string pocket version
|
||||
---@param nic nic network interface 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, nic, pkt_channel, svr_channel, crd_channel, range, sv_watchdog, api_watchdog)
|
||||
function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
local self = {
|
||||
sv = {
|
||||
linked = false,
|
||||
@@ -42,13 +83,13 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
establish_delay_counter = 0
|
||||
}
|
||||
|
||||
comms.set_trusted_range(range)
|
||||
comms.set_trusted_range(config.TrustedRange)
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- configure network channels
|
||||
nic.closeAll()
|
||||
nic.open(pkt_channel)
|
||||
nic.open(config.PKT_Channel)
|
||||
|
||||
-- send a management packet to the supervisor
|
||||
---@param msg_type MGMT_TYPE
|
||||
@@ -60,7 +101,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv.addr, self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
||||
|
||||
nic.transmit(svr_channel, pkt_channel, s_pkt)
|
||||
nic.transmit(config.SVR_Channel, config.PKT_Channel, s_pkt)
|
||||
self.sv.seq_num = self.sv.seq_num + 1
|
||||
end
|
||||
|
||||
@@ -74,7 +115,21 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
||||
|
||||
nic.transmit(crd_channel, pkt_channel, s_pkt)
|
||||
nic.transmit(config.CRD_Channel, config.PKT_Channel, s_pkt)
|
||||
self.api.seq_num = self.api.seq_num + 1
|
||||
end
|
||||
|
||||
-- send an API packet to the coordinator
|
||||
---@param msg_type CRDN_TYPE
|
||||
---@param msg table
|
||||
local function _send_api(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local pkt = comms.crdn_packet()
|
||||
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_CRDN, pkt.raw_sendable())
|
||||
|
||||
nic.transmit(config.CRD_Channel, config.PKT_Channel, s_pkt)
|
||||
self.api.seq_num = self.api.seq_num + 1
|
||||
end
|
||||
|
||||
@@ -85,7 +140,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
|
||||
-- attempt coordinator API connection establishment
|
||||
local function _send_api_establish()
|
||||
_send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
|
||||
_send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT, comms.api_version })
|
||||
end
|
||||
|
||||
-- keep alive ack to supervisor
|
||||
@@ -151,7 +206,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
end
|
||||
else
|
||||
-- linked, all good!
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED)
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -174,6 +229,11 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end
|
||||
end
|
||||
|
||||
-- coordinator get unit data
|
||||
function public.api__get_unit(unit)
|
||||
if self.api.linked then _send_api(CRDN_TYPE.API_GET_UNIT, { unit }) end
|
||||
end
|
||||
|
||||
-- parse a packet
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
@@ -206,6 +266,25 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
return pkt
|
||||
end
|
||||
|
||||
---@param packet mgmt_frame|crdn_frame
|
||||
---@param length integer
|
||||
---@param max integer?
|
||||
---@return boolean
|
||||
local function _check_length(packet, length, max)
|
||||
local ok = util.trinary(max == nil, packet.length == length, packet.length >= length and packet.length <= (max or 0))
|
||||
if not ok then
|
||||
local fmt = "[comms] RX_PACKET{r_chan=%d,proto=%d,type=%d}: packet length mismatch -> expect %d != actual %d"
|
||||
log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type))
|
||||
end
|
||||
return ok
|
||||
end
|
||||
|
||||
---@param packet mgmt_frame|crdn_frame
|
||||
local function _fail_type(packet)
|
||||
local fmt = "[comms] RX_PACKET{r_chan=%d,proto=%d,type=%d}: unrecognized packet type"
|
||||
log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type))
|
||||
end
|
||||
|
||||
-- handle a packet
|
||||
---@param packet mgmt_frame|crdn_frame|nil
|
||||
function public.handle_packet(packet)
|
||||
@@ -217,9 +296,9 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
local src_addr = packet.scada_frame.src_addr()
|
||||
|
||||
if l_chan ~= pkt_channel then
|
||||
if l_chan ~= config.PKT_Channel then
|
||||
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
||||
elseif r_chan == crd_channel then
|
||||
elseif r_chan == config.CRD_Channel then
|
||||
-- check sequence number
|
||||
if self.api.r_seq_num == nil then
|
||||
self.api.r_seq_num = packet.scada_frame.seq_num()
|
||||
@@ -228,7 +307,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
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?")
|
||||
"); channel in use by another system?")
|
||||
return
|
||||
else
|
||||
self.api.r_seq_num = packet.scada_frame.seq_num()
|
||||
@@ -237,12 +316,27 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
-- feed watchdog on valid sequence number
|
||||
api_watchdog.feed()
|
||||
|
||||
if protocol == PROTOCOL.SCADA_MGMT then
|
||||
if protocol == PROTOCOL.SCADA_CRDN then
|
||||
---@cast packet crdn_frame
|
||||
if self.api.linked then
|
||||
if packet.type == CRDN_TYPE.API_GET_FAC then
|
||||
if _check_length(packet, 11) then
|
||||
iocontrol.record_facility_data(packet.data)
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.API_GET_UNIT then
|
||||
if _check_length(packet, 9) then
|
||||
iocontrol.record_unit_data(packet.data)
|
||||
end
|
||||
else _fail_type(packet) end
|
||||
else
|
||||
log.debug("discarding coordinator SCADA_CRDN packet before linked")
|
||||
end
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
if self.api.linked then
|
||||
if packet.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if packet.length == 1 then
|
||||
if _check_length(packet, 1) then
|
||||
local timestamp = packet.data[1]
|
||||
local trip_time = util.time() - timestamp
|
||||
|
||||
@@ -250,11 +344,11 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
log.warning("pocket coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug("pocket coordinator RTT = " .. trip_time .. "ms")
|
||||
-- log.debug("pocket coordinator TT = " .. trip_time .. "ms")
|
||||
|
||||
_send_api_keep_alive_ack(timestamp)
|
||||
else
|
||||
log.debug("coordinator SCADA keep alive packet length mismatch")
|
||||
|
||||
iocontrol.report_crd_tt(trip_time)
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
@@ -263,24 +357,38 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
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 _fail_type(packet) end
|
||||
elseif packet.type == MGMT_TYPE.ESTABLISH then
|
||||
-- connection with coordinator established
|
||||
if packet.length == 1 then
|
||||
if _check_length(packet, 1, 2) 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 packet.length == 2 then
|
||||
local fac_config = packet.data[2]
|
||||
|
||||
if self.sv.linked then
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED)
|
||||
if type(fac_config) == "table" and #fac_config == 2 then
|
||||
-- get configuration
|
||||
local conf = { num_units = fac_config[1], cooling = fac_config[2] }
|
||||
|
||||
---@todo unit options
|
||||
iocontrol.init_fac(conf, 1)
|
||||
|
||||
log.info("coordinator connection established")
|
||||
self.establish_delay_counter = 0
|
||||
self.api.linked = true
|
||||
self.api.addr = src_addr
|
||||
|
||||
if self.sv.linked then
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
|
||||
else
|
||||
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY)
|
||||
end
|
||||
else
|
||||
log.debug("invalid facility configuration table received from coordinator, establish failed")
|
||||
end
|
||||
else
|
||||
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY)
|
||||
log.debug("received coordinator establish allow without facility configuration")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.DENY then
|
||||
if self.api.last_est_ack ~= est_ack then
|
||||
@@ -294,13 +402,15 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
if self.api.last_est_ack ~= est_ack then
|
||||
log.info("coordinator comms version mismatch")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_API_VERSION then
|
||||
if self.api.last_est_ack ~= est_ack then
|
||||
log.info("coordinator api 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
|
||||
else
|
||||
log.debug("discarding coordinator non-link SCADA_MGMT packet before linked")
|
||||
@@ -308,7 +418,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
else
|
||||
log.debug("illegal packet type " .. protocol .. " from coordinator", true)
|
||||
end
|
||||
elseif r_chan == svr_channel then
|
||||
elseif r_chan == config.SVR_Channel then
|
||||
-- check sequence number
|
||||
if self.sv.r_seq_num == nil then
|
||||
self.sv.r_seq_num = packet.scada_frame.seq_num()
|
||||
@@ -332,7 +442,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
if self.sv.linked then
|
||||
if packet.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if packet.length == 1 then
|
||||
if _check_length(packet, 1) then
|
||||
local timestamp = packet.data[1]
|
||||
local trip_time = util.time() - timestamp
|
||||
|
||||
@@ -340,11 +450,11 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
log.warning("pocket supervisor KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug("pocket supervisor RTT = " .. trip_time .. "ms")
|
||||
-- log.debug("pocket supervisor TT = " .. trip_time .. "ms")
|
||||
|
||||
_send_sv_keep_alive_ack(timestamp)
|
||||
else
|
||||
log.debug("supervisor SCADA keep alive packet length mismatch")
|
||||
|
||||
iocontrol.report_svr_tt(trip_time)
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
@@ -354,12 +464,10 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
self.sv.addr = comms.BROADCAST
|
||||
log.info("supervisor server connection closed by remote host")
|
||||
elseif packet.type == MGMT_TYPE.DIAG_TONE_GET then
|
||||
if packet.length == 8 then
|
||||
if _check_length(packet, 8) then
|
||||
for i = 1, #packet.data do
|
||||
diag.tone_test.tone_indicators[i].update(packet.data[i] == true)
|
||||
end
|
||||
else
|
||||
log.debug("supervisor SCADA diag alarm states packet length mismatch")
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.DIAG_TONE_SET then
|
||||
if packet.length == 1 and packet.data[1] == false then
|
||||
@@ -398,12 +506,10 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
else
|
||||
log.debug("supervisor SCADA diag alarm set packet length/type mismatch")
|
||||
end
|
||||
else
|
||||
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor")
|
||||
end
|
||||
else _fail_type(packet) end
|
||||
elseif packet.type == MGMT_TYPE.ESTABLISH then
|
||||
-- connection with supervisor established
|
||||
if packet.length == 1 then
|
||||
if _check_length(packet, 1) then
|
||||
local est_ack = packet.data[1]
|
||||
|
||||
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||
@@ -413,7 +519,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
self.sv.addr = src_addr
|
||||
|
||||
if self.api.linked then
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED)
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
|
||||
else
|
||||
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY)
|
||||
end
|
||||
@@ -434,15 +540,11 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
end
|
||||
|
||||
self.sv.last_est_ack = est_ack
|
||||
else
|
||||
log.debug("supervisor SCADA_MGMT establish packet length mismatch")
|
||||
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 _fail_type(packet) end
|
||||
else
|
||||
log.debug("received packet from unconfigured channel " .. r_chan, true)
|
||||
end
|
||||
@@ -460,5 +562,4 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
return public
|
||||
end
|
||||
|
||||
|
||||
return pocket
|
||||
|
||||
@@ -13,44 +13,48 @@ local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local config = require("pocket.config")
|
||||
local configure = require("pocket.configure")
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local pocket = require("pocket.pocket")
|
||||
local renderer = require("pocket.renderer")
|
||||
|
||||
local POCKET_VERSION = "v0.6.3-alpha"
|
||||
local POCKET_VERSION = "v0.9.0-alpha"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
|
||||
----------------------------------------
|
||||
-- config validation
|
||||
-- get configuration
|
||||
----------------------------------------
|
||||
|
||||
local cfv = util.new_validator()
|
||||
if not pocket.load_config() then
|
||||
-- try to reconfigure (user action)
|
||||
local success, error = configure.configure(true)
|
||||
if success then
|
||||
if not pocket.load_config() then
|
||||
println("failed to load a valid configuration, please reconfigure")
|
||||
return
|
||||
end
|
||||
else
|
||||
println("configuration error: " .. error)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
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")
|
||||
local config = pocket.config
|
||||
|
||||
----------------------------------------
|
||||
-- log init
|
||||
----------------------------------------
|
||||
|
||||
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
|
||||
log.init(config.LogPath, config.LogMode, config.LogDebug)
|
||||
|
||||
log.info("========================================")
|
||||
log.info("BOOTING pocket.startup " .. POCKET_VERSION)
|
||||
log.info("========================================")
|
||||
|
||||
crash.set_env("pocket", POCKET_VERSION)
|
||||
crash.dbg_log_env()
|
||||
|
||||
----------------------------------------
|
||||
-- main application
|
||||
@@ -64,13 +68,16 @@ local function main()
|
||||
-- mount connected devices
|
||||
ppm.mount_all()
|
||||
|
||||
-- record version for GUI
|
||||
iocontrol.get_db().version = POCKET_VERSION
|
||||
|
||||
----------------------------------------
|
||||
-- setup communications & clocks
|
||||
----------------------------------------
|
||||
|
||||
-- message authentication init
|
||||
if type(config.AUTH_KEY) == "string" then
|
||||
network.init_mac(config.AUTH_KEY)
|
||||
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
|
||||
network.init_mac(config.AuthKey)
|
||||
end
|
||||
|
||||
iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED)
|
||||
@@ -85,8 +92,8 @@ local function main()
|
||||
|
||||
-- create connection watchdogs
|
||||
local conn_wd = {
|
||||
sv = util.new_watchdog(config.COMMS_TIMEOUT),
|
||||
api = util.new_watchdog(config.COMMS_TIMEOUT)
|
||||
sv = util.new_watchdog(config.ConnTimeout),
|
||||
api = util.new_watchdog(config.ConnTimeout)
|
||||
}
|
||||
|
||||
conn_wd.sv.cancel()
|
||||
@@ -96,8 +103,7 @@ local function main()
|
||||
|
||||
-- create network interface then setup comms
|
||||
local nic = network.nic(modem)
|
||||
local pocket_comms = pocket.comms(POCKET_VERSION, nic, config.PKT_CHANNEL, config.SVR_CHANNEL,
|
||||
config.CRD_CHANNEL, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api)
|
||||
local pocket_comms = pocket.comms(POCKET_VERSION, nic, conn_wd.sv, conn_wd.api)
|
||||
log.debug("startup> comms init")
|
||||
|
||||
-- base loop clock (2Hz, 10 ticks)
|
||||
@@ -128,7 +134,7 @@ local function main()
|
||||
-- start connection watchdogs
|
||||
conn_wd.sv.feed()
|
||||
conn_wd.api.feed()
|
||||
log.debug("startup> conn watchdog started")
|
||||
log.debug("startup> conn watchdogs started")
|
||||
|
||||
local io_db = iocontrol.get_db()
|
||||
local nav = io_db.nav
|
||||
@@ -146,11 +152,8 @@ local function main()
|
||||
pocket_comms.link_update()
|
||||
|
||||
-- update any tasks for the active page
|
||||
if (type(nav.tasks[nav.page]) == "table") then
|
||||
for i = 1, #nav.tasks[nav.page] do
|
||||
nav.tasks[nav.page][i]()
|
||||
end
|
||||
end
|
||||
local page_tasks = nav.get_current_page().tasks
|
||||
for i = 1, #page_tasks do page_tasks[i]() end
|
||||
|
||||
loop_clock.start()
|
||||
elseif conn_wd.sv.is_timer(param1) then
|
||||
|
||||
@@ -1,58 +1,39 @@
|
||||
--
|
||||
-- Diagnostic Apps
|
||||
--
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||
|
||||
local App = require("graphics.elements.controls.app")
|
||||
local Checkbox = require("graphics.elements.controls.checkbox")
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
local SwitchButton = require("graphics.elements.controls.switch_button")
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local NAV_PAGE = iocontrol.NAV_PAGE
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
-- new diagnostics page view
|
||||
-- create diagnostic app pages
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local function create_pages(root)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
local diag_home = Div{parent=main,x=1,y=1}
|
||||
|
||||
TextBox{parent=diag_home,text="Diagnostic Apps",x=1,y=2,height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
local alarm_test = Div{parent=main,x=1,y=1}
|
||||
|
||||
local panes = { diag_home, alarm_test }
|
||||
|
||||
local page_pane = MultiPane{parent=main,x=1,y=1,panes=panes}
|
||||
|
||||
local function navigate_diag()
|
||||
page_pane.set_value(1)
|
||||
db.nav.page = NAV_PAGE.DIAG
|
||||
db.nav.sub_pages[NAV_PAGE.DIAG] = NAV_PAGE.DIAG
|
||||
end
|
||||
|
||||
local function navigate_alarm()
|
||||
page_pane.set_value(2)
|
||||
db.nav.page = NAV_PAGE.D_ALARMS
|
||||
db.nav.sub_pages[NAV_PAGE.DIAG] = NAV_PAGE.D_ALARMS
|
||||
end
|
||||
|
||||
------------------------
|
||||
-- Alarm Testing Page --
|
||||
------------------------
|
||||
|
||||
db.nav.register_task(NAV_PAGE.D_ALARMS, db.diag.tone_test.get_tone_states)
|
||||
local alarm_test = Div{parent=root,x=1,y=1}
|
||||
|
||||
local alarm_app = db.nav.register_app(iocontrol.APP_ID.ALARMS, alarm_test)
|
||||
|
||||
local page = alarm_app.new_page(nil, function () end)
|
||||
page.tasks = { db.diag.tone_test.get_tone_states }
|
||||
|
||||
local ttest = db.diag.tone_test
|
||||
|
||||
@@ -67,8 +48,6 @@ local function new_view(root)
|
||||
|
||||
ttest.ready_warn = TextBox{parent=audio,y=2,text="",height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
|
||||
|
||||
PushButton{parent=audio,x=13,y=18,text="\x11 BACK",min_width=8,fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=c_wht_gray,callback=navigate_diag}
|
||||
|
||||
local tones = Div{parent=audio,x=2,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)}
|
||||
|
||||
TextBox{parent=tones,text="Tones",height=1,alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
|
||||
@@ -132,16 +111,6 @@ local function new_view(root)
|
||||
local t_8 = IndicatorLight{parent=states,x=6,label="8",colors=c_blue_gray}
|
||||
|
||||
ttest.tone_indicators = { t_1, t_2, t_3, t_4, t_5, t_6, t_7, t_8 }
|
||||
|
||||
--------------
|
||||
-- App List --
|
||||
--------------
|
||||
|
||||
App{parent=diag_home,x=3,y=4,text="\x0f",title="Alarm",callback=navigate_alarm,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
App{parent=diag_home,x=10,y=4,text="\x1e",title="LoopT",callback=function()end,app_fg_bg=cpair(colors.black,colors.cyan)}
|
||||
App{parent=diag_home,x=17,y=4,text="@",title="Comps",callback=function()end,app_fg_bg=cpair(colors.black,colors.orange)}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
return create_pages
|
||||
26
pocket/ui/apps/dummy_app.lua
Normal file
26
pocket/ui/apps/dummy_app.lua
Normal file
@@ -0,0 +1,26 @@
|
||||
--
|
||||
-- Placeholder App
|
||||
--
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
-- create placeholder app page
|
||||
---@param root graphics_element parent
|
||||
local function create_pages(root)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
db.nav.register_app(iocontrol.APP_ID.DUMMY, main).new_page(nil, function () end)
|
||||
|
||||
TextBox{parent=main,text="This app is not implemented yet.",x=1,y=2,alignment=core.ALIGN.CENTER}
|
||||
|
||||
TextBox{parent=main,text=" pretend something cool is here \x03",x=1,y=10,alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.gray,colors.black)}
|
||||
end
|
||||
|
||||
return create_pages
|
||||
145
pocket/ui/apps/sys_apps.lua
Normal file
145
pocket/ui/apps/sys_apps.lua
Normal file
@@ -0,0 +1,145 @@
|
||||
--
|
||||
-- System Apps
|
||||
--
|
||||
|
||||
local comms = require("scada-common.comms")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local lockbox = require("lockbox")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local ListBox = require("graphics.elements.listbox")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
-- create system app pages
|
||||
---@param root graphics_element parent
|
||||
local function create_pages(root)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
----------------
|
||||
-- About Page --
|
||||
----------------
|
||||
|
||||
local about_root = Div{parent=root,x=1,y=1}
|
||||
|
||||
local about_app = db.nav.register_app(iocontrol.APP_ID.ABOUT, about_root)
|
||||
|
||||
local about_page = about_app.new_page(nil, 1)
|
||||
local nt_page = about_app.new_page(about_page, 2)
|
||||
local fw_page = about_app.new_page(about_page, 3)
|
||||
local hw_page = about_app.new_page(about_page, 4)
|
||||
|
||||
local about = Div{parent=about_root,x=1,y=2}
|
||||
|
||||
TextBox{parent=about,y=1,text="System Information",height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
local btn_fg_bg = cpair(colors.lightBlue, colors.black)
|
||||
local btn_active = cpair(colors.white, colors.black)
|
||||
local label = cpair(colors.lightGray, colors.black)
|
||||
|
||||
PushButton{parent=about,x=2,y=3,text="Network >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=nt_page.nav_to}
|
||||
PushButton{parent=about,x=2,y=4,text="Firmware >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fw_page.nav_to}
|
||||
PushButton{parent=about,x=2,y=5,text="Host Details >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=hw_page.nav_to}
|
||||
|
||||
--#region Network Details
|
||||
|
||||
local config = pocket.config
|
||||
|
||||
local nt_div = Div{parent=about_root,x=1,y=2}
|
||||
TextBox{parent=nt_div,y=1,text="Network Details",height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
PushButton{parent=nt_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
|
||||
|
||||
TextBox{parent=nt_div,x=2,y=3,text="Pocket Address",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
TextBox{parent=nt_div,x=2,text=util.c(os.getComputerID(),":",config.PKT_Channel),height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
nt_div.line_break()
|
||||
TextBox{parent=nt_div,x=2,text="Supervisor Address",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
local sv = TextBox{parent=nt_div,x=2,text="",height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
nt_div.line_break()
|
||||
TextBox{parent=nt_div,x=2,text="Coordinator Address",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
local coord = TextBox{parent=nt_div,x=2,text="",height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
sv.register(db.ps, "sv_addr", function (addr) sv.set_value(util.c(addr, ":", config.SVR_Channel)) end)
|
||||
coord.register(db.ps, "api_addr", function (addr) coord.set_value(util.c(addr, ":", config.CRD_Channel)) end)
|
||||
|
||||
nt_div.line_break()
|
||||
TextBox{parent=nt_div,x=2,text="Message Authentication",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
local auth = util.trinary(type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0, "HMAC-MD5", "None")
|
||||
TextBox{parent=nt_div,x=2,text=auth,height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Firmware Versions
|
||||
|
||||
local fw_div = Div{parent=about_root,x=1,y=2}
|
||||
TextBox{parent=fw_div,y=1,text="Firmware Versions",height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
PushButton{parent=fw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
|
||||
|
||||
local fw_list_box = ListBox{parent=fw_div,x=1,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
|
||||
|
||||
local fw_list = Div{parent=fw_list_box,x=1,y=2,height=18}
|
||||
|
||||
TextBox{parent=fw_list,x=2,text="Pocket Version",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=db.version,height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="Comms Version",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=comms.version,height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="API Version",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=comms.api_version,height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="Common Lib Version",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=util.version,height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="Graphics Version",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=core.version,height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="Lockbox Version",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=lockbox.version,height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Host Versions
|
||||
|
||||
local hw_div = Div{parent=about_root,x=1,y=2}
|
||||
TextBox{parent=hw_div,y=1,text="Host Versions",height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
PushButton{parent=hw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
|
||||
|
||||
hw_div.line_break()
|
||||
TextBox{parent=hw_div,x=2,text="Lua Version",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=hw_div,x=2,text=_VERSION,height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
hw_div.line_break()
|
||||
TextBox{parent=hw_div,x=2,text="Environment",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=hw_div,x=2,text=_HOST,height=6,alignment=ALIGN.LEFT}
|
||||
|
||||
--#endregion
|
||||
|
||||
local root_pane = MultiPane{parent=about_root,x=1,y=1,panes={about,nt_div,fw_div,hw_div}}
|
||||
|
||||
about_app.set_root_pane(root_pane)
|
||||
end
|
||||
|
||||
return create_pages
|
||||
@@ -4,27 +4,29 @@
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
local diag_apps = require("pocket.ui.apps.diag_apps")
|
||||
local dummy_app = require("pocket.ui.apps.dummy_app")
|
||||
local sys_apps = require("pocket.ui.apps.sys_apps")
|
||||
|
||||
local conn_waiting = require("pocket.ui.components.conn_waiting")
|
||||
|
||||
local boiler_page = require("pocket.ui.pages.boiler_page")
|
||||
local diag_page = require("pocket.ui.pages.diag_page")
|
||||
local home_page = require("pocket.ui.pages.home_page")
|
||||
local reactor_page = require("pocket.ui.pages.reactor_page")
|
||||
local turbine_page = require("pocket.ui.pages.turbine_page")
|
||||
local unit_page = require("pocket.ui.pages.unit_page")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
local Sidebar = require("graphics.elements.controls.sidebar")
|
||||
|
||||
local SignalBar = require("graphics.elements.indicators.signal")
|
||||
|
||||
local LINK_STATE = iocontrol.LINK_STATE
|
||||
local NAV_PAGE = iocontrol.NAV_PAGE
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
@@ -33,26 +35,27 @@ local cpair = core.cpair
|
||||
-- create new main view
|
||||
---@param main graphics_element main displaybox
|
||||
local function init(main)
|
||||
local nav = iocontrol.get_db().nav
|
||||
local ps = iocontrol.get_db().ps
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
-- window header message
|
||||
TextBox{parent=main,y=1,text="",alignment=ALIGN.LEFT,height=1,fg_bg=style.header}
|
||||
TextBox{parent=main,y=1,text="WIP ALPHA APP S C ",alignment=ALIGN.LEFT,height=1,fg_bg=style.header}
|
||||
local svr_conn = SignalBar{parent=main,y=1,x=22,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
|
||||
local crd_conn = SignalBar{parent=main,y=1,x=26,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
|
||||
|
||||
--
|
||||
-- root panel panes (connection screens + main screen)
|
||||
--
|
||||
db.ps.subscribe("svr_conn_quality", svr_conn.set_value)
|
||||
db.ps.subscribe("crd_conn_quality", crd_conn.set_value)
|
||||
|
||||
--#region 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}
|
||||
local root_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={conn_sv_wait,conn_api_wait,main_pane}}
|
||||
|
||||
root_pane.register(ps, "link_state", function (state)
|
||||
root_pane.register(db.ps, "link_state", function (state)
|
||||
if state == LINK_STATE.UNLINKED or state == LINK_STATE.API_LINK_ONLY then
|
||||
root_pane.set_value(1)
|
||||
elseif state == LINK_STATE.SV_LINK_ONLY then
|
||||
@@ -62,62 +65,29 @@ local function init(main)
|
||||
end
|
||||
end)
|
||||
|
||||
--
|
||||
-- main page panel panes & sidebar
|
||||
--
|
||||
--#endregion
|
||||
|
||||
--#region 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)
|
||||
},
|
||||
{
|
||||
char = "D",
|
||||
color = cpair(colors.black,colors.orange)
|
||||
}
|
||||
}
|
||||
home_page(page_div)
|
||||
unit_page(page_div)
|
||||
|
||||
local panes = { home_page(page_div), unit_page(page_div), reactor_page(page_div), boiler_page(page_div), turbine_page(page_div), diag_page(page_div) }
|
||||
diag_apps(page_div)
|
||||
sys_apps(page_div)
|
||||
dummy_app(page_div)
|
||||
|
||||
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
assert(#db.nav.get_containers() == iocontrol.APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered")
|
||||
|
||||
local function navigate_sidebar(page)
|
||||
if page == 1 then
|
||||
nav.page = nav.sub_pages[NAV_PAGE.HOME]
|
||||
elseif page == 2 then
|
||||
nav.page = nav.sub_pages[NAV_PAGE.UNITS]
|
||||
elseif page == 3 then
|
||||
nav.page = nav.sub_pages[NAV_PAGE.REACTORS]
|
||||
elseif page == 4 then
|
||||
nav.page = nav.sub_pages[NAV_PAGE.BOILERS]
|
||||
elseif page == 5 then
|
||||
nav.page = nav.sub_pages[NAV_PAGE.TURBINES]
|
||||
elseif page == 6 then
|
||||
nav.page = nav.sub_pages[NAV_PAGE.DIAG]
|
||||
end
|
||||
db.nav.set_pane(MultiPane{parent=page_div,x=1,y=1,panes=db.nav.get_containers()})
|
||||
db.nav.set_sidebar(Sidebar{parent=main_pane,x=1,y=1,height=18,fg_bg=cpair(colors.white,colors.gray)})
|
||||
|
||||
page_pane.set_value(page)
|
||||
end
|
||||
PushButton{parent=main_pane,x=1,y=19,text="\x1b",min_width=3,fg_bg=cpair(colors.white,colors.gray),active_fg_bg=cpair(colors.gray,colors.black),callback=db.nav.nav_up}
|
||||
|
||||
Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=navigate_sidebar}
|
||||
db.nav.open_app(iocontrol.APP_ID.ROOT)
|
||||
|
||||
--#endregion
|
||||
end
|
||||
|
||||
return init
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
-- 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 ALIGN = core.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=ALIGN.CENTER}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
@@ -1,21 +1,64 @@
|
||||
local core = require("graphics.core")
|
||||
--
|
||||
-- Main Home Page
|
||||
--
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
|
||||
local App = require("graphics.elements.controls.app")
|
||||
local core = require("graphics.core")
|
||||
|
||||
local AppMultiPane = require("graphics.elements.appmultipane")
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local App = require("graphics.elements.controls.app")
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local APP_ID = iocontrol.APP_ID
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
-- new home page view
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
App{parent=main,x=3,y=2,text="\x17",title="PRC",callback=function()end,app_fg_bg=cpair(colors.black,colors.purple)}
|
||||
App{parent=main,x=10,y=2,text="\x15",title="CTL",callback=function()end,app_fg_bg=cpair(colors.black,colors.green)}
|
||||
App{parent=main,x=17,y=2,text="\x08",title="DEV",callback=function()end,app_fg_bg=cpair(colors.black,colors.lightGray)}
|
||||
App{parent=main,x=3,y=7,text="\x7f",title="Waste",callback=function()end,app_fg_bg=cpair(colors.black,colors.brown)}
|
||||
App{parent=main,x=10,y=7,text="\xb6",title="Guide",callback=function()end,app_fg_bg=cpair(colors.black,colors.cyan)}
|
||||
local main = Div{parent=root,x=1,y=1,height=19}
|
||||
|
||||
local app = db.nav.register_app(iocontrol.APP_ID.ROOT, main)
|
||||
|
||||
local apps_1 = Div{parent=main,x=1,y=1,height=15}
|
||||
local apps_2 = Div{parent=main,x=1,y=1,height=15}
|
||||
|
||||
local panes = { apps_1, apps_2 }
|
||||
|
||||
local app_pane = AppMultiPane{parent=main,x=1,y=1,height=18,panes=panes,active_color=colors.lightGray,nav_colors=cpair(colors.lightGray,colors.gray),scroll_nav=true,drag_nav=true,callback=app.switcher}
|
||||
|
||||
app.set_root_pane(app_pane)
|
||||
app.new_page(app.new_page(nil, 1), 2)
|
||||
|
||||
local function open(id) db.nav.open_app(id) end
|
||||
|
||||
app.set_sidebar({
|
||||
{ label = " #\x10", tall = true, color = core.cpair(colors.black, colors.green), callback = function () open(APP_ID.ROOT) end }
|
||||
})
|
||||
|
||||
local active_fg_bg = cpair(colors.white,colors.gray)
|
||||
|
||||
App{parent=apps_1,x=2,y=2,text="U",title="Units",callback=function()open(APP_ID.UNITS)end,app_fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=9,y=2,text="F",title="Facil",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=16,y=2,text="\x15",title="Control",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.green),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=2,y=7,text="\x17",title="Process",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.purple),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=9,y=7,text="\x7f",title="Waste",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.brown),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=16,y=7,text="\x08",title="Devices",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=2,y=12,text="\xb6",title="Guide",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=9,y=12,text="?",title="About",callback=function()open(APP_ID.ABOUT)end,app_fg_bg=cpair(colors.black,colors.white),active_fg_bg=active_fg_bg}
|
||||
|
||||
TextBox{parent=apps_2,text="Diagnostic Apps",x=1,y=2,height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
App{parent=apps_2,x=2,y=4,text="\x0f",title="Alarm",callback=function()open(APP_ID.ALARMS)end,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_2,x=9,y=4,text="\x1e",title="LoopT",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_2,x=16,y=4,text="@",title="Comps",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
-- 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 ALIGN = core.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=ALIGN.CENTER}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
@@ -1,22 +0,0 @@
|
||||
-- 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 ALIGN = core.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=ALIGN.CENTER}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
@@ -1,20 +1,319 @@
|
||||
-- local style = require("pocket.ui.style")
|
||||
--
|
||||
-- Unit Overview Page
|
||||
--
|
||||
|
||||
local core = require("graphics.core")
|
||||
local util = require("scada-common.util")
|
||||
-- local log = require("scada-common.log")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
|
||||
-- local cpair = core.cpair
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local IconIndicator = require("graphics.elements.indicators.icon")
|
||||
-- local RadIndicator = require("graphics.elements.indicators.rad")
|
||||
-- local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
local cpair = core.cpair
|
||||
|
||||
local basic_states = {
|
||||
{ color = cpair(colors.black, colors.lightGray), symbol = "\x07" },
|
||||
{ color = cpair(colors.black, colors.red), symbol = "-" },
|
||||
{ color = cpair(colors.black, colors.yellow), symbol = "\x1e" },
|
||||
{ color = cpair(colors.black, colors.green), symbol = "+" }
|
||||
}
|
||||
|
||||
local mode_states = {
|
||||
{ color = cpair(colors.black, colors.lightGray), symbol = "\x07" },
|
||||
{ color = cpair(colors.black, colors.red), symbol = "-" },
|
||||
{ color = cpair(colors.black, colors.green), symbol = "+" },
|
||||
{ color = cpair(colors.black, colors.purple), symbol = "A" }
|
||||
}
|
||||
|
||||
local emc_ind_s = {
|
||||
{ color = cpair(colors.black, colors.gray), symbol = "-" },
|
||||
{ color = cpair(colors.black, colors.white), symbol = "\x07" },
|
||||
{ color = cpair(colors.black, colors.green), symbol = "+" }
|
||||
}
|
||||
|
||||
local red_ind_s = {
|
||||
{ color = cpair(colors.black, colors.lightGray), symbol = "+" },
|
||||
{ color = cpair(colors.black, colors.red), symbol = "-" }
|
||||
}
|
||||
|
||||
local yel_ind_s = {
|
||||
{ color = cpair(colors.black, colors.lightGray), symbol = "+" },
|
||||
{ color = cpair(colors.black, colors.yellow), symbol = "-" }
|
||||
}
|
||||
|
||||
-- new unit page view
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
TextBox{parent=main,text="UNITS",x=1,y=1,height=1,alignment=ALIGN.CENTER}
|
||||
local app = db.nav.register_app(iocontrol.APP_ID.UNITS, main)
|
||||
|
||||
TextBox{parent=main,y=2,text="Units App",height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
TextBox{parent=main,y=4,text="Loading...",height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
local btn_fg_bg = cpair(colors.yellow, colors.black)
|
||||
local btn_active = cpair(colors.white, colors.black)
|
||||
-- local label = cpair(colors.lightGray, colors.black)
|
||||
|
||||
local nav_links = {}
|
||||
|
||||
local function set_sidebar(id)
|
||||
-- local unit = db.units[id] ---@type pioctl_unit
|
||||
|
||||
local list = {
|
||||
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(iocontrol.APP_ID.ROOT) end },
|
||||
{ label = "U-" .. id, color = core.cpair(colors.black, colors.yellow), callback = function () app.switcher(id) end },
|
||||
{ label = " \x13 ", color = core.cpair(colors.black, colors.red), callback = nav_links[id].alarm },
|
||||
{ label = "RPS", tall = true, color = core.cpair(colors.black, colors.cyan), callback = nav_links[id].rps },
|
||||
-- { label = " R ", color = core.cpair(colors.black, colors.lightGray), callback = function () end },
|
||||
{ label = "RCS", tall = true, color = core.cpair(colors.black, colors.blue), callback = nav_links[id].rcs },
|
||||
}
|
||||
|
||||
-- for i = 1, unit.num_boilers do
|
||||
-- table.insert(list, { label = "B-" .. i, color = core.cpair(colors.black, colors.lightBlue), callback = function () end })
|
||||
-- end
|
||||
|
||||
-- for i = 1, unit.num_turbines do
|
||||
-- table.insert(list, { label = "T-" .. i, color = core.cpair(colors.black, colors.white), callback = function () end })
|
||||
-- end
|
||||
|
||||
app.set_sidebar(list)
|
||||
end
|
||||
|
||||
local function load()
|
||||
local page_div = Div{parent=main,x=2,y=2,width=main.get_width()-2}
|
||||
|
||||
local panes = {}
|
||||
|
||||
local active_unit = 1
|
||||
|
||||
-- create all page divs
|
||||
for _ = 1, db.facility.num_units do
|
||||
local div = Div{parent=page_div}
|
||||
table.insert(panes, div)
|
||||
table.insert(nav_links, {})
|
||||
end
|
||||
|
||||
-- previous unit
|
||||
local function prev(x)
|
||||
active_unit = util.trinary(x == 1, db.facility.num_units, x - 1)
|
||||
app.switcher(active_unit)
|
||||
set_sidebar(active_unit)
|
||||
end
|
||||
|
||||
-- next unit
|
||||
local function next(x)
|
||||
active_unit = util.trinary(x == db.facility.num_units, 1, x + 1)
|
||||
app.switcher(active_unit)
|
||||
set_sidebar(active_unit)
|
||||
end
|
||||
|
||||
for i = 1, db.facility.num_units do
|
||||
local u_div = panes[i] ---@type graphics_element
|
||||
local unit = db.units[i] ---@type pioctl_unit
|
||||
local u_ps = unit.unit_ps
|
||||
|
||||
-- refresh data callback, every 500ms it will re-send the query
|
||||
local last_update = 0
|
||||
local function update()
|
||||
if util.time_ms() - last_update >= 500 then
|
||||
db.api.get_unit(i)
|
||||
last_update = util.time_ms()
|
||||
end
|
||||
end
|
||||
|
||||
--#region Main Unit Overview
|
||||
|
||||
local u_page = app.new_page(nil, i)
|
||||
u_page.tasks = { update }
|
||||
|
||||
TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,height=1,alignment=ALIGN.CENTER}
|
||||
PushButton{parent=u_div,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()prev(i)end}
|
||||
PushButton{parent=u_div,x=21,y=1,text=">",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()next(i)end}
|
||||
|
||||
local type = util.trinary(unit.num_boilers > 0, "Sodium Cooled Reactor", "Boiling Water Reactor")
|
||||
TextBox{parent=u_div,y=3,text=type,height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.black)}
|
||||
|
||||
local lu_col = cpair(colors.lightGray, colors.lightGray)
|
||||
local text_fg = cpair(colors.white, colors._INHERIT)
|
||||
|
||||
local rate = DataIndicator{parent=u_div,y=5,lu_colors=lu_col,label="Rate",unit="mB/t",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
|
||||
local temp = DataIndicator{parent=u_div,lu_colors=lu_col,label="Temp",unit="K",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
|
||||
|
||||
local ctrl = IconIndicator{parent=u_div,x=1,y=8,label="Control State",states=mode_states}
|
||||
|
||||
rate.register(u_ps, "act_burn_rate", rate.update)
|
||||
temp.register(u_ps, "temp", temp.update)
|
||||
ctrl.register(u_ps, "U_ControlStatus", ctrl.update)
|
||||
|
||||
u_div.line_break()
|
||||
|
||||
local rct = IconIndicator{parent=u_div,x=1,label="Fission Reactor",states=basic_states}
|
||||
local rps = IconIndicator{parent=u_div,x=1,label="Protection System",states=basic_states}
|
||||
|
||||
rct.register(u_ps, "U_ReactorStatus", rct.update)
|
||||
rps.register(u_ps, "U_RPS", rps.update)
|
||||
|
||||
u_div.line_break()
|
||||
|
||||
local rcs = IconIndicator{parent=u_div,x=1,label="Coolant System",states=basic_states}
|
||||
rcs.register(u_ps, "U_RCS", rcs.update)
|
||||
|
||||
for b = 1, unit.num_boilers do
|
||||
local blr = IconIndicator{parent=u_div,x=1,label="Boiler "..b,states=basic_states}
|
||||
blr.register(unit.boiler_ps_tbl[b], "BoilerStatus", blr.update)
|
||||
end
|
||||
|
||||
for t = 1, unit.num_turbines do
|
||||
local tbn = IconIndicator{parent=u_div,x=1,label="Turbine "..t,states=basic_states}
|
||||
tbn.register(unit.turbine_ps_tbl[t], "TurbineStatus", tbn.update)
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Alarms Tab
|
||||
|
||||
local alm_div = Div{parent=page_div}
|
||||
table.insert(panes, alm_div)
|
||||
|
||||
local alm_page = app.new_page(u_page, #panes)
|
||||
alm_page.tasks = { update }
|
||||
|
||||
nav_links[i].alarm = alm_page.nav_to
|
||||
|
||||
TextBox{parent=alm_div,y=1,text="Unit Alarms",height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
TextBox{parent=alm_div,y=3,text="work in progress",height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.black)}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region RPS Tab
|
||||
|
||||
local rps_div = Div{parent=page_div}
|
||||
table.insert(panes, rps_div)
|
||||
|
||||
local rps_page = app.new_page(u_page, #panes)
|
||||
rps_page.tasks = { update }
|
||||
|
||||
nav_links[i].rps = rps_page.nav_to
|
||||
|
||||
TextBox{parent=rps_div,y=1,text="Protection System",height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
local r_trip = IconIndicator{parent=rps_div,y=3,label="RPS Trip",states=basic_states}
|
||||
r_trip.register(u_ps, "U_RPS", r_trip.update)
|
||||
|
||||
local r_mscrm = IconIndicator{parent=rps_div,y=5,label="Manual SCRAM",states=red_ind_s}
|
||||
local r_ascrm = IconIndicator{parent=rps_div,label="Automatic SCRAM",states=red_ind_s}
|
||||
local rps_tmo = IconIndicator{parent=rps_div,label="Timeout",states=yel_ind_s}
|
||||
local rps_flt = IconIndicator{parent=rps_div,label="PPM Fault",states=yel_ind_s}
|
||||
local rps_sfl = IconIndicator{parent=rps_div,label="Not Formed",states=red_ind_s}
|
||||
|
||||
r_mscrm.register(u_ps, "manual", r_mscrm.update)
|
||||
r_ascrm.register(u_ps, "automatic", r_ascrm.update)
|
||||
rps_tmo.register(u_ps, "timeout", rps_tmo.update)
|
||||
rps_flt.register(u_ps, "fault", rps_flt.update)
|
||||
rps_sfl.register(u_ps, "sys_fail", rps_sfl.update)
|
||||
|
||||
rps_div.line_break()
|
||||
local rps_dmg = IconIndicator{parent=rps_div,label="Reactor Damage Hi",states=red_ind_s}
|
||||
local rps_tmp = IconIndicator{parent=rps_div,label="Temp. Critical",states=red_ind_s}
|
||||
local rps_nof = IconIndicator{parent=rps_div,label="Fuel Level Lo",states=yel_ind_s}
|
||||
local rps_exw = IconIndicator{parent=rps_div,label="Waste Level Hi",states=yel_ind_s}
|
||||
local rps_loc = IconIndicator{parent=rps_div,label="Coolant Lo Lo",states=yel_ind_s}
|
||||
local rps_exh = IconIndicator{parent=rps_div,label="Heated Coolant Hi",states=yel_ind_s}
|
||||
|
||||
rps_dmg.register(u_ps, "high_dmg", rps_dmg.update)
|
||||
rps_tmp.register(u_ps, "high_temp", rps_tmp.update)
|
||||
rps_nof.register(u_ps, "no_fuel", rps_nof.update)
|
||||
rps_exw.register(u_ps, "ex_waste", rps_exw.update)
|
||||
rps_loc.register(u_ps, "low_cool", rps_loc.update)
|
||||
rps_exh.register(u_ps, "ex_hcool", rps_exh.update)
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region RCS Tab
|
||||
|
||||
local rcs_div = Div{parent=page_div}
|
||||
table.insert(panes, rcs_div)
|
||||
|
||||
local rcs_page = app.new_page(u_page, #panes)
|
||||
rcs_page.tasks = { update }
|
||||
|
||||
nav_links[i].rcs = rcs_page.nav_to
|
||||
|
||||
TextBox{parent=rcs_div,y=1,text="Coolant System",height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
local r_rtrip = IconIndicator{parent=rcs_div,y=3,label="RCP Trip",states=red_ind_s}
|
||||
local r_cflow = IconIndicator{parent=rcs_div,label="RCS Flow Lo",states=yel_ind_s}
|
||||
local r_clow = IconIndicator{parent=rcs_div,label="Coolant Level Lo",states=yel_ind_s}
|
||||
|
||||
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)
|
||||
|
||||
local c_flt = IconIndicator{parent=rcs_div,label="RCS HW Fault",states=yel_ind_s}
|
||||
local c_emg = IconIndicator{parent=rcs_div,label="Emergency Coolant",states=emc_ind_s}
|
||||
local c_mwrf = IconIndicator{parent=rcs_div,label="Max Water Return",states=yel_ind_s}
|
||||
|
||||
c_flt.register(u_ps, "RCSFault", c_flt.update)
|
||||
c_emg.register(u_ps, "EmergencyCoolant", c_emg.update)
|
||||
c_mwrf.register(u_ps, "MaxWaterReturnFeed", c_mwrf.update)
|
||||
|
||||
-- rcs_div.line_break()
|
||||
-- TextBox{parent=rcs_div,text="Mismatches",height=1,alignment=ALIGN.CENTER,fg_bg=label}
|
||||
local c_cfm = IconIndicator{parent=rcs_div,label="Coolant Feed",states=yel_ind_s}
|
||||
local c_brm = IconIndicator{parent=rcs_div,label="Boil Rate",states=yel_ind_s}
|
||||
local c_sfm = IconIndicator{parent=rcs_div,label="Steam Feed",states=yel_ind_s}
|
||||
|
||||
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)
|
||||
|
||||
rcs_div.line_break()
|
||||
-- TextBox{parent=rcs_div,text="Aggregate Checks",height=1,alignment=ALIGN.CENTER,fg_bg=label}
|
||||
|
||||
if unit.num_boilers > 0 then
|
||||
local wll = IconIndicator{parent=rcs_div,label="Boiler Water Lo",states=red_ind_s}
|
||||
local hrl = IconIndicator{parent=rcs_div,label="Heating Rate Lo",states=yel_ind_s}
|
||||
|
||||
wll.register(u_ps, "U_WaterLevelLow", wll.update)
|
||||
hrl.register(u_ps, "U_HeatingRateLow", hrl.update)
|
||||
end
|
||||
|
||||
local tospd = IconIndicator{parent=rcs_div,label="TRB Over Speed",states=red_ind_s}
|
||||
local gtrip = IconIndicator{parent=rcs_div,label="Generator Trip",states=yel_ind_s}
|
||||
local ttrip = IconIndicator{parent=rcs_div,label="Turbine Trip",states=red_ind_s}
|
||||
|
||||
tospd.register(u_ps, "U_TurbineOverSpeed", tospd.update)
|
||||
gtrip.register(u_ps, "U_GeneratorTrip", gtrip.update)
|
||||
ttrip.register(u_ps, "U_TurbineTrip", ttrip.update)
|
||||
|
||||
--#endregion
|
||||
end
|
||||
|
||||
-- setup multipane
|
||||
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
app.set_root_pane(u_pane)
|
||||
|
||||
set_sidebar(active_unit)
|
||||
end
|
||||
|
||||
app.set_on_load(load)
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local rsio = require("scada-common.rsio")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
local Div = require("graphics.elements.div")
|
||||
@@ -22,7 +24,10 @@ local RadioButton = require("graphics.elements.controls.radio_button")
|
||||
local NumberField = require("graphics.elements.form.number_field")
|
||||
local TextField = require("graphics.elements.form.text_field")
|
||||
|
||||
local IndLight = require("graphics.elements.indicators.light")
|
||||
|
||||
local println = util.println
|
||||
local tri = util.trinary
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
@@ -30,6 +35,14 @@ local LEFT = core.ALIGN.LEFT
|
||||
local CENTER = core.ALIGN.CENTER
|
||||
local RIGHT = core.ALIGN.RIGHT
|
||||
|
||||
-- changes to the config data/format to let the user know
|
||||
local changes = {
|
||||
{ "v1.6.2", { "AuthKey minimum length is now 8 (if set)" } },
|
||||
{ "v1.6.8", { "ConnTimeout can now have a fractional part" } },
|
||||
{ "v1.6.15", { "Added front panel UI theme", "Added color accessibility modes" } },
|
||||
{ "v1.7.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } }
|
||||
}
|
||||
|
||||
---@class plc_configurator
|
||||
local configurator = {}
|
||||
|
||||
@@ -37,31 +50,26 @@ local style = {}
|
||||
|
||||
style.root = cpair(colors.black, colors.lightGray)
|
||||
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.lightGray, hex = 0xcacaca },
|
||||
{ c = colors.gray, hex = 0x575757 }
|
||||
}
|
||||
style.colors = themes.smooth_stone.colors
|
||||
|
||||
local bw_fg_bg = cpair(colors.black, colors.white)
|
||||
local g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
|
||||
local nav_fg_bg = bw_fg_bg
|
||||
local btn_act_fg_bg = cpair(colors.white, colors.gray)
|
||||
|
||||
---@class _plc_cfg_tool_ctl
|
||||
local tool_ctl = {
|
||||
ask_config = false,
|
||||
has_config = false,
|
||||
viewing_config = false,
|
||||
importing_legacy = false,
|
||||
jumped_to_color = false,
|
||||
|
||||
view_cfg = nil, ---@type graphics_element
|
||||
color_cfg = nil, ---@type graphics_element
|
||||
color_next = nil, ---@type graphics_element
|
||||
color_apply = nil, ---@type graphics_element
|
||||
settings_apply = nil, ---@type graphics_element
|
||||
|
||||
set_networked = nil, ---@type function
|
||||
@@ -81,35 +89,42 @@ local tmp_cfg = {
|
||||
Networked = false,
|
||||
UnitID = 0,
|
||||
EmerCoolEnable = false,
|
||||
EmerCoolSide = nil,
|
||||
EmerCoolColor = nil,
|
||||
SVR_Channel = nil,
|
||||
PLC_Channel = nil,
|
||||
ConnTimeout = nil,
|
||||
TrustedRange = nil,
|
||||
AuthKey = nil,
|
||||
EmerCoolSide = nil, ---@type string|nil
|
||||
EmerCoolColor = nil, ---@type color|nil
|
||||
SVR_Channel = nil, ---@type integer
|
||||
PLC_Channel = nil, ---@type integer
|
||||
ConnTimeout = nil, ---@type number
|
||||
TrustedRange = nil, ---@type number
|
||||
AuthKey = nil, ---@type string|nil
|
||||
LogMode = 0,
|
||||
LogPath = "",
|
||||
LogDebug = false,
|
||||
FrontPanelTheme = 1,
|
||||
ColorMode = 1
|
||||
}
|
||||
|
||||
---@class plc_config
|
||||
local ini_cfg = {}
|
||||
---@class plc_config
|
||||
local settings_cfg = {}
|
||||
|
||||
-- all settings fields, their nice names, and their default values
|
||||
local fields = {
|
||||
{ "Networked", "Networked" },
|
||||
{ "UnitID", "Unit ID" },
|
||||
{ "EmerCoolEnable", "Emergency Coolant" },
|
||||
{ "EmerCoolSide", "Emergency Coolant Side" },
|
||||
{ "EmerCoolColor", "Emergency Coolant Color" },
|
||||
{ "SVR_Channel", "SVR Channel" },
|
||||
{ "PLC_Channel", "PLC Channel" },
|
||||
{ "ConnTimeout", "Connection Timeout" },
|
||||
{ "TrustedRange", "Trusted Range" },
|
||||
{ "AuthKey", "Facility Auth Key" },
|
||||
{ "LogMode", "Log Mode" },
|
||||
{ "LogPath", "Log Path" },
|
||||
{ "LogDebug","Log Debug Messages" }
|
||||
{ "Networked", "Networked", false },
|
||||
{ "UnitID", "Unit ID", 1 },
|
||||
{ "EmerCoolEnable", "Emergency Coolant", false },
|
||||
{ "EmerCoolSide", "Emergency Coolant Side", nil },
|
||||
{ "EmerCoolColor", "Emergency Coolant Color", nil },
|
||||
{ "SVR_Channel", "SVR Channel", 16240 },
|
||||
{ "PLC_Channel", "PLC Channel", 16241 },
|
||||
{ "ConnTimeout", "Connection Timeout", 5 },
|
||||
{ "TrustedRange", "Trusted Range", 0 },
|
||||
{ "AuthKey", "Facility Auth Key" , ""},
|
||||
{ "LogMode", "Log Mode", log.MODE.APPEND },
|
||||
{ "LogPath", "Log Path", "/log.txt" },
|
||||
{ "LogDebug", "Log Debug Messages", false },
|
||||
{ "FrontPanelTheme", "Front Panel Theme", themes.FP_THEME.SANDSTONE },
|
||||
{ "ColorMode", "Color Mode", themes.COLOR_MODE.STANDARD }
|
||||
}
|
||||
|
||||
local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" }
|
||||
@@ -117,25 +132,6 @@ local side_options_map = { "top", "bottom", "left", "right", "front", "back" }
|
||||
local color_options = { "Red", "Orange", "Yellow", "Lime", "Green", "Cyan", "Light Blue", "Blue", "Purple", "Magenta", "Pink", "White", "Light Gray", "Gray", "Black", "Brown" }
|
||||
local color_options_map = { colors.red, colors.orange, colors.yellow, colors.lime, colors.green, colors.cyan, colors.lightBlue, colors.blue, colors.purple, colors.magenta, colors.pink, colors.white, colors.lightGray, colors.gray, colors.black, colors.brown }
|
||||
|
||||
local color_name_map = {
|
||||
[colors.red] = "red",
|
||||
[colors.orange] = "orange",
|
||||
[colors.yellow] = "yellow",
|
||||
[colors.lime] = "lime",
|
||||
[colors.green] = "green",
|
||||
[colors.cyan] = "cyan",
|
||||
[colors.lightBlue] = "lightBlue",
|
||||
[colors.blue] = "blue",
|
||||
[colors.purple] = "purple",
|
||||
[colors.magenta] = "magenta",
|
||||
[colors.pink] = "pink",
|
||||
[colors.white] = "white",
|
||||
[colors.lightGray] = "lightGray",
|
||||
[colors.gray] = "gray",
|
||||
[colors.black] = "black",
|
||||
[colors.brown] = "brown"
|
||||
}
|
||||
|
||||
-- convert text representation to index
|
||||
---@param side string
|
||||
local function side_to_idx(side)
|
||||
@@ -154,27 +150,20 @@ end
|
||||
|
||||
-- load data from the settings file
|
||||
---@param target plc_config
|
||||
local function load_settings(target)
|
||||
target.Networked = settings.get("Networked", false)
|
||||
target.UnitID = settings.get("UnitID", 1)
|
||||
target.EmerCoolEnable = settings.get("EmerCoolEnable", false)
|
||||
target.EmerCoolSide = settings.get("EmerCoolSide", nil)
|
||||
target.EmerCoolColor = settings.get("EmerCoolColor", nil)
|
||||
target.SVR_Channel = settings.get("SVR_Channel", 16240)
|
||||
target.PLC_Channel = settings.get("PLC_Channel", 16241)
|
||||
target.ConnTimeout = settings.get("ConnTimeout", 5)
|
||||
target.TrustedRange = settings.get("TrustedRange", 0)
|
||||
target.AuthKey = settings.get("AuthKey", "")
|
||||
target.LogMode = settings.get("LogMode", log.MODE.APPEND)
|
||||
target.LogPath = settings.get("LogPath", "/log.txt")
|
||||
target.LogDebug = settings.get("LogDebug", false)
|
||||
---@param raw boolean? true to not use default values
|
||||
local function load_settings(target, raw)
|
||||
for _, v in pairs(fields) do settings.unset(v[1]) end
|
||||
|
||||
local loaded = settings.load("/reactor-plc.settings")
|
||||
|
||||
for _, v in pairs(fields) do target[v[1]] = settings.get(v[1], tri(raw, nil, v[3])) end
|
||||
|
||||
return loaded
|
||||
end
|
||||
|
||||
-- create the config view
|
||||
---@param display graphics_element
|
||||
local function config_view(display)
|
||||
local nav_fg_bg = cpair(colors.black,colors.white)
|
||||
local btn_act_fg_bg = cpair(colors.white,colors.gray)
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local function exit() os.queueEvent("terminate") end
|
||||
@@ -187,26 +176,28 @@ local function config_view(display)
|
||||
local plc_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local clr_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local summary = Div{parent=root_pane_div,x=1,y=1}
|
||||
local changelog = Div{parent=root_pane_div,x=1,y=1}
|
||||
|
||||
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,plc_cfg,net_cfg,log_cfg,summary}}
|
||||
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,plc_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog}}
|
||||
|
||||
-- MAIN PAGE
|
||||
-- Main Page
|
||||
|
||||
local y_start = 5
|
||||
|
||||
TextBox{parent=main_page,x=2,y=2,height=2,text_align=CENTER,text="Welcome to the Reactor PLC configurator! Please select one of the following options."}
|
||||
TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the Reactor PLC configurator! Please select one of the following options."}
|
||||
|
||||
if tool_ctl.ask_config then
|
||||
TextBox{parent=main_page,x=2,y=y_start,height=2,text_align=CENTER,text="Notice: This device has no valid config. The configurator has been automatically started.",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
y_start = y_start + 3
|
||||
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device has no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
y_start = y_start + 5
|
||||
end
|
||||
|
||||
local function view_config()
|
||||
tool_ctl.viewing_config = true
|
||||
tool_ctl.gen_summary(ini_cfg)
|
||||
tool_ctl.gen_summary(settings_cfg)
|
||||
tool_ctl.settings_apply.hide(true)
|
||||
main_pane.set_value(5)
|
||||
main_pane.set_value(6)
|
||||
end
|
||||
|
||||
if fs.exists("/reactor-plc/config.lua") then
|
||||
@@ -217,11 +208,23 @@ local function config_view(display)
|
||||
PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure System",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
|
||||
if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end
|
||||
local function jump_color()
|
||||
tool_ctl.jumped_to_color = true
|
||||
tool_ctl.color_next.hide(true)
|
||||
tool_ctl.color_apply.show()
|
||||
main_pane.set_value(5)
|
||||
end
|
||||
|
||||
PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
-- PLC CONFIG
|
||||
if not tool_ctl.has_config then
|
||||
tool_ctl.view_cfg.disable()
|
||||
tool_ctl.color_cfg.disable()
|
||||
end
|
||||
|
||||
--#region PLC
|
||||
|
||||
local plc_c_1 = Div{parent=plc_cfg,x=2,y=4,width=49}
|
||||
local plc_c_2 = Div{parent=plc_cfg,x=2,y=4,width=49}
|
||||
@@ -230,10 +233,10 @@ local function config_view(display)
|
||||
|
||||
local plc_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4}}
|
||||
|
||||
TextBox{parent=plc_cfg,x=1,y=2,height=1,text_align=CENTER,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)}
|
||||
TextBox{parent=plc_cfg,x=1,y=2,height=1,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)}
|
||||
|
||||
TextBox{parent=plc_c_1,x=1,y=1,height=1,text_align=CENTER,text="Would you like to set this PLC as networked?"}
|
||||
TextBox{parent=plc_c_1,x=1,y=3,height=4,text_align=CENTER,text="If you have a supervisor, select the box. You will later be prompted to select the network configuration. If you instead want to use this as a standalone safety system, don't select the box.",fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
TextBox{parent=plc_c_1,x=1,y=1,height=1,text="Would you like to set this PLC as networked?"}
|
||||
TextBox{parent=plc_c_1,x=1,y=3,height=4,text="If you have a supervisor, select the box. You will later be prompted to select the network configuration. If you instead want to use this as a standalone safety system, don't select the box.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local networked = CheckBox{parent=plc_c_1,x=1,y=8,label="Networked",default=ini_cfg.Networked,box_fg_bg=cpair(colors.orange,colors.black)}
|
||||
|
||||
@@ -242,16 +245,16 @@ local function config_view(display)
|
||||
plc_pane.set_value(2)
|
||||
end
|
||||
|
||||
PushButton{parent=plc_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_networked,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_1,x=44,y=14,text="Next \x1a",callback=submit_networked,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=plc_c_2,x=1,y=1,height=1,text_align=CENTER,text="Please enter the reactor unit ID for this PLC."}
|
||||
TextBox{parent=plc_c_2,x=1,y=3,height=3,text_align=CENTER,text="If this is a networked PLC, currently only IDs 1 through 4 are acceptable.",fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
TextBox{parent=plc_c_2,x=1,y=1,height=1,text="Please enter the reactor unit ID for this PLC."}
|
||||
TextBox{parent=plc_c_2,x=1,y=3,height=3,text="If this is a networked PLC, currently only IDs 1 through 4 are acceptable.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=plc_c_2,x=1,y=6,height=1,text_align=CENTER,text="Unit #"}
|
||||
local u_id = NumberField{parent=plc_c_2,x=7,y=6,width=5,max_digits=3,default=ini_cfg.UnitID,min=1,fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=plc_c_2,x=1,y=6,height=1,text="Unit #"}
|
||||
local u_id = NumberField{parent=plc_c_2,x=7,y=6,width=5,max_chars=3,default=ini_cfg.UnitID,min=1,fg_bg=bw_fg_bg}
|
||||
|
||||
local u_id_err = TextBox{parent=plc_c_2,x=8,y=14,height=1,width=35,text_align=LEFT,text="Please set a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local u_id_err = TextBox{parent=plc_c_2,x=8,y=14,height=1,width=35,text="Please set a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_id()
|
||||
local unit_id = tonumber(u_id.get_value())
|
||||
@@ -262,11 +265,11 @@ local function config_view(display)
|
||||
else u_id_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=plc_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_id,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_2,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_2,x=44,y=14,text="Next \x1a",callback=submit_id,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=plc_c_3,x=1,y=1,height=4,text_align=CENTER,text="When networked, the supervisor takes care of emergency coolant via RTUs. However, you can configure independent emergency coolant via the PLC. "}
|
||||
TextBox{parent=plc_c_3,x=1,y=6,height=5,text_align=CENTER,text="This independent control can be used with or without a supervisor. To configure, you would next select the interface of the redstone output connected to one or more mekanism pipes.",fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
TextBox{parent=plc_c_3,x=1,y=1,height=4,text="When networked, the supervisor takes care of emergency coolant via RTUs. However, you can configure independent emergency coolant via the PLC."}
|
||||
TextBox{parent=plc_c_3,x=1,y=6,height=5,text="This independent control can be used with or without a supervisor. To configure, you would next select the interface of the redstone output connected to one or more mekanism pipes.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local en_em_cool = CheckBox{parent=plc_c_3,x=1,y=11,label="Enable PLC Emergency Coolant Control",default=ini_cfg.EmerCoolEnable,box_fg_bg=cpair(colors.orange,colors.black)}
|
||||
|
||||
@@ -279,27 +282,29 @@ local function config_view(display)
|
||||
if tmp_cfg.EmerCoolEnable then plc_pane.set_value(4) else next_from_plc() end
|
||||
end
|
||||
|
||||
PushButton{parent=plc_c_3,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_3,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_en_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_3,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_3,x=44,y=14,text="Next \x1a",callback=submit_en_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=plc_c_4,x=1,y=1,height=1,text_align=CENTER,text="Emergency Coolant Redstone Output Side"}
|
||||
TextBox{parent=plc_c_4,x=1,y=1,height=1,text="Emergency Coolant Redstone Output Side"}
|
||||
local side = Radio2D{parent=plc_c_4,x=1,y=2,rows=2,columns=3,default=side_to_idx(ini_cfg.EmerCoolSide),options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.orange}
|
||||
|
||||
TextBox{parent=plc_c_4,x=1,y=5,height=1,text_align=CENTER,text="Bundled Redstone Configuration"}
|
||||
TextBox{parent=plc_c_4,x=1,y=5,height=1,text="Bundled Redstone Configuration"}
|
||||
local bundled = CheckBox{parent=plc_c_4,x=1,y=6,label="Is Bundled?",default=ini_cfg.EmerCoolColor~=nil,box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)tool_ctl.bundled_emcool(v)end}
|
||||
local color = Radio2D{parent=plc_c_4,x=1,y=8,rows=4,columns=4,default=color_to_idx(ini_cfg.EmerCoolColor),options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
local color = Radio2D{parent=plc_c_4,x=1,y=8,rows=4,columns=4,default=color_to_idx(ini_cfg.EmerCoolColor),options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
|
||||
if ini_cfg.EmerCoolColor == nil then color.disable() end
|
||||
|
||||
local function submit_emcool()
|
||||
tmp_cfg.EmerCoolSide = side_options_map[side.get_value()]
|
||||
tmp_cfg.EmerCoolColor = color_options_map[color.get_value()]
|
||||
tmp_cfg.EmerCoolColor = util.trinary(bundled.get_value(), color_options_map[color.get_value()], nil)
|
||||
next_from_plc()
|
||||
end
|
||||
|
||||
PushButton{parent=plc_c_4,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_4,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_4,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_4,x=44,y=14,text="Next \x1a",callback=submit_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
-- NET CONFIG
|
||||
--#endregion
|
||||
|
||||
--#region Network
|
||||
|
||||
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
@@ -307,19 +312,19 @@ local function config_view(display)
|
||||
|
||||
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}}
|
||||
|
||||
TextBox{parent=net_cfg,x=1,y=2,height=1,text_align=CENTER,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
TextBox{parent=net_cfg,x=1,y=2,height=1,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=1,height=1,text_align=CENTER,text="Please set the network channels below."}
|
||||
TextBox{parent=net_c_1,x=1,y=3,height=4,text_align=CENTER,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
TextBox{parent=net_c_1,x=1,y=1,height=1,text="Please set the network channels below."}
|
||||
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=8,height=1,text_align=CENTER,text="Supervisor Channel"}
|
||||
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=net_c_1,x=9,y=9,height=4,text_align=CENTER,text="[SVR_CHANNEL]",fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
TextBox{parent=net_c_1,x=1,y=11,height=1,text_align=CENTER,text="PLC Channel"}
|
||||
local plc_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=net_c_1,x=9,y=12,height=4,text_align=CENTER,text="[PLC_CHANNEL]",fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
TextBox{parent=net_c_1,x=1,y=8,height=1,text="Supervisor Channel"}
|
||||
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_1,x=1,y=11,height=1,text="PLC Channel"}
|
||||
local plc_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=12,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local chan_err = TextBox{parent=net_c_1,x=8,y=14,height=1,width=35,text_align=LEFT,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local chan_err = TextBox{parent=net_c_1,x=8,y=14,height=1,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_channels()
|
||||
local svr_c = tonumber(svr_chan.get_value())
|
||||
@@ -338,19 +343,19 @@ local function config_view(display)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=1,height=1,text_align=CENTER,text="Connection Timeout"}
|
||||
local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=net_c_2,x=9,y=2,height=2,text_align=CENTER,text="seconds (default 5)",fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
TextBox{parent=net_c_2,x=1,y=3,height=4,text_align=CENTER,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
TextBox{parent=net_c_2,x=1,y=1,height=1,text="Connection Timeout"}
|
||||
local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_2,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=8,height=1,text_align=CENTER,text="Trusted Range"}
|
||||
local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_digits=20,allow_decimal=true,fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=net_c_2,x=1,y=10,height=4,text_align=CENTER,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
TextBox{parent=net_c_2,x=1,y=8,height=1,text="Trusted Range"}
|
||||
local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_2,x=1,y=10,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local p2_err = TextBox{parent=net_c_2,x=8,y=14,height=1,width=35,text_align=LEFT,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local p2_err = TextBox{parent=net_c_2,x=8,y=14,height=1,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_ct_tr()
|
||||
local timeout_val = tonumber(timeout.get_value())
|
||||
@@ -369,14 +374,14 @@ local function config_view(display)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,x=1,y=1,height=2,text_align=CENTER,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
|
||||
TextBox{parent=net_c_3,x=1,y=4,height=6,text_align=CENTER,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra compution (can slow things down).",fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
TextBox{parent=net_c_3,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
|
||||
TextBox{parent=net_c_3,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra compution (can slow things down).",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,x=1,y=11,height=1,text_align=CENTER,text="Facility Auth Key"}
|
||||
local key, _, censor = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=net_c_3,x=1,y=11,height=1,text="Facility Auth Key"}
|
||||
local key, _, censor = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
|
||||
|
||||
local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
|
||||
|
||||
@@ -385,32 +390,40 @@ local function config_view(display)
|
||||
hide_key.set_value(true)
|
||||
censor_key(true)
|
||||
|
||||
local key_err = TextBox{parent=net_c_3,x=8,y=14,height=1,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_auth()
|
||||
tmp_cfg.AuthKey = key.get_value()
|
||||
main_pane.set_value(4)
|
||||
local v = key.get_value()
|
||||
if string.len(v) == 0 or string.len(v) >= 8 then
|
||||
tmp_cfg.AuthKey = key.get_value()
|
||||
main_pane.set_value(4)
|
||||
key_err.hide(true)
|
||||
else key_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_3,x=1,y=14,min_width=6,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
-- LOG CONFIG
|
||||
--#endregion
|
||||
|
||||
--#region Logging
|
||||
|
||||
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=log_cfg,x=1,y=2,height=1,text_align=CENTER,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
TextBox{parent=log_cfg,x=1,y=2,height=1,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=1,height=1,text_align=CENTER,text="Please configure logging below."}
|
||||
TextBox{parent=log_c_1,x=1,y=1,height=1,text="Please configure logging below."}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=3,height=1,text_align=CENTER,text="Log File Mode"}
|
||||
TextBox{parent=log_c_1,x=1,y=3,height=1,text="Log File Mode"}
|
||||
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=7,height=1,text_align=CENTER,text="Log File Path"}
|
||||
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=log_c_1,x=1,y=7,height=1,text="Log File Path"}
|
||||
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||
|
||||
local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||
TextBox{parent=log_c_1,x=3,y=11,height=2,text_align=CENTER,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local path_err = TextBox{parent=log_c_1,x=8,y=14,height=1,width=35,text_align=LEFT,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local path_err = TextBox{parent=log_c_1,x=8,y=14,height=1,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_log()
|
||||
if path.get_value() ~= "" then
|
||||
@@ -418,10 +431,8 @@ local function config_view(display)
|
||||
tmp_cfg.LogMode = mode.get_value() - 1
|
||||
tmp_cfg.LogPath = path.get_value()
|
||||
tmp_cfg.LogDebug = en_dbg.get_value()
|
||||
tool_ctl.gen_summary(tmp_cfg)
|
||||
tool_ctl.viewing_config = false
|
||||
tool_ctl.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
tool_ctl.color_apply.hide(true)
|
||||
tool_ctl.color_next.show()
|
||||
main_pane.set_value(5)
|
||||
else path_err.show() end
|
||||
end
|
||||
@@ -430,10 +441,122 @@ local function config_view(display)
|
||||
if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(2) end
|
||||
end
|
||||
|
||||
PushButton{parent=log_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=back_from_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=back_from_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
-- SUMMARY OF CHANGES
|
||||
--#endregion
|
||||
|
||||
--#region Color Options
|
||||
|
||||
local clr_c_1 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
local clr_c_2 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
local clr_c_3 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
local clr_c_4 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
|
||||
local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}}
|
||||
|
||||
TextBox{parent=clr_cfg,x=1,y=2,height=1,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)}
|
||||
|
||||
TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color theme for the front panel."}
|
||||
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=clr_c_1,x=1,y=7,height=1,text="Front Panel Theme"}
|
||||
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
|
||||
TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."}
|
||||
|
||||
TextBox{parent=clr_c_2,x=21,y=7,height=1,text="Preview"}
|
||||
local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)}
|
||||
_ = IndLight{parent=clr_c_2,x=21,y=9,label="Warning",colors=cpair(colors.black,colors.yellow)}
|
||||
_ = IndLight{parent=clr_c_2,x=21,y=10,label="Bad",colors=cpair(colors.black,colors.red)}
|
||||
local b_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.black,colors.black),hidden=true}
|
||||
local g_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.gray,colors.gray),hidden=true}
|
||||
|
||||
local function recolor(value)
|
||||
local c = themes.smooth_stone.color_modes[value]
|
||||
|
||||
if value == themes.COLOR_MODE.STANDARD or value == themes.COLOR_MODE.BLUE_IND then
|
||||
b_off.hide()
|
||||
g_off.show()
|
||||
else
|
||||
g_off.hide()
|
||||
b_off.show()
|
||||
end
|
||||
|
||||
if #c == 0 then
|
||||
for i = 1, #style.colors do term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end
|
||||
else
|
||||
term.setPaletteColor(colors.green, c[1].hex)
|
||||
term.setPaletteColor(colors.yellow, c[2].hex)
|
||||
term.setPaletteColor(colors.red, c[3].hex)
|
||||
end
|
||||
end
|
||||
|
||||
TextBox{parent=clr_c_2,x=1,y=7,height=1,width=10,text="Color Mode"}
|
||||
local c_mode = RadioButton{parent=clr_c_2,x=1,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
|
||||
TextBox{parent=clr_c_2,x=21,y=13,height=2,width=18,text="Note: exact color varies by theme.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
local function back_from_colors()
|
||||
main_pane.set_value(util.trinary(tool_ctl.jumped_to_color, 1, 4))
|
||||
tool_ctl.jumped_to_color = false
|
||||
recolor(1)
|
||||
end
|
||||
|
||||
local function show_access()
|
||||
clr_pane.set_value(2)
|
||||
recolor(c_mode.get_value())
|
||||
end
|
||||
|
||||
local function submit_colors()
|
||||
tmp_cfg.FrontPanelTheme = fp_theme.get_value()
|
||||
tmp_cfg.ColorMode = c_mode.get_value()
|
||||
|
||||
if tool_ctl.jumped_to_color then
|
||||
settings.set("FrontPanelTheme", tmp_cfg.FrontPanelTheme)
|
||||
settings.set("ColorMode", tmp_cfg.ColorMode)
|
||||
|
||||
if settings.save("/reactor-plc.settings") then
|
||||
load_settings(settings_cfg, true)
|
||||
load_settings(ini_cfg)
|
||||
clr_pane.set_value(3)
|
||||
else
|
||||
clr_pane.set_value(4)
|
||||
end
|
||||
else
|
||||
tool_ctl.gen_summary(tmp_cfg)
|
||||
tool_ctl.viewing_config = false
|
||||
tool_ctl.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
main_pane.set_value(6)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=clr_c_1,x=1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=clr_c_1,x=8,y=14,min_width=15,text="Accessibility",callback=show_access,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.color_next = PushButton{parent=clr_c_1,x=44,y=14,text="Next \x1a",callback=submit_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.color_apply = PushButton{parent=clr_c_1,x=43,y=14,min_width=7,text="Apply",callback=submit_colors,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
tool_ctl.color_apply.hide(true)
|
||||
|
||||
local function c_go_home()
|
||||
main_pane.set_value(1)
|
||||
clr_pane.set_value(1)
|
||||
end
|
||||
|
||||
TextBox{parent=clr_c_3,x=1,y=1,height=1,text="Settings saved!"}
|
||||
PushButton{parent=clr_c_3,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
PushButton{parent=clr_c_3,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=clr_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=clr_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
PushButton{parent=clr_c_4,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Summary and Saving
|
||||
|
||||
local sum_c_1 = Div{parent=summary,x=2,y=4,width=49}
|
||||
local sum_c_2 = Div{parent=summary,x=2,y=4,width=49}
|
||||
@@ -442,9 +565,9 @@ local function config_view(display)
|
||||
|
||||
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
|
||||
|
||||
TextBox{parent=summary,x=1,y=2,height=1,text_align=CENTER,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||
TextBox{parent=summary,x=1,y=2,height=1,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||
|
||||
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=51,scroll_height=100,fg_bg=cpair(colors.black,colors.white),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
|
||||
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local function back_from_settings()
|
||||
if tool_ctl.viewing_config or tool_ctl.importing_legacy then
|
||||
@@ -453,7 +576,7 @@ local function config_view(display)
|
||||
tool_ctl.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
else
|
||||
main_pane.set_value(4)
|
||||
main_pane.set_value(5)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -466,7 +589,8 @@ local function config_view(display)
|
||||
local function save_and_continue()
|
||||
for k, v in pairs(tmp_cfg) do settings.set(k, v) end
|
||||
|
||||
if settings.save("reactor-plc.settings") then
|
||||
if settings.save("/reactor-plc.settings") then
|
||||
load_settings(settings_cfg, true)
|
||||
load_settings(ini_cfg)
|
||||
|
||||
try_set(networked, ini_cfg.Networked)
|
||||
@@ -483,6 +607,10 @@ local function config_view(display)
|
||||
try_set(mode, ini_cfg.LogMode)
|
||||
try_set(path, ini_cfg.LogPath)
|
||||
try_set(en_dbg, ini_cfg.LogDebug)
|
||||
try_set(fp_theme, ini_cfg.FrontPanelTheme)
|
||||
try_set(c_mode, ini_cfg.ColorMode)
|
||||
|
||||
tool_ctl.view_cfg.enable()
|
||||
|
||||
if tool_ctl.importing_legacy then
|
||||
tool_ctl.importing_legacy = false
|
||||
@@ -495,23 +623,24 @@ local function config_view(display)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_1,x=1,y=14,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=sum_c_2,x=1,y=1,height=1,text_align=CENTER,text="Settings saved!"}
|
||||
TextBox{parent=sum_c_2,x=1,y=1,height=1,text="Settings saved!"}
|
||||
|
||||
local function go_home()
|
||||
main_pane.set_value(1)
|
||||
plc_pane.set_value(1)
|
||||
net_pane.set_value(1)
|
||||
clr_pane.set_value(1)
|
||||
sum_pane.set_value(1)
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_3,x=1,y=1,height=2,text_align=CENTER,text="The old config.lua file will now be deleted, then the configurator will exit."}
|
||||
TextBox{parent=sum_c_3,x=1,y=1,height=2,text="The old config.lua file will now be deleted, then the configurator will exit."}
|
||||
|
||||
local function delete_legacy()
|
||||
fs.delete("/reactor-plc/config.lua")
|
||||
@@ -521,11 +650,31 @@ local function config_view(display)
|
||||
PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_4,x=1,y=1,height=5,text_align=CENTER,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
|
||||
TextBox{parent=sum_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
--#endregion
|
||||
|
||||
-- Config Change Log
|
||||
|
||||
local cl = Div{parent=changelog,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=changelog,x=1,y=2,height=1,text=" Config Change Log",fg_bg=bw_fg_bg}
|
||||
|
||||
local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
for _, change in ipairs(changes) do
|
||||
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg}
|
||||
for _, v in ipairs(change[2]) do
|
||||
local e = Div{parent=c_log,height=#util.strwrap(v,46)}
|
||||
TextBox{parent=e,y=1,x=1,text="- ",height=1,fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
-- set tool functions now that we have the elements
|
||||
|
||||
function tool_ctl.set_networked(enable)
|
||||
@@ -562,7 +711,7 @@ local function config_view(display)
|
||||
|
||||
tool_ctl.gen_summary(tmp_cfg)
|
||||
sum_pane.set_value(1)
|
||||
main_pane.set_value(5)
|
||||
main_pane.set_value(6)
|
||||
tool_ctl.importing_legacy = true
|
||||
end
|
||||
|
||||
@@ -580,7 +729,7 @@ local function config_view(display)
|
||||
local alternate = false
|
||||
local inner_width = setting_list.get_width() - 1
|
||||
|
||||
tool_ctl.show_key_btn.enable()
|
||||
if cfg.AuthKey then tool_ctl.show_key_btn.enable() else tool_ctl.show_key_btn.disable() end
|
||||
tool_ctl.auth_key_value = cfg.AuthKey or "" -- to show auth key
|
||||
|
||||
for i = 1, #fields do
|
||||
@@ -591,12 +740,18 @@ local function config_view(display)
|
||||
local raw = cfg[f[1]]
|
||||
local val = util.strval(raw)
|
||||
|
||||
if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) end
|
||||
if f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end
|
||||
if f[1] == "EmerCoolColor" and raw ~= nil then val = color_name_map[raw] end
|
||||
if val == "nil" then val = "n/a" end
|
||||
if f[1] == "AuthKey" and raw then val = string.rep("*", string.len(val))
|
||||
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace")
|
||||
elseif f[1] == "EmerCoolColor" and raw ~= nil then val = rsio.color_name(raw)
|
||||
elseif f[1] == "FrontPanelTheme" then
|
||||
val = util.strval(themes.fp_theme_name(raw))
|
||||
elseif f[1] == "ColorMode" then
|
||||
val = util.strval(themes.color_mode_name(raw))
|
||||
end
|
||||
|
||||
local c = util.trinary(alternate, cpair(colors.gray,colors.lightGray), cpair(colors.gray,colors.white))
|
||||
if val == "nil" then val = "<not set>" end
|
||||
|
||||
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
|
||||
alternate = not alternate
|
||||
|
||||
if string.len(val) > val_max_w then
|
||||
@@ -628,12 +783,12 @@ local function reset_term()
|
||||
end
|
||||
|
||||
-- run the reactor PLC configurator
|
||||
---@param ask_config? boolean indicate if this is being called by the PLC startup app due to an invalid configuration
|
||||
---@param ask_config? boolean indicate if this is being called by the startup app due to an invalid configuration
|
||||
function configurator.configure(ask_config)
|
||||
tool_ctl.ask_config = ask_config == true
|
||||
tool_ctl.has_config = settings.load("/reactor-plc.settings")
|
||||
|
||||
load_settings(ini_cfg)
|
||||
load_settings(settings_cfg, true)
|
||||
tool_ctl.has_config = load_settings(ini_cfg)
|
||||
|
||||
reset_term()
|
||||
|
||||
@@ -651,18 +806,14 @@ function configurator.configure(ask_config)
|
||||
|
||||
-- handle event
|
||||
if event == "timer" then
|
||||
-- notify timer callback dispatcher
|
||||
tcd.handle(param1)
|
||||
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
|
||||
-- handle a mouse event
|
||||
local m_e = core.events.new_mouse_event(event, param1, param2, param3)
|
||||
if m_e then display.handle_mouse(m_e) end
|
||||
elseif event == "char" or event == "key" or event == "key_up" then
|
||||
-- handle a key event
|
||||
local k_e = core.events.new_key_event(event, param1, param2)
|
||||
if k_e then display.handle_key(k_e) end
|
||||
elseif event == "paste" then
|
||||
-- handle a paste event
|
||||
display.handle_paste(param1)
|
||||
end
|
||||
|
||||
|
||||
@@ -70,9 +70,9 @@ function databus.tx_link_state(state)
|
||||
end
|
||||
|
||||
-- transmit reactor enable state across the bus
|
||||
---@param active boolean reactor active
|
||||
---@param active any reactor active
|
||||
function databus.tx_reactor_state(active)
|
||||
databus.ps.publish("reactor_active", active)
|
||||
databus.ps.publish("reactor_active", active == true)
|
||||
end
|
||||
|
||||
-- transmit RPS data across the bus
|
||||
|
||||
@@ -23,6 +23,8 @@ local LED = require("graphics.elements.indicators.led")
|
||||
local LEDPair = require("graphics.elements.indicators.ledpair")
|
||||
local RGBLED = require("graphics.elements.indicators.ledrgb")
|
||||
|
||||
local LINK_STATE = types.PANEL_LINK_STATE
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
@@ -34,8 +36,12 @@ local ind_red = style.ind_red
|
||||
-- create new front panel view
|
||||
---@param panel graphics_element main displaybox
|
||||
local function init(panel)
|
||||
local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=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)
|
||||
local s_hi_box = style.theme.highlight_box
|
||||
|
||||
local disabled_fg = style.fp.disabled_fg
|
||||
|
||||
local header = TextBox{parent=panel,y=1,text="FISSION REACTOR PLC - UNIT ?",alignment=ALIGN.CENTER,height=1,fg_bg=style.theme.header}
|
||||
header.register(databus.ps, "unit_id", function (id) header.set_value(util.c("FISSION REACTOR PLC - UNIT ", id)) end)
|
||||
|
||||
--
|
||||
-- system indicators
|
||||
@@ -52,13 +58,47 @@ local function init(panel)
|
||||
|
||||
local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green}
|
||||
local modem = LED{parent=system,label="MODEM",colors=ind_grn}
|
||||
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)
|
||||
|
||||
if not style.colorblind then
|
||||
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,style.ind_bkg}}
|
||||
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
network.register(databus.ps, "link_state", network.update)
|
||||
else
|
||||
local nt_lnk = LEDPair{parent=system,label="NT LINKED",off=style.ind_bkg,c1=colors.red,c2=colors.green}
|
||||
local nt_ver = LEDPair{parent=system,label="NT VERSION",off=style.ind_bkg,c1=colors.red,c2=colors.green}
|
||||
local nt_col = LED{parent=system,label="NT COLLISION",colors=ind_red}
|
||||
|
||||
nt_lnk.register(databus.ps, "link_state", function (state)
|
||||
local value = 2
|
||||
|
||||
if state == LINK_STATE.DISCONNECTED then
|
||||
value = 1
|
||||
elseif state == LINK_STATE.LINKED then
|
||||
value = 3
|
||||
end
|
||||
|
||||
nt_lnk.update(value)
|
||||
end)
|
||||
|
||||
nt_ver.register(databus.ps, "link_state", function (state)
|
||||
local value = 3
|
||||
|
||||
if state == LINK_STATE.BAD_VERSION then
|
||||
value = 2
|
||||
elseif state == LINK_STATE.DISCONNECTED then
|
||||
value = 1
|
||||
end
|
||||
|
||||
nt_ver.update(value)
|
||||
end)
|
||||
|
||||
nt_col.register(databus.ps, "link_state", function (state) nt_col.update(state == LINK_STATE.COLLISION) end)
|
||||
end
|
||||
|
||||
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=ind_grn}
|
||||
local rt_rps = LED{parent=system,label="RT RPS",colors=ind_grn}
|
||||
@@ -75,7 +115,7 @@ local function init(panel)
|
||||
|
||||
---@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)}
|
||||
TextBox{parent=system,x=9,y=5,width=6,height=1,text=comp_id,fg_bg=disabled_fg}
|
||||
|
||||
--
|
||||
-- status & controls
|
||||
@@ -91,12 +131,12 @@ local function init(panel)
|
||||
emer_cool.register(databus.ps, "emer_cool", emer_cool.update)
|
||||
end
|
||||
|
||||
local status_trip_rct = Rectangle{parent=status,width=20,height=3,x=1,border=border(1,colors.lightGray,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)}
|
||||
local status_trip = Div{parent=status_trip_rct,width=18,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
|
||||
local status_trip_rct = Rectangle{parent=status,width=20,height=3,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true}
|
||||
local status_trip = Div{parent=status_trip_rct,width=18,height=1,fg_bg=s_hi_box}
|
||||
local scram = LED{parent=status_trip,width=10,label="RPS TRIP",colors=ind_red,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)}
|
||||
local controls_rct = Rectangle{parent=status,width=17,height=3,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true}
|
||||
local controls = Div{parent=controls_rct,width=15,height=1,fg_bg=s_hi_box}
|
||||
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)}
|
||||
|
||||
@@ -107,9 +147,9 @@ local function init(panel)
|
||||
-- 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=ALIGN.LEFT,height=1}
|
||||
local comms_v = TextBox{parent=about,x=17,y=1,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1}
|
||||
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=disabled_fg}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=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)
|
||||
@@ -118,7 +158,7 @@ local function init(panel)
|
||||
-- rps list
|
||||
--
|
||||
|
||||
local rps = Rectangle{parent=panel,width=16,height=16,x=36,y=3,border=border(1,colors.lightGray),thin=true,fg_bg=cpair(colors.black,colors.lightGray)}
|
||||
local rps = Rectangle{parent=panel,width=16,height=16,x=36,y=3,border=border(1,s_hi_box.bkg),thin=true,fg_bg=s_hi_box}
|
||||
local rps_man = LED{parent=rps,label="MANUAL",colors=ind_red}
|
||||
local rps_auto = LED{parent=rps,label="AUTOMATIC",colors=ind_red}
|
||||
local rps_tmo = LED{parent=rps,label="TIMEOUT",colors=ind_red}
|
||||
|
||||
@@ -2,46 +2,40 @@
|
||||
-- Graphics Style Options
|
||||
--
|
||||
|
||||
local core = require("graphics.core")
|
||||
local core = require("graphics.core")
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
---@class plc_style
|
||||
local style = {}
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
-- GLOBAL --
|
||||
|
||||
-- remap global colors
|
||||
colors.ivory = colors.pink
|
||||
colors.yellow_hc = colors.purple
|
||||
colors.red_off = colors.brown
|
||||
colors.yellow_off = colors.magenta
|
||||
colors.green_off = colors.lime
|
||||
|
||||
style.root = cpair(colors.black, colors.ivory)
|
||||
style.header = cpair(colors.black, colors.lightGray)
|
||||
|
||||
style.colors = {
|
||||
{ c = colors.red, hex = 0xdf4949 }, -- RED ON
|
||||
{ c = colors.orange, hex = 0xffb659 },
|
||||
{ c = colors.yellow, hex = 0xf9fb53 }, -- YELLOW ON
|
||||
{ c = colors.lime, hex = 0x16665a }, -- GREEN OFF
|
||||
{ c = colors.green, hex = 0x6be551 }, -- GREEN ON
|
||||
{ c = colors.cyan, hex = 0x34bac8 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.purple, hex = 0xb156ee }, -- YELLOW HIGH CONTRAST
|
||||
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
|
||||
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
|
||||
-- { c = colors.white, hex = 0xdcd9ca },
|
||||
{ c = colors.lightGray, hex = 0xb1b8b3 },
|
||||
{ c = colors.gray, hex = 0x575757 },
|
||||
-- { c = colors.black, hex = 0x191919 },
|
||||
{ c = colors.brown, hex = 0x672223 } -- RED OFF
|
||||
}
|
||||
|
||||
-- COMMON COLOR PAIRS --
|
||||
style.theme = themes.sandstone
|
||||
style.fp = themes.get_fp_style(style.theme)
|
||||
style.colorblind = false
|
||||
|
||||
style.ind_grn = cpair(colors.green, colors.green_off)
|
||||
style.ind_red = cpair(colors.red, colors.red_off)
|
||||
|
||||
-- set theme per configuration
|
||||
---@param fp FP_THEME front panel theme
|
||||
---@param color_mode COLOR_MODE the color mode to use
|
||||
function style.set_theme(fp, color_mode)
|
||||
if fp == themes.FP_THEME.SANDSTONE then
|
||||
style.theme = themes.sandstone
|
||||
elseif fp == themes.FP_THEME.BASALT then
|
||||
style.theme = themes.basalt
|
||||
end
|
||||
|
||||
style.fp = themes.get_fp_style(style.theme)
|
||||
|
||||
style.colorblind = color_mode ~= themes.COLOR_MODE.STANDARD and color_mode ~= themes.COLOR_MODE.STD_ON_BLACK
|
||||
|
||||
if color_mode == themes.COLOR_MODE.STANDARD or color_mode == themes.COLOR_MODE.BLUE_IND then
|
||||
style.ind_bkg = colors.gray
|
||||
else
|
||||
style.ind_bkg = colors.black
|
||||
end
|
||||
end
|
||||
|
||||
return style
|
||||
|
||||
@@ -6,6 +6,8 @@ local rsio = require("scada-common.rsio")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
local databus = require("reactor-plc.databus")
|
||||
|
||||
local plc = {}
|
||||
@@ -23,8 +25,8 @@ local RPS_LIMITS = const.RPS_LIMITS
|
||||
|
||||
-- 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."
|
||||
local PCALL_START_MSG = "pcall: Reactor is already active."
|
||||
local PCALL_SCRAM_MSG = "Scram requires the reactor to be active."
|
||||
local PCALL_START_MSG = "Reactor is already active."
|
||||
|
||||
---@type plc_config
|
||||
local config = {}
|
||||
@@ -37,34 +39,55 @@ function plc.load_config()
|
||||
|
||||
config.Networked = settings.get("Networked")
|
||||
config.UnitID = settings.get("UnitID")
|
||||
|
||||
config.EmerCoolEnable = settings.get("EmerCoolEnable")
|
||||
config.EmerCoolSide = settings.get("EmerCoolSide")
|
||||
config.EmerCoolColor = settings.get("EmerCoolColor")
|
||||
|
||||
config.SVR_Channel = settings.get("SVR_Channel")
|
||||
config.PLC_Channel = settings.get("PLC_Channel")
|
||||
config.ConnTimeout = settings.get("ConnTimeout")
|
||||
config.TrustedRange = settings.get("TrustedRange")
|
||||
config.AuthKey = settings.get("AuthKey")
|
||||
|
||||
config.LogMode = settings.get("LogMode")
|
||||
config.LogPath = settings.get("LogPath")
|
||||
config.LogDebug = settings.get("LogDebug")
|
||||
|
||||
config.FrontPanelTheme = settings.get("FrontPanelTheme")
|
||||
config.ColorMode = settings.get("ColorMode")
|
||||
|
||||
local cfv = util.new_validator()
|
||||
|
||||
cfv.assert_type_bool(config.Networked)
|
||||
cfv.assert_type_int(config.UnitID)
|
||||
cfv.assert_type_bool(config.EmerCoolEnable)
|
||||
cfv.assert_channel(config.SVR_Channel)
|
||||
cfv.assert_channel(config.PLC_Channel)
|
||||
cfv.assert_type_int(config.ConnTimeout)
|
||||
cfv.assert_min(config.ConnTimeout, 2)
|
||||
cfv.assert_type_num(config.TrustedRange)
|
||||
cfv.assert_min(config.TrustedRange, 0)
|
||||
cfv.assert_type_str(config.AuthKey)
|
||||
|
||||
if config.Networked == true then
|
||||
cfv.assert_channel(config.SVR_Channel)
|
||||
cfv.assert_channel(config.PLC_Channel)
|
||||
cfv.assert_type_num(config.ConnTimeout)
|
||||
cfv.assert_min(config.ConnTimeout, 2)
|
||||
cfv.assert_type_num(config.TrustedRange)
|
||||
cfv.assert_min(config.TrustedRange, 0)
|
||||
cfv.assert_type_str(config.AuthKey)
|
||||
|
||||
if type(config.AuthKey) == "string" then
|
||||
local len = string.len(config.AuthKey)
|
||||
cfv.assert(len == 0 or len >= 8)
|
||||
end
|
||||
end
|
||||
|
||||
cfv.assert_type_int(config.LogMode)
|
||||
cfv.assert_range(config.LogMode, 0, 1)
|
||||
cfv.assert_type_str(config.LogPath)
|
||||
cfv.assert_type_bool(config.LogDebug)
|
||||
|
||||
cfv.assert_type_int(config.FrontPanelTheme)
|
||||
cfv.assert_range(config.FrontPanelTheme, 1, 2)
|
||||
cfv.assert_type_int(config.ColorMode)
|
||||
cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
|
||||
|
||||
-- check emergency coolant configuration if enabled
|
||||
if config.EmerCoolEnable then
|
||||
cfv.assert_eq(rsio.is_valid_side(config.EmerCoolSide), true)
|
||||
@@ -116,6 +139,21 @@ function plc.rps_init(reactor, is_formed)
|
||||
end
|
||||
end
|
||||
|
||||
-- check if the result of a peripheral call was OK, handle the failure if not
|
||||
---@nodiscard
|
||||
---@param result any PPM function call result
|
||||
---@return boolean succeeded if the result is OK, false if it was a PPM failure
|
||||
local function _check_and_handle_ppm_call(result)
|
||||
if result == ppm.ACCESS_FAULT then
|
||||
_set_fault()
|
||||
|
||||
-- if undefined, then the reactor isn't formed
|
||||
if reactor.__p_last_fault() == ppm.UNDEFINED_FIELD then self.formed = false end
|
||||
else return true end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- set emergency coolant control (if configured)
|
||||
---@param state boolean true to enable emergency coolant, false to disable
|
||||
local function _set_emer_cool(state)
|
||||
@@ -154,25 +192,20 @@ function plc.rps_init(reactor, is_formed)
|
||||
-- check if the reactor is formed
|
||||
local function _is_formed()
|
||||
local formed = reactor.isFormed()
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
_set_fault()
|
||||
else
|
||||
if _check_and_handle_ppm_call(formed) then
|
||||
self.formed = formed
|
||||
end
|
||||
|
||||
if not self.state[state_keys.sys_fail] then
|
||||
self.state[state_keys.sys_fail] = not formed
|
||||
end
|
||||
-- always update, since some ppm failures constitute not being formed
|
||||
if not self.state[state_keys.sys_fail] then
|
||||
self.state[state_keys.sys_fail] = not self.formed
|
||||
end
|
||||
end
|
||||
|
||||
-- check if the reactor is force disabled
|
||||
local function _is_force_disabled()
|
||||
local disabled = reactor.isForceDisabled()
|
||||
if disabled == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
_set_fault()
|
||||
else
|
||||
if _check_and_handle_ppm_call(disabled) then
|
||||
self.force_disabled = disabled
|
||||
|
||||
if not self.state[state_keys.force_disabled] then
|
||||
@@ -184,22 +217,16 @@ function plc.rps_init(reactor, is_formed)
|
||||
-- check for high damage
|
||||
local function _high_damage()
|
||||
local damage_percent = reactor.getDamagePercent()
|
||||
if damage_percent == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
_set_fault()
|
||||
elseif not self.state[state_keys.high_dmg] then
|
||||
if _check_and_handle_ppm_call(damage_percent) and not self.state[state_keys.high_dmg] then
|
||||
self.state[state_keys.high_dmg] = damage_percent >= RPS_LIMITS.MAX_DAMAGE_PERCENT
|
||||
end
|
||||
end
|
||||
|
||||
-- check if the reactor is at a critically high temperature
|
||||
local function _high_temp()
|
||||
-- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200
|
||||
-- mekanism: MAX_DAMAGE_TEMPERATURE = 1200K
|
||||
local temp = reactor.getTemperature()
|
||||
if temp == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
_set_fault()
|
||||
elseif not self.state[state_keys.high_temp] then
|
||||
if _check_and_handle_ppm_call(temp) and not self.state[state_keys.high_temp] then
|
||||
self.state[state_keys.high_temp] = temp >= RPS_LIMITS.MAX_DAMAGE_TEMPERATURE
|
||||
end
|
||||
end
|
||||
@@ -207,10 +234,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
-- check if there is very low coolant
|
||||
local function _low_coolant()
|
||||
local coolant_filled = reactor.getCoolantFilledPercentage()
|
||||
if coolant_filled == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
_set_fault()
|
||||
elseif not self.state[state_keys.low_coolant] then
|
||||
if _check_and_handle_ppm_call(coolant_filled) and not self.state[state_keys.low_coolant] then
|
||||
self.state[state_keys.low_coolant] = coolant_filled < RPS_LIMITS.MIN_COOLANT_FILL
|
||||
end
|
||||
end
|
||||
@@ -218,10 +242,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
-- check for excess waste (>80% filled)
|
||||
local function _excess_waste()
|
||||
local w_filled = reactor.getWasteFilledPercentage()
|
||||
if w_filled == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
_set_fault()
|
||||
elseif not self.state[state_keys.ex_waste] then
|
||||
if _check_and_handle_ppm_call(w_filled) and not self.state[state_keys.ex_waste] then
|
||||
self.state[state_keys.ex_waste] = w_filled > RPS_LIMITS.MAX_WASTE_FILL
|
||||
end
|
||||
end
|
||||
@@ -229,10 +250,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
-- check for heated coolant backup (>95% filled)
|
||||
local function _excess_heated_coolant()
|
||||
local hc_filled = reactor.getHeatedCoolantFilledPercentage()
|
||||
if hc_filled == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
_set_fault()
|
||||
elseif not self.state[state_keys.ex_hcoolant] then
|
||||
if _check_and_handle_ppm_call(hc_filled) and not self.state[state_keys.ex_hcoolant] then
|
||||
self.state[state_keys.ex_hcoolant] = hc_filled > RPS_LIMITS.MAX_HEATED_COLLANT_FILL
|
||||
end
|
||||
end
|
||||
@@ -240,10 +258,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
-- check if there is no fuel
|
||||
local function _insufficient_fuel()
|
||||
local fuel = reactor.getFuelFilledPercentage()
|
||||
if fuel == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
_set_fault()
|
||||
elseif not self.state[state_keys.no_fuel] then
|
||||
if _check_and_handle_ppm_call(fuel) and not self.state[state_keys.no_fuel] then
|
||||
self.state[state_keys.no_fuel] = fuel <= RPS_LIMITS.NO_FUEL_FILL
|
||||
end
|
||||
end
|
||||
@@ -292,7 +307,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
log.info("RPS: reactor SCRAM")
|
||||
|
||||
reactor.scram()
|
||||
if reactor.__p_is_faulted() and (reactor.__p_last_fault() ~= PCALL_SCRAM_MSG) then
|
||||
if reactor.__p_is_faulted() and not string.find(reactor.__p_last_fault(), PCALL_SCRAM_MSG) then
|
||||
log.error("RPS: failed reactor SCRAM")
|
||||
return false
|
||||
else
|
||||
@@ -310,7 +325,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
log.info("RPS: reactor start")
|
||||
|
||||
reactor.activate()
|
||||
if reactor.__p_is_faulted() and (reactor.__p_last_fault() ~= PCALL_START_MSG) then
|
||||
if reactor.__p_is_faulted() and not string.find(reactor.__p_last_fault(), PCALL_START_MSG) then
|
||||
log.error("RPS: failed reactor start")
|
||||
else
|
||||
self.reactor_enabled = true
|
||||
@@ -464,13 +479,22 @@ function plc.rps_init(reactor, is_formed)
|
||||
self.tripped = false
|
||||
self.trip_cause = RPS_TRIP_CAUSE.OK
|
||||
|
||||
for i = 1, #self.state do
|
||||
self.state[i] = false
|
||||
end
|
||||
for i = 1, #self.state do self.state[i] = false end
|
||||
|
||||
if not quiet then log.info("RPS: reset") end
|
||||
end
|
||||
|
||||
-- partial RPS reset that only clears fault and sys_fail
|
||||
function public.reset_formed()
|
||||
self.tripped = false
|
||||
self.trip_cause = RPS_TRIP_CAUSE.OK
|
||||
|
||||
self.state[state_keys.fault] = false
|
||||
self.state[state_keys.sys_fail] = false
|
||||
|
||||
log.info("RPS: partial reset on formed")
|
||||
end
|
||||
|
||||
-- reset the automatic and timeout trip flags, then clear trip if that was the trip cause
|
||||
function public.auto_reset()
|
||||
self.state[state_keys.automatic] = false
|
||||
|
||||
@@ -18,11 +18,16 @@ local ui = {
|
||||
}
|
||||
|
||||
-- try to start the UI
|
||||
---@param theme FP_THEME front panel theme
|
||||
---@param color_mode COLOR_MODE color mode
|
||||
---@return boolean success, any error_msg
|
||||
function renderer.try_start_ui()
|
||||
function renderer.try_start_ui(theme, color_mode)
|
||||
local status, msg = true, nil
|
||||
|
||||
if ui.display == nil then
|
||||
-- set theme
|
||||
style.set_theme(theme, color_mode)
|
||||
|
||||
-- reset terminal
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
@@ -30,13 +35,19 @@ function renderer.try_start_ui()
|
||||
term.setCursorPos(1, 1)
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.colors do
|
||||
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||
for i = 1, #style.theme.colors do
|
||||
term.setPaletteColor(style.theme.colors[i].c, style.theme.colors[i].hex)
|
||||
end
|
||||
|
||||
-- apply color mode
|
||||
local c_mode_overrides = style.theme.color_modes[color_mode]
|
||||
for i = 1, #c_mode_overrides do
|
||||
term.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
|
||||
end
|
||||
|
||||
-- init front panel view
|
||||
status, msg = pcall(function ()
|
||||
ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||
ui.display = DisplayBox{window=term.current(),fg_bg=style.fp.root}
|
||||
panel_view(ui.display)
|
||||
end)
|
||||
|
||||
@@ -64,9 +75,9 @@ function renderer.close_ui()
|
||||
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)
|
||||
for i = 1, #style.theme.colors do
|
||||
local r, g, b = term.nativePaletteColor(style.theme.colors[i].c)
|
||||
term.setPaletteColor(style.theme.colors[i].c, r, g, b)
|
||||
end
|
||||
|
||||
-- reset terminal
|
||||
|
||||
@@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
|
||||
local renderer = require("reactor-plc.renderer")
|
||||
local threads = require("reactor-plc.threads")
|
||||
|
||||
local R_PLC_VERSION = "v1.6.0"
|
||||
local R_PLC_VERSION = "v1.7.11"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
@@ -31,9 +31,13 @@ if not plc.load_config() then
|
||||
-- try to reconfigure (user action)
|
||||
local success, error = configure.configure(true)
|
||||
if success then
|
||||
assert(plc.load_config(), "failed to load valid reactor PLC configuration")
|
||||
if not plc.load_config() then
|
||||
println("failed to load a valid configuration, please reconfigure")
|
||||
return
|
||||
end
|
||||
else
|
||||
assert(success, "reactor PLC configuration error: " .. error)
|
||||
println("configuration error: " .. error)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
@@ -50,7 +54,8 @@ log.info("BOOTING reactor-plc.startup " .. R_PLC_VERSION)
|
||||
log.info("========================================")
|
||||
println(">> Reactor PLC " .. R_PLC_VERSION .. " <<")
|
||||
|
||||
crash.set_env("plc", R_PLC_VERSION)
|
||||
crash.set_env("reactor-plc", R_PLC_VERSION)
|
||||
crash.dbg_log_env()
|
||||
|
||||
----------------------------------------
|
||||
-- main application
|
||||
@@ -69,7 +74,7 @@ local function main()
|
||||
ppm.mount_all()
|
||||
|
||||
-- message authentication init
|
||||
if string.len(config.AuthKey) > 0 then
|
||||
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
|
||||
network.init_mac(config.AuthKey)
|
||||
end
|
||||
|
||||
@@ -131,13 +136,13 @@ local function main()
|
||||
|
||||
-- we need a reactor, can at least do some things even if it isn't formed though
|
||||
if plc_state.no_reactor then
|
||||
println("init> fission reactor not found");
|
||||
println("init> fission reactor not found")
|
||||
log.warning("init> no reactor on startup")
|
||||
|
||||
plc_state.init_ok = false
|
||||
plc_state.degraded = true
|
||||
elseif not smem_dev.reactor.isFormed() then
|
||||
println("init> fission reactor not formed");
|
||||
println("init> fission reactor is not formed")
|
||||
log.warning("init> reactor logic adapter present, but reactor is not formed")
|
||||
|
||||
plc_state.degraded = true
|
||||
@@ -172,8 +177,9 @@ local function main()
|
||||
-- front panel time!
|
||||
if not renderer.ui_ready() then
|
||||
local message
|
||||
plc_state.fp_ok, message = renderer.try_start_ui()
|
||||
plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode)
|
||||
|
||||
-- ...or not
|
||||
if not plc_state.fp_ok then
|
||||
println_ts(util.c("UI error: ", message))
|
||||
println("init> running without front panel")
|
||||
|
||||
@@ -71,77 +71,49 @@ function threads.thread__main(smem, init)
|
||||
-- blink heartbeat indicator
|
||||
databus.heartbeat()
|
||||
|
||||
-- core clock tick
|
||||
if networked then
|
||||
-- start next clock timer
|
||||
loop_clock.start()
|
||||
-- start next clock timer
|
||||
loop_clock.start()
|
||||
|
||||
-- send updated data
|
||||
if nic.is_connected() then
|
||||
if plc_comms.is_linked() then
|
||||
smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS)
|
||||
-- send updated data
|
||||
if networked and nic.is_connected() then
|
||||
if plc_comms.is_linked() then
|
||||
smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS)
|
||||
else
|
||||
if ticks_to_update == 0 then
|
||||
plc_comms.send_link_req()
|
||||
ticks_to_update = LINK_TICKS
|
||||
else
|
||||
if ticks_to_update == 0 then
|
||||
plc_comms.send_link_req()
|
||||
ticks_to_update = LINK_TICKS
|
||||
else
|
||||
ticks_to_update = ticks_to_update - 1
|
||||
end
|
||||
ticks_to_update = ticks_to_update - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- are we now formed after waiting to be formed?
|
||||
-- check for formed state change
|
||||
if (not plc_state.reactor_formed) and rps.is_formed() then
|
||||
-- push a connect event and unmount it from the PPM
|
||||
local iface = ppm.get_iface(plc_dev.reactor)
|
||||
if iface then
|
||||
log.info("unmounting and remounting unformed reactor")
|
||||
ppm.unmount(plc_dev.reactor)
|
||||
-- reactor now formed
|
||||
plc_state.reactor_formed = true
|
||||
|
||||
local type, device = ppm.mount(iface)
|
||||
println_ts("reactor is now formed.")
|
||||
log.info("reactor is now formed")
|
||||
|
||||
if type == "fissionReactorLogicAdapter" and device ~= nil then
|
||||
-- reconnect reactor
|
||||
plc_dev.reactor = device
|
||||
-- SCRAM newly formed reactor
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
|
||||
|
||||
-- we need to assume formed here as we cannot check in this main loop
|
||||
-- RPS will identify if it isn't and this will get set false later
|
||||
plc_state.reactor_formed = true
|
||||
|
||||
println_ts("reactor reconnected.")
|
||||
log.info("reactor reconnected")
|
||||
|
||||
-- SCRAM newly connected reactor
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
|
||||
|
||||
-- determine if we are still in a degraded state
|
||||
if (not networked) or nic.is_connected() then
|
||||
plc_state.degraded = false
|
||||
end
|
||||
|
||||
rps.reconnect_reactor(plc_dev.reactor)
|
||||
if networked then
|
||||
plc_comms.reconnect_reactor(plc_dev.reactor)
|
||||
end
|
||||
|
||||
-- reset RPS for newly connected reactor
|
||||
-- without this, is_formed will be out of date and cause it to think its no longer formed again
|
||||
rps.reset()
|
||||
else
|
||||
-- fully lost the reactor now :(
|
||||
println_ts("reactor lost (failed reconnect)!")
|
||||
log.error("reactor lost (failed reconnect)")
|
||||
|
||||
plc_state.no_reactor = true
|
||||
plc_state.degraded = true
|
||||
end
|
||||
else
|
||||
log.error("failed to get interface of previously connected reactor", true)
|
||||
-- determine if we are still in a degraded state
|
||||
if (not networked) or nic.is_connected() then
|
||||
plc_state.degraded = false
|
||||
end
|
||||
elseif not rps.is_formed() then
|
||||
|
||||
-- partial reset of RPS, specific to becoming formed
|
||||
-- without this, auto control can't resume on chunk load
|
||||
rps.reset_formed()
|
||||
elseif plc_state.reactor_formed and not rps.is_formed() then
|
||||
-- reactor no longer formed
|
||||
println_ts("reactor is no longer formed.")
|
||||
log.info("reactor is no longer formed")
|
||||
|
||||
plc_state.reactor_formed = false
|
||||
plc_state.degraded = true
|
||||
end
|
||||
|
||||
-- update indicators
|
||||
@@ -173,7 +145,8 @@ function threads.thread__main(smem, init)
|
||||
plc_state.degraded = true
|
||||
elseif networked and type == "modem" then
|
||||
-- we only care if this is our wireless modem
|
||||
if nic.is_modem(device) then
|
||||
-- note, check init_ok first since nic will be nil if it is false
|
||||
if plc_state.init_ok and nic.is_modem(device) then
|
||||
nic.disconnect()
|
||||
|
||||
println_ts("comms modem disconnected!")
|
||||
@@ -193,7 +166,7 @@ function threads.thread__main(smem, init)
|
||||
end
|
||||
end
|
||||
else
|
||||
log.warning("non-comms modem disconnected")
|
||||
log.warning("a modem was disconnected")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -230,12 +203,13 @@ function threads.thread__main(smem, init)
|
||||
plc_comms.reconnect_reactor(plc_dev.reactor)
|
||||
end
|
||||
|
||||
-- reset RPS for newly connected reactor
|
||||
-- without this, is_formed will be out of date and cause it to think its no longer formed again
|
||||
rps.reset()
|
||||
-- partial reset of RPS, specific to becoming formed/reconnected
|
||||
-- without this, auto control can't resume on chunk load
|
||||
rps.reset_formed()
|
||||
end
|
||||
elseif networked and type == "modem" then
|
||||
if device.isWireless() and not nic.is_connected() then
|
||||
-- note, check init_ok first since nic will be nil if it is false
|
||||
if device.isWireless() and not (plc_state.init_ok and nic.is_connected()) then
|
||||
-- reconnected modem
|
||||
plc_dev.modem = device
|
||||
plc_state.no_modem = false
|
||||
@@ -368,9 +342,9 @@ function threads.thread__rps(smem)
|
||||
end
|
||||
end
|
||||
|
||||
-- if we are in standalone mode, continuously reset RPS
|
||||
-- if we are in standalone mode and the front panel isn't working, continuously reset RPS
|
||||
-- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable
|
||||
if not networked then rps.reset(true) end
|
||||
if not (networked or smem.plc_state.fp_ok) then rps.reset(true) end
|
||||
|
||||
-- check safety (SCRAM occurs if tripped)
|
||||
if not plc_state.no_reactor then
|
||||
@@ -662,8 +636,9 @@ function threads.thread__setpoint_control(smem)
|
||||
if (type(cur_burn_rate) == "number") and (setpoints.burn_rate ~= cur_burn_rate) and rps.is_active() then
|
||||
last_burn_sp = setpoints.burn_rate
|
||||
|
||||
-- update without ramp if <= 2.5 mB/t change
|
||||
running = math.abs(setpoints.burn_rate - cur_burn_rate) > 2.5
|
||||
-- update without ramp if <= 2.5 mB/t increase
|
||||
-- no need to ramp down, as the ramp up poses the safety risks
|
||||
running = (setpoints.burn_rate - cur_burn_rate) > 2.5
|
||||
|
||||
if running then
|
||||
log.debug(util.c("SPCTL: starting burn rate ramp from ", cur_burn_rate, " mB/t to ", setpoints.burn_rate, " mB/t"))
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
local rsio = require("scada-common.rsio")
|
||||
|
||||
local config = {}
|
||||
|
||||
-- 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
|
||||
-- facility authentication key (do NOT use one of your passwords)
|
||||
-- this enables verifying that messages are authentic
|
||||
-- all devices on the same network must use the same key
|
||||
-- config.AUTH_KEY = "SCADAfacility123"
|
||||
|
||||
-- alarm sounder volume (0.0 to 3.0, 1.0 being standard max volume, this is the option given to to speaker.play())
|
||||
-- note: alarm sine waves are at half saturation, so that multiple will be required to reach full scale
|
||||
config.SOUNDER_VOLUME = 1.0
|
||||
|
||||
-- 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
|
||||
|
||||
-- RTU peripheral devices (named: side/network device name)
|
||||
config.RTU_DEVICES = {
|
||||
{
|
||||
name = "boilerValve_0",
|
||||
index = 1,
|
||||
for_reactor = 1
|
||||
},
|
||||
{
|
||||
name = "turbineValve_0",
|
||||
index = 1,
|
||||
for_reactor = 1
|
||||
}
|
||||
}
|
||||
-- RTU redstone interface definitions
|
||||
config.RTU_REDSTONE = {
|
||||
-- {
|
||||
-- for_reactor = 1,
|
||||
-- io = {
|
||||
-- {
|
||||
-- port = rsio.IO.WASTE_PO,
|
||||
-- side = "top",
|
||||
-- bundled_color = colors.red
|
||||
-- },
|
||||
-- {
|
||||
-- port = rsio.IO.WASTE_PU,
|
||||
-- side = "top",
|
||||
-- bundled_color = colors.orange
|
||||
-- },
|
||||
-- {
|
||||
-- port = rsio.IO.WASTE_POPL,
|
||||
-- side = "top",
|
||||
-- bundled_color = colors.yellow
|
||||
-- },
|
||||
-- {
|
||||
-- port = rsio.IO.WASTE_AM,
|
||||
-- side = "top",
|
||||
-- bundled_color = colors.lime
|
||||
-- }
|
||||
-- }
|
||||
-- }
|
||||
}
|
||||
|
||||
return config
|
||||
1699
rtu/configure.lua
Normal file
1699
rtu/configure.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,60 +7,51 @@ local boilerv_rtu = {}
|
||||
---@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_clear_fault()
|
||||
boiler.__p_disable_afc()
|
||||
local unit = rtu.init_unit(boiler)
|
||||
|
||||
-- discrete inputs --
|
||||
unit.connect_di(boiler.isFormed)
|
||||
unit.connect_di("isFormed")
|
||||
|
||||
-- coils --
|
||||
-- none
|
||||
|
||||
-- input registers --
|
||||
-- multiblock properties
|
||||
unit.connect_input_reg(boiler.getLength)
|
||||
unit.connect_input_reg(boiler.getWidth)
|
||||
unit.connect_input_reg(boiler.getHeight)
|
||||
unit.connect_input_reg(boiler.getMinPos)
|
||||
unit.connect_input_reg(boiler.getMaxPos)
|
||||
unit.connect_input_reg("getLength")
|
||||
unit.connect_input_reg("getWidth")
|
||||
unit.connect_input_reg("getHeight")
|
||||
unit.connect_input_reg("getMinPos")
|
||||
unit.connect_input_reg("getMaxPos")
|
||||
-- build properties
|
||||
unit.connect_input_reg(boiler.getBoilCapacity)
|
||||
unit.connect_input_reg(boiler.getSteamCapacity)
|
||||
unit.connect_input_reg(boiler.getWaterCapacity)
|
||||
unit.connect_input_reg(boiler.getHeatedCoolantCapacity)
|
||||
unit.connect_input_reg(boiler.getCooledCoolantCapacity)
|
||||
unit.connect_input_reg(boiler.getSuperheaters)
|
||||
unit.connect_input_reg(boiler.getMaxBoilRate)
|
||||
unit.connect_input_reg("getBoilCapacity")
|
||||
unit.connect_input_reg("getSteamCapacity")
|
||||
unit.connect_input_reg("getWaterCapacity")
|
||||
unit.connect_input_reg("getHeatedCoolantCapacity")
|
||||
unit.connect_input_reg("getCooledCoolantCapacity")
|
||||
unit.connect_input_reg("getSuperheaters")
|
||||
unit.connect_input_reg("getMaxBoilRate")
|
||||
-- current state
|
||||
unit.connect_input_reg(boiler.getTemperature)
|
||||
unit.connect_input_reg(boiler.getBoilRate)
|
||||
unit.connect_input_reg(boiler.getEnvironmentalLoss)
|
||||
unit.connect_input_reg("getTemperature")
|
||||
unit.connect_input_reg("getBoilRate")
|
||||
unit.connect_input_reg("getEnvironmentalLoss")
|
||||
-- tanks
|
||||
unit.connect_input_reg(boiler.getSteam)
|
||||
unit.connect_input_reg(boiler.getSteamNeeded)
|
||||
unit.connect_input_reg(boiler.getSteamFilledPercentage)
|
||||
unit.connect_input_reg(boiler.getWater)
|
||||
unit.connect_input_reg(boiler.getWaterNeeded)
|
||||
unit.connect_input_reg(boiler.getWaterFilledPercentage)
|
||||
unit.connect_input_reg(boiler.getHeatedCoolant)
|
||||
unit.connect_input_reg(boiler.getHeatedCoolantNeeded)
|
||||
unit.connect_input_reg(boiler.getHeatedCoolantFilledPercentage)
|
||||
unit.connect_input_reg(boiler.getCooledCoolant)
|
||||
unit.connect_input_reg(boiler.getCooledCoolantNeeded)
|
||||
unit.connect_input_reg(boiler.getCooledCoolantFilledPercentage)
|
||||
unit.connect_input_reg("getSteam")
|
||||
unit.connect_input_reg("getSteamNeeded")
|
||||
unit.connect_input_reg("getSteamFilledPercentage")
|
||||
unit.connect_input_reg("getWater")
|
||||
unit.connect_input_reg("getWaterNeeded")
|
||||
unit.connect_input_reg("getWaterFilledPercentage")
|
||||
unit.connect_input_reg("getHeatedCoolant")
|
||||
unit.connect_input_reg("getHeatedCoolantNeeded")
|
||||
unit.connect_input_reg("getHeatedCoolantFilledPercentage")
|
||||
unit.connect_input_reg("getCooledCoolant")
|
||||
unit.connect_input_reg("getCooledCoolantNeeded")
|
||||
unit.connect_input_reg("getCooledCoolantFilledPercentage")
|
||||
|
||||
-- holding registers --
|
||||
-- none
|
||||
|
||||
-- check if any calls faulted
|
||||
local faulted = boiler.__p_is_faulted()
|
||||
boiler.__p_clear_fault()
|
||||
boiler.__p_enable_afc()
|
||||
|
||||
return unit.interface(), faulted
|
||||
return unit.interface(), false
|
||||
end
|
||||
|
||||
return boilerv_rtu
|
||||
|
||||
@@ -7,14 +7,10 @@ local dynamicv_rtu = {}
|
||||
---@param dynamic_tank table
|
||||
---@return rtu_device interface, boolean faulted
|
||||
function dynamicv_rtu.new(dynamic_tank)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- disable auto fault clearing
|
||||
dynamic_tank.__p_clear_fault()
|
||||
dynamic_tank.__p_disable_afc()
|
||||
local unit = rtu.init_unit(dynamic_tank)
|
||||
|
||||
-- discrete inputs --
|
||||
unit.connect_di(dynamic_tank.isFormed)
|
||||
unit.connect_di("isFormed")
|
||||
|
||||
-- coils --
|
||||
unit.connect_coil(function () dynamic_tank.incrementContainerEditMode() end, function () end)
|
||||
@@ -22,27 +18,22 @@ function dynamicv_rtu.new(dynamic_tank)
|
||||
|
||||
-- input registers --
|
||||
-- multiblock properties
|
||||
unit.connect_input_reg(dynamic_tank.getLength)
|
||||
unit.connect_input_reg(dynamic_tank.getWidth)
|
||||
unit.connect_input_reg(dynamic_tank.getHeight)
|
||||
unit.connect_input_reg(dynamic_tank.getMinPos)
|
||||
unit.connect_input_reg(dynamic_tank.getMaxPos)
|
||||
unit.connect_input_reg("getLength")
|
||||
unit.connect_input_reg("getWidth")
|
||||
unit.connect_input_reg("getHeight")
|
||||
unit.connect_input_reg("getMinPos")
|
||||
unit.connect_input_reg("getMaxPos")
|
||||
-- build properties
|
||||
unit.connect_input_reg(dynamic_tank.getTankCapacity)
|
||||
unit.connect_input_reg(dynamic_tank.getChemicalTankCapacity)
|
||||
unit.connect_input_reg("getTankCapacity")
|
||||
unit.connect_input_reg("getChemicalTankCapacity")
|
||||
-- tanks/containers
|
||||
unit.connect_input_reg(dynamic_tank.getStored)
|
||||
unit.connect_input_reg(dynamic_tank.getFilledPercentage)
|
||||
unit.connect_input_reg("getStored")
|
||||
unit.connect_input_reg("getFilledPercentage")
|
||||
|
||||
-- holding registers --
|
||||
unit.connect_holding_reg(dynamic_tank.getContainerEditMode, dynamic_tank.setContainerEditMode)
|
||||
unit.connect_holding_reg("getContainerEditMode", "setContainerEditMode")
|
||||
|
||||
-- check if any calls faulted
|
||||
local faulted = dynamic_tank.__p_is_faulted()
|
||||
dynamic_tank.__p_clear_fault()
|
||||
dynamic_tank.__p_enable_afc()
|
||||
|
||||
return unit.interface(), faulted
|
||||
return unit.interface(), false
|
||||
end
|
||||
|
||||
return dynamicv_rtu
|
||||
|
||||
@@ -7,7 +7,7 @@ local envd_rtu = {}
|
||||
---@param envd table
|
||||
---@return rtu_device interface, boolean faulted
|
||||
function envd_rtu.new(envd)
|
||||
local unit = rtu.init_unit()
|
||||
local unit = rtu.init_unit(envd)
|
||||
|
||||
-- disable auto fault clearing
|
||||
envd.__p_clear_fault()
|
||||
|
||||
@@ -7,47 +7,38 @@ local imatrix_rtu = {}
|
||||
---@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_clear_fault()
|
||||
imatrix.__p_disable_afc()
|
||||
local unit = rtu.init_unit(imatrix)
|
||||
|
||||
-- discrete inputs --
|
||||
unit.connect_di(imatrix.isFormed)
|
||||
unit.connect_di("isFormed")
|
||||
|
||||
-- coils --
|
||||
-- none
|
||||
|
||||
-- input registers --
|
||||
-- multiblock properties
|
||||
unit.connect_input_reg(imatrix.getLength)
|
||||
unit.connect_input_reg(imatrix.getWidth)
|
||||
unit.connect_input_reg(imatrix.getHeight)
|
||||
unit.connect_input_reg(imatrix.getMinPos)
|
||||
unit.connect_input_reg(imatrix.getMaxPos)
|
||||
unit.connect_input_reg("getLength")
|
||||
unit.connect_input_reg("getWidth")
|
||||
unit.connect_input_reg("getHeight")
|
||||
unit.connect_input_reg("getMinPos")
|
||||
unit.connect_input_reg("getMaxPos")
|
||||
-- build properties
|
||||
unit.connect_input_reg(imatrix.getMaxEnergy)
|
||||
unit.connect_input_reg(imatrix.getTransferCap)
|
||||
unit.connect_input_reg(imatrix.getInstalledCells)
|
||||
unit.connect_input_reg(imatrix.getInstalledProviders)
|
||||
unit.connect_input_reg("getMaxEnergy")
|
||||
unit.connect_input_reg("getTransferCap")
|
||||
unit.connect_input_reg("getInstalledCells")
|
||||
unit.connect_input_reg("getInstalledProviders")
|
||||
-- I/O rates
|
||||
unit.connect_input_reg(imatrix.getLastInput)
|
||||
unit.connect_input_reg(imatrix.getLastOutput)
|
||||
unit.connect_input_reg("getLastInput")
|
||||
unit.connect_input_reg("getLastOutput")
|
||||
-- tanks
|
||||
unit.connect_input_reg(imatrix.getEnergy)
|
||||
unit.connect_input_reg(imatrix.getEnergyNeeded)
|
||||
unit.connect_input_reg(imatrix.getEnergyFilledPercentage)
|
||||
unit.connect_input_reg("getEnergy")
|
||||
unit.connect_input_reg("getEnergyNeeded")
|
||||
unit.connect_input_reg("getEnergyFilledPercentage")
|
||||
|
||||
-- holding registers --
|
||||
-- none
|
||||
|
||||
-- check if any calls faulted
|
||||
local faulted = imatrix.__p_is_faulted()
|
||||
imatrix.__p_clear_fault()
|
||||
imatrix.__p_enable_afc()
|
||||
|
||||
return unit.interface(), faulted
|
||||
return unit.interface(), false
|
||||
end
|
||||
|
||||
return imatrix_rtu
|
||||
|
||||
@@ -7,7 +7,7 @@ local sna_rtu = {}
|
||||
---@param sna table
|
||||
---@return rtu_device interface, boolean faulted
|
||||
function sna_rtu.new(sna)
|
||||
local unit = rtu.init_unit()
|
||||
local unit = rtu.init_unit(sna)
|
||||
|
||||
-- disable auto fault clearing
|
||||
sna.__p_clear_fault()
|
||||
|
||||
@@ -7,52 +7,43 @@ local sps_rtu = {}
|
||||
---@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_clear_fault()
|
||||
sps.__p_disable_afc()
|
||||
local unit = rtu.init_unit(sps)
|
||||
|
||||
-- discrete inputs --
|
||||
unit.connect_di(sps.isFormed)
|
||||
unit.connect_di("isFormed")
|
||||
|
||||
-- coils --
|
||||
-- none
|
||||
|
||||
-- input registers --
|
||||
-- multiblock properties
|
||||
unit.connect_input_reg(sps.getLength)
|
||||
unit.connect_input_reg(sps.getWidth)
|
||||
unit.connect_input_reg(sps.getHeight)
|
||||
unit.connect_input_reg(sps.getMinPos)
|
||||
unit.connect_input_reg(sps.getMaxPos)
|
||||
unit.connect_input_reg("getLength")
|
||||
unit.connect_input_reg("getWidth")
|
||||
unit.connect_input_reg("getHeight")
|
||||
unit.connect_input_reg("getMinPos")
|
||||
unit.connect_input_reg("getMaxPos")
|
||||
-- build properties
|
||||
unit.connect_input_reg(sps.getCoils)
|
||||
unit.connect_input_reg(sps.getInputCapacity)
|
||||
unit.connect_input_reg(sps.getOutputCapacity)
|
||||
unit.connect_input_reg(sps.getMaxEnergy)
|
||||
unit.connect_input_reg("getCoils")
|
||||
unit.connect_input_reg("getInputCapacity")
|
||||
unit.connect_input_reg("getOutputCapacity")
|
||||
unit.connect_input_reg("getMaxEnergy")
|
||||
-- current state
|
||||
unit.connect_input_reg(sps.getProcessRate)
|
||||
unit.connect_input_reg("getProcessRate")
|
||||
-- tanks
|
||||
unit.connect_input_reg(sps.getInput)
|
||||
unit.connect_input_reg(sps.getInputNeeded)
|
||||
unit.connect_input_reg(sps.getInputFilledPercentage)
|
||||
unit.connect_input_reg(sps.getOutput)
|
||||
unit.connect_input_reg(sps.getOutputNeeded)
|
||||
unit.connect_input_reg(sps.getOutputFilledPercentage)
|
||||
unit.connect_input_reg(sps.getEnergy)
|
||||
unit.connect_input_reg(sps.getEnergyNeeded)
|
||||
unit.connect_input_reg(sps.getEnergyFilledPercentage)
|
||||
unit.connect_input_reg("getInput")
|
||||
unit.connect_input_reg("getInputNeeded")
|
||||
unit.connect_input_reg("getInputFilledPercentage")
|
||||
unit.connect_input_reg("getOutput")
|
||||
unit.connect_input_reg("getOutputNeeded")
|
||||
unit.connect_input_reg("getOutputFilledPercentage")
|
||||
unit.connect_input_reg("getEnergy")
|
||||
unit.connect_input_reg("getEnergyNeeded")
|
||||
unit.connect_input_reg("getEnergyFilledPercentage")
|
||||
|
||||
-- holding registers --
|
||||
-- none
|
||||
|
||||
-- check if any calls faulted
|
||||
local faulted = sps.__p_is_faulted()
|
||||
sps.__p_clear_fault()
|
||||
sps.__p_enable_afc()
|
||||
|
||||
return unit.interface(), faulted
|
||||
return unit.interface(), false
|
||||
end
|
||||
|
||||
return sps_rtu
|
||||
|
||||
@@ -7,14 +7,10 @@ local turbinev_rtu = {}
|
||||
---@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_clear_fault()
|
||||
turbine.__p_disable_afc()
|
||||
local unit = rtu.init_unit(turbine)
|
||||
|
||||
-- discrete inputs --
|
||||
unit.connect_di(turbine.isFormed)
|
||||
unit.connect_di("isFormed")
|
||||
|
||||
-- coils --
|
||||
unit.connect_coil(function () turbine.incrementDumpingMode() end, function () end)
|
||||
@@ -22,44 +18,39 @@ function turbinev_rtu.new(turbine)
|
||||
|
||||
-- input registers --
|
||||
-- multiblock properties
|
||||
unit.connect_input_reg(turbine.getLength)
|
||||
unit.connect_input_reg(turbine.getWidth)
|
||||
unit.connect_input_reg(turbine.getHeight)
|
||||
unit.connect_input_reg(turbine.getMinPos)
|
||||
unit.connect_input_reg(turbine.getMaxPos)
|
||||
unit.connect_input_reg("getLength")
|
||||
unit.connect_input_reg("getWidth")
|
||||
unit.connect_input_reg("getHeight")
|
||||
unit.connect_input_reg("getMinPos")
|
||||
unit.connect_input_reg("getMaxPos")
|
||||
-- build properties
|
||||
unit.connect_input_reg(turbine.getBlades)
|
||||
unit.connect_input_reg(turbine.getCoils)
|
||||
unit.connect_input_reg(turbine.getVents)
|
||||
unit.connect_input_reg(turbine.getDispersers)
|
||||
unit.connect_input_reg(turbine.getCondensers)
|
||||
unit.connect_input_reg(turbine.getSteamCapacity)
|
||||
unit.connect_input_reg(turbine.getMaxEnergy)
|
||||
unit.connect_input_reg(turbine.getMaxFlowRate)
|
||||
unit.connect_input_reg(turbine.getMaxProduction)
|
||||
unit.connect_input_reg(turbine.getMaxWaterOutput)
|
||||
unit.connect_input_reg("getBlades")
|
||||
unit.connect_input_reg("getCoils")
|
||||
unit.connect_input_reg("getVents")
|
||||
unit.connect_input_reg("getDispersers")
|
||||
unit.connect_input_reg("getCondensers")
|
||||
unit.connect_input_reg("getSteamCapacity")
|
||||
unit.connect_input_reg("getMaxEnergy")
|
||||
unit.connect_input_reg("getMaxFlowRate")
|
||||
unit.connect_input_reg("getMaxProduction")
|
||||
unit.connect_input_reg("getMaxWaterOutput")
|
||||
-- current state
|
||||
unit.connect_input_reg(turbine.getFlowRate)
|
||||
unit.connect_input_reg(turbine.getProductionRate)
|
||||
unit.connect_input_reg(turbine.getLastSteamInputRate)
|
||||
unit.connect_input_reg(turbine.getDumpingMode)
|
||||
unit.connect_input_reg("getFlowRate")
|
||||
unit.connect_input_reg("getProductionRate")
|
||||
unit.connect_input_reg("getLastSteamInputRate")
|
||||
unit.connect_input_reg("getDumpingMode")
|
||||
-- tanks/containers
|
||||
unit.connect_input_reg(turbine.getSteam)
|
||||
unit.connect_input_reg(turbine.getSteamNeeded)
|
||||
unit.connect_input_reg(turbine.getSteamFilledPercentage)
|
||||
unit.connect_input_reg(turbine.getEnergy)
|
||||
unit.connect_input_reg(turbine.getEnergyNeeded)
|
||||
unit.connect_input_reg(turbine.getEnergyFilledPercentage)
|
||||
unit.connect_input_reg("getSteam")
|
||||
unit.connect_input_reg("getSteamNeeded")
|
||||
unit.connect_input_reg("getSteamFilledPercentage")
|
||||
unit.connect_input_reg("getEnergy")
|
||||
unit.connect_input_reg("getEnergyNeeded")
|
||||
unit.connect_input_reg("getEnergyFilledPercentage")
|
||||
|
||||
-- holding registers --
|
||||
unit.connect_holding_reg(turbine.getDumpingMode, turbine.setDumpingMode)
|
||||
unit.connect_holding_reg("getDumpingMode", "setDumpingMode")
|
||||
|
||||
-- check if any calls faulted
|
||||
local faulted = turbine.__p_is_faulted()
|
||||
turbine.__p_clear_fault()
|
||||
turbine.__p_enable_afc()
|
||||
|
||||
return unit.interface(), faulted
|
||||
return unit.interface(), false
|
||||
end
|
||||
|
||||
return turbinev_rtu
|
||||
|
||||
@@ -37,12 +37,7 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
end)
|
||||
else
|
||||
readings[i], access_fault = rtu_dev.read_coil(addr)
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
|
||||
break
|
||||
end
|
||||
if access_fault then break end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -86,12 +81,7 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
end)
|
||||
else
|
||||
readings[i], access_fault = rtu_dev.read_di(addr)
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
|
||||
break
|
||||
end
|
||||
if access_fault then break end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -135,12 +125,7 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
end)
|
||||
else
|
||||
readings[i], access_fault = rtu_dev.read_holding_reg(addr)
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
|
||||
break
|
||||
end
|
||||
if access_fault then break end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -184,12 +169,7 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
end)
|
||||
else
|
||||
readings[i], access_fault = rtu_dev.read_input_reg(addr)
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
|
||||
break
|
||||
end
|
||||
if access_fault then break end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -16,14 +16,15 @@ local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local LED = require("graphics.elements.indicators.led")
|
||||
local LEDPair = require("graphics.elements.indicators.ledpair")
|
||||
local RGBLED = require("graphics.elements.indicators.ledrgb")
|
||||
|
||||
local LINK_STATE = types.PANEL_LINK_STATE
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local fp_label = style.fp_label
|
||||
|
||||
local ind_grn = style.ind_grn
|
||||
|
||||
local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC TANK", "IND MATRIX", "SPS", "SNA", "ENV DETECTOR" }
|
||||
@@ -32,7 +33,9 @@ local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC
|
||||
---@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=ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
local disabled_fg = style.fp.disabled_fg
|
||||
|
||||
TextBox{parent=panel,y=1,text="RTU GATEWAY",alignment=ALIGN.CENTER,height=1,fg_bg=style.theme.header}
|
||||
|
||||
--
|
||||
-- system indicators
|
||||
@@ -48,12 +51,43 @@ local function init(panel, units)
|
||||
heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
|
||||
|
||||
local modem = LED{parent=system,label="MODEM",colors=ind_grn}
|
||||
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)
|
||||
|
||||
if not style.colorblind then
|
||||
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,style.ind_bkg}}
|
||||
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
network.register(databus.ps, "link_state", network.update)
|
||||
else
|
||||
local nt_lnk = LEDPair{parent=system,label="NT LINKED",off=style.ind_bkg,c1=colors.red,c2=colors.green}
|
||||
local nt_ver = LEDPair{parent=system,label="NT VERSION",off=style.ind_bkg,c1=colors.red,c2=colors.green}
|
||||
|
||||
nt_lnk.register(databus.ps, "link_state", function (state)
|
||||
local value = 2
|
||||
|
||||
if state == LINK_STATE.DISCONNECTED then
|
||||
value = 1
|
||||
elseif state == LINK_STATE.LINKED then
|
||||
value = 3
|
||||
end
|
||||
|
||||
nt_lnk.update(value)
|
||||
end)
|
||||
|
||||
nt_ver.register(databus.ps, "link_state", function (state)
|
||||
local value = 3
|
||||
|
||||
if state == LINK_STATE.BAD_VERSION then
|
||||
value = 2
|
||||
elseif state == LINK_STATE.DISCONNECTED then
|
||||
value = 1
|
||||
end
|
||||
|
||||
nt_ver.update(value)
|
||||
end)
|
||||
end
|
||||
|
||||
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=ind_grn}
|
||||
local rt_comm = LED{parent=system,label="RT COMMS",colors=ind_grn}
|
||||
@@ -64,17 +98,17 @@ local function init(panel, units)
|
||||
|
||||
---@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=fp_label}
|
||||
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=disabled_fg}
|
||||
|
||||
TextBox{parent=system,x=1,y=14,text="SPEAKERS",height=1,width=8,fg_bg=style.label}
|
||||
local speaker_count = DataIndicator{parent=system,x=10,y=14,label="",format="%3d",value=0,width=3,fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=system,x=1,y=14,text="SPEAKERS",height=1,width=8,fg_bg=style.fp.text_fg}
|
||||
local speaker_count = DataIndicator{parent=system,x=10,y=14,label="",format="%3d",value=0,width=3,fg_bg=style.theme.field_box}
|
||||
speaker_count.register(databus.ps, "speaker_count", speaker_count.update)
|
||||
|
||||
--
|
||||
-- about label
|
||||
--
|
||||
|
||||
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=fp_label}
|
||||
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=disabled_fg}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1}
|
||||
|
||||
@@ -109,16 +143,14 @@ local function init(panel, units)
|
||||
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}
|
||||
local function get_name(t) return util.c(UNIT_TYPE_LABELS[t + 1], " ", util.trinary(util.is_int(unit.index), unit.index, "")) end
|
||||
local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(unit.type),width=15,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)
|
||||
name_box.register(databus.ps, "unit_type_" .. i, function (t) name_box.set_value(get_name(t)) 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=fp_label}
|
||||
TextBox{parent=unit_hw_statuses,y=i,x=19,text=for_unit,height=1,fg_bg=disabled_fg}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -2,47 +2,39 @@
|
||||
-- Graphics Style Options
|
||||
--
|
||||
|
||||
local core = require("graphics.core")
|
||||
local core = require("graphics.core")
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
---@class rtu_style
|
||||
local style = {}
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
-- GLOBAL --
|
||||
|
||||
-- remap global colors
|
||||
colors.ivory = colors.pink
|
||||
colors.yellow_hc = colors.purple
|
||||
colors.red_off = colors.brown
|
||||
colors.yellow_off = colors.magenta
|
||||
colors.green_off = colors.lime
|
||||
|
||||
style.root = cpair(colors.black, colors.ivory)
|
||||
style.header = cpair(colors.black, colors.lightGray)
|
||||
|
||||
style.colors = {
|
||||
{ c = colors.red, hex = 0xdf4949 }, -- RED ON
|
||||
{ c = colors.orange, hex = 0xffb659 },
|
||||
{ c = colors.yellow, hex = 0xf9fb53 }, -- YELLOW ON
|
||||
{ c = colors.lime, hex = 0x16665a }, -- GREEN OFF
|
||||
{ c = colors.green, hex = 0x6be551 }, -- GREEN ON
|
||||
{ c = colors.cyan, hex = 0x34bac8 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.purple, hex = 0xb156ee }, -- YELLOW HIGH CONTRAST
|
||||
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
|
||||
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
|
||||
-- { c = colors.white, hex = 0xdcd9ca },
|
||||
{ c = colors.lightGray, hex = 0xb1b8b3 },
|
||||
{ c = colors.gray, hex = 0x575757 },
|
||||
-- { c = colors.black, hex = 0x191919 },
|
||||
{ c = colors.brown, hex = 0x672223 } -- RED OFF
|
||||
}
|
||||
|
||||
-- COMMON COLOR PAIRS --
|
||||
|
||||
style.fp_label = cpair(colors.lightGray, colors.ivory)
|
||||
style.theme = themes.sandstone
|
||||
style.fp = themes.get_fp_style(style.theme)
|
||||
style.colorblind = false
|
||||
|
||||
style.ind_grn = cpair(colors.green, colors.green_off)
|
||||
|
||||
-- set theme per configuration
|
||||
---@param fp FP_THEME front panel theme
|
||||
---@param color_mode COLOR_MODE the color mode to use
|
||||
function style.set_theme(fp, color_mode)
|
||||
if fp == themes.FP_THEME.SANDSTONE then
|
||||
style.theme = themes.sandstone
|
||||
elseif fp == themes.FP_THEME.BASALT then
|
||||
style.theme = themes.basalt
|
||||
end
|
||||
|
||||
style.fp = themes.get_fp_style(style.theme)
|
||||
|
||||
style.colorblind = color_mode ~= themes.COLOR_MODE.STANDARD and color_mode ~= themes.COLOR_MODE.STD_ON_BLACK
|
||||
|
||||
if color_mode == themes.COLOR_MODE.STANDARD or color_mode == themes.COLOR_MODE.BLUE_IND then
|
||||
style.ind_bkg = colors.gray
|
||||
else
|
||||
style.ind_bkg = colors.black
|
||||
end
|
||||
end
|
||||
|
||||
return style
|
||||
|
||||
@@ -19,11 +19,16 @@ local ui = {
|
||||
|
||||
-- try to start the UI
|
||||
---@param units table RTU units
|
||||
---@param theme FP_THEME front panel theme
|
||||
---@param color_mode COLOR_MODE color mode
|
||||
---@return boolean success, any error_msg
|
||||
function renderer.try_start_ui(units)
|
||||
function renderer.try_start_ui(units, theme, color_mode)
|
||||
local status, msg = true, nil
|
||||
|
||||
if ui.display == nil then
|
||||
-- set theme
|
||||
style.set_theme(theme, color_mode)
|
||||
|
||||
-- reset terminal
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
@@ -31,13 +36,19 @@ function renderer.try_start_ui(units)
|
||||
term.setCursorPos(1, 1)
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.colors do
|
||||
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||
for i = 1, #style.theme.colors do
|
||||
term.setPaletteColor(style.theme.colors[i].c, style.theme.colors[i].hex)
|
||||
end
|
||||
|
||||
-- apply color mode
|
||||
local c_mode_overrides = style.theme.color_modes[color_mode]
|
||||
for i = 1, #c_mode_overrides do
|
||||
term.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
|
||||
end
|
||||
|
||||
-- init front panel view
|
||||
status, msg = pcall(function ()
|
||||
ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||
ui.display = DisplayBox{window=term.current(),fg_bg=style.fp.root}
|
||||
panel_view(ui.display, units)
|
||||
end)
|
||||
|
||||
@@ -65,9 +76,9 @@ function renderer.close_ui()
|
||||
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)
|
||||
for i = 1, #style.theme.colors do
|
||||
local r, g, b = term.nativePaletteColor(style.theme.colors[i].c)
|
||||
term.setPaletteColor(style.theme.colors[i].c, r, g, b)
|
||||
end
|
||||
|
||||
-- reset terminal
|
||||
|
||||
144
rtu/rtu.lua
144
rtu/rtu.lua
@@ -5,7 +5,8 @@ local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local config = require("rtu.config")
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
local databus = require("rtu.databus")
|
||||
local modbus = require("rtu.modbus")
|
||||
|
||||
@@ -17,9 +18,72 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
|
||||
-- create a new RTU unit
|
||||
---@type rtu_config
|
||||
local config = {}
|
||||
|
||||
rtu.config = config
|
||||
|
||||
-- load the RTU configuration
|
||||
function rtu.load_config()
|
||||
if not settings.load("/rtu.settings") then return false end
|
||||
|
||||
config.Peripherals = settings.get("Peripherals")
|
||||
config.Redstone = settings.get("Redstone")
|
||||
|
||||
config.SpeakerVolume = settings.get("SpeakerVolume")
|
||||
|
||||
config.SVR_Channel = settings.get("SVR_Channel")
|
||||
config.RTU_Channel = settings.get("RTU_Channel")
|
||||
config.ConnTimeout = settings.get("ConnTimeout")
|
||||
config.TrustedRange = settings.get("TrustedRange")
|
||||
config.AuthKey = settings.get("AuthKey")
|
||||
|
||||
config.LogMode = settings.get("LogMode")
|
||||
config.LogPath = settings.get("LogPath")
|
||||
config.LogDebug = settings.get("LogDebug")
|
||||
|
||||
config.FrontPanelTheme = settings.get("FrontPanelTheme")
|
||||
config.ColorMode = settings.get("ColorMode")
|
||||
|
||||
local cfv = util.new_validator()
|
||||
|
||||
cfv.assert_type_num(config.SpeakerVolume)
|
||||
cfv.assert_range(config.SpeakerVolume, 0, 3)
|
||||
|
||||
cfv.assert_channel(config.SVR_Channel)
|
||||
cfv.assert_channel(config.RTU_Channel)
|
||||
cfv.assert_type_num(config.ConnTimeout)
|
||||
cfv.assert_min(config.ConnTimeout, 2)
|
||||
cfv.assert_type_num(config.TrustedRange)
|
||||
cfv.assert_min(config.TrustedRange, 0)
|
||||
cfv.assert_type_str(config.AuthKey)
|
||||
|
||||
if type(config.AuthKey) == "string" then
|
||||
local len = string.len(config.AuthKey)
|
||||
cfv.assert(len == 0 or len >= 8)
|
||||
end
|
||||
|
||||
cfv.assert_type_int(config.LogMode)
|
||||
cfv.assert_range(config.LogMode, 0, 1)
|
||||
cfv.assert_type_str(config.LogPath)
|
||||
cfv.assert_type_bool(config.LogDebug)
|
||||
|
||||
cfv.assert_type_int(config.FrontPanelTheme)
|
||||
cfv.assert_range(config.FrontPanelTheme, 1, 2)
|
||||
cfv.assert_type_int(config.ColorMode)
|
||||
cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
|
||||
|
||||
cfv.assert_type_table(config.Peripherals)
|
||||
cfv.assert_type_table(config.Redstone)
|
||||
|
||||
return cfv.valid()
|
||||
end
|
||||
|
||||
-- create a new RTU unit<br>
|
||||
-- if this is for a PPM peripheral, auto fault clearing MUST stay enabled once access begins
|
||||
---@nodiscard
|
||||
function rtu.init_unit()
|
||||
---@param device table|nil peripheral device, if applicable
|
||||
function rtu.init_unit(device)
|
||||
local self = {
|
||||
discrete_inputs = {},
|
||||
coils = {},
|
||||
@@ -30,12 +94,18 @@ function rtu.init_unit()
|
||||
|
||||
local insert = table.insert
|
||||
|
||||
local stub = function () log.warning("tried to call an RTU function stub") end
|
||||
|
||||
---@class rtu_device
|
||||
local public = {}
|
||||
|
||||
---@class rtu
|
||||
local protected = {}
|
||||
|
||||
-- function to check if the peripheral (if exists) is faulted
|
||||
local function _is_faulted() return false end
|
||||
if device then _is_faulted = device.__p_is_faulted end
|
||||
|
||||
-- refresh IO count
|
||||
local function _count_io()
|
||||
self.io_count_cache = { #self.discrete_inputs, #self.coils, #self.input_regs, #self.holding_regs }
|
||||
@@ -47,13 +117,26 @@ function rtu.init_unit()
|
||||
return self.io_count_cache[1], self.io_count_cache[2], self.io_count_cache[3], self.io_count_cache[4]
|
||||
end
|
||||
|
||||
-- pass a function through or generate one to call a function by name from the device
|
||||
---@param f function|string function or device function name
|
||||
local function _as_func(f)
|
||||
if type(f) == "string" then
|
||||
local name = f
|
||||
if device then
|
||||
f = function (...) return device[name](...) end
|
||||
else f = stub end
|
||||
end
|
||||
|
||||
return f
|
||||
end
|
||||
|
||||
-- discrete inputs: single bit read-only
|
||||
|
||||
-- connect discrete input
|
||||
---@param f function
|
||||
---@param f function|string function or function name
|
||||
---@return integer count count of discrete inputs
|
||||
function protected.connect_di(f)
|
||||
insert(self.discrete_inputs, { read = f })
|
||||
insert(self.discrete_inputs, { read = _as_func(f) })
|
||||
_count_io()
|
||||
return #self.discrete_inputs
|
||||
end
|
||||
@@ -62,19 +145,18 @@ function rtu.init_unit()
|
||||
---@param di_addr integer
|
||||
---@return any value, boolean access_fault
|
||||
function public.read_di(di_addr)
|
||||
ppm.clear_fault()
|
||||
local value = self.discrete_inputs[di_addr].read()
|
||||
return value, ppm.is_faulted()
|
||||
return value, _is_faulted()
|
||||
end
|
||||
|
||||
-- coils: single bit read-write
|
||||
|
||||
-- connect coil
|
||||
---@param f_read function
|
||||
---@param f_write function
|
||||
---@param f_read function|string function or function name
|
||||
---@param f_write function|string function or function name
|
||||
---@return integer count count of coils
|
||||
function protected.connect_coil(f_read, f_write)
|
||||
insert(self.coils, { read = f_read, write = f_write })
|
||||
insert(self.coils, { read = _as_func(f_read), write = _as_func(f_write) })
|
||||
_count_io()
|
||||
return #self.coils
|
||||
end
|
||||
@@ -83,9 +165,8 @@ function rtu.init_unit()
|
||||
---@param coil_addr integer
|
||||
---@return any value, boolean access_fault
|
||||
function public.read_coil(coil_addr)
|
||||
ppm.clear_fault()
|
||||
local value = self.coils[coil_addr].read()
|
||||
return value, ppm.is_faulted()
|
||||
return value, _is_faulted()
|
||||
end
|
||||
|
||||
-- write coil
|
||||
@@ -93,18 +174,17 @@ function rtu.init_unit()
|
||||
---@param value any
|
||||
---@return boolean access_fault
|
||||
function public.write_coil(coil_addr, value)
|
||||
ppm.clear_fault()
|
||||
self.coils[coil_addr].write(value)
|
||||
return ppm.is_faulted()
|
||||
return _is_faulted()
|
||||
end
|
||||
|
||||
-- input registers: multi-bit read-only
|
||||
|
||||
-- connect input register
|
||||
---@param f function
|
||||
---@param f function|string function or function name
|
||||
---@return integer count count of input registers
|
||||
function protected.connect_input_reg(f)
|
||||
insert(self.input_regs, { read = f })
|
||||
insert(self.input_regs, { read = _as_func(f) })
|
||||
_count_io()
|
||||
return #self.input_regs
|
||||
end
|
||||
@@ -113,19 +193,18 @@ function rtu.init_unit()
|
||||
---@param reg_addr integer
|
||||
---@return any value, boolean access_fault
|
||||
function public.read_input_reg(reg_addr)
|
||||
ppm.clear_fault()
|
||||
local value = self.input_regs[reg_addr].read()
|
||||
return value, ppm.is_faulted()
|
||||
return value, _is_faulted()
|
||||
end
|
||||
|
||||
-- holding registers: multi-bit read-write
|
||||
|
||||
-- connect holding register
|
||||
---@param f_read function
|
||||
---@param f_write function
|
||||
---@param f_read function|string function or function name
|
||||
---@param f_write function|string function or function name
|
||||
---@return integer count count of holding registers
|
||||
function protected.connect_holding_reg(f_read, f_write)
|
||||
insert(self.holding_regs, { read = f_read, write = f_write })
|
||||
insert(self.holding_regs, { read = _as_func(f_read), write = _as_func(f_write) })
|
||||
_count_io()
|
||||
return #self.holding_regs
|
||||
end
|
||||
@@ -134,9 +213,8 @@ function rtu.init_unit()
|
||||
---@param reg_addr integer
|
||||
---@return any value, boolean access_fault
|
||||
function public.read_holding_reg(reg_addr)
|
||||
ppm.clear_fault()
|
||||
local value = self.holding_regs[reg_addr].read()
|
||||
return value, ppm.is_faulted()
|
||||
return value, _is_faulted()
|
||||
end
|
||||
|
||||
-- write holding register
|
||||
@@ -144,9 +222,8 @@ function rtu.init_unit()
|
||||
---@param value any
|
||||
---@return boolean access_fault
|
||||
function public.write_holding_reg(reg_addr, value)
|
||||
ppm.clear_fault()
|
||||
self.holding_regs[reg_addr].write(value)
|
||||
return ppm.is_faulted()
|
||||
return _is_faulted()
|
||||
end
|
||||
|
||||
-- public RTU device access
|
||||
@@ -175,7 +252,7 @@ function rtu.init_sounder(speaker)
|
||||
function spkr_ctl.continue()
|
||||
if spkr_ctl.playing then
|
||||
if spkr_ctl.speaker ~= nil and spkr_ctl.stream.has_next_block() then
|
||||
local success = spkr_ctl.speaker.playAudio(spkr_ctl.stream.get_next_block(), config.SOUNDER_VOLUME)
|
||||
local success = spkr_ctl.speaker.playAudio(spkr_ctl.stream.get_next_block(), config.SpeakerVolume)
|
||||
if not success then log.error(util.c("rtu_sounder(", spkr_ctl.name, "): error playing audio")) end
|
||||
end
|
||||
end
|
||||
@@ -203,11 +280,8 @@ end
|
||||
---@nodiscard
|
||||
---@param version string RTU version
|
||||
---@param nic nic network interface device
|
||||
---@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, nic, rtu_channel, svr_channel, range, conn_watchdog)
|
||||
function rtu.comms(version, nic, conn_watchdog)
|
||||
local self = {
|
||||
sv_addr = comms.BROADCAST,
|
||||
seq_num = 0,
|
||||
@@ -218,13 +292,13 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
|
||||
|
||||
local insert = table.insert
|
||||
|
||||
comms.set_trusted_range(range)
|
||||
comms.set_trusted_range(config.TrustedRange)
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- configure modem channels
|
||||
nic.closeAll()
|
||||
nic.open(rtu_channel)
|
||||
nic.open(config.RTU_Channel)
|
||||
|
||||
-- send a scada management packet
|
||||
---@param msg_type MGMT_TYPE
|
||||
@@ -236,7 +310,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
|
||||
m_pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
nic.transmit(svr_channel, rtu_channel, s_pkt)
|
||||
nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
@@ -280,7 +354,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
|
||||
function public.send_modbus(m_pkt)
|
||||
local s_pkt = comms.scada_packet()
|
||||
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
|
||||
nic.transmit(svr_channel, rtu_channel, s_pkt)
|
||||
nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
@@ -365,7 +439,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
|
||||
local l_chan = packet.scada_frame.local_channel()
|
||||
local src_addr = packet.scada_frame.src_addr()
|
||||
|
||||
if l_chan == rtu_channel then
|
||||
if l_chan == config.RTU_Channel then
|
||||
-- check sequence number
|
||||
if self.r_seq_num == nil then
|
||||
self.r_seq_num = packet.scada_frame.seq_num()
|
||||
|
||||
383
rtu/startup.lua
383
rtu/startup.lua
@@ -15,7 +15,7 @@ local rsio = require("scada-common.rsio")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local config = require("rtu.config")
|
||||
local configure = require("rtu.configure")
|
||||
local databus = require("rtu.databus")
|
||||
local modbus = require("rtu.modbus")
|
||||
local renderer = require("rtu.renderer")
|
||||
@@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
|
||||
local sps_rtu = require("rtu.dev.sps_rtu")
|
||||
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
|
||||
|
||||
local RTU_VERSION = "v1.6.6"
|
||||
local RTU_VERSION = "v1.9.6"
|
||||
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
|
||||
@@ -40,27 +40,30 @@ local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
|
||||
----------------------------------------
|
||||
-- config validation
|
||||
-- get configuration
|
||||
----------------------------------------
|
||||
|
||||
local cfv = util.new_validator()
|
||||
if not rtu.load_config() then
|
||||
-- try to reconfigure (user action)
|
||||
local success, error = configure.configure(true)
|
||||
if success then
|
||||
if not rtu.load_config() then
|
||||
println("failed to load a valid configuration, please reconfigure")
|
||||
return
|
||||
end
|
||||
else
|
||||
println("configuration error: " .. error)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
cfv.assert_channel(config.SVR_CHANNEL)
|
||||
cfv.assert_channel(config.RTU_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)
|
||||
cfv.assert_type_table(config.RTU_DEVICES)
|
||||
cfv.assert_type_table(config.RTU_REDSTONE)
|
||||
assert(cfv.valid(), "bad config file: missing/invalid fields")
|
||||
local config = rtu.config
|
||||
|
||||
----------------------------------------
|
||||
-- log init
|
||||
----------------------------------------
|
||||
|
||||
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
|
||||
log.init(config.LogPath, config.LogMode, config.LogDebug)
|
||||
|
||||
log.info("========================================")
|
||||
log.info("BOOTING rtu.startup " .. RTU_VERSION)
|
||||
@@ -68,6 +71,7 @@ log.info("========================================")
|
||||
println(">> RTU GATEWAY " .. RTU_VERSION .. " <<")
|
||||
|
||||
crash.set_env("rtu", RTU_VERSION)
|
||||
crash.dbg_log_env()
|
||||
|
||||
----------------------------------------
|
||||
-- main application
|
||||
@@ -85,8 +89,8 @@ local function main()
|
||||
ppm.mount_all()
|
||||
|
||||
-- message authentication init
|
||||
if type(config.AUTH_KEY) == "string" then
|
||||
network.init_mac(config.AUTH_KEY)
|
||||
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
|
||||
network.init_mac(config.AuthKey)
|
||||
end
|
||||
|
||||
-- get modem
|
||||
@@ -139,173 +143,174 @@ local function main()
|
||||
|
||||
local units = __shared_memory.rtu_sys.units
|
||||
|
||||
local rtu_redstone = config.RTU_REDSTONE
|
||||
local rtu_devices = config.RTU_DEVICES
|
||||
local rtu_redstone = config.Redstone
|
||||
local rtu_devices = config.Peripherals
|
||||
|
||||
-- configure RTU gateway based on config file definitions
|
||||
local function configure()
|
||||
-- configure RTU gateway based on settings file definitions
|
||||
local function sys_config()
|
||||
-- redstone interfaces
|
||||
local rs_rtus = {}
|
||||
|
||||
-- go through redstone definitions list
|
||||
for entry_idx = 1, #rtu_redstone do
|
||||
local rs_rtu = redstone_rtu.new()
|
||||
local io_table = rtu_redstone[entry_idx].io ---@type table
|
||||
local io_reactor = rtu_redstone[entry_idx].for_reactor ---@type integer
|
||||
local entry = rtu_redstone[entry_idx] ---@type rtu_rs_definition
|
||||
local assignment
|
||||
local for_reactor = entry.unit
|
||||
local iface_name = util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side)
|
||||
|
||||
-- CHECK: reactor ID must be >= to 1
|
||||
if (not util.is_int(io_reactor)) or (io_reactor < 0) then
|
||||
local message = util.c("configure> redstone entry #", entry_idx, " : ", io_reactor, " isn't an integer >= 0")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
end
|
||||
|
||||
-- CHECK: io table exists
|
||||
if type(io_table) ~= "table" then
|
||||
local message = util.c("configure> redstone entry #", entry_idx, " no IO table found")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
end
|
||||
|
||||
local capabilities = {}
|
||||
|
||||
log.debug(util.c("configure> starting redstone RTU I/O linking for reactor ", io_reactor, "..."))
|
||||
|
||||
local continue = true
|
||||
|
||||
-- CHECK: no duplicate entries
|
||||
for i = 1, #units do
|
||||
local unit = units[i] ---@type rtu_unit_registry_entry
|
||||
if unit.reactor == io_reactor and unit.type == RTU_UNIT_TYPE.REDSTONE then
|
||||
-- duplicate entry
|
||||
local message = util.c("configure> skipping definition block #", entry_idx, " for reactor ", io_reactor,
|
||||
" with already defined redstone I/O")
|
||||
println(message)
|
||||
log.warning(message)
|
||||
continue = false
|
||||
break
|
||||
if util.is_int(entry.unit) and entry.unit > 0 and entry.unit < 5 then
|
||||
---@cast for_reactor integer
|
||||
assignment = "reactor unit " .. entry.unit
|
||||
if rs_rtus[for_reactor] == nil then
|
||||
log.debug(util.c("sys_config> allocated redstone RTU for reactor unit ", entry.unit))
|
||||
rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} }
|
||||
end
|
||||
elseif entry.unit == nil then
|
||||
assignment = "facility"
|
||||
for_reactor = 0
|
||||
if rs_rtus[for_reactor] == nil then
|
||||
log.debug(util.c("sys_config> allocated redstone RTU for the facility"))
|
||||
rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} }
|
||||
end
|
||||
else
|
||||
local message = util.c("sys_config> invalid unit assignment at block index #", entry_idx)
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
end
|
||||
|
||||
-- not a duplicate
|
||||
if continue then
|
||||
for i = 1, #io_table do
|
||||
local valid = false
|
||||
local conf = io_table[i]
|
||||
-- verify configuration
|
||||
local valid = false
|
||||
if rsio.is_valid_port(entry.port) and rsio.is_valid_side(entry.side) then
|
||||
valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color))
|
||||
end
|
||||
|
||||
-- verify configuration
|
||||
if rsio.is_valid_port(conf.port) and rsio.is_valid_side(conf.side) then
|
||||
if conf.bundled_color then
|
||||
valid = rsio.is_color(conf.bundled_color)
|
||||
else
|
||||
valid = true
|
||||
end
|
||||
end
|
||||
local rs_rtu = rs_rtus[for_reactor].rtu
|
||||
local capabilities = rs_rtus[for_reactor].capabilities
|
||||
|
||||
if not valid then
|
||||
local message = util.c("configure> invalid redstone definition at index ", i, " in definition block #", entry_idx,
|
||||
" (for reactor ", io_reactor, ")")
|
||||
if not valid then
|
||||
local message = util.c("sys_config> invalid redstone definition at block index #", entry_idx)
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
else
|
||||
-- link redstone in RTU
|
||||
local mode = rsio.get_io_mode(entry.port)
|
||||
if mode == rsio.IO_MODE.DIGITAL_IN then
|
||||
-- can't have duplicate inputs
|
||||
if util.table_contains(capabilities, entry.port) then
|
||||
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name)
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
log.warning(message)
|
||||
else
|
||||
-- link redstone in RTU
|
||||
local mode = rsio.get_io_mode(conf.port)
|
||||
if mode == rsio.IO_MODE.DIGITAL_IN then
|
||||
-- can't have duplicate inputs
|
||||
if util.table_contains(capabilities, conf.port) then
|
||||
local message = util.c("configure> skipping duplicate input for port ", rsio.to_string(conf.port), " on side ", conf.side)
|
||||
println(message)
|
||||
log.warning(message)
|
||||
else
|
||||
rs_rtu.link_di(conf.side, conf.bundled_color)
|
||||
end
|
||||
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
|
||||
rs_rtu.link_do(conf.side, conf.bundled_color)
|
||||
elseif mode == rsio.IO_MODE.ANALOG_IN then
|
||||
-- can't have duplicate inputs
|
||||
if util.table_contains(capabilities, conf.port) then
|
||||
local message = util.c("configure> skipping duplicate input for port ", rsio.to_string(conf.port), " on side ", conf.side)
|
||||
println(message)
|
||||
log.warning(message)
|
||||
else
|
||||
rs_rtu.link_ai(conf.side)
|
||||
end
|
||||
elseif mode == rsio.IO_MODE.ANALOG_OUT then
|
||||
rs_rtu.link_ao(conf.side)
|
||||
else
|
||||
-- should be unreachable code, we already validated ports
|
||||
log.error("configure> fell through if chain attempting to identify IO mode", true)
|
||||
println("configure> encountered a software error, check logs")
|
||||
return false
|
||||
end
|
||||
|
||||
table.insert(capabilities, conf.port)
|
||||
|
||||
log.debug(util.c("configure> linked redstone ", #capabilities, ": ", rsio.to_string(conf.port),
|
||||
" (", conf.side, ") for reactor ", io_reactor))
|
||||
rs_rtu.link_di(entry.side, entry.color)
|
||||
end
|
||||
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
|
||||
rs_rtu.link_do(entry.side, entry.color)
|
||||
elseif mode == rsio.IO_MODE.ANALOG_IN then
|
||||
-- can't have duplicate inputs
|
||||
if util.table_contains(capabilities, entry.port) then
|
||||
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name)
|
||||
println(message)
|
||||
log.warning(message)
|
||||
else
|
||||
rs_rtu.link_ai(entry.side)
|
||||
end
|
||||
elseif mode == rsio.IO_MODE.ANALOG_OUT then
|
||||
rs_rtu.link_ao(entry.side)
|
||||
else
|
||||
-- should be unreachable code, we already validated ports
|
||||
log.error("sys_config> fell through if chain attempting to identify IO mode at block index #" .. entry_idx, true)
|
||||
println("sys_config> encountered a software error, check logs")
|
||||
return false
|
||||
end
|
||||
|
||||
---@class rtu_unit_registry_entry
|
||||
local unit = {
|
||||
uid = 0, ---@type integer
|
||||
name = "redstone_io", ---@type string
|
||||
type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE
|
||||
index = entry_idx, ---@type integer
|
||||
reactor = io_reactor, ---@type integer
|
||||
device = capabilities, ---@type table use device field for redstone ports
|
||||
is_multiblock = false, ---@type boolean
|
||||
formed = nil, ---@type boolean|nil
|
||||
hw_state = RTU_UNIT_HW_STATE.OK, ---@type RTU_UNIT_HW_STATE
|
||||
rtu = rs_rtu, ---@type rtu_device|rtu_rs_device
|
||||
modbus_io = modbus.new(rs_rtu, false),
|
||||
pkt_queue = nil, ---@type mqueue|nil
|
||||
thread = nil ---@type parallel_thread|nil
|
||||
}
|
||||
table.insert(capabilities, entry.port)
|
||||
|
||||
table.insert(units, unit)
|
||||
|
||||
local for_message = "facility"
|
||||
if io_reactor > 0 then
|
||||
for_message = util.c("reactor ", io_reactor)
|
||||
end
|
||||
|
||||
log.info(util.c("configure> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message))
|
||||
|
||||
unit.uid = #units
|
||||
|
||||
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
|
||||
log.debug(util.c("sys_config> linked redstone ", #capabilities, ": ", rsio.to_string(entry.port), " (", iface_name, ") for ", assignment))
|
||||
end
|
||||
end
|
||||
|
||||
-- create unit entries for redstone RTUs
|
||||
for for_reactor, def in pairs(rs_rtus) do
|
||||
---@class rtu_unit_registry_entry
|
||||
local unit = {
|
||||
uid = 0, ---@type integer
|
||||
name = "redstone_io", ---@type string
|
||||
type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE
|
||||
index = false, ---@type integer|false
|
||||
reactor = for_reactor, ---@type integer
|
||||
device = def.capabilities, ---@type table use device field for redstone ports
|
||||
is_multiblock = false, ---@type boolean
|
||||
formed = nil, ---@type boolean|nil
|
||||
hw_state = RTU_UNIT_HW_STATE.OK, ---@type RTU_UNIT_HW_STATE
|
||||
rtu = def.rtu, ---@type rtu_device|rtu_rs_device
|
||||
modbus_io = modbus.new(def.rtu, false),
|
||||
pkt_queue = nil, ---@type mqueue|nil
|
||||
thread = nil ---@type parallel_thread|nil
|
||||
}
|
||||
|
||||
table.insert(units, unit)
|
||||
|
||||
local for_message = "facility"
|
||||
if util.is_int(for_reactor) then
|
||||
for_message = util.c("reactor unit ", for_reactor)
|
||||
end
|
||||
|
||||
log.info(util.c("sys_config> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message))
|
||||
|
||||
unit.uid = #units
|
||||
|
||||
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
|
||||
end
|
||||
|
||||
-- mounted peripherals
|
||||
for i = 1, #rtu_devices do
|
||||
local name = rtu_devices[i].name
|
||||
local index = rtu_devices[i].index
|
||||
local for_reactor = rtu_devices[i].for_reactor
|
||||
local entry = rtu_devices[i] ---@type rtu_peri_definition
|
||||
local name = entry.name
|
||||
local index = entry.index
|
||||
local for_reactor = util.trinary(entry.unit == nil, 0, entry.unit)
|
||||
|
||||
-- CHECK: name is a string
|
||||
if type(name) ~= "string" then
|
||||
local message = util.c("configure> device entry #", i, ": device ", name, " isn't a string")
|
||||
local message = util.c("sys_config> device entry #", i, ": device ", name, " isn't a string")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
end
|
||||
|
||||
-- CHECK: index is an integer >= 1
|
||||
if (not util.is_int(index)) or (index <= 0) then
|
||||
local message = util.c("configure> device entry #", i, ": index ", index, " isn't an integer >= 1")
|
||||
-- CHECK: index type
|
||||
if (index ~= nil) and (not util.is_int(index)) then
|
||||
local message = util.c("sys_config> device entry #", i, ": index ", index, " isn't valid")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
end
|
||||
|
||||
-- CHECK: index range
|
||||
local function validate_index(min, max)
|
||||
if (not util.is_int(index)) or ((index < min) and (max ~= nil and index > max)) then
|
||||
local message = util.c("sys_config> device entry #", i, ": index ", index, " isn't >= ", min)
|
||||
if max ~= nil then message = util.c(message, " and <= ", max) end
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
else return true end
|
||||
end
|
||||
|
||||
-- CHECK: reactor is an integer >= 0
|
||||
if (not util.is_int(for_reactor)) or (for_reactor < 0) then
|
||||
local message = util.c("configure> device entry #", i, ": reactor ", for_reactor, " isn't an integer >= 0")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
local function validate_assign(for_facility)
|
||||
if for_facility and for_reactor ~= 0 then
|
||||
local message = util.c("sys_config> device entry #", i, ": must only be for the facility")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
elseif (not for_facility) and ((not util.is_int(for_reactor)) or (for_reactor < 1) or (for_reactor > 4)) then
|
||||
local message = util.c("sys_config> device entry #", i, ": unit assignment ", for_reactor, " isn't vaild")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
else return true end
|
||||
end
|
||||
|
||||
local device = ppm.get_periph(name)
|
||||
@@ -318,7 +323,7 @@ local function main()
|
||||
local faulted = nil ---@type boolean|nil
|
||||
|
||||
if device == nil then
|
||||
local message = util.c("configure> '", name, "' not found, using placeholder")
|
||||
local message = util.c("sys_config> '", name, "' not found, using placeholder")
|
||||
println(message)
|
||||
log.warning(message)
|
||||
|
||||
@@ -330,70 +335,93 @@ local function main()
|
||||
|
||||
if type == "boilerValve" then
|
||||
-- boiler multiblock
|
||||
if not validate_index(1, 2) then return false end
|
||||
if not validate_assign() then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.BOILER_VALVE
|
||||
rtu_iface, faulted = boilerv_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
|
||||
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
|
||||
log.fatal(util.c("configure> failed to check if '", name, "' is a formed boiler multiblock"))
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
|
||||
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed boiler multiblock"))
|
||||
return false
|
||||
end
|
||||
elseif type == "turbineValve" then
|
||||
-- turbine multiblock
|
||||
if not validate_index(1, 3) then return false end
|
||||
if not validate_assign() then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE
|
||||
rtu_iface, faulted = turbinev_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
|
||||
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
|
||||
log.fatal(util.c("configure> failed to check if '", name, "' is a formed turbine multiblock"))
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
|
||||
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed turbine multiblock"))
|
||||
return false
|
||||
end
|
||||
elseif type == "dynamicValve" then
|
||||
-- dynamic tank multiblock
|
||||
if entry.unit == nil then
|
||||
if not validate_index(1, 4) then return false end
|
||||
if not validate_assign(true) then return false end
|
||||
else
|
||||
if not validate_index(1, 1) then return false end
|
||||
if not validate_assign() then return false end
|
||||
end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE
|
||||
rtu_iface, faulted = dynamicv_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
|
||||
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
|
||||
log.fatal(util.c("configure> failed to check if '", name, "' is a formed dynamic tank multiblock"))
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
|
||||
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed dynamic tank multiblock"))
|
||||
return false
|
||||
end
|
||||
elseif type == "inductionPort" then
|
||||
-- induction matrix multiblock
|
||||
if not validate_assign(true) then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.IMATRIX
|
||||
rtu_iface, faulted = imatrix_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
|
||||
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
|
||||
log.fatal(util.c("configure> failed to check if '", name, "' is a formed induction matrix multiblock"))
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
|
||||
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed induction matrix multiblock"))
|
||||
return false
|
||||
end
|
||||
elseif type == "spsPort" then
|
||||
-- SPS multiblock
|
||||
if not validate_assign(true) then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.SPS
|
||||
rtu_iface, faulted = sps_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
|
||||
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
|
||||
log.fatal(util.c("configure> failed to check if '", name, "' is a formed SPS multiblock"))
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
|
||||
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed SPS multiblock"))
|
||||
return false
|
||||
end
|
||||
elseif type == "solarNeutronActivator" then
|
||||
-- SNA
|
||||
if not validate_assign() then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.SNA
|
||||
rtu_iface, faulted = sna_rtu.new(device)
|
||||
elseif type == "environmentDetector" then
|
||||
-- advanced peripherals environment detector
|
||||
if not validate_index(1) then return false end
|
||||
if not validate_assign(entry.unit == nil) then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
|
||||
rtu_iface, faulted = envd_rtu.new(device)
|
||||
elseif type == ppm.VIRTUAL_DEVICE_TYPE then
|
||||
@@ -401,7 +429,7 @@ local function main()
|
||||
rtu_type = RTU_UNIT_TYPE.VIRTUAL
|
||||
rtu_iface = rtu.init_unit().interface()
|
||||
else
|
||||
local message = util.c("configure> device '", name, "' is not a known type (", type, ")")
|
||||
local message = util.c("sys_config> device '", name, "' is not a known type (", type, ")")
|
||||
println_ts(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
@@ -409,12 +437,12 @@ local function main()
|
||||
|
||||
if is_multiblock then
|
||||
if not formed then
|
||||
log.info(util.c("configure> device '", name, "' is not formed"))
|
||||
log.info(util.c("sys_config> device '", name, "' is not formed"))
|
||||
elseif faulted then
|
||||
-- sometimes there is a race condition on server boot where it reports formed, but
|
||||
-- the other functions are not yet defined (that's the theory at least). mark as unformed to attempt connection later
|
||||
formed = false
|
||||
log.warning(util.c("configure> device '", name, "' is formed, but initialization had one or more faults: marked as unformed"))
|
||||
log.warning(util.c("sys_config> device '", name, "' is formed, but initialization had one or more faults: marked as unformed"))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -423,7 +451,7 @@ local function main()
|
||||
uid = 0, ---@type integer
|
||||
name = name, ---@type string
|
||||
type = rtu_type, ---@type RTU_UNIT_TYPE
|
||||
index = index, ---@type integer
|
||||
index = index or false, ---@type integer|false
|
||||
reactor = for_reactor, ---@type integer
|
||||
device = device, ---@type table
|
||||
is_multiblock = is_multiblock, ---@type boolean
|
||||
@@ -444,7 +472,8 @@ local function main()
|
||||
for_message = util.c("reactor ", for_reactor)
|
||||
end
|
||||
|
||||
log.info(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ") [", index, "] for ", for_message))
|
||||
local index_str = util.trinary(index ~= nil, util.c(" [", index, "]"), "")
|
||||
log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ")", index_str, " for ", for_message))
|
||||
|
||||
rtu_unit.uid = #units
|
||||
|
||||
@@ -465,7 +494,6 @@ local function main()
|
||||
databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state)
|
||||
end
|
||||
|
||||
-- we made it through all that trusting-user-to-write-a-config-file chaos
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -475,12 +503,12 @@ local function main()
|
||||
|
||||
local rtu_state = __shared_memory.rtu_state
|
||||
|
||||
log.debug("boot> running configure()")
|
||||
log.debug("boot> running sys_config()")
|
||||
|
||||
if configure() then
|
||||
if sys_config() then
|
||||
-- start UI
|
||||
local message
|
||||
rtu_state.fp_ok, message = renderer.try_start_ui(units)
|
||||
rtu_state.fp_ok, message = renderer.try_start_ui(units, config.FrontPanelTheme, config.ColorMode)
|
||||
|
||||
if not rtu_state.fp_ok then
|
||||
println_ts(util.c("UI error: ", message))
|
||||
@@ -502,12 +530,11 @@ local function main()
|
||||
databus.tx_hw_spkr_count(#__shared_memory.rtu_dev.sounders)
|
||||
|
||||
-- start connection watchdog
|
||||
smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT)
|
||||
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
|
||||
log.debug("startup> conn watchdog started")
|
||||
|
||||
-- setup comms
|
||||
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, config.RTU_CHANNEL, config.SVR_CHANNEL,
|
||||
config.TRUSTED_RANGE, smem_sys.conn_watchdog)
|
||||
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
|
||||
log.debug("startup> comms init")
|
||||
|
||||
-- init threads
|
||||
|
||||
317
rtu/threads.lua
317
rtu/threads.lua
@@ -28,6 +28,147 @@ local UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
|
||||
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
|
||||
local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
|
||||
|
||||
---@param smem rtu_shared_memory
|
||||
---@param println_ts function
|
||||
---@param iface string
|
||||
---@param type string
|
||||
---@param device table
|
||||
---@param unit rtu_unit_registry_entry
|
||||
local function handle_unit_mount(smem, println_ts, iface, type, device, unit)
|
||||
local sys = smem.rtu_sys
|
||||
|
||||
-- find disconnected device to reconnect
|
||||
-- note: cannot check isFormed as that would yield this coroutine and consume events
|
||||
if unit.name == iface then
|
||||
local resend_advert, faulted, unknown, invalid = false, false, false, false
|
||||
|
||||
local function fail(msg)
|
||||
invalid = true
|
||||
log.error(msg .. " in config")
|
||||
end
|
||||
|
||||
-- found, re-link
|
||||
unit.device = device
|
||||
|
||||
if unit.type == RTU_UNIT_TYPE.VIRTUAL then
|
||||
resend_advert = true
|
||||
if type == "boilerValve" then
|
||||
-- boiler multiblock
|
||||
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("boiler '", unit.name, "' cannot init, not assigned to a valid unit")) end
|
||||
if (unit.index == false) or unit.index < 1 or unit.index > 2 then fail(util.c("boiler '", unit.name, "' cannot init, invalid index provided")) end
|
||||
|
||||
unit.type = RTU_UNIT_TYPE.BOILER_VALVE
|
||||
elseif type == "turbineValve" then
|
||||
-- turbine multiblock
|
||||
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("turbine '", unit.name, "' cannot init, not assigned to a valid unit")) end
|
||||
if (unit.index == false) or unit.index < 1 or unit.index > 3 then fail(util.c("turbine '", unit.name, "' cannot init, invalid index provided")) end
|
||||
|
||||
unit.type = RTU_UNIT_TYPE.TURBINE_VALVE
|
||||
elseif type == "dynamicValve" then
|
||||
-- dynamic tank multiblock
|
||||
if unit.reactor < 0 or unit.reactor > 4 then fail(util.c("dynamic tank '", unit.name, "' cannot init, no valid assignment provided")) end
|
||||
|
||||
if (unit.reactor == 0 and ((unit.index == false) or unit.index < 1 or unit.index > 4)) or
|
||||
(unit.reactor > 0 and unit.index ~= 1) then
|
||||
fail(util.c("dynamic tank '", unit.name, "' cannot init, invalid index provided"))
|
||||
end
|
||||
|
||||
unit.type = RTU_UNIT_TYPE.DYNAMIC_VALVE
|
||||
elseif type == "inductionPort" then
|
||||
-- induction matrix multiblock
|
||||
if unit.reactor ~= 0 then fail(util.c("induction matrix '", unit.name, "' cannot init, not assigned to facility")) end
|
||||
|
||||
unit.type = RTU_UNIT_TYPE.IMATRIX
|
||||
elseif type == "spsPort" then
|
||||
-- SPS multiblock
|
||||
if unit.reactor ~= 0 then fail(util.c("SPS '", unit.name, "' cannot init, not assigned to facility")) end
|
||||
|
||||
unit.type = RTU_UNIT_TYPE.SPS
|
||||
elseif type == "solarNeutronActivator" then
|
||||
-- SNA
|
||||
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("SNA '", unit.name, "' cannot init, not assigned to a valid unit")) end
|
||||
|
||||
unit.type = RTU_UNIT_TYPE.SNA
|
||||
elseif type == "environmentDetector" then
|
||||
-- advanced peripherals environment detector
|
||||
if unit.reactor < 0 or unit.reactor > 4 then fail(util.c("environment detector '", unit.name, "' cannot init, no valid assignment provided")) end
|
||||
if (unit.index == false) or unit.index < 1 then fail(util.c("environment detector '", unit.name, "' cannot init, invalid index provided")) end
|
||||
|
||||
unit.type = RTU_UNIT_TYPE.ENV_DETECTOR
|
||||
else
|
||||
resend_advert = false
|
||||
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
|
||||
end
|
||||
|
||||
databus.tx_unit_hw_type(unit.uid, unit.type)
|
||||
end
|
||||
|
||||
-- if disconnected on startup, config wouldn't have been validated
|
||||
-- checking now that it has connected; the config isn't valid, so don't connect it
|
||||
if invalid then
|
||||
unit.hw_state = UNIT_HW_STATE.OFFLINE
|
||||
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
|
||||
return
|
||||
end
|
||||
|
||||
-- note for multiblock structures: if not formed, indexing the multiblock functions results in a PPM fault
|
||||
|
||||
if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
|
||||
unit.rtu, faulted = boilerv_rtu.new(device)
|
||||
unit.formed = util.trinary(faulted, false, nil)
|
||||
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
|
||||
unit.rtu, faulted = turbinev_rtu.new(device)
|
||||
unit.formed = util.trinary(faulted, false, nil)
|
||||
elseif unit.type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
|
||||
unit.rtu, faulted = dynamicv_rtu.new(device)
|
||||
unit.formed = util.trinary(faulted, false, nil)
|
||||
elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
|
||||
unit.rtu, faulted = imatrix_rtu.new(device)
|
||||
unit.formed = util.trinary(faulted, false, nil)
|
||||
elseif unit.type == RTU_UNIT_TYPE.SPS then
|
||||
unit.rtu, faulted = sps_rtu.new(device)
|
||||
unit.formed = util.trinary(faulted, false, nil)
|
||||
elseif unit.type == RTU_UNIT_TYPE.SNA then
|
||||
unit.rtu, faulted = sna_rtu.new(device)
|
||||
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
|
||||
unit.rtu, faulted = envd_rtu.new(device)
|
||||
else
|
||||
unknown = true
|
||||
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
|
||||
end
|
||||
|
||||
if unit.is_multiblock then
|
||||
unit.hw_state = UNIT_HW_STATE.UNFORMED
|
||||
if unit.formed == false then
|
||||
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
|
||||
end
|
||||
elseif faulted then
|
||||
unit.hw_state = UNIT_HW_STATE.FAULTED
|
||||
elseif not unknown then
|
||||
unit.hw_state = UNIT_HW_STATE.OK
|
||||
else
|
||||
unit.hw_state = UNIT_HW_STATE.OFFLINE
|
||||
end
|
||||
|
||||
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
|
||||
|
||||
if not unknown then
|
||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||
|
||||
local type_name = types.rtu_type_to_string(unit.type)
|
||||
local message = util.c("reconnected the ", type_name, " on interface ", unit.name)
|
||||
println_ts(message)
|
||||
log.info(message)
|
||||
|
||||
if resend_advert then
|
||||
sys.rtu_comms.send_advertisement(sys.units)
|
||||
else
|
||||
sys.rtu_comms.send_remounted(unit.uid)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- main thread
|
||||
---@nodiscard
|
||||
---@param smem rtu_shared_memory
|
||||
@@ -180,102 +321,7 @@ function threads.thread__main(smem)
|
||||
else
|
||||
-- relink lost peripheral to correct unit entry
|
||||
for i = 1, #units do
|
||||
local unit = units[i] ---@type rtu_unit_registry_entry
|
||||
|
||||
-- find disconnected device to reconnect
|
||||
-- note: cannot check isFormed as that would yield this coroutine and consume events
|
||||
if unit.name == param1 then
|
||||
local resend_advert = false
|
||||
local faulted = false
|
||||
local unknown = false
|
||||
|
||||
-- found, re-link
|
||||
unit.device = device
|
||||
|
||||
if unit.type == RTU_UNIT_TYPE.VIRTUAL then
|
||||
resend_advert = true
|
||||
if type == "boilerValve" then
|
||||
-- boiler multiblock
|
||||
unit.type = RTU_UNIT_TYPE.BOILER_VALVE
|
||||
elseif type == "turbineValve" then
|
||||
-- turbine multiblock
|
||||
unit.type = RTU_UNIT_TYPE.TURBINE_VALVE
|
||||
elseif type == "inductionPort" then
|
||||
-- induction matrix multiblock
|
||||
unit.type = RTU_UNIT_TYPE.IMATRIX
|
||||
elseif type == "spsPort" then
|
||||
-- SPS multiblock
|
||||
unit.type = RTU_UNIT_TYPE.SPS
|
||||
elseif type == "solarNeutronActivator" then
|
||||
-- SNA
|
||||
unit.type = RTU_UNIT_TYPE.SNA
|
||||
elseif type == "environmentDetector" then
|
||||
-- advanced peripherals environment detector
|
||||
unit.type = RTU_UNIT_TYPE.ENV_DETECTOR
|
||||
else
|
||||
resend_advert = false
|
||||
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
|
||||
end
|
||||
|
||||
databus.tx_unit_hw_type(unit.uid, unit.type)
|
||||
end
|
||||
|
||||
-- note for multiblock structures: if not formed, indexing the multiblock functions results in a PPM fault
|
||||
|
||||
if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
|
||||
unit.rtu, faulted = boilerv_rtu.new(device)
|
||||
unit.formed = util.trinary(faulted, false, nil)
|
||||
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
|
||||
unit.rtu, faulted = turbinev_rtu.new(device)
|
||||
unit.formed = util.trinary(faulted, false, nil)
|
||||
elseif unit.type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
|
||||
unit.rtu, faulted = dynamicv_rtu.new(device)
|
||||
unit.formed = util.trinary(faulted, false, nil)
|
||||
elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
|
||||
unit.rtu, faulted = imatrix_rtu.new(device)
|
||||
unit.formed = util.trinary(faulted, false, nil)
|
||||
elseif unit.type == RTU_UNIT_TYPE.SPS then
|
||||
unit.rtu, faulted = sps_rtu.new(device)
|
||||
unit.formed = util.trinary(faulted, false, nil)
|
||||
elseif unit.type == RTU_UNIT_TYPE.SNA then
|
||||
unit.rtu, faulted = sna_rtu.new(device)
|
||||
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
|
||||
unit.rtu, faulted = envd_rtu.new(device)
|
||||
else
|
||||
unknown = true
|
||||
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
|
||||
end
|
||||
|
||||
if unit.is_multiblock then
|
||||
unit.hw_state = UNIT_HW_STATE.UNFORMED
|
||||
if unit.formed == false then
|
||||
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
|
||||
end
|
||||
elseif faulted then
|
||||
unit.hw_state = UNIT_HW_STATE.FAULTED
|
||||
elseif not unknown then
|
||||
unit.hw_state = UNIT_HW_STATE.OK
|
||||
else
|
||||
unit.hw_state = UNIT_HW_STATE.OFFLINE
|
||||
end
|
||||
|
||||
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
|
||||
|
||||
if not unknown then
|
||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||
|
||||
local type_name = types.rtu_type_to_string(unit.type)
|
||||
local message = util.c("reconnected the ", type_name, " on interface ", unit.name)
|
||||
println_ts(message)
|
||||
log.info(message)
|
||||
|
||||
if resend_advert then
|
||||
rtu_comms.send_advertisement(units)
|
||||
else
|
||||
rtu_comms.send_remounted(unit.uid)
|
||||
end
|
||||
end
|
||||
end
|
||||
handle_unit_mount(smem, println_ts, param1, type, device, units[i])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -471,82 +517,23 @@ function threads.thread__unit_comms(smem, unit)
|
||||
|
||||
-- check if multiblock is still formed if this is a multiblock
|
||||
if unit.is_multiblock and (util.time_ms() - last_f_check > 250) then
|
||||
local is_formed = unit.device.isFormed()
|
||||
|
||||
last_f_check = util.time_ms()
|
||||
|
||||
local is_formed = unit.device.isFormed()
|
||||
|
||||
if unit.formed == nil then
|
||||
unit.formed = is_formed
|
||||
if is_formed then unit.hw_state = UNIT_HW_STATE.OK end
|
||||
elseif not unit.formed then
|
||||
unit.hw_state = UNIT_HW_STATE.UNFORMED
|
||||
end
|
||||
|
||||
if not unit.formed then unit.hw_state = UNIT_HW_STATE.UNFORMED end
|
||||
|
||||
if (not unit.formed) and is_formed then
|
||||
-- newly re-formed
|
||||
local iface = ppm.get_iface(unit.device)
|
||||
if iface then
|
||||
log.info(util.c("unmounting and remounting reformed RTU unit ", detail_name))
|
||||
|
||||
ppm.unmount(unit.device)
|
||||
|
||||
local type, device = ppm.mount(iface)
|
||||
local faulted = false
|
||||
|
||||
if device ~= nil then
|
||||
if type == "boilerValve" and unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
|
||||
-- boiler multiblock
|
||||
unit.device = device
|
||||
unit.rtu, faulted = boilerv_rtu.new(device)
|
||||
unit.formed = device.isFormed()
|
||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||
elseif type == "turbineValve" and unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
|
||||
-- turbine multiblock
|
||||
unit.device = device
|
||||
unit.rtu, faulted = turbinev_rtu.new(device)
|
||||
unit.formed = device.isFormed()
|
||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||
elseif type == "dynamicValve" and unit.type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
|
||||
-- dynamic tank multiblock
|
||||
unit.device = device
|
||||
unit.rtu, faulted = dynamicv_rtu.new(device)
|
||||
unit.formed = device.isFormed()
|
||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||
elseif type == "inductionPort" and unit.type == RTU_UNIT_TYPE.IMATRIX then
|
||||
-- induction matrix multiblock
|
||||
unit.device = device
|
||||
unit.rtu, faulted = imatrix_rtu.new(device)
|
||||
unit.formed = device.isFormed()
|
||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||
elseif type == "spsPort" and unit.type == RTU_UNIT_TYPE.SPS then
|
||||
-- SPS multiblock
|
||||
unit.device = device
|
||||
unit.rtu, faulted = sps_rtu.new(device)
|
||||
unit.formed = device.isFormed()
|
||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||
else
|
||||
log.error("illegal remount of non-multiblock RTU or type change attempted for " .. short_name, true)
|
||||
end
|
||||
|
||||
if unit.formed and faulted then
|
||||
-- something is still wrong = can't mark as formed yet
|
||||
unit.formed = false
|
||||
unit.hw_state = UNIT_HW_STATE.UNFORMED
|
||||
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
|
||||
else
|
||||
unit.hw_state = UNIT_HW_STATE.OK
|
||||
rtu_comms.send_remounted(unit.uid)
|
||||
end
|
||||
|
||||
local type_name = types.rtu_type_to_string(unit.type)
|
||||
log.info(util.c("reconnected the ", type_name, " on interface ", unit.name))
|
||||
else
|
||||
-- fully lost the peripheral now :(
|
||||
log.error(util.c(unit.name, " lost (failed reconnect)"))
|
||||
end
|
||||
else
|
||||
log.error("failed to get interface of previously connected RTU unit " .. detail_name, true)
|
||||
end
|
||||
if (is_formed == true) and not unit.formed then
|
||||
unit.hw_state = UNIT_HW_STATE.OK
|
||||
log.info(util.c(detail_name, " is now formed"))
|
||||
rtu_comms.send_remounted(unit.uid)
|
||||
elseif (is_formed == false) and unit.formed then
|
||||
log.warning(util.c(detail_name, " is no longer formed"))
|
||||
end
|
||||
|
||||
unit.formed = is_formed
|
||||
|
||||
@@ -16,8 +16,9 @@ local max_distance = nil
|
||||
---@class comms
|
||||
local comms = {}
|
||||
|
||||
-- protocol/data version (protocol/data independent changes tracked by util.lua version)
|
||||
comms.version = "2.4.1"
|
||||
-- protocol/data versions (protocol/data independent changes tracked by util.lua version)
|
||||
comms.version = "2.5.1"
|
||||
comms.api_version = "0.0.2"
|
||||
|
||||
---@enum PROTOCOL
|
||||
local PROTOCOL = {
|
||||
@@ -64,7 +65,9 @@ local CRDN_TYPE = {
|
||||
FAC_CMD = 3, -- faility command
|
||||
UNIT_BUILDS = 4, -- build of each reactor unit (reactor + RTUs)
|
||||
UNIT_STATUSES = 5, -- state of each of the reactor units
|
||||
UNIT_CMD = 6 -- command a reactor unit
|
||||
UNIT_CMD = 6, -- command a reactor unit
|
||||
API_GET_FAC = 7, -- API: get all the facility data
|
||||
API_GET_UNIT = 8 -- API: get reactor unit data
|
||||
}
|
||||
|
||||
---@enum ESTABLISH_ACK
|
||||
@@ -72,7 +75,8 @@ local ESTABLISH_ACK = {
|
||||
ALLOW = 0, -- link approved
|
||||
DENY = 1, -- link denied
|
||||
COLLISION = 2, -- link denied due to existing active link
|
||||
BAD_VERSION = 3 -- link denied due to comms version mismatch
|
||||
BAD_VERSION = 3, -- link denied due to comms version mismatch
|
||||
BAD_API_VERSION = 4 -- link denied due to api version mismatch
|
||||
}
|
||||
|
||||
---@enum DEVICE_TYPE device types for establish messages
|
||||
@@ -93,7 +97,8 @@ local FAC_COMMAND = {
|
||||
START = 2, -- start automatic process control
|
||||
ACK_ALL_ALARMS = 3, -- acknowledge all alarms on all units
|
||||
SET_WASTE_MODE = 4, -- set automatic waste processing mode
|
||||
SET_PU_FB = 5 -- set plutonium fallback mode
|
||||
SET_PU_FB = 5, -- set plutonium fallback mode
|
||||
SET_SPS_LP = 6 -- set SPS at low power mode
|
||||
}
|
||||
|
||||
---@enum UNIT_COMMAND
|
||||
@@ -311,7 +316,7 @@ function comms.authd_packet()
|
||||
self.valid = false
|
||||
self.raw = self.modem_msg_in.msg
|
||||
|
||||
if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then
|
||||
if (type(max_distance) == "number") and ((type(distance) ~= "number") or (distance > max_distance)) then
|
||||
-- outside of maximum allowable transmission distance
|
||||
-- log.debug("comms.authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
|
||||
else
|
||||
|
||||
@@ -29,7 +29,7 @@ local annunc = {}
|
||||
annunc.RCSFlowLow_H2O = -3.2 -- flow < -3.2 mB/s
|
||||
annunc.RCSFlowLow_NA = -2.0 -- flow < -2.0 mB/s
|
||||
annunc.CoolantLevelLow = 0.4 -- fill < 40%
|
||||
annunc.ReactorTempHigh = 1000 -- temp > 1000K
|
||||
annunc.OpTempTolerance = 5 -- high temp if >= operational temp + X
|
||||
annunc.ReactorHighDeltaT = 50 -- rate > 50K/s
|
||||
annunc.FuelLevelLow = 0.05 -- fill <= 5%
|
||||
annunc.WasteLevelHigh = 0.80 -- fill >= 80%
|
||||
@@ -66,21 +66,48 @@ constants.ALARM_LIMITS = alarms
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Supervisor Redstone Activation Thresholds
|
||||
|
||||
---@class _rs_threshold_constants
|
||||
local rs = {}
|
||||
|
||||
rs.IMATRIX_CHARGE_LOW = 0.05 -- activation threshold (less than) for F_MATRIX_LOW
|
||||
rs.IMATRIX_CHARGE_HIGH = 0.95 -- activation threshold (greater than) for F_MATRIX_HIGH
|
||||
|
||||
constants.RS_THRESHOLDS = rs
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Supervisor Constants
|
||||
|
||||
-- milliseconds until turbine flow is assumed to be stable enough to enable coolant checks
|
||||
constants.FLOW_STABILITY_DELAY_MS = 15000
|
||||
-- milliseconds until coolant flow is assumed to be stable enough to enable certain coolant checks
|
||||
constants.FLOW_STABILITY_DELAY_MS = 10000
|
||||
|
||||
-- Notes on Radiation
|
||||
-- - background radiation 0.0000001 Sv/h (99.99 nSv/h)
|
||||
-- - "green tint" radiation 0.00001 Sv/h (10 uSv/h)
|
||||
-- - damaging radiation 0.00006 Sv/h (60 uSv/h)
|
||||
constants.LOW_RADIATION = 0.00001
|
||||
constants.HAZARD_RADIATION = 0.00006
|
||||
constants.HIGH_RADIATION = 0.001
|
||||
constants.LOW_RADIATION = 0.00001
|
||||
constants.HAZARD_RADIATION = 0.00006
|
||||
constants.HIGH_RADIATION = 0.001
|
||||
constants.VERY_HIGH_RADIATION = 0.1
|
||||
constants.SEVERE_RADIATION = 8.0
|
||||
constants.EXTREME_RADIATION = 100.0
|
||||
constants.SEVERE_RADIATION = 8.0
|
||||
constants.EXTREME_RADIATION = 100.0
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Mekanism Configuration Constants
|
||||
|
||||
---@class _mek_constants
|
||||
local mek = {}
|
||||
|
||||
mek.BASE_BOIL_TEMP = 373.15 -- mekanism: HeatUtils.BASE_BOIL_TEMP
|
||||
mek.JOULES_PER_MB = 1000000 -- mekanism: energyPerFissionFuel
|
||||
mek.TURBINE_GAS_PER_TANK = 64000 -- mekanism: turbineGasPerTank
|
||||
mek.TURBINE_DISPERSER_FLOW = 1280 -- mekanism: turbineDisperserGasFlow
|
||||
mek.TURBINE_VENT_FLOW = 32000 -- mekanism: turbineVentGasFlow
|
||||
|
||||
constants.mek = mek
|
||||
|
||||
--#endregion
|
||||
|
||||
|
||||
@@ -24,6 +24,21 @@ function crash.set_env(application, version)
|
||||
ver = version
|
||||
end
|
||||
|
||||
-- log environment versions
|
||||
---@param log_msg function log function to use
|
||||
local function log_versions(log_msg)
|
||||
log_msg(util.c("RUNTIME: ", _HOST))
|
||||
log_msg(util.c("LUA VERSION: ", _VERSION))
|
||||
log_msg(util.c("APPLICATION: ", app))
|
||||
log_msg(util.c("FIRMWARE VERSION: ", ver))
|
||||
log_msg(util.c("COMMS VERSION: ", comms.version))
|
||||
if has_graphics then log_msg(util.c("GRAPHICS VERSION: ", core.version)) end
|
||||
if has_lockbox then log_msg(util.c("LOCKBOX VERSION: ", lockbox.version)) end
|
||||
end
|
||||
|
||||
-- when running with debug logs, log the useful information that the crash handler knows
|
||||
function crash.dbg_log_env() log_versions(log.debug) end
|
||||
|
||||
-- handle a crash error
|
||||
---@param error string error message
|
||||
function crash.handler(error)
|
||||
@@ -31,13 +46,7 @@ function crash.handler(error)
|
||||
log.info("=====> FATAL SOFTWARE FAULT <=====")
|
||||
log.fatal(error)
|
||||
log.info("----------------------------------")
|
||||
log.info(util.c("RUNTIME: ", _HOST))
|
||||
log.info(util.c("LUA VERSION: ", _VERSION))
|
||||
log.info(util.c("APPLICATION: ", app))
|
||||
log.info(util.c("FIRMWARE VERSION: ", ver))
|
||||
log.info(util.c("COMMS VERSION: ", comms.version))
|
||||
if has_graphics then log.info(util.c("GRAPHICS VERSION: ", core.version)) end
|
||||
if has_lockbox then log.info(util.c("LOCKBOX VERSION: ", lockbox.version)) end
|
||||
log_versions(log.info)
|
||||
log.info("----------------------------------")
|
||||
log.info(debug.traceback("--- begin debug trace ---", 1))
|
||||
log.info("--- end debug trace ---")
|
||||
|
||||
@@ -13,6 +13,7 @@ local MODE = { APPEND = 0, NEW = 1 }
|
||||
log.MODE = MODE
|
||||
|
||||
local logger = {
|
||||
not_ready = true,
|
||||
path = "/log.txt",
|
||||
mode = MODE.APPEND,
|
||||
debug = false,
|
||||
@@ -32,6 +33,8 @@ local free_space = fs.getFreeSpace
|
||||
-- private log write function
|
||||
---@param msg string
|
||||
local function _log(msg)
|
||||
if logger.not_ready then return end
|
||||
|
||||
local out_of_space = false
|
||||
local time_stamp = os.date("[%c] ")
|
||||
local stamped = time_stamp .. util.strval(msg)
|
||||
@@ -94,6 +97,8 @@ function log.init(path, write_mode, include_debug, dmesg_redirect)
|
||||
else
|
||||
logger.dmesg_out = term.current()
|
||||
end
|
||||
|
||||
logger.not_ready = false
|
||||
end
|
||||
|
||||
-- close the log file handle
|
||||
|
||||
@@ -7,7 +7,7 @@ local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local md5 = require("lockbox.digest.md5")
|
||||
local sha256 = require("lockbox.digest.sha2_256")
|
||||
local sha1 = require("lockbox.digest.sha1")
|
||||
local pbkdf2 = require("lockbox.kdf.pbkdf2")
|
||||
local hmac = require("lockbox.mac.hmac")
|
||||
local stream = require("lockbox.util.stream")
|
||||
@@ -31,12 +31,12 @@ function network.init_mac(passkey)
|
||||
local key_deriv = pbkdf2()
|
||||
|
||||
-- setup PBKDF2
|
||||
key_deriv.setPassword(passkey)
|
||||
key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha1))
|
||||
key_deriv.setBlockLen(20)
|
||||
key_deriv.setDKeyLen(20)
|
||||
key_deriv.setIterations(256)
|
||||
key_deriv.setSalt("pepper")
|
||||
key_deriv.setIterations(32)
|
||||
key_deriv.setBlockLen(8)
|
||||
key_deriv.setDKeyLen(16)
|
||||
key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha256))
|
||||
key_deriv.setPassword(passkey)
|
||||
key_deriv.finish()
|
||||
|
||||
c_eng.key = array.fromHex(key_deriv.asHex())
|
||||
@@ -53,6 +53,11 @@ function network.init_mac(passkey)
|
||||
return init_time
|
||||
end
|
||||
|
||||
-- de-initialize message authentication system
|
||||
function network.deinit_mac()
|
||||
c_eng.key, c_eng.hmac = nil, nil
|
||||
end
|
||||
|
||||
-- generate HMAC of message
|
||||
---@nodiscard
|
||||
---@param message string initial value concatenated with ciphertext
|
||||
|
||||
@@ -9,7 +9,7 @@ local util = require("scada-common.util")
|
||||
local ppm = {}
|
||||
|
||||
local ACCESS_FAULT = nil ---@type nil
|
||||
local UNDEFINED_FIELD = "undefined field"
|
||||
local UNDEFINED_FIELD = "__PPM_UNDEF_FIELD__"
|
||||
local VIRTUAL_DEVICE_TYPE = "ppm_vdev"
|
||||
|
||||
ppm.ACCESS_FAULT = ACCESS_FAULT
|
||||
@@ -51,11 +51,13 @@ local function peri_init(iface)
|
||||
self.device = peripheral.wrap(iface)
|
||||
end
|
||||
|
||||
-- initialization process (re-map)
|
||||
|
||||
for key, func in pairs(self.device) do
|
||||
self.fault_counts[key] = 0
|
||||
self.device[key] = function (...)
|
||||
-- create a protected version of a peripheral function call
|
||||
---@nodiscard
|
||||
---@param key string function name
|
||||
---@param func function function
|
||||
---@return function method protected version of the function
|
||||
local function protect_peri_function(key, func)
|
||||
return function (...)
|
||||
local return_table = table.pack(pcall(func, ...))
|
||||
|
||||
local status = return_table[1]
|
||||
@@ -85,20 +87,24 @@ local function peri_init(iface)
|
||||
count_str = " [" .. self.fault_counts[key] .. " total faults]"
|
||||
end
|
||||
|
||||
log.error(util.c("PPM: protected ", key, "() -> ", result, count_str))
|
||||
log.error(util.c("PPM: [@", iface, "] protected ", key, "() -> ", result, count_str))
|
||||
end
|
||||
|
||||
self.fault_counts[key] = self.fault_counts[key] + 1
|
||||
|
||||
if result == "Terminated" then
|
||||
ppm_sys.terminate = true
|
||||
end
|
||||
if result == "Terminated" then ppm_sys.terminate = true end
|
||||
|
||||
return ACCESS_FAULT
|
||||
return ACCESS_FAULT, result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- initialization process (re-map)
|
||||
for key, func in pairs(self.device) do
|
||||
self.fault_counts[key] = 0
|
||||
self.device[key] = protect_peri_function(key, func)
|
||||
end
|
||||
|
||||
-- fault management & monitoring functions
|
||||
|
||||
local function clear_fault() self.faulted = false end
|
||||
@@ -131,40 +137,53 @@ local function peri_init(iface)
|
||||
|
||||
local mt = {
|
||||
__index = function (_, key)
|
||||
-- this will continuously be counting calls here as faults
|
||||
-- unlike other functions, faults here can't be cleared as it is just not defined
|
||||
if self.fault_counts[key] == nil then
|
||||
-- try to find the function in case it was added (multiblock formed)
|
||||
local funcs = peripheral.wrap(iface)
|
||||
if (type(funcs) == "table") and (type(funcs[key]) == "function") then
|
||||
-- add this function then return it
|
||||
self.fault_counts[key] = 0
|
||||
self.device[key] = protect_peri_function(key, funcs[key])
|
||||
|
||||
log.info(util.c("PPM: [@", iface, "] initialized previously undefined field ", key, "()"))
|
||||
|
||||
return self.device[key]
|
||||
end
|
||||
|
||||
-- function failed
|
||||
self.faulted = true
|
||||
self.last_fault = UNDEFINED_FIELD
|
||||
-- function still missing, return an undefined function handler
|
||||
-- note: code should avoid storing functions for multiblocks and instead try to index them again
|
||||
return (function ()
|
||||
-- this will continuously be counting calls here as faults
|
||||
if self.fault_counts[key] == nil then self.fault_counts[key] = 0 end
|
||||
|
||||
ppm_sys.faulted = true
|
||||
ppm_sys.last_fault = UNDEFINED_FIELD
|
||||
-- function failed
|
||||
self.faulted = true
|
||||
self.last_fault = UNDEFINED_FIELD
|
||||
|
||||
if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
|
||||
local count_str = ""
|
||||
if self.fault_counts[key] > 0 then
|
||||
count_str = " [" .. self.fault_counts[key] .. " total calls]"
|
||||
ppm_sys.faulted = true
|
||||
ppm_sys.last_fault = UNDEFINED_FIELD
|
||||
|
||||
if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
|
||||
local count_str = ""
|
||||
if self.fault_counts[key] > 0 then
|
||||
count_str = " [" .. self.fault_counts[key] .. " total calls]"
|
||||
end
|
||||
|
||||
log.error(util.c("PPM: [@", iface, "] caught undefined function ", key, "()", count_str))
|
||||
end
|
||||
|
||||
log.error(util.c("PPM: caught undefined function ", key, "()", count_str))
|
||||
end
|
||||
self.fault_counts[key] = self.fault_counts[key] + 1
|
||||
|
||||
self.fault_counts[key] = self.fault_counts[key] + 1
|
||||
|
||||
return (function () return ACCESS_FAULT end)
|
||||
return ACCESS_FAULT, UNDEFINED_FIELD
|
||||
end)
|
||||
end
|
||||
}
|
||||
|
||||
setmetatable(self.device, mt)
|
||||
|
||||
return {
|
||||
type = self.type,
|
||||
dev = self.device
|
||||
}
|
||||
---@class ppm_entry
|
||||
local entry = { type = self.type, dev = self.device }
|
||||
|
||||
return entry
|
||||
end
|
||||
|
||||
----------------------
|
||||
@@ -300,6 +319,17 @@ function ppm.handle_unmount(iface)
|
||||
return pm_type, pm_dev
|
||||
end
|
||||
|
||||
-- log all mounts, to be used if `ppm.mount_all` is called before logging is ready
|
||||
function ppm.log_mounts()
|
||||
for iface, mount in pairs(ppm_sys.mounts) do
|
||||
log.info(util.c("PPM: had found a ", mount.type, " (", iface, ")"))
|
||||
end
|
||||
|
||||
if util.table_len(ppm_sys.mounts) == 0 then
|
||||
log.warning("PPM: no devices had been found")
|
||||
end
|
||||
end
|
||||
|
||||
-- GENERAL ACCESSORS --
|
||||
|
||||
-- list all available peripherals
|
||||
@@ -310,7 +340,11 @@ function ppm.list_avail() return peripheral.getNames() end
|
||||
-- list mounted peripherals
|
||||
---@nodiscard
|
||||
---@return table mounts
|
||||
function ppm.list_mounts() return ppm_sys.mounts end
|
||||
function ppm.list_mounts()
|
||||
local list = {}
|
||||
for k, v in pairs(ppm_sys.mounts) do list[k] = v end
|
||||
return list
|
||||
end
|
||||
|
||||
-- get a mounted peripheral side/interface by device table
|
||||
---@nodiscard
|
||||
@@ -417,4 +451,15 @@ function ppm.get_monitor_list()
|
||||
return list
|
||||
end
|
||||
|
||||
-- HELPER FUNCTIONS
|
||||
|
||||
-- get the block size of a monitor given its width and height <b>at a text scale of 0.5</b>
|
||||
---@nodiscard
|
||||
---@param width integer character width
|
||||
---@param height integer character height
|
||||
---@return integer block_width, integer block_height
|
||||
function ppm.monitor_block_size(width, height)
|
||||
return math.floor((width - 15) / 21) + 1, math.floor((height - 10) / 14) + 1
|
||||
end
|
||||
|
||||
return ppm
|
||||
|
||||
@@ -52,6 +52,8 @@ local IO_PORT = {
|
||||
-- facility
|
||||
F_ALARM = 7, -- active high, facility-wide alarm (any high priority unit alarm)
|
||||
F_ALARM_ANY = 8, -- active high, any alarm regardless of priority
|
||||
F_MATRIX_LOW = 27, -- active high, induction matrix charge low
|
||||
F_MATRIX_HIGH = 28, -- active high, induction matrix charge high
|
||||
|
||||
-- waste
|
||||
WASTE_PU = 9, -- active low, waste -> plutonium -> pellets route
|
||||
@@ -75,7 +77,12 @@ local IO_PORT = {
|
||||
|
||||
-- unit outputs
|
||||
U_ALARM = 25, -- active high, unit alarm
|
||||
U_EMER_COOL = 26 -- active low, emergency coolant control
|
||||
U_EMER_COOL = 26, -- active low, emergency coolant control
|
||||
|
||||
-- analog outputs --
|
||||
|
||||
-- facility
|
||||
F_MATRIX_CHG = 29 -- analog charge level of the induction matrix
|
||||
}
|
||||
|
||||
rsio.IO_LVL = IO_LVL
|
||||
@@ -83,45 +90,74 @@ rsio.IO_DIR = IO_DIR
|
||||
rsio.IO_MODE = IO_MODE
|
||||
rsio.IO = IO_PORT
|
||||
|
||||
rsio.NUM_PORTS = 29
|
||||
rsio.NUM_DIG_PORTS = 28
|
||||
rsio.NUM_ANA_PORTS = 1
|
||||
|
||||
-- self checks
|
||||
|
||||
assert(rsio.NUM_PORTS == (rsio.NUM_DIG_PORTS + rsio.NUM_ANA_PORTS), "port counts inconsistent")
|
||||
|
||||
local dup_chk = {}
|
||||
for _, v in pairs(IO_PORT) do
|
||||
assert(dup_chk[v] ~= true, "duplicate in port list")
|
||||
dup_chk[v] = true
|
||||
end
|
||||
|
||||
assert(#dup_chk == rsio.NUM_PORTS, "port list malformed")
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Utility Functions
|
||||
--#region Utility Functions and Attribute Tables
|
||||
|
||||
local IO = IO_PORT
|
||||
|
||||
-- list of all port names
|
||||
local PORT_NAMES = {}
|
||||
for k, v in pairs(IO) do PORT_NAMES[v] = k end
|
||||
|
||||
-- list of all port I/O modes
|
||||
local MODES = {
|
||||
[IO.F_SCRAM] = IO_MODE.DIGITAL_IN,
|
||||
[IO.F_ACK] = IO_MODE.DIGITAL_IN,
|
||||
[IO.R_SCRAM] = IO_MODE.DIGITAL_IN,
|
||||
[IO.R_RESET] = IO_MODE.DIGITAL_IN,
|
||||
[IO.R_ENABLE] = IO_MODE.DIGITAL_IN,
|
||||
[IO.U_ACK] = IO_MODE.DIGITAL_IN,
|
||||
[IO.F_ALARM] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.F_ALARM_ANY] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.F_MATRIX_LOW] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.F_MATRIX_HIGH] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.WASTE_PU] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.WASTE_PO] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.WASTE_POPL] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.WASTE_AM] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_ACTIVE] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_AUTO_CTRL] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_SCRAMMED] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_AUTO_SCRAM] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_HIGH_DMG] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_HIGH_TEMP] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_LOW_COOLANT] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_EXCESS_HC] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_EXCESS_WS] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_INSUFF_FUEL] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_PLC_FAULT] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_PLC_TIMEOUT] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.U_ALARM] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.U_EMER_COOL] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.F_MATRIX_CHG] = IO_MODE.ANALOG_OUT
|
||||
}
|
||||
|
||||
assert(rsio.NUM_PORTS == #PORT_NAMES, "port names length incorrect")
|
||||
assert(rsio.NUM_PORTS == #MODES, "modes length incorrect")
|
||||
|
||||
-- port to string
|
||||
---@nodiscard
|
||||
---@param port IO_PORT
|
||||
function rsio.to_string(port)
|
||||
local names = {
|
||||
"F_SCRAM",
|
||||
"F_ACK",
|
||||
"R_SCRAM",
|
||||
"R_RESET",
|
||||
"R_ENABLE",
|
||||
"U_ACK",
|
||||
"F_ALARM",
|
||||
"F_ALARM_ANY",
|
||||
"WASTE_PU",
|
||||
"WASTE_PO",
|
||||
"WASTE_POPL",
|
||||
"WASTE_AM",
|
||||
"R_ACTIVE",
|
||||
"R_AUTO_CTRL",
|
||||
"R_SCRAMMED",
|
||||
"R_AUTO_SCRAM",
|
||||
"R_HIGH_DMG",
|
||||
"R_HIGH_TEMP",
|
||||
"R_LOW_COOLANT",
|
||||
"R_EXCESS_HC",
|
||||
"R_EXCESS_WS",
|
||||
"R_INSUFF_FUEL",
|
||||
"R_PLC_FAULT",
|
||||
"R_PLC_TIMEOUT",
|
||||
"U_ALARM",
|
||||
"U_EMER_COOL"
|
||||
}
|
||||
|
||||
if util.is_int(port) and port > 0 and port <= #names then
|
||||
return names[port]
|
||||
if util.is_int(port) and port > 0 and port <= #PORT_NAMES then
|
||||
return PORT_NAMES[port]
|
||||
else
|
||||
return "UNKNOWN"
|
||||
end
|
||||
@@ -136,105 +172,61 @@ local function _O_ACTIVE_LOW(active) if active then return IO_LVL.LOW else retur
|
||||
|
||||
-- I/O mappings to I/O function and I/O mode
|
||||
local RS_DIO_MAP = {
|
||||
-- F_SCRAM
|
||||
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN },
|
||||
-- F_ACK
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
|
||||
[IO.F_SCRAM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN },
|
||||
[IO.F_ACK] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
|
||||
|
||||
-- R_SCRAM
|
||||
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN },
|
||||
-- R_RESET
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
|
||||
-- R_ENABLE
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
|
||||
[IO.R_SCRAM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN },
|
||||
[IO.R_RESET] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
|
||||
[IO.R_ENABLE] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
|
||||
|
||||
-- U_ACK
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
|
||||
[IO.U_ACK] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
|
||||
|
||||
-- F_ALARM
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- F_ALARM_ANY
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.F_ALARM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.F_ALARM_ANY] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.F_MATRIX_LOW] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.F_MATRIX_HIGH] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
|
||||
-- WASTE_PU
|
||||
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
|
||||
-- WASTE_PO
|
||||
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
|
||||
-- WASTE_POPL
|
||||
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
|
||||
-- WASTE_AM
|
||||
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
|
||||
[IO.WASTE_PU] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
|
||||
[IO.WASTE_PO] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
|
||||
[IO.WASTE_POPL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
|
||||
[IO.WASTE_AM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
|
||||
|
||||
-- R_ACTIVE
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_AUTO_CTRL
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_SCRAMMED
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_AUTO_SCRAM
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_HIGH_DMG
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_HIGH_TEMP
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_LOW_COOLANT
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_EXCESS_HC
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_EXCESS_WS
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_INSUFF_FUEL
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_PLC_FAULT
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_PLC_TIMEOUT
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_ACTIVE] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_AUTO_CTRL] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_SCRAMMED] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_AUTO_SCRAM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_HIGH_DMG] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_HIGH_TEMP] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_LOW_COOLANT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_EXCESS_HC] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_EXCESS_WS] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_INSUFF_FUEL] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_PLC_FAULT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_PLC_TIMEOUT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
|
||||
-- U_ALARM
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- U_EMER_COOL
|
||||
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }
|
||||
[IO.U_ALARM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.U_EMER_COOL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }
|
||||
}
|
||||
|
||||
assert(rsio.NUM_DIG_PORTS == #RS_DIO_MAP, "RS_DIO_MAP length incorrect")
|
||||
|
||||
-- get the I/O direction of a port
|
||||
---@nodiscard
|
||||
---@param port IO_PORT
|
||||
---@return IO_DIR
|
||||
function rsio.get_io_dir(port)
|
||||
if rsio.is_valid_port(port) then
|
||||
return util.trinary(MODES[port] == IO_MODE.DIGITAL_OUT or MODES[port] == IO_MODE.ANALOG_OUT, IO_DIR.OUT, IO_DIR.IN)
|
||||
else return IO_DIR.IN end
|
||||
end
|
||||
|
||||
-- get the mode of a port
|
||||
---@nodiscard
|
||||
---@param port IO_PORT
|
||||
---@return IO_MODE
|
||||
function rsio.get_io_mode(port)
|
||||
local modes = {
|
||||
IO_MODE.DIGITAL_IN, -- F_SCRAM
|
||||
IO_MODE.DIGITAL_IN, -- F_ACK
|
||||
IO_MODE.DIGITAL_IN, -- R_SCRAM
|
||||
IO_MODE.DIGITAL_IN, -- R_RESET
|
||||
IO_MODE.DIGITAL_IN, -- R_ENABLE
|
||||
IO_MODE.DIGITAL_IN, -- U_ACK
|
||||
IO_MODE.DIGITAL_OUT, -- F_ALARM
|
||||
IO_MODE.DIGITAL_OUT, -- F_ALARM_ANY
|
||||
IO_MODE.DIGITAL_OUT, -- WASTE_PU
|
||||
IO_MODE.DIGITAL_OUT, -- WASTE_PO
|
||||
IO_MODE.DIGITAL_OUT, -- WASTE_POPL
|
||||
IO_MODE.DIGITAL_OUT, -- WASTE_AM
|
||||
IO_MODE.DIGITAL_OUT, -- R_ACTIVE
|
||||
IO_MODE.DIGITAL_OUT, -- R_AUTO_CTRL
|
||||
IO_MODE.DIGITAL_OUT, -- R_SCRAMMED
|
||||
IO_MODE.DIGITAL_OUT, -- R_AUTO_SCRAM
|
||||
IO_MODE.DIGITAL_OUT, -- R_HIGH_DMG
|
||||
IO_MODE.DIGITAL_OUT, -- R_HIGH_TEMP
|
||||
IO_MODE.DIGITAL_OUT, -- R_LOW_COOLANT
|
||||
IO_MODE.DIGITAL_OUT, -- R_EXCESS_HC
|
||||
IO_MODE.DIGITAL_OUT, -- R_EXCESS_WS
|
||||
IO_MODE.DIGITAL_OUT, -- R_INSUFF_FUEL
|
||||
IO_MODE.DIGITAL_OUT, -- R_PLC_FAULT
|
||||
IO_MODE.DIGITAL_OUT, -- R_PLC_TIMEOUT
|
||||
IO_MODE.DIGITAL_OUT, -- U_ALARM
|
||||
IO_MODE.DIGITAL_OUT -- U_EMER_COOL
|
||||
}
|
||||
|
||||
if util.is_int(port) and port > 0 and port <= #modes then
|
||||
return modes[port]
|
||||
else
|
||||
return IO_MODE.ANALOG_IN
|
||||
end
|
||||
if rsio.is_valid_port(port) then return MODES[port]
|
||||
else return IO_MODE.ANALOG_IN end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
@@ -248,7 +240,7 @@ local RS_SIDES = rs.getSides()
|
||||
---@param port IO_PORT
|
||||
---@return boolean valid
|
||||
function rsio.is_valid_port(port)
|
||||
return util.is_int(port) and (port > 0) and (port <= IO_PORT.U_EMER_COOL)
|
||||
return util.is_int(port) and port > 0 and port <= rsio.NUM_PORTS
|
||||
end
|
||||
|
||||
-- check if a side is valid
|
||||
@@ -266,16 +258,35 @@ end
|
||||
|
||||
-- check if a color is a valid single color
|
||||
---@nodiscard
|
||||
---@param color integer
|
||||
---@param color any
|
||||
---@return boolean valid
|
||||
function rsio.is_color(color)
|
||||
return util.is_int(color) and (color > 0) and (_B_AND(color, (color - 1)) == 0)
|
||||
end
|
||||
|
||||
-- color to string
|
||||
---@nodiscard
|
||||
---@param color color
|
||||
---@return string
|
||||
function rsio.color_name(color)
|
||||
local color_name_map = { [colors.red] = "red", [colors.orange] = "orange", [colors.yellow] = "yellow", [colors.lime] = "lime", [colors.green] = "green", [colors.cyan] = "cyan", [colors.lightBlue] = "lightBlue", [colors.blue] = "blue", [colors.purple] = "purple", [colors.magenta] = "magenta", [colors.pink] = "pink", [colors.white] = "white", [colors.lightGray] = "lightGray", [colors.gray] = "gray", [colors.black] = "black", [colors.brown] = "brown" }
|
||||
|
||||
if rsio.is_color(color) then
|
||||
return color_name_map[color]
|
||||
else return "unknown" end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Digital I/O
|
||||
|
||||
-- check if a port is digital
|
||||
---@nodiscard
|
||||
---@param port IO_PORT
|
||||
function rsio.is_digital(port)
|
||||
return rsio.is_valid_port(port) and (MODES[port] == IO_MODE.DIGITAL_IN or MODES[port] == IO_MODE.DIGITAL_OUT)
|
||||
end
|
||||
|
||||
-- get digital I/O level reading from a redstone boolean input value
|
||||
---@nodiscard
|
||||
---@param rs_value boolean raw value from redstone
|
||||
@@ -296,7 +307,7 @@ function rsio.digital_write(level) return level == IO_LVL.HIGH end
|
||||
---@param active boolean state to convert to logic level
|
||||
---@return IO_LVL|false
|
||||
function rsio.digital_write_active(port, active)
|
||||
if (not util.is_int(port)) or (port < IO_PORT.F_ALARM) or (port > IO_PORT.U_EMER_COOL) then
|
||||
if not rsio.is_digital(port) then
|
||||
return false
|
||||
else
|
||||
return RS_DIO_MAP[port]._out(active)
|
||||
@@ -309,9 +320,7 @@ end
|
||||
---@param level IO_LVL logic level
|
||||
---@return boolean|nil state true for active, false for inactive, or nil if invalid port or level provided
|
||||
function rsio.digital_is_active(port, level)
|
||||
if not util.is_int(port) then
|
||||
return nil
|
||||
elseif level == IO_LVL.FLOATING or level == IO_LVL.DISCONNECT then
|
||||
if (not rsio.is_digital(port)) or level == IO_LVL.FLOATING or level == IO_LVL.DISCONNECT then
|
||||
return nil
|
||||
else
|
||||
return RS_DIO_MAP[port]._in(level)
|
||||
@@ -322,6 +331,13 @@ end
|
||||
|
||||
--#region Analog I/O
|
||||
|
||||
-- check if a port is analog
|
||||
---@nodiscard
|
||||
---@param port IO_PORT
|
||||
function rsio.is_analog(port)
|
||||
return rsio.is_valid_port(port) and (MODES[port] == IO_MODE.ANALOG_IN or MODES[port] == IO_MODE.ANALOG_OUT)
|
||||
end
|
||||
|
||||
-- read an analog value scaled from min to max
|
||||
---@nodiscard
|
||||
---@param rs_value number redstone reading (0 to 15)
|
||||
@@ -338,7 +354,7 @@ end
|
||||
---@param value number value to write (from min to max range)
|
||||
---@param min number minimum of range
|
||||
---@param max number maximum of range
|
||||
---@return number rs_value scaled redstone reading (0 to 15)
|
||||
---@return integer rs_value scaled redstone reading (0 to 15)
|
||||
function rsio.analog_write(value, min, max)
|
||||
local scaled_value = (value - min) / (max - min)
|
||||
return math.floor(scaled_value * 15)
|
||||
|
||||
@@ -63,7 +63,7 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
|
||||
|
||||
---@class rtu_advertisement
|
||||
---@field type RTU_UNIT_TYPE
|
||||
---@field index integer
|
||||
---@field index integer|false
|
||||
---@field reactor integer
|
||||
---@field rsio table|nil
|
||||
|
||||
@@ -252,6 +252,14 @@ types.ALARM_STATE_NAMES = {
|
||||
-- STRING TYPES --
|
||||
--#region
|
||||
|
||||
---@alias side
|
||||
---|"top"
|
||||
---|"bottom"
|
||||
---|"left"
|
||||
---|"right"
|
||||
---|"front"
|
||||
---|"back"
|
||||
|
||||
---@alias os_event
|
||||
---| "alarm"
|
||||
---| "char"
|
||||
|
||||
@@ -4,19 +4,30 @@
|
||||
|
||||
local cc_strings = require("cc.strings")
|
||||
|
||||
local math = math
|
||||
local string = string
|
||||
local table = table
|
||||
local os = os
|
||||
|
||||
local getmetatable = getmetatable
|
||||
local print = print
|
||||
local tostring = tostring
|
||||
local type = type
|
||||
|
||||
local t_concat = table.concat
|
||||
local t_insert = table.insert
|
||||
local t_pack = table.pack
|
||||
|
||||
---@class util
|
||||
local util = {}
|
||||
|
||||
-- scada-common version
|
||||
util.version = "1.1.3"
|
||||
|
||||
-- ENVIRONMENT CONSTANTS --
|
||||
util.version = "1.3.0"
|
||||
|
||||
util.TICK_TIME_S = 0.05
|
||||
util.TICK_TIME_MS = 50
|
||||
|
||||
-- OPERATORS --
|
||||
--#region
|
||||
--#region OPERATORS
|
||||
|
||||
-- trinary operator
|
||||
---@nodiscard
|
||||
@@ -30,37 +41,29 @@ end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- PRINT --
|
||||
--#region
|
||||
--#region PRINT
|
||||
|
||||
local p_time = "[%H:%M:%S] "
|
||||
|
||||
-- print
|
||||
---@param message any
|
||||
function util.print(message)
|
||||
term.write(tostring(message))
|
||||
end
|
||||
function util.print(message) term.write(tostring(message)) end
|
||||
|
||||
-- print line
|
||||
---@param message any
|
||||
function util.println(message)
|
||||
print(tostring(message))
|
||||
end
|
||||
function util.println(message) print(tostring(message)) end
|
||||
|
||||
-- timestamped print
|
||||
---@param message any
|
||||
function util.print_ts(message)
|
||||
term.write(os.date("[%H:%M:%S] ") .. tostring(message))
|
||||
end
|
||||
function util.print_ts(message) term.write(os.date(p_time) .. tostring(message)) end
|
||||
|
||||
-- timestamped print line
|
||||
---@param message any
|
||||
function util.println_ts(message)
|
||||
print(os.date("[%H:%M:%S] ") .. tostring(message))
|
||||
end
|
||||
function util.println_ts(message) print(os.date(p_time) .. tostring(message)) end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- STRING TOOLS --
|
||||
--#region
|
||||
--#region STRING TOOLS
|
||||
|
||||
-- get a value as a string
|
||||
---@nodiscard
|
||||
@@ -68,21 +71,29 @@ end
|
||||
---@return string
|
||||
function util.strval(val)
|
||||
local t = type(val)
|
||||
if t == "string" then return val end
|
||||
-- this depends on Lua short-circuiting the or check for metatables (note: metatables won't have metatables)
|
||||
if (t == "table" and (getmetatable(val) == nil or getmetatable(val).__tostring == nil)) or t == "function" then
|
||||
return "[" .. tostring(val) .. "]"
|
||||
else
|
||||
return tostring(val)
|
||||
end
|
||||
return t_concat{"[", tostring(val), "]"}
|
||||
else return tostring(val) end
|
||||
end
|
||||
|
||||
-- tokenize a string by a separator<br>
|
||||
-- does not behave exactly like C's strtok
|
||||
---@param str string string to tokenize
|
||||
---@param sep string separator to tokenize by
|
||||
---@return table token_list
|
||||
function util.strtok(str, sep)
|
||||
local list = {}
|
||||
for part in string.gmatch(str, "([^" .. sep .. "]+)") do t_insert(list, part) end
|
||||
return list
|
||||
end
|
||||
|
||||
-- repeat a space n times
|
||||
---@nodiscard
|
||||
---@param n integer
|
||||
---@return string
|
||||
function util.spaces(n)
|
||||
return string.rep(" ", n)
|
||||
end
|
||||
function util.spaces(n) return string.rep(" ", n) end
|
||||
|
||||
-- pad text to a minimum width
|
||||
---@nodiscard
|
||||
@@ -94,7 +105,7 @@ function util.pad(str, n)
|
||||
local lpad = math.floor((n - len) / 2)
|
||||
local rpad = (n - len) - lpad
|
||||
|
||||
return util.spaces(lpad) .. str .. util.spaces(rpad)
|
||||
return t_concat{util.spaces(lpad), str, util.spaces(rpad)}
|
||||
end
|
||||
|
||||
-- wrap a string into a table of lines
|
||||
@@ -104,19 +115,14 @@ end
|
||||
---@return table lines
|
||||
function util.strwrap(str, limit) return cc_strings.wrap(str, limit) end
|
||||
|
||||
-- luacheck: no unused args
|
||||
|
||||
-- concatenation with built-in to string
|
||||
---@nodiscard
|
||||
---@vararg any
|
||||
---@return string
|
||||
---@diagnostic disable-next-line: unused-vararg
|
||||
function util.concat(...)
|
||||
local str = ""
|
||||
|
||||
for _, v in ipairs(arg) do str = str .. util.strval(v) end
|
||||
|
||||
return str
|
||||
local args, strings = t_pack(...), {}
|
||||
for i = 1, args.n do strings[i] = util.strval(args[i]) end
|
||||
return t_concat(strings)
|
||||
end
|
||||
|
||||
-- alias
|
||||
@@ -126,12 +132,7 @@ util.c = util.concat
|
||||
---@nodiscard
|
||||
---@param format string
|
||||
---@vararg any
|
||||
---@diagnostic disable-next-line: unused-vararg
|
||||
function util.sprintf(format, ...)
|
||||
return string.format(format, table.unpack(arg))
|
||||
end
|
||||
|
||||
-- luacheck: unused args
|
||||
function util.sprintf(format, ...) return string.format(format, ...) end
|
||||
|
||||
-- format a number string with commas as the thousands separator<br>
|
||||
-- subtracts from spaces at the start if present for each comma used
|
||||
@@ -144,7 +145,7 @@ function util.comma_format(num)
|
||||
local i = 1
|
||||
|
||||
while i > 0 do
|
||||
formatted, i = formatted:gsub("^(%s-%d+)(%d%d%d)", '%1,%2')
|
||||
formatted, i = formatted:gsub("^(%s-%d+)(%d%d%d)", "%1,%2")
|
||||
if i > 0 then commas = commas + 1 end
|
||||
end
|
||||
|
||||
@@ -158,58 +159,52 @@ end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- MATH --
|
||||
--#region
|
||||
--#region MATH
|
||||
|
||||
-- is a value an integer
|
||||
---@nodiscard
|
||||
---@param x any value
|
||||
---@return boolean is_integer if the number is an integer
|
||||
function util.is_int(x)
|
||||
return type(x) == "number" and x == math.floor(x)
|
||||
end
|
||||
---@return boolean is_integer
|
||||
function util.is_int(x) return type(x) == "number" and x == math.floor(x) end
|
||||
|
||||
-- get the sign of a number
|
||||
---@nodiscard
|
||||
---@param x number value
|
||||
---@return integer sign (-1 for < 0, 1 otherwise)
|
||||
function util.sign(x)
|
||||
return util.trinary(x < 0, -1, 1)
|
||||
end
|
||||
function util.sign(x) return util.trinary(x < 0, -1, 1) end
|
||||
|
||||
-- round a number to an integer
|
||||
---@nodiscard
|
||||
---@return integer rounded
|
||||
function util.round(x)
|
||||
return math.floor(x + 0.5)
|
||||
end
|
||||
function util.round(x) return math.floor(x + 0.5) end
|
||||
|
||||
-- get a new moving average object
|
||||
---@nodiscard
|
||||
---@param length integer history length
|
||||
---@param default number value to fill history with for first call to compute()
|
||||
function util.mov_avg(length, default)
|
||||
function util.mov_avg(length)
|
||||
local data = {}
|
||||
local index = 1
|
||||
local last_t = 0 ---@type number|nil
|
||||
local last_t = 0 ---@type number|nil
|
||||
|
||||
---@class moving_average
|
||||
local public = {}
|
||||
|
||||
-- reset all to a given value
|
||||
---@param x number value
|
||||
-- reset all to a given value, or clear all data if no value is given
|
||||
---@param x number? value
|
||||
function public.reset(x)
|
||||
index = 1
|
||||
data = {}
|
||||
for _ = 1, length do table.insert(data, x) end
|
||||
|
||||
if x then
|
||||
for _ = 1, length do t_insert(data, x) end
|
||||
end
|
||||
end
|
||||
|
||||
-- record a new value
|
||||
---@param x number new value
|
||||
---@param t number? optional last update time to prevent duplicated entries
|
||||
function public.record(x, t)
|
||||
if type(t) == "number" and last_t == t then
|
||||
return
|
||||
end
|
||||
if type(t) == "number" and last_t == t then return end
|
||||
|
||||
data[index] = x
|
||||
last_t = t
|
||||
@@ -222,33 +217,34 @@ function util.mov_avg(length, default)
|
||||
---@nodiscard
|
||||
---@return number average
|
||||
function public.compute()
|
||||
local sum = 0
|
||||
for i = 1, length do sum = sum + data[i] end
|
||||
return sum / length
|
||||
end
|
||||
if #data == 0 then return 0 end
|
||||
|
||||
public.reset(default)
|
||||
local sum = 0
|
||||
for i = 1, #data do
|
||||
sum = sum + data[i]
|
||||
end
|
||||
|
||||
return sum / #data
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- TIME --
|
||||
--#endregion
|
||||
|
||||
--#region TIME
|
||||
|
||||
-- current time
|
||||
---@nodiscard
|
||||
---@return integer milliseconds
|
||||
function util.time_ms()
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
return os.epoch('local')
|
||||
end
|
||||
function util.time_ms() return os.epoch("local") end
|
||||
|
||||
-- current time
|
||||
---@nodiscard
|
||||
---@return number seconds
|
||||
function util.time_s()
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
return os.epoch('local') / 1000.0
|
||||
end
|
||||
function util.time_s() return os.epoch("local") / 1000.0 end
|
||||
|
||||
-- current time
|
||||
---@nodiscard
|
||||
@@ -257,17 +253,14 @@ function util.time() return util.time_ms() end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- OS --
|
||||
--#region
|
||||
--#region OS
|
||||
|
||||
-- OS pull event raw wrapper with types
|
||||
---@nodiscard
|
||||
---@param target_event? string event to wait for
|
||||
---@return os_event event, any param1, any param2, any param3, any param4, any param5
|
||||
function util.pull_event(target_event)
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
return os.pullEventRaw(target_event)
|
||||
end
|
||||
function util.pull_event(target_event) return os.pullEventRaw(target_event) end
|
||||
|
||||
-- OS queue event raw wrapper with types
|
||||
---@param event os_event
|
||||
@@ -285,30 +278,25 @@ end
|
||||
---@nodiscard
|
||||
---@param t number timer duration in seconds
|
||||
---@return integer timer ID
|
||||
function util.start_timer(t)
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
return os.startTimer(t)
|
||||
end
|
||||
function util.start_timer(t) return os.startTimer(t) end
|
||||
|
||||
-- cancel an OS timer
|
||||
---@param timer integer timer ID
|
||||
function util.cancel_timer(timer)
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
os.cancelTimer(timer)
|
||||
end
|
||||
function util.cancel_timer(timer) os.cancelTimer(timer) end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- PARALLELIZATION --
|
||||
--#region
|
||||
--#region PARALLELIZATION
|
||||
|
||||
-- protected sleep call so we still are in charge of catching termination
|
||||
---@param t integer seconds
|
||||
-- protected sleep call so we still are in charge of catching termination<br>
|
||||
-- returns the result of pcall
|
||||
---@param t number seconds
|
||||
---@return boolean success, any result, any ...
|
||||
--- EVENT_CONSUMER: this function consumes events
|
||||
function util.psleep(t)
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
pcall(os.sleep, t)
|
||||
end
|
||||
function util.psleep(t) return pcall(os.sleep, t) end
|
||||
|
||||
-- no-op to provide a brief pause (1 tick) to yield<br>
|
||||
--- EVENT_CONSUMER: this function consumes events
|
||||
@@ -330,8 +318,7 @@ end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- TABLE UTILITIES --
|
||||
--#region
|
||||
--#region TABLE UTILITIES
|
||||
|
||||
-- delete elements from a table if the passed function returns false when passed a table element<br>
|
||||
-- put briefly: deletes elements that return false, keeps elements that return true
|
||||
@@ -369,10 +356,19 @@ function util.table_contains(t, element)
|
||||
return false
|
||||
end
|
||||
|
||||
-- count the length of a table, even if the values are not sequential or contain named keys
|
||||
---@nodiscard
|
||||
---@param t table
|
||||
---@return integer length
|
||||
function util.table_len(t)
|
||||
local n = 0
|
||||
for _, _ in pairs(t) do n = n + 1 end
|
||||
return n
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- MEKANISM POWER --
|
||||
--#region
|
||||
--#region MEKANISM POWER
|
||||
|
||||
-- convert Joules to FE
|
||||
---@nodiscard
|
||||
@@ -401,8 +397,7 @@ local function ZFE(fe) return fe / 1000000000000000000000.0 end -- how & why did
|
||||
---@param format? string format override
|
||||
---@return string str, string? unit
|
||||
function util.power_format(fe, combine_label, format)
|
||||
local unit
|
||||
local value
|
||||
local unit, value
|
||||
|
||||
if type(format) ~= "string" then format = "%.2f" end
|
||||
|
||||
@@ -441,8 +436,7 @@ end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- UTILITY CLASSES --
|
||||
--#region
|
||||
--#region UTILITY CLASSES
|
||||
|
||||
-- WATCHDOG --
|
||||
|
||||
@@ -451,32 +445,25 @@ end
|
||||
---@nodiscard
|
||||
---@param timeout number timeout duration
|
||||
function util.new_watchdog(timeout)
|
||||
local self = {
|
||||
timeout = timeout,
|
||||
wd_timer = util.start_timer(timeout)
|
||||
}
|
||||
local self = { timeout = timeout, wd_timer = util.start_timer(timeout) }
|
||||
|
||||
---@class watchdog
|
||||
local public = {}
|
||||
|
||||
-- check if a timer is this watchdog
|
||||
---@nodiscard
|
||||
---@param timer number timer event timer ID
|
||||
---@param timer number event timer ID
|
||||
function public.is_timer(timer) return self.wd_timer == timer end
|
||||
|
||||
-- satiate the beast
|
||||
function public.feed()
|
||||
if self.wd_timer ~= nil then
|
||||
util.cancel_timer(self.wd_timer)
|
||||
end
|
||||
public.cancel()
|
||||
self.wd_timer = util.start_timer(self.timeout)
|
||||
end
|
||||
|
||||
-- cancel the watchdog
|
||||
function public.cancel()
|
||||
if self.wd_timer ~= nil then
|
||||
util.cancel_timer(self.wd_timer)
|
||||
end
|
||||
if self.wd_timer ~= nil then util.cancel_timer(self.wd_timer) end
|
||||
end
|
||||
|
||||
return public
|
||||
@@ -489,10 +476,7 @@ end
|
||||
---@nodiscard
|
||||
---@param period number clock period
|
||||
function util.new_clock(period)
|
||||
local self = {
|
||||
period = period,
|
||||
timer = nil
|
||||
}
|
||||
local self = { period = period, timer = nil }
|
||||
|
||||
---@class clock
|
||||
local public = {}
|
||||
@@ -525,6 +509,7 @@ function util.new_validator()
|
||||
function public.assert_type_str(value) valid = valid and type(value) == "string" end
|
||||
function public.assert_type_table(value) valid = valid and type(value) == "table" end
|
||||
|
||||
function public.assert(check) valid = valid and (check == true) end
|
||||
function public.assert_eq(check, expect) valid = valid and check == expect end
|
||||
function public.assert_min(check, min) valid = valid and check >= min end
|
||||
function public.assert_min_ex(check, min) valid = valid and check > min end
|
||||
@@ -533,7 +518,7 @@ function util.new_validator()
|
||||
function public.assert_range(check, min, max) valid = valid and check >= min and check <= max end
|
||||
function public.assert_range_ex(check, min, max) valid = valid and check > min and check < max end
|
||||
|
||||
function public.assert_channel(channel) valid = valid and type(channel) == "number" and channel >= 0 and channel <= 65535 end
|
||||
function public.assert_channel(channel) valid = valid and util.is_int(channel) and channel >= 0 and channel <= 65535 end
|
||||
|
||||
-- check if all assertions passed successfully
|
||||
---@nodiscard
|
||||
|
||||
22
startup.lua
22
startup.lua
@@ -1,30 +1,28 @@
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local BOOTLOADER_VERSION = "0.3"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
|
||||
local BOOTLOADER_VERSION = "1.0"
|
||||
|
||||
println("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION)
|
||||
println("BOOT> SCANNING FOR APPLICATIONS...")
|
||||
|
||||
local exit_code ---@type boolean
|
||||
|
||||
println_ts("BOOT> SCANNING FOR APPLICATIONS...")
|
||||
local exit_code
|
||||
|
||||
if fs.exists("reactor-plc/startup.lua") then
|
||||
println("BOOT> FOUND REACTOR PLC CODE: EXEC STARTUP")
|
||||
println("BOOT> EXEC REACTOR PLC STARTUP")
|
||||
exit_code = shell.execute("reactor-plc/startup")
|
||||
elseif fs.exists("rtu/startup.lua") then
|
||||
println("BOOT> FOUND RTU CODE: EXEC STARTUP")
|
||||
println("BOOT> EXEC RTU STARTUP")
|
||||
exit_code = shell.execute("rtu/startup")
|
||||
elseif fs.exists("supervisor/startup.lua") then
|
||||
println("BOOT> FOUND SUPERVISOR CODE: EXEC STARTUP")
|
||||
println("BOOT> EXEC SUPERVISOR STARTUP")
|
||||
exit_code = shell.execute("supervisor/startup")
|
||||
elseif fs.exists("coordinator/startup.lua") then
|
||||
println("BOOT> FOUND COORDINATOR CODE: EXEC STARTUP")
|
||||
println("BOOT> EXEC COORDINATOR STARTUP")
|
||||
exit_code = shell.execute("coordinator/startup")
|
||||
elseif fs.exists("pocket/startup.lua") then
|
||||
println("BOOT> FOUND POCKET CODE: EXEC STARTUP")
|
||||
println("BOOT> EXEC POCKET STARTUP")
|
||||
exit_code = shell.execute("pocket/startup")
|
||||
else
|
||||
println("BOOT> NO SCADA STARTUP FOUND")
|
||||
@@ -32,6 +30,6 @@ else
|
||||
return false
|
||||
end
|
||||
|
||||
if not exit_code then println_ts("BOOT> APPLICATION CRASHED") end
|
||||
if not exit_code then println("BOOT> APPLICATION CRASHED") end
|
||||
|
||||
return exit_code
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
local config = {}
|
||||
|
||||
-- supervisor comms channel
|
||||
config.SVR_CHANNEL = 16240
|
||||
-- PLC comms channel
|
||||
config.PLC_CHANNEL = 16241
|
||||
-- RTU/MODBUS comms channel
|
||||
config.RTU_CHANNEL = 16242
|
||||
-- coordinator comms channel
|
||||
config.CRD_CHANNEL = 16243
|
||||
-- pocket comms channel
|
||||
config.PKT_CHANNEL = 16244
|
||||
-- max trusted modem message distance
|
||||
-- (0 to disable check)
|
||||
config.TRUSTED_RANGE = 0
|
||||
-- time in seconds (>= 2) before assuming a remote
|
||||
-- device is no longer active
|
||||
config.PLC_TIMEOUT = 5
|
||||
config.RTU_TIMEOUT = 5
|
||||
config.CRD_TIMEOUT = 5
|
||||
config.PKT_TIMEOUT = 5
|
||||
-- facility authentication key
|
||||
-- (do NOT use one of your passwords)
|
||||
-- this enables verifying that messages are authentic
|
||||
-- all devices on this network must use this key
|
||||
-- config.AUTH_KEY = "SCADAfacility123"
|
||||
|
||||
-- expected number of reactors
|
||||
config.NUM_REACTORS = 4
|
||||
-- expected number of devices for each unit
|
||||
config.REACTOR_COOLING = {
|
||||
-- reactor unit 1
|
||||
{ BOILERS = 1, TURBINES = 1, TANK = false },
|
||||
-- reactor unit 2
|
||||
{ BOILERS = 1, TURBINES = 1, TANK = false },
|
||||
-- reactor unit 3
|
||||
{ BOILERS = 1, TURBINES = 1, TANK = false },
|
||||
-- reactor unit 4
|
||||
{ BOILERS = 1, TURBINES = 1, TANK = false }
|
||||
}
|
||||
-- advanced facility dynamic tank configuration
|
||||
-- (see wiki for details)
|
||||
-- by default, dynamic tanks are for each unit
|
||||
config.FAC_TANK_MODE = 0
|
||||
config.FAC_TANK_DEFS = { 0, 0, 0, 0 }
|
||||
|
||||
-- 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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user