Compare commits
432 Commits
v1.6.2-bet
...
v1.8.16-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b96eb7d89d | ||
|
|
9b8947fba2 | ||
|
|
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 | ||
|
|
8b1775b0af | ||
|
|
8bbd04d133 | ||
|
|
26dc6ff6d1 | ||
|
|
24d190921d | ||
|
|
4fec116e93 | ||
|
|
ef6fdaa3ac | ||
|
|
0160d4c53a | ||
|
|
d2a1951b66 | ||
|
|
5d7a0b266a | ||
|
|
ebabd99f2b | ||
|
|
b5e0183e54 | ||
|
|
d450c9ca3e | ||
|
|
894229831d | ||
|
|
bfa24b3665 | ||
|
|
b1446637ad | ||
|
|
02e9c09daf | ||
|
|
21d5cb3858 | ||
|
|
c0a602385d | ||
|
|
4d4dd4ed39 | ||
|
|
3a5d69d96f | ||
|
|
d38a2dea7c | ||
|
|
560d48084a | ||
|
|
625feb3fd1 | ||
|
|
ed4180a072 | ||
|
|
70831b49d2 | ||
|
|
881a120d34 | ||
|
|
8ab1307b2b | ||
|
|
689d474796 | ||
|
|
18bcfb4014 | ||
|
|
645a5f5137 | ||
|
|
1f9743efd0 | ||
|
|
9cef6e6175 | ||
|
|
70b03896d5 | ||
|
|
f9d0ef60b4 | ||
|
|
09ab60f79d | ||
|
|
d21604ea09 | ||
|
|
611b048cb4 | ||
|
|
a2182d9566 | ||
|
|
7a87499aa4 | ||
|
|
b173b72f21 | ||
|
|
29cc107ea5 | ||
|
|
c24766a4db | ||
|
|
29e910ba3c | ||
|
|
1cb240b1b0 | ||
|
|
1525ed9d60 | ||
|
|
b1c2c4d291 | ||
|
|
5585088e3a | ||
|
|
b9073153b3 | ||
|
|
cb554e5d16 | ||
|
|
71d8b5ba0a | ||
|
|
e4f49e9949 | ||
|
|
3afc765f72 | ||
|
|
048714817e | ||
|
|
f267a4e569 | ||
|
|
cfc6479dd5 | ||
|
|
70f24edb53 | ||
|
|
31df4a7f7e | ||
|
|
ca49cf90b4 | ||
|
|
785dbe9533 | ||
|
|
a9d1bc2b50 | ||
|
|
f7766d8cba | ||
|
|
37f8b85924 | ||
|
|
2ed28cf74d | ||
|
|
17698b7fb4 | ||
|
|
386a33ffd8 | ||
|
|
b7d4468cea | ||
|
|
8b0a5d529e | ||
|
|
d18a93f7d2 | ||
|
|
89d1087b1c | ||
|
|
d9e48f5cac | ||
|
|
56377ef595 | ||
|
|
95c300e450 | ||
|
|
2985898b7e | ||
|
|
57d50e6745 | ||
|
|
3dc1a06969 | ||
|
|
fcba935240 | ||
|
|
9b32bb4675 | ||
|
|
f59f484e7b | ||
|
|
0fe9b391d8 | ||
|
|
97f0191875 | ||
|
|
70db8d782c | ||
|
|
2acd166c3e | ||
|
|
c78f7e173a | ||
|
|
99a0b0a55a | ||
|
|
6e51e70b62 | ||
|
|
fd2abad5cf | ||
|
|
b93c6b7c6e | ||
|
|
8b3f558f68 | ||
|
|
8c5289867c | ||
|
|
d179920565 | ||
|
|
504ee0594f | ||
|
|
a92f182156 | ||
|
|
c5d38a5584 | ||
|
|
9bf07e6c3e | ||
|
|
7656936982 | ||
|
|
f477ad9426 | ||
|
|
59950e9d15 | ||
|
|
11d86d92eb | ||
|
|
1275f61113 | ||
|
|
d17e2b8321 | ||
|
|
ce780c3d72 | ||
|
|
76ab4e17bf | ||
|
|
ac1733c46e | ||
|
|
17731de61b | ||
|
|
d85385c1fe | ||
|
|
e0809f52a6 | ||
|
|
b2c55f9d4b | ||
|
|
ba896ea163 | ||
|
|
1a64591256 | ||
|
|
9ce75eb4bd | ||
|
|
451f804f87 | ||
|
|
724d13510d | ||
|
|
3f01ce7ec5 | ||
|
|
df67795239 | ||
|
|
775d4dc95b | ||
|
|
b3c7263bc4 | ||
|
|
9f8732830c | ||
|
|
1c87ef18a1 | ||
|
|
f111b711c5 | ||
|
|
92d1945bea | ||
|
|
4192ea426c | ||
|
|
7bd8f34773 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ko_fi: mikayla_f
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
labels: "enhancement,feature request"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
4
.github/workflows/check.yml
vendored
4
.github/workflows/check.yml
vendored
@@ -5,12 +5,10 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- latest
|
||||
- devel
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- latest
|
||||
- devel
|
||||
jobs:
|
||||
check:
|
||||
@@ -28,4 +26,4 @@ jobs:
|
||||
# --no-max-line-length = Disable warnings for long line lengths
|
||||
# --exclude-files ... = Exclude lockbox library (external) and config files
|
||||
# --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os'
|
||||
args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http parallel periphemu peripheral read rs settings shell term textutils window
|
||||
args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http keys parallel periphemu peripheral read rs settings shell term textutils window
|
||||
|
||||
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
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -5,6 +5,7 @@
|
||||
"colors",
|
||||
"fs",
|
||||
"http",
|
||||
"keys",
|
||||
"parallel",
|
||||
"periphemu",
|
||||
"peripheral",
|
||||
@@ -24,6 +25,7 @@
|
||||
},
|
||||
"Lua.hint.setType": true,
|
||||
"Lua.diagnostics.disable": [
|
||||
"duplicate-set-field"
|
||||
"duplicate-set-field",
|
||||
"inject-field"
|
||||
]
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
19
README.md
19
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,7 +16,10 @@ Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fi
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
|
||||

|
||||
@@ -34,14 +36,21 @@ 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
|
||||
|
||||
You can install this on a ComputerCraft computer using either:
|
||||
* `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua`
|
||||
* `pastebin get RGasyTM4 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.
|
||||
|
||||
343
ccmsi.lua
343
ccmsi.lua
@@ -1,9 +1,7 @@
|
||||
--
|
||||
-- ComputerCraft Mekanism SCADA System Installer Utility
|
||||
--
|
||||
|
||||
--[[
|
||||
Copyright (c) 2023 Mikayla Fischler
|
||||
CC-MEK-SCADA Installer Utility
|
||||
|
||||
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,
|
||||
@@ -20,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.7d"
|
||||
local CCMSI_VERSION = "v1.14"
|
||||
|
||||
local install_dir = "/.install-cache"
|
||||
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
|
||||
@@ -28,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
|
||||
@@ -60,19 +59,18 @@ 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_local, v_remote)
|
||||
if v_local ~= nil then
|
||||
if v_local ~= v_remote then
|
||||
print("[" .. name .. "] updating ");blue();print(v_local);white();print(" \xbb ");blue();println(v_remote);white()
|
||||
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()
|
||||
elseif mode == "install" then
|
||||
pkg_message("[" .. name .. "] reinstalling", v_local)
|
||||
pkg_message("["..name.."] reinstalling", v.v_local)
|
||||
end
|
||||
else
|
||||
pkg_message("[" .. name .. "] new install of", v_remote)
|
||||
end
|
||||
else pkg_message("["..name.."] new install of", v.v_remote) end
|
||||
return v.v_local ~= v.v_remote
|
||||
end
|
||||
|
||||
-- read the local manifest file
|
||||
@@ -91,8 +89,8 @@ end
|
||||
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()
|
||||
orange();println("Failed to get installation manifest from GitHub, cannot update or install.")
|
||||
red();println("HTTP error: "..error);white()
|
||||
return false, {}
|
||||
end
|
||||
|
||||
@@ -157,92 +155,92 @@ 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) then
|
||||
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")
|
||||
|
||||
lgray()
|
||||
table.insert(tree, "log.txt") ---@fixme fix after migration to settings files?
|
||||
|
||||
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
|
||||
elseif not _in_array(val, tree) then
|
||||
root_ext = true
|
||||
yellow();println(val .. " not used")
|
||||
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
|
||||
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")
|
||||
println(" update - update files EXCEPT for config/logs")
|
||||
println(" remove - delete files EXCEPT for config/logs")
|
||||
println(" purge - delete files INCLUDING config/logs")
|
||||
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")
|
||||
println(" rtu - RTU firmware")
|
||||
println(" supervisor - supervisor server application")
|
||||
println(" coordinator - coordinator application")
|
||||
println(" pocket - pocket application")
|
||||
white();println("<branch>");yellow()
|
||||
println(" second parameter when used with check")
|
||||
lgray();println(" main (default) | latest | devel");white()
|
||||
println(" installer - ccmsi installer (update only)")
|
||||
white();println("<branch>")
|
||||
lgray();println(" main (default) | devel");white()
|
||||
return
|
||||
else
|
||||
mode = get_opt(opts[1], { "check", "install", "update", "remove", "purge" })
|
||||
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
|
||||
if mode == nil then
|
||||
red();println("Unrecognized mode.");white()
|
||||
return
|
||||
end
|
||||
|
||||
app = get_opt(opts[2], { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket" })
|
||||
app = get_opt(opts[2], { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket", "installer" })
|
||||
if app == nil and mode ~= "check" then
|
||||
red();println("Unrecognized application.");white()
|
||||
return
|
||||
elseif app == "installer" and mode ~= "update" then
|
||||
red();println("Installer app only supports 'update' option.");white()
|
||||
return
|
||||
end
|
||||
|
||||
-- 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
|
||||
@@ -262,21 +260,24 @@ 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 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"
|
||||
local ok, manifest = get_remote_manifest()
|
||||
if not ok then return end
|
||||
|
||||
@@ -284,71 +285,75 @@ elseif mode == "install" or mode == "update" then
|
||||
app = { v_local = nil, v_remote = nil, changed = false },
|
||||
boot = { v_local = nil, v_remote = nil, changed = false },
|
||||
comms = { v_local = nil, v_remote = nil, changed = false },
|
||||
common = { v_local = nil, v_remote = nil, changed = false },
|
||||
graphics = { v_local = nil, v_remote = nil, changed = false },
|
||||
lockbox = { v_local = nil, v_remote = nil, changed = false }
|
||||
}
|
||||
|
||||
-- try to find local versions
|
||||
local local_ok, local_manifest = read_local_manifest()
|
||||
local local_ok, lmnf = read_local_manifest()
|
||||
if not local_ok then
|
||||
if mode == "update" then
|
||||
red();println("failed to load local installation information, cannot update");white()
|
||||
red();println("Failed to load local installation information, cannot update.");white()
|
||||
return
|
||||
end
|
||||
else
|
||||
ver.boot.v_local = local_manifest.versions.bootloader
|
||||
ver.app.v_local = local_manifest.versions[app]
|
||||
ver.comms.v_local = local_manifest.versions.comms
|
||||
ver.graphics.v_local = local_manifest.versions.graphics
|
||||
ver.lockbox.v_local = local_manifest.versions.lockbox
|
||||
elseif not update_installer then
|
||||
ver.boot.v_local = lmnf.versions.bootloader
|
||||
ver.app.v_local = lmnf.versions[app]
|
||||
ver.comms.v_local = lmnf.versions.comms
|
||||
ver.common.v_local = lmnf.versions.common
|
||||
ver.graphics.v_local = lmnf.versions.graphics
|
||||
ver.lockbox.v_local = lmnf.versions.lockbox
|
||||
|
||||
if local_manifest.versions[app] == nil then
|
||||
red();println("another application is already installed, please purge it before installing a new application");white()
|
||||
if lmnf.versions[app] == nil then
|
||||
red();println("Another application is already installed, please uninstall it before installing a new application.");white()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local_manifest.versions.installer = CCMSI_VERSION
|
||||
if manifest.versions.installer ~= CCMSI_VERSION then
|
||||
yellow();println("a newer version of the installer is available, it is recommended to download it");white()
|
||||
if manifest.versions.installer ~= CCMSI_VERSION then
|
||||
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")
|
||||
|
||||
if dl == nil then
|
||||
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
|
||||
handle.write(dl.readAll())
|
||||
handle.close()
|
||||
green();println("Installer updated successfully.");white()
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
elseif update_installer then
|
||||
green();println("Installer already up-to-date.");white()
|
||||
return
|
||||
end
|
||||
|
||||
ver.boot.v_remote = manifest.versions.bootloader
|
||||
ver.app.v_remote = manifest.versions[app]
|
||||
ver.comms.v_remote = manifest.versions.comms
|
||||
ver.common.v_remote = manifest.versions.common
|
||||
ver.graphics.v_remote = manifest.versions.graphics
|
||||
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()
|
||||
|
||||
-- display bootloader version change information
|
||||
show_pkg_change("bootldr", ver.boot.v_local, ver.boot.v_remote)
|
||||
ver.boot.changed = ver.boot.v_local ~= ver.boot.v_remote
|
||||
|
||||
-- display app version change information
|
||||
show_pkg_change(app, ver.app.v_local, ver.app.v_remote)
|
||||
ver.app.changed = ver.app.v_local ~= ver.app.v_remote
|
||||
|
||||
-- display comms version change information
|
||||
show_pkg_change("comms", ver.comms.v_local, ver.comms.v_remote)
|
||||
ver.comms.changed = ver.comms.v_local ~= ver.comms.v_remote
|
||||
ver.boot.changed = show_pkg_change("bootldr", ver.boot)
|
||||
ver.common.changed = show_pkg_change("common", ver.common)
|
||||
ver.comms.changed = show_pkg_change("comms", ver.comms)
|
||||
if ver.comms.changed and ver.comms.v_local ~= nil then
|
||||
print("[comms] ");yellow();println("other devices on the network will require an update");white()
|
||||
end
|
||||
|
||||
-- display graphics version change information
|
||||
show_pkg_change("graphics", ver.graphics.v_local, ver.graphics.v_remote)
|
||||
ver.graphics.changed = ver.graphics.v_local ~= ver.graphics.v_remote
|
||||
|
||||
-- display lockbox version change information
|
||||
show_pkg_change("lockbox", ver.lockbox.v_local, ver.lockbox.v_remote)
|
||||
ver.lockbox.changed = ver.lockbox.v_local ~= ver.lockbox.v_remote
|
||||
ver.app.changed = show_pkg_change(app, ver.app)
|
||||
ver.graphics.changed = show_pkg_change("graphics", ver.graphics)
|
||||
ver.lockbox.changed = show_pkg_change("lockbox", ver.lockbox)
|
||||
|
||||
-- ask for confirmation
|
||||
if not ask_y_n("Continue", false) then return end
|
||||
@@ -364,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)
|
||||
|
||||
@@ -376,10 +380,11 @@ elseif mode == "install" or mode == "update" then
|
||||
-- check space constraints
|
||||
if space_available < space_required then
|
||||
single_file_mode = true
|
||||
yellow();println("WARNING: Insufficient space available for a full download!");white()
|
||||
println("Files can be downloaded one by one, so if you are replacing a current install this will not be a problem unless installation fails.")
|
||||
if mode == "update" then println("If installation still fails, delete this device's log file or uninstall the app (not purge) and try again.") end
|
||||
if not ask_y_n("Do you wish to continue?", false) then
|
||||
yellow();println("NOTICE: Insufficient space available for a full cached download!");white()
|
||||
lgray();println("Files can instead be downloaded one by one. If you are replacing a current install this may corrupt your install ONLY if it fails (such as a sudden network issue). If that occurs, you can still try again.")
|
||||
if mode == "update" then println("If installation still fails, delete this device's log file and/or any unrelated files you have on this computer then try again.") end
|
||||
white();
|
||||
if not ask_y_n("Do you wish to continue", false) then
|
||||
println("Operation cancelled.")
|
||||
return
|
||||
end
|
||||
@@ -392,16 +397,13 @@ elseif mode == "install" or mode == "update" then
|
||||
if dependency == "system" then return not ver.boot.changed
|
||||
elseif dependency == "graphics" then return not ver.graphics.changed
|
||||
elseif dependency == "lockbox" then return not ver.lockbox.changed
|
||||
elseif dependency == "common" then return not (ver.app.changed or ver.comms.changed)
|
||||
elseif dependency == "common" then return not (ver.common.changed or ver.comms.changed)
|
||||
elseif dependency == app then return not ver.app.changed
|
||||
else return true end
|
||||
end
|
||||
|
||||
if not single_file_mode then
|
||||
if fs.exists(install_dir) then
|
||||
fs.delete(install_dir)
|
||||
fs.makeDir(install_dir)
|
||||
end
|
||||
if fs.exists(install_dir) then fs.delete(install_dir);fs.makeDir(install_dir) end
|
||||
|
||||
-- download all dependencies
|
||||
for _, dependency in pairs(dependencies) do
|
||||
@@ -413,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("GET 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
|
||||
@@ -440,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
|
||||
@@ -477,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("GET 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
|
||||
@@ -511,22 +509,19 @@ elseif mode == "install" or mode == "update" then
|
||||
else println("Update failed, files may have been skipped.") end
|
||||
end
|
||||
end
|
||||
elseif mode == "remove" or mode == "purge" then
|
||||
elseif mode == "uninstall" then
|
||||
local ok, manifest = read_local_manifest()
|
||||
if not ok then
|
||||
red();println("Error parsing local installation manifest.");white()
|
||||
return
|
||||
elseif mode == "remove" and manifest.versions[app] == nil then
|
||||
red();println(app .. " is not installed, cannot remove.");white()
|
||||
end
|
||||
|
||||
if manifest.versions[app] == nil then
|
||||
red();println("Error: '"..app.."' is not installed.")
|
||||
return
|
||||
end
|
||||
|
||||
orange()
|
||||
if mode == "remove" then
|
||||
println("Removing all " .. app .. " files except for config and log...")
|
||||
elseif mode == "purge" then
|
||||
println("Purging all " .. app .. " files including config and log...")
|
||||
end
|
||||
orange();println("Uninstalling all "..app.." files...")
|
||||
|
||||
-- ask for confirmation
|
||||
if not ask_y_n("Continue", false) then return end
|
||||
@@ -536,76 +531,68 @@ elseif mode == "remove" or mode == "purge" then
|
||||
|
||||
local file_list = manifest.files
|
||||
local dependencies = manifest.depends[app]
|
||||
local config_file = app .. "/config.lua"
|
||||
|
||||
table.insert(dependencies, app)
|
||||
|
||||
-- delete log file if purging
|
||||
-- delete log file
|
||||
local log_deleted = false
|
||||
local settings_file = app..".settings"
|
||||
local legacy_config_file = app.."/config.lua"
|
||||
|
||||
lgray()
|
||||
if mode == "purge" and fs.exists(config_file) then
|
||||
local log_deleted = pcall(function ()
|
||||
local config = require(app .. ".config")
|
||||
if fs.exists(legacy_config_file) then
|
||||
log_deleted = pcall(function ()
|
||||
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)
|
||||
|
||||
if not log_deleted then
|
||||
red();println("failed to delete log file")
|
||||
white();println("press any key to continue...")
|
||||
any_key();lgray()
|
||||
elseif fs.exists(settings_file) and settings.load(settings_file) then
|
||||
local log = settings.get("LogPath")
|
||||
if log ~= nil and fs.exists(log) then
|
||||
log_deleted = true
|
||||
fs.delete(log)
|
||||
println("deleted log file "..log)
|
||||
end
|
||||
end
|
||||
|
||||
-- delete all files except config unless purging
|
||||
if not log_deleted then
|
||||
red();println("Failed to delete log file.")
|
||||
white();println("press any key to continue...")
|
||||
any_key();lgray()
|
||||
end
|
||||
|
||||
-- delete all installed files
|
||||
for _, dependency in pairs(dependencies) do
|
||||
local files = file_list[dependency]
|
||||
for _, file in pairs(files) do
|
||||
if mode == "purge" or file ~= config_file then
|
||||
if fs.exists(file) then fs.delete(file);println("deleted " .. file) end
|
||||
end
|
||||
if fs.exists(file) then fs.delete(file);println("deleted "..file) end
|
||||
end
|
||||
|
||||
-- delete folders that we should be deleteing
|
||||
if mode == "purge" or dependency ~= app then
|
||||
local folder = files[1]
|
||||
while true do
|
||||
local dir = fs.getDir(folder)
|
||||
if dir == "" or dir == ".." then break else folder = dir end
|
||||
end
|
||||
local folder = files[1]
|
||||
while true do
|
||||
local dir = fs.getDir(folder)
|
||||
if dir == "" or dir == ".." then break else folder = dir end
|
||||
end
|
||||
|
||||
if fs.isDir(folder) then
|
||||
fs.delete(folder)
|
||||
println("deleted directory " .. folder)
|
||||
end
|
||||
elseif dependency == app then
|
||||
-- delete individual subdirectories so we can leave the config
|
||||
for _, folder in pairs(files) do
|
||||
while true do
|
||||
local dir = fs.getDir(folder)
|
||||
if dir == "" or dir == ".." or dir == app then break else folder = dir end
|
||||
end
|
||||
|
||||
if folder ~= app and fs.isDir(folder) then
|
||||
fs.delete(folder);println("deleted app subdirectory " .. folder)
|
||||
end
|
||||
end
|
||||
if fs.isDir(folder) then
|
||||
fs.delete(folder)
|
||||
println("deleted directory "..folder)
|
||||
end
|
||||
end
|
||||
|
||||
-- only delete manifest if purging
|
||||
if mode == "purge" then
|
||||
fs.delete("install_manifest.json")
|
||||
println("deleted install_manifest.json")
|
||||
else
|
||||
-- remove all data from versions list to show nothing is installed
|
||||
manifest.versions = {}
|
||||
local imfile = fs.open("install_manifest.json", "w")
|
||||
imfile.write(textutils.serializeJSON(manifest))
|
||||
imfile.close()
|
||||
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)
|
||||
end
|
||||
|
||||
fs.delete("install_manifest.json")
|
||||
println("deleted install_manifest.json")
|
||||
|
||||
green();println("Done!")
|
||||
end
|
||||
|
||||
|
||||
11
configure.lua
Normal file
11
configure.lua
Normal file
@@ -0,0 +1,11 @@
|
||||
print("CONFIGURE> SCANNING FOR CONFIGURATOR...")
|
||||
|
||||
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")
|
||||
end
|
||||
@@ -1,38 +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
|
||||
|
||||
-- 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,21 +4,18 @@ 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
|
||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||
local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
|
||||
@@ -26,153 +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
|
||||
---@return boolean success, monitors_struct? monitors
|
||||
function coordinator.configure_monitors(num_units)
|
||||
---@class monitors_struct
|
||||
local monitors = {
|
||||
primary = nil,
|
||||
primary_name = "",
|
||||
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)
|
||||
local num_displays_needed = num_units + 1
|
||||
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 _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)
|
||||
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
|
||||
-- lets get a monitor
|
||||
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
|
||||
|
||||
-------------------
|
||||
-- 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
|
||||
-- lets get a monitor
|
||||
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
|
||||
-- lets get a monitor
|
||||
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
|
||||
@@ -182,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,
|
||||
@@ -196,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
|
||||
@@ -217,12 +227,8 @@ end
|
||||
---@nodiscard
|
||||
---@param version string coordinator version
|
||||
---@param nic nic network interface device
|
||||
---@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, 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,
|
||||
@@ -237,19 +243,19 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
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 SCADA_MGMT_TYPE|SCADA_CRDN_TYPE
|
||||
---@param msg_type MGMT_TYPE|CRDN_TYPE
|
||||
---@param msg table
|
||||
local function _send_sv(protocol, msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
@@ -266,33 +272,34 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
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(SCADA_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
|
||||
|
||||
-- attempt connection establishment
|
||||
local function _send_establish()
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRDN })
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRD })
|
||||
end
|
||||
|
||||
-- keep alive ack
|
||||
---@param srv_time integer
|
||||
local function _send_keep_alive_ack(srv_time)
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
@@ -309,24 +316,24 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
|
||||
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")
|
||||
@@ -341,11 +348,12 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
|
||||
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)
|
||||
@@ -364,21 +372,21 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
self.sv_linked = false
|
||||
self.sv_r_seq_num = nil
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {})
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {})
|
||||
end
|
||||
|
||||
-- send a facility command
|
||||
---@param cmd FAC_COMMAND command
|
||||
---@param option any? optional option options for the optional options (like waste mode)
|
||||
function public.send_fac_command(cmd, option)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd, option })
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, { cmd, option })
|
||||
end
|
||||
|
||||
-- send the auto process control configuration with a start command
|
||||
---@param config coord_auto_config configuration
|
||||
function public.send_auto_start(config)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, {
|
||||
FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits
|
||||
---@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, auto_cfg.mode, auto_cfg.burn_target, auto_cfg.charge_target, auto_cfg.gen_target, auto_cfg.limits
|
||||
})
|
||||
end
|
||||
|
||||
@@ -387,7 +395,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
---@param unit integer unit ID
|
||||
---@param option any? optional option options for the optional options (like burn rate)
|
||||
function public.send_unit_command(cmd, unit, option)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
|
||||
end
|
||||
|
||||
-- parse a packet
|
||||
@@ -396,7 +404,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
---@param reply_to integer
|
||||
---@param message any
|
||||
---@param distance integer
|
||||
---@return mgmt_frame|crdn_frame|capi_frame|nil packet
|
||||
---@return mgmt_frame|crdn_frame|nil packet
|
||||
function public.parse_packet(side, sender, reply_to, message, distance)
|
||||
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
|
||||
local pkt = nil
|
||||
@@ -414,12 +422,6 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
if crdn_pkt.decode(s_pkt) then
|
||||
pkt = crdn_pkt.get()
|
||||
end
|
||||
-- get as coordinator API packet
|
||||
elseif s_pkt.protocol() == PROTOCOL.COORD_API then
|
||||
local capi_pkt = comms.capi_packet()
|
||||
if capi_pkt.decode(s_pkt) then
|
||||
pkt = capi_pkt.get()
|
||||
end
|
||||
else
|
||||
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
|
||||
end
|
||||
@@ -429,7 +431,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
end
|
||||
|
||||
-- handle a packet
|
||||
---@param packet mgmt_frame|crdn_frame|capi_frame|nil
|
||||
---@param packet mgmt_frame|crdn_frame|nil
|
||||
---@return boolean close_ui
|
||||
function public.handle_packet(packet)
|
||||
local was_linked = self.sv_linked
|
||||
@@ -440,23 +442,23 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
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.COORD_API then
|
||||
---@cast packet capi_frame
|
||||
elseif protocol == PROTOCOL.SCADA_CRDN then
|
||||
---@cast packet crdn_frame
|
||||
-- look for an associated session
|
||||
local session = apisessions.find_session(src_addr)
|
||||
|
||||
-- API packet
|
||||
-- coordinator packet
|
||||
if session ~= nil then
|
||||
-- pass the packet onto the session handler
|
||||
session.in_queue.push_packet(packet)
|
||||
else
|
||||
-- any other packet should be session related, discard it
|
||||
log.debug("discarding COORD_API packet without a known session")
|
||||
log.debug("discarding SCADA_CRDN packet without a known session")
|
||||
end
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
@@ -467,13 +469,14 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
if session ~= nil then
|
||||
-- pass the packet onto the session handler
|
||||
session.in_queue.push_packet(packet)
|
||||
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
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
|
||||
@@ -481,12 +484,19 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
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)
|
||||
@@ -502,7 +512,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
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()
|
||||
@@ -523,7 +533,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
if protocol == PROTOCOL.SCADA_CRDN then
|
||||
---@cast packet crdn_frame
|
||||
if self.sv_linked then
|
||||
if packet.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then
|
||||
if packet.type == CRDN_TYPE.INITIAL_BUILDS then
|
||||
if packet.length == 2 then
|
||||
-- record builds
|
||||
local fac_builds = iocontrol.record_facility_builds(packet.data[1])
|
||||
@@ -531,31 +541,31 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
|
||||
if fac_builds and unit_builds then
|
||||
-- acknowledge receipt of builds
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.INITIAL_BUILDS, {})
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.INITIAL_BUILDS, {})
|
||||
else
|
||||
log.debug("received invalid INITIAL_BUILDS packet")
|
||||
end
|
||||
else
|
||||
log.debug("INITIAL_BUILDS packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.FAC_BUILDS then
|
||||
elseif packet.type == CRDN_TYPE.FAC_BUILDS then
|
||||
if packet.length == 1 then
|
||||
-- record facility builds
|
||||
if iocontrol.record_facility_builds(packet.data[1]) then
|
||||
-- acknowledge receipt of builds
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_BUILDS, {})
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_BUILDS, {})
|
||||
else
|
||||
log.debug("received invalid FAC_BUILDS packet")
|
||||
end
|
||||
else
|
||||
log.debug("FAC_BUILDS packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.FAC_STATUS then
|
||||
elseif packet.type == CRDN_TYPE.FAC_STATUS then
|
||||
-- update facility status
|
||||
if not iocontrol.update_facility_status(packet.data) then
|
||||
log.debug("received invalid FAC_STATUS packet")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.FAC_CMD then
|
||||
elseif packet.type == CRDN_TYPE.FAC_CMD then
|
||||
-- facility command acknowledgement
|
||||
if packet.length >= 2 then
|
||||
local cmd = packet.data[1]
|
||||
@@ -583,24 +593,24 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
else
|
||||
log.debug("SCADA_CRDN facility command ack packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.UNIT_BUILDS then
|
||||
elseif packet.type == CRDN_TYPE.UNIT_BUILDS then
|
||||
-- record builds
|
||||
if packet.length == 1 then
|
||||
if iocontrol.record_unit_builds(packet.data[1]) then
|
||||
-- acknowledge receipt of builds
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_BUILDS, {})
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_BUILDS, {})
|
||||
else
|
||||
log.debug("received invalid UNIT_BUILDS packet")
|
||||
end
|
||||
else
|
||||
log.debug("UNIT_BUILDS packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.UNIT_STATUSES then
|
||||
elseif packet.type == CRDN_TYPE.UNIT_STATUSES then
|
||||
-- update statuses
|
||||
if not iocontrol.update_unit_statuses(packet.data) then
|
||||
log.debug("received invalid UNIT_STATUSES packet")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.UNIT_CMD then
|
||||
elseif packet.type == CRDN_TYPE.UNIT_CMD then
|
||||
-- unit command acknowledgement
|
||||
if packet.length == 3 then
|
||||
local cmd = packet.data[1]
|
||||
@@ -642,7 +652,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
if self.sv_linked then
|
||||
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
if packet.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if packet.length == 1 then
|
||||
local timestamp = packet.data[1]
|
||||
@@ -660,7 +670,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
else
|
||||
log.debug("SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
|
||||
elseif packet.type == MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
sv_watchdog.cancel()
|
||||
self.sv_addr = comms.BROADCAST
|
||||
@@ -671,33 +681,28 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
else
|
||||
log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
elseif packet.type == MGMT_TYPE.ESTABLISH then
|
||||
-- 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 > 1 then
|
||||
if type(sv_config) == "table" and #sv_config == 2 then
|
||||
-- get configuration
|
||||
|
||||
---@class facility_conf
|
||||
local conf = {
|
||||
num_units = config[1], ---@type integer
|
||||
defs = {} -- boilers and turbines
|
||||
num_units = sv_config[1], ---@type integer
|
||||
cooling = sv_config[2] ---@type sv_cooling_conf
|
||||
}
|
||||
|
||||
if (#config - 1) == (conf.num_units * 2) then
|
||||
-- record sequence of pairs of [#boilers, #turbines] per unit
|
||||
for i = 2, #config do
|
||||
table.insert(conf.defs, config[i])
|
||||
end
|
||||
|
||||
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
|
||||
@@ -707,7 +712,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED)
|
||||
else
|
||||
self.sv_config_err = true
|
||||
log.warning("invalid supervisor configuration definitions received, establish failed")
|
||||
log.warning("supervisor config's number of units don't match coordinator's config, establish failed")
|
||||
end
|
||||
else
|
||||
log.debug("invalid supervisor configuration table received, establish failed")
|
||||
|
||||
@@ -38,9 +38,7 @@ local function __generic_ack(success) end
|
||||
---@param comms_v string comms version
|
||||
function iocontrol.init_fp(firmware_v, comms_v)
|
||||
---@class ioctl_front_panel
|
||||
io.fp = {
|
||||
ps = psil.create()
|
||||
}
|
||||
io.fp = { ps = psil.create() }
|
||||
|
||||
io.fp.ps.publish("version", firmware_v)
|
||||
io.fp.ps.publish("comms_version", comms_v)
|
||||
@@ -49,10 +47,30 @@ 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 = {
|
||||
num_units = conf.num_units, ---@type integer
|
||||
conf = conf,
|
||||
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,
|
||||
|
||||
@@ -74,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(),
|
||||
|
||||
@@ -83,6 +102,8 @@ function iocontrol.init(conf, comms)
|
||||
scram_ack = __generic_ack,
|
||||
ack_alarms_ack = __generic_ack,
|
||||
|
||||
alarm_tones = { false, false, false, false, false, false, false, false },
|
||||
|
||||
ps = psil.create(),
|
||||
|
||||
induction_ps_tbl = {},
|
||||
@@ -104,6 +125,101 @@ function iocontrol.init(conf, comms)
|
||||
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
|
||||
local function ack(alarm) process.ack_alarm(i, alarm) end
|
||||
@@ -112,15 +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].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,
|
||||
@@ -161,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
|
||||
@@ -190,18 +312,29 @@ function iocontrol.init(conf, comms)
|
||||
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.defs[(i * 2) - 1] do
|
||||
local data = {} ---@type boilerv_session_db
|
||||
for _ = 1, conf.cooling.r_cool[i].BoilerCount do
|
||||
table.insert(entry.boiler_ps_tbl, psil.create())
|
||||
table.insert(entry.boiler_data_tbl, data)
|
||||
table.insert(entry.boiler_data_tbl, {})
|
||||
table.insert(entry.rtu_hw.boilers, { connected = false, faulted = false })
|
||||
end
|
||||
|
||||
-- create turbine tables
|
||||
for _ = 1, conf.defs[i * 2] do
|
||||
local data = {} ---@type turbinev_session_db
|
||||
for _ = 1, conf.cooling.r_cool[i].TurbineCount do
|
||||
table.insert(entry.turbine_ps_tbl, psil.create())
|
||||
table.insert(entry.turbine_data_tbl, data)
|
||||
table.insert(entry.turbine_data_tbl, {})
|
||||
table.insert(entry.rtu_hw.turbines, { connected = false, faulted = false })
|
||||
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
|
||||
@@ -232,11 +365,28 @@ function iocontrol.fp_has_speaker(has_speaker) io.fp.ps.publish("has_speaker", h
|
||||
function iocontrol.fp_link_state(state) io.fp.ps.publish("link_state", state) end
|
||||
|
||||
-- report monitor connection state
|
||||
---@param id integer unit ID or 0 for main
|
||||
---@param id string|integer unit ID for unit monitor, "main" for main monitor, or "flow" for flow monitor
|
||||
function iocontrol.fp_monitor_state(id, connected)
|
||||
local name = "main_monitor"
|
||||
if id > 0 then name = "unit_monitor_" .. id end
|
||||
io.fp.ps.publish(name, connected)
|
||||
local name = nil
|
||||
|
||||
if id == "main" then
|
||||
name = "main_monitor"
|
||||
elseif id == "flow" then
|
||||
name = "flow_monitor"
|
||||
elseif type(id) == "number" then
|
||||
name = "unit_monitor_" .. id
|
||||
end
|
||||
|
||||
if name ~= nil then
|
||||
io.fp.ps.publish(name, 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
|
||||
@@ -266,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
|
||||
|
||||
@@ -351,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, "]: ")
|
||||
|
||||
@@ -448,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]
|
||||
|
||||
@@ -499,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
|
||||
@@ -518,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
|
||||
@@ -576,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)
|
||||
|
||||
@@ -614,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)
|
||||
|
||||
@@ -642,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
|
||||
@@ -664,6 +847,16 @@ function iocontrol.update_facility_status(status)
|
||||
end
|
||||
|
||||
fac.ps.publish("rtu_count", fac.rtu_count)
|
||||
|
||||
-- alarm tone commands
|
||||
|
||||
if (type(status[3]) == "table") and (#status[3] == 8) then
|
||||
fac.alarm_tones = status[3]
|
||||
sounder.set(fac.alarm_tones)
|
||||
else
|
||||
log.debug(log_header .. "alarm tones not a table or length mismatch")
|
||||
valid = false
|
||||
end
|
||||
end
|
||||
|
||||
return valid
|
||||
@@ -684,8 +877,7 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
else
|
||||
local burn_rate_sum = 0.0
|
||||
local sna_count_sum = 0
|
||||
local pu_rate = 0.0
|
||||
local po_rate = 0.0
|
||||
local pu_rate, po_rate, po_pl_rate, po_am_rate, spent_rate = 0.0, 0.0, 0.0, 0.0, 0.0
|
||||
|
||||
-- get all unit statuses
|
||||
for i = 1, #statuses do
|
||||
@@ -696,7 +888,7 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
|
||||
local burn_rate = 0.0
|
||||
|
||||
if type(status) ~= "table" or #status ~= 5 then
|
||||
if type(status) ~= "table" or #status ~= 6 then
|
||||
log.debug(log_header .. "invalid status entry in unit statuses (not a table or invalid length)")
|
||||
valid = false
|
||||
else
|
||||
@@ -709,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]
|
||||
@@ -768,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
|
||||
@@ -779,8 +974,13 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
if type(rtu_statuses) == "table" then
|
||||
-- boiler statuses
|
||||
if type(rtu_statuses.boilers) == "table" then
|
||||
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
|
||||
@@ -788,14 +988,17 @@ 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
|
||||
elseif data.formed then
|
||||
boil_sum = boil_sum + data.state.boil_rate
|
||||
|
||||
if data.state.boil_rate > 0 then
|
||||
ps.publish("computed_status", 5) -- active
|
||||
else
|
||||
@@ -809,6 +1012,8 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
valid = false
|
||||
end
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("boiler_boil_sum", boil_sum)
|
||||
else
|
||||
log.debug(log_header .. "boiler list not a table")
|
||||
valid = false
|
||||
@@ -816,8 +1021,13 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
|
||||
-- turbine statuses
|
||||
if type(rtu_statuses.turbines) == "table" then
|
||||
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
|
||||
@@ -825,14 +1035,17 @@ 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
|
||||
elseif data.formed then
|
||||
flow_sum = flow_sum + data.state.flow_rate
|
||||
|
||||
if data.tanks.energy_fill >= 0.99 then
|
||||
ps.publish("computed_status", 6) -- trip
|
||||
elseif data.state.flow_rate < 100 then
|
||||
@@ -848,6 +1061,8 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
valid = false
|
||||
end
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("turbine_flow_sum", flow_sum)
|
||||
else
|
||||
log.debug(log_header .. "turbine list not a table")
|
||||
valid = false
|
||||
@@ -856,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
|
||||
@@ -877,7 +1092,7 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
elseif data.tanks.fill < 0.20 then
|
||||
ps.publish("computed_status", 5) -- low
|
||||
else
|
||||
ps.publish("computed_status", 5) -- active
|
||||
ps.publish("computed_status", 4) -- on-line
|
||||
end
|
||||
else
|
||||
ps.publish("computed_status", 2) -- not formed
|
||||
@@ -894,10 +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.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_prod_rate", unit.sna_prod_rate)
|
||||
unit.unit_ps.publish("sna_count", unit.num_snas)
|
||||
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
|
||||
@@ -906,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
|
||||
@@ -1002,10 +1234,64 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
valid = false
|
||||
end
|
||||
|
||||
-- valve states
|
||||
local valve_states = status[6]
|
||||
|
||||
if type(valve_states) == "table" then
|
||||
if #valve_states == 5 then
|
||||
unit.unit_ps.publish("V_pu_conn", valve_states[1] > 0)
|
||||
unit.unit_ps.publish("V_pu_state", valve_states[1] == 2)
|
||||
unit.unit_ps.publish("V_po_conn", valve_states[2] > 0)
|
||||
unit.unit_ps.publish("V_po_state", valve_states[2] == 2)
|
||||
unit.unit_ps.publish("V_pl_conn", valve_states[3] > 0)
|
||||
unit.unit_ps.publish("V_pl_state", valve_states[3] == 2)
|
||||
unit.unit_ps.publish("V_am_conn", valve_states[4] > 0)
|
||||
unit.unit_ps.publish("V_am_state", valve_states[4] == 2)
|
||||
unit.unit_ps.publish("V_emc_conn", valve_states[5] > 0)
|
||||
unit.unit_ps.publish("V_emc_state", valve_states[5] == 2)
|
||||
else
|
||||
log.debug(log_header .. "valve states length mismatch")
|
||||
valid = false
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "valve states not a table")
|
||||
valid = false
|
||||
end
|
||||
|
||||
-- determine waste production for this unit, add to statistics
|
||||
|
||||
local is_pu = unit.waste_product == types.WASTE_PRODUCT.PLUTONIUM
|
||||
pu_rate = pu_rate + util.trinary(is_pu, burn_rate / 10.0, 0.0)
|
||||
po_rate = po_rate + util.trinary(not is_pu, math.min(burn_rate / 10.0, unit.sna_prod_rate), 0.0)
|
||||
local waste_rate = burn_rate / 10.0
|
||||
|
||||
local u_spent_rate = waste_rate
|
||||
local u_pu_rate = util.trinary(is_pu, waste_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)
|
||||
|
||||
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
|
||||
else
|
||||
unit.unit_ps.publish("po_pl_rate", 0)
|
||||
unit.unit_ps.publish("po_am_rate", 0)
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("ws_rate", u_spent_rate)
|
||||
|
||||
pu_rate = pu_rate + u_pu_rate
|
||||
po_rate = po_rate + u_po_rate
|
||||
spent_rate = spent_rate + u_spent_rate
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1013,9 +1299,9 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
io.facility.ps.publish("sna_count", sna_count_sum)
|
||||
io.facility.ps.publish("pu_rate", pu_rate)
|
||||
io.facility.ps.publish("po_rate", po_rate)
|
||||
|
||||
-- update alarm sounder
|
||||
sounder.eval(io.units)
|
||||
io.facility.ps.publish("po_pl_rate", po_pl_rate)
|
||||
io.facility.ps.publish("po_am_rate", po_am_rate)
|
||||
io.facility.ps.publish("spent_waste_rate", spent_rate)
|
||||
end
|
||||
|
||||
return valid
|
||||
|
||||
@@ -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,26 +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 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 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,
|
||||
@@ -29,8 +36,10 @@ local engine = {
|
||||
ui = {
|
||||
front_panel = nil, ---@type graphics_element|nil
|
||||
main_display = nil, ---@type graphics_element|nil
|
||||
flow_display = nil, ---@type graphics_element|nil
|
||||
unit_displays = {}
|
||||
}
|
||||
},
|
||||
disable_flow_view = false
|
||||
}
|
||||
|
||||
-- init a display to the "default", but set text scale to 0.5
|
||||
@@ -43,9 +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
|
||||
|
||||
-- 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
|
||||
@@ -54,14 +88,16 @@ function renderer.set_displays(monitors)
|
||||
engine.monitors = monitors
|
||||
|
||||
-- report to front panel as connected
|
||||
iocontrol.fp_monitor_state(0, true)
|
||||
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 monitor
|
||||
_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
|
||||
for _, monitor in ipairs(engine.monitors.unit_displays) do
|
||||
@@ -75,56 +111,48 @@ 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 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
|
||||
|
||||
-- start the coordinator front panel
|
||||
function renderer.start_fp()
|
||||
-- try to start the front panel
|
||||
---@return boolean success, any error_msg
|
||||
function renderer.try_start_fp()
|
||||
local status, msg = true, nil
|
||||
|
||||
if not engine.fp_ready then
|
||||
-- show front panel view on terminal
|
||||
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root}
|
||||
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
|
||||
status, msg = pcall(function ()
|
||||
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root}
|
||||
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
|
||||
end)
|
||||
|
||||
-- start flasher callback task
|
||||
flasher.run()
|
||||
|
||||
-- report front panel as ready
|
||||
engine.fp_ready = true
|
||||
if status then
|
||||
-- start flasher callback task and report ready
|
||||
flasher.run()
|
||||
engine.fp_ready = true
|
||||
else
|
||||
-- report fail and close front panel
|
||||
msg = core.extract_assert_msg(msg)
|
||||
renderer.close_fp()
|
||||
end
|
||||
end
|
||||
|
||||
return status, msg
|
||||
end
|
||||
|
||||
-- close out the front panel
|
||||
@@ -144,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
|
||||
@@ -157,30 +185,50 @@ function renderer.close_fp()
|
||||
end
|
||||
end
|
||||
|
||||
-- start the coordinator GUI
|
||||
function renderer.start_ui()
|
||||
-- try to start the main GUI
|
||||
---@return boolean success, any error_msg
|
||||
function renderer.try_start_ui()
|
||||
local status, msg = true, nil
|
||||
|
||||
if not engine.ui_ready then
|
||||
-- hide dmesg
|
||||
engine.dmesg_window.setVisible(false)
|
||||
|
||||
-- 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}
|
||||
main_view(engine.ui.main_display)
|
||||
status, msg = pcall(function ()
|
||||
-- show main view on main monitor
|
||||
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)
|
||||
|
||||
if status then
|
||||
-- start flasher callback task and report ready
|
||||
flasher.run()
|
||||
engine.ui_ready = true
|
||||
else
|
||||
-- report fail and close ui
|
||||
msg = core.extract_assert_msg(msg)
|
||||
renderer.close_ui()
|
||||
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)
|
||||
end
|
||||
|
||||
-- start flasher callback task
|
||||
flasher.run()
|
||||
|
||||
-- report ui as ready
|
||||
engine.ui_ready = true
|
||||
end
|
||||
|
||||
return status, msg
|
||||
end
|
||||
|
||||
-- close out the UI
|
||||
@@ -192,6 +240,7 @@ function renderer.close_ui()
|
||||
|
||||
-- delete element trees
|
||||
if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end
|
||||
if engine.ui.flow_display ~= nil then engine.ui.flow_display.delete() end
|
||||
for _, display in pairs(engine.ui.unit_displays) do display.delete() end
|
||||
|
||||
-- report ui as not ready
|
||||
@@ -199,11 +248,17 @@ function renderer.close_ui()
|
||||
|
||||
-- clear root UI elements
|
||||
engine.ui.main_display = nil
|
||||
engine.ui.flow_display = nil
|
||||
engine.ui.unit_displays = {}
|
||||
|
||||
-- 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()
|
||||
@@ -225,32 +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(0, 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.main = nil
|
||||
engine.ui.main_display = nil
|
||||
|
||||
is_used = true
|
||||
engine.monitors.unit_displays[idx] = nil
|
||||
engine.ui.unit_displays[idx] = 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(idx, false)
|
||||
break
|
||||
is_used = true
|
||||
engine.monitors.flow = nil
|
||||
engine.ui.flow_display = nil
|
||||
|
||||
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
|
||||
@@ -265,41 +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(0, 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
|
||||
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.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
|
||||
@@ -307,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
|
||||
@@ -315,14 +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
|
||||
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
|
||||
@@ -42,8 +42,8 @@ local function _api_handle_outq(session)
|
||||
|
||||
-- max 100ms spent processing queue
|
||||
if util.time() - handle_start > 100 then
|
||||
log.warning("[API] out queue handler exceeded 100ms queue process limit")
|
||||
log.warning(util.c("[API] offending session: ", session))
|
||||
log.warning("API: out queue handler exceeded 100ms queue process limit")
|
||||
log.warning(util.c("API: offending session: ", session))
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -59,19 +59,21 @@ 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
|
||||
|
||||
log.debug(util.c("[API] closed session ", session))
|
||||
log.debug(util.c("API: closed session ", session))
|
||||
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 = {
|
||||
@@ -114,7 +116,7 @@ function apisessions.establish_session(source_addr, version)
|
||||
setmetatable(pkt_s, mt)
|
||||
|
||||
iocontrol.fp_pkt_connected(id, version, source_addr)
|
||||
log.debug(util.c("[API] established new session: ", pkt_s))
|
||||
log.debug(util.c("API: established new session: ", pkt_s))
|
||||
|
||||
self.next_id = id + 1
|
||||
|
||||
@@ -130,7 +132,7 @@ function apisessions.check_all_watchdogs(timer_event)
|
||||
if session.open then
|
||||
local triggered = session.instance.check_wd(timer_event)
|
||||
if triggered then
|
||||
log.debug(util.c("[API] watchdog closing session ", session, "..."))
|
||||
log.debug(util.c("API: watchdog closing session ", session, "..."))
|
||||
_shutdown(session)
|
||||
end
|
||||
end
|
||||
@@ -156,7 +158,7 @@ function apisessions.free_all_closed()
|
||||
|
||||
---@param session pkt_session_struct
|
||||
local on_delete = function (session)
|
||||
log.debug(util.c("[API] free'ing closed session ", session))
|
||||
log.debug(util.c("API: free'ing closed session ", session))
|
||||
end
|
||||
|
||||
util.filter_table(self.sessions, f, on_delete)
|
||||
|
||||
@@ -8,8 +8,8 @@ local iocontrol = require("coordinator.iocontrol")
|
||||
local pocket = {}
|
||||
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
-- local CAPI_TYPE = comms.CAPI_TYPE
|
||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||
local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
|
||||
-- retry time constants in ms
|
||||
-- local INITIAL_WAIT = 1500
|
||||
@@ -72,22 +72,22 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
iocontrol.fp_pkt_disconnected(id)
|
||||
end
|
||||
|
||||
-- send a CAPI packet
|
||||
-----@param msg_type CAPI_TYPE
|
||||
-----@param msg table
|
||||
-- local function _send(msg_type, msg)
|
||||
-- local s_pkt = comms.scada_packet()
|
||||
-- local c_pkt = comms.capi_packet()
|
||||
-- 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()
|
||||
|
||||
-- c_pkt.make(msg_type, msg)
|
||||
-- s_pkt.make(self.seq_num, PROTOCOL.COORD_API, 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 SCADA_MGMT_TYPE
|
||||
---@param msg_type MGMT_TYPE
|
||||
---@param msg table
|
||||
local function _send_mgmt(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
@@ -101,7 +101,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
end
|
||||
|
||||
-- handle a packet
|
||||
---@param pkt mgmt_frame|capi_frame
|
||||
---@param pkt mgmt_frame|crdn_frame
|
||||
local function _handle_packet(pkt)
|
||||
-- check sequence number
|
||||
if self.r_seq_num == nil then
|
||||
@@ -117,17 +117,53 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
self.conn_watchdog.feed()
|
||||
|
||||
-- process packet
|
||||
if pkt.scada_frame.protocol() == PROTOCOL.COORD_API then
|
||||
---@cast pkt capi_frame
|
||||
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 CAPI packet type " .. pkt.type)
|
||||
log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type)
|
||||
end
|
||||
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
---@cast pkt mgmt_frame
|
||||
if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
if pkt.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive reply
|
||||
if pkt.length == 2 then
|
||||
local srv_start = pkt.data[1]
|
||||
@@ -146,7 +182,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then
|
||||
elseif pkt.type == MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_close()
|
||||
else
|
||||
@@ -174,7 +210,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
-- close the connection
|
||||
function public.close()
|
||||
_close()
|
||||
_send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
|
||||
_send_mgmt(MGMT_TYPE.CLOSE, {})
|
||||
log.info(log_header .. "session closed by server")
|
||||
end
|
||||
|
||||
@@ -229,7 +265,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
|
||||
periodics.keep_alive = periodics.keep_alive + elapsed
|
||||
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
|
||||
_send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() })
|
||||
_send_mgmt(MGMT_TYPE.KEEP_ALIVE, { util.time() })
|
||||
periodics.keep_alive = 0
|
||||
end
|
||||
|
||||
|
||||
@@ -2,269 +2,25 @@
|
||||
-- Alarm Sounder
|
||||
--
|
||||
|
||||
local audio = require("scada-common.audio")
|
||||
local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local ALARM = types.ALARM
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
|
||||
---@class sounder
|
||||
local sounder = {}
|
||||
|
||||
-- note: max samples = 0x20000 (128 * 1024 samples)
|
||||
|
||||
local _2_PI = 2 * math.pi -- 2 whole pies, hope you're hungry
|
||||
local _DRATE = 48000 -- 48kHz audio
|
||||
local _MAX_VAL = 127 / 2 -- max signed integer in this 8-bit audio
|
||||
local _05s_SAMPLES = 24000 -- half a second worth of samples
|
||||
|
||||
local test_alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
|
||||
|
||||
local alarm_ctl = {
|
||||
speaker = nil,
|
||||
volume = 0.5,
|
||||
playing = false,
|
||||
num_active = 0,
|
||||
next_block = 1,
|
||||
-- split audio up into 0.5s samples so specific components can be ended quicker
|
||||
quad_buffer = { {}, {}, {}, {} }
|
||||
stream = audio.new_stream()
|
||||
}
|
||||
|
||||
-- sounds modeled after https://www.e2s.com/references-and-guidelines/listen-and-download-alarm-tones
|
||||
|
||||
local T_340Hz_Int_2Hz = 1
|
||||
local T_544Hz_440Hz_Alt = 2
|
||||
local T_660Hz_Int_125ms = 3
|
||||
local T_745Hz_Int_1Hz = 4
|
||||
local T_800Hz_Int = 5
|
||||
local T_800Hz_1000Hz_Alt = 6
|
||||
local T_1000Hz_Int = 7
|
||||
local T_1800Hz_Int_4Hz = 8
|
||||
|
||||
local TONES = {
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 340Hz @ 2Hz Intermittent
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 544Hz 100mS / 440Hz 400mS Alternating
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 660Hz @ 125ms On 125ms Off
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 745Hz @ 1Hz Intermittent
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 800Hz @ 0.25s On 1.75s Off
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 800/1000Hz @ 0.25s Alternating
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 1KHz 1s on, 1s off Intermittent
|
||||
{ active = false, component = { {}, {}, {}, {} } } -- 1.8KHz @ 4Hz Intermittent
|
||||
}
|
||||
|
||||
-- calculate how many samples are in the given number of milliseconds
|
||||
---@nodiscard
|
||||
---@param ms integer milliseconds
|
||||
---@return integer samples
|
||||
local function ms_to_samples(ms) return math.floor(ms * 48) end
|
||||
|
||||
--#region Tone Generation (the Maths)
|
||||
|
||||
-- 340Hz @ 2Hz Intermittent
|
||||
local function gen_tone_1()
|
||||
local t, dt = 0, _2_PI * 340 / _DRATE
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
TONES[1].component[1][i] = val
|
||||
TONES[1].component[3][i] = val
|
||||
TONES[1].component[2][i] = 0
|
||||
TONES[1].component[4][i] = 0
|
||||
t = (t + dt) % _2_PI
|
||||
end
|
||||
end
|
||||
|
||||
-- 544Hz 100mS / 440Hz 400mS Alternating
|
||||
local function gen_tone_2()
|
||||
local t1, dt1 = 0, _2_PI * 544 / _DRATE
|
||||
local t2, dt2 = 0, _2_PI * 440 / _DRATE
|
||||
local alternate_at = ms_to_samples(100)
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local value
|
||||
|
||||
if i <= alternate_at then
|
||||
value = math.floor(math.sin(t1) * _MAX_VAL)
|
||||
t1 = (t1 + dt1) % _2_PI
|
||||
else
|
||||
value = math.floor(math.sin(t2) * _MAX_VAL)
|
||||
t2 = (t2 + dt2) % _2_PI
|
||||
end
|
||||
|
||||
TONES[2].component[1][i] = value
|
||||
TONES[2].component[2][i] = value
|
||||
TONES[2].component[3][i] = value
|
||||
TONES[2].component[4][i] = value
|
||||
end
|
||||
end
|
||||
|
||||
-- 660Hz @ 125ms On 125ms Off
|
||||
local function gen_tone_3()
|
||||
local elapsed_samples = 0
|
||||
local alternate_after = ms_to_samples(125)
|
||||
local alternate_at = alternate_after
|
||||
local mode = true
|
||||
|
||||
local t, dt = 0, _2_PI * 660 / _DRATE
|
||||
|
||||
for set = 1, 4 do
|
||||
for i = 1, _05s_SAMPLES do
|
||||
if mode then
|
||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
TONES[3].component[set][i] = val
|
||||
t = (t + dt) % _2_PI
|
||||
else
|
||||
t = 0
|
||||
TONES[3].component[set][i] = 0
|
||||
end
|
||||
|
||||
if elapsed_samples == alternate_at then
|
||||
mode = not mode
|
||||
alternate_at = elapsed_samples + alternate_after
|
||||
end
|
||||
|
||||
elapsed_samples = elapsed_samples + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 745Hz @ 1Hz Intermittent
|
||||
local function gen_tone_4()
|
||||
local t, dt = 0, _2_PI * 745 / _DRATE
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
TONES[4].component[1][i] = val
|
||||
TONES[4].component[3][i] = val
|
||||
TONES[4].component[2][i] = 0
|
||||
TONES[4].component[4][i] = 0
|
||||
t = (t + dt) % _2_PI
|
||||
end
|
||||
end
|
||||
|
||||
-- 800Hz @ 0.25s On 1.75s Off
|
||||
local function gen_tone_5()
|
||||
local t, dt = 0, _2_PI * 800 / _DRATE
|
||||
local stop_at = ms_to_samples(250)
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
|
||||
if i > stop_at then
|
||||
TONES[5].component[1][i] = val
|
||||
else
|
||||
TONES[5].component[1][i] = 0
|
||||
end
|
||||
|
||||
TONES[5].component[2][i] = 0
|
||||
TONES[5].component[3][i] = 0
|
||||
TONES[5].component[4][i] = 0
|
||||
|
||||
t = (t + dt) % _2_PI
|
||||
end
|
||||
end
|
||||
|
||||
-- 1000/800Hz @ 0.25s Alternating
|
||||
local function gen_tone_6()
|
||||
local t1, dt1 = 0, _2_PI * 1000 / _DRATE
|
||||
local t2, dt2 = 0, _2_PI * 800 / _DRATE
|
||||
|
||||
local alternate_at = ms_to_samples(250)
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val
|
||||
if i <= alternate_at then
|
||||
val = math.floor(math.sin(t1) * _MAX_VAL)
|
||||
t1 = (t1 + dt1) % _2_PI
|
||||
else
|
||||
val = math.floor(math.sin(t2) * _MAX_VAL)
|
||||
t2 = (t2 + dt2) % _2_PI
|
||||
end
|
||||
|
||||
TONES[6].component[1][i] = val
|
||||
TONES[6].component[2][i] = val
|
||||
TONES[6].component[3][i] = val
|
||||
TONES[6].component[4][i] = val
|
||||
end
|
||||
end
|
||||
|
||||
-- 1KHz 1s on, 1s off Intermittent
|
||||
local function gen_tone_7()
|
||||
local t, dt = 0, _2_PI * 1000 / _DRATE
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
TONES[7].component[1][i] = val
|
||||
TONES[7].component[2][i] = val
|
||||
TONES[7].component[3][i] = 0
|
||||
TONES[7].component[4][i] = 0
|
||||
t = (t + dt) % _2_PI
|
||||
end
|
||||
end
|
||||
|
||||
-- 1800Hz @ 4Hz Intermittent
|
||||
local function gen_tone_8()
|
||||
local t, dt = 0, _2_PI * 1800 / _DRATE
|
||||
|
||||
local off_at = ms_to_samples(250)
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val = 0
|
||||
|
||||
if i <= off_at then
|
||||
val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
t = (t + dt) % _2_PI
|
||||
end
|
||||
|
||||
TONES[8].component[1][i] = val
|
||||
TONES[8].component[2][i] = val
|
||||
TONES[8].component[3][i] = val
|
||||
TONES[8].component[4][i] = val
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- hard audio limiter
|
||||
---@nodiscard
|
||||
---@param output number output level
|
||||
---@return number limited -128.0 to 127.0
|
||||
local function limit(output)
|
||||
return math.max(-128, math.min(127, output))
|
||||
end
|
||||
|
||||
-- zero the alarm audio buffer
|
||||
local function zero()
|
||||
for i = 1, 4 do
|
||||
for s = 1, _05s_SAMPLES do alarm_ctl.quad_buffer[i][s] = 0 end
|
||||
end
|
||||
end
|
||||
|
||||
-- add an alarm to the output buffer
|
||||
---@param alarm_idx integer tone ID
|
||||
local function add(alarm_idx)
|
||||
alarm_ctl.num_active = alarm_ctl.num_active + 1
|
||||
TONES[alarm_idx].active = true
|
||||
|
||||
for i = 1, 4 do
|
||||
for s = 1, _05s_SAMPLES do
|
||||
alarm_ctl.quad_buffer[i][s] = limit(alarm_ctl.quad_buffer[i][s] + TONES[alarm_idx].component[i][s])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- start audio or continue audio on buffer empty
|
||||
---@return boolean success successfully added buffer to audio output
|
||||
local function play()
|
||||
if not alarm_ctl.playing then
|
||||
alarm_ctl.playing = true
|
||||
alarm_ctl.next_block = 1
|
||||
|
||||
return sounder.continue()
|
||||
else
|
||||
return true
|
||||
end
|
||||
else return true end
|
||||
end
|
||||
|
||||
-- initialize the annunciator alarm system
|
||||
@@ -273,23 +29,10 @@ end
|
||||
function sounder.init(speaker, volume)
|
||||
alarm_ctl.speaker = speaker
|
||||
alarm_ctl.speaker.stop()
|
||||
|
||||
alarm_ctl.volume = volume
|
||||
alarm_ctl.playing = false
|
||||
alarm_ctl.num_active = 0
|
||||
alarm_ctl.next_block = 1
|
||||
alarm_ctl.stream.stop()
|
||||
|
||||
zero()
|
||||
|
||||
-- generate tones
|
||||
gen_tone_1()
|
||||
gen_tone_2()
|
||||
gen_tone_3()
|
||||
gen_tone_4()
|
||||
gen_tone_5()
|
||||
gen_tone_6()
|
||||
gen_tone_7()
|
||||
gen_tone_8()
|
||||
audio.generate_tones()
|
||||
end
|
||||
|
||||
-- reconnect the speaker peripheral
|
||||
@@ -297,173 +40,40 @@ end
|
||||
function sounder.reconnect(speaker)
|
||||
alarm_ctl.speaker = speaker
|
||||
alarm_ctl.playing = false
|
||||
alarm_ctl.next_block = 1
|
||||
alarm_ctl.num_active = 0
|
||||
for id = 1, #TONES do TONES[id].active = false end
|
||||
alarm_ctl.stream.stop()
|
||||
end
|
||||
|
||||
-- check alarm state to enable/disable alarms
|
||||
---@param units table|nil unit list or nil to use test mode
|
||||
function sounder.eval(units)
|
||||
local changed = false
|
||||
local any_active = false
|
||||
local new_states = { false, false, false, false, false, false, false, false }
|
||||
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
|
||||
-- set alarm tones
|
||||
---@param states table alarm tone commands from supervisor
|
||||
function sounder.set(states)
|
||||
-- set tone states
|
||||
for id = 1, #states do alarm_ctl.stream.set_active(id, states[id]) end
|
||||
|
||||
if units ~= nil then
|
||||
-- check all alarms for all units
|
||||
for i = 1, #units do
|
||||
local unit = units[i] ---@type ioctl_unit
|
||||
for id = 1, #unit.alarms do
|
||||
alarms[id] = alarms[id] or (unit.alarms[id] == ALARM_STATE.TRIPPED)
|
||||
end
|
||||
end
|
||||
else
|
||||
alarms = test_alarms
|
||||
end
|
||||
|
||||
-- containment breach is worst case CRITICAL alarm, this takes priority
|
||||
if alarms[ALARM.ContainmentBreach] then
|
||||
new_states[T_1800Hz_Int_4Hz] = true
|
||||
else
|
||||
-- critical damage is highest priority CRITICAL level alarm
|
||||
if alarms[ALARM.CriticalDamage] then
|
||||
new_states[T_660Hz_Int_125ms] = true
|
||||
else
|
||||
-- EMERGENCY level alarms + URGENT over temp
|
||||
if alarms[ALARM.ReactorDamage] or alarms[ALARM.ReactorOverTemp] or alarms[ALARM.ReactorWasteLeak] then
|
||||
new_states[T_544Hz_440Hz_Alt] = true
|
||||
-- URGENT level turbine trip
|
||||
elseif alarms[ALARM.TurbineTrip] then
|
||||
new_states[T_745Hz_Int_1Hz] = true
|
||||
-- URGENT level reactor lost
|
||||
elseif alarms[ALARM.ReactorLost] then
|
||||
new_states[T_340Hz_Int_2Hz] = true
|
||||
-- TIMELY level alarms
|
||||
elseif alarms[ALARM.ReactorHighTemp] or alarms[ALARM.ReactorHighWaste] or alarms[ALARM.RCSTransient] then
|
||||
new_states[T_800Hz_Int] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- check RPS transient URGENT level alarm
|
||||
if alarms[ALARM.RPSTransient] then
|
||||
new_states[T_1000Hz_Int] = true
|
||||
-- disable really painful audio combination
|
||||
new_states[T_340Hz_Int_2Hz] = false
|
||||
end
|
||||
end
|
||||
|
||||
-- radiation is a big concern, always play this CRITICAL level alarm if active
|
||||
if alarms[ALARM.ContainmentRadiation] then
|
||||
new_states[T_800Hz_1000Hz_Alt] = true
|
||||
-- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled
|
||||
-- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one
|
||||
if new_states[T_1000Hz_Int] and alarms[ALARM.ReactorLost] then new_states[T_340Hz_Int_2Hz] = true end
|
||||
-- it sounds *really* bad if this is in conjunction with these other tones, so disable them
|
||||
new_states[T_745Hz_Int_1Hz] = false
|
||||
new_states[T_800Hz_Int] = false
|
||||
new_states[T_1000Hz_Int] = false
|
||||
end
|
||||
|
||||
-- check if any changed, check if any active, update active flags
|
||||
for id = 1, #TONES do
|
||||
if new_states[id] ~= TONES[id].active then
|
||||
TONES[id].active = new_states[id]
|
||||
changed = true
|
||||
end
|
||||
|
||||
if TONES[id].active then any_active = true end
|
||||
end
|
||||
|
||||
-- zero and re-add tones if changed
|
||||
if changed then
|
||||
zero()
|
||||
|
||||
for id = 1, #TONES do
|
||||
if TONES[id].active then add(id) end
|
||||
end
|
||||
end
|
||||
|
||||
if any_active then play() else sounder.stop() end
|
||||
-- re-compute output if needed, then play audio if available
|
||||
if alarm_ctl.stream.is_recompute_needed() then alarm_ctl.stream.compute_buffer() end
|
||||
if alarm_ctl.stream.any_active() then play() else sounder.stop() end
|
||||
end
|
||||
|
||||
-- stop all audio and clear output buffer
|
||||
function sounder.stop()
|
||||
alarm_ctl.playing = false
|
||||
alarm_ctl.speaker.stop()
|
||||
alarm_ctl.next_block = 1
|
||||
alarm_ctl.num_active = 0
|
||||
for id = 1, #TONES do TONES[id].active = false end
|
||||
zero()
|
||||
alarm_ctl.stream.stop()
|
||||
end
|
||||
|
||||
-- continue audio on buffer empty
|
||||
---@return boolean success successfully added buffer to audio output
|
||||
function sounder.continue()
|
||||
local success = false
|
||||
|
||||
if alarm_ctl.playing then
|
||||
if alarm_ctl.speaker ~= nil and #alarm_ctl.quad_buffer[alarm_ctl.next_block] > 0 then
|
||||
local success = alarm_ctl.speaker.playAudio(alarm_ctl.quad_buffer[alarm_ctl.next_block], alarm_ctl.volume)
|
||||
|
||||
alarm_ctl.next_block = alarm_ctl.next_block + 1
|
||||
if alarm_ctl.next_block > 4 then alarm_ctl.next_block = 1 end
|
||||
|
||||
if not success then
|
||||
log.debug("SOUNDER: error playing audio")
|
||||
end
|
||||
|
||||
return success
|
||||
else
|
||||
return false
|
||||
end
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
--#region Test Functions
|
||||
|
||||
function sounder.test_1() add(1) play() end -- play tone T_340Hz_Int_2Hz
|
||||
function sounder.test_2() add(2) play() end -- play tone T_544Hz_440Hz_Alt
|
||||
function sounder.test_3() add(3) play() end -- play tone T_660Hz_Int_125ms
|
||||
function sounder.test_4() add(4) play() end -- play tone T_745Hz_Int_1Hz
|
||||
function sounder.test_5() add(5) play() end -- play tone T_800Hz_Int
|
||||
function sounder.test_6() add(6) play() end -- play tone T_800Hz_1000Hz_Alt
|
||||
function sounder.test_7() add(7) play() end -- play tone T_1000Hz_Int
|
||||
function sounder.test_8() add(8) play() end -- play tone T_1800Hz_Int_4Hz
|
||||
|
||||
function sounder.test_breach(active) test_alarms[ALARM.ContainmentBreach] = active end ---@param active boolean
|
||||
function sounder.test_rad(active) test_alarms[ALARM.ContainmentRadiation] = active end ---@param active boolean
|
||||
function sounder.test_lost(active) test_alarms[ALARM.ReactorLost] = active end ---@param active boolean
|
||||
function sounder.test_crit(active) test_alarms[ALARM.CriticalDamage] = active end ---@param active boolean
|
||||
function sounder.test_dmg(active) test_alarms[ALARM.ReactorDamage] = active end ---@param active boolean
|
||||
function sounder.test_overtemp(active) test_alarms[ALARM.ReactorOverTemp] = active end ---@param active boolean
|
||||
function sounder.test_hightemp(active) test_alarms[ALARM.ReactorHighTemp] = active end ---@param active boolean
|
||||
function sounder.test_wasteleak(active) test_alarms[ALARM.ReactorWasteLeak] = active end ---@param active boolean
|
||||
function sounder.test_highwaste(active) test_alarms[ALARM.ReactorHighWaste] = active end ---@param active boolean
|
||||
function sounder.test_rps(active) test_alarms[ALARM.RPSTransient] = active end ---@param active boolean
|
||||
function sounder.test_rcs(active) test_alarms[ALARM.RCSTransient] = active end ---@param active boolean
|
||||
function sounder.test_turbinet(active) test_alarms[ALARM.TurbineTrip] = active end ---@param active boolean
|
||||
|
||||
-- power rescaling limiter test
|
||||
function sounder.test_power_scale()
|
||||
local start = util.time_ms()
|
||||
|
||||
zero()
|
||||
|
||||
for id = 1, #TONES do
|
||||
if TONES[id].active then
|
||||
for i = 1, 4 do
|
||||
for s = 1, _05s_SAMPLES do
|
||||
alarm_ctl.quad_buffer[i][s] = limit(alarm_ctl.quad_buffer[i][s] +
|
||||
(TONES[id].component[i][s] / math.sqrt(alarm_ctl.num_active)))
|
||||
end
|
||||
end
|
||||
if alarm_ctl.speaker ~= nil and alarm_ctl.stream.has_next_block() then
|
||||
success = alarm_ctl.speaker.playAudio(alarm_ctl.stream.get_next_block(), alarm_ctl.volume)
|
||||
if not success then log.error("SOUNDER: error playing audio") end
|
||||
end
|
||||
end
|
||||
|
||||
log.debug("SOUNDER: power rescale test took " .. (util.time_ms() - start) .. "ms")
|
||||
return success
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
return sounder
|
||||
|
||||
@@ -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 = "v0.21.2"
|
||||
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,49 +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)
|
||||
if not configured or monitors == nil then
|
||||
println("startup> monitor setup failed")
|
||||
log.fatal("monitor configuration failed")
|
||||
return
|
||||
end
|
||||
|
||||
-- init renderer
|
||||
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 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")
|
||||
@@ -127,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)
|
||||
@@ -138,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")
|
||||
@@ -156,238 +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.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 = pcall(renderer.start_fp)
|
||||
if not fp_ok then
|
||||
renderer.close_fp()
|
||||
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 = pcall(renderer.start_ui)
|
||||
if not ui_ok then
|
||||
renderer.close_ui()
|
||||
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" 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 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")
|
||||
@@ -18,23 +20,25 @@ local border = core.border
|
||||
---@param y integer top left y
|
||||
---@param ps psil ps interface
|
||||
local function new_view(root, x, y, ps)
|
||||
local boiler = Rectangle{parent=root,border=border(1, colors.gray, true),width=31,height=7,x=x,y=y}
|
||||
local text_fg = style.theme.text_fg
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
local text_fg_bg = cpair(colors.black, colors.lightGray)
|
||||
local lu_col = cpair(colors.gray, colors.gray)
|
||||
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=lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg}
|
||||
local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg}
|
||||
local 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")
|
||||
@@ -16,7 +17,7 @@ local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
-- new induction matrix view
|
||||
---@param root graphics_element parent
|
||||
@@ -26,71 +27,120 @@ local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
---@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=cpair(colors.lightGray,colors.gray)}
|
||||
TextBox{parent=matrix,text=title,alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.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 text_fg_bg = cpair(colors.black, colors.lightGray)
|
||||
local label_fg_bg = cpair(colors.gray, colors.lightGray)
|
||||
local lu_col = cpair(colors.gray, colors.gray)
|
||||
|
||||
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
|
||||
return val / data.build.transfer_cap
|
||||
else
|
||||
return 0
|
||||
end
|
||||
else return 0 end
|
||||
end
|
||||
|
||||
charge.register(ps, "energy_fill", charge.update)
|
||||
in_cap.register(ps, "last_input", function (val) in_cap.update(calc_saturation(val)) end)
|
||||
out_cap.register(ps, "last_output", function (val) out_cap.update(calc_saturation(val)) end)
|
||||
|
||||
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
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
@@ -11,7 +13,7 @@ local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
@@ -19,26 +21,31 @@ local cpair = core.cpair
|
||||
---@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=cpair(colors.black,colors.white)}
|
||||
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=cpair(colors.black,colors.lightGray)}
|
||||
local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)}
|
||||
TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
|
||||
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=cpair(colors.lightGray,colors.white)}
|
||||
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=cpair(colors.lightGray,colors.white)}
|
||||
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
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)
|
||||
|
||||
|
||||
@@ -23,11 +23,13 @@ local HazardButton = require("graphics.elements.controls.hazard_button")
|
||||
local RadioButton = require("graphics.elements.controls.radio_button")
|
||||
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
|
||||
local period = core.flasher.PERIOD
|
||||
|
||||
-- new process control view
|
||||
@@ -35,16 +37,28 @@ 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)
|
||||
local blk_brn = cpair(colors.black, colors.brown)
|
||||
local blk_pur = cpair(colors.black, colors.purple)
|
||||
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
|
||||
local bw_fg_bg = cpair(colors.black, colors.white)
|
||||
local hzd_fg_bg = cpair(colors.white, colors.gray)
|
||||
local lu_cpair = cpair(colors.gray, colors.gray)
|
||||
local dis_colors = cpair(colors.white, colors.lightGray)
|
||||
|
||||
local main = Div{parent=root,width=128,height=24,x=x,y=y}
|
||||
|
||||
local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,fg_bg=hzd_fg_bg}
|
||||
@@ -53,10 +67,10 @@ local function new_view(root, x, y)
|
||||
facility.scram_ack = scram.on_response
|
||||
facility.ack_alarms_ack = ack_a.on_response
|
||||
|
||||
local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=cpair(colors.green,colors.red)}
|
||||
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
|
||||
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=cpair(colors.green,colors.gray)}
|
||||
local sps = IndicatorLight{parent=main,label="SPS Connected",colors=cpair(colors.green,colors.gray)}
|
||||
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=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}
|
||||
|
||||
all_ok.register(facility.ps, "all_sys_ok", all_ok.update)
|
||||
rad_mon.register(facility.ps, "rad_computed_status", rad_mon.update)
|
||||
@@ -65,10 +79,10 @@ local function new_view(root, x, y)
|
||||
|
||||
main.line_break()
|
||||
|
||||
local auto_ready = IndicatorLight{parent=main,label="Configured Units Ready",colors=cpair(colors.green,colors.red)}
|
||||
local auto_act = IndicatorLight{parent=main,label="Process Active",colors=cpair(colors.green,colors.gray)}
|
||||
local auto_ramp = IndicatorLight{parent=main,label="Process Ramping",colors=cpair(colors.white,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local auto_sat = IndicatorLight{parent=main,label="Min/Max Burn Rate",colors=cpair(colors.yellow,colors.gray)}
|
||||
local auto_ready = IndicatorLight{parent=main,label="Configured Units Ready",colors=ind_grn}
|
||||
local auto_act = IndicatorLight{parent=main,label="Process Active",colors=ind_grn}
|
||||
local auto_ramp = IndicatorLight{parent=main,label="Process Ramping",colors=ind_wht,flash=true,period=period.BLINK_250_MS}
|
||||
local auto_sat = IndicatorLight{parent=main,label="Min/Max Burn Rate",colors=ind_yel}
|
||||
|
||||
auto_ready.register(facility.ps, "auto_ready", auto_ready.update)
|
||||
auto_act.register(facility.ps, "auto_active", auto_act.update)
|
||||
@@ -77,12 +91,12 @@ local function new_view(root, x, y)
|
||||
|
||||
main.line_break()
|
||||
|
||||
local auto_scram = IndicatorLight{parent=main,label="Automatic SCRAM",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local matrix_dc = IndicatorLight{parent=main,label="Matrix Disconnected",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local matrix_fill = IndicatorLight{parent=main,label="Matrix Charge High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local unit_crit = IndicatorLight{parent=main,label="Unit Critical Alarm",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local fac_rad_h = IndicatorLight{parent=main,label="Facility Radiation High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local gen_fault = IndicatorLight{parent=main,label="Gen. Control Fault",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local auto_scram = IndicatorLight{parent=main,label="Automatic SCRAM",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local matrix_dc = IndicatorLight{parent=main,label="Matrix Disconnected",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
|
||||
local matrix_fill = IndicatorLight{parent=main,label="Matrix Charge High",colors=ind_red,flash=true,period=period.BLINK_500_MS}
|
||||
local unit_crit = IndicatorLight{parent=main,label="Unit Critical Alarm",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local fac_rad_h = IndicatorLight{parent=main,label="Facility Radiation High",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local gen_fault = IndicatorLight{parent=main,label="Gen. Control Fault",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
|
||||
|
||||
auto_scram.register(facility.ps, "auto_scram", auto_scram.update)
|
||||
matrix_dc.register(facility.ps, "as_matrix_dc", matrix_dc.update)
|
||||
@@ -92,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)
|
||||
|
||||
---------------------
|
||||
@@ -111,35 +125,35 @@ local function new_view(root, x, y)
|
||||
|
||||
local targets = Div{parent=proc,width=31,height=24,x=1,y=1}
|
||||
|
||||
local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
|
||||
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=cpair(colors.gray,colors.white)}
|
||||
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
|
||||
TextBox{parent=burn_target,x=18,y=2,text="mB/t"}
|
||||
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
|
||||
local burn_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)
|
||||
burn_sum.register(facility.ps, "burn_sum", burn_sum.update)
|
||||
|
||||
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
|
||||
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=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=cpair(colors.gray,colors.white)}
|
||||
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
|
||||
TextBox{parent=chg_target,x=18,y=2,text="MFE"}
|
||||
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
|
||||
local 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=cpair(colors.black,colors.purple)}
|
||||
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=cpair(colors.gray,colors.white)}
|
||||
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
|
||||
TextBox{parent=gen_target,x=18,y=2,text="kFE/t"}
|
||||
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
|
||||
local 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)
|
||||
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end)
|
||||
@@ -154,18 +168,18 @@ local function new_view(root, x, y)
|
||||
|
||||
for i = 1, 4 do
|
||||
local unit
|
||||
local tag_fg_bg = cpair(colors.gray,colors.white)
|
||||
local lim_fg_bg = cpair(colors.lightGray,colors.white)
|
||||
local ctl_fg = colors.lightGray
|
||||
local cur_fg_bg = cpair(colors.lightGray,colors.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
|
||||
cur_fg_bg = cpair(colors.black,colors.brown)
|
||||
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
|
||||
|
||||
@@ -174,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=cpair(colors.gray,colors.white),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}
|
||||
|
||||
@@ -198,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 = cpair(colors.gray,colors.white)
|
||||
local ind_fg_bg = cpair(colors.lightGray,colors.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
|
||||
tag_fg_bg = cpair(colors.black, colors.cyan)
|
||||
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
|
||||
@@ -214,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
|
||||
@@ -230,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.white,colors.black),radio_bg=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=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
|
||||
local stat_line_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=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=cpair(colors.gray,colors.white)}
|
||||
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()
|
||||
@@ -307,7 +321,7 @@ local function new_view(root, x, y)
|
||||
local unit = units[i] ---@type ioctl_unit
|
||||
|
||||
TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8,height=1}
|
||||
local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=cpair(colors.white,colors.gray)}
|
||||
local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=ind_wht}
|
||||
local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.waste.states_abbrv,value=1,min_width=6}
|
||||
|
||||
a_waste.register(unit.unit_ps, "U_AutoWaste", a_waste.update)
|
||||
@@ -316,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=cpair(colors.black,colors.brown)}
|
||||
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=TEXT_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.white,colors.black),radio_bg=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=cpair(colors.white,colors.gray)}
|
||||
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="%12.2f",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")
|
||||
@@ -20,29 +22,31 @@ local border = core.border
|
||||
---@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 text_fg_bg = cpair(colors.black, colors.lightGray)
|
||||
local lu_col = cpair(colors.gray, colors.gray)
|
||||
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}
|
||||
|
||||
@@ -21,24 +21,24 @@ local border = core.border
|
||||
---@param y integer top left y
|
||||
---@param ps psil ps interface
|
||||
local function new_view(root, x, y, ps)
|
||||
local turbine = Rectangle{parent=root,border=border(1, colors.gray, true),width=23,height=7,x=x,y=y}
|
||||
local text_fg = style.theme.text_fg
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
local text_fg_bg = cpair(colors.black, colors.lightGray)
|
||||
local lu_col = cpair(colors.gray, colors.gray)
|
||||
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)
|
||||
flow_rate.register(ps, "flow_rate", flow_rate.update)
|
||||
flow_rate.register(ps, "steam_input_rate", flow_rate.update)
|
||||
|
||||
local steam = VerticalBar{parent=turbine,x=2,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
|
||||
local energy = VerticalBar{parent=turbine,x=3,y=1,fg_bg=cpair(colors.green,colors.gray),height=4,width=1}
|
||||
|
||||
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)
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
-- Reactor Unit SCADA Coordinator GUI
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
@@ -26,19 +29,39 @@ local PushButton = require("graphics.elements.controls.push_button")
|
||||
local RadioButton = require("graphics.elements.controls.radio_button")
|
||||
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local gry_wht = style.gray_white
|
||||
|
||||
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}
|
||||
|
||||
@@ -48,11 +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=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
|
||||
local bw_fg_bg = cpair(colors.black, colors.white)
|
||||
local hzd_fg_bg = cpair(colors.white, colors.gray)
|
||||
local lu_cpair = cpair(colors.gray, colors.gray)
|
||||
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,height=1,fg_bg=style.theme.header}
|
||||
|
||||
-----------------------------
|
||||
-- main stats and core map --
|
||||
@@ -63,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}
|
||||
@@ -77,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}
|
||||
@@ -88,7 +107,7 @@ local function init(parent, id)
|
||||
waste.register(u_ps, "waste_fill", waste.update)
|
||||
|
||||
ccool.register(u_ps, "ccool_type", function (type)
|
||||
if type == "mekanism:sodium" then
|
||||
if type == types.FLUID.SODIUM then
|
||||
ccool.recolor(cpair(colors.lightBlue, colors.gray))
|
||||
else
|
||||
ccool.recolor(cpair(colors.blue, colors.gray))
|
||||
@@ -96,7 +115,7 @@ local function init(parent, id)
|
||||
end)
|
||||
|
||||
hcool.register(u_ps, "hcool_type", function (type)
|
||||
if type == "mekanism:superheated_sodium" then
|
||||
if type == types.FLUID.SUPERHEATED_SODIUM then
|
||||
hcool.recolor(cpair(colors.orange, colors.gray))
|
||||
else
|
||||
hcool.recolor(cpair(colors.white, colors.gray))
|
||||
@@ -104,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)
|
||||
|
||||
-------------------
|
||||
@@ -124,8 +144,8 @@ local function init(parent, id)
|
||||
-------------------
|
||||
|
||||
local u_stat = Rectangle{parent=main,border=border(1,colors.gray,true),thin=true,width=33,height=4,x=46,y=3,fg_bg=bw_fg_bg}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,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=33,height=1,alignment=ALIGN.CENTER,fg_bg=gry_wht}
|
||||
|
||||
stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value)
|
||||
stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value)
|
||||
@@ -139,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_hbeat = IndicatorLight{parent=annunciator,label="PLC Heartbeat",colors=cpair(colors.white,colors.gray)}
|
||||
local rad_mon = TriIndicatorLight{parent=annunciator,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
|
||||
local 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=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)
|
||||
@@ -150,25 +170,25 @@ local function init(parent, id)
|
||||
annunciator.line_break()
|
||||
|
||||
-- operating state
|
||||
local r_active = IndicatorLight{parent=annunciator,label="Active",colors=cpair(colors.green,colors.gray)}
|
||||
local r_auto = IndicatorLight{parent=annunciator,label="Automatic Control",colors=cpair(colors.white,colors.gray)}
|
||||
local r_active = IndicatorLight{parent=annunciator,label="Active",colors=ind_grn}
|
||||
local r_auto = IndicatorLight{parent=annunciator,label="Automatic Control",colors=ind_wht}
|
||||
|
||||
r_active.register(u_ps, "status", r_active.update)
|
||||
r_auto.register(u_ps, "AutoControl", r_auto.update)
|
||||
|
||||
-- main unit transient/warning annunciator panel
|
||||
local r_scram = IndicatorLight{parent=annunciator,label="Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
|
||||
local r_mscrm = IndicatorLight{parent=annunciator,label="Manual Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
|
||||
local r_ascrm = IndicatorLight{parent=annunciator,label="Auto Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
|
||||
local rad_wrn = IndicatorLight{parent=annunciator,label="Radiation Warning",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=cpair(colors.red,colors.gray)}
|
||||
local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=cpair(colors.red,colors.gray)}
|
||||
local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_wloc = IndicatorLight{parent=annunciator,label="Waste Line Occlusion",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_hsrt = IndicatorLight{parent=annunciator,label="Startup Rate High",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_scram = IndicatorLight{parent=annunciator,label="Reactor SCRAM",colors=ind_red}
|
||||
local r_mscrm = IndicatorLight{parent=annunciator,label="Manual Reactor SCRAM",colors=ind_red}
|
||||
local r_ascrm = IndicatorLight{parent=annunciator,label="Auto Reactor SCRAM",colors=ind_red}
|
||||
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_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}
|
||||
local r_wloc = IndicatorLight{parent=annunciator,label="Waste Line Occlusion",colors=ind_yel}
|
||||
local r_hsrt = IndicatorLight{parent=annunciator,label="Startup Rate High",colors=ind_yel}
|
||||
|
||||
r_scram.register(u_ps, "ReactorSCRAM", r_scram.update)
|
||||
r_mscrm.register(u_ps, "ManualReactorSCRAM", r_mscrm.update)
|
||||
@@ -185,20 +205,20 @@ local function init(parent, id)
|
||||
|
||||
-- RPS annunciator panel
|
||||
|
||||
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=8}
|
||||
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=8}
|
||||
local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9}
|
||||
local rps_annunc = Div{parent=rps,width=31,height=10,x=2,y=1}
|
||||
|
||||
local rps_trp = IndicatorLight{parent=rps_annunc,label="RPS Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local rps_dmg = IndicatorLight{parent=rps_annunc,label="Damage Level High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local rps_exh = IndicatorLight{parent=rps_annunc,label="Excess Heated Coolant",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_exw = IndicatorLight{parent=rps_annunc,label="Excess Waste",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_tmp = IndicatorLight{parent=rps_annunc,label="Core Temperature High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local rps_nof = IndicatorLight{parent=rps_annunc,label="No Fuel",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_loc = IndicatorLight{parent=rps_annunc,label="Coolant Level Low Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_flt = IndicatorLight{parent=rps_annunc,label="PPM Fault",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local rps_tmo = IndicatorLight{parent=rps_annunc,label="Connection Timeout",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local rps_sfl = IndicatorLight{parent=rps_annunc,label="System Failure",colors=cpair(colors.orange,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local rps_trp = IndicatorLight{parent=rps_annunc,label="RPS Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local rps_dmg = IndicatorLight{parent=rps_annunc,label="Damage Level High",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local rps_exh = IndicatorLight{parent=rps_annunc,label="Excess Heated Coolant",colors=ind_yel}
|
||||
local rps_exw = IndicatorLight{parent=rps_annunc,label="Excess Waste",colors=ind_yel}
|
||||
local rps_tmp = IndicatorLight{parent=rps_annunc,label="Core Temperature High",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local rps_nof = IndicatorLight{parent=rps_annunc,label="No Fuel",colors=ind_yel}
|
||||
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=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)
|
||||
@@ -213,17 +233,17 @@ local function init(parent, id)
|
||||
|
||||
-- cooling annunciator panel
|
||||
|
||||
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=22}
|
||||
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=22}
|
||||
local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23}
|
||||
local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1}
|
||||
local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7}
|
||||
|
||||
local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=colors.gray,c2=colors.white,c3=colors.green}
|
||||
local c_cfm = IndicatorLight{parent=rcs_annunc,label="Coolant Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=ind_yel}
|
||||
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}
|
||||
local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=ind_yel}
|
||||
|
||||
c_flt.register(u_ps, "RCSFault", c_flt.update)
|
||||
c_emg.register(u_ps, "EmergencyCoolant", c_emg.update)
|
||||
@@ -242,15 +262,15 @@ 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=cpair(colors.red,colors.gray)}
|
||||
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}
|
||||
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
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
|
||||
if unit.num_boilers > 1 then
|
||||
@@ -261,12 +281,12 @@ local function init(parent, id)
|
||||
_add_space()
|
||||
end
|
||||
|
||||
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=cpair(colors.red,colors.gray)}
|
||||
b2_wll.register(b_ps[2], "WasterLevelLow", b2_wll.update)
|
||||
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], "WaterLevelLow", b2_wll.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
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
|
||||
|
||||
@@ -274,20 +294,20 @@ 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}
|
||||
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||
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}
|
||||
local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
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}
|
||||
local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
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)
|
||||
|
||||
if unit.num_turbines > 1 then
|
||||
@@ -295,55 +315,55 @@ 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}
|
||||
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||
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}
|
||||
local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
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}
|
||||
local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
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
|
||||
|
||||
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}
|
||||
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||
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}
|
||||
local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
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}
|
||||
local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
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 dis_colors = cpair(colors.white, colors.lightGray)
|
||||
|
||||
local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=cpair(colors.gray,colors.white)}
|
||||
local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
|
||||
TextBox{parent=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=cpair(colors.white,colors.gray),dis_fg_bg=dis_colors,callback=set_burn}
|
||||
local set_burn_btn = PushButton{parent=burn_control,x=14,y=2,text="SET",min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=style.wh_gray,dis_fg_bg=dis_colors,callback=set_burn}
|
||||
|
||||
burn_rate.register(u_ps, "burn_rate", burn_rate.set_value)
|
||||
burn_rate.register(u_ps, "max_burn", burn_rate.set_max)
|
||||
@@ -374,7 +394,7 @@ local function init(parent, id)
|
||||
|
||||
reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
|
||||
|
||||
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=48}
|
||||
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=48}
|
||||
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
|
||||
local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1}
|
||||
|
||||
@@ -386,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)
|
||||
@@ -454,40 +474,40 @@ 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 --
|
||||
--------------------------------
|
||||
|
||||
TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=TEXT_ALIGN.CENTER,width=13,height=1,x=32,y=36}
|
||||
TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,height=1,x=32,y=36}
|
||||
local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37}
|
||||
local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1}
|
||||
|
||||
local ctl_opts = { "Manual", "Primary", "Secondary", "Tertiary", "Backup" }
|
||||
|
||||
local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.blue,colors.white),radio_bg=colors.gray}
|
||||
local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(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)
|
||||
|
||||
auto_div.line_break()
|
||||
|
||||
local function set_group() unit.set_group(group.get_value() - 1) end
|
||||
local set_grp_btn = PushButton{parent=auto_div,text="SET",x=4,min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),dis_fg_bg=cpair(colors.gray,colors.white),callback=set_group}
|
||||
local set_grp_btn = PushButton{parent=auto_div,text="SET",x=4,min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=style.wh_gray,dis_fg_bg=gry_wht,callback=set_group}
|
||||
|
||||
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)
|
||||
|
||||
auto_div.line_break()
|
||||
|
||||
local a_rdy = IndicatorLight{parent=auto_div,label="Ready",x=2,colors=cpair(colors.green,colors.gray)}
|
||||
local a_stb = IndicatorLight{parent=auto_div,label="Standby",x=2,colors=cpair(colors.white,colors.gray),flash=true,period=period.BLINK_1000_MS}
|
||||
local a_rdy = IndicatorLight{parent=auto_div,label="Ready",x=2,colors=ind_grn}
|
||||
local a_stb = IndicatorLight{parent=auto_div,label="Standby",x=2,colors=ind_wht,flash=true,period=period.BLINK_1000_MS}
|
||||
|
||||
a_rdy.register(u_ps, "U_AutoReady", a_rdy.update)
|
||||
|
||||
|
||||
229
coordinator/ui/components/unit_flow.lua
Normal file
229
coordinator/ui/components/unit_flow.lua
Normal file
@@ -0,0 +1,229 @@
|
||||
--
|
||||
-- Basic Unit Flow Overview
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local PipeNetwork = require("graphics.elements.pipenet")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
|
||||
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
|
||||
|
||||
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 lg_gray = style.lg_gray
|
||||
|
||||
-- make a new unit flow window
|
||||
---@param parent graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@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)
|
||||
local prv_start = 1 + ((unit.unit_id - 1) * 3)
|
||||
local v_fields = { "pu", "po", "pl", "am" }
|
||||
local v_names = {
|
||||
sprintf("PV%02d-PU", v_start),
|
||||
sprintf("PV%02d-PO", v_start + 1),
|
||||
sprintf("PV%02d-PL", v_start + 2),
|
||||
sprintf("PV%02d-AM", v_start + 3),
|
||||
sprintf("PRV%02d", prv_start),
|
||||
sprintf("PRV%02d", prv_start + 1),
|
||||
sprintf("PRV%02d", prv_start + 2)
|
||||
}
|
||||
|
||||
assert(parent.get_height() >= (y + height), "flow display not of sufficient vertical resolution (add an additional row of monitors) " .. y .. "," .. parent.get_height())
|
||||
|
||||
local function _wide(a, b) return util.trinary(wide, a, b) end
|
||||
|
||||
-- bounding box div
|
||||
local root = Div{parent=parent,x=x,y=y,width=_wide(136, 114),height=height}
|
||||
|
||||
------------------
|
||||
-- COOLING LOOP --
|
||||
------------------
|
||||
|
||||
local reactor = Rectangle{parent=root,x=1,y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
|
||||
TextBox{parent=reactor,y=1,text="FISSION REACTOR",alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=root,x=19,y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
|
||||
TextBox{parent=root,x=3,y=5,text="\x19",width=1,height=1,fg_bg=lg_gray}
|
||||
|
||||
local rc_pipes = {}
|
||||
|
||||
local emc_x = 42 -- emergency coolant connection x point
|
||||
|
||||
if unit.num_boilers > 0 then
|
||||
table.insert(rc_pipes, pipe(0, 1, _wide(28, 19), 1, colors.lightBlue, true))
|
||||
table.insert(rc_pipes, pipe(0, 3, _wide(28, 19), 3, colors.orange, true))
|
||||
table.insert(rc_pipes, pipe(_wide(46 ,39), 1, _wide(72,58), 1, colors.blue, true))
|
||||
table.insert(rc_pipes, pipe(_wide(46,39), 3, _wide(72,58), 3, colors.white, true))
|
||||
else
|
||||
emc_x = 3
|
||||
table.insert(rc_pipes, pipe(0, 1, _wide(72,58), 1, colors.blue, true))
|
||||
table.insert(rc_pipes, pipe(0, 3, _wide(72,58), 3, colors.white, true))
|
||||
end
|
||||
|
||||
if unit.has_tank then
|
||||
table.insert(rc_pipes, pipe(emc_x, 1, emc_x, 0, colors.blue, true, true))
|
||||
end
|
||||
|
||||
local prv_yo = math.max(3 - unit.num_turbines, 0)
|
||||
for i = 1, unit.num_turbines do
|
||||
local py = 2 * (i - 1) + prv_yo
|
||||
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=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=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}
|
||||
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=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=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}
|
||||
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}
|
||||
|
||||
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=style.ind_bkg,c2=style.ind_yel.fgd,c3=style.ind_red.fgd}
|
||||
state.register(unit.turbine_ps_tbl[i], "SteamDumpOpen", state.update)
|
||||
end
|
||||
|
||||
----------------------
|
||||
-- WASTE PROCESSING --
|
||||
----------------------
|
||||
|
||||
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),
|
||||
pipe(_wide(22, 19), 1, _wide(49, 45), 1, colors.brown, true),
|
||||
pipe(_wide(22, 19), 5, _wide(28, 24), 5, colors.brown, true),
|
||||
|
||||
pipe(_wide(64, 53), 1, _wide(95, 81), 1, colors.green, true),
|
||||
|
||||
pipe(_wide(48, 43), 4, _wide(71, 61), 4, colors.cyan, true),
|
||||
pipe(_wide(66, 57), 4, _wide(71, 61), 8, colors.cyan, true),
|
||||
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, 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=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}
|
||||
local conn = IndicatorLight{parent=waste,x=vx-3,y=vy+1,label=v_names[n],colors=ind_grn}
|
||||
local open = IndicatorLight{parent=waste,x=vx-3,y=vy+2,label="OPEN",colors=ind_wht}
|
||||
conn.register(unit.unit_ps, util.c("V_", v_fields[n], "_conn"), conn.update)
|
||||
open.register(unit.unit_ps, util.c("V_", v_fields[n], "_state"), open.update)
|
||||
end
|
||||
|
||||
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=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=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)
|
||||
po_rate.register(unit.unit_ps, "po_rate", po_rate.update)
|
||||
popl_rate.register(unit.unit_ps, "po_pl_rate", popl_rate.update)
|
||||
poam_rate.register(unit.unit_ps, "po_am_rate", poam_rate.update)
|
||||
spent_rate.register(unit.unit_ps, "ws_rate", spent_rate.update)
|
||||
|
||||
_valve(_wide(21, 18), 2, 1)
|
||||
_valve(_wide(21, 18), 6, 2)
|
||||
_valve(_wide(73, 62), 5, 3)
|
||||
_valve(_wide(73, 62), 9, 4)
|
||||
|
||||
_machine(_wide(51, 45), 1, "CENTRIFUGE \x1a");
|
||||
_machine(_wide(97, 83), 1, "PRC [Pu] \x1a");
|
||||
_machine(_wide(97, 83), 4, "PRC [Po] \x1a");
|
||||
_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=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_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_max_rate", sna_max.update)
|
||||
sna_in.register(unit.unit_ps, "sna_in", sna_in.update)
|
||||
|
||||
return root
|
||||
end
|
||||
|
||||
return make
|
||||
@@ -14,7 +14,7 @@ local Div = require("graphics.elements.div")
|
||||
local PipeNetwork = require("graphics.elements.pipenet")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local pipe = core.pipe
|
||||
|
||||
@@ -34,7 +34,7 @@ local function make(parent, x, y, unit)
|
||||
|
||||
if num_boilers == 0 and num_turbines == 1 then
|
||||
height = 9
|
||||
elseif num_boilers == 1 and num_turbines <= 2 then
|
||||
elseif num_boilers <= 1 and num_turbines <= 2 then
|
||||
height = 17
|
||||
end
|
||||
|
||||
@@ -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=TEXT_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
|
||||
393
coordinator/ui/layout/flow_view.lua
Normal file
393
coordinator/ui/layout/flow_view.lua
Normal file
@@ -0,0 +1,393 @@
|
||||
--
|
||||
-- Flow Monitor GUI
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local unit_flow = require("coordinator.ui.components.unit_flow")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local PipeNetwork = require("graphics.elements.pipenet")
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local HorizontalBar = require("graphics.elements.indicators.hbar")
|
||||
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
|
||||
local CONTAINER_MODE = types.CONTAINER_MODE
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
local pipe = core.pipe
|
||||
|
||||
local wh_gray = style.wh_gray
|
||||
|
||||
-- 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
|
||||
|
||||
local tank_defs = facility.tank_defs
|
||||
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.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.theme.header}
|
||||
|
||||
datetime.register(facility.ps, "date_time", datetime.set_value)
|
||||
|
||||
local po_pipes = {}
|
||||
local water_pipes = {}
|
||||
|
||||
-- get the y offset for this unit index
|
||||
---@param idx integer unit index
|
||||
local function y_ofs(idx) return ((idx - 1) * 20) end
|
||||
|
||||
-- determinte facility tank start/end from the definitions list
|
||||
---@param start_idx integer start index of table iteration
|
||||
---@param end_idx integer end index of table iteration
|
||||
local function find_fdef(start_idx, end_idx)
|
||||
local first, last = 4, 0
|
||||
for i = start_idx, end_idx do
|
||||
if tank_defs[i] == 2 then
|
||||
last = i
|
||||
if i < first then first = i end
|
||||
end
|
||||
end
|
||||
return first, last
|
||||
end
|
||||
|
||||
if facility.tank_mode == 0 or facility.tank_mode == 8 then
|
||||
-- (0) tanks belong to reactor units OR (8) 4 total facility tanks (A B C D)
|
||||
for i = 1, facility.num_units do
|
||||
if units[i].has_tank then
|
||||
local y = y_ofs(i)
|
||||
table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true))
|
||||
table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true))
|
||||
|
||||
local u = units[i] ---@type ioctl_unit
|
||||
local x = util.trinary(u.num_boilers == 0, 45, 84)
|
||||
table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true))
|
||||
end
|
||||
end
|
||||
else
|
||||
-- setup connections for units with emergency coolant, always the same
|
||||
for i = 1, #tank_defs do
|
||||
if tank_defs[i] > 0 then
|
||||
local y = y_ofs(i)
|
||||
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(1, y, 21, y, colors.blue, true))
|
||||
else
|
||||
table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true))
|
||||
table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true))
|
||||
end
|
||||
|
||||
local u = units[i] ---@type ioctl_unit
|
||||
local x = util.trinary(u.num_boilers == 0, 45, 84)
|
||||
table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true))
|
||||
end
|
||||
end
|
||||
|
||||
if facility.tank_mode == 1 then
|
||||
-- (1) 1 total facility tank (A A A A)
|
||||
local first_fdef, last_fdef = find_fdef(1, #tank_defs)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 2 then
|
||||
-- (2) 2 total facility tanks (A A A B)
|
||||
local first_fdef, last_fdef = find_fdef(1, math.min(3, #tank_defs))
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 4 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.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 then
|
||||
table.insert(water_pipes, pipe(0, y_ofs(a), 1, y_ofs(a) + 6, colors.blue, true))
|
||||
if tank_defs[b] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y_ofs(b) - 13, 1, y_ofs(b), colors.blue, true))
|
||||
end
|
||||
elseif tank_defs[b] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y_ofs(b), 1, y_ofs(b) + 6, colors.blue, true))
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 4 then
|
||||
-- (4) 2 total facility tanks (A B B B)
|
||||
local first_fdef, last_fdef = find_fdef(2, #tank_defs)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 1 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 5 then
|
||||
-- (5) 3 total facility tanks (A A B C)
|
||||
local first_fdef, last_fdef = find_fdef(1, math.min(2, #tank_defs))
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 3 or i == 4 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 6 then
|
||||
-- (6) 3 total facility tanks (A B B C)
|
||||
local first_fdef, last_fdef = find_fdef(2, math.min(3, #tank_defs))
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 1 or i == 4 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 7 then
|
||||
-- (7) 3 total facility tanks (A B C C)
|
||||
local first_fdef, last_fdef = find_fdef(3, #tank_defs)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 1 or i == 2 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local flow_x = 3
|
||||
if #water_pipes > 0 then
|
||||
flow_x = 25
|
||||
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=style.theme.bg}
|
||||
|
||||
-----------------
|
||||
-- tank valves --
|
||||
-----------------
|
||||
|
||||
local next_f_id = 1
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
if tank_defs[i] > 0 then
|
||||
local vy = 3 + y_ofs(i)
|
||||
|
||||
TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=text_col,width=2,height=1}
|
||||
|
||||
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", i * 5),colors=style.ind_grn}
|
||||
local open = IndicatorLight{parent=main,x=9,y=vy+2,label="OPEN",colors=style.ind_wht}
|
||||
|
||||
conn.register(units[i].unit_ps, "V_emc_conn", conn.update)
|
||||
open.register(units[i].unit_ps, "V_emc_state", open.update)
|
||||
end
|
||||
end
|
||||
|
||||
-------------------
|
||||
-- dynamic tanks --
|
||||
-------------------
|
||||
|
||||
for i = 1, #tank_list do
|
||||
if tank_list[i] > 0 then
|
||||
local id = "U-" .. i
|
||||
local f_id = next_f_id
|
||||
if tank_list[i] == 2 then
|
||||
id = "F-" .. next_f_id
|
||||
next_f_id = next_f_id + 1
|
||||
end
|
||||
|
||||
local y_offset = y_ofs(i)
|
||||
|
||||
local tank = Div{parent=main,x=3,y=7+y_offset,width=20,height=14}
|
||||
|
||||
TextBox{parent=tank,text=" ",height=1,x=1,y=1,fg_bg=style.lg_gray}
|
||||
TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=ALIGN.CENTER,height=1,fg_bg=style.wh_gray}
|
||||
|
||||
local tank_box = Rectangle{parent=tank,border=border(1,colors.gray,true),width=20,height=12}
|
||||
|
||||
local status = StateIndicator{parent=tank_box,x=3,y=1,states=style.dtank.states,value=1,min_width=14}
|
||||
|
||||
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=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}
|
||||
|
||||
TextBox{parent=tank_box,x=2,y=9,text="In/Out Mode",height=1,width=11,fg_bg=style.label}
|
||||
local can_fill = IndicatorLight{parent=tank_box,x=2,y=10,label="FILL",colors=style.ind_wht}
|
||||
local can_empty = IndicatorLight{parent=tank_box,x=10,y=10,label="EMPTY",colors=style.ind_wht}
|
||||
|
||||
local function _can_fill(mode)
|
||||
can_fill.update((mode == CONTAINER_MODE.BOTH) or (mode == CONTAINER_MODE.FILL))
|
||||
end
|
||||
|
||||
local function _can_empty(mode)
|
||||
can_empty.update((mode == CONTAINER_MODE.BOTH) or (mode == CONTAINER_MODE.EMPTY))
|
||||
end
|
||||
|
||||
if tank_list[i] == 1 then
|
||||
status.register(units[i].tank_ps_tbl[1], "computed_status", status.update)
|
||||
tank_pcnt.register(units[i].tank_ps_tbl[1], "fill", function (f) tank_pcnt.update(f * 100) end)
|
||||
tank_amnt.register(units[i].tank_ps_tbl[1], "stored", function (sto) tank_amnt.update(sto.amount) end)
|
||||
level.register(units[i].tank_ps_tbl[1], "fill", level.update)
|
||||
can_fill.register(units[i].tank_ps_tbl[1], "container_mode", _can_fill)
|
||||
can_empty.register(units[i].tank_ps_tbl[1], "container_mode", _can_empty)
|
||||
else
|
||||
status.register(facility.tank_ps_tbl[f_id], "computed_status", status.update)
|
||||
tank_pcnt.register(facility.tank_ps_tbl[f_id], "fill", function (f) tank_pcnt.update(f * 100) end)
|
||||
tank_amnt.register(facility.tank_ps_tbl[f_id], "stored", function (sto) tank_amnt.update(sto.amount) end)
|
||||
level.register(facility.tank_ps_tbl[f_id], "fill", level.update)
|
||||
can_fill.register(facility.tank_ps_tbl[f_id], "container_mode", _can_fill)
|
||||
can_empty.register(facility.tank_ps_tbl[f_id], "container_mode", _can_empty)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
util.nop()
|
||||
|
||||
---------
|
||||
-- SPS --
|
||||
---------
|
||||
|
||||
local sps = Div{parent=main,x=140,y=3,height=12}
|
||||
|
||||
TextBox{parent=sps,text=" ",width=24,height=1,x=1,y=1,fg_bg=style.lg_gray}
|
||||
TextBox{parent=sps,text="SPS",alignment=ALIGN.CENTER,width=24,height=1,fg_bg=wh_gray}
|
||||
|
||||
local sps_box = Rectangle{parent=sps,border=border(1,colors.gray,true),width=24,height=10}
|
||||
|
||||
local status = StateIndicator{parent=sps_box,x=5,y=1,states=style.sps.states,value=1,min_width=14}
|
||||
|
||||
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.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=s_field}
|
||||
|
||||
sps_rate.register(facility.sps_ps_tbl[1], "process_rate", function (r) sps_rate.update(r * 1000) end)
|
||||
|
||||
----------------
|
||||
-- statistics --
|
||||
----------------
|
||||
|
||||
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=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=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=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
|
||||
|
||||
return init
|
||||
@@ -2,39 +2,44 @@
|
||||
-- Coordinator Front Panel GUI
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
local style = require("coordinator.ui.style")
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local pkt_entry = require("coordinator.ui.components.pkt_entry")
|
||||
local pkt_entry = require("coordinator.ui.components.pkt_entry")
|
||||
|
||||
local core = require("graphics.core")
|
||||
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 Div = require("graphics.elements.div")
|
||||
local ListBox = require("graphics.elements.listbox")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local TabBar = require("graphics.elements.controls.tabbar")
|
||||
local TabBar = require("graphics.elements.controls.tabbar")
|
||||
|
||||
local LED = require("graphics.elements.indicators.led")
|
||||
local RGBLED = require("graphics.elements.indicators.ledrgb")
|
||||
local LED = require("graphics.elements.indicators.led")
|
||||
local LEDPair = require("graphics.elements.indicators.ledpair")
|
||||
local RGBLED = require("graphics.elements.indicators.ledrgb")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local LINK_STATE = types.PANEL_LINK_STATE
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local led_grn = style.led_grn
|
||||
|
||||
-- create new front panel view
|
||||
---@param panel graphics_element main displaybox
|
||||
---@param num_units integer number of units (number of unit monitors)
|
||||
local function init(panel, num_units)
|
||||
local ps = iocontrol.get_db().fp.ps
|
||||
|
||||
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=TEXT_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}
|
||||
|
||||
@@ -47,36 +52,78 @@ local function init(panel, num_units)
|
||||
local system = Div{parent=main_page,width=14,height=17,x=2,y=2}
|
||||
|
||||
local status = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
|
||||
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)}
|
||||
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=led_grn}
|
||||
status.update(true)
|
||||
system.line_break()
|
||||
|
||||
heartbeat.register(ps, "heartbeat", heartbeat.update)
|
||||
|
||||
local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)}
|
||||
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
|
||||
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
local modem = LED{parent=system,label="MODEM",colors=led_grn}
|
||||
|
||||
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=cpair(colors.green,colors.green_off)}
|
||||
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=cpair(colors.lightGray,colors.ivory)}
|
||||
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}
|
||||
|
||||
local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=cpair(colors.green,colors.green_off)}
|
||||
local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=led_grn}
|
||||
main_monitor.register(ps, "main_monitor", main_monitor.update)
|
||||
|
||||
local flow_monitor = LED{parent=monitors,label="FLOW MONITOR",colors=led_grn}
|
||||
flow_monitor.register(ps, "flow_monitor", flow_monitor.update)
|
||||
|
||||
monitors.line_break()
|
||||
|
||||
for i = 1, num_units do
|
||||
local unit_monitor = LED{parent=monitors,label="UNIT "..i.." MONITOR",colors=cpair(colors.green,colors.green_off)}
|
||||
local unit_monitor = LED{parent=monitors,label="UNIT "..i.." MONITOR",colors=led_grn}
|
||||
unit_monitor.register(ps, "unit_monitor_" .. i, unit_monitor.update)
|
||||
end
|
||||
|
||||
@@ -84,9 +131,9 @@ local function init(panel, num_units)
|
||||
-- about footer
|
||||
--
|
||||
|
||||
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=cpair(colors.lightGray,colors.ivory)}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
|
||||
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}
|
||||
|
||||
fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
|
||||
comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
|
||||
@@ -98,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=cpair(colors.black,colors.ivory),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
|
||||
@@ -108,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 = cpair(colors.black, colors.ivory) },
|
||||
{ name = "API", color = cpair(colors.black, colors.ivory) },
|
||||
{ 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=cpair(colors.black,colors.white)}
|
||||
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)
|
||||
|
||||
@@ -18,21 +18,21 @@ local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
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=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=cpair(colors.lightGray, colors.white),width=12,fg_bg=style.header}
|
||||
local 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=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=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)
|
||||
@@ -55,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
|
||||
@@ -66,23 +68,25 @@ 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
|
||||
|
||||
assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||
|
||||
TextBox{parent=main,y=cnc_bottom_align_start,text=util.strrep("\x8c", header.get_width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=cpair(colors.lightGray,colors.gray)}
|
||||
TextBox{parent=main,y=cnc_bottom_align_start,text=string.rep("\x8c", header.get_width()),alignment=ALIGN.CENTER,height=1,fg_bg=style.lg_gray}
|
||||
|
||||
cnc_bottom_align_start = cnc_bottom_align_start + 2
|
||||
|
||||
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,73 +2,149 @@
|
||||
-- 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
|
||||
}
|
||||
|
||||
-- MAIN LAYOUT --
|
||||
---@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.hzd_fg_bg = style.wh_gray
|
||||
style.dis_colors = cpair(colors.white, colors.lightGray)
|
||||
|
||||
style.lg_gray = cpair(colors.lightGray, colors.gray)
|
||||
style.lg_white = cpair(colors.lightGray, colors.white)
|
||||
style.gray_white = cpair(colors.gray, colors.white)
|
||||
|
||||
-- UI COMPONENTS --
|
||||
|
||||
style.reactor = {
|
||||
-- reactor states
|
||||
@@ -206,7 +282,7 @@ style.sps = {
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.gray),
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "IDLE"
|
||||
},
|
||||
{
|
||||
@@ -216,6 +292,36 @@ style.sps = {
|
||||
}
|
||||
}
|
||||
|
||||
style.dtank = {
|
||||
-- dynamic tank states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ONLINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "LOW FILL"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "FILLED"
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
style.waste = {
|
||||
-- auto waste processing states
|
||||
states = {
|
||||
|
||||
@@ -7,19 +7,15 @@ local flasher = require("graphics.flasher")
|
||||
|
||||
local core = {}
|
||||
|
||||
core.version = "1.0.2"
|
||||
core.version = "2.2.4"
|
||||
|
||||
core.flasher = flasher
|
||||
core.events = events
|
||||
|
||||
-- Core Types
|
||||
|
||||
---@enum TEXT_ALIGN
|
||||
core.TEXT_ALIGN = {
|
||||
LEFT = 1,
|
||||
CENTER = 2,
|
||||
RIGHT = 3
|
||||
}
|
||||
---@enum ALIGN
|
||||
core.ALIGN = { LEFT = 1, CENTER = 2, RIGHT = 3 }
|
||||
|
||||
---@class graphics_border
|
||||
---@field width integer
|
||||
@@ -35,11 +31,7 @@ core.TEXT_ALIGN = {
|
||||
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
|
||||
---@return graphics_border
|
||||
function core.border(width, color, even)
|
||||
return {
|
||||
width = width,
|
||||
color = color,
|
||||
even = even or false -- convert nil to false
|
||||
}
|
||||
return { width = width, color = color, even = even or false }
|
||||
end
|
||||
|
||||
---@class graphics_frame
|
||||
@@ -56,12 +48,7 @@ end
|
||||
---@param h integer
|
||||
---@return graphics_frame
|
||||
function core.gframe(x, y, w, h)
|
||||
return {
|
||||
x = x,
|
||||
y = y,
|
||||
w = w,
|
||||
h = h
|
||||
}
|
||||
return { x = x, y = y, w = w, h = h }
|
||||
end
|
||||
|
||||
---@class cpair
|
||||
@@ -74,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
|
||||
@@ -82,15 +72,9 @@ end
|
||||
function core.cpair(a, b)
|
||||
return {
|
||||
-- color pairs
|
||||
color_a = a,
|
||||
color_b = b,
|
||||
blit_a = colors.toBlit(a),
|
||||
blit_b = colors.toBlit(b),
|
||||
color_a = a, color_b = b, blit_a = colors.toBlit(a), blit_b = colors.toBlit(b),
|
||||
-- aliases
|
||||
fgd = a,
|
||||
bkg = b,
|
||||
blit_fgd = colors.toBlit(a),
|
||||
blit_bkg = colors.toBlit(b)
|
||||
fgd = a, bkg = b, blit_fgd = colors.toBlit(a), blit_bkg = colors.toBlit(b)
|
||||
}
|
||||
end
|
||||
|
||||
@@ -130,4 +114,215 @@ function core.pipe(x1, y1, x2, y2, color, thin, align_tr)
|
||||
}
|
||||
end
|
||||
|
||||
-- Assertion Handling
|
||||
|
||||
-- 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, "@") or 0) + 1)
|
||||
end
|
||||
|
||||
-- Interactive Field Manager
|
||||
|
||||
---@param e graphics_base
|
||||
---@param max_len any
|
||||
---@param fg_bg any
|
||||
---@param dis_fg_bg any
|
||||
function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
|
||||
local self = {
|
||||
frame_start = 1,
|
||||
visible_text = e.value,
|
||||
cursor_pos = string.len(e.value) + 1,
|
||||
selected_all = false
|
||||
}
|
||||
|
||||
-- update visible text
|
||||
local function _update_visible()
|
||||
self.visible_text = string.sub(e.value, self.frame_start, self.frame_start + math.min(string.len(e.value), e.frame.w) - 1)
|
||||
end
|
||||
|
||||
-- try shifting frame left
|
||||
local function _try_lshift()
|
||||
if self.frame_start > 1 then
|
||||
self.frame_start = self.frame_start - 1
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- try shifting frame right
|
||||
local function _try_rshift()
|
||||
if (self.frame_start + e.frame.w - 1) <= string.len(e.value) then
|
||||
self.frame_start = self.frame_start + 1
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
---@class ifield
|
||||
local public = {}
|
||||
|
||||
-- censor the display (for private info, for example) with the provided character<br>
|
||||
-- disable by passing no argument
|
||||
---@param censor string? character to hide data with
|
||||
function public.censor(censor)
|
||||
if type(censor) == "string" and string.len(censor) == 1 then
|
||||
self.censor = censor
|
||||
else self.censor = nil end
|
||||
public.show()
|
||||
end
|
||||
|
||||
-- show the field
|
||||
function public.show()
|
||||
_update_visible()
|
||||
|
||||
if e.enabled then
|
||||
e.w_set_bkg(fg_bg.bkg)
|
||||
e.w_set_fgd(fg_bg.fgd)
|
||||
elseif dis_fg_bg ~= nil then
|
||||
e.w_set_bkg(dis_fg_bg.bkg)
|
||||
e.w_set_fgd(dis_fg_bg.fgd)
|
||||
end
|
||||
|
||||
-- clear and print
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(string.rep(" ", e.frame.w))
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
local function _write()
|
||||
if self.censor then
|
||||
e.w_write(string.rep(self.censor, string.len(self.visible_text)))
|
||||
else
|
||||
e.w_write(self.visible_text)
|
||||
end
|
||||
end
|
||||
|
||||
if e.is_focused() and e.enabled then
|
||||
-- write text with cursor
|
||||
if self.selected_all then
|
||||
e.w_set_bkg(fg_bg.fgd)
|
||||
e.w_set_fgd(fg_bg.bkg)
|
||||
_write()
|
||||
elseif self.cursor_pos >= (string.len(self.visible_text) + 1) then
|
||||
-- write text with cursor at the end, no need to blit
|
||||
_write()
|
||||
e.w_set_fgd(colors.lightGray)
|
||||
e.w_write("_")
|
||||
else
|
||||
local a, b = "", ""
|
||||
|
||||
if self.cursor_pos <= string.len(self.visible_text) then
|
||||
a = fg_bg.blit_bkg
|
||||
b = fg_bg.blit_fgd
|
||||
end
|
||||
|
||||
local b_fgd = string.rep(fg_bg.blit_fgd, self.cursor_pos - 1) .. a .. string.rep(fg_bg.blit_fgd, string.len(self.visible_text) - self.cursor_pos)
|
||||
local b_bkg = string.rep(fg_bg.blit_bkg, self.cursor_pos - 1) .. b .. string.rep(fg_bg.blit_bkg, string.len(self.visible_text) - self.cursor_pos)
|
||||
|
||||
if self.censor then
|
||||
e.w_blit(string.rep(self.censor, string.len(self.visible_text)), b_fgd, b_bkg)
|
||||
else
|
||||
e.w_blit(self.visible_text, b_fgd, b_bkg)
|
||||
end
|
||||
end
|
||||
else
|
||||
self.selected_all = false
|
||||
|
||||
-- write text without cursor
|
||||
_write()
|
||||
end
|
||||
end
|
||||
|
||||
-- move cursor to x
|
||||
---@param x integer
|
||||
function public.move_cursor(x)
|
||||
self.selected_all = false
|
||||
self.cursor_pos = math.min(x, string.len(self.visible_text) + 1)
|
||||
public.show()
|
||||
end
|
||||
|
||||
-- select all text
|
||||
function public.select_all()
|
||||
self.selected_all = true
|
||||
public.show()
|
||||
end
|
||||
|
||||
-- set field value
|
||||
---@param val string
|
||||
function public.set_value(val)
|
||||
e.value = string.sub(val, 1, math.min(max_len, string.len(val)))
|
||||
public.nav_end()
|
||||
end
|
||||
|
||||
-- try to insert a character if there is space
|
||||
---@param char string
|
||||
function public.try_insert_char(char)
|
||||
-- limit length
|
||||
if string.len(e.value) >= max_len then return end
|
||||
|
||||
-- replace if selected all, insert otherwise
|
||||
if self.selected_all then
|
||||
self.selected_all = false
|
||||
self.cursor_pos = 2
|
||||
self.frame_start = 1
|
||||
|
||||
e.value = char
|
||||
public.show()
|
||||
else
|
||||
e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 2) .. char .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value))
|
||||
_update_visible()
|
||||
public.nav_right()
|
||||
end
|
||||
end
|
||||
|
||||
-- remove charcter before cursor if there is anything to remove, or delete all if selected all
|
||||
function public.backspace()
|
||||
if self.selected_all then
|
||||
self.selected_all = false
|
||||
e.value = ""
|
||||
self.cursor_pos = 1
|
||||
self.frame_start = 1
|
||||
public.show()
|
||||
else
|
||||
if self.frame_start + self.cursor_pos > 2 then
|
||||
e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 3) .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value))
|
||||
if self.cursor_pos > 1 then
|
||||
self.cursor_pos = self.cursor_pos - 1
|
||||
public.show()
|
||||
elseif _try_lshift() then public.show() end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- move cursor left by one
|
||||
function public.nav_left()
|
||||
if self.cursor_pos > 1 then
|
||||
self.cursor_pos = self.cursor_pos - 1
|
||||
public.show()
|
||||
elseif _try_lshift() then public.show() end
|
||||
end
|
||||
|
||||
-- move cursor right by one
|
||||
function public.nav_right()
|
||||
if self.cursor_pos < math.min(string.len(self.visible_text) + 1, e.frame.w) then
|
||||
self.cursor_pos = self.cursor_pos + 1
|
||||
public.show()
|
||||
elseif _try_rshift() then public.show() end
|
||||
end
|
||||
|
||||
-- move cursor to the start
|
||||
function public.nav_start()
|
||||
self.cursor_pos = 1
|
||||
self.frame_start = 1
|
||||
public.show()
|
||||
end
|
||||
|
||||
-- move cursor to the end
|
||||
function public.nav_end()
|
||||
self.frame_start = math.max(1, string.len(e.value) - e.frame.w + 2)
|
||||
_update_visible()
|
||||
self.cursor_pos = string.len(self.visible_text) + 1
|
||||
public.show()
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return core
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
-- Generic Graphics Element
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local events = core.events
|
||||
|
||||
local element = {}
|
||||
|
||||
---@class graphics_args_generic
|
||||
@@ -17,18 +21,23 @@ local element = {}
|
||||
---@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
|
||||
---@field can_focus? boolean true if this element can be focused, false by default
|
||||
|
||||
---@alias graphics_args graphics_args_generic
|
||||
---|waiting_args
|
||||
---|app_button_args
|
||||
---|checkbox_args
|
||||
---|hazard_button_args
|
||||
---|multi_button_args
|
||||
---|push_button_args
|
||||
---|radio_2d_args
|
||||
---|radio_button_args
|
||||
---|sidebar_args
|
||||
---|spinbox_args
|
||||
---|switch_button_args
|
||||
---|tabbar_args
|
||||
---|number_field_args
|
||||
---|text_field_args
|
||||
---|alarm_indicator_light
|
||||
---|core_map_args
|
||||
---|data_indicator_args
|
||||
@@ -40,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
|
||||
@@ -58,6 +69,16 @@ local element = {}
|
||||
---@field key string data key
|
||||
---@field func function callback
|
||||
|
||||
-- more detailed assert message for element verification
|
||||
---@param condition any assert condition
|
||||
---@param msg string assert message
|
||||
---@param callstack_offset? integer shift value to change targets of debug.getinfo()
|
||||
function element.assert(condition, msg, callstack_offset)
|
||||
callstack_offset = callstack_offset or 0
|
||||
local caller = debug.getinfo(3 + callstack_offset)
|
||||
assert(condition, util.c(caller.source, ":", caller.currentline, "{", debug.getinfo(2 + callstack_offset).name, "}: ", msg))
|
||||
end
|
||||
|
||||
-- a base graphics element, should not be created on its own
|
||||
---@nodiscard
|
||||
---@param args graphics_args arguments
|
||||
@@ -66,14 +87,19 @@ local element = {}
|
||||
function element.new(args, child_offset_x, child_offset_y)
|
||||
local self = {
|
||||
id = nil, ---@type element_id|nil
|
||||
is_root = args.parent == nil,
|
||||
elem_type = debug.getinfo(2).name,
|
||||
define_completed = false,
|
||||
p_window = nil, ---@type table
|
||||
position = { x = 1, y = 1 }, ---@type coordinate_2d
|
||||
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 = {},
|
||||
button_down = { events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1) },
|
||||
focused = false,
|
||||
mt = {}
|
||||
}
|
||||
|
||||
@@ -83,16 +109,16 @@ 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 = {}
|
||||
children = {},
|
||||
child_id_map = {}
|
||||
}
|
||||
|
||||
local name_brief = "graphics.element{" .. self.elem_type .. "}: "
|
||||
|
||||
-- element as string
|
||||
function self.mt.__tostring()
|
||||
return "graphics.element{" .. self.elem_type .. "} @ " .. tostring(self)
|
||||
return util.c("graphics.element{", self.elem_type, "} @ ", self)
|
||||
end
|
||||
|
||||
---@class graphics_element
|
||||
@@ -100,6 +126,69 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
|
||||
setmetatable(public, self.mt)
|
||||
|
||||
-----------------------
|
||||
-- PRIVATE FUNCTIONS --
|
||||
-----------------------
|
||||
|
||||
-- use tab to jump to the next focusable field
|
||||
---@param reverse boolean
|
||||
local function _tab_focusable(reverse)
|
||||
local first_f = nil ---@type graphics_element|nil
|
||||
local prev_f = nil ---@type graphics_element|nil
|
||||
local cur_f = nil ---@type graphics_element|nil
|
||||
local done = false
|
||||
|
||||
---@param elem graphics_element
|
||||
local function handle_element(elem)
|
||||
if elem.is_visible() and elem.is_focusable() and elem.is_enabled() then
|
||||
if first_f == nil then first_f = elem end
|
||||
|
||||
if cur_f == nil then
|
||||
if elem.is_focused() then
|
||||
cur_f = elem
|
||||
if (not done) and (reverse and prev_f ~= nil) then
|
||||
cur_f.unfocus()
|
||||
prev_f.focus()
|
||||
done = true
|
||||
end
|
||||
end
|
||||
else
|
||||
if elem.is_focused() then
|
||||
elem.unfocus()
|
||||
elseif not (reverse or done) then
|
||||
cur_f.unfocus()
|
||||
elem.focus()
|
||||
done = true
|
||||
end
|
||||
end
|
||||
|
||||
prev_f = elem
|
||||
end
|
||||
end
|
||||
|
||||
---@param children table
|
||||
local function traverse(children)
|
||||
for i = 1, #children do
|
||||
local child = children[i] ---@type graphics_base
|
||||
handle_element(child.get())
|
||||
if child.get().is_visible() then traverse(child.children) end
|
||||
end
|
||||
end
|
||||
|
||||
traverse(protected.children)
|
||||
|
||||
-- if no element was focused, wrap focus
|
||||
if first_f ~= nil and not done then
|
||||
if reverse then
|
||||
if cur_f ~= nil then cur_f.unfocus() end
|
||||
if prev_f ~= nil then prev_f.focus() end
|
||||
else
|
||||
if cur_f ~= nil then cur_f.unfocus() end
|
||||
first_f.focus()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------
|
||||
-- PROTECTED FUNCTIONS --
|
||||
-------------------------
|
||||
@@ -109,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
|
||||
@@ -133,21 +229,34 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
end
|
||||
|
||||
-- check frame
|
||||
assert(f.x >= 1, name_brief .. "frame x not >= 1")
|
||||
assert(f.y >= 1, name_brief .. "frame y not >= 1")
|
||||
assert(f.w >= 1, name_brief .. "frame width not >= 1")
|
||||
assert(f.h >= 1, name_brief .. "frame height not >= 1")
|
||||
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)
|
||||
@@ -165,6 +274,31 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
self.bounds.x2 = self.position.x + f.w - 1
|
||||
self.bounds.y1 = self.position.y
|
||||
self.bounds.y2 = self.position.y + f.h - 1
|
||||
|
||||
-- alias functions
|
||||
|
||||
-- window set cursor position
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
function protected.w_set_cur(x, y) protected.window.setCursorPos(x, y) end
|
||||
|
||||
-- set background color
|
||||
---@param c color
|
||||
function protected.w_set_bkg(c) protected.window.setBackgroundColor(c) end
|
||||
|
||||
-- set foreground (text) color
|
||||
---@param c color
|
||||
function protected.w_set_fgd(c) protected.window.setTextColor(c) end
|
||||
|
||||
-- write text
|
||||
---@param str string
|
||||
function protected.w_write(str) protected.window.write(str) end
|
||||
|
||||
-- blit text
|
||||
---@param str string
|
||||
---@param fg string
|
||||
---@param bg string
|
||||
function protected.w_blit(str, fg, bg) protected.window.blit(str, fg, bg) end
|
||||
end
|
||||
|
||||
-- check if a coordinate relative to the parent is within the bounds of this element
|
||||
@@ -185,85 +319,6 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
return in_x and in_y
|
||||
end
|
||||
|
||||
-- luacheck: push ignore
|
||||
---@diagnostic disable: unused-local, unused-vararg
|
||||
|
||||
-- handle a child element having been added
|
||||
---@param id element_id element identifier
|
||||
---@param child graphics_element child element
|
||||
function protected.on_added(id, child)
|
||||
end
|
||||
|
||||
-- handle a child element having been removed
|
||||
---@param id element_id element identifier
|
||||
function protected.on_removed(id)
|
||||
end
|
||||
|
||||
-- handle a mouse event
|
||||
---@param event mouse_interaction mouse interaction event
|
||||
function protected.handle_mouse(event)
|
||||
end
|
||||
|
||||
-- handle data value changes
|
||||
---@vararg any value(s)
|
||||
function protected.on_update(...)
|
||||
end
|
||||
|
||||
-- callback on control press responses
|
||||
---@param result any
|
||||
function protected.response_callback(result)
|
||||
end
|
||||
|
||||
-- get value
|
||||
---@nodiscard
|
||||
function protected.get_value()
|
||||
return protected.value
|
||||
end
|
||||
|
||||
-- set value
|
||||
---@param value any value to set
|
||||
function protected.set_value(value)
|
||||
end
|
||||
|
||||
-- set minimum input value
|
||||
---@param min integer minimum allowed value
|
||||
function protected.set_min(min)
|
||||
end
|
||||
|
||||
-- set maximum input value
|
||||
---@param max integer maximum allowed value
|
||||
function protected.set_max(max)
|
||||
end
|
||||
|
||||
-- enable the control
|
||||
function protected.enable()
|
||||
end
|
||||
|
||||
-- disable the control
|
||||
function protected.disable()
|
||||
end
|
||||
|
||||
-- custom recolor command, varies by element if implemented
|
||||
---@vararg cpair|color color(s)
|
||||
function protected.recolor(...)
|
||||
end
|
||||
|
||||
-- custom resize command, varies by element if implemented
|
||||
---@vararg integer sizing
|
||||
function protected.resize(...)
|
||||
end
|
||||
|
||||
-- luacheck: pop
|
||||
---@diagnostic enable: unused-local, unused-vararg
|
||||
|
||||
-- start animations
|
||||
function protected.start_anim()
|
||||
end
|
||||
|
||||
-- stop animations
|
||||
function protected.stop_anim()
|
||||
end
|
||||
|
||||
-- get public interface
|
||||
---@nodiscard
|
||||
---@return graphics_element element, element_id id
|
||||
@@ -277,6 +332,111 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
return public, self.id
|
||||
end
|
||||
|
||||
-- protected version of public is_focused()
|
||||
---@nodiscard
|
||||
---@return boolean is_focused
|
||||
function protected.is_focused() return self.focused end
|
||||
|
||||
-- defocus this element
|
||||
function protected.defocus() public.unfocus_all() end
|
||||
|
||||
-- focus this element and take away focus from all other elements
|
||||
function protected.take_focus() args.parent.__focus_child(public) end
|
||||
|
||||
-- action handlers --
|
||||
|
||||
-- luacheck: push ignore
|
||||
---@diagnostic disable: unused-local, unused-vararg
|
||||
|
||||
-- handle a child element having been added
|
||||
---@param id element_id element identifier
|
||||
---@param child graphics_element child element
|
||||
function protected.on_added(id, child) end
|
||||
|
||||
-- handle a child element having been removed
|
||||
---@param id element_id element identifier
|
||||
function protected.on_removed(id) end
|
||||
|
||||
-- handle enabled
|
||||
function protected.on_enabled() end
|
||||
|
||||
-- handle disabled
|
||||
function protected.on_disabled() end
|
||||
|
||||
-- handle this element having been focused
|
||||
function protected.on_focused() end
|
||||
|
||||
-- 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
|
||||
|
||||
-- handle this element having been hidden
|
||||
function protected.on_hidden() end
|
||||
|
||||
-- handle a mouse event
|
||||
---@param event mouse_interaction mouse interaction event
|
||||
function protected.handle_mouse(event) end
|
||||
|
||||
-- handle a keyboard event
|
||||
---@param event key_interaction key interaction event
|
||||
function protected.handle_key(event) end
|
||||
|
||||
-- handle a paste event
|
||||
---@param text string pasted text
|
||||
function protected.handle_paste(text) end
|
||||
|
||||
-- handle data value changes
|
||||
---@vararg any value(s)
|
||||
function protected.on_update(...) end
|
||||
|
||||
-- callback on control press responses
|
||||
---@param result any
|
||||
function protected.response_callback(result) end
|
||||
|
||||
-- accessors and control --
|
||||
|
||||
-- get value
|
||||
---@nodiscard
|
||||
function protected.get_value() return protected.value end
|
||||
|
||||
-- set value
|
||||
---@param value any value to set
|
||||
function protected.set_value(value) end
|
||||
|
||||
-- set minimum input value
|
||||
---@param min integer minimum allowed value
|
||||
function protected.set_min(min) end
|
||||
|
||||
-- set maximum input value
|
||||
---@param max integer maximum allowed value
|
||||
function protected.set_max(max) end
|
||||
|
||||
-- custom recolor command, varies by element if implemented
|
||||
---@vararg cpair|color color(s)
|
||||
function protected.recolor(...) end
|
||||
|
||||
-- custom resize command, varies by element if implemented
|
||||
---@vararg integer sizing
|
||||
function protected.resize(...) end
|
||||
|
||||
-- luacheck: pop
|
||||
---@diagnostic enable: unused-local, unused-vararg
|
||||
|
||||
-- re-draw this element
|
||||
function protected.redraw() end
|
||||
|
||||
-- start animations
|
||||
function protected.start_anim() end
|
||||
|
||||
-- stop animations
|
||||
function protected.stop_anim() end
|
||||
|
||||
-----------
|
||||
-- SETUP --
|
||||
-----------
|
||||
@@ -288,7 +448,7 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
end
|
||||
|
||||
-- check window
|
||||
assert(self.p_window, name_brief .. "no parent window provided")
|
||||
element.assert(self.p_window, "no parent window provided", 1)
|
||||
|
||||
-- prepare the template
|
||||
if args.parent == nil then
|
||||
@@ -329,7 +489,7 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
|
||||
-- delete all children
|
||||
for k, v in pairs(protected.children) do
|
||||
v.delete()
|
||||
v.get().delete()
|
||||
protected.children[k] = nil
|
||||
end
|
||||
|
||||
@@ -351,65 +511,94 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
|
||||
self.next_y = child.frame.y + child.frame.h
|
||||
|
||||
local child_element = child.get()
|
||||
|
||||
local id = key ---@type string|integer|nil
|
||||
if id == nil then
|
||||
id = self.next_id
|
||||
self.next_id = self.next_id + 1
|
||||
end
|
||||
|
||||
protected.children[id] = child_element
|
||||
table.insert(protected.children, child)
|
||||
|
||||
protected.child_id_map[id] = #protected.children
|
||||
|
||||
return id
|
||||
end
|
||||
|
||||
-- remove a child element
|
||||
---@param key element_id id
|
||||
function public.__remove_child(key)
|
||||
if protected.children[key] ~= nil then
|
||||
protected.on_removed(key)
|
||||
protected.children[key] = nil
|
||||
---@param id element_id id
|
||||
function public.__remove_child(id)
|
||||
local index = protected.child_id_map[id]
|
||||
if protected.children[index] ~= nil then
|
||||
protected.on_removed(id)
|
||||
protected.children[index] = nil
|
||||
protected.child_id_map[id] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- actions to take upon a child element becoming ready (initial draw/construction completed)
|
||||
---@param key element_id id
|
||||
---@param child graphics_element
|
||||
function public.__child_ready(key, child)
|
||||
protected.on_added(key, child)
|
||||
function public.__child_ready(key, child) protected.on_added(key, child) end
|
||||
|
||||
-- focus solely on this child
|
||||
---@param child graphics_element
|
||||
function public.__focus_child(child)
|
||||
if self.is_root then
|
||||
public.unfocus_all()
|
||||
child.focus()
|
||||
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
|
||||
---@return graphics_element
|
||||
function public.get_child(id) return protected.children[id] end
|
||||
function public.get_child(id) return protected.children[protected.child_id_map[id]].get() end
|
||||
|
||||
-- remove a child element
|
||||
---@param id element_id
|
||||
function public.remove(id)
|
||||
if protected.children[id] ~= nil then
|
||||
protected.children[id].delete()
|
||||
local index = protected.child_id_map[id]
|
||||
if protected.children[index] ~= nil then
|
||||
protected.children[index].get().delete()
|
||||
protected.on_removed(id)
|
||||
protected.children[id] = nil
|
||||
protected.children[index] = nil
|
||||
protected.child_id_map[id] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- remove all child elements and reset next y
|
||||
function public.remove_all()
|
||||
for i = 1, #protected.children do
|
||||
local child = protected.children[i].get() ---@type graphics_element
|
||||
child.delete()
|
||||
protected.on_removed(child.get_id())
|
||||
end
|
||||
|
||||
self.next_y = 1
|
||||
protected.children = {}
|
||||
protected.child_id_map = {}
|
||||
end
|
||||
|
||||
-- attempt to get a child element by ID (does not include this element itself)
|
||||
---@nodiscard
|
||||
---@param id element_id
|
||||
---@return graphics_element|nil element
|
||||
function public.get_element_by_id(id)
|
||||
if protected.children[id] == nil then
|
||||
local index = protected.child_id_map[id]
|
||||
if protected.children[index] == nil then
|
||||
for _, child in pairs(protected.children) do
|
||||
local elem = child.get_element_by_id(id)
|
||||
local elem = child.get().get_element_by_id(id)
|
||||
if elem ~= nil then return elem end
|
||||
end
|
||||
else
|
||||
return protected.children[id]
|
||||
end
|
||||
|
||||
return nil
|
||||
else return protected.children[index].get() end
|
||||
end
|
||||
|
||||
-- AUTO-PLACEMENT --
|
||||
@@ -421,89 +610,109 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
|
||||
-- PROPERTIES --
|
||||
|
||||
-- get the foreground/background colors
|
||||
-- get element id
|
||||
---@nodiscard
|
||||
---@return cpair fg_bg
|
||||
function public.get_fg_bg()
|
||||
return protected.fg_bg
|
||||
end
|
||||
---@return element_id
|
||||
function public.get_id() return self.id end
|
||||
|
||||
-- get element x
|
||||
---@nodiscard
|
||||
---@return integer x
|
||||
function public.get_x()
|
||||
return protected.frame.x
|
||||
end
|
||||
function public.get_x() return protected.frame.x end
|
||||
|
||||
-- get element y
|
||||
---@nodiscard
|
||||
---@return integer y
|
||||
function public.get_y()
|
||||
return protected.frame.y
|
||||
end
|
||||
function public.get_y() return protected.frame.y end
|
||||
|
||||
-- get element width
|
||||
---@nodiscard
|
||||
---@return integer width
|
||||
function public.get_width()
|
||||
return protected.frame.w
|
||||
end
|
||||
function public.get_width() return protected.frame.w end
|
||||
|
||||
-- get element height
|
||||
---@nodiscard
|
||||
---@return integer height
|
||||
function public.get_height()
|
||||
return protected.frame.h
|
||||
end
|
||||
function public.get_height() return protected.frame.h end
|
||||
|
||||
-- get the foreground/background colors
|
||||
---@nodiscard
|
||||
---@return cpair fg_bg
|
||||
function public.get_fg_bg() return protected.fg_bg end
|
||||
|
||||
-- get the element value
|
||||
---@nodiscard
|
||||
---@return any value
|
||||
function public.get_value()
|
||||
return protected.get_value()
|
||||
end
|
||||
function public.get_value() return protected.get_value() end
|
||||
|
||||
-- set the element value
|
||||
---@param value any new value
|
||||
function public.set_value(value)
|
||||
protected.set_value(value)
|
||||
end
|
||||
function public.set_value(value) protected.set_value(value) end
|
||||
|
||||
-- set minimum input value
|
||||
---@param min integer minimum allowed value
|
||||
function public.set_min(min)
|
||||
protected.set_min(min)
|
||||
end
|
||||
function public.set_min(min) protected.set_min(min) end
|
||||
|
||||
-- set maximum input value
|
||||
---@param max integer maximum allowed value
|
||||
function public.set_max(max)
|
||||
protected.set_max(max)
|
||||
end
|
||||
function public.set_max(max) protected.set_max(max) end
|
||||
|
||||
-- check if this element is enabled
|
||||
function public.is_enabled() return protected.enabled end
|
||||
|
||||
-- enable the element
|
||||
function public.enable()
|
||||
protected.enabled = true
|
||||
protected.enable()
|
||||
if not protected.enabled then
|
||||
protected.enabled = true
|
||||
protected.on_enabled()
|
||||
end
|
||||
end
|
||||
|
||||
-- disable the element
|
||||
function public.disable()
|
||||
protected.enabled = false
|
||||
protected.disable()
|
||||
if protected.enabled then
|
||||
protected.enabled = false
|
||||
protected.on_disabled()
|
||||
public.unfocus_all()
|
||||
end
|
||||
end
|
||||
|
||||
-- can this element be focused
|
||||
function public.is_focusable() return args.can_focus end
|
||||
|
||||
-- is this element focused
|
||||
function public.is_focused() return self.focused end
|
||||
|
||||
-- focus the element
|
||||
function public.focus()
|
||||
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
|
||||
|
||||
-- unfocus this element
|
||||
function public.unfocus()
|
||||
if args.can_focus and self.focused then
|
||||
self.focused = false
|
||||
protected.on_unfocused()
|
||||
end
|
||||
end
|
||||
|
||||
-- unfocus this element and all its children
|
||||
function public.unfocus_all()
|
||||
public.unfocus()
|
||||
for _, child in pairs(protected.children) do child.get().unfocus_all() end
|
||||
end
|
||||
|
||||
-- custom recolor command, varies by element if implemented
|
||||
---@vararg cpair|color color(s)
|
||||
function public.recolor(...)
|
||||
protected.recolor(...)
|
||||
end
|
||||
function public.recolor(...) protected.recolor(...) end
|
||||
|
||||
-- resize attributes of the element value if supported
|
||||
---@vararg number dimensions (element specific)
|
||||
function public.resize(...)
|
||||
protected.resize(...)
|
||||
end
|
||||
function public.resize(...) protected.resize(...) end
|
||||
|
||||
-- reposition the element window<br>
|
||||
-- offsets relative to parent frame are where (1, 1) would be on top of the parent's top left corner
|
||||
@@ -511,37 +720,89 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
---@param y integer y position relative to parent frame
|
||||
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 --
|
||||
|
||||
-- handle a monitor touch or mouse click
|
||||
-- handle a monitor touch or mouse click if this element is visible
|
||||
---@param event mouse_interaction mouse interaction event
|
||||
function public.handle_mouse(event)
|
||||
local x_ini, y_ini = event.initial.x, event.initial.y
|
||||
if protected.window.isVisible() then
|
||||
local x_ini, y_ini = event.initial.x, event.initial.y
|
||||
|
||||
local ini_in = protected.in_window_bounds(x_ini, y_ini)
|
||||
local ini_in = protected.in_window_bounds(x_ini, y_ini)
|
||||
|
||||
if ini_in then
|
||||
local event_T = core.events.mouse_transposed(event, self.position.x, self.position.y)
|
||||
if ini_in then
|
||||
if event.type == events.MOUSE_CLICK.UP or event.type == events.MOUSE_CLICK.DRAG then
|
||||
-- make sure we don't handle mouse events that started before this element was made visible
|
||||
if (event.initial.x ~= self.button_down[event.button].x) or (event.initial.y ~= self.button_down[event.button].y) then
|
||||
return
|
||||
end
|
||||
elseif event.type == events.MOUSE_CLICK.DOWN then
|
||||
self.button_down[event.button] = event.initial
|
||||
end
|
||||
|
||||
-- handle the mouse event then pass to children
|
||||
protected.handle_mouse(event_T)
|
||||
for _, child in pairs(protected.children) do child.handle_mouse(event_T) end
|
||||
local event_T = events.mouse_transposed(event, self.position.x, self.position.y)
|
||||
protected.handle_mouse(event_T)
|
||||
|
||||
-- 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()
|
||||
end
|
||||
else
|
||||
-- don't track clicks while hidden
|
||||
self.button_down[event.button] = events.new_coord_2d(-1, -1)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle a keyboard click if this element is visible and focused
|
||||
---@param event key_interaction keyboard interaction event
|
||||
function public.handle_key(event)
|
||||
if protected.window.isVisible() then
|
||||
if self.is_root and (event.type == events.KEY_CLICK.DOWN) and (event.key == keys.tab) then
|
||||
-- try to jump to the next/previous focusable field
|
||||
_tab_focusable(event.shift)
|
||||
else
|
||||
-- handle the key event then pass to children
|
||||
if self.focused then protected.handle_key(event) end
|
||||
for _, child in pairs(protected.children) do child.get().handle_key(event) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle text paste
|
||||
---@param text string pasted text
|
||||
function public.handle_paste(text)
|
||||
if protected.window.isVisible() then
|
||||
-- handle the paste event then pass to children
|
||||
if self.focused then protected.handle_paste(text) end
|
||||
for _, child in pairs(protected.children) do child.get().handle_paste(text) end
|
||||
end
|
||||
end
|
||||
|
||||
-- draw the element given new data
|
||||
---@vararg any new data
|
||||
function public.update(...)
|
||||
protected.on_update(...)
|
||||
end
|
||||
function public.update(...) protected.on_update(...) end
|
||||
|
||||
-- on a control request response
|
||||
---@param result any
|
||||
function public.on_response(result)
|
||||
protected.response_callback(result)
|
||||
end
|
||||
function public.on_response(result) protected.response_callback(result) end
|
||||
|
||||
-- register a callback with a PSIL, allowing for automatic unregister on delete<br>
|
||||
-- do not use graphics elements directly with PSIL subscribe()
|
||||
@@ -555,6 +816,9 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
|
||||
-- VISIBILITY & ANIMATIONS --
|
||||
|
||||
-- check if this element is visible
|
||||
function public.is_visible() return protected.window.isVisible() end
|
||||
|
||||
-- show the element and enables animations by default
|
||||
---@param animate? boolean true (default) to automatically resume animations
|
||||
function public.show(animate)
|
||||
@@ -564,47 +828,54 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
|
||||
-- hide the element and disables animations<br>
|
||||
-- this alone does not cause an element to be fully hidden, it only prevents updates from being shown<br>
|
||||
---@see graphics_element.redraw
|
||||
---@see graphics_element.content_redraw
|
||||
function public.hide()
|
||||
---@param clear? boolean true to visibly hide this element (redraws the parent)
|
||||
function public.hide(clear)
|
||||
public.freeze_all() -- stop animations for efficiency/performance
|
||||
public.unfocus_all()
|
||||
protected.window.setVisible(false)
|
||||
if clear and args.parent then args.parent.redraw() end
|
||||
end
|
||||
|
||||
-- start/resume animation(s)
|
||||
function public.animate()
|
||||
protected.start_anim()
|
||||
end
|
||||
function public.animate() protected.start_anim() end
|
||||
|
||||
-- start/resume animation(s) for this element and all its children<br>
|
||||
-- only animates if a window is visible
|
||||
function public.animate_all()
|
||||
if protected.window.isVisible() then
|
||||
public.animate()
|
||||
for _, child in pairs(protected.children) do child.animate_all() end
|
||||
for _, child in pairs(protected.children) do child.get().animate_all() end
|
||||
end
|
||||
end
|
||||
|
||||
-- freeze animation(s)
|
||||
function public.freeze()
|
||||
protected.stop_anim()
|
||||
end
|
||||
function public.freeze() protected.stop_anim() end
|
||||
|
||||
-- freeze animation(s) for this element and all its children
|
||||
function public.freeze_all()
|
||||
public.freeze()
|
||||
for _, child in pairs(protected.children) do child.freeze_all() end
|
||||
for _, child in pairs(protected.children) do child.get().freeze_all() end
|
||||
end
|
||||
|
||||
-- re-draw the element
|
||||
-- re-draw this element and all its children
|
||||
function public.redraw()
|
||||
protected.window.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
|
||||
|
||||
-- if a content window is set, clears it then re-draws all children
|
||||
function public.content_redraw()
|
||||
if protected.content_window ~= nil then
|
||||
protected.content_window.clear()
|
||||
for _, child in pairs(protected.children) do child.redraw() end
|
||||
for _, child in pairs(protected.children) do child.get().redraw() end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -36,49 +36,49 @@ local function waiting(args)
|
||||
|
||||
if state >= 0 and state < 7 then
|
||||
-- top
|
||||
e.window.setCursorPos(1 + math.floor(state / 2), 1)
|
||||
e.w_set_cur(1 + math.floor(state / 2), 1)
|
||||
if state % 2 == 0 then
|
||||
e.window.blit("\x8f", blit_fg, blit_bg)
|
||||
e.w_blit("\x8f", blit_fg, blit_bg)
|
||||
else
|
||||
e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
|
||||
e.w_blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
|
||||
end
|
||||
|
||||
-- bottom
|
||||
e.window.setCursorPos(4 - math.ceil(state / 2), 3)
|
||||
e.w_set_cur(4 - math.ceil(state / 2), 3)
|
||||
if state % 2 == 0 then
|
||||
e.window.blit("\x8f", blit_fg, blit_bg)
|
||||
e.w_blit("\x8f", blit_fg, blit_bg)
|
||||
else
|
||||
e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
|
||||
e.w_blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
|
||||
end
|
||||
else
|
||||
local st = state - 7
|
||||
|
||||
-- right
|
||||
if st % 3 == 0 then
|
||||
e.window.setCursorPos(4, 1 + math.floor(st / 3))
|
||||
e.window.blit("\x83", blit_bg, blit_fg)
|
||||
e.w_set_cur(4, 1 + math.floor(st / 3))
|
||||
e.w_blit("\x83", blit_bg, blit_fg)
|
||||
elseif st % 3 == 1 then
|
||||
e.window.setCursorPos(4, 1 + math.floor(st / 3))
|
||||
e.window.blit("\x8f", blit_bg, blit_fg)
|
||||
e.window.setCursorPos(4, 2 + math.floor(st / 3))
|
||||
e.window.blit("\x83", blit_fg, blit_bg)
|
||||
e.w_set_cur(4, 1 + math.floor(st / 3))
|
||||
e.w_blit("\x8f", blit_bg, blit_fg)
|
||||
e.w_set_cur(4, 2 + math.floor(st / 3))
|
||||
e.w_blit("\x83", blit_fg, blit_bg)
|
||||
else
|
||||
e.window.setCursorPos(4, 2 + math.floor(st / 3))
|
||||
e.window.blit("\x8f", blit_fg, blit_bg)
|
||||
e.w_set_cur(4, 2 + math.floor(st / 3))
|
||||
e.w_blit("\x8f", blit_fg, blit_bg)
|
||||
end
|
||||
|
||||
-- left
|
||||
if st % 3 == 0 then
|
||||
e.window.setCursorPos(1, 3 - math.floor(st / 3))
|
||||
e.window.blit("\x83", blit_fg, blit_bg)
|
||||
e.window.setCursorPos(1, 2 - math.floor(st / 3))
|
||||
e.window.blit("\x8f", blit_bg, blit_fg)
|
||||
e.w_set_cur(1, 3 - math.floor(st / 3))
|
||||
e.w_blit("\x83", blit_fg, blit_bg)
|
||||
e.w_set_cur(1, 2 - math.floor(st / 3))
|
||||
e.w_blit("\x8f", blit_bg, blit_fg)
|
||||
elseif st % 3 == 1 then
|
||||
e.window.setCursorPos(1, 2 - math.floor(st / 3))
|
||||
e.window.blit("\x83", blit_bg, blit_fg)
|
||||
e.w_set_cur(1, 2 - math.floor(st / 3))
|
||||
e.w_blit("\x83", blit_bg, blit_fg)
|
||||
else
|
||||
e.window.setCursorPos(1, 2 - math.floor(st / 3))
|
||||
e.window.blit("\x8f", blit_fg, blit_bg)
|
||||
e.w_set_cur(1, 2 - math.floor(st / 3))
|
||||
e.w_blit("\x8f", blit_fg, blit_bg)
|
||||
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
|
||||
@@ -1,7 +1,5 @@
|
||||
-- Color Map Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class colormap_args
|
||||
@@ -16,7 +14,7 @@ local element = require("graphics.element")
|
||||
---@return graphics_element element, element_id id
|
||||
local function colormap(args)
|
||||
local bkg = "008877FFCCEE114455DD9933BBAA2266"
|
||||
local spaces = util.spaces(32)
|
||||
local spaces = string.rep(" ", 32)
|
||||
|
||||
args.width = 32
|
||||
args.height = 1
|
||||
@@ -25,8 +23,13 @@ local function colormap(args)
|
||||
local e = element.new(args)
|
||||
|
||||
-- draw color map
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(spaces, bkg, bkg)
|
||||
function e.redraw()
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(spaces, bkg, bkg)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
132
graphics/elements/controls/app.lua
Normal file
132
graphics/elements/controls/app.lua
Normal file
@@ -0,0 +1,132 @@
|
||||
-- App Button Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class app_button_args
|
||||
---@field text string app icon text
|
||||
---@field title string app title text
|
||||
---@field callback function function to call on touch
|
||||
---@field app_fg_bg cpair app icon foreground/background colors
|
||||
---@field active_fg_bg? cpair foreground/background colors when pressed
|
||||
---@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
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new app button
|
||||
---@param args app_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function app_button(args)
|
||||
element.assert(type(args.text) == "string", "text is a required field")
|
||||
element.assert(type(args.title) == "string", "title is a required field")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
element.assert(type(args.app_fg_bg) == "table", "app_fg_bg is a required field")
|
||||
|
||||
args.height = 4
|
||||
args.width = 7
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- draw the app button
|
||||
local function draw()
|
||||
local fgd = args.app_fg_bg.fgd
|
||||
local bkg = args.app_fg_bg.bkg
|
||||
|
||||
if e.value then
|
||||
fgd = args.active_fg_bg.fgd
|
||||
bkg = args.active_fg_bg.bkg
|
||||
end
|
||||
|
||||
-- draw icon
|
||||
e.w_set_cur(2, 1)
|
||||
e.w_set_fgd(fgd)
|
||||
e.w_set_bkg(bkg)
|
||||
e.w_write("\x9f\x83\x83\x83")
|
||||
e.w_set_fgd(bkg)
|
||||
e.w_set_bkg(fgd)
|
||||
e.w_write("\x90")
|
||||
e.w_set_fgd(fgd)
|
||||
e.w_set_bkg(bkg)
|
||||
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(2, 3)
|
||||
e.w_write("\x82\x8f\x8f\x8f\x81")
|
||||
|
||||
-- write the icon text
|
||||
e.w_set_cur(4, 2)
|
||||
e.w_set_fgd(fgd)
|
||||
e.w_set_bkg(bkg)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
-- draw the app button as pressed (if active_fg_bg set)
|
||||
local function show_pressed()
|
||||
if e.enabled and args.active_fg_bg ~= nil then
|
||||
e.value = true
|
||||
e.w_set_fgd(args.active_fg_bg.fgd)
|
||||
e.w_set_bkg(args.active_fg_bg.bkg)
|
||||
draw()
|
||||
end
|
||||
end
|
||||
|
||||
-- draw the app button as unpressed (if active_fg_bg set)
|
||||
local function show_unpressed()
|
||||
if e.enabled and args.active_fg_bg ~= nil then
|
||||
e.value = false
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
draw()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled then
|
||||
if event.type == MOUSE_CLICK.TAP then
|
||||
show_pressed()
|
||||
-- show as unpressed in 0.25 seconds
|
||||
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
|
||||
args.callback()
|
||||
elseif event.type == MOUSE_CLICK.DOWN then
|
||||
show_pressed()
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
show_unpressed()
|
||||
if e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
args.callback()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value (true simulates pressing the app button)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
|
||||
end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
e.w_set_cur(math.floor((e.frame.w - string.len(args.title)) / 2) + 1, 4)
|
||||
e.w_write(args.title)
|
||||
draw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return app_button
|
||||
@@ -6,7 +6,8 @@ local element = require("graphics.element")
|
||||
---@class checkbox_args
|
||||
---@field label string checkbox text
|
||||
---@field box_fg_bg cpair colors for checkbox
|
||||
---@field callback function function to call on press
|
||||
---@field default? boolean default value
|
||||
---@field callback? function function to call on press
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
@@ -18,48 +19,75 @@ local element = require("graphics.element")
|
||||
---@param args checkbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function checkbox(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.controls.checkbox: label is a required field")
|
||||
assert(type(args.box_fg_bg) == "table", "graphics.elements.controls.checkbox: box_fg_bg is a required field")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.checkbox: callback is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.box_fg_bg) == "table", "box_fg_bg is a required field")
|
||||
|
||||
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)
|
||||
|
||||
e.value = false
|
||||
e.value = args.default == true
|
||||
|
||||
-- show the button state
|
||||
local function draw()
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if e.value then
|
||||
-- show as selected
|
||||
e.window.setTextColor(args.box_fg_bg.bkg)
|
||||
e.window.setBackgroundColor(args.box_fg_bg.fgd)
|
||||
e.window.write("\x88")
|
||||
e.window.setTextColor(args.box_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.write("\x95")
|
||||
e.w_set_fgd(args.box_fg_bg.bkg)
|
||||
e.w_set_bkg(args.box_fg_bg.fgd)
|
||||
e.w_write("\x88")
|
||||
e.w_set_fgd(args.box_fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write("\x95")
|
||||
else
|
||||
-- show as unselected
|
||||
e.window.setTextColor(e.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(args.box_fg_bg.bkg)
|
||||
e.window.write("\x88")
|
||||
e.window.setTextColor(args.box_fg_bg.bkg)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.write("\x95")
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(args.box_fg_bg.bkg)
|
||||
e.w_write("\x88")
|
||||
e.w_set_fgd(args.box_fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write("\x95")
|
||||
end
|
||||
end
|
||||
|
||||
-- write label text
|
||||
local function draw_label()
|
||||
if e.enabled and e.is_focused() then
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.fgd)
|
||||
e.w_write(args.label)
|
||||
else
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
end
|
||||
|
||||
-- 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
|
||||
draw()
|
||||
args.callback(e.value)
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == core.events.KEY_CLICK.DOWN then
|
||||
if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then
|
||||
e.value = not e.value
|
||||
draw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -70,14 +98,22 @@ local function checkbox(args)
|
||||
draw()
|
||||
end
|
||||
|
||||
-- write label text
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.write(args.label)
|
||||
-- handle focus
|
||||
e.on_focused = draw_label
|
||||
e.on_unfocused = draw_label
|
||||
|
||||
-- handle enable
|
||||
e.on_enabled = draw_label
|
||||
e.on_disabled = draw_label
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
draw()
|
||||
draw_label()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
-- Hazard-bordered Button Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
@@ -22,47 +21,42 @@ local element = require("graphics.element")
|
||||
---@param args hazard_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function hazard_button(args)
|
||||
assert(type(args.text) == "string", "graphics.elements.controls.hazard_button: text is a required field")
|
||||
assert(type(args.accent) == "number", "graphics.elements.controls.hazard_button: accent is a required field")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.hazard_button: callback is a required field")
|
||||
element.assert(type(args.text) == "string", "text is a required field")
|
||||
element.assert(type(args.accent) == "number", "accent is a required field")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
|
||||
-- static dimensions
|
||||
args.height = 3
|
||||
args.width = string.len(args.text) + 4
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- write the button text
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
|
||||
-- draw border
|
||||
---@param accent color accent color
|
||||
local function draw_border(accent)
|
||||
-- top
|
||||
e.window.setTextColor(accent)
|
||||
e.window.setBackgroundColor(args.fg_bg.bkg)
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write("\x99" .. util.strrep("\x89", args.width - 2) .. "\x99")
|
||||
e.w_set_fgd(accent)
|
||||
e.w_set_bkg(args.fg_bg.bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write("\x99" .. string.rep("\x89", args.width - 2) .. "\x99")
|
||||
|
||||
-- center left
|
||||
e.window.setCursorPos(1, 2)
|
||||
e.window.setTextColor(args.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(accent)
|
||||
e.window.write("\x99")
|
||||
e.w_set_cur(1, 2)
|
||||
e.w_set_fgd(args.fg_bg.bkg)
|
||||
e.w_set_bkg(accent)
|
||||
e.w_write("\x99")
|
||||
|
||||
-- center right
|
||||
e.window.setTextColor(args.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(accent)
|
||||
e.window.setCursorPos(args.width, 2)
|
||||
e.window.write("\x99")
|
||||
e.w_set_fgd(args.fg_bg.bkg)
|
||||
e.w_set_bkg(accent)
|
||||
e.w_set_cur(args.width, 2)
|
||||
e.w_write("\x99")
|
||||
|
||||
-- bottom
|
||||
e.window.setTextColor(accent)
|
||||
e.window.setBackgroundColor(args.fg_bg.bkg)
|
||||
e.window.setCursorPos(1, 3)
|
||||
e.window.write("\x99" .. util.strrep("\x98", args.width - 2) .. "\x99")
|
||||
e.w_set_fgd(accent)
|
||||
e.w_set_bkg(args.fg_bg.bkg)
|
||||
e.w_set_cur(1, 3)
|
||||
e.w_write("\x99" .. string.rep("\x98", args.width - 2) .. "\x99")
|
||||
end
|
||||
|
||||
-- on request timeout: recursively calls itself to double flash button text
|
||||
@@ -73,9 +67,9 @@ local function hazard_button(args)
|
||||
|
||||
if n == 0 then
|
||||
-- go back off
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
if n >= 4 then
|
||||
@@ -83,18 +77,18 @@ local function hazard_button(args)
|
||||
elseif n % 2 == 0 then
|
||||
-- toggle text color on after 0.25 seconds
|
||||
tcd.dispatch(0.25, function ()
|
||||
e.window.setTextColor(args.accent)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.accent)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
on_timeout(n + 1)
|
||||
on_timeout(n + 1)
|
||||
end)
|
||||
elseif n % 1 then
|
||||
-- toggle text color off after 0.25 seconds
|
||||
tcd.dispatch(0.25, function ()
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
on_timeout(n + 1)
|
||||
end)
|
||||
end
|
||||
@@ -102,9 +96,9 @@ local function hazard_button(args)
|
||||
|
||||
-- blink routine for success indication
|
||||
local function on_success()
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
-- blink routine for failure indication
|
||||
@@ -115,9 +109,9 @@ local function hazard_button(args)
|
||||
|
||||
if n == 0 then
|
||||
-- go back off
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
if n >= 2 then
|
||||
@@ -125,17 +119,17 @@ local function hazard_button(args)
|
||||
elseif n % 2 == 0 then
|
||||
-- toggle text color on after 0.5 seconds
|
||||
tcd.dispatch(0.5, function ()
|
||||
e.window.setTextColor(args.accent)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.accent)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
on_failure(n + 1)
|
||||
end)
|
||||
elseif n % 1 then
|
||||
-- toggle text color off after 0.25 seconds
|
||||
tcd.dispatch(0.25, function ()
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
on_failure(n + 1)
|
||||
end)
|
||||
end
|
||||
@@ -144,24 +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.window.setTextColor(args.accent)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.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)
|
||||
|
||||
-- call the touch callback
|
||||
args.callback()
|
||||
end
|
||||
args.callback()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -175,29 +166,37 @@ local function hazard_button(args)
|
||||
-- set the value (true simulates pressing the button)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
|
||||
end
|
||||
|
||||
-- show the button as disabled
|
||||
function e.disable()
|
||||
function e.on_disabled()
|
||||
if args.dis_colors then
|
||||
draw_border(args.dis_colors.color_a)
|
||||
e.window.setTextColor(args.dis_colors.color_b)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.dis_colors.color_b)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
end
|
||||
|
||||
-- show the button as enabled
|
||||
function e.enable()
|
||||
function e.on_enabled()
|
||||
draw_border(args.accent)
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
-- initial draw of border
|
||||
draw_border(args.accent)
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
-- write the button text and draw border
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
draw_border(args.accent)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -29,13 +29,11 @@ local element = require("graphics.element")
|
||||
---@param args multi_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function multi_button(args)
|
||||
assert(type(args.options) == "table", "graphics.elements.controls.multi_button: options is a required field")
|
||||
assert(#args.options > 0, "graphics.elements.controls.multi_button: at least one option is required")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.multi_button: callback is a required field")
|
||||
assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0),
|
||||
"graphics.elements.controls.multi_button: default must be nil or a number > 0")
|
||||
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||
"graphics.elements.controls.multi_button: min_width must be nil or a number > 0")
|
||||
element.assert(type(args.options) == "table", "options is a required field")
|
||||
element.assert(#args.options > 0, "at least one option is required")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
|
||||
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")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
@@ -71,23 +69,23 @@ local function multi_button(args)
|
||||
end
|
||||
|
||||
-- show the button state
|
||||
local function draw()
|
||||
function e.redraw()
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type button_option
|
||||
|
||||
e.window.setCursorPos(opt._start_x, 1)
|
||||
e.w_set_cur(opt._start_x, 1)
|
||||
|
||||
if e.value == i then
|
||||
-- show as pressed
|
||||
e.window.setTextColor(opt.active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(opt.active_fg_bg.bkg)
|
||||
e.w_set_fgd(opt.active_fg_bg.fgd)
|
||||
e.w_set_bkg(opt.active_fg_bg.bkg)
|
||||
else
|
||||
-- show as unpressed
|
||||
e.window.setTextColor(opt.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(opt.fg_bg.bkg)
|
||||
e.w_set_fgd(opt.fg_bg.fgd)
|
||||
e.w_set_bkg(opt.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.window.write(util.pad(opt.text, button_width))
|
||||
e.w_write(util.pad(opt.text, button_width))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -115,7 +113,7 @@ local function multi_button(args)
|
||||
-- tap always has identical coordinates, so this always passes for taps
|
||||
if button_ini == button_cur and button_cur ~= nil then
|
||||
e.value = button_cur
|
||||
draw()
|
||||
e.redraw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
@@ -125,11 +123,11 @@ local function multi_button(args)
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw()
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -5,12 +5,16 @@ local tcd = require("scada-common.tcd")
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
|
||||
---@class push_button_args
|
||||
---@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
|
||||
@@ -25,14 +29,15 @@ local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
---@param args push_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function push_button(args)
|
||||
assert(type(args.text) == "string", "graphics.elements.controls.push_button: text is a required field")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.push_button: callback is a required field")
|
||||
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||
"graphics.elements.controls.push_button: min_width must be nil or a number > 0")
|
||||
element.assert(type(args.text) == "string", "text is a required field")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
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
|
||||
|
||||
-- single line height, calculate width
|
||||
-- set automatic settings
|
||||
args.can_focus = true
|
||||
args.height = 1
|
||||
args.min_width = args.min_width or 0
|
||||
args.width = math.max(text_width, args.min_width)
|
||||
@@ -40,25 +45,31 @@ 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
|
||||
local function draw()
|
||||
function e.redraw()
|
||||
e.window.clear()
|
||||
|
||||
-- write the button text
|
||||
e.window.setCursorPos(h_pad, v_pad)
|
||||
e.window.write(args.text)
|
||||
e.w_set_cur(h_pad, v_pad)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
-- draw the button as pressed (if active_fg_bg set)
|
||||
local function show_pressed()
|
||||
if e.enabled and args.active_fg_bg ~= nil then
|
||||
e.value = true
|
||||
e.window.setTextColor(args.active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(args.active_fg_bg.bkg)
|
||||
draw()
|
||||
e.w_set_fgd(args.active_fg_bg.fgd)
|
||||
e.w_set_bkg(args.active_fg_bg.bkg)
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -66,9 +77,9 @@ local function push_button(args)
|
||||
local function show_unpressed()
|
||||
if e.enabled and args.active_fg_bg ~= nil then
|
||||
e.value = false
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
draw()
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -76,14 +87,14 @@ local function push_button(args)
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled then
|
||||
if event.type == CLICK_TYPE.TAP then
|
||||
if event.type == MOUSE_CLICK.TAP then
|
||||
show_pressed()
|
||||
-- show as unpressed in 0.25 seconds
|
||||
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
|
||||
args.callback()
|
||||
elseif event.type == CLICK_TYPE.DOWN then
|
||||
elseif event.type == MOUSE_CLICK.DOWN then
|
||||
show_pressed()
|
||||
elseif event.type == CLICK_TYPE.UP then
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
show_unpressed()
|
||||
if e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
args.callback()
|
||||
@@ -92,34 +103,49 @@ local function push_button(args)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == KEY_CLICK.DOWN then
|
||||
if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then
|
||||
args.callback()
|
||||
e.defocus()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value (true simulates pressing the button)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
|
||||
end
|
||||
|
||||
-- show butten as enabled
|
||||
function e.enable()
|
||||
function e.on_enabled()
|
||||
if args.dis_fg_bg ~= nil then
|
||||
e.value = false
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
draw()
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- show button as disabled
|
||||
function e.disable()
|
||||
function e.on_disabled()
|
||||
if args.dis_fg_bg ~= nil then
|
||||
e.value = false
|
||||
e.window.setTextColor(args.dis_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(args.dis_fg_bg.bkg)
|
||||
draw()
|
||||
e.w_set_fgd(args.dis_fg_bg.fgd)
|
||||
e.w_set_bkg(args.dis_fg_bg.bkg)
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle focus
|
||||
e.on_focused = show_pressed
|
||||
e.on_unfocused = show_unpressed
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
203
graphics/elements/controls/radio_2d.lua
Normal file
203
graphics/elements/controls/radio_2d.lua
Normal file
@@ -0,0 +1,203 @@
|
||||
-- 2D Radio Button Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class radio_2d_args
|
||||
---@field rows integer
|
||||
---@field columns integer
|
||||
---@field options table
|
||||
---@field radio_colors cpair radio button colors (inner & outer)
|
||||
---@field select_color? color color for radio button when selected
|
||||
---@field color_map? table colors for each radio button when selected
|
||||
---@field disable_color? color color for radio button when disabled
|
||||
---@field disable_fg_bg? cpair text colors when disabled
|
||||
---@field default? integer default state, defaults to options[1]
|
||||
---@field callback? function function to call on touch
|
||||
---@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
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new 2D radio button list (latch selection, exclusively one color at a time)
|
||||
---@param args radio_2d_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function radio_2d_button(args)
|
||||
element.assert(type(args.options) == "table" and #args.options > 0, "options should be a table with length >= 1")
|
||||
element.assert(util.is_int(args.rows) and util.is_int(args.columns), "rows/columns must be integers")
|
||||
element.assert((args.rows * args.columns) >= #args.options, "rows x columns size insufficient for provided number of options")
|
||||
element.assert(type(args.radio_colors) == "table", "radio_colors is a required field")
|
||||
element.assert(type(args.select_color) == "number" or type(args.color_map) == "table", "select_color or color_map is required")
|
||||
element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
|
||||
|
||||
local array = {}
|
||||
local col_widths = {}
|
||||
|
||||
local next_idx = 1
|
||||
local total_width = 0
|
||||
local max_rows = 1
|
||||
|
||||
local focused_opt = 1
|
||||
local focus_x, focus_y = 1, 1
|
||||
|
||||
-- build table to display
|
||||
for col = 1, args.columns do
|
||||
local max_width = 0
|
||||
array[col] = {}
|
||||
|
||||
for row = 1, args.rows do
|
||||
local len = string.len(args.options[next_idx])
|
||||
if len > max_width then max_width = len end
|
||||
if row > max_rows then max_rows = row end
|
||||
|
||||
table.insert(array[col], { text = args.options[next_idx], id = next_idx, x_1 = 1 + total_width, x_2 = 2 + total_width + len })
|
||||
|
||||
next_idx = next_idx + 1
|
||||
if next_idx > #args.options then break end
|
||||
end
|
||||
|
||||
table.insert(col_widths, max_width + 3)
|
||||
total_width = total_width + max_width + 3
|
||||
if next_idx > #args.options then break end
|
||||
end
|
||||
|
||||
args.can_focus = true
|
||||
args.width = total_width
|
||||
args.height = max_rows
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- selected option (convert nil to 1 if missing)
|
||||
e.value = args.default or 1
|
||||
|
||||
-- draw the element
|
||||
function e.redraw()
|
||||
local col_x = 1
|
||||
|
||||
local radio_color_b = util.trinary(type(args.disable_color) == "number" and not e.enabled, args.disable_color, args.radio_colors.color_b)
|
||||
|
||||
for col = 1, #array do
|
||||
for row = 1, #array[col] do
|
||||
local opt = array[col][row]
|
||||
local select_color = args.select_color
|
||||
|
||||
if type(args.color_map) == "table" and args.color_map[opt.id] then
|
||||
select_color = args.color_map[opt.id]
|
||||
end
|
||||
|
||||
local inner_color = util.trinary((e.value == opt.id) and e.enabled, radio_color_b, args.radio_colors.color_a)
|
||||
local outer_color = util.trinary((e.value == opt.id) and e.enabled, select_color, radio_color_b)
|
||||
|
||||
e.w_set_cur(col_x, row)
|
||||
|
||||
e.w_set_fgd(inner_color)
|
||||
e.w_set_bkg(outer_color)
|
||||
e.w_write("\x88")
|
||||
|
||||
e.w_set_fgd(outer_color)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write("\x95")
|
||||
|
||||
if opt.id == focused_opt then
|
||||
focus_x, focus_y = row, col
|
||||
end
|
||||
|
||||
-- write button text
|
||||
if opt.id == focused_opt and e.is_focused() and e.enabled then
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.fgd)
|
||||
elseif type(args.disable_fg_bg) == "table" and not e.enabled then
|
||||
e.w_set_fgd(args.disable_fg_bg.fgd)
|
||||
e.w_set_bkg(args.disable_fg_bg.bkg)
|
||||
else
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.w_write(opt.text)
|
||||
end
|
||||
|
||||
col_x = col_x + col_widths[col]
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) and (event.initial.y == event.current.y) then
|
||||
-- determine what was pressed
|
||||
for _, row in ipairs(array) do
|
||||
local elem = row[event.current.y]
|
||||
if elem ~= nil and event.initial.x >= elem.x_1 and event.initial.x <= elem.x_2 and event.current.x >= elem.x_1 and event.current.x <= elem.x_2 then
|
||||
e.value = elem.id
|
||||
focused_opt = elem.id
|
||||
e.redraw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == core.events.KEY_CLICK.DOWN or event.type == core.events.KEY_CLICK.HELD then
|
||||
if event.type == core.events.KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then
|
||||
e.value = focused_opt
|
||||
e.redraw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
elseif event.key == keys.down then
|
||||
if focused_opt < #args.options then
|
||||
focused_opt = focused_opt + 1
|
||||
e.redraw()
|
||||
end
|
||||
elseif event.key == keys.up then
|
||||
if focused_opt > 1 then
|
||||
focused_opt = focused_opt - 1
|
||||
e.redraw()
|
||||
end
|
||||
elseif event.key == keys.right then
|
||||
if array[focus_y + 1] and array[focus_y + 1][focus_x] then
|
||||
focused_opt = array[focus_y + 1][focus_x].id
|
||||
else focused_opt = array[1][focus_x].id end
|
||||
e.redraw()
|
||||
elseif event.key == keys.left then
|
||||
if array[focus_y - 1] and array[focus_y - 1][focus_x] then
|
||||
focused_opt = array[focus_y - 1][focus_x].id
|
||||
e.redraw()
|
||||
elseif array[#array][focus_x] then
|
||||
focused_opt = array[#array][focus_x].id
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
if type(val) == "number" and val > 0 and val <= #args.options then
|
||||
e.value = val
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle focus & enable
|
||||
e.on_focused = e.redraw
|
||||
e.on_unfocused = e.redraw
|
||||
e.on_enabled = e.redraw
|
||||
e.on_disabled = e.redraw
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return radio_2d_button
|
||||
@@ -1,15 +1,19 @@
|
||||
-- Radio Button Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
|
||||
---@class radio_button_args
|
||||
---@field options table button options
|
||||
---@field callback function function to call on touch
|
||||
---@field radio_colors cpair colors for radio button center dot when active (a) or inactive (b)
|
||||
---@field radio_bg color background color of radio button
|
||||
---@field radio_colors cpair radio button colors (inner & outer)
|
||||
---@field select_color color color for radio button border when selected
|
||||
---@field default? integer default state, defaults to options[1]
|
||||
---@field min_width? integer text length + 2 if omitted
|
||||
---@field callback? function function to call on touch
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
@@ -21,16 +25,12 @@ local element = require("graphics.element")
|
||||
---@param args radio_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function radio_button(args)
|
||||
assert(type(args.options) == "table", "graphics.elements.controls.radio_button: options is a required field")
|
||||
assert(#args.options > 0, "graphics.elements.controls.radio_button: at least one option is required")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.radio_button: callback is a required field")
|
||||
assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0),
|
||||
"graphics.elements.controls.radio_button: default must be nil or a number > 0")
|
||||
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||
"graphics.elements.controls.radio_button: min_width must be nil or a number > 0")
|
||||
|
||||
-- one line per option
|
||||
args.height = #args.options
|
||||
element.assert(type(args.options) == "table", "options is a required field")
|
||||
element.assert(#args.options > 0, "at least one option is required")
|
||||
element.assert(type(args.radio_colors) == "table", "radio_colors is a required field")
|
||||
element.assert(type(args.select_color) == "number", "select_color is a required field")
|
||||
element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
|
||||
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")
|
||||
|
||||
-- determine widths
|
||||
local max_width = 1
|
||||
@@ -43,53 +43,83 @@ local function radio_button(args)
|
||||
|
||||
local button_text_width = math.max(max_width, args.min_width or 0)
|
||||
|
||||
-- set automatic args
|
||||
args.can_focus = true
|
||||
args.width = button_text_width + 2
|
||||
args.height = #args.options -- one line per option
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local focused_opt = 1
|
||||
|
||||
-- button state (convert nil to 1 if missing)
|
||||
e.value = args.default or 1
|
||||
|
||||
-- show the button state
|
||||
local function draw()
|
||||
function e.redraw()
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type string
|
||||
|
||||
e.window.setCursorPos(1, i)
|
||||
local inner_color = util.trinary(e.value == i, args.radio_colors.color_b, args.radio_colors.color_a)
|
||||
local outer_color = util.trinary(e.value == i, args.select_color, args.radio_colors.color_b)
|
||||
|
||||
if e.value == i then
|
||||
-- show as selected
|
||||
e.window.setTextColor(args.radio_colors.color_a)
|
||||
e.window.setBackgroundColor(args.radio_bg)
|
||||
else
|
||||
-- show as unselected
|
||||
e.window.setTextColor(args.radio_colors.color_b)
|
||||
e.window.setBackgroundColor(args.radio_bg)
|
||||
end
|
||||
e.w_set_cur(1, i)
|
||||
|
||||
e.window.write("\x88")
|
||||
e.w_set_fgd(inner_color)
|
||||
e.w_set_bkg(outer_color)
|
||||
e.w_write("\x88")
|
||||
|
||||
e.window.setTextColor(args.radio_bg)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.write("\x95")
|
||||
e.w_set_fgd(outer_color)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write("\x95")
|
||||
|
||||
-- write button text
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.write(opt)
|
||||
if i == focused_opt and e.is_focused() and e.enabled then
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.fgd)
|
||||
else
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.w_write(opt)
|
||||
end
|
||||
end
|
||||
|
||||
-- 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
|
||||
draw()
|
||||
args.callback(e.value)
|
||||
focused_opt = e.value
|
||||
e.redraw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
end
|
||||
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.type == KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then
|
||||
e.value = focused_opt
|
||||
e.redraw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
elseif event.key == keys.down then
|
||||
if focused_opt < #args.options then
|
||||
focused_opt = focused_opt + 1
|
||||
e.redraw()
|
||||
end
|
||||
elseif event.key == keys.up then
|
||||
if focused_opt > 1 then
|
||||
focused_opt = focused_opt - 1
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -97,12 +127,20 @@ local function radio_button(args)
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw()
|
||||
if type(val) == "number" and val > 0 and val <= #args.options then
|
||||
e.value = val
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle focus & enable
|
||||
e.on_focused = e.redraw
|
||||
e.on_unfocused = e.redraw
|
||||
e.on_enabled = e.redraw
|
||||
e.on_disabled = e.redraw
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
-- Sidebar Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
|
||||
---@class sidebar_tab
|
||||
---@field char string character identifier
|
||||
---@field color cpair tab colors (fg/bg)
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@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
|
||||
@@ -26,53 +21,71 @@ local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
---@param args sidebar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function sidebar(args)
|
||||
assert(type(args.tabs) == "table", "graphics.elements.controls.sidebar: tabs is a required field")
|
||||
assert(#args.tabs > 0, "graphics.elements.controls.sidebar: at least one tab is required")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.sidebar: callback is a required field")
|
||||
|
||||
-- always 3 wide
|
||||
args.width = 3
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
assert(e.frame.h >= (#args.tabs * 3), "graphics.elements.controls.sidebar: height insufficent to display all tabs")
|
||||
|
||||
-- default to 1st tab
|
||||
e.value = 1
|
||||
|
||||
local was_pressed = false
|
||||
local tabs = {}
|
||||
|
||||
-- show the button state
|
||||
---@param pressed boolean if the currently selected tab should appear as actively pressed
|
||||
---@param pressed? boolean if the currently selected tab should appear as actively pressed
|
||||
---@param pressed_idx? integer optional index to show as held (that is not yet selected)
|
||||
local function draw(pressed, pressed_idx)
|
||||
pressed = util.trinary(pressed == nil, was_pressed, pressed)
|
||||
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.window.setCursorPos(1, y)
|
||||
e.w_set_cur(1, y)
|
||||
|
||||
if pressed and i == pressed_idx then
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
else
|
||||
e.window.setTextColor(tab.color.fgd)
|
||||
e.window.setBackgroundColor(tab.color.bkg)
|
||||
e.w_set_fgd(tab.color.fgd)
|
||||
e.w_set_bkg(tab.color.bkg)
|
||||
end
|
||||
|
||||
e.window.write(" ")
|
||||
e.window.setCursorPos(1, y + 1)
|
||||
if e.value == i then
|
||||
-- show as selected
|
||||
e.window.write(" " .. tab.char .. "\x10")
|
||||
else
|
||||
-- show as unselected
|
||||
e.window.write(" " .. tab.char .. " ")
|
||||
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
|
||||
e.window.setCursorPos(1, y + 2)
|
||||
e.window.write(" ")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -81,26 +94,28 @@ 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
|
||||
if event.type == CLICK_TYPE.TAP 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)
|
||||
elseif event.type == CLICK_TYPE.DOWN then
|
||||
tab.callback()
|
||||
elseif event.type == MOUSE_CLICK.DOWN then
|
||||
draw(true, cur_idx)
|
||||
elseif event.type == CLICK_TYPE.UP then
|
||||
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 == CLICK_TYPE.UP then
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
draw(false)
|
||||
end
|
||||
end
|
||||
@@ -113,8 +128,39 @@ local function sidebar(args)
|
||||
draw(false)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw(false)
|
||||
-- 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
|
||||
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -29,8 +29,8 @@ local function spinbox(args)
|
||||
local wn_prec = args.whole_num_precision
|
||||
local fr_prec = args.fractional_precision
|
||||
|
||||
assert(util.is_int(wn_prec), "graphics.element.controls.spinbox_numeric: whole number precision must be an integer")
|
||||
assert(util.is_int(fr_prec), "graphics.element.controls.spinbox_numeric: fractional precision must be an integer")
|
||||
element.assert(util.is_int(wn_prec), "whole number precision must be an integer")
|
||||
element.assert(util.is_int(fr_prec), "fractional precision must be an integer")
|
||||
|
||||
local fmt, fmt_init ---@type string, string
|
||||
|
||||
@@ -44,7 +44,7 @@ local function spinbox(args)
|
||||
|
||||
local dec_point_x = args.whole_num_precision + 1
|
||||
|
||||
assert(type(args.arrow_fg_bg) == "table", "graphics.element.spinbox_numeric: arrow_fg_bg is a required field")
|
||||
element.assert(type(args.arrow_fg_bg) == "table", "arrow_fg_bg is a required field")
|
||||
|
||||
-- determine widths
|
||||
args.width = wn_prec + fr_prec + util.trinary(fr_prec > 0, 1, 0)
|
||||
@@ -58,22 +58,20 @@ local function spinbox(args)
|
||||
|
||||
-- draw the arrows
|
||||
local function draw_arrows(color)
|
||||
e.window.setBackgroundColor(args.arrow_fg_bg.bkg)
|
||||
e.window.setTextColor(color)
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write(util.strrep("\x1e", wn_prec))
|
||||
e.window.setCursorPos(1, 3)
|
||||
e.window.write(util.strrep("\x1f", wn_prec))
|
||||
e.w_set_bkg(args.arrow_fg_bg.bkg)
|
||||
e.w_set_fgd(color)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(string.rep("\x1e", wn_prec))
|
||||
e.w_set_cur(1, 3)
|
||||
e.w_write(string.rep("\x1f", wn_prec))
|
||||
if fr_prec > 0 then
|
||||
e.window.setCursorPos(1 + wn_prec, 1)
|
||||
e.window.write(" " .. util.strrep("\x1e", fr_prec))
|
||||
e.window.setCursorPos(1 + wn_prec, 3)
|
||||
e.window.write(" " .. util.strrep("\x1f", fr_prec))
|
||||
e.w_set_cur(1 + wn_prec, 1)
|
||||
e.w_write(" " .. string.rep("\x1e", fr_prec))
|
||||
e.w_set_cur(1 + wn_prec, 3)
|
||||
e.w_write(" " .. string.rep("\x1f", fr_prec))
|
||||
end
|
||||
end
|
||||
|
||||
draw_arrows(args.arrow_fg_bg.fgd)
|
||||
|
||||
-- populate digits from current value
|
||||
local function set_digits()
|
||||
local initial_str = util.sprintf(fmt_init, e.value)
|
||||
@@ -119,35 +117,29 @@ local function spinbox(args)
|
||||
end
|
||||
|
||||
-- draw
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setCursorPos(1, 2)
|
||||
e.window.write(util.sprintf(fmt, e.value))
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_cur(1, 2)
|
||||
e.w_write(util.sprintf(fmt, e.value))
|
||||
end
|
||||
|
||||
-- init with the default value
|
||||
show_num()
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- only handle if on an increment or decrement arrow
|
||||
if e.enabled and core.events.was_clicked(event.type) and
|
||||
(event.current.x ~= dec_point_x) and (event.current.y ~= 2) then
|
||||
if event.current.x == event.initial.x and event.current.y == event.initial.y then
|
||||
local idx = util.trinary(event.current.x > dec_point_x, event.current.x - 1, event.current.x)
|
||||
if digits[idx] ~= nil then
|
||||
if event.current.y == 1 then
|
||||
-- increment
|
||||
digits[idx] = digits[idx] + 1
|
||||
elseif event.current.y == 3 then
|
||||
-- decrement
|
||||
digits[idx] = digits[idx] - 1
|
||||
end
|
||||
|
||||
update_value()
|
||||
show_num()
|
||||
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
|
||||
@@ -176,18 +168,19 @@ local function spinbox(args)
|
||||
end
|
||||
|
||||
-- enable this input
|
||||
function e.enable()
|
||||
draw_arrows(args.arrow_fg_bg.fgd)
|
||||
end
|
||||
function e.on_enabled() draw_arrows(args.arrow_fg_bg.fgd) end
|
||||
|
||||
-- disable this input
|
||||
function e.disable()
|
||||
draw_arrows(args.arrow_disable or colors.lightGray)
|
||||
function e.on_disabled() draw_arrows(args.arrow_disable or colors.lightGray) end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
show_num()
|
||||
draw_arrows(util.trinary(e.enabled, args.arrow_fg_bg.fgd, args.arrow_disable or colors.lightGray))
|
||||
end
|
||||
|
||||
-- default to zero, init digits table
|
||||
e.value = 0
|
||||
set_digits()
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -21,15 +21,13 @@ local element = require("graphics.element")
|
||||
---@param args switch_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function switch_button(args)
|
||||
assert(type(args.text) == "string", "graphics.elements.controls.switch_button: text is a required field")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.switch_button: callback is a required field")
|
||||
assert(type(args.active_fg_bg) == "table", "graphics.elements.controls.switch_button: active_fg_bg is a required field")
|
||||
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||
"graphics.elements.controls.switch_button: min_width must be nil or a number > 0")
|
||||
element.assert(type(args.text) == "string", "text is a required field")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
element.assert(type(args.active_fg_bg) == "table", "active_fg_bg is a required field")
|
||||
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)
|
||||
|
||||
-- single line height, calculate width
|
||||
args.height = 1
|
||||
args.min_width = args.min_width or 0
|
||||
args.width = math.max(text_width, args.min_width)
|
||||
@@ -37,56 +35,46 @@ local function switch_button(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- button state (convert nil to false if missing)
|
||||
e.value = args.default or false
|
||||
|
||||
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1
|
||||
local v_pad = math.floor(e.frame.h / 2) + 1
|
||||
|
||||
-- show the button state
|
||||
local function draw_state()
|
||||
function e.redraw()
|
||||
if e.value then
|
||||
-- show as pressed
|
||||
e.window.setTextColor(args.active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(args.active_fg_bg.bkg)
|
||||
e.w_set_fgd(args.active_fg_bg.fgd)
|
||||
e.w_set_bkg(args.active_fg_bg.bkg)
|
||||
else
|
||||
-- show as unpressed
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- clear to redraw background
|
||||
e.window.clear()
|
||||
|
||||
-- write the button text
|
||||
e.window.setCursorPos(h_pad, v_pad)
|
||||
e.window.write(args.text)
|
||||
e.w_set_cur(h_pad, v_pad)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw_state()
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) then
|
||||
-- toggle state
|
||||
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
|
||||
draw_state()
|
||||
|
||||
-- call the touch callback with state
|
||||
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)
|
||||
-- set state
|
||||
e.value = val
|
||||
draw_state()
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
|
||||
@@ -27,13 +27,11 @@ local element = require("graphics.element")
|
||||
---@param args tabbar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function tabbar(args)
|
||||
assert(type(args.tabs) == "table", "graphics.elements.controls.tabbar: tabs is a required field")
|
||||
assert(#args.tabs > 0, "graphics.elements.controls.tabbar: at least one tab is required")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.tabbar: callback is a required field")
|
||||
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||
"graphics.elements.controls.tabbar: min_width must be nil or a number > 0")
|
||||
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")
|
||||
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")
|
||||
|
||||
-- always 1 tall
|
||||
args.height = 1
|
||||
|
||||
-- determine widths
|
||||
@@ -50,7 +48,7 @@ local function tabbar(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
assert(e.frame.w >= (button_width * #args.tabs), "graphics.elements.controls.tabbar: width insufficent to display all tabs")
|
||||
element.assert(e.frame.w >= (button_width * #args.tabs), "width insufficent to display all tabs")
|
||||
|
||||
-- default to 1st tab
|
||||
e.value = 1
|
||||
@@ -67,21 +65,21 @@ local function tabbar(args)
|
||||
end
|
||||
|
||||
-- show the tab state
|
||||
local function draw()
|
||||
function e.redraw()
|
||||
for i = 1, #args.tabs do
|
||||
local tab = args.tabs[i] ---@type tabbar_tab
|
||||
|
||||
e.window.setCursorPos(tab._start_x, 1)
|
||||
e.w_set_cur(tab._start_x, 1)
|
||||
|
||||
if e.value == i then
|
||||
e.window.setTextColor(tab.color.fgd)
|
||||
e.window.setBackgroundColor(tab.color.bkg)
|
||||
e.w_set_fgd(tab.color.fgd)
|
||||
e.w_set_bkg(tab.color.bkg)
|
||||
else
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.window.write(util.pad(tab.name, button_width))
|
||||
e.w_write(util.pad(tab.name, button_width))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -100,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)
|
||||
@@ -109,7 +107,7 @@ local function tabbar(args)
|
||||
-- tap always has identical coordinates, so this always passes for taps
|
||||
if tab_ini == tab_cur and tab_cur ~= nil then
|
||||
e.value = tab_cur
|
||||
draw()
|
||||
e.redraw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
@@ -119,11 +117,11 @@ local function tabbar(args)
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw()
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
197
graphics/elements/form/number_field.lua
Normal file
197
graphics/elements/form/number_field.lua
Normal file
@@ -0,0 +1,197 @@
|
||||
-- Numeric Value Entry Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
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 number_field_args
|
||||
---@field default? number default value, defaults to 0
|
||||
---@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
|
||||
---@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 fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new numeric entry field
|
||||
---@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
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local has_decimal = false
|
||||
|
||||
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_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 and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
if core.events.was_clicked(event.type) then
|
||||
e.take_focus()
|
||||
|
||||
if event.type == MOUSE_CLICK.UP then
|
||||
ifield.move_cursor(event.current.x)
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
|
||||
ifield.select_all()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 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_chars then
|
||||
if tonumber(event.name) then
|
||||
if e.value == 0 then e.value = "" end
|
||||
ifield.try_insert_char(event.name)
|
||||
end
|
||||
elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
|
||||
if (event.key == keys.backspace or event.key == keys.delete) and (string.len(e.value) > 0) then
|
||||
ifield.backspace()
|
||||
has_decimal = string.find(e.value, "%.") ~= nil
|
||||
elseif (event.key == keys.period or event.key == keys.numPadDecimal) and (not has_decimal) and args.allow_decimal then
|
||||
has_decimal = true
|
||||
ifield.try_insert_char(".")
|
||||
elseif (event.key == keys.minus or event.key == keys.numPadSubtract) and (string.len(e.value) == 0) and args.allow_negative then
|
||||
ifield.set_value("-")
|
||||
elseif event.key == keys.left then
|
||||
ifield.nav_left()
|
||||
elseif event.key == keys.right then
|
||||
ifield.nav_right()
|
||||
elseif event.key == keys.a and event.ctrl then
|
||||
ifield.select_all()
|
||||
elseif event.key == keys.home or event.key == keys.up then
|
||||
ifield.nav_start()
|
||||
elseif event.key == keys["end"] or event.key == keys.down then
|
||||
ifield.nav_end()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value (must be a number)
|
||||
---@param val number number to show
|
||||
function e.set_value(val)
|
||||
if tonumber(val) then ifield.set_value("" .. tonumber(val)) end
|
||||
end
|
||||
|
||||
-- set minimum input value
|
||||
---@param min integer minimum allowed value
|
||||
function e.set_min(min)
|
||||
args.min = min
|
||||
e.on_unfocused()
|
||||
end
|
||||
|
||||
-- set maximum input value
|
||||
---@param max integer maximum allowed value
|
||||
function e.set_max(max)
|
||||
args.max = max
|
||||
e.on_unfocused()
|
||||
end
|
||||
|
||||
-- replace text with pasted text if its a number
|
||||
---@param text string string pasted
|
||||
function e.handle_paste(text)
|
||||
if tonumber(text) then
|
||||
ifield.set_value("" .. tonumber(text))
|
||||
else
|
||||
ifield.set_value("0")
|
||||
end
|
||||
end
|
||||
|
||||
-- handle unfocused
|
||||
function e.on_unfocused()
|
||||
local val = tonumber(e.value)
|
||||
local max = tonumber(args.max)
|
||||
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 = ""
|
||||
end
|
||||
|
||||
ifield.show()
|
||||
end
|
||||
|
||||
-- handle focus (not unfocus), enable, and redraw with show()
|
||||
e.on_focused = ifield.show
|
||||
e.on_enabled = ifield.show
|
||||
e.on_disabled = ifield.show
|
||||
e.redraw = ifield.show
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return number_field
|
||||
105
graphics/elements/form/text_field.lua
Normal file
105
graphics/elements/form/text_field.lua
Normal file
@@ -0,0 +1,105 @@
|
||||
-- Text Value Entry Graphics Element
|
||||
|
||||
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 text_field_args
|
||||
---@field value? string initial value
|
||||
---@field max_len? integer maximum string length
|
||||
---@field censor? string character to replace text with when printing to screen
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
---@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 fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new text entry field
|
||||
---@param args text_field_args
|
||||
---@return graphics_element element, element_id id, function censor_ctl
|
||||
local function text_field(args)
|
||||
args.height = 1
|
||||
args.can_focus = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- set initial value
|
||||
e.value = args.value or ""
|
||||
|
||||
-- make an interactive field manager
|
||||
local ifield = core.new_ifield(e, args.max_len or e.frame.w, args.fg_bg, args.dis_fg_bg)
|
||||
|
||||
ifield.censor(args.censor)
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- only handle if on an increment or decrement arrow
|
||||
if e.enabled and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
if core.events.was_clicked(event.type) then
|
||||
e.take_focus()
|
||||
|
||||
if event.type == MOUSE_CLICK.UP then
|
||||
ifield.move_cursor(event.current.x)
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
|
||||
ifield.select_all()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == KEY_CLICK.CHAR then
|
||||
ifield.try_insert_char(event.name)
|
||||
elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
|
||||
if (event.key == keys.backspace or event.key == keys.delete) then
|
||||
ifield.backspace()
|
||||
elseif event.key == keys.left then
|
||||
ifield.nav_left()
|
||||
elseif event.key == keys.right then
|
||||
ifield.nav_right()
|
||||
elseif event.key == keys.a and event.ctrl then
|
||||
ifield.select_all()
|
||||
elseif event.key == keys.home or event.key == keys.up then
|
||||
ifield.nav_start()
|
||||
elseif event.key == keys["end"] or event.key == keys.down then
|
||||
ifield.nav_end()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val string string to set
|
||||
function e.set_value(val)
|
||||
ifield.set_value(val)
|
||||
end
|
||||
|
||||
-- replace text with pasted text
|
||||
---@param text string string to set
|
||||
function e.handle_paste(text)
|
||||
ifield.set_value(text)
|
||||
end
|
||||
|
||||
-- handle focus, enable, and redraw with show()
|
||||
e.on_focused = ifield.show
|
||||
e.on_unfocused = ifield.show
|
||||
e.on_enabled = ifield.show
|
||||
e.on_disabled = ifield.show
|
||||
e.redraw = ifield.show
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
local elem, id = e.complete()
|
||||
return elem, id, ifield.censor
|
||||
end
|
||||
|
||||
return text_field
|
||||
@@ -25,13 +25,13 @@ local flasher = require("graphics.flasher")
|
||||
---@param args alarm_indicator_light
|
||||
---@return graphics_element element, element_id id
|
||||
local function alarm_indicator_light(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.alight: label is a required field")
|
||||
assert(type(args.c1) == "number", "graphics.elements.indicators.alight: c1 is a required field")
|
||||
assert(type(args.c2) == "number", "graphics.elements.indicators.alight: c2 is a required field")
|
||||
assert(type(args.c3) == "number", "graphics.elements.indicators.alight: c3 is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.c1) == "number", "c1 is a required field")
|
||||
element.assert(type(args.c2) == "number", "c2 is a required field")
|
||||
element.assert(type(args.c3) == "number", "c3 is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.alight: period is a required field if flash is enabled")
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
@@ -51,19 +51,21 @@ local function alarm_indicator_light(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 1
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
if e.value == 2 then
|
||||
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
else
|
||||
if e.value == 3 then
|
||||
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -76,7 +78,7 @@ local function alarm_indicator_light(args)
|
||||
local was_off = e.value ~= 2
|
||||
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if args.flash then
|
||||
if was_off and (new_state == 2) then
|
||||
@@ -87,17 +89,17 @@ local function alarm_indicator_light(args)
|
||||
flasher.stop(flash_callback)
|
||||
|
||||
if new_state == 3 then
|
||||
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
elseif new_state == 2 then
|
||||
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
elseif new_state == 3 then
|
||||
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -105,9 +107,14 @@ local function alarm_indicator_light(args)
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(1)
|
||||
e.window.write(args.label)
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(e.value)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -18,8 +18,8 @@ local element = require("graphics.element")
|
||||
---@param args core_map_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function core_map(args)
|
||||
assert(util.is_int(args.reactor_l), "graphics.elements.indicators.coremap: reactor_l is a required field")
|
||||
assert(util.is_int(args.reactor_w), "graphics.elements.indicators.coremap: reactor_w is a required field")
|
||||
element.assert(util.is_int(args.reactor_l), "reactor_l is a required field")
|
||||
element.assert(util.is_int(args.reactor_w), "reactor_w is a required field")
|
||||
|
||||
-- require max dimensions
|
||||
args.width = 18
|
||||
@@ -31,6 +31,8 @@ local function core_map(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 0
|
||||
|
||||
local alternator = true
|
||||
|
||||
local core_l = args.reactor_l - 2
|
||||
@@ -47,25 +49,25 @@ local function core_map(args)
|
||||
|
||||
-- create coordinate grid and frame
|
||||
local function draw_frame()
|
||||
e.window.setTextColor(colors.white)
|
||||
e.w_set_fgd(colors.white)
|
||||
|
||||
for x = 0, (inner_width - 1) do
|
||||
e.window.setCursorPos(x + start_x, 1)
|
||||
e.window.write(util.sprintf("%X", x))
|
||||
e.w_set_cur(x + start_x, 1)
|
||||
e.w_write(util.sprintf("%X", x))
|
||||
end
|
||||
|
||||
for y = 0, (inner_height - 1) do
|
||||
e.window.setCursorPos(1, y + start_y)
|
||||
e.window.write(util.sprintf("%X", y))
|
||||
e.w_set_cur(1, y + start_y)
|
||||
e.w_write(util.sprintf("%X", y))
|
||||
end
|
||||
|
||||
-- even out bottom edge
|
||||
e.window.setTextColor(e.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(args.parent.get_fg_bg().bkg)
|
||||
e.window.setCursorPos(1, e.frame.h)
|
||||
e.window.write(util.strrep("\x8f", e.frame.w))
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(args.parent.get_fg_bg().bkg)
|
||||
e.w_set_cur(1, e.frame.h)
|
||||
e.w_write(string.rep("\x8f", e.frame.w))
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- draw the core
|
||||
@@ -102,13 +104,13 @@ local function core_map(args)
|
||||
|
||||
-- draw pattern
|
||||
for y = start_y, inner_height + (start_y - 1) do
|
||||
e.window.setCursorPos(start_x, y)
|
||||
e.w_set_cur(start_x, y)
|
||||
for _ = 1, inner_width do
|
||||
if alternator then
|
||||
i = i + 1
|
||||
e.window.blit("\x07", text_c, back_c)
|
||||
e.w_blit("\x07", text_c, back_c)
|
||||
else
|
||||
e.window.blit("\x07", "7", "8")
|
||||
e.w_blit("\x07", "7", "8")
|
||||
end
|
||||
|
||||
alternator = not alternator
|
||||
@@ -157,11 +159,14 @@ local function core_map(args)
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial (one-time except for resize()) frame draw
|
||||
draw_frame()
|
||||
-- redraw both frame and core
|
||||
function e.redraw()
|
||||
draw_frame()
|
||||
draw_core(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.on_update(0)
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -24,25 +24,17 @@ local element = require("graphics.element")
|
||||
---@param args data_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function data(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.data: label is a required field")
|
||||
assert(type(args.format) == "string", "graphics.elements.indicators.data: format is a required field")
|
||||
assert(args.value ~= nil, "graphics.elements.indicators.data: value is a required field")
|
||||
assert(util.is_int(args.width), "graphics.elements.indicators.data: width is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.format) == "string", "format is a required field")
|
||||
element.assert(args.value ~= nil, "value is a required field")
|
||||
element.assert(util.is_int(args.width), "width is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- label color
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_a)
|
||||
end
|
||||
|
||||
-- write label
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write(args.label)
|
||||
e.value = args.value
|
||||
|
||||
local value_color = e.fg_bg.fgd
|
||||
local label_len = string.len(args.label)
|
||||
@@ -60,25 +52,25 @@ local function data(args)
|
||||
e.value = value
|
||||
|
||||
-- clear old data and label
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.write(util.spaces(clear_width))
|
||||
e.w_set_cur(data_start, 1)
|
||||
e.w_write(util.spaces(clear_width))
|
||||
|
||||
-- write data
|
||||
local data_str = util.sprintf(args.format, value)
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.setTextColor(value_color)
|
||||
e.w_set_cur(data_start, 1)
|
||||
e.w_set_fgd(value_color)
|
||||
if args.commas then
|
||||
e.window.write(util.comma_format(data_str))
|
||||
e.w_write(util.comma_format(data_str))
|
||||
else
|
||||
e.window.write(data_str)
|
||||
e.w_write(data_str)
|
||||
end
|
||||
|
||||
-- write label
|
||||
if args.unit ~= nil then
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_b)
|
||||
e.w_set_fgd(args.lu_colors.color_b)
|
||||
end
|
||||
e.window.write(" " .. args.unit)
|
||||
e.w_write(" " .. args.unit)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -93,8 +85,17 @@ local function data(args)
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial value draw
|
||||
e.on_update(args.value)
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(args.label)
|
||||
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -22,16 +22,17 @@ local element = require("graphics.element")
|
||||
---@param args hbar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function hbar(args)
|
||||
-- properties/state
|
||||
local last_num_bars = -1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 0.0
|
||||
|
||||
-- bar width is width - 5 characters for " 100%" if showing percent
|
||||
local bar_width = util.trinary(args.show_percent, e.frame.w - 5, e.frame.w)
|
||||
|
||||
assert(bar_width > 0, "graphics.elements.indicators.hbar: too small for bar")
|
||||
element.assert(bar_width > 0, "too small for bar")
|
||||
|
||||
local last_num_bars = -1
|
||||
|
||||
-- determine bar colors
|
||||
local bar_bkg = e.fg_bg.blit_bkg
|
||||
@@ -87,16 +88,16 @@ local function hbar(args)
|
||||
|
||||
-- draw bar
|
||||
for y = 1, e.frame.h do
|
||||
e.window.setCursorPos(1, y)
|
||||
e.w_set_cur(1, y)
|
||||
-- intentionally swapped fgd/bkg since we use spaces as fill, but they are the opposite
|
||||
e.window.blit(spaces, bkg, fgd)
|
||||
e.w_blit(spaces, bkg, fgd)
|
||||
end
|
||||
end
|
||||
|
||||
-- update percentage
|
||||
if args.show_percent then
|
||||
e.window.setCursorPos(bar_width + 2, math.max(1, math.ceil(e.frame.h / 2)))
|
||||
e.window.write(util.sprintf("%3.0f%%", fraction * 100))
|
||||
e.w_set_cur(bar_width + 2, math.max(1, math.ceil(e.frame.h / 2)))
|
||||
e.w_write(util.sprintf("%3.0f%%", fraction * 100))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -105,20 +106,21 @@ local function hbar(args)
|
||||
function e.recolor(bar_fg_bg)
|
||||
bar_bkg = bar_fg_bg.blit_bkg
|
||||
bar_fgd = bar_fg_bg.blit_fgd
|
||||
|
||||
-- re-draw
|
||||
last_num_bars = 0
|
||||
if type(e.value) == "number" then
|
||||
e.on_update(e.value)
|
||||
end
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- set the percentage value
|
||||
---@param val number 0.0 to 1.0
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initialize to 0
|
||||
e.on_update(0)
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
last_num_bars = -1
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
-- Icon Indicator Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class icon_sym_color
|
||||
@@ -11,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
|
||||
@@ -25,18 +23,18 @@ local element = require("graphics.element")
|
||||
---@param args icon_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function icon(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.icon: label is a required field")
|
||||
assert(type(args.states) == "table", "graphics.elements.indicators.icon: states is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.states) == "table", "states is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4
|
||||
|
||||
-- create new graphics element base object
|
||||
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 = {}
|
||||
for i = 1, #args.states do
|
||||
@@ -44,30 +42,37 @@ local function icon(args)
|
||||
|
||||
table.insert(state_blit_cmds, {
|
||||
text = " " .. sym_color.symbol .. " ",
|
||||
fgd = util.strrep(sym_color.color.blit_fgd, 3),
|
||||
bkg = util.strrep(sym_color.color.blit_bkg, 3)
|
||||
fgd = string.rep(sym_color.color.blit_fgd, 3),
|
||||
bkg = string.rep(sym_color.color.blit_bkg, 3)
|
||||
})
|
||||
end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.window.setCursorPos(5, 1)
|
||||
e.window.write(args.label)
|
||||
|
||||
-- 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.window.setCursorPos(1, 1)
|
||||
e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
|
||||
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
|
||||
|
||||
-- initial icon draw
|
||||
e.on_update(args.value or 1)
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
e.w_set_cur(5, 1)
|
||||
e.w_write(args.label)
|
||||
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -23,33 +23,31 @@ local flasher = require("graphics.flasher")
|
||||
---@param args indicator_led_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.led: label is a required field")
|
||||
assert(type(args.colors) == "table", "graphics.elements.indicators.led: colors is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.colors) == "table", "colors is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.led: period is a required field if flash is enabled")
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
local flash_on = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = false
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
@@ -61,8 +59,8 @@ local function indicator_led(args)
|
||||
flash_on = true
|
||||
flasher.start(flash_callback, args.period)
|
||||
else
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -73,8 +71,8 @@ local function indicator_led(args)
|
||||
flasher.stop(flash_callback)
|
||||
end
|
||||
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
-- on state change
|
||||
@@ -88,13 +86,18 @@ local function indicator_led(args)
|
||||
---@param val boolean indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(false)
|
||||
if string.len(args.label) > 0 then
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(e.value)
|
||||
if string.len(args.label) > 0 then
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
|
||||
@@ -25,25 +25,20 @@ local flasher = require("graphics.flasher")
|
||||
---@param args indicator_led_pair_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led_pair(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.ledpair: label is a required field")
|
||||
assert(type(args.off) == "number", "graphics.elements.indicators.ledpair: off is a required field")
|
||||
assert(type(args.c1) == "number", "graphics.elements.indicators.ledpair: c1 is a required field")
|
||||
assert(type(args.c2) == "number", "graphics.elements.indicators.ledpair: c2 is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.off) == "number", "off is a required field")
|
||||
element.assert(type(args.c1) == "number", "c1 is a required field")
|
||||
element.assert(type(args.c2) == "number", "c2 is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.ledpair: period is a required field if flash is enabled")
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
local flash_on = true
|
||||
|
||||
-- blit translations
|
||||
local co = colors.toBlit(args.off)
|
||||
local c1 = colors.toBlit(args.c1)
|
||||
local c2 = colors.toBlit(args.c2)
|
||||
@@ -51,21 +46,20 @@ local function indicator_led_pair(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- init value for initial check in on_update
|
||||
e.value = 1
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
if e.value == 2 then
|
||||
e.window.blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
elseif e.value == 3 then
|
||||
e.window.blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
@@ -77,7 +71,7 @@ local function indicator_led_pair(args)
|
||||
local was_off = e.value <= 1
|
||||
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if args.flash then
|
||||
if was_off and (new_state > 1) then
|
||||
@@ -86,15 +80,14 @@ local function indicator_led_pair(args)
|
||||
elseif new_state <= 1 then
|
||||
flash_on = false
|
||||
flasher.stop(flash_callback)
|
||||
|
||||
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
elseif new_state == 2 then
|
||||
e.window.blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
elseif new_state == 3 then
|
||||
e.window.blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -102,13 +95,18 @@ local function indicator_led_pair(args)
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(1)
|
||||
if string.len(args.label) > 0 then
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(e.value)
|
||||
if string.len(args.label) > 0 then
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
|
||||
@@ -18,28 +18,24 @@ local element = require("graphics.element")
|
||||
---@param args indicator_led_rgb_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led_rgb(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.ledrgb: label is a required field")
|
||||
assert(type(args.colors) == "table", "graphics.elements.indicators.ledrgb: colors is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.colors) == "table", "colors is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- init value for initial check in on_update
|
||||
e.value = 1
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
if type(args.colors[new_state]) == "number" then
|
||||
e.window.blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,13 +43,18 @@ local function indicator_led_rgb(args)
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(1)
|
||||
if string.len(args.label) > 0 then
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(e.value)
|
||||
if string.len(args.label) > 0 then
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
|
||||
@@ -23,33 +23,31 @@ local flasher = require("graphics.flasher")
|
||||
---@param args indicator_light_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_light(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.light: label is a required field")
|
||||
assert(type(args.colors) == "table", "graphics.elements.indicators.light: colors is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.colors) == "table", "colors is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.light: period is a required field if flash is enabled")
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
local flash_on = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = false
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
e.window.blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
@@ -61,8 +59,8 @@ local function indicator_light(args)
|
||||
flash_on = true
|
||||
flasher.start(flash_callback, args.period)
|
||||
else
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -73,8 +71,8 @@ local function indicator_light(args)
|
||||
flasher.stop(flash_callback)
|
||||
end
|
||||
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
-- on state change
|
||||
@@ -88,10 +86,15 @@ local function indicator_light(args)
|
||||
---@param val boolean indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(false)
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(false)
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -9,7 +9,7 @@ local element = require("graphics.element")
|
||||
---@field format string power format override (lua string format)
|
||||
---@field rate boolean? whether to append /t to the end (power per tick)
|
||||
---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
|
||||
---@field value any default value
|
||||
---@field value number default value
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
@@ -23,26 +23,17 @@ local element = require("graphics.element")
|
||||
---@param args power_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function power(args)
|
||||
assert(args.value ~= nil, "graphics.elements.indicators.power: value is a required field")
|
||||
assert(util.is_int(args.width), "graphics.elements.indicators.power: width is a required field")
|
||||
element.assert(type(args.value) == "number", "value is a required field")
|
||||
element.assert(util.is_int(args.width), "width is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- label color
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_a)
|
||||
end
|
||||
e.value = args.value
|
||||
|
||||
-- write label
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write(args.label)
|
||||
|
||||
local data_start = string.len(args.label) + 2
|
||||
if string.len(args.label) == 0 then data_start = 1 end
|
||||
local data_start = 0
|
||||
|
||||
-- on state change
|
||||
---@param value any new value
|
||||
@@ -52,13 +43,13 @@ local function power(args)
|
||||
local data_str, unit = util.power_format(value, false, args.format)
|
||||
|
||||
-- write data
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.write(util.comma_format(data_str))
|
||||
e.w_set_cur(data_start, 1)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_write(util.comma_format(data_str))
|
||||
|
||||
-- write unit
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_b)
|
||||
e.w_set_fgd(args.lu_colors.color_b)
|
||||
end
|
||||
|
||||
-- append per tick if rate is set
|
||||
@@ -70,15 +61,27 @@ local function power(args)
|
||||
if unit == "FE" then unit = "FE " end
|
||||
end
|
||||
|
||||
e.window.write(" " .. unit)
|
||||
e.w_write(" " .. unit)
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val any new value
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial value draw
|
||||
e.on_update(args.value)
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(args.label)
|
||||
|
||||
data_start = string.len(args.label) + 2
|
||||
if string.len(args.label) == 0 then data_start = 1 end
|
||||
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ local element = require("graphics.element")
|
||||
---@field format string data format (lua string format)
|
||||
---@field commas? boolean whether to use commas if a number is given (default to false)
|
||||
---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
|
||||
---@field value any default value
|
||||
---@field value? radiation_reading default value
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
@@ -24,24 +24,16 @@ local element = require("graphics.element")
|
||||
---@param args rad_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function rad(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.rad: label is a required field")
|
||||
assert(type(args.format) == "string", "graphics.elements.indicators.rad: format is a required field")
|
||||
assert(util.is_int(args.width), "graphics.elements.indicators.rad: width is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.format) == "string", "format is a required field")
|
||||
element.assert(util.is_int(args.width), "width is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- label color
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_a)
|
||||
end
|
||||
|
||||
-- write label
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write(args.label)
|
||||
e.value = args.value or types.new_zero_radiation_reading()
|
||||
|
||||
local label_len = string.len(args.label)
|
||||
local data_start = 1
|
||||
@@ -58,32 +50,41 @@ local function rad(args)
|
||||
e.value = value.radiation
|
||||
|
||||
-- clear old data and label
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.write(util.spaces(clear_width))
|
||||
e.w_set_cur(data_start, 1)
|
||||
e.w_write(util.spaces(clear_width))
|
||||
|
||||
-- write data
|
||||
local data_str = util.sprintf(args.format, e.value)
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.w_set_cur(data_start, 1)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
if args.commas then
|
||||
e.window.write(util.comma_format(data_str))
|
||||
e.w_write(util.comma_format(data_str))
|
||||
else
|
||||
e.window.write(data_str)
|
||||
e.w_write(data_str)
|
||||
end
|
||||
|
||||
-- write unit
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_b)
|
||||
e.w_set_fgd(args.lu_colors.color_b)
|
||||
end
|
||||
e.window.write(" " .. value.unit)
|
||||
e.w_write(" " .. value.unit)
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val any new value
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial value draw
|
||||
e.on_update(types.new_zero_radiation_reading())
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(args.label)
|
||||
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
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
|
||||
@@ -25,16 +25,12 @@ local element = require("graphics.element")
|
||||
---@param args state_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function state_indicator(args)
|
||||
assert(type(args.states) == "table", "graphics.elements.indicators.state: states is a required field")
|
||||
element.assert(type(args.states) == "table", "states is a required field")
|
||||
|
||||
-- determine height
|
||||
if util.is_int(args.height) then
|
||||
assert(args.height % 2 == 1, "graphics.elements.indicators.state: height should be an odd number")
|
||||
else
|
||||
args.height = 1
|
||||
end
|
||||
element.assert(args.height % 2 == 1, "height should be an odd number")
|
||||
else args.height = 1 end
|
||||
|
||||
-- initial guess at width
|
||||
args.width = args.min_width or 1
|
||||
|
||||
-- state blit strings
|
||||
@@ -42,7 +38,6 @@ local function state_indicator(args)
|
||||
for i = 1, #args.states do
|
||||
local state_def = args.states[i] ---@type state_text_color
|
||||
|
||||
-- re-determine width
|
||||
if string.len(state_def.text) > args.width then
|
||||
args.width = string.len(state_def.text)
|
||||
end
|
||||
@@ -51,21 +46,28 @@ local function state_indicator(args)
|
||||
|
||||
table.insert(state_blit_cmds, {
|
||||
text = text,
|
||||
fgd = util.strrep(state_def.color.blit_fgd, string.len(text)),
|
||||
bkg = util.strrep(state_def.color.blit_bkg, string.len(text))
|
||||
fgd = string.rep(state_def.color.blit_fgd, string.len(text)),
|
||||
bkg = string.rep(state_def.color.blit_bkg, string.len(text))
|
||||
})
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = args.value or 1
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
local blit_cmd = state_blit_cmds[e.value]
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
local blit_cmd = state_blit_cmds[new_state]
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
@@ -73,7 +75,7 @@ local function state_indicator(args)
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial draw
|
||||
e.on_update(args.value or 1)
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -25,47 +25,41 @@ local flasher = require("graphics.flasher")
|
||||
---@param args tristate_indicator_light_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function tristate_indicator_light(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.trilight: label is a required field")
|
||||
assert(type(args.c1) == "number", "graphics.elements.indicators.trilight: c1 is a required field")
|
||||
assert(type(args.c2) == "number", "graphics.elements.indicators.trilight: c2 is a required field")
|
||||
assert(type(args.c3) == "number", "graphics.elements.indicators.trilight: c3 is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.c1) == "number", "c1 is a required field")
|
||||
element.assert(type(args.c2) == "number", "c2 is a required field")
|
||||
element.assert(type(args.c3) == "number", "c3 is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.trilight: period is a required field if flash is enabled")
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
local flash_on = true
|
||||
|
||||
-- blit translations
|
||||
local c1 = colors.toBlit(args.c1)
|
||||
local c2 = colors.toBlit(args.c2)
|
||||
local c3 = colors.toBlit(args.c3)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- init value for initial check in on_update
|
||||
e.value = 1
|
||||
|
||||
local flash_on = true
|
||||
|
||||
local c1 = colors.toBlit(args.c1)
|
||||
local c2 = colors.toBlit(args.c2)
|
||||
local c3 = colors.toBlit(args.c3)
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
if e.value == 2 then
|
||||
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
elseif e.value == 3 then
|
||||
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
@@ -77,7 +71,7 @@ local function tristate_indicator_light(args)
|
||||
local was_off = e.value <= 1
|
||||
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if args.flash then
|
||||
if was_off and (new_state > 1) then
|
||||
@@ -87,14 +81,14 @@ local function tristate_indicator_light(args)
|
||||
flash_on = false
|
||||
flasher.stop(flash_callback)
|
||||
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
elseif new_state == 2 then
|
||||
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
elseif new_state == 3 then
|
||||
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -102,9 +96,14 @@ local function tristate_indicator_light(args)
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(1)
|
||||
e.window.write(args.label)
|
||||
-- draw light and label
|
||||
function e.redraw()
|
||||
e.on_update(1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -20,18 +20,18 @@ local element = require("graphics.element")
|
||||
---@param args vbar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function vbar(args)
|
||||
-- properties/state
|
||||
local last_num_bars = -1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- blit strings
|
||||
local fgd = util.strrep(e.fg_bg.blit_fgd, e.frame.w)
|
||||
local bkg = util.strrep(e.fg_bg.blit_bkg, e.frame.w)
|
||||
e.value = 0.0
|
||||
|
||||
local last_num_bars = -1
|
||||
|
||||
local fgd = string.rep(e.fg_bg.blit_fgd, e.frame.w)
|
||||
local bkg = string.rep(e.fg_bg.blit_bkg, e.frame.w)
|
||||
local spaces = util.spaces(e.frame.w)
|
||||
local one_third = util.strrep("\x8f", e.frame.w)
|
||||
local two_thirds = util.strrep("\x83", e.frame.w)
|
||||
local one_third = string.rep("\x8f", e.frame.w)
|
||||
local two_thirds = string.rep("\x83", e.frame.w)
|
||||
|
||||
-- handle data changes
|
||||
---@param fraction number 0.0 to 1.0
|
||||
@@ -52,54 +52,55 @@ local function vbar(args)
|
||||
if num_bars ~= last_num_bars then
|
||||
last_num_bars = num_bars
|
||||
|
||||
-- start bottom up
|
||||
local y = e.frame.h
|
||||
|
||||
-- start at base of vertical bar
|
||||
e.window.setCursorPos(1, y)
|
||||
e.w_set_cur(1, y)
|
||||
|
||||
-- fill percentage
|
||||
for _ = 1, num_bars / 3 do
|
||||
e.window.blit(spaces, bkg, fgd)
|
||||
e.w_blit(spaces, bkg, fgd)
|
||||
y = y - 1
|
||||
e.window.setCursorPos(1, y)
|
||||
e.w_set_cur(1, y)
|
||||
end
|
||||
|
||||
-- add fractional bar if needed
|
||||
if num_bars % 3 == 1 then
|
||||
e.window.blit(one_third, bkg, fgd)
|
||||
e.w_blit(one_third, bkg, fgd)
|
||||
y = y - 1
|
||||
elseif num_bars % 3 == 2 then
|
||||
e.window.blit(two_thirds, bkg, fgd)
|
||||
e.w_blit(two_thirds, bkg, fgd)
|
||||
y = y - 1
|
||||
end
|
||||
|
||||
-- fill the rest blank
|
||||
while y > 0 do
|
||||
e.window.setCursorPos(1, y)
|
||||
e.window.blit(spaces, fgd, bkg)
|
||||
e.w_set_cur(1, y)
|
||||
e.w_blit(spaces, fgd, bkg)
|
||||
y = y - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- change bar color
|
||||
---@param fg_bg cpair new bar colors
|
||||
function e.recolor(fg_bg)
|
||||
fgd = util.strrep(fg_bg.blit_fgd, e.frame.w)
|
||||
bkg = util.strrep(fg_bg.blit_bkg, e.frame.w)
|
||||
|
||||
-- re-draw
|
||||
last_num_bars = 0
|
||||
if type(e.value) == "number" then
|
||||
e.on_update(e.value)
|
||||
end
|
||||
end
|
||||
|
||||
-- set the percentage value
|
||||
---@param val number 0.0 to 1.0
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
last_num_bars = -1
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- change bar color
|
||||
---@param fg_bg cpair new bar colors
|
||||
function e.recolor(fg_bg)
|
||||
fgd = string.rep(fg_bg.blit_fgd, e.frame.w)
|
||||
bkg = string.rep(fg_bg.blit_bkg, e.frame.w)
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ local tcd = require("scada-common.tcd")
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class listbox_args
|
||||
---@field scroll_height integer height of internal scrolling container (must fit all elements vertically tiled)
|
||||
@@ -33,6 +34,8 @@ local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
---@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)
|
||||
|
||||
@@ -63,34 +66,34 @@ local function listbox(args)
|
||||
|
||||
-- draw up/down arrows
|
||||
if pressed_arrow == 1 then
|
||||
e.window.setTextColor(active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(active_fg_bg.bkg)
|
||||
e.window.setCursorPos(e.frame.w, 1)
|
||||
e.window.write("\x1e")
|
||||
e.window.setTextColor(nav_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(nav_fg_bg.bkg)
|
||||
e.window.setCursorPos(e.frame.w, e.frame.h)
|
||||
e.window.write("\x1f")
|
||||
e.w_set_fgd(active_fg_bg.fgd)
|
||||
e.w_set_bkg(active_fg_bg.bkg)
|
||||
e.w_set_cur(e.frame.w, 1)
|
||||
e.w_write("\x1e")
|
||||
e.w_set_fgd(nav_fg_bg.fgd)
|
||||
e.w_set_bkg(nav_fg_bg.bkg)
|
||||
e.w_set_cur(e.frame.w, e.frame.h)
|
||||
e.w_write("\x1f")
|
||||
elseif pressed_arrow == -1 then
|
||||
e.window.setTextColor(nav_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(nav_fg_bg.bkg)
|
||||
e.window.setCursorPos(e.frame.w, 1)
|
||||
e.window.write("\x1e")
|
||||
e.window.setTextColor(active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(active_fg_bg.bkg)
|
||||
e.window.setCursorPos(e.frame.w, e.frame.h)
|
||||
e.window.write("\x1f")
|
||||
e.w_set_fgd(nav_fg_bg.fgd)
|
||||
e.w_set_bkg(nav_fg_bg.bkg)
|
||||
e.w_set_cur(e.frame.w, 1)
|
||||
e.w_write("\x1e")
|
||||
e.w_set_fgd(active_fg_bg.fgd)
|
||||
e.w_set_bkg(active_fg_bg.bkg)
|
||||
e.w_set_cur(e.frame.w, e.frame.h)
|
||||
e.w_write("\x1f")
|
||||
else
|
||||
e.window.setTextColor(nav_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(nav_fg_bg.bkg)
|
||||
e.window.setCursorPos(e.frame.w, 1)
|
||||
e.window.write("\x1e")
|
||||
e.window.setCursorPos(e.frame.w, e.frame.h)
|
||||
e.window.write("\x1f")
|
||||
e.w_set_fgd(nav_fg_bg.fgd)
|
||||
e.w_set_bkg(nav_fg_bg.bkg)
|
||||
e.w_set_cur(e.frame.w, 1)
|
||||
e.w_write("\x1e")
|
||||
e.w_set_cur(e.frame.w, e.frame.h)
|
||||
e.w_write("\x1f")
|
||||
end
|
||||
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- render the scroll bar and re-cacluate height & bounds
|
||||
@@ -115,23 +118,23 @@ local function listbox(args)
|
||||
for i = 2, e.frame.h - 1 do
|
||||
if (i >= offset and i < (bar_height + offset)) and (bar_height ~= max_bar_height) then
|
||||
if args.nav_fg_bg ~= nil then
|
||||
e.window.setBackgroundColor(args.nav_fg_bg.fgd)
|
||||
e.w_set_bkg(args.nav_fg_bg.fgd)
|
||||
else
|
||||
e.window.setBackgroundColor(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.fgd)
|
||||
end
|
||||
else
|
||||
if args.nav_fg_bg ~= nil then
|
||||
e.window.setBackgroundColor(args.nav_fg_bg.bkg)
|
||||
e.w_set_bkg(args.nav_fg_bg.bkg)
|
||||
else
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
end
|
||||
|
||||
e.window.setCursorPos(e.frame.w, i)
|
||||
e.window.write(" ")
|
||||
e.w_set_cur(e.frame.w, i)
|
||||
if e.is_focused() then e.w_write("\x7f") else e.w_write(" ") end
|
||||
end
|
||||
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- update item y positions and move elements
|
||||
@@ -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,30 +225,60 @@ 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)
|
||||
if e.enabled then
|
||||
if event.type == CLICK_TYPE.TAP then
|
||||
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 == CLICK_TYPE.DOWN then
|
||||
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
|
||||
@@ -250,10 +286,10 @@ local function listbox(args)
|
||||
mouse_last_y = event.current.y
|
||||
end
|
||||
end
|
||||
elseif event.type == CLICK_TYPE.UP then
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
holding_bar = false
|
||||
draw_arrows(0)
|
||||
elseif event.type == CLICK_TYPE.DRAG then
|
||||
elseif event.type == MOUSE_CLICK.DRAG then
|
||||
if holding_bar then
|
||||
-- if mouse is within vertical frame, including the grip point
|
||||
if event.current.y > (1 + bar_grip_pos) and event.current.y <= ((e.frame.h - bar_height) + bar_grip_pos) then
|
||||
@@ -266,16 +302,40 @@ local function listbox(args)
|
||||
mouse_last_y = event.current.y
|
||||
end
|
||||
end
|
||||
elseif event.type == CLICK_TYPE.SCROLL_DOWN then
|
||||
elseif event.type == MOUSE_CLICK.SCROLL_DOWN then
|
||||
scroll_down()
|
||||
elseif event.type == CLICK_TYPE.SCROLL_UP then
|
||||
elseif event.type == MOUSE_CLICK.SCROLL_UP then
|
||||
scroll_up()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
draw_arrows(0)
|
||||
draw_bar()
|
||||
-- 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)
|
||||
draw_bar()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -19,23 +19,30 @@ local element = require("graphics.element")
|
||||
---@param args multipane_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function multipane(args)
|
||||
assert(type(args.panes) == "table", "graphics.elements.multipane: panes is a required field")
|
||||
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
|
||||
|
||||
-- show the selected pane
|
||||
function e.redraw()
|
||||
for i = 1, #args.panes do args.panes[i].hide() end
|
||||
args.panes[e.value].show()
|
||||
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
|
||||
|
||||
for i = 1, #args.panes do args.panes[i].hide() end
|
||||
args.panes[value].show()
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
e.set_value(1)
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
@@ -14,16 +14,21 @@ local element = require("graphics.element")
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
---@class _pipe_map_entry
|
||||
---@field atr boolean align top right (or bottom left for false)
|
||||
---@field thin boolean thin pipe or not
|
||||
---@field fg string foreground blit
|
||||
---@field bg string background blit
|
||||
|
||||
-- new pipe network
|
||||
---@param args pipenet_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function pipenet(args)
|
||||
assert(type(args.pipes) == "table", "graphics.elements.indicators.pipenet: pipes is a required field")
|
||||
element.assert(type(args.pipes) == "table", "pipes is a required field")
|
||||
|
||||
args.width = 0
|
||||
args.height = 0
|
||||
|
||||
-- determine width/height
|
||||
for i = 1, #args.pipes do
|
||||
local pipe = args.pipes[i] ---@type pipe
|
||||
|
||||
@@ -44,104 +49,282 @@ local function pipenet(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- draw all pipes
|
||||
-- determine if there are any thin pipes involved
|
||||
local any_thin = false
|
||||
for p = 1, #args.pipes do
|
||||
local pipe = args.pipes[p] ---@type pipe
|
||||
any_thin = args.pipes[p].thin
|
||||
if any_thin then break end
|
||||
end
|
||||
|
||||
local x = 1 + pipe.x1
|
||||
local y = 1 + pipe.y1
|
||||
-- draw all pipes by drawing out lines
|
||||
local function vector_draw()
|
||||
for p = 1, #args.pipes do
|
||||
local pipe = args.pipes[p] ---@type pipe
|
||||
|
||||
local x_step = util.trinary(pipe.x1 >= pipe.x2, -1, 1)
|
||||
local y_step = util.trinary(pipe.y1 >= pipe.y2, -1, 1)
|
||||
local x = 1 + pipe.x1
|
||||
local y = 1 + pipe.y1
|
||||
|
||||
e.window.setCursorPos(x, y)
|
||||
local x_step = util.trinary(pipe.x1 >= pipe.x2, -1, 1)
|
||||
local y_step = util.trinary(pipe.y1 >= pipe.y2, -1, 1)
|
||||
|
||||
local c = core.cpair(pipe.color, e.fg_bg.bkg)
|
||||
|
||||
if pipe.align_tr then
|
||||
-- cross width then height
|
||||
for i = 1, pipe.w do
|
||||
if pipe.thin then
|
||||
if i == pipe.w then
|
||||
-- corner
|
||||
if y_step > 0 then
|
||||
e.window.blit("\x93", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit("\x8e", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
if i == pipe.w and y_step > 0 then
|
||||
-- corner
|
||||
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit("\x8f", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
x = x + x_step
|
||||
e.window.setCursorPos(x, y)
|
||||
if pipe.thin then
|
||||
x_step = util.trinary(pipe.x1 == pipe.x2, 0, x_step)
|
||||
y_step = util.trinary(pipe.y1 == pipe.y2, 0, y_step)
|
||||
end
|
||||
|
||||
-- back up one
|
||||
x = x - x_step
|
||||
e.w_set_cur(x, y)
|
||||
|
||||
for _ = 1, pipe.h - 1 do
|
||||
y = y + y_step
|
||||
e.window.setCursorPos(x, y)
|
||||
local c = core.cpair(pipe.color, e.fg_bg.bkg)
|
||||
|
||||
if pipe.thin then
|
||||
e.window.blit("\x95", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
if pipe.align_tr then
|
||||
-- cross width then height
|
||||
for i = 1, pipe.w do
|
||||
if pipe.thin then
|
||||
if i == pipe.w then
|
||||
-- corner
|
||||
if y_step > 0 then
|
||||
e.w_blit("\x93", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.w_blit("\x8e", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
if i == pipe.w and y_step > 0 then
|
||||
-- corner
|
||||
e.w_blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.w_blit("\x8f", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
x = x + x_step
|
||||
e.w_set_cur(x, y)
|
||||
end
|
||||
|
||||
-- back up one
|
||||
x = x - x_step
|
||||
|
||||
for _ = 1, pipe.h - 1 do
|
||||
y = y + y_step
|
||||
e.w_set_cur(x, y)
|
||||
|
||||
if pipe.thin then
|
||||
e.w_blit("\x95", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.w_blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- cross height then width
|
||||
for i = 1, pipe.h do
|
||||
if pipe.thin then
|
||||
if i == pipe.h then
|
||||
-- corner
|
||||
if y_step < 0 then
|
||||
e.w_blit("\x97", c.blit_bkg, c.blit_fgd)
|
||||
elseif y_step > 0 then
|
||||
e.w_blit("\x8d", c.blit_fgd, c.blit_bkg)
|
||||
else
|
||||
e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.w_blit("\x95", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
if i == pipe.h and y_step < 0 then
|
||||
-- corner
|
||||
e.w_blit("\x83", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.w_blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
end
|
||||
|
||||
y = y + y_step
|
||||
e.w_set_cur(x, y)
|
||||
end
|
||||
|
||||
-- back up one
|
||||
y = y - y_step
|
||||
|
||||
for _ = 1, pipe.w - 1 do
|
||||
x = x + x_step
|
||||
e.w_set_cur(x, y)
|
||||
|
||||
if pipe.thin then
|
||||
e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
else
|
||||
e.w_blit("\x83", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- draw a particular map cell
|
||||
---@param map table 2D cell map
|
||||
---@param x integer x coord
|
||||
---@param y integer y coord
|
||||
local function draw_map_cell(map, x, y)
|
||||
local entry = map[x][y] ---@type _pipe_map_entry already confirmed not false
|
||||
local char
|
||||
local invert = false
|
||||
|
||||
local function check(cx, cy)
|
||||
return (map[cx] ~= nil) and (map[cx][cy] ~= nil) and (map[cx][cy] ~= false) and (map[cx][cy].fg == entry.fg)
|
||||
end
|
||||
|
||||
if entry.thin then
|
||||
if check(x - 1, y) then -- if left
|
||||
if check(x, y - 1) then -- if above
|
||||
if check(x + 1, y) then -- if right
|
||||
if check(x, y + 1) then -- if below
|
||||
char = util.trinary(entry.atr, "\x91", "\x9d")
|
||||
invert = entry.atr
|
||||
else -- not below
|
||||
char = util.trinary(entry.atr, "\x8e", "\x8d")
|
||||
end
|
||||
else -- not right
|
||||
if check(x, y + 1) then -- if below
|
||||
char = util.trinary(entry.atr, "\x91", "\x95")
|
||||
invert = entry.atr
|
||||
else -- not below
|
||||
char = util.trinary(entry.atr, "\x8e", "\x85")
|
||||
end
|
||||
end
|
||||
elseif check(x, y + 1) then-- not above, if below
|
||||
if check(x + 1, y) then -- if right
|
||||
char = util.trinary(entry.atr, "\x93", "\x9c")
|
||||
invert = entry.atr
|
||||
else -- not right
|
||||
char = util.trinary(entry.atr, "\x93", "\x94")
|
||||
invert = entry.atr
|
||||
end
|
||||
else -- not above, not below
|
||||
char = "\x8c"
|
||||
end
|
||||
elseif check(x + 1, y) then -- not left, if right
|
||||
if check(x, y - 1) then -- if above
|
||||
if check(x, y + 1) then -- if below
|
||||
char = util.trinary(entry.atr, "\x95", "\x9d")
|
||||
invert = entry.atr
|
||||
else -- not below
|
||||
char = util.trinary(entry.atr, "\x8a", "\x8d")
|
||||
end
|
||||
else -- not above
|
||||
if check(x, y + 1) then -- if below
|
||||
char = util.trinary(entry.atr, "\x97", "\x9c")
|
||||
invert = entry.atr
|
||||
else -- not below
|
||||
char = "\x8c"
|
||||
end
|
||||
end
|
||||
else -- not left, not right
|
||||
char = "\x95"
|
||||
invert = entry.atr
|
||||
end
|
||||
else
|
||||
-- cross height then width
|
||||
for i = 1, pipe.h do
|
||||
if pipe.thin then
|
||||
if i == pipe.h then
|
||||
-- corner
|
||||
if y_step < 0 then
|
||||
e.window.blit("\x97", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit("\x8d", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.window.blit("\x95", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
if i == pipe.h and y_step < 0 then
|
||||
-- corner
|
||||
e.window.blit("\x83", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
if check(x, y - 1) then -- above
|
||||
-- not below and (if left or right)
|
||||
if (not check(x, y + 1)) and (check(x - 1, y) or check(x + 1, y)) then
|
||||
char = util.trinary(entry.atr, "\x8f", " ")
|
||||
invert = not entry.atr
|
||||
else -- not below w/ sides only
|
||||
char = " "
|
||||
invert = true
|
||||
end
|
||||
elseif check(x, y + 1) then -- not above, if below
|
||||
-- if left or right
|
||||
if (check(x - 1, y) or check(x + 1, y)) then
|
||||
char = "\x83"
|
||||
invert = true
|
||||
else -- not left or right
|
||||
char = " "
|
||||
invert = true
|
||||
end
|
||||
else -- not above, not below
|
||||
char = util.trinary(entry.atr, "\x8f", "\x83")
|
||||
invert = not entry.atr
|
||||
end
|
||||
end
|
||||
|
||||
e.w_set_cur(x, y)
|
||||
|
||||
if invert then
|
||||
e.w_blit(char, entry.bg, entry.fg)
|
||||
else
|
||||
e.w_blit(char, entry.fg, entry.bg)
|
||||
end
|
||||
end
|
||||
|
||||
-- draw all pipes by assembling and marking up a 2D map<br>
|
||||
-- this is an easy way to check adjacent blocks, which is required to properly draw thin pipes
|
||||
local function map_draw()
|
||||
local map = {}
|
||||
|
||||
for x = 1, args.width do
|
||||
table.insert(map, {})
|
||||
for _ = 1, args.height do table.insert(map[x], false) end
|
||||
end
|
||||
|
||||
-- build map
|
||||
for p = 1, #args.pipes do
|
||||
local pipe = args.pipes[p] ---@type pipe
|
||||
|
||||
local x = 1 + pipe.x1
|
||||
local y = 1 + pipe.y1
|
||||
|
||||
local x_step = util.trinary(pipe.x1 >= pipe.x2, -1, 1)
|
||||
local y_step = util.trinary(pipe.y1 >= pipe.y2, -1, 1)
|
||||
|
||||
local entry = { atr = pipe.align_tr, thin = pipe.thin, fg = colors.toBlit(pipe.color), bg = e.fg_bg.blit_bkg }
|
||||
|
||||
if pipe.align_tr then
|
||||
-- cross width then height
|
||||
for _ = 1, pipe.w do
|
||||
map[x][y] = entry
|
||||
x = x + x_step
|
||||
end
|
||||
|
||||
y = y + y_step
|
||||
e.window.setCursorPos(x, y)
|
||||
end
|
||||
x = x - x_step -- back up one
|
||||
|
||||
-- back up one
|
||||
y = y - y_step
|
||||
for _ = 1, pipe.h do
|
||||
map[x][y] = entry
|
||||
y = y + y_step
|
||||
end
|
||||
else
|
||||
-- cross height then width
|
||||
for _ = 1, pipe.h do
|
||||
map[x][y] = entry
|
||||
y = y + y_step
|
||||
end
|
||||
|
||||
for _ = 1, pipe.w - 1 do
|
||||
x = x + x_step
|
||||
e.window.setCursorPos(x, y)
|
||||
y = y - y_step -- back up one
|
||||
|
||||
if pipe.thin then
|
||||
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
else
|
||||
e.window.blit("\x83", c.blit_bkg, c.blit_fgd)
|
||||
for _ = 1, pipe.w do
|
||||
map[x][y] = entry
|
||||
x = x + x_step
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- render
|
||||
for x = 1, args.width do
|
||||
for y = 1, args.height do
|
||||
if map[x][y] ~= false then draw_map_cell(map, x, y) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
if any_thin then map_draw() else vector_draw() end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ local element = require("graphics.element")
|
||||
---@param args rectangle_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function rectangle(args)
|
||||
assert(args.border ~= nil or args.thin ~= true, "graphics.elements.rectangle: thin requires border to be provided")
|
||||
element.assert(args.border ~= nil or args.thin ~= true, "thin requires border to be provided")
|
||||
|
||||
-- if thin, then width will always need to be 1
|
||||
if args.thin == true then
|
||||
@@ -56,7 +56,7 @@ local function rectangle(args)
|
||||
-- draw bordered box if requested
|
||||
-- element constructor will have drawn basic colored rectangle regardless
|
||||
if args.border ~= nil then
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
local border_width = offset_x
|
||||
local border_height = offset_y
|
||||
@@ -65,50 +65,50 @@ local function rectangle(args)
|
||||
local inner_width = e.frame.w - width_x2
|
||||
|
||||
-- check dimensions
|
||||
assert(width_x2 <= e.frame.w, "graphics.elements.rectangle: border too thick for width")
|
||||
assert(width_x2 <= e.frame.h, "graphics.elements.rectangle: border too thick for height")
|
||||
element.assert(width_x2 <= e.frame.w, "border too thick for width")
|
||||
element.assert(width_x2 <= e.frame.h, "border too thick for height")
|
||||
|
||||
-- form the basic line strings and top/bottom blit strings
|
||||
local spaces = util.spaces(e.frame.w)
|
||||
local blit_fg = util.strrep(e.fg_bg.blit_fgd, e.frame.w)
|
||||
local blit_fg = string.rep(e.fg_bg.blit_fgd, e.frame.w)
|
||||
local blit_fg_sides = blit_fg
|
||||
local blit_bg_sides = ""
|
||||
local blit_bg_top_bot = util.strrep(border_blit, e.frame.w)
|
||||
local blit_bg_top_bot = string.rep(border_blit, e.frame.w)
|
||||
|
||||
-- partial bars
|
||||
local p_a, p_b, p_s
|
||||
if args.thin == true then
|
||||
if args.even_inner == true then
|
||||
p_a = "\x9c" .. util.strrep("\x8c", inner_width) .. "\x93"
|
||||
p_b = "\x8d" .. util.strrep("\x8c", inner_width) .. "\x8e"
|
||||
p_a = "\x9c" .. string.rep("\x8c", inner_width) .. "\x93"
|
||||
p_b = "\x8d" .. string.rep("\x8c", inner_width) .. "\x8e"
|
||||
else
|
||||
p_a = "\x97" .. util.strrep("\x83", inner_width) .. "\x94"
|
||||
p_b = "\x8a" .. util.strrep("\x8f", inner_width) .. "\x85"
|
||||
p_a = "\x97" .. string.rep("\x83", inner_width) .. "\x94"
|
||||
p_b = "\x8a" .. string.rep("\x8f", inner_width) .. "\x85"
|
||||
end
|
||||
|
||||
p_s = "\x95" .. util.spaces(inner_width) .. "\x95"
|
||||
else
|
||||
if args.even_inner == true then
|
||||
p_a = util.strrep("\x83", inner_width + width_x2)
|
||||
p_b = util.strrep("\x8f", inner_width + width_x2)
|
||||
p_a = string.rep("\x83", inner_width + width_x2)
|
||||
p_b = string.rep("\x8f", inner_width + width_x2)
|
||||
else
|
||||
p_a = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width)
|
||||
p_b = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width)
|
||||
p_a = util.spaces(border_width) .. string.rep("\x8f", inner_width) .. util.spaces(border_width)
|
||||
p_b = util.spaces(border_width) .. string.rep("\x83", inner_width) .. util.spaces(border_width)
|
||||
end
|
||||
|
||||
p_s = spaces
|
||||
end
|
||||
|
||||
local p_inv_fg = util.strrep(border_blit, border_width) .. util.strrep(e.fg_bg.blit_bkg, inner_width) ..
|
||||
util.strrep(border_blit, border_width)
|
||||
local p_inv_bg = util.strrep(e.fg_bg.blit_bkg, border_width) .. util.strrep(border_blit, inner_width) ..
|
||||
util.strrep(e.fg_bg.blit_bkg, border_width)
|
||||
local p_inv_fg = string.rep(border_blit, border_width) .. string.rep(e.fg_bg.blit_bkg, inner_width) ..
|
||||
string.rep(border_blit, border_width)
|
||||
local p_inv_bg = string.rep(e.fg_bg.blit_bkg, border_width) .. string.rep(border_blit, inner_width) ..
|
||||
string.rep(e.fg_bg.blit_bkg, border_width)
|
||||
|
||||
if args.thin == true then
|
||||
p_inv_fg = e.fg_bg.blit_bkg .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. util.strrep(border_blit, border_width)
|
||||
p_inv_bg = border_blit .. util.strrep(border_blit, inner_width) .. util.strrep(e.fg_bg.blit_bkg, border_width)
|
||||
p_inv_fg = e.fg_bg.blit_bkg .. string.rep(e.fg_bg.blit_bkg, inner_width) .. string.rep(border_blit, border_width)
|
||||
p_inv_bg = border_blit .. string.rep(border_blit, inner_width) .. string.rep(e.fg_bg.blit_bkg, border_width)
|
||||
|
||||
blit_fg_sides = border_blit .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. e.fg_bg.blit_bkg
|
||||
blit_fg_sides = border_blit .. string.rep(e.fg_bg.blit_bkg, inner_width) .. e.fg_bg.blit_bkg
|
||||
end
|
||||
|
||||
-- form the body blit strings (sides are border, inside is normal)
|
||||
@@ -126,64 +126,69 @@ local function rectangle(args)
|
||||
end
|
||||
|
||||
-- draw rectangle with borders
|
||||
for y = 1, e.frame.h do
|
||||
e.window.setCursorPos(1, y)
|
||||
-- top border
|
||||
if y <= border_height then
|
||||
-- partial pixel fill
|
||||
if args.border.even and y == border_height then
|
||||
if args.thin == true then
|
||||
e.window.blit(p_a, p_inv_bg, p_inv_fg)
|
||||
else
|
||||
local _fg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), p_inv_bg)
|
||||
local _bg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
|
||||
function e.redraw()
|
||||
for y = 1, e.frame.h do
|
||||
e.w_set_cur(1, y)
|
||||
-- top border
|
||||
if y <= border_height then
|
||||
-- partial pixel fill
|
||||
if args.border.even and y == border_height then
|
||||
if args.thin == true then
|
||||
e.w_blit(p_a, p_inv_bg, p_inv_fg)
|
||||
else
|
||||
local _fg = util.trinary(args.even_inner == true, string.rep(e.fg_bg.blit_bkg, e.frame.w), p_inv_bg)
|
||||
local _bg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
|
||||
|
||||
if width_x2 % 3 == 1 then
|
||||
e.window.blit(p_b, _fg, _bg)
|
||||
elseif width_x2 % 3 == 2 then
|
||||
e.window.blit(p_a, _fg, _bg)
|
||||
else
|
||||
-- skip line
|
||||
e.window.blit(spaces, blit_fg, blit_bg_sides)
|
||||
end
|
||||
end
|
||||
else
|
||||
e.window.blit(spaces, blit_fg, blit_bg_top_bot)
|
||||
end
|
||||
-- bottom border
|
||||
elseif y > (e.frame.h - border_width) then
|
||||
-- partial pixel fill
|
||||
if args.border.even and y == ((e.frame.h - border_width) + 1) then
|
||||
if args.thin == true then
|
||||
if args.even_inner == true then
|
||||
e.window.blit(p_b, blit_bg_top_bot, util.strrep(e.fg_bg.blit_bkg, e.frame.w))
|
||||
else
|
||||
e.window.blit(p_b, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
|
||||
if width_x2 % 3 == 1 then
|
||||
e.w_blit(p_b, _fg, _bg)
|
||||
elseif width_x2 % 3 == 2 then
|
||||
e.w_blit(p_a, _fg, _bg)
|
||||
else
|
||||
-- skip line
|
||||
e.w_blit(spaces, blit_fg, blit_bg_sides)
|
||||
end
|
||||
end
|
||||
else
|
||||
local _fg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
|
||||
local _bg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
|
||||
|
||||
if width_x2 % 3 == 1 then
|
||||
e.window.blit(p_a, _fg, _bg)
|
||||
elseif width_x2 % 3 == 2 then
|
||||
e.window.blit(p_b, _fg, _bg)
|
||||
e.w_blit(spaces, blit_fg, blit_bg_top_bot)
|
||||
end
|
||||
-- bottom border
|
||||
elseif y > (e.frame.h - border_width) then
|
||||
-- partial pixel fill
|
||||
if args.border.even and y == ((e.frame.h - border_width) + 1) then
|
||||
if args.thin == true then
|
||||
if args.even_inner == true then
|
||||
e.w_blit(p_b, blit_bg_top_bot, string.rep(e.fg_bg.blit_bkg, e.frame.w))
|
||||
else
|
||||
e.w_blit(p_b, string.rep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
|
||||
end
|
||||
else
|
||||
-- skip line
|
||||
e.window.blit(spaces, blit_fg, blit_bg_sides)
|
||||
local _fg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
|
||||
local _bg = util.trinary(args.even_inner == true, string.rep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
|
||||
|
||||
if width_x2 % 3 == 1 then
|
||||
e.w_blit(p_a, _fg, _bg)
|
||||
elseif width_x2 % 3 == 2 then
|
||||
e.w_blit(p_b, _fg, _bg)
|
||||
else
|
||||
-- skip line
|
||||
e.w_blit(spaces, blit_fg, blit_bg_sides)
|
||||
end
|
||||
end
|
||||
else
|
||||
e.w_blit(spaces, blit_fg, blit_bg_top_bot)
|
||||
end
|
||||
else
|
||||
e.window.blit(spaces, blit_fg, blit_bg_top_bot)
|
||||
end
|
||||
else
|
||||
if args.thin == true then
|
||||
e.window.blit(p_s, blit_fg_sides, blit_bg_sides)
|
||||
else
|
||||
e.window.blit(p_s, blit_fg, blit_bg_sides)
|
||||
if args.thin == true then
|
||||
e.w_blit(p_s, blit_fg_sides, blit_bg_sides)
|
||||
else
|
||||
e.w_blit(p_s, blit_fg, blit_bg_sides)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw of border
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
|
||||
@@ -5,11 +5,11 @@ local util = require("scada-common.util")
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
---@class textbox_args
|
||||
---@field text string text to show
|
||||
---@field alignment? TEXT_ALIGN text alignment, left by default
|
||||
---@field alignment? ALIGN text alignment, left by default
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
@@ -24,19 +24,20 @@ local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
---@param args textbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function textbox(args)
|
||||
assert(type(args.text) == "string", "graphics.elements.textbox: text is a required field")
|
||||
element.assert(type(args.text) == "string", "text is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local alignment = args.alignment or TEXT_ALIGN.LEFT
|
||||
e.value = args.text
|
||||
|
||||
local alignment = args.alignment or ALIGN.LEFT
|
||||
|
||||
-- draw textbox
|
||||
function e.redraw()
|
||||
e.window.clear()
|
||||
|
||||
local function display_text(text)
|
||||
e.value = text
|
||||
|
||||
local lines = util.strwrap(text, e.frame.w)
|
||||
local lines = util.strwrap(e.value, e.frame.w)
|
||||
|
||||
for i = 1, #lines do
|
||||
if i > e.frame.h then break end
|
||||
@@ -44,27 +45,28 @@ local function textbox(args)
|
||||
local len = string.len(lines[i])
|
||||
|
||||
-- use cursor position to align this line
|
||||
if alignment == TEXT_ALIGN.CENTER then
|
||||
e.window.setCursorPos(math.floor((e.frame.w - len) / 2) + 1, i)
|
||||
elseif alignment == TEXT_ALIGN.RIGHT then
|
||||
e.window.setCursorPos((e.frame.w - len) + 1, i)
|
||||
if alignment == ALIGN.CENTER then
|
||||
e.w_set_cur(math.floor((e.frame.w - len) / 2) + 1, i)
|
||||
elseif alignment == ALIGN.RIGHT then
|
||||
e.w_set_cur((e.frame.w - len) + 1, i)
|
||||
else
|
||||
e.window.setCursorPos(1, i)
|
||||
e.w_set_cur(1, i)
|
||||
end
|
||||
|
||||
e.window.write(lines[i])
|
||||
e.w_write(lines[i])
|
||||
end
|
||||
end
|
||||
|
||||
display_text(args.text)
|
||||
|
||||
-- set the string value and re-draw the text
|
||||
---@param val string value
|
||||
function e.set_value(val)
|
||||
e.window.clear()
|
||||
display_text(val)
|
||||
e.value = val
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
|
||||
@@ -22,13 +22,11 @@ local element = require("graphics.element")
|
||||
---@param args tiling_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function tiling(args)
|
||||
assert(type(args.fill_c) == "table", "graphics.elements.tiling: fill_c is a required field")
|
||||
element.assert(type(args.fill_c) == "table", "fill_c is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- draw tiling box
|
||||
|
||||
local fill_a = args.fill_c.blit_a
|
||||
local fill_b = args.fill_c.blit_b
|
||||
|
||||
@@ -38,13 +36,9 @@ local function tiling(args)
|
||||
local start_y = 1
|
||||
local inner_width = math.floor(e.frame.w / util.trinary(even, 2, 1))
|
||||
local inner_height = e.frame.h
|
||||
local alternator = true
|
||||
|
||||
-- border
|
||||
if args.border_c ~= nil then
|
||||
e.window.setBackgroundColor(args.border_c)
|
||||
e.window.clear()
|
||||
|
||||
start_x = 1 + util.trinary(even, 2, 1)
|
||||
start_y = 2
|
||||
|
||||
@@ -53,35 +47,48 @@ local function tiling(args)
|
||||
end
|
||||
|
||||
-- check dimensions
|
||||
assert(inner_width > 0, "graphics.elements.tiling: inner_width <= 0")
|
||||
assert(inner_height > 0, "graphics.elements.tiling: inner_height <= 0")
|
||||
assert(start_x <= inner_width, "graphics.elements.tiling: start_x > inner_width")
|
||||
assert(start_y <= inner_height, "graphics.elements.tiling: start_y > inner_height")
|
||||
element.assert(inner_width > 0, "inner_width <= 0")
|
||||
element.assert(inner_height > 0, "inner_height <= 0")
|
||||
element.assert(start_x <= inner_width, "start_x > inner_width")
|
||||
element.assert(start_y <= inner_height, "start_y > inner_height")
|
||||
|
||||
-- create pattern
|
||||
for y = start_y, inner_height + (start_y - 1) do
|
||||
e.window.setCursorPos(start_x, y)
|
||||
for _ = 1, inner_width do
|
||||
if alternator then
|
||||
if even then
|
||||
e.window.blit(" ", "00", fill_a .. fill_a)
|
||||
else
|
||||
e.window.blit(" ", "0", fill_a)
|
||||
end
|
||||
else
|
||||
if even then
|
||||
e.window.blit(" ", "00", fill_b .. fill_b)
|
||||
else
|
||||
e.window.blit(" ", "0", fill_b)
|
||||
end
|
||||
end
|
||||
-- draw tiling box
|
||||
function e.redraw()
|
||||
local alternator = true
|
||||
|
||||
alternator = not alternator
|
||||
if args.border_c ~= nil then
|
||||
e.w_set_bkg(args.border_c)
|
||||
e.window.clear()
|
||||
end
|
||||
|
||||
if inner_width % 2 == 0 then alternator = not alternator end
|
||||
-- draw pattern
|
||||
for y = start_y, inner_height + (start_y - 1) do
|
||||
e.w_set_cur(start_x, y)
|
||||
for _ = 1, inner_width do
|
||||
if alternator then
|
||||
if even then
|
||||
e.w_blit(" ", "00", fill_a .. fill_a)
|
||||
else
|
||||
e.w_blit(" ", "0", fill_a)
|
||||
end
|
||||
else
|
||||
if even then
|
||||
e.w_blit(" ", "00", fill_b .. fill_b)
|
||||
else
|
||||
e.w_blit(" ", "0", fill_b)
|
||||
end
|
||||
end
|
||||
|
||||
alternator = not alternator
|
||||
end
|
||||
|
||||
if inner_width % 2 == 0 then alternator = not alternator end
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
|
||||
@@ -4,46 +4,77 @@
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local DOUBLE_CLICK_MS = 500
|
||||
|
||||
local events = {}
|
||||
|
||||
---@enum CLICK_BUTTON
|
||||
events.CLICK_BUTTON = {
|
||||
local CLICK_BUTTON = {
|
||||
GENERIC = 0,
|
||||
LEFT_BUTTON = 1,
|
||||
RIGHT_BUTTON = 2,
|
||||
MID_BUTTON = 3
|
||||
}
|
||||
|
||||
---@enum CLICK_TYPE
|
||||
events.CLICK_TYPE = {
|
||||
events.CLICK_BUTTON = CLICK_BUTTON
|
||||
|
||||
---@enum MOUSE_CLICK
|
||||
local MOUSE_CLICK = {
|
||||
TAP = 1, -- screen tap (complete click)
|
||||
DOWN = 2, -- button down
|
||||
UP = 3, -- button up (completed a click)
|
||||
DRAG = 4, -- mouse dragged
|
||||
SCROLL_DOWN = 5, -- scroll down
|
||||
SCROLL_UP = 6 -- scroll up
|
||||
SCROLL_UP = 6, -- scroll up
|
||||
DOUBLE_CLICK = 7 -- double left click
|
||||
}
|
||||
|
||||
events.MOUSE_CLICK = MOUSE_CLICK
|
||||
|
||||
---@enum KEY_CLICK
|
||||
local KEY_CLICK = {
|
||||
DOWN = 1,
|
||||
HELD = 2,
|
||||
UP = 3,
|
||||
CHAR = 4
|
||||
}
|
||||
|
||||
events.KEY_CLICK = KEY_CLICK
|
||||
|
||||
-- create a new 2D coordinate
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return coordinate_2d
|
||||
local function _coord2d(x, y) return { x = x, y = y } end
|
||||
|
||||
events.new_coord_2d = _coord2d
|
||||
|
||||
---@class mouse_interaction
|
||||
---@field monitor string
|
||||
---@field button CLICK_BUTTON
|
||||
---@field type CLICK_TYPE
|
||||
---@field type MOUSE_CLICK
|
||||
---@field initial coordinate_2d
|
||||
---@field current coordinate_2d
|
||||
|
||||
---@class key_interaction
|
||||
---@field type KEY_CLICK
|
||||
---@field key number key code
|
||||
---@field name string key character name
|
||||
---@field shift boolean shift held
|
||||
---@field ctrl boolean ctrl held
|
||||
---@field alt boolean alt held
|
||||
|
||||
local handler = {
|
||||
-- left, right, middle button down tracking
|
||||
button_down = {
|
||||
_coord2d(0, 0),
|
||||
_coord2d(0, 0),
|
||||
_coord2d(0, 0)
|
||||
}
|
||||
button_down = { _coord2d(0, 0), _coord2d(0, 0), _coord2d(0, 0) },
|
||||
-- keyboard modifiers
|
||||
shift = false,
|
||||
alt = false,
|
||||
ctrl = false,
|
||||
-- double click tracking
|
||||
dc_start = 0,
|
||||
dc_step = 1,
|
||||
dc_coord = _coord2d(0, 0)
|
||||
}
|
||||
|
||||
-- create a new monitor touch mouse interaction event
|
||||
@@ -55,8 +86,8 @@ local handler = {
|
||||
local function _monitor_touch(monitor, x, y)
|
||||
return {
|
||||
monitor = monitor,
|
||||
button = events.CLICK_BUTTON.GENERIC,
|
||||
type = events.CLICK_TYPE.TAP,
|
||||
button = CLICK_BUTTON.GENERIC,
|
||||
type = MOUSE_CLICK.TAP,
|
||||
initial = _coord2d(x, y),
|
||||
current = _coord2d(x, y)
|
||||
}
|
||||
@@ -65,7 +96,7 @@ end
|
||||
-- create a new mouse button mouse interaction event
|
||||
---@nodiscard
|
||||
---@param button CLICK_BUTTON mouse button
|
||||
---@param type CLICK_TYPE click type
|
||||
---@param type MOUSE_CLICK click type
|
||||
---@param x1 integer initial x
|
||||
---@param y1 integer initial y
|
||||
---@param x2 integer current x
|
||||
@@ -83,14 +114,14 @@ end
|
||||
|
||||
-- create a new generic mouse interaction event
|
||||
---@nodiscard
|
||||
---@param type CLICK_TYPE
|
||||
---@param type MOUSE_CLICK
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return mouse_interaction
|
||||
function events.mouse_generic(type, x, y)
|
||||
return {
|
||||
monitor = "",
|
||||
button = events.CLICK_BUTTON.GENERIC,
|
||||
button = CLICK_BUTTON.GENERIC,
|
||||
type = type,
|
||||
initial = _coord2d(x, y),
|
||||
current = _coord2d(x, y)
|
||||
@@ -115,8 +146,8 @@ end
|
||||
|
||||
-- check if an event qualifies as a click (tap or up)
|
||||
---@nodiscard
|
||||
---@param t CLICK_TYPE
|
||||
function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events.CLICK_TYPE.UP end
|
||||
---@param t MOUSE_CLICK
|
||||
function events.was_clicked(t) return t == MOUSE_CLICK.TAP or t == MOUSE_CLICK.UP end
|
||||
|
||||
-- create a new mouse event to pass onto graphics renderer<br>
|
||||
-- supports: mouse_click, mouse_up, mouse_drag, mouse_scroll, and monitor_touch
|
||||
@@ -126,35 +157,97 @@ function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events.
|
||||
---@param y integer y coordinate
|
||||
---@return mouse_interaction|nil
|
||||
function events.new_mouse_event(event_type, opt, x, y)
|
||||
local h = handler
|
||||
|
||||
if event_type == "mouse_click" then
|
||||
---@cast opt 1|2|3
|
||||
handler.button_down[opt] = _coord2d(x, y)
|
||||
return _mouse_event(opt, events.CLICK_TYPE.DOWN, x, y, x, y)
|
||||
|
||||
local init = true
|
||||
|
||||
if opt == 1 and (h.dc_step % 2) == 1 then
|
||||
if h.dc_step ~= 1 and h.dc_coord.x == x and h.dc_coord.y == y and (util.time_ms() - h.dc_start) < DOUBLE_CLICK_MS then
|
||||
init = false
|
||||
h.dc_step = h.dc_step + 1
|
||||
end
|
||||
end
|
||||
|
||||
if init then
|
||||
h.dc_start = util.time_ms()
|
||||
h.dc_coord = _coord2d(x, y)
|
||||
h.dc_step = 2
|
||||
end
|
||||
|
||||
h.button_down[opt] = _coord2d(x, y)
|
||||
return _mouse_event(opt, MOUSE_CLICK.DOWN, x, y, x, y)
|
||||
elseif event_type == "mouse_up" then
|
||||
---@cast opt 1|2|3
|
||||
local initial = handler.button_down[opt] ---@type coordinate_2d
|
||||
return _mouse_event(opt, events.CLICK_TYPE.UP, initial.x, initial.y, x, y)
|
||||
|
||||
if opt == 1 and (h.dc_step % 2) == 0 and h.dc_coord.x == x and h.dc_coord.y == y and
|
||||
(util.time_ms() - h.dc_start) < DOUBLE_CLICK_MS then
|
||||
if h.dc_step == 4 then
|
||||
util.push_event("double_click", 1, x, y)
|
||||
h.dc_step = 1
|
||||
else h.dc_step = h.dc_step + 1 end
|
||||
else h.dc_step = 1 end
|
||||
|
||||
local initial = h.button_down[opt] ---@type coordinate_2d
|
||||
return _mouse_event(opt, MOUSE_CLICK.UP, initial.x, initial.y, x, y)
|
||||
elseif event_type == "monitor_touch" then
|
||||
---@cast opt string
|
||||
return _monitor_touch(opt, x, y)
|
||||
elseif event_type == "mouse_drag" then
|
||||
---@cast opt 1|2|3
|
||||
local initial = handler.button_down[opt] ---@type coordinate_2d
|
||||
return _mouse_event(opt, events.CLICK_TYPE.DRAG, initial.x, initial.y, x, y)
|
||||
local initial = h.button_down[opt] ---@type coordinate_2d
|
||||
return _mouse_event(opt, MOUSE_CLICK.DRAG, initial.x, initial.y, x, y)
|
||||
elseif event_type == "mouse_scroll" then
|
||||
---@cast opt 1|-1
|
||||
local scroll_direction = util.trinary(opt == 1, events.CLICK_TYPE.SCROLL_DOWN, events.CLICK_TYPE.SCROLL_UP)
|
||||
return _mouse_event(events.CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y)
|
||||
local scroll_direction = util.trinary(opt == 1, MOUSE_CLICK.SCROLL_DOWN, MOUSE_CLICK.SCROLL_UP)
|
||||
return _mouse_event(CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y)
|
||||
elseif event_type == "double_click" then
|
||||
return _mouse_event(CLICK_BUTTON.LEFT_BUTTON, MOUSE_CLICK.DOUBLE_CLICK, x, y, x, y)
|
||||
end
|
||||
end
|
||||
|
||||
-- create a new key event to pass onto graphics renderer<br>
|
||||
-- create a new keyboard interaction event
|
||||
---@nodiscard
|
||||
---@param click_type KEY_CLICK key click type
|
||||
---@param key integer|string keyboard key code or character for 'char' event
|
||||
---@return key_interaction
|
||||
local function _key_event(click_type, key)
|
||||
local name = key
|
||||
if type(key) == "number" then name = keys.getName(key) end
|
||||
return { type = click_type, key = key, name = name, shift = handler.shift, ctrl = handler.ctrl, alt = handler.alt }
|
||||
end
|
||||
|
||||
-- create a new keyboard event to pass onto graphics renderer<br>
|
||||
-- supports: char, key, and key_up
|
||||
---@param event_type os_event
|
||||
function events.new_key_event(event_type)
|
||||
---@param event_type os_event OS event to handle
|
||||
---@param key integer keyboard key code
|
||||
---@param held boolean? if the key is being held (for 'key' event)
|
||||
---@return key_interaction|nil
|
||||
function events.new_key_event(event_type, key, held)
|
||||
if event_type == "char" then
|
||||
return _key_event(KEY_CLICK.CHAR, key)
|
||||
elseif event_type == "key" then
|
||||
if key == keys.leftShift or key == keys.rightShift then
|
||||
handler.shift = true
|
||||
elseif key == keys.leftCtrl or key == keys.rightCtrl then
|
||||
handler.ctrl = true
|
||||
elseif key == keys.leftAlt or key == keys.rightAlt then
|
||||
handler.alt = true
|
||||
else
|
||||
return _key_event(util.trinary(held, KEY_CLICK.HELD, KEY_CLICK.DOWN), key)
|
||||
end
|
||||
elseif event_type == "key_up" then
|
||||
if key == keys.leftShift or key == keys.rightShift then
|
||||
handler.shift = false
|
||||
elseif key == keys.leftCtrl or key == keys.rightCtrl then
|
||||
handler.ctrl = false
|
||||
elseif key == keys.leftAlt or key == keys.rightAlt then
|
||||
handler.alt = false
|
||||
else
|
||||
return _key_event(KEY_CLICK.UP, key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
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
|
||||
5
imgen.py
5
imgen.py
@@ -48,6 +48,7 @@ def make_manifest(size):
|
||||
"versions" : {
|
||||
"installer" : get_version("./ccmsi.lua"),
|
||||
"bootloader" : get_version("./startup.lua"),
|
||||
"common" : get_version("./scada-common/util.lua", True),
|
||||
"comms" : get_version("./scada-common/comms.lua", True),
|
||||
"graphics" : get_version("./graphics/core.lua", True),
|
||||
"lockbox" : get_version("./lockbox/init.lua", True),
|
||||
@@ -59,7 +60,7 @@ def make_manifest(size):
|
||||
},
|
||||
"files" : {
|
||||
# common files
|
||||
"system" : [ "initenv.lua", "startup.lua" ],
|
||||
"system" : [ "initenv.lua", "startup.lua", "configure.lua", "LICENSE" ],
|
||||
"common" : list_files("./scada-common"),
|
||||
"graphics" : list_files("./graphics"),
|
||||
"lockbox" : list_files("./lockbox"),
|
||||
@@ -81,7 +82,7 @@ def make_manifest(size):
|
||||
# manifest file estimate
|
||||
"manifest" : size,
|
||||
# common files
|
||||
"system" : os.path.getsize("initenv.lua") + os.path.getsize("startup.lua"),
|
||||
"system" : os.path.getsize("initenv.lua") + os.path.getsize("startup.lua") + os.path.getsize("configure.lua"),
|
||||
"common" : dir_size("./scada-common"),
|
||||
"graphics" : dir_size("./graphics"),
|
||||
"lockbox" : dir_size("./lockbox"),
|
||||
|
||||
22
initenv.lua
22
initenv.lua
@@ -1,18 +1,8 @@
|
||||
--
|
||||
-- Initialize the Post-Boot Module Environment
|
||||
--
|
||||
|
||||
return {
|
||||
-- initialize booted environment
|
||||
init_env = function ()
|
||||
local _require = require("cc.require")
|
||||
local _env = setmetatable({}, { __index = _ENV })
|
||||
|
||||
-- overwrite require/package globals
|
||||
require, package = _require.make(_env, "/")
|
||||
|
||||
-- reset terminal
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
end
|
||||
-- initialize booted environment
|
||||
init_env = function ()
|
||||
local _require, _env = require("cc.require"), setmetatable({}, { __index = _ENV })
|
||||
require, package = _require.make(_env, "/")
|
||||
term.clear(); term.setCursorPos(1, 1)
|
||||
end
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,3 @@
|
||||
require("lockbox").insecure();
|
||||
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local String = require("string");
|
||||
local Math = require("math");
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
require("lockbox").insecure();
|
||||
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local String = require("string");
|
||||
local Math = require("math");
|
||||
|
||||
@@ -1,25 +1,6 @@
|
||||
local Lockbox = {};
|
||||
local Lockbox = {}
|
||||
|
||||
-- cc-mek-scada lockbox version
|
||||
Lockbox.version = "1.0"
|
||||
Lockbox.version = "1.1"
|
||||
|
||||
--[[
|
||||
package.path = "./?.lua;"
|
||||
.. "./cipher/?.lua;"
|
||||
.. "./digest/?.lua;"
|
||||
.. "./kdf/?.lua;"
|
||||
.. "./mac/?.lua;"
|
||||
.. "./padding/?.lua;"
|
||||
.. "./test/?.lua;"
|
||||
.. "./util/?.lua;"
|
||||
.. package.path;
|
||||
--]]
|
||||
Lockbox.ALLOW_INSECURE = true;
|
||||
|
||||
Lockbox.insecure = function()
|
||||
assert(Lockbox.ALLOW_INSECURE,
|
||||
"This module is insecure! It should not be used in production." ..
|
||||
"If you really want to use it, set Lockbox.ALLOW_INSECURE to true before importing it");
|
||||
end
|
||||
|
||||
return Lockbox;
|
||||
return Lockbox
|
||||
|
||||
@@ -8,7 +8,7 @@ local HMAC = function()
|
||||
|
||||
local public = {};
|
||||
local blockSize = 64;
|
||||
local Digest = nil;
|
||||
local Digest;
|
||||
local outerPadding = {};
|
||||
local innerPadding = {}
|
||||
local digest;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
local String = require("string");
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
@@ -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
|
||||
@@ -1,35 +0,0 @@
|
||||
--
|
||||
-- Core I/O - Pocket Central I/O Management
|
||||
--
|
||||
|
||||
local psil = require("scada-common.psil")
|
||||
|
||||
local coreio = {}
|
||||
|
||||
---@class pocket_core_io
|
||||
local io = {
|
||||
ps = psil.create()
|
||||
}
|
||||
|
||||
---@enum POCKET_LINK_STATE
|
||||
local LINK_STATE = {
|
||||
UNLINKED = 0,
|
||||
SV_LINK_ONLY = 1,
|
||||
API_LINK_ONLY = 2,
|
||||
LINKED = 3
|
||||
}
|
||||
|
||||
coreio.LINK_STATE = LINK_STATE
|
||||
|
||||
-- get the core PSIL
|
||||
function coreio.core_ps()
|
||||
return io.ps
|
||||
end
|
||||
|
||||
-- set network link state
|
||||
---@param state POCKET_LINK_STATE
|
||||
function coreio.report_link_state(state)
|
||||
io.ps.publish("link_state", state)
|
||||
end
|
||||
|
||||
return coreio
|
||||
779
pocket/iocontrol.lua
Normal file
779
pocket/iocontrol.lua
Normal file
@@ -0,0 +1,779 @@
|
||||
--
|
||||
-- I/O Control for Pocket Integration with Supervisor & Coordinator
|
||||
--
|
||||
|
||||
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 = {}
|
||||
|
||||
---@enum POCKET_LINK_STATE
|
||||
local LINK_STATE = {
|
||||
UNLINKED = 0,
|
||||
SV_LINK_ONLY = 1,
|
||||
API_LINK_ONLY = 2,
|
||||
LINKED = 3
|
||||
}
|
||||
|
||||
iocontrol.LINK_STATE = LINK_STATE
|
||||
|
||||
---@enum POCKET_APP_ID
|
||||
local APP_ID = {
|
||||
ROOT = 1,
|
||||
-- main app page
|
||||
UNITS = 2,
|
||||
ABOUT = 3,
|
||||
-- diag app page
|
||||
ALARMS = 4,
|
||||
-- other
|
||||
DUMMY = 5,
|
||||
NUM_APPS = 5
|
||||
}
|
||||
|
||||
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 = {}
|
||||
|
||||
-- alarm testing
|
||||
io.diag.tone_test = {
|
||||
test_1 = function (state) comms.diag__set_alarm_tone(1, state) end,
|
||||
test_2 = function (state) comms.diag__set_alarm_tone(2, state) end,
|
||||
test_3 = function (state) comms.diag__set_alarm_tone(3, state) end,
|
||||
test_4 = function (state) comms.diag__set_alarm_tone(4, state) end,
|
||||
test_5 = function (state) comms.diag__set_alarm_tone(5, state) end,
|
||||
test_6 = function (state) comms.diag__set_alarm_tone(6, state) end,
|
||||
test_7 = function (state) comms.diag__set_alarm_tone(7, state) end,
|
||||
test_8 = function (state) comms.diag__set_alarm_tone(8, state) end,
|
||||
stop_tones = function () comms.diag__set_alarm_tone(0, false) end,
|
||||
|
||||
test_breach = function (state) comms.diag__set_alarm(ALARM.ContainmentBreach, state) end,
|
||||
test_rad = function (state) comms.diag__set_alarm(ALARM.ContainmentRadiation, state) end,
|
||||
test_lost = function (state) comms.diag__set_alarm(ALARM.ReactorLost, state) end,
|
||||
test_crit = function (state) comms.diag__set_alarm(ALARM.CriticalDamage, state) end,
|
||||
test_dmg = function (state) comms.diag__set_alarm(ALARM.ReactorDamage, state) end,
|
||||
test_overtemp = function (state) comms.diag__set_alarm(ALARM.ReactorOverTemp, state) end,
|
||||
test_hightemp = function (state) comms.diag__set_alarm(ALARM.ReactorHighTemp, state) end,
|
||||
test_wasteleak = function (state) comms.diag__set_alarm(ALARM.ReactorWasteLeak, state) end,
|
||||
test_highwaste = function (state) comms.diag__set_alarm(ALARM.ReactorHighWaste, state) end,
|
||||
test_rps = function (state) comms.diag__set_alarm(ALARM.RPSTransient, state) end,
|
||||
test_rcs = function (state) comms.diag__set_alarm(ALARM.RCSTransient, state) end,
|
||||
test_turbinet = function (state) comms.diag__set_alarm(ALARM.TurbineTrip, state) end,
|
||||
stop_alarms = function () comms.diag__set_alarm(0, false) end,
|
||||
|
||||
get_tone_states = function () comms.diag__get_alarm_tones() end,
|
||||
|
||||
ready_warn = nil, ---@type graphics_element
|
||||
tone_buttons = {},
|
||||
alarm_buttons = {},
|
||||
tone_indicators = {} -- indicators to update from supervisor tone states
|
||||
}
|
||||
|
||||
-- API access
|
||||
---@class pocket_ioctl_api
|
||||
io.api = {
|
||||
get_unit = function (unit) comms.api__get_unit(unit) end
|
||||
}
|
||||
end
|
||||
|
||||
-- initialize facility-dependent components of pocket iocontrol
|
||||
---@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
|
||||
---@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
|
||||
local every = true
|
||||
|
||||
-- split up online arrays
|
||||
for id = 1, #val do
|
||||
every = every and val[id]
|
||||
|
||||
if key == "BoilerOnline" then
|
||||
unit.boiler_ps_tbl[id].publish(key, val[id])
|
||||
else
|
||||
unit.turbine_ps_tbl[id].publish(key, val[id])
|
||||
end
|
||||
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
|
||||
|
||||
return iocontrol
|
||||
@@ -1,30 +1,70 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local coreio = require("pocket.coreio")
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||
-- local CAPI_TYPE = comms.CAPI_TYPE
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
local CRDN_TYPE = comms.CRDN_TYPE
|
||||
|
||||
local LINK_STATE = coreio.LINK_STATE
|
||||
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,
|
||||
@@ -43,16 +83,16 @@ 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 SCADA_MGMT_TYPE
|
||||
---@param msg_type MGMT_TYPE
|
||||
---@param msg table
|
||||
local function _send_sv(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
@@ -61,12 +101,12 @@ 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
|
||||
|
||||
-- send a management packet to the coordinator
|
||||
---@param msg_type SCADA_MGMT_TYPE
|
||||
---@param msg_type MGMT_TYPE
|
||||
---@param msg table
|
||||
local function _send_crd(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
@@ -75,44 +115,44 @@ 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 a packet to the coordinator API
|
||||
-----@param msg_type CAPI_TYPE
|
||||
-----@param msg table
|
||||
-- local function _send_api(msg_type, msg)
|
||||
-- local s_pkt = comms.scada_packet()
|
||||
-- local pkt = comms.capi_packet()
|
||||
-- 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.COORD_API, pkt.raw_sendable())
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_CRDN, pkt.raw_sendable())
|
||||
|
||||
-- nic.transmit(crd_channel, pkt_channel, s_pkt)
|
||||
-- self.api.seq_num = self.api.seq_num + 1
|
||||
-- end
|
||||
nic.transmit(config.CRD_Channel, config.PKT_Channel, s_pkt)
|
||||
self.api.seq_num = self.api.seq_num + 1
|
||||
end
|
||||
|
||||
-- attempt supervisor connection establishment
|
||||
local function _send_sv_establish()
|
||||
_send_sv(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
|
||||
_send_sv(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
|
||||
end
|
||||
|
||||
-- attempt coordinator API connection establishment
|
||||
local function _send_api_establish()
|
||||
_send_crd(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
|
||||
_send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT, comms.api_version })
|
||||
end
|
||||
|
||||
-- keep alive ack to supervisor
|
||||
---@param srv_time integer
|
||||
local function _send_sv_keep_alive_ack(srv_time)
|
||||
_send_sv(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||
_send_sv(MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||
end
|
||||
|
||||
-- keep alive ack to coordinator
|
||||
---@param srv_time integer
|
||||
local function _send_api_keep_alive_ack(srv_time)
|
||||
_send_crd(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||
_send_crd(MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
@@ -126,7 +166,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
self.sv.linked = false
|
||||
self.sv.r_seq_num = nil
|
||||
self.sv.addr = comms.BROADCAST
|
||||
_send_sv(SCADA_MGMT_TYPE.CLOSE, {})
|
||||
_send_sv(MGMT_TYPE.CLOSE, {})
|
||||
end
|
||||
|
||||
-- close connection to coordinator API server
|
||||
@@ -135,7 +175,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
self.api.linked = false
|
||||
self.api.r_seq_num = nil
|
||||
self.api.addr = comms.BROADCAST
|
||||
_send_crd(SCADA_MGMT_TYPE.CLOSE, {})
|
||||
_send_crd(MGMT_TYPE.CLOSE, {})
|
||||
end
|
||||
|
||||
-- close the connections to the servers
|
||||
@@ -147,7 +187,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
-- attempt to re-link if any of the dependent links aren't active
|
||||
function public.link_update()
|
||||
if not self.sv.linked then
|
||||
coreio.report_link_state(util.trinary(self.api.linked, LINK_STATE.API_LINK_ONLY, LINK_STATE.UNLINKED))
|
||||
iocontrol.report_link_state(util.trinary(self.api.linked, LINK_STATE.API_LINK_ONLY, LINK_STATE.UNLINKED))
|
||||
|
||||
if self.establish_delay_counter <= 0 then
|
||||
_send_sv_establish()
|
||||
@@ -156,7 +196,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
self.establish_delay_counter = self.establish_delay_counter - 1
|
||||
end
|
||||
elseif not self.api.linked then
|
||||
coreio.report_link_state(LINK_STATE.SV_LINK_ONLY)
|
||||
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY)
|
||||
|
||||
if self.establish_delay_counter <= 0 then
|
||||
_send_api_establish()
|
||||
@@ -166,17 +206,41 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
end
|
||||
else
|
||||
-- linked, all good!
|
||||
coreio.report_link_state(LINK_STATE.LINKED)
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
|
||||
end
|
||||
end
|
||||
|
||||
-- supervisor get active alarm tones
|
||||
function public.diag__get_alarm_tones()
|
||||
if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_TONE_GET, {}) end
|
||||
end
|
||||
|
||||
-- supervisor test alarm tones by tone
|
||||
---@param id TONE|0 tone ID, or 0 to stop all
|
||||
---@param state boolean tone state
|
||||
function public.diag__set_alarm_tone(id, state)
|
||||
if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_TONE_SET, { id, state }) end
|
||||
end
|
||||
|
||||
-- supervisor test alarm tones by alarm
|
||||
---@param id ALARM|0 alarm ID, 0 to stop all
|
||||
---@param state boolean alarm state
|
||||
function public.diag__set_alarm(id, state)
|
||||
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
|
||||
---@param reply_to integer
|
||||
---@param message any
|
||||
---@param distance integer
|
||||
---@return mgmt_frame|capi_frame|nil packet
|
||||
---@return mgmt_frame|crdn_frame|nil packet
|
||||
function public.parse_packet(side, sender, reply_to, message, distance)
|
||||
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
|
||||
local pkt = nil
|
||||
@@ -188,11 +252,11 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
pkt = mgmt_pkt.get()
|
||||
end
|
||||
-- get as coordinator API packet
|
||||
elseif s_pkt.protocol() == PROTOCOL.COORD_API then
|
||||
local capi_pkt = comms.capi_packet()
|
||||
if capi_pkt.decode(s_pkt) then
|
||||
pkt = capi_pkt.get()
|
||||
-- get as coordinator packet
|
||||
elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
local crdn_pkt = comms.crdn_packet()
|
||||
if crdn_pkt.decode(s_pkt) then
|
||||
pkt = crdn_pkt.get()
|
||||
end
|
||||
else
|
||||
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
|
||||
@@ -202,18 +266,39 @@ 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|capi_frame|nil
|
||||
---@param packet mgmt_frame|crdn_frame|nil
|
||||
function public.handle_packet(packet)
|
||||
local diag = iocontrol.get_db().diag
|
||||
|
||||
if packet ~= nil then
|
||||
local l_chan = packet.scada_frame.local_channel()
|
||||
local r_chan = packet.scada_frame.remote_channel()
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
local src_addr = packet.scada_frame.src_addr()
|
||||
|
||||
if l_chan ~= pkt_channel then
|
||||
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()
|
||||
@@ -222,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()
|
||||
@@ -231,25 +316,79 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
-- feed watchdog on valid sequence number
|
||||
api_watchdog.feed()
|
||||
|
||||
if protocol == PROTOCOL.COORD_API then
|
||||
---@cast packet capi_frame
|
||||
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 packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
if self.api.linked then
|
||||
if packet.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if _check_length(packet, 1) then
|
||||
local timestamp = packet.data[1]
|
||||
local trip_time = util.time() - timestamp
|
||||
|
||||
if trip_time > 750 then
|
||||
log.warning("pocket coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug("pocket coordinator TT = " .. trip_time .. "ms")
|
||||
|
||||
_send_api_keep_alive_ack(timestamp)
|
||||
|
||||
iocontrol.report_crd_tt(trip_time)
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
api_watchdog.cancel()
|
||||
self.api.linked = false
|
||||
self.api.r_seq_num = nil
|
||||
self.api.addr = comms.BROADCAST
|
||||
log.info("coordinator server connection closed by remote host")
|
||||
else _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
|
||||
coreio.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
|
||||
coreio.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
|
||||
@@ -263,40 +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
|
||||
elseif self.api.linked then
|
||||
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if packet.length == 1 then
|
||||
local timestamp = packet.data[1]
|
||||
local trip_time = util.time() - timestamp
|
||||
|
||||
if trip_time > 750 then
|
||||
log.warning("pocket coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug("pocket coordinator RTT = " .. trip_time .. "ms")
|
||||
|
||||
_send_api_keep_alive_ack(timestamp)
|
||||
else
|
||||
log.debug("coordinator SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
api_watchdog.cancel()
|
||||
self.api.linked = false
|
||||
self.api.r_seq_num = nil
|
||||
self.api.addr = comms.BROADCAST
|
||||
log.info("coordinator server connection closed by remote host")
|
||||
else
|
||||
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator")
|
||||
end
|
||||
else
|
||||
log.debug("discarding coordinator non-link SCADA_MGMT packet before linked")
|
||||
@@ -304,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()
|
||||
@@ -325,9 +439,77 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
-- handle packet
|
||||
if protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
if self.sv.linked then
|
||||
if packet.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if _check_length(packet, 1) then
|
||||
local timestamp = packet.data[1]
|
||||
local trip_time = util.time() - timestamp
|
||||
|
||||
if trip_time > 750 then
|
||||
log.warning("pocket supervisor KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug("pocket supervisor TT = " .. trip_time .. "ms")
|
||||
|
||||
_send_sv_keep_alive_ack(timestamp)
|
||||
|
||||
iocontrol.report_svr_tt(trip_time)
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
sv_watchdog.cancel()
|
||||
self.sv.linked = false
|
||||
self.sv.r_seq_num = nil
|
||||
self.sv.addr = comms.BROADCAST
|
||||
log.info("supervisor server connection closed by remote host")
|
||||
elseif packet.type == MGMT_TYPE.DIAG_TONE_GET 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
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.DIAG_TONE_SET then
|
||||
if packet.length == 1 and packet.data[1] == false then
|
||||
diag.tone_test.ready_warn.set_value("testing denied")
|
||||
log.debug("supervisor SCADA diag tone set failed")
|
||||
elseif packet.length == 2 and type(packet.data[2]) == "table" then
|
||||
local ready = packet.data[1]
|
||||
local states = packet.data[2]
|
||||
|
||||
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready"))
|
||||
|
||||
for i = 1, #states do
|
||||
if diag.tone_test.tone_buttons[i] ~= nil then
|
||||
diag.tone_test.tone_buttons[i].set_value(states[i] == true)
|
||||
diag.tone_test.tone_indicators[i].update(states[i] == true)
|
||||
end
|
||||
end
|
||||
else
|
||||
log.debug("supervisor SCADA diag tone set packet length/type mismatch")
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.DIAG_ALARM_SET then
|
||||
if packet.length == 1 and packet.data[1] == false then
|
||||
diag.tone_test.ready_warn.set_value("testing denied")
|
||||
log.debug("supervisor SCADA diag alarm set failed")
|
||||
elseif packet.length == 2 and type(packet.data[2]) == "table" then
|
||||
local ready = packet.data[1]
|
||||
local states = packet.data[2]
|
||||
|
||||
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready"))
|
||||
|
||||
for i = 1, #states do
|
||||
if diag.tone_test.alarm_buttons[i] ~= nil then
|
||||
diag.tone_test.alarm_buttons[i].set_value(states[i] == true)
|
||||
end
|
||||
end
|
||||
else
|
||||
log.debug("supervisor SCADA diag alarm set packet length/type mismatch")
|
||||
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
|
||||
@@ -337,9 +519,9 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
self.sv.addr = src_addr
|
||||
|
||||
if self.api.linked then
|
||||
coreio.report_link_state(LINK_STATE.LINKED)
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
|
||||
else
|
||||
coreio.report_link_state(LINK_STATE.SV_LINK_ONLY)
|
||||
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY)
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.DENY then
|
||||
if self.sv.last_est_ack ~= est_ack then
|
||||
@@ -358,42 +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
|
||||
elseif self.sv.linked then
|
||||
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if packet.length == 1 then
|
||||
local timestamp = packet.data[1]
|
||||
local trip_time = util.time() - timestamp
|
||||
|
||||
if trip_time > 750 then
|
||||
log.warning("pocket supervisor KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug("pocket supervisor RTT = " .. trip_time .. "ms")
|
||||
|
||||
_send_sv_keep_alive_ack(timestamp)
|
||||
else
|
||||
log.debug("supervisor SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
sv_watchdog.cancel()
|
||||
self.sv.linked = false
|
||||
self.sv.r_seq_num = nil
|
||||
self.sv.addr = comms.BROADCAST
|
||||
log.info("supervisor server connection closed by remote host")
|
||||
else
|
||||
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor")
|
||||
end
|
||||
else
|
||||
log.debug("discarding supervisor non-link SCADA_MGMT packet before linked")
|
||||
end
|
||||
else
|
||||
log.debug("illegal packet type " .. protocol .. " from supervisor", true)
|
||||
end
|
||||
else _fail_type(packet) end
|
||||
else
|
||||
log.debug("received packet from unconfigured channel " .. r_chan, true)
|
||||
end
|
||||
@@ -411,5 +562,4 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
||||
return public
|
||||
end
|
||||
|
||||
|
||||
return pocket
|
||||
|
||||
@@ -5,18 +5,23 @@
|
||||
local main_view = require("pocket.ui.main")
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
|
||||
---@class pocket_renderer
|
||||
local renderer = {}
|
||||
|
||||
local ui = {
|
||||
display = nil
|
||||
}
|
||||
|
||||
-- start the pocket GUI
|
||||
function renderer.start_ui()
|
||||
-- try to start the pocket GUI
|
||||
---@return boolean success, any error_msg
|
||||
function renderer.try_start_ui()
|
||||
local status, msg = true, nil
|
||||
|
||||
if ui.display == nil then
|
||||
-- reset screen
|
||||
term.setTextColor(colors.white)
|
||||
@@ -30,12 +35,22 @@ function renderer.start_ui()
|
||||
end
|
||||
|
||||
-- init front panel view
|
||||
ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||
main_view(ui.display)
|
||||
status, msg = pcall(function ()
|
||||
ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||
main_view(ui.display)
|
||||
end)
|
||||
|
||||
-- start flasher callback task
|
||||
flasher.run()
|
||||
if status then
|
||||
-- start flasher callback task
|
||||
flasher.run()
|
||||
else
|
||||
-- report fail and close ui
|
||||
msg = core.extract_assert_msg(msg)
|
||||
renderer.close_ui()
|
||||
end
|
||||
end
|
||||
|
||||
return status, msg
|
||||
end
|
||||
|
||||
-- close out the UI
|
||||
|
||||
@@ -4,53 +4,57 @@
|
||||
|
||||
require("/initenv").init_env()
|
||||
|
||||
local crash = require("scada-common.crash")
|
||||
local log = require("scada-common.log")
|
||||
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 crash = require("scada-common.crash")
|
||||
local log = require("scada-common.log")
|
||||
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 core = require("graphics.core")
|
||||
|
||||
local config = require("pocket.config")
|
||||
local coreio = require("pocket.coreio")
|
||||
local pocket = require("pocket.pocket")
|
||||
local renderer = require("pocket.renderer")
|
||||
local configure = require("pocket.configure")
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local pocket = require("pocket.pocket")
|
||||
local renderer = require("pocket.renderer")
|
||||
|
||||
local POCKET_VERSION = "alpha-v0.5.2"
|
||||
local POCKET_VERSION = "v0.9.1-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,16 +68,19 @@ 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
|
||||
|
||||
coreio.report_link_state(coreio.LINK_STATE.UNLINKED)
|
||||
iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED)
|
||||
|
||||
-- get the communications modem
|
||||
local modem = ppm.get_wireless_modem()
|
||||
@@ -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,21 +103,22 @@ 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)
|
||||
local MAIN_CLOCK = 0.5
|
||||
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||
|
||||
-- init I/O control
|
||||
iocontrol.init_core(pocket_comms)
|
||||
|
||||
----------------------------------------
|
||||
-- start the UI
|
||||
----------------------------------------
|
||||
|
||||
local ui_ok, message = pcall(renderer.start_ui)
|
||||
local ui_ok, message = renderer.try_start_ui()
|
||||
if not ui_ok then
|
||||
renderer.close_ui()
|
||||
println(util.c("UI error: ", message))
|
||||
log.error(util.c("startup> GUI render failed with error ", message))
|
||||
else
|
||||
@@ -126,7 +134,10 @@ 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
|
||||
|
||||
-- main event loop
|
||||
while true do
|
||||
@@ -140,6 +151,10 @@ local function main()
|
||||
-- relink if necessary
|
||||
pocket_comms.link_update()
|
||||
|
||||
-- update any tasks for the active page
|
||||
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
|
||||
-- supervisor watchdog timeout
|
||||
@@ -158,7 +173,8 @@ local function main()
|
||||
-- got a packet
|
||||
local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
pocket_comms.handle_packet(packet)
|
||||
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then
|
||||
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
|
||||
event == "double_click" then
|
||||
-- handle a monitor touch event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
end
|
||||
|
||||
116
pocket/ui/apps/diag_apps.lua
Normal file
116
pocket/ui/apps/diag_apps.lua
Normal file
@@ -0,0 +1,116 @@
|
||||
--
|
||||
-- Diagnostic Apps
|
||||
--
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||
|
||||
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 ALIGN = core.ALIGN
|
||||
|
||||
-- create diagnostic app pages
|
||||
---@param root graphics_element parent
|
||||
local function create_pages(root)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
------------------------
|
||||
-- Alarm Testing Page --
|
||||
------------------------
|
||||
|
||||
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
|
||||
|
||||
local c_wht_gray = cpair(colors.white, colors.gray)
|
||||
local c_red_gray = cpair(colors.red, colors.gray)
|
||||
local c_yel_gray = cpair(colors.yellow, colors.gray)
|
||||
local c_blue_gray = cpair(colors.blue, colors.gray)
|
||||
|
||||
local audio = Div{parent=alarm_test,x=1,y=1}
|
||||
|
||||
TextBox{parent=audio,y=1,text="Alarm Sounder Tests",height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
ttest.ready_warn = TextBox{parent=audio,y=2,text="",height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
|
||||
|
||||
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()}
|
||||
|
||||
local test_btns = {}
|
||||
test_btns[1] = SwitchButton{parent=tones,text="TEST 1",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_1}
|
||||
test_btns[2] = SwitchButton{parent=tones,text="TEST 2",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_2}
|
||||
test_btns[3] = SwitchButton{parent=tones,text="TEST 3",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_3}
|
||||
test_btns[4] = SwitchButton{parent=tones,text="TEST 4",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_4}
|
||||
test_btns[5] = SwitchButton{parent=tones,text="TEST 5",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_5}
|
||||
test_btns[6] = SwitchButton{parent=tones,text="TEST 6",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_6}
|
||||
test_btns[7] = SwitchButton{parent=tones,text="TEST 7",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_7}
|
||||
test_btns[8] = SwitchButton{parent=tones,text="TEST 8",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_8}
|
||||
|
||||
ttest.tone_buttons = test_btns
|
||||
|
||||
local function stop_all_tones()
|
||||
for i = 1, #test_btns do test_btns[i].set_value(false) end
|
||||
ttest.stop_tones()
|
||||
end
|
||||
|
||||
PushButton{parent=tones,text="STOP",min_width=8,active_fg_bg=c_wht_gray,fg_bg=cpair(colors.black,colors.red),callback=stop_all_tones}
|
||||
|
||||
local alarms = Div{parent=audio,x=11,y=3,height=15,fg_bg=cpair(colors.lightGray,colors.black)}
|
||||
|
||||
TextBox{parent=alarms,text="Alarms (\x13)",height=1,alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
|
||||
|
||||
local alarm_btns = {}
|
||||
alarm_btns[1] = Checkbox{parent=alarms,label="BREACH",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_breach}
|
||||
alarm_btns[2] = Checkbox{parent=alarms,label="RADIATION",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_rad}
|
||||
alarm_btns[3] = Checkbox{parent=alarms,label="RCT LOST",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_lost}
|
||||
alarm_btns[4] = Checkbox{parent=alarms,label="CRIT DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_crit}
|
||||
alarm_btns[5] = Checkbox{parent=alarms,label="DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_dmg}
|
||||
alarm_btns[6] = Checkbox{parent=alarms,label="OVER TEMP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_overtemp}
|
||||
alarm_btns[7] = Checkbox{parent=alarms,label="HIGH TEMP",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_hightemp}
|
||||
alarm_btns[8] = Checkbox{parent=alarms,label="WASTE LEAK",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_wasteleak}
|
||||
alarm_btns[9] = Checkbox{parent=alarms,label="WASTE HIGH",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_highwaste}
|
||||
alarm_btns[10] = Checkbox{parent=alarms,label="RPS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rps}
|
||||
alarm_btns[11] = Checkbox{parent=alarms,label="RCS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rcs}
|
||||
alarm_btns[12] = Checkbox{parent=alarms,label="TURBINE TRP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_turbinet}
|
||||
|
||||
ttest.alarm_buttons = alarm_btns
|
||||
|
||||
local function stop_all_alarms()
|
||||
for i = 1, #alarm_btns do alarm_btns[i].set_value(false) end
|
||||
ttest.stop_alarms()
|
||||
end
|
||||
|
||||
PushButton{parent=alarms,x=3,y=15,text="STOP \x13",min_width=8,fg_bg=cpair(colors.black,colors.red),active_fg_bg=c_wht_gray,callback=stop_all_alarms}
|
||||
|
||||
local states = Div{parent=audio,x=2,y=14,height=5,width=8}
|
||||
|
||||
TextBox{parent=states,text="States",height=1,alignment=ALIGN.CENTER}
|
||||
local t_1 = IndicatorLight{parent=states,label="1",colors=c_blue_gray}
|
||||
local t_2 = IndicatorLight{parent=states,label="2",colors=c_blue_gray}
|
||||
local t_3 = IndicatorLight{parent=states,label="3",colors=c_blue_gray}
|
||||
local t_4 = IndicatorLight{parent=states,label="4",colors=c_blue_gray}
|
||||
local t_5 = IndicatorLight{parent=states,x=6,y=2,label="5",colors=c_blue_gray}
|
||||
local t_6 = IndicatorLight{parent=states,x=6,label="6",colors=c_blue_gray}
|
||||
local t_7 = IndicatorLight{parent=states,x=6,label="7",colors=c_blue_gray}
|
||||
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 }
|
||||
end
|
||||
|
||||
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
|
||||
@@ -11,7 +11,7 @@ local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local WaitingAnim = require("graphics.elements.animations.waiting")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
@@ -29,10 +29,10 @@ local function init(parent, y, is_api)
|
||||
|
||||
if is_api then
|
||||
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)}
|
||||
TextBox{parent=box,text="Connecting to API",alignment=TEXT_ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)}
|
||||
TextBox{parent=box,text="Connecting to API",alignment=ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)}
|
||||
else
|
||||
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.green,style.root.bkg)}
|
||||
TextBox{parent=box,text="Connecting to Supervisor",alignment=TEXT_ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)}
|
||||
TextBox{parent=box,text="Connecting to Supervisor",alignment=ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)}
|
||||
end
|
||||
|
||||
return root
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
-- Pocket GUI Root
|
||||
--
|
||||
|
||||
local coreio = require("pocket.coreio")
|
||||
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 home_page = require("pocket.ui.pages.home_page")
|
||||
local unit_page = require("pocket.ui.pages.unit_page")
|
||||
local reactor_page = require("pocket.ui.pages.reactor_page")
|
||||
local boiler_page = require("pocket.ui.pages.boiler_page")
|
||||
local turbine_page = require("pocket.ui.pages.turbine_page")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
@@ -20,80 +21,73 @@ 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 TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local SignalBar = require("graphics.elements.indicators.signal")
|
||||
|
||||
local LINK_STATE = iocontrol.LINK_STATE
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
-- create new main view
|
||||
---@param main graphics_element main displaybox
|
||||
local function init(main)
|
||||
-- window header message
|
||||
TextBox{parent=main,y=1,text="",alignment=TEXT_ALIGN.LEFT,height=1,fg_bg=style.header}
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
--
|
||||
-- root panel panes (connection screens + main screen)
|
||||
--
|
||||
-- window header message
|
||||
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)}
|
||||
|
||||
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(coreio.core_ps(), "link_state", function (state)
|
||||
if state == coreio.LINK_STATE.UNLINKED or state == coreio.LINK_STATE.API_LINK_ONLY then
|
||||
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 == coreio.LINK_STATE.SV_LINK_ONLY then
|
||||
elseif state == LINK_STATE.SV_LINK_ONLY then
|
||||
root_pane.set_value(2)
|
||||
else
|
||||
root_pane.set_value(3)
|
||||
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)
|
||||
}
|
||||
}
|
||||
home_page(page_div)
|
||||
unit_page(page_div)
|
||||
|
||||
local pane_1 = home_page(page_div)
|
||||
local pane_2 = unit_page(page_div)
|
||||
local pane_3 = reactor_page(page_div)
|
||||
local pane_4 = boiler_page(page_div)
|
||||
local pane_5 = turbine_page(page_div)
|
||||
local panes = { pane_1, pane_2, pane_3, pane_4, pane_5 }
|
||||
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")
|
||||
|
||||
Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=page_pane.set_value}
|
||||
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)})
|
||||
|
||||
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}
|
||||
|
||||
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 TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
-- new boiler page view
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
TextBox{parent=main,text="BOILERS",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
@@ -1,20 +1,64 @@
|
||||
-- local style = require("pocket.ui.style")
|
||||
--
|
||||
-- Main Home Page
|
||||
--
|
||||
|
||||
local core = require("graphics.core")
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
local core = require("graphics.core")
|
||||
|
||||
-- local cpair = core.cpair
|
||||
local AppMultiPane = require("graphics.elements.appmultipane")
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
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()
|
||||
|
||||
TextBox{parent=main,text="HOME",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER}
|
||||
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 TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
-- new reactor page view
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
TextBox{parent=main,text="REACTOR",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
@@ -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 TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
-- new turbine page view
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
TextBox{parent=main,text="TURBINES",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
@@ -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 TEXT_ALIGN = core.TEXT_ALIGN
|
||||
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=TEXT_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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user