Compare commits
550 Commits
v1.8.15-be
...
649-plc-cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a7cb9eaa8 | ||
|
|
3139dc2176 | ||
|
|
25fc0050c3 | ||
|
|
390cf98b0a | ||
|
|
7ddd6f32c5 | ||
|
|
18a488f1b9 | ||
|
|
4c7ad0c539 | ||
|
|
a083f8983b | ||
|
|
f7fe9754fe | ||
|
|
ff68eeae1a | ||
|
|
20f949a9dd | ||
|
|
99c9fec195 | ||
|
|
e7a859438e | ||
|
|
c81c83f432 | ||
|
|
6ad63aedeb | ||
|
|
a1bb5ce50b | ||
|
|
afc89ac727 | ||
|
|
55685fb6a6 | ||
|
|
61f1af7f4e | ||
|
|
16f62bc32a | ||
|
|
a2ec418d53 | ||
|
|
384ebb461f | ||
|
|
0392385037 | ||
|
|
9e020b2852 | ||
|
|
4f7285573f | ||
|
|
59f99f70a4 | ||
|
|
df9f1195e3 | ||
|
|
aba79e88cf | ||
|
|
d0276e149b | ||
|
|
eb95f2331d | ||
|
|
28150042cc | ||
|
|
4bc5af46ab | ||
|
|
2cee1ea895 | ||
|
|
7d0bbafd6c | ||
|
|
dc19127836 | ||
|
|
6db6a7d7b7 | ||
|
|
017deec06e | ||
|
|
d0401fe51f | ||
|
|
92113671ff | ||
|
|
e3d0692dcc | ||
|
|
83e29abea7 | ||
|
|
4a1730ec47 | ||
|
|
691b781c52 | ||
|
|
3b856655c3 | ||
|
|
415cb71294 | ||
|
|
a678b8dbe0 | ||
|
|
9bb3f59496 | ||
|
|
170cba702c | ||
|
|
fe78360948 | ||
|
|
e29a88eeea | ||
|
|
f3eb6d0464 | ||
|
|
f4d4de659c | ||
|
|
1ce0bbfc65 | ||
|
|
0bfe767710 | ||
|
|
4fb39213f2 | ||
|
|
acaa9369f4 | ||
|
|
2998371b89 | ||
|
|
12664c6190 | ||
|
|
abe0c45534 | ||
|
|
a104d8ba83 | ||
|
|
a629c04d11 | ||
|
|
6d3b35a41d | ||
|
|
e1ac42f5f8 | ||
|
|
9e59883a84 | ||
|
|
79d63fce78 | ||
|
|
ce92fd15ef | ||
|
|
264edc0030 | ||
|
|
fcb17ae5e7 | ||
|
|
35f82af2e2 | ||
|
|
1a2ecd0599 | ||
|
|
5f8c947105 | ||
|
|
41e6d89a4b | ||
|
|
f01fb62863 | ||
|
|
8f6425b814 | ||
|
|
069a7ce0ad | ||
|
|
8eff1c0d76 | ||
|
|
7404e6da31 | ||
|
|
12ead136a3 | ||
|
|
e3dbda3c54 | ||
|
|
41b6a558d5 | ||
|
|
07bb0f13e3 | ||
|
|
f1014ce941 | ||
|
|
0debbdc167 | ||
|
|
0a26629e20 | ||
|
|
9393b1830d | ||
|
|
0df1e48780 | ||
|
|
b8c30ba8a4 | ||
|
|
eafd39fa35 | ||
|
|
86dc92f09a | ||
|
|
e6f5ab8ef4 | ||
|
|
be462db50b | ||
|
|
1dc3d82e59 | ||
|
|
fa2a6d7786 | ||
|
|
04c53c7074 | ||
|
|
1af2cdba8d | ||
|
|
0d7302dc8e | ||
|
|
48ec973695 | ||
|
|
ee868eb607 | ||
|
|
e4da9a62d9 | ||
|
|
c8910bfc40 | ||
|
|
d6e3a67562 | ||
|
|
f7c0a1d97d | ||
|
|
13509136b8 | ||
|
|
bfab2d6af2 | ||
|
|
ae055a7d99 | ||
|
|
592f1110ed | ||
|
|
97875f4e52 | ||
|
|
657261642c | ||
|
|
0da944c3ea | ||
|
|
1b692b5b9a | ||
|
|
b4a9366f73 | ||
|
|
2b3099ac59 | ||
|
|
cd654fb9b8 | ||
|
|
ad834218c2 | ||
|
|
c6a7de2669 | ||
|
|
d374967cb7 | ||
|
|
1971153dae | ||
|
|
5fc8912590 | ||
|
|
122fa1a7a7 | ||
|
|
2b73196130 | ||
|
|
d45f19c8a6 | ||
|
|
a9f68ce3ea | ||
|
|
7ab5ea710f | ||
|
|
de41ee56aa | ||
|
|
99ea59a86b | ||
|
|
234652b886 | ||
|
|
e37e3ba696 | ||
|
|
20b71bead1 | ||
|
|
18d093e72d | ||
|
|
21eae4932f | ||
|
|
9163fb14c4 | ||
|
|
02db01524c | ||
|
|
e0d1eb3445 | ||
|
|
7c22c172d5 | ||
|
|
7b29702000 | ||
|
|
425a6c8775 | ||
|
|
eafcd89aba | ||
|
|
016cd988e1 | ||
|
|
06a8e3d9ca | ||
|
|
5f22069ce1 | ||
|
|
ecdaf78ed0 | ||
|
|
3b2fb00285 | ||
|
|
54167e2113 | ||
|
|
22cdbc8638 | ||
|
|
556331f75b | ||
|
|
40cb9f599a | ||
|
|
cab3427c70 | ||
|
|
4e31b33b09 | ||
|
|
f32855084e | ||
|
|
b3cf40a01a | ||
|
|
cbc84c5998 | ||
|
|
869e67710f | ||
|
|
1b9d3d3f23 | ||
|
|
0a060b656c | ||
|
|
c859c22964 | ||
|
|
3767c0f8d9 | ||
|
|
fbebc2a021 | ||
|
|
afd6800be6 | ||
|
|
baba2e1411 | ||
|
|
127c878794 | ||
|
|
767b54c3e6 | ||
|
|
1c57fc1fe3 | ||
|
|
2d83de8b88 | ||
|
|
4a4234c8c8 | ||
|
|
eb197e7fdd | ||
|
|
78b0e1bf24 | ||
|
|
cbc004a6c7 | ||
|
|
d05abf6e00 | ||
|
|
813e30bcde | ||
|
|
fb139949f8 | ||
|
|
2fdc9feea7 | ||
|
|
eabb065d17 | ||
|
|
fb221a566c | ||
|
|
cd4caf0163 | ||
|
|
1190fe2dd5 | ||
|
|
4cb6f9ca0f | ||
|
|
872082b970 | ||
|
|
071df9e431 | ||
|
|
ae85cfc579 | ||
|
|
1dece587b2 | ||
|
|
01c5d62f38 | ||
|
|
c6a5d487e0 | ||
|
|
ba4a5aa85e | ||
|
|
11fa9f625d | ||
|
|
b61fd2c620 | ||
|
|
cb2ebd409d | ||
|
|
57c75be997 | ||
|
|
22a7fdae88 | ||
|
|
5a9768f005 | ||
|
|
fd414a814c | ||
|
|
909bd78912 | ||
|
|
c487b22fe1 | ||
|
|
9892fbc602 | ||
|
|
1695b58329 | ||
|
|
178681941f | ||
|
|
de3fa163c5 | ||
|
|
68977bcdea | ||
|
|
c4c45ae329 | ||
|
|
e8b8dfde5b | ||
|
|
3f42adea5b | ||
|
|
feabed6a1e | ||
|
|
ffd4bae2d5 | ||
|
|
bc4228d4eb | ||
|
|
4501cb783f | ||
|
|
78225a8cf4 | ||
|
|
9b443709f4 | ||
|
|
33803a1ace | ||
|
|
fe8ac349d6 | ||
|
|
1538fb3d26 | ||
|
|
a546b946ee | ||
|
|
019284de7b | ||
|
|
849caa2521 | ||
|
|
6838d21bd7 | ||
|
|
6bd43af5c0 | ||
|
|
7eebf0524f | ||
|
|
20bffec79f | ||
|
|
49b545ba2c | ||
|
|
e54ecf43ed | ||
|
|
0544587d84 | ||
|
|
72fcc01acd | ||
|
|
7372908637 | ||
|
|
50b2f62c66 | ||
|
|
68851a6b30 | ||
|
|
56e4f93db8 | ||
|
|
bc7a38b9d4 | ||
|
|
8bdb6b9ed6 | ||
|
|
8469bb78a3 | ||
|
|
fc603677ef | ||
|
|
532c15e258 | ||
|
|
7b6b1de539 | ||
|
|
edde416889 | ||
|
|
8fad94c4c6 | ||
|
|
3e1f567c0f | ||
|
|
bafd20ec22 | ||
|
|
21591f4d7d | ||
|
|
b15835ab87 | ||
|
|
d36f7adab1 | ||
|
|
8439e02586 | ||
|
|
764638c212 | ||
|
|
459ddbaef8 | ||
|
|
627dd99dd7 | ||
|
|
129bf8809a | ||
|
|
55f6e4756e | ||
|
|
e27d5eeb85 | ||
|
|
661bef063c | ||
|
|
801fd99448 | ||
|
|
c1c3723b67 | ||
|
|
7fb88becb8 | ||
|
|
051d119b99 | ||
|
|
21a3a18764 | ||
|
|
91cb51bad9 | ||
|
|
8ddc233da0 | ||
|
|
e847505ac2 | ||
|
|
0b14a01784 | ||
|
|
b7969d2cd7 | ||
|
|
9ecff2fa2b | ||
|
|
dbe5ee1f54 | ||
|
|
7bb49c51c8 | ||
|
|
620fa362f6 | ||
|
|
0639870410 | ||
|
|
440989aed6 | ||
|
|
48e5c50f0a | ||
|
|
c780cd2664 | ||
|
|
60ff22f57d | ||
|
|
c0b7d7e13c | ||
|
|
30f37c0ef9 | ||
|
|
4bd64e71bf | ||
|
|
da87745996 | ||
|
|
8b9f83754b | ||
|
|
40e749d363 | ||
|
|
38a1a4282c | ||
|
|
41843a2478 | ||
|
|
75a3b82f31 | ||
|
|
eae2dfef60 | ||
|
|
26906d10d6 | ||
|
|
89ab742f8e | ||
|
|
10b675d84d | ||
|
|
0497ec44e9 | ||
|
|
eea3a8f7d0 | ||
|
|
46d19c180b | ||
|
|
8428b68f77 | ||
|
|
f61791427d | ||
|
|
acb5a1cbf9 | ||
|
|
393be2acec | ||
|
|
4e5858bd2d | ||
|
|
f1a13f1125 | ||
|
|
65609ddaa2 | ||
|
|
8238b26eec | ||
|
|
9521acd8af | ||
|
|
749c84490b | ||
|
|
519fae3a27 | ||
|
|
2ccba197c7 | ||
|
|
6a04354964 | ||
|
|
60c4cc2f80 | ||
|
|
966ca94775 | ||
|
|
35bbd14cbc | ||
|
|
316dc5819f | ||
|
|
4b188bef8f | ||
|
|
00157cc45e | ||
|
|
499ec7c5b0 | ||
|
|
cc3b04a184 | ||
|
|
85df0d61d5 | ||
|
|
4e3330d4b3 | ||
|
|
a873c921c0 | ||
|
|
7b3147008e | ||
|
|
2579feaf32 | ||
|
|
17e53fdba2 | ||
|
|
ec1fc13ae7 | ||
|
|
69855af861 | ||
|
|
e4cb1f6c70 | ||
|
|
bc2ae291a7 | ||
|
|
a4a59d4a3d | ||
|
|
741dd2467f | ||
|
|
2b2ca237cb | ||
|
|
6acd6b161c | ||
|
|
a1efca3fc8 | ||
|
|
b766488dea | ||
|
|
851d481b76 | ||
|
|
2becaeccd7 | ||
|
|
d41eb3aaeb | ||
|
|
0daf314918 | ||
|
|
b15c60fdab | ||
|
|
7df060e1fb | ||
|
|
a80c2a4cc5 | ||
|
|
525330ab59 | ||
|
|
5d1379d60d | ||
|
|
1d53241b82 | ||
|
|
2047794173 | ||
|
|
ec2921e393 | ||
|
|
85fc8d2920 | ||
|
|
63a9e23b3a | ||
|
|
f1b7bac6f9 | ||
|
|
c3ccd051dc | ||
|
|
0bf7b8204d | ||
|
|
033bcdb9e3 | ||
|
|
b2e5ced54d | ||
|
|
a1dbc15d16 | ||
|
|
3003e57531 | ||
|
|
fb56634fc4 | ||
|
|
48fa715aaa | ||
|
|
753f062bfc | ||
|
|
356657c9c0 | ||
|
|
a4452ebbd2 | ||
|
|
35134822a9 | ||
|
|
f56d68d972 | ||
|
|
06933b2fb7 | ||
|
|
a1494b4afd | ||
|
|
2933b24318 | ||
|
|
402d8607b6 | ||
|
|
2e978db859 | ||
|
|
a7b3a2a0b8 | ||
|
|
6ff096fd31 | ||
|
|
13bb6cb026 | ||
|
|
5b311fcfbc | ||
|
|
d6a9f9c5f3 | ||
|
|
8ffbbb5ac9 | ||
|
|
bf10b3241e | ||
|
|
ab11ff03b5 | ||
|
|
66fae0695c | ||
|
|
dbd79cbc4f | ||
|
|
b5b67b425a | ||
|
|
f8bd79a234 | ||
|
|
07c3b3ec63 | ||
|
|
d7ea68ed3a | ||
|
|
db94ac7ff5 | ||
|
|
ee922a3aed | ||
|
|
75c77cc5b5 | ||
|
|
7683293c5e | ||
|
|
672a9c8dd1 | ||
|
|
3c10e28d03 | ||
|
|
035a26cc07 | ||
|
|
097edc5bf9 | ||
|
|
8a0d05c94b | ||
|
|
fbbd7e1ccd | ||
|
|
0f40c1d7f2 | ||
|
|
c299dce8ef | ||
|
|
11e9c11cf7 | ||
|
|
61ff055d60 | ||
|
|
f4be6519e8 | ||
|
|
705494bb7e | ||
|
|
610fb12bb3 | ||
|
|
6b20445446 | ||
|
|
f93db02793 | ||
|
|
fe1b916b1f | ||
|
|
ebeeecc5ab | ||
|
|
dbabcd13b0 | ||
|
|
acc8e1c058 | ||
|
|
5a38acf2a7 | ||
|
|
b3be2d4bfc | ||
|
|
0ab2d57b66 | ||
|
|
183af8a5ca | ||
|
|
6f63092d4b | ||
|
|
a087eda0ee | ||
|
|
a1b6ff4bcc | ||
|
|
8c6b264f6b | ||
|
|
8a5c468606 | ||
|
|
12f187f596 | ||
|
|
01a1c374ab | ||
|
|
465875b287 | ||
|
|
45d4b4e653 | ||
|
|
fc7441b2f6 | ||
|
|
6917697290 | ||
|
|
c323967b6a | ||
|
|
4775639245 | ||
|
|
072613959c | ||
|
|
f259f85a99 | ||
|
|
e076e327d8 | ||
|
|
f34747372f | ||
|
|
affe2d6c6d | ||
|
|
5597ea2097 | ||
|
|
0f4a8b6dfc | ||
|
|
ab97f8935d | ||
|
|
bee96ed12e | ||
|
|
50bd59781e | ||
|
|
196e0b1daf | ||
|
|
bcc55628cf | ||
|
|
9bffd6feee | ||
|
|
1500004481 | ||
|
|
2904621e81 | ||
|
|
08eee198c8 | ||
|
|
e750ffe69d | ||
|
|
de6d8a89ca | ||
|
|
f00751edeb | ||
|
|
d58a6a3369 | ||
|
|
340c6689a9 | ||
|
|
7cc088ca95 | ||
|
|
01f6b1e190 | ||
|
|
3ffc79b181 | ||
|
|
8e4bb583a8 | ||
|
|
ec107929bc | ||
|
|
3406d12681 | ||
|
|
03bbf8a891 | ||
|
|
b61867be3c | ||
|
|
1358d95269 | ||
|
|
fd06730e46 | ||
|
|
fb5f3b9474 | ||
|
|
3afc1e6cfa | ||
|
|
715765d442 | ||
|
|
3762e9dced | ||
|
|
022d1f9f49 | ||
|
|
b1da76c2f6 | ||
|
|
0364b4df7b | ||
|
|
e04bd032fe | ||
|
|
34fe5dc382 | ||
|
|
1f8ea56095 | ||
|
|
f2cd98c57a | ||
|
|
1e341af8a5 | ||
|
|
2fb3d9b515 | ||
|
|
220f9d152f | ||
|
|
604b4a1927 | ||
|
|
0ecaa42a7f | ||
|
|
9614407c37 | ||
|
|
f1c4f8c00a | ||
|
|
8a409f0313 | ||
|
|
da68398fa4 | ||
|
|
375e969161 | ||
|
|
c93cd4d0bd | ||
|
|
e69cbc8633 | ||
|
|
ea1bcbf81c | ||
|
|
f13f03fddc | ||
|
|
72a480e475 | ||
|
|
ec1b56b853 | ||
|
|
c5fb299f55 | ||
|
|
63c990a3cf | ||
|
|
89e84f9711 | ||
|
|
270aeb13ca | ||
|
|
df8c71f12e | ||
|
|
347f67c8ee | ||
|
|
8de2d7071e | ||
|
|
d424cf74d3 | ||
|
|
a1b571d7c0 | ||
|
|
8e0e4df3eb | ||
|
|
b025958173 | ||
|
|
f83eecf2e2 | ||
|
|
0b2f7b13a1 | ||
|
|
3ad3cbb4eb | ||
|
|
e868fd3397 | ||
|
|
a4add9370c | ||
|
|
4f48ba8abc | ||
|
|
3cc6781844 | ||
|
|
c05a45c29a | ||
|
|
f2937b47e9 | ||
|
|
8e14fa1591 | ||
|
|
aebb9f42be | ||
|
|
2de30ef064 | ||
|
|
8dedb092e7 | ||
|
|
807e575580 | ||
|
|
55dc203cdd | ||
|
|
a15cbadd32 | ||
|
|
bc76c01aa5 | ||
|
|
d2bc4f6bc0 | ||
|
|
4cdbe3b07f | ||
|
|
897a3ed22d | ||
|
|
2bc20ec312 | ||
|
|
fc42049aa0 | ||
|
|
4a7028f401 | ||
|
|
006c5e6adf | ||
|
|
f64db66448 | ||
|
|
a8e6bc0e35 | ||
|
|
4a39ed9d38 | ||
|
|
9fe0669fda | ||
|
|
219f02b188 | ||
|
|
ea8f62dea6 | ||
|
|
1c719ad67b | ||
|
|
87a91e309d | ||
|
|
c66ad44adb | ||
|
|
14736e414f | ||
|
|
fb1f85a626 | ||
|
|
697a3d6f6b | ||
|
|
00cacd6d0a | ||
|
|
0b97d4d4b0 | ||
|
|
def5b49327 | ||
|
|
38457cfbbc | ||
|
|
e851a5275f | ||
|
|
5848c2ac1a | ||
|
|
f8a5dd9c32 | ||
|
|
ff8ae5e609 | ||
|
|
95b93eb795 | ||
|
|
356caf7b4d | ||
|
|
09c44a6969 | ||
|
|
83ba6e3961 | ||
|
|
e88e1afcc4 | ||
|
|
b457edbc71 | ||
|
|
5e70e4131e | ||
|
|
375b7f680e | ||
|
|
e37b8758cd | ||
|
|
9404b50a8c | ||
|
|
58fb35e85b | ||
|
|
b9030d6bed | ||
|
|
25ebf2c8c7 | ||
|
|
4d87887709 | ||
|
|
b63a17bda0 | ||
|
|
39233dae8a | ||
|
|
a2af0d3829 | ||
|
|
db901129f9 | ||
|
|
c1c49ea3fb | ||
|
|
be6c3755a4 | ||
|
|
ac2d189c1a | ||
|
|
0fa0324940 | ||
|
|
3181ab96f1 | ||
|
|
30c9215658 | ||
|
|
946c28c929 | ||
|
|
e6d6353d05 | ||
|
|
0e81391144 | ||
|
|
b18cadb53e | ||
|
|
9a500d8f96 | ||
|
|
a268a770f2 | ||
|
|
b96eb7d89d | ||
|
|
9b8947fba2 | ||
|
|
43b44cc425 |
4
.github/workflows/check.yml
vendored
4
.github/workflows/check.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3.5.1
|
||||
uses: actions/checkout@v4
|
||||
- name: Luacheck
|
||||
uses: lunarmodules/luacheck@v1.1.0
|
||||
with:
|
||||
@@ -26,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 keys parallel periphemu peripheral read rs settings shell term textutils window
|
||||
args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* --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
@@ -29,13 +29,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v3.1.3
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
# Generate manifest + shields files for main branch
|
||||
- name: Checkout main
|
||||
id: checkout-main
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: 'main'
|
||||
clean: false
|
||||
@@ -46,7 +48,7 @@ jobs:
|
||||
- name: Generate manifest and shields for main branch
|
||||
id: manifest-main
|
||||
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
|
||||
run: python imgen.py shields
|
||||
run: python build/imgen.py shields
|
||||
- name: Save main's manifest
|
||||
if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }}
|
||||
run: mv install_manifest.json deploy/manifests/main
|
||||
@@ -54,14 +56,14 @@ jobs:
|
||||
- name: Checkout devel
|
||||
id: checkout-devel
|
||||
if: success() || failure()
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: 'devel'
|
||||
clean: false
|
||||
- name: Generate manifest for devel
|
||||
id: manifest-devel
|
||||
if: ${{ (success() || failure()) && steps.checkout-devel.outcome == 'success' }}
|
||||
run: python imgen.py
|
||||
run: python build/imgen.py
|
||||
- name: Save devel's manifest
|
||||
if: ${{ (success() || failure()) && steps.manifest-devel.outcome == 'success' }}
|
||||
run: mv install_manifest.json deploy/manifests/devel
|
||||
@@ -69,11 +71,11 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
id: upload-artifacts
|
||||
if: ${{ (success() || failure()) && (steps.manifest-main.outcome == 'success' || steps.manifest-latest.outcome == 'success' || steps.manifest-devel.outcome == 'success') }}
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
# Upload manifest JSON
|
||||
path: 'deploy/'
|
||||
- name: Deploy to GitHub Pages
|
||||
if: ${{ (success() || failure()) && steps.upload-artifacts.outcome == 'success' }}
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,2 @@
|
||||
_notes/
|
||||
_*/
|
||||
/*program.sh
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright © 2022 - 2024 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
|
||||
|
||||
118
build/_offline.lua
Normal file
118
build/_offline.lua
Normal file
@@ -0,0 +1,118 @@
|
||||
---@diagnostic disable: undefined-global
|
||||
-- luacheck: push ignore install_manifest ccmsi_offline app_files dep_files lgray green white
|
||||
|
||||
local b64_lookup = {
|
||||
['A'] = 0, ['B'] = 1, ['C'] = 2, ['D'] = 3, ['E'] = 4, ['F'] = 5, ['G'] = 6, ['H'] = 7, ['I'] = 8, ['J'] = 9, ['K'] = 10, ['L'] = 11, ['M'] = 12, ['N'] = 13, ['O'] = 14, ['P'] = 15, ['Q'] = 16, ['R'] = 17, ['S'] = 18, ['T'] = 19, ['U'] = 20, ['V'] = 21, ['W'] = 22, ['X'] = 23, ['Y'] = 24, ['Z'] = 25,
|
||||
['a'] = 26, ['b'] = 27, ['c'] = 28, ['d'] = 29, ['e'] = 30, ['f'] = 31, ['g'] = 32, ['h'] = 33, ['i'] = 34, ['j'] = 35, ['k'] = 36, ['l'] = 37, ['m'] = 38, ['n'] = 39, ['o'] = 40, ['p'] = 41, ['q'] = 42, ['r'] = 43, ['s'] = 44, ['t'] = 45, ['u'] = 46, ['v'] = 47, ['w'] = 48, ['x'] = 49, ['y'] = 50, ['z'] = 51,
|
||||
['0'] = 52, ['1'] = 53, ['2'] = 54, ['3'] = 55, ['4'] = 56, ['5'] = 57, ['6'] = 58, ['7'] = 59, ['8'] = 60, ['9'] = 61, ['+'] = 62, ['/'] = 63
|
||||
}
|
||||
|
||||
local BYTE = 0xFF
|
||||
local CHAR = string.char
|
||||
local BOR = bit.bor ---@type function
|
||||
local BAND = bit.band ---@type function
|
||||
local LSHFT = bit.blshift ---@type function
|
||||
local RSHFT = bit.blogic_rshift ---@type function
|
||||
|
||||
-- decode a base64 string
|
||||
---@param input string
|
||||
local function b64_decode(input)
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local t_start = os.epoch("local")
|
||||
|
||||
local decoded = {}
|
||||
|
||||
local c_idx, idx = 1, 1
|
||||
|
||||
for _ = 1, input:len() / 4 do
|
||||
local block = input:sub(idx, idx + 4)
|
||||
local word = 0x0
|
||||
|
||||
-- build the 24-bit sequence from the 4 characters
|
||||
for i = 1, 4 do
|
||||
local num = b64_lookup[block:sub(i, i)]
|
||||
|
||||
if num then
|
||||
word = BOR(word, LSHFT(b64_lookup[block:sub(i, i)], (4 - i) * 6))
|
||||
end
|
||||
end
|
||||
|
||||
-- decode the 24-bit sequence as 8 bytes
|
||||
for i = 1, 3 do
|
||||
local char = BAND(BYTE, RSHFT(word, (3 - i) * 8))
|
||||
|
||||
if char ~= 0 then
|
||||
decoded[c_idx] = CHAR(char)
|
||||
c_idx = c_idx + 1
|
||||
end
|
||||
end
|
||||
|
||||
idx = idx + 4
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local elapsed = (os.epoch("local") - t_start)
|
||||
local decoded_str = table.concat(decoded)
|
||||
|
||||
return decoded_str, elapsed
|
||||
end
|
||||
|
||||
-- write files recursively from base64 encodings in a table
|
||||
---@param files table
|
||||
---@param path string
|
||||
local function write_files(files, path)
|
||||
fs.makeDir(path)
|
||||
|
||||
for k, v in pairs(files) do
|
||||
if type(v) == "table" then
|
||||
if k == "system" then
|
||||
-- write system files to root
|
||||
write_files(v, "/")
|
||||
else
|
||||
-- descend into directories
|
||||
write_files(v, path .. "/" .. k .. "/")
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
os.sleep(0.05)
|
||||
else
|
||||
local handle = fs.open(path .. k, "w")
|
||||
local text, time = b64_decode(v)
|
||||
|
||||
print("decoded '" .. k .. "' in " .. time .. "ms")
|
||||
|
||||
handle.write(text)
|
||||
handle.close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- write installation manifiest and offline install manager
|
||||
local function write_install()
|
||||
local handle = fs.open("install_manifest.json", "w")
|
||||
handle.write(b64_decode(install_manifest))
|
||||
handle.close()
|
||||
|
||||
handle = fs.open("ccmsim.lua", "w")
|
||||
handle.write(b64_decode(ccmsi_offline))
|
||||
handle.close()
|
||||
end
|
||||
|
||||
lgray()
|
||||
|
||||
-- write both app and dependency files
|
||||
write_files(app_files, "/")
|
||||
write_files(dep_files, "/")
|
||||
|
||||
-- write an install manifest and the offline installer
|
||||
write_install()
|
||||
|
||||
green()
|
||||
print("Done!")
|
||||
white()
|
||||
print("All files have been installed. The app can be started with 'startup' and configured with 'configure'.")
|
||||
lgray()
|
||||
print("Hint: You can use 'ccmsim' to manage your off-line installation.")
|
||||
white()
|
||||
|
||||
--luacheck: pop
|
||||
213
build/bundle.py
Normal file
213
build/bundle.py
Normal file
@@ -0,0 +1,213 @@
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
path_prefix = "./_minified/"
|
||||
|
||||
# get git build info
|
||||
build = subprocess.check_output(["git", "describe", "--tags"]).strip().decode('UTF-8')
|
||||
|
||||
# list files in a directory
|
||||
def list_files(path):
|
||||
list = []
|
||||
|
||||
for (root, dirs, files) in os.walk(path):
|
||||
for f in files:
|
||||
list.append((root[2:] + "/" + f).replace('\\','/'))
|
||||
|
||||
return list
|
||||
|
||||
# recursively encode files with base64
|
||||
def encode_recursive(path):
|
||||
list = {}
|
||||
|
||||
for item in os.listdir(path):
|
||||
item_path = path + '/' + item
|
||||
|
||||
if os.path.isfile(item_path):
|
||||
handle = open(item_path, 'r')
|
||||
list[item] = base64.b64encode(bytes(handle.read(), 'UTF-8')).decode('ASCII')
|
||||
handle.close()
|
||||
else:
|
||||
list[item] = encode_recursive(item_path)
|
||||
|
||||
return list
|
||||
|
||||
# encode listed files with base64
|
||||
def encode_files(files):
|
||||
list = {}
|
||||
|
||||
for item in files:
|
||||
item_path = path_prefix + './' + item
|
||||
|
||||
handle = open(item_path, 'r')
|
||||
list[item] = base64.b64encode(bytes(handle.read(), 'UTF-8')).decode('ASCII')
|
||||
handle.close()
|
||||
|
||||
return list
|
||||
|
||||
# get the version of an application at the provided path
|
||||
def get_version(path, is_lib = False):
|
||||
ver = ""
|
||||
string = ".version = \""
|
||||
|
||||
if not is_lib:
|
||||
string = "_VERSION = \""
|
||||
|
||||
f = open(path, "r")
|
||||
|
||||
for line in f:
|
||||
pos = line.find(string)
|
||||
if pos >= 0:
|
||||
ver = line[(pos + len(string)):(len(line) - 2)]
|
||||
break
|
||||
|
||||
f.close()
|
||||
|
||||
return ver
|
||||
|
||||
# file manifest (reflects imgen.py)
|
||||
manifest = {
|
||||
"common_versions" : {
|
||||
"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),
|
||||
},
|
||||
"app_versions" : {
|
||||
"reactor-plc" : get_version("./reactor-plc/startup.lua"),
|
||||
"rtu" : get_version("./rtu/startup.lua"),
|
||||
"supervisor" : get_version("./supervisor/startup.lua"),
|
||||
"coordinator" : get_version("./coordinator/startup.lua"),
|
||||
"pocket" : get_version("./pocket/startup.lua")
|
||||
},
|
||||
"files" : {
|
||||
# common files
|
||||
"system" : encode_files([ "initenv.lua", "startup.lua", "configure.lua", "LICENSE" ]),
|
||||
"scada-common" : encode_recursive(path_prefix + "./scada-common"),
|
||||
"graphics" : encode_recursive(path_prefix + "./graphics"),
|
||||
"lockbox" : encode_recursive(path_prefix + "./lockbox"),
|
||||
# platform files
|
||||
"reactor-plc" : encode_recursive(path_prefix + "./reactor-plc"),
|
||||
"rtu" : encode_recursive(path_prefix + "./rtu"),
|
||||
"supervisor" : encode_recursive(path_prefix + "./supervisor"),
|
||||
"coordinator" : encode_recursive(path_prefix + "./coordinator"),
|
||||
"pocket" : encode_recursive(path_prefix + "./pocket"),
|
||||
},
|
||||
"install_files" : {
|
||||
# common files
|
||||
"system" : [ "initenv.lua", "startup.lua", "configure.lua", "LICENSE" ],
|
||||
"scada-common" : list_files("./scada-common"),
|
||||
"graphics" : list_files("./graphics"),
|
||||
"lockbox" : list_files("./lockbox"),
|
||||
# platform files
|
||||
"reactor-plc" : list_files("./reactor-plc"),
|
||||
"rtu" : list_files("./rtu"),
|
||||
"supervisor" : list_files("./supervisor"),
|
||||
"coordinator" : list_files("./coordinator"),
|
||||
"pocket" : list_files("./pocket"),
|
||||
},
|
||||
"depends" : [ "system", "scada-common", "graphics", "lockbox" ]
|
||||
}
|
||||
|
||||
# write the application installation items as Lua tables
|
||||
def write_items(body, items, indent):
|
||||
indent_str = " " * indent
|
||||
for key, value in items.items():
|
||||
if isinstance(value, str):
|
||||
body = body + f"{indent_str}['{key}'] = \"{value}\",\n"
|
||||
else:
|
||||
body = body + f"{indent_str}['{key}'] = {{\n"
|
||||
body = write_items(body, value, indent + 4)
|
||||
body = body + f"{indent_str}}},\n"
|
||||
|
||||
return body
|
||||
|
||||
# create output directory
|
||||
if not os.path.exists("./BUNDLE"):
|
||||
os.makedirs("./BUNDLE")
|
||||
|
||||
# get offline installer
|
||||
ccmsim_file = open("./build/ccmsim.lua", "r")
|
||||
ccmsim_script = ccmsim_file.read()
|
||||
ccmsim_file.close()
|
||||
|
||||
# create dependency bundled file
|
||||
dep_file = "common_" + build + ".lua"
|
||||
f_d = open("./BUNDLE/" + dep_file, "w")
|
||||
|
||||
body_b = "local dep_files = {\n"
|
||||
|
||||
for depend in manifest["depends"]:
|
||||
body_b = body_b + write_items("", { f"{depend}": manifest["files"][depend] }, 4)
|
||||
body_b = body_b + "}\n"
|
||||
|
||||
body_b = body_b + f"""
|
||||
if select("#", ...) == 0 then
|
||||
term.setTextColor(colors.red)
|
||||
print("You must run the other file you should have uploaded (it has the app in its name).")
|
||||
term.setTextColor(colors.white)
|
||||
end
|
||||
|
||||
return dep_files
|
||||
"""
|
||||
|
||||
f_d.write(body_b)
|
||||
f_d.close()
|
||||
|
||||
# application bundled files
|
||||
for app in [ "reactor-plc", "rtu", "supervisor", "coordinator", "pocket" ]:
|
||||
app_file = app + "_" + build + ".lua"
|
||||
|
||||
f_script = open("./build/_offline.lua", "r")
|
||||
script = f_script.read()
|
||||
f_script.close()
|
||||
|
||||
f_a = open("./BUNDLE/" + app_file, "w")
|
||||
|
||||
body_a = "local app_files = {\n"
|
||||
|
||||
body_a = body_a + write_items("", { f"{app}": manifest["files"][app] }, 4) + "}\n"
|
||||
|
||||
versions = manifest["common_versions"].copy()
|
||||
versions[app] = manifest["app_versions"][app]
|
||||
|
||||
depends = manifest["depends"].copy()
|
||||
depends.append(app)
|
||||
|
||||
install_manifest = json.dumps({ "versions" : versions, "files" : manifest["install_files"], "depends" : depends })
|
||||
|
||||
body_a = body_a + f"""
|
||||
-- install manifest JSON and offline installer
|
||||
local install_manifest = "{base64.b64encode(bytes(install_manifest, 'UTF-8')).decode('ASCII')}"
|
||||
local ccmsi_offline = "{base64.b64encode(bytes(ccmsim_script, 'UTF-8')).decode('ASCII')}"
|
||||
|
||||
local function red() term.setTextColor(colors.red) end
|
||||
local function green() term.setTextColor(colors.green) end
|
||||
local function white() term.setTextColor(colors.white) end
|
||||
local function lgray() term.setTextColor(colors.lightGray) end
|
||||
|
||||
if not fs.exists("{dep_file}") then
|
||||
red()
|
||||
print("Missing '{dep_file}'! Please upload it, then run this file again.")
|
||||
white()
|
||||
return
|
||||
end
|
||||
|
||||
-- rename the dependency file
|
||||
fs.move("{dep_file}", "install_depends.lua")
|
||||
|
||||
-- load the other file
|
||||
local dep_files = require("install_depends")
|
||||
|
||||
-- delete the uploaded files to free up space to actually install
|
||||
fs.delete("{app_file}")
|
||||
fs.delete("install_depends.lua")
|
||||
|
||||
-- get started installing
|
||||
{script}"""
|
||||
|
||||
f_a.write(body_a)
|
||||
f_a.close()
|
||||
237
build/ccmsim.lua
Normal file
237
build/ccmsim.lua
Normal file
@@ -0,0 +1,237 @@
|
||||
local function println(message) print(tostring(message)) end
|
||||
local function print(message) term.write(tostring(message)) end
|
||||
|
||||
local opts = { ... }
|
||||
local mode, app
|
||||
|
||||
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 blue() term.setTextColor(colors.blue) end
|
||||
local function white() term.setTextColor(colors.white) end
|
||||
local function lgray() term.setTextColor(colors.lightGray) end
|
||||
|
||||
-- get command line option in list
|
||||
local function get_opt(opt, options)
|
||||
for _, v in pairs(options) do if opt == v then return v end end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- wait for any key to be pressed
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local function any_key() os.pullEvent("key_up") end
|
||||
|
||||
-- ask the user yes or no
|
||||
local function ask_y_n(question, default)
|
||||
print(question)
|
||||
if default == true then print(" (Y/n)? ") else print(" (y/N)? ") end
|
||||
local response = read();any_key()
|
||||
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
|
||||
|
||||
-- read the local manifest file
|
||||
local function read_local_manifest()
|
||||
local local_ok = false
|
||||
local local_manifest = {}
|
||||
local imfile = fs.open("install_manifest.json", "r")
|
||||
if imfile ~= nil then
|
||||
local_ok, local_manifest = pcall(function () return textutils.unserializeJSON(imfile.readAll()) end)
|
||||
imfile.close()
|
||||
end
|
||||
return local_ok, local_manifest
|
||||
end
|
||||
|
||||
-- recursively build a tree out of the file manifest
|
||||
local function gen_tree(manifest, log)
|
||||
local function _tree_add(tree, split)
|
||||
if #split > 1 then
|
||||
local name = table.remove(split, 1)
|
||||
if tree[name] == nil then tree[name] = {} end
|
||||
table.insert(tree[name], _tree_add(tree[name], split))
|
||||
else return split[1] end
|
||||
return nil
|
||||
end
|
||||
|
||||
local list, tree = { log }, {}
|
||||
|
||||
-- make a list of each and every file
|
||||
for _, files in pairs(manifest.files) do for i = 1, #files do table.insert(list, files[i]) end end
|
||||
|
||||
for i = 1, #list do
|
||||
local split = {}
|
||||
---@diagnostic disable-next-line: discard-returns
|
||||
string.gsub(list[i], "([^/]+)", function(c) split[#split + 1] = c end)
|
||||
if #split == 1 then table.insert(tree, list[i])
|
||||
else table.insert(tree, _tree_add(tree, split)) end
|
||||
end
|
||||
|
||||
return tree
|
||||
end
|
||||
|
||||
local function _in_array(val, array)
|
||||
for _, v in pairs(array) do if v == val then return true end end
|
||||
return false
|
||||
end
|
||||
|
||||
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
|
||||
if fs.isDir(path) then
|
||||
_clean_dir(path, tree[val])
|
||||
if #fs.list(path) == 0 then fs.delete(path);println("deleted "..path) end
|
||||
elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then
|
||||
fs.delete(path)
|
||||
println("deleted "..path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- go through app/common directories to delete unused files
|
||||
local function clean(manifest)
|
||||
local log = nil
|
||||
if fs.exists(app..".settings") and settings.load(app..".settings") then
|
||||
log = settings.get("LogPath")
|
||||
if log:sub(1, 1) == "/" then log = log:sub(2) end
|
||||
end
|
||||
|
||||
local tree = gen_tree(manifest, log)
|
||||
|
||||
table.insert(tree, "install_manifest.json")
|
||||
table.insert(tree, "ccmsim.lua")
|
||||
|
||||
local ls = fs.list("/")
|
||||
for _, val in pairs(ls) do
|
||||
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()
|
||||
end
|
||||
|
||||
-- get and validate command line options
|
||||
|
||||
println("-- CC Mekanism SCADA Install Manager (Off-Line) --")
|
||||
|
||||
if #opts == 0 or opts[1] == "help" then
|
||||
println("usage: ccmsim <mode>")
|
||||
println("<mode>")
|
||||
lgray()
|
||||
println(" check - check your installed versions")
|
||||
println(" update-rm - delete everything except the config,")
|
||||
println(" so that you can upload files for a")
|
||||
println(" new two-file off-line update")
|
||||
println(" uninstall - delete all app files and config")
|
||||
return
|
||||
else
|
||||
mode = get_opt(opts[1], { "check", "update-rm", "uninstall" })
|
||||
if mode == nil then
|
||||
red();println("Unrecognized mode.");white()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- run selected mode
|
||||
if mode == "check" then
|
||||
local local_ok, manifest = read_local_manifest()
|
||||
if not local_ok then
|
||||
yellow();println("failed to load local installation information");white()
|
||||
end
|
||||
|
||||
-- list all versions
|
||||
for key, value in pairs(manifest.versions) do
|
||||
term.setTextColor(colors.purple)
|
||||
print(string.format("%-14s", "["..key.."]"))
|
||||
blue();println(value);white()
|
||||
end
|
||||
elseif mode == "update-rm" or mode == "uninstall" then
|
||||
local ok, manifest = read_local_manifest()
|
||||
if not ok then
|
||||
red();println("Error parsing local installation manifest.");white()
|
||||
return
|
||||
end
|
||||
|
||||
app = manifest.depends[#manifest.depends]
|
||||
|
||||
if mode == "uninstall" then
|
||||
orange();println("Uninstalling all app files...")
|
||||
else
|
||||
orange();println("Deleting all app files except for configuration...")
|
||||
end
|
||||
|
||||
-- ask for confirmation
|
||||
if not ask_y_n("Continue", false) then return end
|
||||
|
||||
-- delete unused files first
|
||||
clean(manifest)
|
||||
|
||||
local file_list = manifest.files
|
||||
local dependencies = manifest.depends
|
||||
|
||||
-- delete all installed files
|
||||
lgray()
|
||||
for _, dependency in pairs(dependencies) do
|
||||
local files = file_list[dependency]
|
||||
for _, file in pairs(files) do
|
||||
if fs.exists(file) then fs.delete(file);println("deleted "..file) end
|
||||
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
|
||||
end
|
||||
|
||||
-- delete log file
|
||||
local log_deleted = false
|
||||
local settings_file = app..".settings"
|
||||
|
||||
if fs.exists(settings_file) and settings.load(settings_file) then
|
||||
local log = settings.get("LogPath")
|
||||
if log ~= nil then
|
||||
log_deleted = true
|
||||
if fs.exists(log) then
|
||||
fs.delete(log)
|
||||
println("deleted log file "..log)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not log_deleted then
|
||||
red();println("Failed to delete log file (it may not exist).");lgray()
|
||||
end
|
||||
|
||||
if mode == "uninstall" then
|
||||
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")
|
||||
|
||||
fs.delete("ccmsim.lua")
|
||||
println("deleted ccmsim.lua")
|
||||
end
|
||||
|
||||
green();println("Done!")
|
||||
end
|
||||
|
||||
white()
|
||||
14
build/package.sh
Executable file
14
build/package.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Create zips to attach to GitHub releases.
|
||||
# These can be extracted onto a computer and will include all files CCMSI would otherwise install.
|
||||
|
||||
tag=$(git describe --tags)
|
||||
apps=(coordinator pocket reactor-plc rtu supervisor)
|
||||
|
||||
for app in "${apps[@]}" do
|
||||
mkdir ${tag}_${app}
|
||||
cp -R $app scada-common graphics lockbox configure.lua initenv.lua startup.lua LICENSE ${tag}_${app}
|
||||
zip -r ${tag}_${app}.zip ${tag}_${app}
|
||||
rm -R ${tag}_${app}
|
||||
done
|
||||
83
build/safemin.py
Normal file
83
build/safemin.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
# minify files in a directory
|
||||
def min_files(path):
|
||||
start_sum, end_sum = 0, 0
|
||||
|
||||
for (root, _, files) in os.walk(path):
|
||||
os.makedirs('_minified/' + root, exist_ok=True)
|
||||
|
||||
for f in files:
|
||||
start, end = minify(root + "/" + f)
|
||||
|
||||
start_sum = start_sum + start
|
||||
end_sum = end_sum + end
|
||||
|
||||
delta = start_sum - end_sum
|
||||
|
||||
print(f"> done with '{path}': shrunk from {start_sum} bytes to {end_sum} bytes (saved {delta} bytes, or {(100*delta/start_sum):.2f}%)")
|
||||
|
||||
return list
|
||||
|
||||
# minify a file
|
||||
def minify(path: str):
|
||||
size_start = os.stat(path).st_size
|
||||
|
||||
f = open(path, "r")
|
||||
contents = f.read()
|
||||
f.close()
|
||||
|
||||
# remove --[[@as type]] hints before anything, since it would detect as multiline comments
|
||||
contents = re.sub(r' --+\[.+]]', '', contents)
|
||||
|
||||
if re.search(r'--+\[+', contents) != None:
|
||||
# absolutely not dealing with lua multiline comments
|
||||
# - there are more important things to do
|
||||
# - this minification is intended to be 100% safe, so working with multiline comments is asking for trouble
|
||||
# - the project doesn't use them as of writing this (except in test/), and it might as well stay that way
|
||||
raise Exception(f"no multiline comments allowed! (offending file: {path})")
|
||||
|
||||
if re.search(r'\\$', contents, flags=re.MULTILINE) != None:
|
||||
# '\' allows for multiline strings, which would require reverting to processing syntax line by line to support them
|
||||
raise Exception(f"no escaping newlines! (offending file: {path})")
|
||||
|
||||
# drop the comments, unless the line has quotes, because quotes are scary
|
||||
# (quotes are scary since we could actually be inside a string: "-- ..." shouldn't get deleted)
|
||||
# -> whitespace before '--' and anything after that, which includes '---' comments
|
||||
minified = re.sub(r'\s*--+(?!.*[\'"]).*', '', contents)
|
||||
|
||||
# drop leading whitespace on each line
|
||||
minified = re.sub(r'^ +', '', minified, flags=re.MULTILINE)
|
||||
|
||||
# drop blank lines
|
||||
while minified != re.sub(r'\n\n', '\n', minified):
|
||||
minified = re.sub(r'\n\n', '\n', minified)
|
||||
|
||||
# write the minified file
|
||||
f_min = open(f"_minified/{path}", "w")
|
||||
f_min.write(minified)
|
||||
f_min.close()
|
||||
|
||||
size_end = os.stat(f"_minified/{path}").st_size
|
||||
|
||||
print(f">> shrunk '{path}' from {size_start} bytes to {size_end} bytes (saved {size_start-size_end} bytes)")
|
||||
|
||||
return size_start, size_end
|
||||
|
||||
# minify applications and libraries
|
||||
dirs = [ 'scada-common', 'graphics', 'lockbox', 'reactor-plc', 'rtu', 'supervisor', 'coordinator', 'pocket' ]
|
||||
for _, d in enumerate(dirs):
|
||||
min_files(d)
|
||||
|
||||
# minify root files
|
||||
minify("startup.lua")
|
||||
minify("initenv.lua")
|
||||
minify("configure.lua")
|
||||
|
||||
# copy in license for build usage
|
||||
lic1 = open("LICENSE", "r")
|
||||
lic2 = open("_minified/LICENSE", "w")
|
||||
lic2.write(lic1.read())
|
||||
lic1.close()
|
||||
lic2.close()
|
||||
566
ccmsi.lua
566
ccmsi.lua
@@ -15,15 +15,63 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
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.14"
|
||||
local CCMSI_VERSION = "v1.21"
|
||||
|
||||
local install_dir = "/.install-cache"
|
||||
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
|
||||
local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/"
|
||||
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local _is_pkt_env = pocket -- luacheck: ignore pocket
|
||||
|
||||
local function println(msg) print(tostring(msg)) end
|
||||
|
||||
-- stripped down & modified copy of log.dmesg
|
||||
local function print(msg)
|
||||
msg = tostring(msg)
|
||||
|
||||
local cur_x, cur_y = term.getCursorPos()
|
||||
local out_w, out_h = term.getSize()
|
||||
|
||||
-- jump to next line if needed
|
||||
if cur_x == out_w then
|
||||
cur_x = 1
|
||||
if cur_y == out_h then
|
||||
term.scroll(1)
|
||||
term.setCursorPos(1, cur_y)
|
||||
else
|
||||
term.setCursorPos(1, cur_y + 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- wrap
|
||||
local lines, remaining, s_start, s_end, ln = {}, true, 1, out_w + 1 - cur_x, 1
|
||||
while remaining do
|
||||
local line = string.sub(msg, s_start, s_end)
|
||||
|
||||
if line == "" then
|
||||
remaining = false
|
||||
else
|
||||
lines[ln] = line
|
||||
s_start = s_end + 1
|
||||
s_end = s_end + out_w
|
||||
ln = ln + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- print
|
||||
for i = 1, #lines do
|
||||
cur_x, cur_y = term.getCursorPos()
|
||||
if i > 1 and cur_x > 1 then
|
||||
if cur_y == out_h then
|
||||
term.scroll(1)
|
||||
term.setCursorPos(1, cur_y)
|
||||
else term.setCursorPos(1, cur_y + 1) end
|
||||
end
|
||||
term.write(lines[i])
|
||||
end
|
||||
end
|
||||
|
||||
local opts = { ... }
|
||||
local mode, app, target
|
||||
local install_manifest = manifest_path.."main/install_manifest.json"
|
||||
@@ -101,16 +149,16 @@ local function get_remote_manifest()
|
||||
end
|
||||
|
||||
-- record the local installation manifest
|
||||
local function write_install_manifest(manifest, dependencies)
|
||||
local function write_install_manifest(manifest, deps)
|
||||
local versions = {}
|
||||
for key, value in pairs(manifest.versions) do
|
||||
local is_dependency = false
|
||||
for _, dependency in pairs(dependencies) do
|
||||
if (key == "bootloader" and dependency == "system") or key == dependency then
|
||||
is_dependency = true;break
|
||||
local is_dep = false
|
||||
for _, dep in pairs(deps) do
|
||||
if (key == "bootloader" and dep == "system") or key == dep then
|
||||
is_dep = true;break
|
||||
end
|
||||
end
|
||||
if key == app or key == "comms" or is_dependency then versions[key] = value end
|
||||
if key == app or key == "comms" or is_dep then versions[key] = value end
|
||||
end
|
||||
|
||||
manifest.versions = versions
|
||||
@@ -120,8 +168,41 @@ local function write_install_manifest(manifest, dependencies)
|
||||
imfile.close()
|
||||
end
|
||||
|
||||
-- try at most 3 times to download a file from the repository and write into w_path base directory
|
||||
---@return 0|1|2|3 success 0: ok, 1: download fail, 2: file open fail, 3: out of space
|
||||
local function http_get_file(file, w_path)
|
||||
local dl, err
|
||||
for i = 1, 3 do
|
||||
dl, err = http.get(repo_path..file)
|
||||
if dl then
|
||||
if i > 1 then green();println("success!");lgray() end
|
||||
local f = fs.open(w_path..file, "w")
|
||||
if not f then return 2 end
|
||||
local ok, msg = pcall(function() f.write(dl.readAll()) end)
|
||||
f.close()
|
||||
if not ok then
|
||||
if string.find(msg or "", "Out of space") ~= nil then
|
||||
red();println("[out of space]");lgray()
|
||||
return 3
|
||||
else return 2 end
|
||||
end
|
||||
break
|
||||
else
|
||||
red();println("HTTP Error: "..err)
|
||||
if i < 3 then
|
||||
lgray();print("> retrying...")
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
os.sleep(i/3.0)
|
||||
else
|
||||
return 1
|
||||
end
|
||||
end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- recursively build a tree out of the file manifest
|
||||
local function gen_tree(manifest)
|
||||
local function gen_tree(manifest, log)
|
||||
local function _tree_add(tree, split)
|
||||
if #split > 1 then
|
||||
local name = table.remove(split, 1)
|
||||
@@ -131,13 +212,14 @@ local function gen_tree(manifest)
|
||||
return nil
|
||||
end
|
||||
|
||||
local list, tree = {}, {}
|
||||
local list, tree = { log }, {}
|
||||
|
||||
-- make a list of each and every file
|
||||
for _, files in pairs(manifest.files) do for i = 1, #files do table.insert(list, files[i]) end end
|
||||
|
||||
for i = 1, #list do
|
||||
local split = {}
|
||||
---@diagnostic disable-next-line: discard-returns
|
||||
string.gsub(list[i], "([^/]+)", function(c) split[#split + 1] = c end)
|
||||
if #split == 1 then table.insert(tree, list[i])
|
||||
else table.insert(tree, _tree_add(tree, split)) end
|
||||
@@ -159,7 +241,7 @@ local function _clean_dir(dir, tree)
|
||||
if fs.isDir(path) then
|
||||
_clean_dir(path, tree[val])
|
||||
if #fs.list(path) == 0 then fs.delete(path);println("deleted "..path) end
|
||||
elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then
|
||||
elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then ---@todo remove config.lua on full release
|
||||
fs.delete(path)
|
||||
println("deleted "..path)
|
||||
end
|
||||
@@ -168,11 +250,16 @@ end
|
||||
|
||||
-- go through app/common directories to delete unused files
|
||||
local function clean(manifest)
|
||||
local tree = gen_tree(manifest)
|
||||
local log = nil
|
||||
if fs.exists(app..".settings") and settings.load(app..".settings") then
|
||||
log = settings.get("LogPath")
|
||||
if log:sub(1, 1) == "/" then log = log:sub(2) end
|
||||
end
|
||||
|
||||
local tree = gen_tree(manifest, log)
|
||||
|
||||
table.insert(tree, "install_manifest.json")
|
||||
table.insert(tree, "ccmsi.lua")
|
||||
table.insert(tree, "log.txt") ---@fixme fix after migration to settings files?
|
||||
|
||||
local ls = fs.list("/")
|
||||
for _, val in pairs(ls) do
|
||||
@@ -192,10 +279,27 @@ end
|
||||
|
||||
-- get and validate command line options
|
||||
|
||||
println("-- CC Mekanism SCADA Installer "..CCMSI_VERSION.." --")
|
||||
if _is_pkt_env then println("- SCADA Installer "..CCMSI_VERSION.." -")
|
||||
else println("-- CC Mekanism SCADA Installer "..CCMSI_VERSION.." --") end
|
||||
|
||||
if #opts == 0 or opts[1] == "help" then
|
||||
println("usage: ccmsi <mode> <app> <branch>")
|
||||
if _is_pkt_env then
|
||||
yellow();println("<mode>");lgray()
|
||||
println(" check - check latest")
|
||||
println(" install - fresh install")
|
||||
println(" update - update app")
|
||||
println(" uninstall - remove app")
|
||||
yellow();println("<app>");lgray()
|
||||
println(" reactor-plc")
|
||||
println(" rtu")
|
||||
println(" supervisor")
|
||||
println(" coordinator")
|
||||
println(" pocket")
|
||||
println(" installer (update only)")
|
||||
yellow();println("<branch>");lgray();
|
||||
println(" main (default) | devel");white()
|
||||
else
|
||||
println("<mode>")
|
||||
lgray()
|
||||
println(" check - check latest versions available")
|
||||
@@ -214,25 +318,41 @@ if #opts == 0 or opts[1] == "help" then
|
||||
println(" installer - ccmsi installer (update only)")
|
||||
white();println("<branch>")
|
||||
lgray();println(" main (default) | devel");white()
|
||||
end
|
||||
return
|
||||
else
|
||||
|
||||
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", "installer" })
|
||||
local next_opt = 3
|
||||
local apps = { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket", "installer" }
|
||||
app = get_opt(opts[2], apps)
|
||||
if app == nil then
|
||||
for _, a in pairs(apps) do
|
||||
if fs.exists(a) and fs.isDir(a) then
|
||||
app = a
|
||||
next_opt = 2
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if app == nil and mode ~= "check" then
|
||||
red();println("Unrecognized application.");white()
|
||||
return
|
||||
elseif mode == "check" then
|
||||
next_opt = 2
|
||||
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
|
||||
target = opts[next_opt]
|
||||
if (target ~= "main") and (target ~= "devel") then
|
||||
if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end
|
||||
target = "main"
|
||||
@@ -244,7 +364,6 @@ else
|
||||
end
|
||||
|
||||
-- run selected mode
|
||||
|
||||
if mode == "check" then
|
||||
local ok, manifest = get_remote_manifest()
|
||||
if not ok then return end
|
||||
@@ -260,25 +379,29 @@ if mode == "check" then
|
||||
-- list all versions
|
||||
for key, value in pairs(manifest.versions) do
|
||||
term.setTextColor(colors.purple)
|
||||
print(string.format("%-14s", "["..key.."]"))
|
||||
local tag = string.format("%-14s", "["..key.."]")
|
||||
if not _is_pkt_env then print(tag) end
|
||||
if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then
|
||||
if _is_pkt_env then println(tag) end
|
||||
blue();print(local_manifest.versions[key])
|
||||
if value ~= local_manifest.versions[key] then
|
||||
white();print(" (")
|
||||
cyan();print(value);white();println(" available)")
|
||||
else green();println(" (up to date)") end
|
||||
else
|
||||
elseif not _is_pkt_env then
|
||||
lgray();print("not installed");white();print(" (latest ")
|
||||
cyan();print(value);white();println(")")
|
||||
end
|
||||
end
|
||||
|
||||
if manifest.versions.installer ~= local_manifest.versions.installer then
|
||||
if manifest.versions.installer ~= local_manifest.versions.installer and not _is_pkt_env 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 ok, r_manifest, l_manifest
|
||||
|
||||
local update_installer = app == "installer"
|
||||
local ok, manifest = get_remote_manifest()
|
||||
ok, r_manifest = get_remote_manifest()
|
||||
if not ok then return end
|
||||
|
||||
local ver = {
|
||||
@@ -291,34 +414,34 @@ elseif mode == "install" or mode == "update" then
|
||||
}
|
||||
|
||||
-- try to find local versions
|
||||
local local_ok, lmnf = read_local_manifest()
|
||||
if not local_ok then
|
||||
if mode == "update" then
|
||||
ok, l_manifest = read_local_manifest()
|
||||
if mode == "update" and not update_installer then
|
||||
if not ok then
|
||||
red();println("Failed to load local installation information, cannot update.");white()
|
||||
return
|
||||
end
|
||||
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
|
||||
else
|
||||
ver.boot.v_local = l_manifest.versions.bootloader
|
||||
ver.app.v_local = l_manifest.versions[app]
|
||||
ver.comms.v_local = l_manifest.versions.comms
|
||||
ver.common.v_local = l_manifest.versions.common
|
||||
ver.graphics.v_local = l_manifest.versions.graphics
|
||||
ver.lockbox.v_local = l_manifest.versions.lockbox
|
||||
|
||||
if lmnf.versions[app] == nil then
|
||||
red();println("Another application is already installed, please uninstall it before installing a new application.");white()
|
||||
return
|
||||
if l_manifest.versions[app] == nil then
|
||||
red();println("Another application is already installed, please uninstall it before installing a new application.");white()
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if manifest.versions.installer ~= CCMSI_VERSION then
|
||||
if r_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
|
||||
if update_installer or ask_y_n("Would you like to update now", true) then
|
||||
lgray();println("GET ccmsi.lua")
|
||||
local dl, err = http.get(repo_path.."ccmsi.lua")
|
||||
|
||||
if dl == nil then
|
||||
red();println("HTTP Error "..err)
|
||||
red();println("HTTP Error: "..err)
|
||||
println("Installer download failed.");white()
|
||||
else
|
||||
local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location
|
||||
@@ -334,12 +457,12 @@ elseif mode == "install" or mode == "update" then
|
||||
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
|
||||
ver.boot.v_remote = r_manifest.versions.bootloader
|
||||
ver.app.v_remote = r_manifest.versions[app]
|
||||
ver.comms.v_remote = r_manifest.versions.comms
|
||||
ver.common.v_remote = r_manifest.versions.common
|
||||
ver.graphics.v_remote = r_manifest.versions.graphics
|
||||
ver.lockbox.v_remote = r_manifest.versions.lockbox
|
||||
|
||||
green()
|
||||
if mode == "install" then print("Installing ") else print("Updating ") end
|
||||
@@ -355,92 +478,197 @@ elseif mode == "install" or mode == "update" then
|
||||
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
|
||||
-- start install/update
|
||||
|
||||
--------------------------
|
||||
-- START INSTALL/UPDATE --
|
||||
--------------------------
|
||||
local space_req = r_manifest.sizes.manifest
|
||||
local space_avail = fs.getFreeSpace("/")
|
||||
|
||||
local space_required = manifest.sizes.manifest
|
||||
local space_available = fs.getFreeSpace("/")
|
||||
local file_list = r_manifest.files
|
||||
local size_list = r_manifest.sizes
|
||||
local deps = r_manifest.depends[app]
|
||||
|
||||
local single_file_mode = false
|
||||
local file_list = manifest.files
|
||||
local size_list = manifest.sizes
|
||||
local dependencies = manifest.depends[app]
|
||||
|
||||
table.insert(dependencies, app)
|
||||
|
||||
for _, dependency in pairs(dependencies) do
|
||||
local size = size_list[dependency]
|
||||
space_required = space_required + size
|
||||
end
|
||||
|
||||
-- check space constraints
|
||||
if space_available < space_required then
|
||||
single_file_mode = true
|
||||
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
|
||||
end
|
||||
|
||||
local success = true
|
||||
table.insert(deps, app)
|
||||
|
||||
-- helper function to check if a dependency is unchanged
|
||||
local function unchanged(dependency)
|
||||
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.common.changed or ver.comms.changed)
|
||||
elseif dependency == app then return not ver.app.changed
|
||||
local function unchanged(dep)
|
||||
if dep == "system" then return not ver.boot.changed
|
||||
elseif dep == "graphics" then return not ver.graphics.changed
|
||||
elseif dep == "lockbox" then return not ver.lockbox.changed
|
||||
elseif dep == "common" then return not (ver.common.changed or ver.comms.changed)
|
||||
elseif dep == 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
|
||||
local any_change = false
|
||||
|
||||
-- download all dependencies
|
||||
for _, dependency in pairs(dependencies) do
|
||||
if mode == "update" and unchanged(dependency) then
|
||||
pkg_message("skipping download of unchanged package", dependency)
|
||||
for _, dep in pairs(deps) do
|
||||
local size = size_list[dep]
|
||||
space_req = space_req + size
|
||||
any_change = any_change or not unchanged(dep)
|
||||
end
|
||||
|
||||
if mode == "update" and not any_change then
|
||||
yellow();println("Nothing to do, everything is already up-to-date!");white()
|
||||
return
|
||||
end
|
||||
|
||||
-- ask for confirmation
|
||||
if not ask_y_n("Continue", false) then return end
|
||||
|
||||
local single_file_mode = space_avail < space_req
|
||||
|
||||
local success = true
|
||||
|
||||
-- delete a file if the capitalization changes so that things work on Windows
|
||||
---@param path string
|
||||
local function mitigate_case(path)
|
||||
local dir, file = fs.getDir(path), fs.getName(path)
|
||||
if not fs.isDir(dir) then return end
|
||||
for _, p in ipairs(fs.list(dir)) do
|
||||
if string.lower(p) == string.lower(file) then
|
||||
if p ~= file then fs.delete(path) end
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param dl_stat 1|2|3 download status
|
||||
---@param file string file name
|
||||
---@param attempt integer recursive attempt #
|
||||
---@param sf_install function installer function for recursion
|
||||
local function handle_dl_fail(dl_stat, file, attempt, sf_install)
|
||||
red()
|
||||
if dl_stat == 1 then
|
||||
println("failed to download "..file)
|
||||
elseif dl_stat > 1 then
|
||||
if dl_stat == 2 then println("filesystem error with "..file) else println("no space for "..file) end
|
||||
if attempt == 1 then
|
||||
orange();println("re-attempting operation...");white()
|
||||
sf_install(2)
|
||||
elseif attempt == 2 then
|
||||
yellow()
|
||||
if dl_stat == 2 then println("There was an error writing to a file.") else println("Insufficient space available.") end
|
||||
lgray()
|
||||
if dl_stat == 2 then
|
||||
println("This may be due to insufficent space available or file permission issues. The installer can now attempt to delete files not used by the SCADA system.")
|
||||
else
|
||||
println("The installer can now attempt to delete files not used by the SCADA system.")
|
||||
end
|
||||
white()
|
||||
if not ask_y_n("Continue", false) then
|
||||
success = false
|
||||
return
|
||||
end
|
||||
clean(r_manifest)
|
||||
sf_install(3)
|
||||
elseif attempt == 3 then
|
||||
yellow()
|
||||
if dl_stat == 2 then println("There again was an error writing to a file.") else println("Insufficient space available.") end
|
||||
lgray()
|
||||
if dl_stat == 2 then
|
||||
println("This may be due to insufficent space available or file permission issues. Please delete any unused files you have on this computer then try again. Do not delete the "..app..".settings file unless you want to re-configure.")
|
||||
else
|
||||
println("Please delete any unused files you have on this computer then try again. Do not delete the "..app..".settings file unless you want to re-configure.")
|
||||
end
|
||||
white()
|
||||
success = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- single file update routine: go through all files and replace one by one
|
||||
---@param attempt integer recursive attempt #
|
||||
local function sf_install(attempt)
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
if attempt > 1 then os.sleep(2.0) end
|
||||
|
||||
local abort_attempt = false
|
||||
success = true
|
||||
|
||||
for _, dep in pairs(deps) do
|
||||
if mode == "update" and unchanged(dep) then
|
||||
pkg_message("skipping install of unchanged package", dep)
|
||||
else
|
||||
pkg_message("downloading package", dependency)
|
||||
pkg_message("installing package", dep)
|
||||
lgray()
|
||||
|
||||
local files = file_list[dependency]
|
||||
-- beginning on the second try, delete the directory before starting
|
||||
if attempt >= 2 then
|
||||
if dep == "system" then
|
||||
elseif dep == "common" then
|
||||
if fs.exists("/scada-common") then
|
||||
fs.delete("/scada-common")
|
||||
println("deleted /scada-common")
|
||||
end
|
||||
else
|
||||
if fs.exists("/"..dep) then
|
||||
fs.delete("/"..dep)
|
||||
println("deleted /"..dep)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local files = file_list[dep]
|
||||
for _, file in pairs(files) do
|
||||
println("GET "..file)
|
||||
local dl, err = http.get(repo_path..file)
|
||||
|
||||
if dl == nil then
|
||||
red();println("HTTP Error "..err)
|
||||
success = false
|
||||
mitigate_case(file)
|
||||
local dl_stat = http_get_file(file, "/")
|
||||
if dl_stat ~= 0 then
|
||||
abort_attempt = true
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
handle_dl_fail(dl_stat, file, attempt, sf_install)
|
||||
break
|
||||
else
|
||||
local handle = fs.open(install_dir.."/"..file, "w")
|
||||
handle.write(dl.readAll())
|
||||
handle.close()
|
||||
end
|
||||
end
|
||||
end
|
||||
if abort_attempt or not success then break end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle update/install
|
||||
if single_file_mode then sf_install(1)
|
||||
else
|
||||
if fs.exists(install_dir) then fs.delete(install_dir);fs.makeDir(install_dir) end
|
||||
|
||||
-- download all dependencies
|
||||
for _, dep in pairs(deps) do
|
||||
if mode == "update" and unchanged(dep) then
|
||||
pkg_message("skipping download of unchanged package", dep)
|
||||
else
|
||||
pkg_message("downloading package", dep)
|
||||
lgray()
|
||||
|
||||
local files = file_list[dep]
|
||||
for _, file in pairs(files) do
|
||||
println("GET "..file)
|
||||
local dl_stat = http_get_file(file, install_dir.."/")
|
||||
success = dl_stat == 0
|
||||
if dl_stat == 1 then
|
||||
red();println("failed to download "..file)
|
||||
break
|
||||
elseif dl_stat == 2 then
|
||||
red();println("filesystem error with "..file)
|
||||
break
|
||||
elseif dl_stat == 3 then
|
||||
-- this shouldn't occur in this mode
|
||||
red();println("no space for "..file)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if not success then break end
|
||||
end
|
||||
|
||||
-- copy in downloaded files (installation)
|
||||
if success then
|
||||
for _, dependency in pairs(dependencies) do
|
||||
if mode == "update" and unchanged(dependency) then
|
||||
pkg_message("skipping install of unchanged package", dependency)
|
||||
for _, dep in pairs(deps) do
|
||||
if mode == "update" and unchanged(dep) then
|
||||
pkg_message("skipping install of unchanged package", dep)
|
||||
else
|
||||
pkg_message("installing package", dependency)
|
||||
pkg_message("installing package", dep)
|
||||
lgray()
|
||||
|
||||
local files = file_list[dependency]
|
||||
local files = file_list[dep]
|
||||
for _, file in pairs(files) do
|
||||
local temp_file = install_dir.."/"..file
|
||||
if fs.exists(file) then fs.delete(file) end
|
||||
@@ -451,62 +679,27 @@ elseif mode == "install" or mode == "update" then
|
||||
end
|
||||
|
||||
fs.delete(install_dir)
|
||||
end
|
||||
|
||||
if success then
|
||||
write_install_manifest(manifest, dependencies)
|
||||
green()
|
||||
if mode == "install" then
|
||||
println("Installation completed successfully.")
|
||||
else println("Update completed successfully.") end
|
||||
white();println("Ready to clean up unused files, press any key to continue...")
|
||||
any_key();clean(manifest)
|
||||
white();println("Done.")
|
||||
else
|
||||
if mode == "install" then
|
||||
red();println("Installation failed.")
|
||||
else orange();println("Update failed, existing files unmodified.") end
|
||||
end
|
||||
if success then
|
||||
write_install_manifest(r_manifest, deps)
|
||||
green()
|
||||
if mode == "install" then
|
||||
println("Installation completed successfully.")
|
||||
else println("Update completed successfully.") end
|
||||
white();println("Ready to clean up unused files, press any key to continue...")
|
||||
any_key();clean(r_manifest)
|
||||
white();println("Done.")
|
||||
else
|
||||
-- go through all files and replace one by one
|
||||
for _, dependency in pairs(dependencies) do
|
||||
if mode == "update" and unchanged(dependency) then
|
||||
pkg_message("skipping install of unchanged package", dependency)
|
||||
else
|
||||
pkg_message("installing package", dependency)
|
||||
lgray()
|
||||
|
||||
local files = file_list[dependency]
|
||||
for _, file in pairs(files) do
|
||||
println("GET "..file)
|
||||
local dl, err = http.get(repo_path..file)
|
||||
|
||||
if dl == nil then
|
||||
red();println("HTTP Error "..err)
|
||||
success = false
|
||||
break
|
||||
else
|
||||
local handle = fs.open("/"..file, "w")
|
||||
handle.write(dl.readAll())
|
||||
handle.close()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if success then
|
||||
write_install_manifest(manifest, dependencies)
|
||||
green()
|
||||
if mode == "install" then
|
||||
println("Installation completed successfully.")
|
||||
else println("Update completed successfully.") end
|
||||
white();println("Ready to clean up unused files, press any key to continue...")
|
||||
any_key();clean(manifest)
|
||||
white();println("Done.")
|
||||
else
|
||||
red()
|
||||
red()
|
||||
if single_file_mode then
|
||||
if mode == "install" then
|
||||
println("Installation failed, files may have been skipped.")
|
||||
else println("Update failed, files may have been skipped.") end
|
||||
else
|
||||
if mode == "install" then
|
||||
println("Installation failed.")
|
||||
else orange();println("Update failed, existing files unmodified.") end
|
||||
end
|
||||
end
|
||||
elseif mode == "uninstall" then
|
||||
@@ -530,42 +723,14 @@ elseif mode == "uninstall" then
|
||||
clean(manifest)
|
||||
|
||||
local file_list = manifest.files
|
||||
local dependencies = manifest.depends[app]
|
||||
local deps = manifest.depends[app]
|
||||
|
||||
table.insert(dependencies, app)
|
||||
|
||||
-- delete log file
|
||||
local log_deleted = false
|
||||
local settings_file = app..".settings"
|
||||
local legacy_config_file = app.."/config.lua"
|
||||
|
||||
lgray()
|
||||
if fs.exists(legacy_config_file) then
|
||||
log_deleted = pcall(function ()
|
||||
local config = require(app..".config")
|
||||
if fs.exists(config.LOG_PATH) then
|
||||
fs.delete(config.LOG_PATH)
|
||||
println("deleted log file "..config.LOG_PATH)
|
||||
end
|
||||
end)
|
||||
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
|
||||
|
||||
if not log_deleted then
|
||||
red();println("Failed to delete log file.")
|
||||
white();println("press any key to continue...")
|
||||
any_key();lgray()
|
||||
end
|
||||
table.insert(deps, app)
|
||||
|
||||
-- delete all installed files
|
||||
for _, dependency in pairs(dependencies) do
|
||||
local files = file_list[dependency]
|
||||
lgray()
|
||||
for _, dep in pairs(deps) do
|
||||
local files = file_list[dep]
|
||||
for _, file in pairs(files) do
|
||||
if fs.exists(file) then fs.delete(file);println("deleted "..file) end
|
||||
end
|
||||
@@ -582,8 +747,23 @@ elseif mode == "uninstall" then
|
||||
end
|
||||
end
|
||||
|
||||
if fs.exists(legacy_config_file) then
|
||||
fs.delete(legacy_config_file);println("deleted "..legacy_config_file)
|
||||
-- delete log file
|
||||
local log_deleted = false
|
||||
local settings_file = app..".settings"
|
||||
|
||||
if fs.exists(settings_file) and settings.load(settings_file) then
|
||||
local log = settings.get("LogPath")
|
||||
if log ~= nil then
|
||||
log_deleted = true
|
||||
if fs.exists(log) then
|
||||
fs.delete(log)
|
||||
println("deleted log file "..log)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not log_deleted then
|
||||
red();println("Failed to delete log file (it may not exist).");lgray()
|
||||
end
|
||||
|
||||
if fs.exists(settings_file) then
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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")
|
||||
for _, app in ipairs({ "reactor-plc", "rtu", "supervisor", "coordinator", "pocket" }) do
|
||||
if fs.exists(app .. "/configure.lua") then
|
||||
local _, _, launch = require(app .. ".configure").configure()
|
||||
if launch then shell.execute("/startup") end
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
print("CONFIGURE> NO CONFIGURATOR FOUND")
|
||||
print("CONFIGURE> EXIT")
|
||||
|
||||
318
coordinator/config/facility.lua
Normal file
318
coordinator/config/facility.lua
Normal file
@@ -0,0 +1,318 @@
|
||||
local comms = require("scada-common.comms")
|
||||
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 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.PushButton")
|
||||
|
||||
local NumberField = require("graphics.elements.form.NumberField")
|
||||
|
||||
local tri = util.trinary
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
|
||||
local self = {
|
||||
nic = nil, ---@type nic
|
||||
net_listen = false,
|
||||
sv_addr = comms.BROADCAST,
|
||||
sv_seq_num = util.time_ms() * 10,
|
||||
show_sv_cfg = nil, ---@type function
|
||||
|
||||
sv_conn_button = nil, ---@type PushButton
|
||||
sv_conn_status = nil, ---@type TextBox
|
||||
sv_conn_detail = nil, ---@type TextBox
|
||||
sv_next = nil, ---@type PushButton
|
||||
sv_skip = nil, ---@type PushButton
|
||||
|
||||
tool_ctl = nil, ---@type _crd_cfg_tool_ctl
|
||||
tmp_cfg = nil ---@type crd_config
|
||||
}
|
||||
|
||||
-- check if a value is an integer within a range (inclusive)
|
||||
---@param x any
|
||||
---@param min integer
|
||||
---@param max integer
|
||||
local function is_int_min_max(x, min, max) return util.is_int(x) and x >= min and x <= max end
|
||||
|
||||
-- send a management packet to the supervisor
|
||||
---@param msg_type MGMT_TYPE
|
||||
---@param msg table
|
||||
local function send_sv(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local pkt = comms.mgmt_packet()
|
||||
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv_addr, self.sv_seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
||||
|
||||
self.nic.transmit(self.tmp_cfg.SVR_Channel, self.tmp_cfg.CRD_Channel, s_pkt)
|
||||
self.sv_seq_num = self.sv_seq_num + 1
|
||||
end
|
||||
|
||||
-- handle an establish message from the supervisor
|
||||
---@param packet mgmt_frame
|
||||
local function handle_packet(packet)
|
||||
local error_msg = nil
|
||||
|
||||
if packet.scada_frame.local_channel() ~= self.tmp_cfg.CRD_Channel then
|
||||
error_msg = "Error: unknown receive channel."
|
||||
elseif packet.scada_frame.remote_channel() == self.tmp_cfg.SVR_Channel and packet.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
if packet.type == MGMT_TYPE.ESTABLISH then
|
||||
if packet.length == 2 then
|
||||
local est_ack = packet.data[1]
|
||||
local config = packet.data[2]
|
||||
|
||||
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||
if type(config) == "table" and #config == 2 then
|
||||
local count_ok = is_int_min_max(config[1], 1, 4)
|
||||
local cool_ok = type(config[2]) == "table" and type(config[2].r_cool) == "table" and #config[2].r_cool == config[1]
|
||||
|
||||
if count_ok and cool_ok then
|
||||
self.tmp_cfg.UnitCount = config[1]
|
||||
self.tool_ctl.sv_cool_conf = {}
|
||||
|
||||
for i = 1, self.tmp_cfg.UnitCount do
|
||||
local num_b = config[2].r_cool[i].BoilerCount
|
||||
local num_t = config[2].r_cool[i].TurbineCount
|
||||
self.tool_ctl.sv_cool_conf[i] = { num_b, num_t }
|
||||
cool_ok = cool_ok and is_int_min_max(num_b, 0, 2) and is_int_min_max(num_t, 1, 3)
|
||||
end
|
||||
end
|
||||
|
||||
if not count_ok then
|
||||
error_msg = "Error: supervisor unit count out of range."
|
||||
elseif not cool_ok then
|
||||
error_msg = "Error: supervisor cooling configuration malformed."
|
||||
self.tool_ctl.sv_cool_conf = nil
|
||||
end
|
||||
|
||||
self.sv_addr = packet.scada_frame.src_addr()
|
||||
send_sv(MGMT_TYPE.CLOSE, {})
|
||||
else
|
||||
error_msg = "Error: invalid cooling configuration supervisor."
|
||||
end
|
||||
else
|
||||
error_msg = "Error: invalid allow reply length from supervisor."
|
||||
end
|
||||
elseif packet.length == 1 then
|
||||
local est_ack = packet.data[1]
|
||||
|
||||
if est_ack == ESTABLISH_ACK.DENY then
|
||||
error_msg = "Error: supervisor connection denied."
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
error_msg = "Error: a coordinator is already/still connected. Please try again."
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
error_msg = "Error: coordinator comms version does not match supervisor comms version."
|
||||
else
|
||||
error_msg = "Error: invalid reply from supervisor."
|
||||
end
|
||||
else
|
||||
error_msg = "Error: invalid reply length from supervisor."
|
||||
end
|
||||
else
|
||||
error_msg = "Error: didn't get an establish reply from supervisor."
|
||||
end
|
||||
end
|
||||
|
||||
self.net_listen = false
|
||||
|
||||
if error_msg then
|
||||
self.sv_conn_status.set_value("")
|
||||
self.sv_conn_detail.set_value(error_msg)
|
||||
self.sv_conn_button.enable()
|
||||
else
|
||||
self.sv_conn_status.set_value("Connected!")
|
||||
self.sv_conn_detail.set_value("Data received successfully, press 'Next' to continue.")
|
||||
self.sv_skip.hide()
|
||||
self.sv_next.show()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle supervisor connection failure
|
||||
local function handle_timeout()
|
||||
self.net_listen = false
|
||||
self.sv_conn_button.enable()
|
||||
self.sv_conn_status.set_value("Timed out.")
|
||||
self.sv_conn_detail.set_value("Supervisor did not reply. Ensure startup app is running on the supervisor.")
|
||||
end
|
||||
|
||||
-- attempt a connection to the supervisor to get cooling info
|
||||
local function sv_connect()
|
||||
self.sv_conn_button.disable()
|
||||
self.sv_conn_detail.set_value("")
|
||||
|
||||
local modem = ppm.get_wireless_modem()
|
||||
if modem == nil then
|
||||
self.sv_conn_status.set_value("Please connect an ender/wireless modem.")
|
||||
else
|
||||
self.sv_conn_status.set_value("Modem found, connecting...")
|
||||
if self.nic == nil then self.nic = network.nic(modem) end
|
||||
|
||||
self.nic.closeAll()
|
||||
self.nic.open(self.tmp_cfg.CRD_Channel)
|
||||
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.net_listen = true
|
||||
|
||||
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.CRD })
|
||||
|
||||
tcd.dispatch_unique(8, handle_timeout)
|
||||
end
|
||||
end
|
||||
|
||||
local facility = {}
|
||||
|
||||
-- create the facility configuration view
|
||||
---@param tool_ctl _crd_cfg_tool_ctl
|
||||
---@param main_pane MultiPane
|
||||
---@param cfg_sys [ crd_config, crd_config, crd_config, { [1]: string, [2]: string, [3]: any }[], function ]
|
||||
---@param fac_cfg Div
|
||||
---@param style { [string]: cpair }
|
||||
---@return MultiPane fac_pane
|
||||
function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
local _, ini_cfg, tmp_cfg, _, _ = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5]
|
||||
|
||||
self.tmp_cfg = tmp_cfg
|
||||
self.tool_ctl = tool_ctl
|
||||
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local g_lg_fg_bg = style.g_lg_fg_bg
|
||||
local nav_fg_bg = style.nav_fg_bg
|
||||
local btn_act_fg_bg = style.btn_act_fg_bg
|
||||
local btn_dis_fg_bg = style.btn_dis_fg_bg
|
||||
|
||||
--#region Facility
|
||||
|
||||
local fac_c_1 = Div{parent=fac_cfg,x=2,y=4,width=49}
|
||||
local fac_c_2 = Div{parent=fac_cfg,x=2,y=4,width=49}
|
||||
local fac_c_3 = Div{parent=fac_cfg,x=2,y=4,width=49}
|
||||
|
||||
local fac_pane = MultiPane{parent=fac_cfg,x=1,y=4,panes={fac_c_1,fac_c_2,fac_c_3}}
|
||||
|
||||
TextBox{parent=fac_cfg,x=1,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)}
|
||||
|
||||
TextBox{parent=fac_c_1,x=1,y=1,height=4,text="This tool can attempt to connect to your supervisor computer. This would load facility information in order to get the unit count and aid monitor setup."}
|
||||
TextBox{parent=fac_c_1,x=1,y=6,height=2,text="The supervisor startup app must be running and fully configured on your supervisor computer."}
|
||||
|
||||
self.sv_conn_status = TextBox{parent=fac_c_1,x=11,y=9,text=""}
|
||||
self.sv_conn_detail = TextBox{parent=fac_c_1,x=1,y=11,height=2,text=""}
|
||||
|
||||
self.sv_conn_button = PushButton{parent=fac_c_1,x=1,y=9,text="Connect",min_width=9,callback=function()sv_connect()end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
|
||||
local function sv_skip()
|
||||
tcd.abort(handle_timeout)
|
||||
tool_ctl.sv_cool_conf = nil
|
||||
self.net_listen = false
|
||||
fac_pane.set_value(2)
|
||||
end
|
||||
|
||||
local function sv_next()
|
||||
self.show_sv_cfg()
|
||||
tool_ctl.update_mon_reqs()
|
||||
fac_pane.set_value(3)
|
||||
end
|
||||
|
||||
PushButton{parent=fac_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
self.sv_skip = PushButton{parent=fac_c_1,x=44,y=14,text="Skip \x1a",callback=sv_skip,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
self.sv_next = PushButton{parent=fac_c_1,x=44,y=14,text="Next \x1a",callback=sv_next,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,hidden=true}
|
||||
|
||||
TextBox{parent=fac_c_2,x=1,y=1,height=3,text="Please enter the number of reactors you have, also referred to as reactor units or 'units' for short. A maximum of 4 is currently supported."}
|
||||
tool_ctl.num_units = NumberField{parent=fac_c_2,x=1,y=5,width=5,max_chars=2,default=ini_cfg.UnitCount,min=1,max=4,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=fac_c_2,x=7,y=5,text="reactors"}
|
||||
TextBox{parent=fac_c_2,x=1,y=7,height=3,text="This will decide how many monitors you need. If this does not match the supervisor's number of reactor units, the coordinator will not connect.",fg_bg=cpair(colors.yellow,colors._INHERIT)}
|
||||
TextBox{parent=fac_c_2,x=1,y=10,height=3,text="Since you skipped supervisor sync, the main monitor minimum height can't be determined precisely. It is marked with * on the next page.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local nu_error = TextBox{parent=fac_c_2,x=8,y=14,width=35,text="Please set the number of reactors.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_num_units()
|
||||
local count = tonumber(tool_ctl.num_units.get_value())
|
||||
if count ~= nil and count > 0 and count < 5 then
|
||||
nu_error.hide(true)
|
||||
tmp_cfg.UnitCount = count
|
||||
tool_ctl.update_mon_reqs()
|
||||
main_pane.set_value(4)
|
||||
else nu_error.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=fac_c_2,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_2,x=44,y=14,text="Next \x1a",callback=submit_num_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=fac_c_3,x=1,y=1,height=2,text="The following facility configuration was fetched from your supervisor computer."}
|
||||
|
||||
local fac_config_list = ListBox{parent=fac_c_3,x=1,y=4,height=9,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
PushButton{parent=fac_c_3,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_3,x=44,y=14,text="Next \x1a",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Tool and Helper Functions
|
||||
|
||||
tool_ctl.is_int_min_max = is_int_min_max
|
||||
|
||||
-- reset the connection display for a new attempt
|
||||
function tool_ctl.init_sv_connect_ui()
|
||||
self.sv_next.hide()
|
||||
self.sv_skip.disable()
|
||||
self.sv_skip.show()
|
||||
self.sv_conn_button.enable()
|
||||
self.sv_conn_status.set_value("")
|
||||
self.sv_conn_detail.set_value("")
|
||||
|
||||
-- the user needs to wait a few seconds, encouraging the to connect
|
||||
tcd.dispatch_unique(2, function () self.sv_skip.enable() end)
|
||||
end
|
||||
|
||||
-- show the facility's unit count and cooling configuration data
|
||||
function self.show_sv_cfg()
|
||||
local conf = tool_ctl.sv_cool_conf
|
||||
fac_config_list.remove_all()
|
||||
|
||||
local str = util.sprintf("Facility has %d reactor unit%s:", #conf, tri(#conf==1,"","s"))
|
||||
TextBox{parent=fac_config_list,text=str,fg_bg=cpair(colors.gray,colors.white)}
|
||||
|
||||
for i = 1, #conf do
|
||||
local num_b, num_t = conf[i][1], conf[i][2]
|
||||
str = util.sprintf("\x07 Unit %d has %d boiler%s and %d turbine%s", i, num_b, tri(num_b == 1, "", "s"), num_t, tri(num_t == 1, "", "s"))
|
||||
TextBox{parent=fac_config_list,text=str,fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
return fac_pane
|
||||
end
|
||||
|
||||
-- handle incoming modem messages
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
---@param reply_to integer
|
||||
---@param message any
|
||||
---@param distance integer
|
||||
function facility.receive_sv(side, sender, reply_to, message, distance)
|
||||
if self.nic ~= nil and self.net_listen then
|
||||
local s_pkt = self.nic.receive(side, sender, reply_to, message, distance)
|
||||
|
||||
if s_pkt and s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
tcd.abort(handle_timeout)
|
||||
handle_packet(mgmt_pkt.get())
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return facility
|
||||
455
coordinator/config/hmi.lua
Normal file
455
coordinator/config/hmi.lua
Normal file
@@ -0,0 +1,455 @@
|
||||
local ppm = require("scada-common.ppm")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
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 Checkbox = require("graphics.elements.controls.Checkbox")
|
||||
local PushButton = require("graphics.elements.controls.PushButton")
|
||||
local RadioButton = require("graphics.elements.controls.RadioButton")
|
||||
|
||||
local NumberField = require("graphics.elements.form.NumberField")
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local self = {
|
||||
apply_mon = nil, ---@type PushButton
|
||||
|
||||
edit_monitor = nil, ---@type function
|
||||
|
||||
mon_iface = "",
|
||||
mon_expect = {} ---@type integer[]
|
||||
}
|
||||
|
||||
local hmi = {}
|
||||
|
||||
-- create the HMI (human machine interface) configuration view
|
||||
---@param tool_ctl _crd_cfg_tool_ctl
|
||||
---@param main_pane MultiPane
|
||||
---@param cfg_sys [ crd_config, crd_config, crd_config, { [1]: string, [2]: string, [3]: any }[], function ]
|
||||
---@param divs Div[]
|
||||
---@param style { [string]: cpair }
|
||||
---@return MultiPane mon_pane
|
||||
function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
|
||||
local _, ini_cfg, tmp_cfg, _, _ = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5]
|
||||
local mon_cfg, spkr_cfg, crd_cfg = divs[1], divs[2], divs[3]
|
||||
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local g_lg_fg_bg = style.g_lg_fg_bg
|
||||
local nav_fg_bg = style.nav_fg_bg
|
||||
local btn_act_fg_bg = style.btn_act_fg_bg
|
||||
local btn_dis_fg_bg = style.btn_dis_fg_bg
|
||||
|
||||
--#region Monitors
|
||||
|
||||
local mon_c_1 = Div{parent=mon_cfg,x=2,y=4,width=49}
|
||||
local mon_c_2 = Div{parent=mon_cfg,x=2,y=4,width=49}
|
||||
local mon_c_3 = Div{parent=mon_cfg,x=2,y=4,width=49}
|
||||
local mon_c_4 = Div{parent=mon_cfg,x=2,y=4,width=49}
|
||||
|
||||
local mon_pane = MultiPane{parent=mon_cfg,x=1,y=4,panes={mon_c_1,mon_c_2,mon_c_3,mon_c_4}}
|
||||
|
||||
TextBox{parent=mon_cfg,x=1,y=2,text=" Monitor Configuration",fg_bg=cpair(colors.black,colors.blue)}
|
||||
|
||||
TextBox{parent=mon_c_1,x=1,y=1,height=5,text="Your configuration requires the following monitors. The main and flow monitors' heights are dependent on your unit count and cooling setup. If you manually entered the unit count, a * will be shown on potentially inaccurate calculations."}
|
||||
local mon_reqs = ListBox{parent=mon_c_1,x=1,y=7,height=6,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local function next_from_reqs()
|
||||
-- unassign unit monitors above the unit count
|
||||
for i = tmp_cfg.UnitCount + 1, 4 do tmp_cfg.UnitDisplays[i] = nil end
|
||||
|
||||
tool_ctl.gen_mon_list()
|
||||
mon_pane.set_value(2)
|
||||
end
|
||||
|
||||
PushButton{parent=mon_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=mon_c_1,x=8,y=14,text="Legacy Options",min_width=16,callback=function()mon_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=mon_c_1,x=44,y=14,text="Next \x1a",callback=next_from_reqs,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=mon_c_2,x=1,y=1,height=5,text="Please configure your monitors below. You can go back to the prior page without losing progress to double check what you need. All of those monitors must be assigned before you can proceed."}
|
||||
|
||||
local mon_list = ListBox{parent=mon_c_2,x=1,y=6,height=7,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local assign_err = TextBox{parent=mon_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_monitors()
|
||||
if tmp_cfg.MainDisplay == nil then
|
||||
assign_err.set_value("Please assign the main monitor.")
|
||||
elseif tmp_cfg.FlowDisplay == nil and not tmp_cfg.DisableFlowView then
|
||||
assign_err.set_value("Please assign the flow monitor.")
|
||||
elseif util.table_len(tmp_cfg.UnitDisplays) ~= tmp_cfg.UnitCount then
|
||||
for i = 1, tmp_cfg.UnitCount do
|
||||
if tmp_cfg.UnitDisplays[i] == nil then
|
||||
assign_err.set_value("Please assign the unit " .. i .. " monitor.")
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
assign_err.hide(true)
|
||||
main_pane.set_value(5)
|
||||
return
|
||||
end
|
||||
|
||||
assign_err.show()
|
||||
end
|
||||
|
||||
PushButton{parent=mon_c_2,x=1,y=14,text="\x1b Back",callback=function()mon_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=mon_c_2,x=44,y=14,text="Next \x1a",callback=submit_monitors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
local mon_desc = TextBox{parent=mon_c_3,x=1,y=1,height=4,text=""}
|
||||
|
||||
local mon_unit_l, mon_unit = nil, nil ---@type TextBox, NumberField
|
||||
|
||||
local mon_warn = TextBox{parent=mon_c_3,x=1,y=11,height=2,text="",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
|
||||
---@param val integer assignment type
|
||||
local function on_assign_mon(val)
|
||||
if val == 2 and tmp_cfg.DisableFlowView then
|
||||
self.apply_mon.disable()
|
||||
mon_warn.set_value("You disabled having a flow view monitor. It can't be set unless you go back and enable it.")
|
||||
mon_warn.show()
|
||||
elseif not util.table_contains(self.mon_expect, val) then
|
||||
self.apply_mon.disable()
|
||||
mon_warn.set_value("That assignment doesn't fit monitor dimensions. You'll need to resize the monitor for it to work.")
|
||||
mon_warn.show()
|
||||
else
|
||||
self.apply_mon.enable()
|
||||
mon_warn.hide(true)
|
||||
end
|
||||
|
||||
if val == 3 then
|
||||
mon_unit_l.show()
|
||||
mon_unit.show()
|
||||
else
|
||||
mon_unit_l.hide(true)
|
||||
mon_unit.hide(true)
|
||||
end
|
||||
|
||||
local value = mon_unit.get_value()
|
||||
mon_unit.set_max(tmp_cfg.UnitCount)
|
||||
if value == "0" or value == nil then mon_unit.set_value(0) end
|
||||
end
|
||||
|
||||
TextBox{parent=mon_c_3,x=1,y=6,width=10,text="Assignment"}
|
||||
local mon_assign = RadioButton{parent=mon_c_3,x=1,y=7,default=1,options={"Main Monitor","Flow Monitor","Unit Monitor"},callback=on_assign_mon,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.blue}
|
||||
|
||||
mon_unit_l = TextBox{parent=mon_c_3,x=18,y=6,width=7,text="Unit ID"}
|
||||
mon_unit = NumberField{parent=mon_c_3,x=18,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
|
||||
|
||||
local mon_u_err = TextBox{parent=mon_c_3,x=8,y=14,width=35,text="Please provide a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
-- purge all assignments for a given monitor
|
||||
---@param iface string
|
||||
local function purge_assignments(iface)
|
||||
if tmp_cfg.MainDisplay == iface then
|
||||
tmp_cfg.MainDisplay = nil
|
||||
elseif tmp_cfg.FlowDisplay == iface then
|
||||
tmp_cfg.FlowDisplay = nil
|
||||
else
|
||||
for i = 1, tmp_cfg.UnitCount do
|
||||
if tmp_cfg.UnitDisplays[i] == iface then tmp_cfg.UnitDisplays[i] = nil end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function apply_monitor()
|
||||
local iface = self.mon_iface
|
||||
local type = mon_assign.get_value()
|
||||
local u_id = tonumber(mon_unit.get_value())
|
||||
|
||||
if type == 1 then
|
||||
purge_assignments(iface)
|
||||
tmp_cfg.MainDisplay = iface
|
||||
elseif type == 2 then
|
||||
purge_assignments(iface)
|
||||
tmp_cfg.FlowDisplay = iface
|
||||
elseif u_id and u_id > 0 then
|
||||
purge_assignments(iface)
|
||||
tmp_cfg.UnitDisplays[u_id] = iface
|
||||
else
|
||||
mon_u_err.show()
|
||||
return
|
||||
end
|
||||
|
||||
tool_ctl.gen_mon_list()
|
||||
mon_u_err.hide(true)
|
||||
mon_pane.set_value(2)
|
||||
end
|
||||
|
||||
PushButton{parent=mon_c_3,x=1,y=14,text="\x1b Back",callback=function()mon_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
self.apply_mon = PushButton{parent=mon_c_3,x=43,y=14,min_width=7,text="Apply",callback=apply_monitor,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
|
||||
TextBox{parent=mon_c_4,x=1,y=1,height=3,text="For legacy compatibility with facilities built without space for a flow monitor, you can disable the flow monitor requirement here."}
|
||||
TextBox{parent=mon_c_4,x=1,y=5,height=3,text="Please be aware that THIS OPTION WILL BE REMOVED ON RELEASE. Disabling it will only be available for the remainder of the beta."}
|
||||
|
||||
tool_ctl.dis_flow_view = Checkbox{parent=mon_c_4,x=1,y=9,default=ini_cfg.DisableFlowView,label="Disable Flow View Monitor",box_fg_bg=cpair(colors.blue,colors.black)}
|
||||
|
||||
local function back_from_legacy()
|
||||
tmp_cfg.DisableFlowView = tool_ctl.dis_flow_view.get_value()
|
||||
tool_ctl.update_mon_reqs()
|
||||
mon_pane.set_value(1)
|
||||
end
|
||||
|
||||
PushButton{parent=mon_c_4,x=44,y=14,min_width=6,text="Done",callback=back_from_legacy,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Speaker
|
||||
|
||||
local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=spkr_cfg,x=1,y=2,text=" Speaker Configuration",fg_bg=cpair(colors.black,colors.cyan)}
|
||||
|
||||
TextBox{parent=spkr_c,x=1,y=1,height=2,text="The coordinator uses a speaker to play alarm sounds."}
|
||||
TextBox{parent=spkr_c,x=1,y=4,height=3,text="You can change the speaker audio volume from the default. The range is 0.0 to 3.0, where 1.0 is standard volume."}
|
||||
|
||||
tool_ctl.s_vol = NumberField{parent=spkr_c,x=1,y=8,width=9,max_chars=7,allow_decimal=true,default=ini_cfg.SpeakerVolume,min=0,max=3,fg_bg=bw_fg_bg}
|
||||
|
||||
TextBox{parent=spkr_c,x=1,y=10,height=3,text="Note: alarm sine waves are at half scale so that multiple will be required to reach full scale.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local s_vol_err = TextBox{parent=spkr_c,x=8,y=14,width=35,text="Please set a volume.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_vol()
|
||||
local vol = tonumber(tool_ctl.s_vol.get_value())
|
||||
if vol ~= nil then
|
||||
s_vol_err.hide(true)
|
||||
tmp_cfg.SpeakerVolume = vol
|
||||
main_pane.set_value(6)
|
||||
else s_vol_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=spkr_c,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=spkr_c,x=44,y=14,text="Next \x1a",callback=submit_vol,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Coordinator UI
|
||||
|
||||
local crd_c_1 = Div{parent=crd_cfg,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=crd_cfg,x=1,y=2,text=" Coordinator UI Configuration",fg_bg=cpair(colors.black,colors.lime)}
|
||||
|
||||
TextBox{parent=crd_c_1,x=1,y=1,height=2,text="You can customize the UI with the interface options below."}
|
||||
|
||||
TextBox{parent=crd_c_1,x=1,y=4,text="Clock Time Format"}
|
||||
tool_ctl.clock_fmt = RadioButton{parent=crd_c_1,x=1,y=5,default=util.trinary(ini_cfg.Time24Hour,1,2),options={"24-Hour","12-Hour"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
|
||||
TextBox{parent=crd_c_1,x=20,y=4,text="Po/Pu Pellet Color"}
|
||||
TextBox{parent=crd_c_1,x=39,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
|
||||
tool_ctl.pellet_color = RadioButton{parent=crd_c_1,x=20,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po (Mek 10.4+)"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
|
||||
TextBox{parent=crd_c_1,x=1,y=8,text="Temperature Scale"}
|
||||
tool_ctl.temp_scale = RadioButton{parent=crd_c_1,x=1,y=9,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
|
||||
TextBox{parent=crd_c_1,x=20,y=8,text="Energy Scale"}
|
||||
tool_ctl.energy_scale = RadioButton{parent=crd_c_1,x=20,y=9,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
|
||||
local function submit_ui_opts()
|
||||
tmp_cfg.Time24Hour = tool_ctl.clock_fmt.get_value() == 1
|
||||
tmp_cfg.GreenPuPellet = tool_ctl.pellet_color.get_value() == 1
|
||||
tmp_cfg.TempScale = tool_ctl.temp_scale.get_value()
|
||||
tmp_cfg.EnergyScale = tool_ctl.energy_scale.get_value()
|
||||
main_pane.set_value(7)
|
||||
end
|
||||
|
||||
PushButton{parent=crd_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=crd_c_1,x=44,y=14,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Tool and Helper Functions
|
||||
|
||||
-- update list of monitor requirements
|
||||
function tool_ctl.update_mon_reqs()
|
||||
local plural = tmp_cfg.UnitCount > 1
|
||||
|
||||
if tool_ctl.sv_cool_conf ~= nil then
|
||||
local cnf = tool_ctl.sv_cool_conf
|
||||
|
||||
local row1_tall = cnf[1][1] > 1 or cnf[1][2] > 2 or (cnf[2] and (cnf[2][1] > 1 or cnf[2][2] > 2))
|
||||
local row1_short = (cnf[1][1] == 0 and cnf[1][2] == 1) and (cnf[2] == nil or (cnf[2][1] == 0 and cnf[2][2] == 1))
|
||||
local row2_tall = (cnf[3] and (cnf[3][1] > 1 or cnf[3][2] > 2)) or (cnf[4] and (cnf[4][1] > 1 or cnf[4][2] > 2))
|
||||
local row2_short = (cnf[3] == nil or (cnf[3][1] == 0 and cnf[3][2] == 1)) and (cnf[4] == nil or (cnf[4][1] == 0 and cnf[4][2] == 1))
|
||||
|
||||
if tmp_cfg.UnitCount <= 2 then
|
||||
tool_ctl.main_mon_h = util.trinary(row1_tall, 5, 4)
|
||||
else
|
||||
-- is only one tall and the other short, or are both tall? -> 5 or 6; are neither tall? -> 5
|
||||
if row1_tall or row2_tall then
|
||||
tool_ctl.main_mon_h = util.trinary((row1_short and row2_tall) or (row1_tall and row2_short), 5, 6)
|
||||
else tool_ctl.main_mon_h = 5 end
|
||||
end
|
||||
else
|
||||
tool_ctl.main_mon_h = util.trinary(tmp_cfg.UnitCount <= 2, 4, 5)
|
||||
end
|
||||
|
||||
tool_ctl.flow_mon_h = 2 + tmp_cfg.UnitCount
|
||||
|
||||
local asterisk = util.trinary(tool_ctl.sv_cool_conf == nil, "*", "")
|
||||
local m_at_least = util.trinary(tool_ctl.main_mon_h < 6, "at least ", "")
|
||||
local f_at_least = util.trinary(tool_ctl.flow_mon_h < 6, "at least ", "")
|
||||
|
||||
mon_reqs.remove_all()
|
||||
|
||||
TextBox{parent=mon_reqs,x=1,y=1,text="\x1a "..tmp_cfg.UnitCount.." Unit View Monitor"..util.trinary(plural,"s","")}
|
||||
TextBox{parent=mon_reqs,x=1,y=1,text=" "..util.trinary(plural,"each ","").."must be 4 blocks wide by 4 tall",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=mon_reqs,x=1,y=1,text="\x1a 1 Main View Monitor"}
|
||||
TextBox{parent=mon_reqs,x=1,y=1,text=" must be 8 blocks wide by "..m_at_least..tool_ctl.main_mon_h..asterisk.." tall",fg_bg=cpair(colors.gray,colors.white)}
|
||||
if not tmp_cfg.DisableFlowView then
|
||||
TextBox{parent=mon_reqs,x=1,y=1,text="\x1a 1 Flow View Monitor"}
|
||||
TextBox{parent=mon_reqs,x=1,y=1,text=" must be 8 blocks wide by "..f_at_least..tool_ctl.flow_mon_h.." tall",fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
end
|
||||
|
||||
-- set/edit a monitor's assignment
|
||||
---@param iface string
|
||||
---@param device ppm_entry
|
||||
function self.edit_monitor(iface, device)
|
||||
self.mon_iface = iface
|
||||
|
||||
local dev = device.dev
|
||||
local w, h = ppm.monitor_block_size(dev.getSize())
|
||||
|
||||
local msg = "This size doesn't match a required screen. Please go back and resize it, or configure below at the risk of it not working."
|
||||
|
||||
self.mon_expect = {}
|
||||
mon_assign.set_value(1)
|
||||
mon_unit.set_value(0)
|
||||
|
||||
if w == 4 and h == 4 then
|
||||
msg = "This could work as a unit display. Please configure below."
|
||||
self.mon_expect = { 3 }
|
||||
mon_assign.set_value(3)
|
||||
elseif w == 8 then
|
||||
if h >= tool_ctl.main_mon_h and h >= tool_ctl.flow_mon_h then
|
||||
msg = "This could work as either your main monitor or flow monitor. Please configure below."
|
||||
self.mon_expect = { 1, 2 }
|
||||
if tmp_cfg.MainDisplay then mon_assign.set_value(2) end
|
||||
elseif h >= tool_ctl.main_mon_h then
|
||||
msg = "This could work as your main monitor. Please configure below."
|
||||
self.mon_expect = { 1 }
|
||||
elseif h >= tool_ctl.flow_mon_h then
|
||||
msg = "This could work as your flow monitor. Please configure below."
|
||||
self.mon_expect = { 2 }
|
||||
mon_assign.set_value(2)
|
||||
end
|
||||
end
|
||||
|
||||
-- override if a config exists
|
||||
if tmp_cfg.MainDisplay == iface then
|
||||
mon_assign.set_value(1)
|
||||
elseif tmp_cfg.FlowDisplay == iface then
|
||||
mon_assign.set_value(2)
|
||||
else
|
||||
for i = 1, tmp_cfg.UnitCount do
|
||||
if tmp_cfg.UnitDisplays[i] == iface then
|
||||
mon_assign.set_value(3)
|
||||
mon_unit.set_value(i)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
on_assign_mon(mon_assign.get_value())
|
||||
|
||||
mon_desc.set_value(util.c("You have selected '", iface, "', which has a block size of ", w, " wide by ", h, " tall. ", msg))
|
||||
mon_pane.set_value(3)
|
||||
end
|
||||
|
||||
-- generate the list of available monitors
|
||||
function tool_ctl.gen_mon_list()
|
||||
mon_list.remove_all()
|
||||
|
||||
local missing = { main = tmp_cfg.MainDisplay ~= nil, flow = tmp_cfg.FlowDisplay ~= nil, unit = {} }
|
||||
for i = 1, tmp_cfg.UnitCount do missing.unit[i] = tmp_cfg.UnitDisplays[i] ~= nil end
|
||||
|
||||
-- list connected monitors
|
||||
local monitors = ppm.get_monitor_list()
|
||||
for iface, device in pairs(monitors) do
|
||||
local dev = device.dev ---@type Monitor
|
||||
|
||||
dev.setTextScale(0.5)
|
||||
dev.setTextColor(colors.white)
|
||||
dev.setBackgroundColor(colors.black)
|
||||
dev.clear()
|
||||
dev.setCursorPos(1, 1)
|
||||
dev.setTextColor(colors.magenta)
|
||||
dev.write("This is monitor")
|
||||
dev.setCursorPos(1, 2)
|
||||
dev.setTextColor(colors.white)
|
||||
dev.write(iface)
|
||||
|
||||
local assignment = "Unused"
|
||||
|
||||
if tmp_cfg.MainDisplay == iface then
|
||||
assignment = "Main"
|
||||
missing.main = false
|
||||
elseif tmp_cfg.FlowDisplay == iface then
|
||||
assignment = "Flow"
|
||||
missing.flow = false
|
||||
else
|
||||
for i = 1, tmp_cfg.UnitCount do
|
||||
if tmp_cfg.UnitDisplays[i] == iface then
|
||||
missing.unit[i] = false
|
||||
assignment = "Unit " .. i
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local line = Div{parent=mon_list,x=1,y=1,height=1}
|
||||
|
||||
TextBox{parent=line,x=1,y=1,width=6,text=assignment,fg_bg=cpair(util.trinary(assignment=="Unused",colors.red,colors.blue),colors.white)}
|
||||
TextBox{parent=line,x=8,y=1,text=iface}
|
||||
|
||||
local w, h = ppm.monitor_block_size(dev.getSize())
|
||||
|
||||
local function unset_mon()
|
||||
purge_assignments(iface)
|
||||
tool_ctl.gen_mon_list()
|
||||
end
|
||||
|
||||
TextBox{parent=line,x=33,y=1,width=4,text=w.."x"..h,fg_bg=cpair(colors.black,colors.white)}
|
||||
PushButton{parent=line,x=37,y=1,min_width=5,height=1,text="SET",callback=function()self.edit_monitor(iface,device)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
|
||||
local unset = PushButton{parent=line,x=42,y=1,min_width=7,height=1,text="UNSET",callback=unset_mon,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.black,colors.gray)}
|
||||
|
||||
if assignment == "Unused" then unset.disable() end
|
||||
end
|
||||
|
||||
local dc_list = {} -- disconnected monitor list
|
||||
|
||||
if missing.main then table.insert(dc_list, { "Main", tmp_cfg.MainDisplay }) end
|
||||
if missing.flow then table.insert(dc_list, { "Flow", tmp_cfg.FlowDisplay }) end
|
||||
for i = 1, tmp_cfg.UnitCount do
|
||||
if missing.unit[i] then table.insert(dc_list, { "Unit " .. i, tmp_cfg.UnitDisplays[i] }) end
|
||||
end
|
||||
|
||||
-- add monitors that are assigned but not connected
|
||||
for i = 1, #dc_list do
|
||||
local line = Div{parent=mon_list,x=1,y=1,height=1}
|
||||
|
||||
TextBox{parent=line,x=1,y=1,width=6,text=dc_list[i][1],fg_bg=cpair(colors.blue,colors.white)}
|
||||
TextBox{parent=line,x=8,y=1,text="disconnected",fg_bg=cpair(colors.red,colors.white)}
|
||||
|
||||
local function unset_mon()
|
||||
purge_assignments(dc_list[i][2])
|
||||
tool_ctl.gen_mon_list()
|
||||
end
|
||||
|
||||
TextBox{parent=line,x=33,y=1,width=4,text="?x?",fg_bg=cpair(colors.black,colors.white)}
|
||||
PushButton{parent=line,x=37,y=1,min_width=5,height=1,text="SET",callback=function()end,dis_fg_bg=cpair(colors.black,colors.gray)}.disable()
|
||||
PushButton{parent=line,x=42,y=1,min_width=7,height=1,text="UNSET",callback=unset_mon,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.black,colors.gray)}
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
return mon_pane
|
||||
end
|
||||
|
||||
return hmi
|
||||
580
coordinator/config/system.lua
Normal file
580
coordinator/config/system.lua
Normal file
@@ -0,0 +1,580 @@
|
||||
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local network = require("scada-common.network")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
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.PushButton")
|
||||
local RadioButton = require("graphics.elements.controls.RadioButton")
|
||||
|
||||
local NumberField = require("graphics.elements.form.NumberField")
|
||||
local TextField = require("graphics.elements.form.TextField")
|
||||
|
||||
local IndLight = require("graphics.elements.indicators.IndicatorLight")
|
||||
|
||||
local tri = util.trinary
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local RIGHT = core.ALIGN.RIGHT
|
||||
|
||||
local self = {
|
||||
importing_legacy = false,
|
||||
|
||||
show_auth_key = nil, ---@type function
|
||||
show_key_btn = nil, ---@type PushButton
|
||||
auth_key_textbox = nil, ---@type TextBox
|
||||
auth_key_value = ""
|
||||
}
|
||||
|
||||
local system = {}
|
||||
|
||||
-- create the system configuration view
|
||||
---@param tool_ctl _crd_cfg_tool_ctl
|
||||
---@param main_pane MultiPane
|
||||
---@param cfg_sys [ crd_config, crd_config, crd_config, { [1]: string, [2]: string, [3]: any }[], function ]
|
||||
---@param divs Div[]
|
||||
---@param ext [ MultiPane, MultiPane, function, function ]
|
||||
---@param style { [string]: cpair }
|
||||
function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
local settings_cfg, ini_cfg, tmp_cfg, fields, load_settings = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5]
|
||||
local net_cfg, log_cfg, clr_cfg, summary = divs[1], divs[2], divs[3], divs[4]
|
||||
local fac_pane, mon_pane, preset_monitor_fields, exit = ext[1], ext[2], ext[3], ext[4]
|
||||
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local g_lg_fg_bg = style.g_lg_fg_bg
|
||||
local nav_fg_bg = style.nav_fg_bg
|
||||
local btn_act_fg_bg = style.btn_act_fg_bg
|
||||
local btn_dis_fg_bg = style.btn_dis_fg_bg
|
||||
|
||||
--#region Network
|
||||
|
||||
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
|
||||
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,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."}
|
||||
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 3 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"}
|
||||
local svr_chan = NumberField{parent=net_c_1,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=10,width=19,text="Coordinator Channel"}
|
||||
local crd_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=29,y=10,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"}
|
||||
local pkt_chan = NumberField{parent=net_c_1,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,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=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=1,text="Please set the connection timeouts below."}
|
||||
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=8,width=19,text="Supervisor Timeout"}
|
||||
local svr_timeout = NumberField{parent=net_c_2,x=20,y=8,width=7,default=ini_cfg.SVR_Timeout,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=1,y=10,width=14,text="Pocket Timeout"}
|
||||
local api_timeout = NumberField{parent=net_c_2,x=20,y=10,width=7,default=ini_cfg.API_Timeout,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=28,y=8,height=4,width=7,text="seconds\n\nseconds",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local ct_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_timeouts()
|
||||
local svr_cto, api_cto = tonumber(svr_timeout.get_value()), tonumber(api_timeout.get_value())
|
||||
if svr_cto ~= nil and api_cto ~= nil then
|
||||
tmp_cfg.SVR_Timeout, tmp_cfg.API_Timeout = svr_cto, api_cto
|
||||
net_pane.set_value(3)
|
||||
ct_err.hide(true)
|
||||
else ct_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,x=1,y=1,text="Please set the trusted range below."}
|
||||
TextBox{parent=net_c_3,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_3,x=1,y=7,height=2,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=10,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=8,y=14,width=35,text="Please 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
|
||||
comms.set_trusted_range(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=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
|
||||
TextBox{parent=net_c_4,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_4,x=1,y=11,text="Facility Auth Key"}
|
||||
local key, _ = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
|
||||
|
||||
local function censor_key(enable) key.censor(tri(enable, "*", nil)) end
|
||||
|
||||
local hide_key = Checkbox{parent=net_c_4,x=34,y=12,label="Hide",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=8,y=14,width=35,text="Key must be at least 8 characters.",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()
|
||||
key_err.hide(true)
|
||||
|
||||
-- init mac for supervisor connection
|
||||
if string.len(v) >= 8 then network.init_mac(tmp_cfg.AuthKey) else network.deinit_mac() end
|
||||
|
||||
-- prep supervisor connection screen
|
||||
tool_ctl.init_sv_connect_ui()
|
||||
|
||||
main_pane.set_value(3)
|
||||
else key_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_4,x=44,y=14,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=49}
|
||||
|
||||
TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=3,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,text="Log File Path"}
|
||||
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||
|
||||
local en_dbg = Checkbox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||
TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local path_err = TextBox{parent=log_c_1,x=8,y=14,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_log()
|
||||
if path.get_value() ~= "" then
|
||||
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.color_apply.hide(true)
|
||||
tool_ctl.color_next.show()
|
||||
main_pane.set_value(8)
|
||||
else path_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Color Options
|
||||
|
||||
local clr_c_1 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
local clr_c_2 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
local clr_c_3 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
local clr_c_4 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
|
||||
local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}}
|
||||
|
||||
TextBox{parent=clr_cfg,x=1,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)}
|
||||
|
||||
TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color themes for the different UI displays."}
|
||||
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=clr_c_1,x=1,y=7,text="Main UI Theme"}
|
||||
local main_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.MainTheme,options=themes.UI_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
|
||||
TextBox{parent=clr_c_1,x=18,y=7,text="Front Panel Theme"}
|
||||
local fp_theme = RadioButton{parent=clr_c_1,x=18,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
|
||||
TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will usually be split up."}
|
||||
|
||||
TextBox{parent=clr_c_2,x=21,y=7,text="Preview"}
|
||||
local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)}
|
||||
_ = IndLight{parent=clr_c_2,x=21,y=9,label="Warning",colors=cpair(colors.black,colors.yellow)}
|
||||
_ = IndLight{parent=clr_c_2,x=21,y=10,label="Bad",colors=cpair(colors.black,colors.red)}
|
||||
local b_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.black,colors.black),hidden=true}
|
||||
local g_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.gray,colors.gray),hidden=true}
|
||||
|
||||
local function recolor(value)
|
||||
local c = themes.smooth_stone.color_modes[value]
|
||||
|
||||
if value == themes.COLOR_MODE.STANDARD or value == themes.COLOR_MODE.BLUE_IND then
|
||||
b_off.hide()
|
||||
g_off.show()
|
||||
else
|
||||
g_off.hide()
|
||||
b_off.show()
|
||||
end
|
||||
|
||||
if #c == 0 then
|
||||
for i = 1, #style.colors do term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end
|
||||
else
|
||||
term.setPaletteColor(colors.green, c[1].hex)
|
||||
term.setPaletteColor(colors.yellow, c[2].hex)
|
||||
term.setPaletteColor(colors.red, c[3].hex)
|
||||
end
|
||||
end
|
||||
|
||||
TextBox{parent=clr_c_2,x=1,y=7,width=10,text="Color Mode"}
|
||||
local c_mode = RadioButton{parent=clr_c_2,x=1,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
|
||||
TextBox{parent=clr_c_2,x=21,y=13,height=2,width=18,text="Note: exact color varies by theme.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
local function back_from_colors()
|
||||
main_pane.set_value(tri(tool_ctl.jumped_to_color, 1, 7))
|
||||
tool_ctl.jumped_to_color = false
|
||||
recolor(1)
|
||||
end
|
||||
|
||||
local function show_access()
|
||||
clr_pane.set_value(2)
|
||||
recolor(c_mode.get_value())
|
||||
end
|
||||
|
||||
local function submit_colors()
|
||||
tmp_cfg.MainTheme = main_theme.get_value()
|
||||
tmp_cfg.FrontPanelTheme = fp_theme.get_value()
|
||||
tmp_cfg.ColorMode = c_mode.get_value()
|
||||
|
||||
if tool_ctl.jumped_to_color then
|
||||
settings.set("MainTheme", tmp_cfg.MainTheme)
|
||||
settings.set("FrontPanelTheme", tmp_cfg.FrontPanelTheme)
|
||||
settings.set("ColorMode", tmp_cfg.ColorMode)
|
||||
|
||||
if settings.save("/coordinator.settings") then
|
||||
load_settings(settings_cfg, true)
|
||||
load_settings(ini_cfg)
|
||||
clr_pane.set_value(3)
|
||||
else
|
||||
clr_pane.set_value(4)
|
||||
end
|
||||
else
|
||||
tool_ctl.gen_summary(tmp_cfg)
|
||||
tool_ctl.viewing_config = false
|
||||
self.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
main_pane.set_value(9)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=clr_c_1,x=1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=clr_c_1,x=8,y=14,min_width=15,text="Accessibility",callback=show_access,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.color_next = PushButton{parent=clr_c_1,x=44,y=14,text="Next \x1a",callback=submit_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.color_apply = PushButton{parent=clr_c_1,x=43,y=14,min_width=7,text="Apply",callback=submit_colors,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
tool_ctl.color_apply.hide(true)
|
||||
|
||||
local function c_go_home()
|
||||
main_pane.set_value(1)
|
||||
clr_pane.set_value(1)
|
||||
end
|
||||
|
||||
TextBox{parent=clr_c_3,x=1,y=1,text="Settings saved!"}
|
||||
PushButton{parent=clr_c_3,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
PushButton{parent=clr_c_3,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=clr_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=clr_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
PushButton{parent=clr_c_4,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Summary and Saving
|
||||
|
||||
local sum_c_1 = Div{parent=summary,x=2,y=4,width=49}
|
||||
local sum_c_2 = Div{parent=summary,x=2,y=4,width=49}
|
||||
local sum_c_3 = Div{parent=summary,x=2,y=4,width=49}
|
||||
local sum_c_4 = Div{parent=summary,x=2,y=4,width=49}
|
||||
|
||||
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,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||
|
||||
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local function back_from_summary()
|
||||
if tool_ctl.viewing_config or self.importing_legacy then
|
||||
main_pane.set_value(1)
|
||||
tool_ctl.viewing_config = false
|
||||
self.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
else
|
||||
main_pane.set_value(8)
|
||||
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 _, field in ipairs(fields) do
|
||||
local k, v = field[1], tmp_cfg[field[1]]
|
||||
if v == nil then settings.unset(k) else settings.set(k, v) end
|
||||
end
|
||||
|
||||
if settings.save("/coordinator.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(svr_timeout, ini_cfg.SVR_Timeout)
|
||||
try_set(api_timeout, ini_cfg.API_Timeout)
|
||||
try_set(range, ini_cfg.TrustedRange)
|
||||
try_set(key, ini_cfg.AuthKey)
|
||||
try_set(tool_ctl.num_units, ini_cfg.UnitCount)
|
||||
try_set(tool_ctl.dis_flow_view, ini_cfg.DisableFlowView)
|
||||
try_set(tool_ctl.s_vol, ini_cfg.SpeakerVolume)
|
||||
try_set(tool_ctl.pellet_color, ini_cfg.GreenPuPellet)
|
||||
try_set(tool_ctl.clock_fmt, tri(ini_cfg.Time24Hour, 1, 2))
|
||||
try_set(tool_ctl.temp_scale, ini_cfg.TempScale)
|
||||
try_set(tool_ctl.energy_scale, ini_cfg.EnergyScale)
|
||||
try_set(mode, ini_cfg.LogMode)
|
||||
try_set(path, ini_cfg.LogPath)
|
||||
try_set(en_dbg, ini_cfg.LogDebug)
|
||||
try_set(main_theme, ini_cfg.MainTheme)
|
||||
try_set(fp_theme, ini_cfg.FrontPanelTheme)
|
||||
try_set(c_mode, ini_cfg.ColorMode)
|
||||
|
||||
preset_monitor_fields()
|
||||
|
||||
tool_ctl.gen_mon_list()
|
||||
|
||||
tool_ctl.view_cfg.enable()
|
||||
tool_ctl.color_cfg.enable()
|
||||
|
||||
if self.importing_legacy then
|
||||
self.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=14,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
self.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"}
|
||||
|
||||
local function go_home()
|
||||
main_pane.set_value(1)
|
||||
net_pane.set_value(1)
|
||||
fac_pane.set_value(1)
|
||||
mon_pane.set_value(1)
|
||||
clr_pane.set_value(1)
|
||||
sum_pane.set_value(1)
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_3,x=1,y=1,height=2,text="The old config.lua and coord.settings files will now be deleted, then the configurator will exit."}
|
||||
|
||||
local function delete_legacy()
|
||||
fs.delete("/coordinator/config.lua")
|
||||
fs.delete("/coord.settings")
|
||||
exit()
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Tool Functions
|
||||
|
||||
-- load a legacy config file
|
||||
function tool_ctl.load_legacy()
|
||||
local config = require("coordinator.config")
|
||||
|
||||
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
|
||||
tmp_cfg.CRD_Channel = config.CRD_CHANNEL
|
||||
tmp_cfg.PKT_Channel = config.PKT_CHANNEL
|
||||
tmp_cfg.SVR_Timeout = config.SV_TIMEOUT
|
||||
tmp_cfg.API_Timeout = config.API_TIMEOUT
|
||||
tmp_cfg.TrustedRange = config.TRUSTED_RANGE
|
||||
tmp_cfg.AuthKey = config.AUTH_KEY or ""
|
||||
|
||||
tmp_cfg.UnitCount = config.NUM_UNITS
|
||||
tmp_cfg.DisableFlowView = config.DISABLE_FLOW_VIEW
|
||||
tmp_cfg.SpeakerVolume = config.SOUNDER_VOLUME
|
||||
tmp_cfg.Time24Hour = config.TIME_24_HOUR
|
||||
|
||||
tmp_cfg.LogMode = config.LOG_MODE
|
||||
tmp_cfg.LogPath = config.LOG_PATH
|
||||
tmp_cfg.LogDebug = config.LOG_DEBUG or false
|
||||
|
||||
settings.load("/coord.settings")
|
||||
|
||||
tmp_cfg.MainDisplay = settings.get("PRIMARY_DISPLAY")
|
||||
tmp_cfg.FlowDisplay = settings.get("FLOW_DISPLAY")
|
||||
tmp_cfg.UnitDisplays = settings.get("UNIT_DISPLAYS", {})
|
||||
|
||||
-- if there are extra monitor entries, delete them now
|
||||
-- not doing so will cause the app to fail to start
|
||||
if tool_ctl.is_int_min_max(tmp_cfg.UnitCount, 1, 4) then
|
||||
for i = tmp_cfg.UnitCount + 1, 4 do tmp_cfg.UnitDisplays[i] = nil end
|
||||
end
|
||||
|
||||
if settings.get("ControlStates") == nil then
|
||||
local ctrl_states = {
|
||||
process = settings.get("PROCESS"),
|
||||
waste_modes = settings.get("WASTE_MODES"),
|
||||
priority_groups = settings.get("PRIORITY_GROUPS"),
|
||||
}
|
||||
|
||||
settings.set("ControlStates", ctrl_states)
|
||||
end
|
||||
|
||||
settings.unset("PRIMARY_DISPLAY")
|
||||
settings.unset("FLOW_DISPLAY")
|
||||
settings.unset("UNIT_DISPLAYS")
|
||||
settings.unset("PROCESS")
|
||||
settings.unset("WASTE_MODES")
|
||||
settings.unset("PRIORITY_GROUPS")
|
||||
|
||||
tool_ctl.gen_summary(tmp_cfg)
|
||||
sum_pane.set_value(1)
|
||||
main_pane.set_value(9)
|
||||
self.importing_legacy = true
|
||||
end
|
||||
|
||||
-- expose the auth key on the summary page
|
||||
function self.show_auth_key()
|
||||
self.show_key_btn.disable()
|
||||
self.auth_key_textbox.set_value(self.auth_key_value)
|
||||
end
|
||||
|
||||
-- generate the summary list
|
||||
---@param cfg crd_config
|
||||
function tool_ctl.gen_summary(cfg)
|
||||
setting_list.remove_all()
|
||||
|
||||
local alternate = false
|
||||
local inner_width = setting_list.get_width() - 1
|
||||
|
||||
self.show_key_btn.enable()
|
||||
self.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")
|
||||
elseif f[1] == "GreenPuPellet" then
|
||||
val = tri(raw, "Green Pu/Cyan Po", "Cyan Pu/Green Po")
|
||||
elseif f[1] == "TempScale" then
|
||||
val = util.strval(types.TEMP_SCALE_NAMES[raw])
|
||||
elseif f[1] == "EnergyScale" then
|
||||
val = util.strval(types.ENERGY_SCALE_NAMES[raw])
|
||||
elseif f[1] == "MainTheme" then
|
||||
val = util.strval(themes.ui_theme_name(raw))
|
||||
elseif f[1] == "FrontPanelTheme" then
|
||||
val = util.strval(themes.fp_theme_name(raw))
|
||||
elseif f[1] == "ColorMode" then
|
||||
val = util.strval(themes.color_mode_name(raw))
|
||||
elseif f[1] == "UnitDisplays" and type(cfg.UnitDisplays) == "table" then
|
||||
val = ""
|
||||
for idx = 1, #cfg.UnitDisplays do
|
||||
val = val .. util.trinary(idx == 1, "", "\n") .. util.sprintf(" \x07 Unit %d - %s", idx, cfg.UnitDisplays[idx])
|
||||
end
|
||||
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) or string.find(val, "\n") then
|
||||
local lines = util.strwrap(val, inner_width)
|
||||
height = #lines + 1
|
||||
end
|
||||
|
||||
if (f[1] == "UnitDisplays") and (height == 1) and (val ~= "<not set>") then height = 2 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}
|
||||
else
|
||||
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||
end
|
||||
|
||||
if f[1] == "AuthKey" then self.auth_key_textbox = textbox end
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
end
|
||||
|
||||
return system
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@ local LINK_TIMEOUT = 60.0
|
||||
local coordinator = {}
|
||||
|
||||
---@type crd_config
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
local config = {}
|
||||
|
||||
coordinator.config = config
|
||||
@@ -37,7 +38,9 @@ function coordinator.load_config()
|
||||
config.UnitCount = settings.get("UnitCount")
|
||||
config.SpeakerVolume = settings.get("SpeakerVolume")
|
||||
config.Time24Hour = settings.get("Time24Hour")
|
||||
config.GreenPuPellet = settings.get("GreenPuPellet")
|
||||
config.TempScale = settings.get("TempScale")
|
||||
config.EnergyScale = settings.get("EnergyScale")
|
||||
|
||||
config.DisableFlowView = settings.get("DisableFlowView")
|
||||
config.MainDisplay = settings.get("MainDisplay")
|
||||
@@ -65,8 +68,11 @@ function coordinator.load_config()
|
||||
cfv.assert_type_int(config.UnitCount)
|
||||
cfv.assert_range(config.UnitCount, 1, 4)
|
||||
cfv.assert_type_bool(config.Time24Hour)
|
||||
cfv.assert_type_bool(config.GreenPuPellet)
|
||||
cfv.assert_type_int(config.TempScale)
|
||||
cfv.assert_range(config.TempScale, 1, 4)
|
||||
cfv.assert_type_int(config.EnergyScale)
|
||||
cfv.assert_range(config.EnergyScale, 1, 3)
|
||||
|
||||
cfv.assert_type_bool(config.DisableFlowView)
|
||||
cfv.assert_type_table(config.UnitDisplays)
|
||||
@@ -108,12 +114,12 @@ function coordinator.load_config()
|
||||
|
||||
---@class monitors_struct
|
||||
local monitors = {
|
||||
main = nil, ---@type table|nil
|
||||
main = nil, ---@type Monitor|nil
|
||||
main_name = "",
|
||||
flow = nil, ---@type table|nil
|
||||
flow = nil, ---@type Monitor|nil
|
||||
flow_name = "",
|
||||
unit_displays = {},
|
||||
unit_name_map = {}
|
||||
unit_displays = {}, ---@type Monitor[]
|
||||
unit_name_map = {} ---@type string[]
|
||||
}
|
||||
|
||||
local mon_cfv = util.new_validator()
|
||||
@@ -232,8 +238,8 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
local self = {
|
||||
sv_linked = false,
|
||||
sv_addr = comms.BROADCAST,
|
||||
sv_seq_num = 0,
|
||||
sv_r_seq_num = nil,
|
||||
sv_seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
|
||||
sv_r_seq_num = nil, ---@type nil|integer
|
||||
sv_config_err = false,
|
||||
last_est_ack = ESTABLISH_ACK.ALLOW,
|
||||
last_api_est_acks = {},
|
||||
@@ -293,6 +299,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
|
||||
-- attempt connection establishment
|
||||
local function _send_establish()
|
||||
self.sv_r_seq_num = nil
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRD })
|
||||
end
|
||||
|
||||
@@ -375,6 +382,18 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {})
|
||||
end
|
||||
|
||||
-- send the resume ready state to the supervisor
|
||||
---@param mode PROCESS process control mode
|
||||
---@param burn_target number burn rate target
|
||||
---@param charge_target number charge level target
|
||||
---@param gen_target number generation rate target
|
||||
---@param limits number[] unit burn rate limits
|
||||
function public.send_ready(mode, burn_target, charge_target, gen_target, limits)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.PROCESS_READY, {
|
||||
mode, burn_target, charge_target, gen_target, limits
|
||||
})
|
||||
end
|
||||
|
||||
-- send a facility command
|
||||
---@param cmd FAC_COMMAND command
|
||||
---@param option any? optional option options for the optional options (like waste mode)
|
||||
@@ -383,10 +402,14 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
end
|
||||
|
||||
-- send the auto process control configuration with a start command
|
||||
---@param auto_cfg coord_auto_config configuration
|
||||
function public.send_auto_start(auto_cfg)
|
||||
---@param mode PROCESS process control mode
|
||||
---@param burn_target number burn rate target
|
||||
---@param charge_target number charge level target
|
||||
---@param gen_target number generation rate target
|
||||
---@param limits number[] unit burn rate limits
|
||||
function public.send_auto_start(mode, burn_target, charge_target, gen_target, limits)
|
||||
_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
|
||||
FAC_COMMAND.START, mode, burn_target, charge_target, gen_target, limits
|
||||
})
|
||||
end
|
||||
|
||||
@@ -492,7 +515,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
_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)
|
||||
local id = apisessions.establish_session(src_addr, packet.scada_frame.seq_num(), firmware_v)
|
||||
coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id))
|
||||
|
||||
local conf = iocontrol.get_db().facility.conf
|
||||
@@ -515,15 +538,15 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
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()
|
||||
elseif self.sv_linked and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
|
||||
log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
|
||||
elseif self.sv_r_seq_num ~= packet.scada_frame.seq_num() then
|
||||
log.warning("sequence out-of-order: next = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
return false
|
||||
elseif self.sv_linked and src_addr ~= self.sv_addr then
|
||||
log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?")
|
||||
return false
|
||||
else
|
||||
self.sv_r_seq_num = packet.scada_frame.seq_num()
|
||||
self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
|
||||
end
|
||||
|
||||
-- feed watchdog on valid sequence number
|
||||
@@ -572,9 +595,9 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
local ack = packet.data[2] == true
|
||||
|
||||
if cmd == FAC_COMMAND.SCRAM_ALL then
|
||||
iocontrol.get_db().facility.scram_ack(ack)
|
||||
process.fac_ack(cmd, ack)
|
||||
elseif cmd == FAC_COMMAND.STOP then
|
||||
iocontrol.get_db().facility.stop_ack(ack)
|
||||
process.fac_ack(cmd, ack)
|
||||
elseif cmd == FAC_COMMAND.START then
|
||||
if packet.length == 7 then
|
||||
process.start_ack_handle({ table.unpack(packet.data, 2) })
|
||||
@@ -582,11 +605,13 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch")
|
||||
end
|
||||
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
|
||||
iocontrol.get_db().facility.ack_alarms_ack(ack)
|
||||
process.fac_ack(cmd, ack)
|
||||
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
|
||||
process.waste_ack_handle(packet.data[2])
|
||||
elseif cmd == FAC_COMMAND.SET_PU_FB then
|
||||
process.pu_fb_ack_handle(packet.data[2])
|
||||
elseif cmd == FAC_COMMAND.SET_SPS_LP then
|
||||
process.sps_lp_ack_handle(packet.data[2])
|
||||
else
|
||||
log.debug(util.c("received facility command ack with unknown command ", cmd))
|
||||
end
|
||||
@@ -617,25 +642,19 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
local unit_id = packet.data[2]
|
||||
local ack = packet.data[3] == true
|
||||
|
||||
local unit = iocontrol.get_db().units[unit_id] ---@type ioctl_unit
|
||||
local unit = iocontrol.get_db().units[unit_id]
|
||||
|
||||
if unit ~= nil then
|
||||
if cmd == UNIT_COMMAND.SCRAM then
|
||||
unit.scram_ack(ack)
|
||||
process.unit_ack(unit_id, cmd, ack)
|
||||
elseif cmd == UNIT_COMMAND.START then
|
||||
unit.start_ack(ack)
|
||||
process.unit_ack(unit_id, cmd, ack)
|
||||
elseif cmd == UNIT_COMMAND.RESET_RPS then
|
||||
unit.reset_rps_ack(ack)
|
||||
elseif cmd == UNIT_COMMAND.SET_BURN then
|
||||
unit.set_burn_ack(ack)
|
||||
elseif cmd == UNIT_COMMAND.SET_WASTE then
|
||||
unit.set_waste_ack(ack)
|
||||
process.unit_ack(unit_id, cmd, ack)
|
||||
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
|
||||
unit.ack_alarms_ack(ack)
|
||||
elseif cmd == UNIT_COMMAND.SET_GROUP then
|
||||
-- UI will be updated to display current group if changed successfully
|
||||
process.unit_ack(unit_id, cmd, ack)
|
||||
else
|
||||
log.debug(util.c("received unit command ack with unknown command ", cmd))
|
||||
log.debug(util.c("received unsupported unit command ack for command ", cmd))
|
||||
end
|
||||
else
|
||||
log.debug(util.c("received unit command ack with unknown unit ", unit_id))
|
||||
@@ -702,11 +721,10 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
|
||||
if conf.num_units == config.UnitCount then
|
||||
-- init io controller
|
||||
iocontrol.init(conf, public, config.TempScale)
|
||||
iocontrol.init(conf, public, config.TempScale, config.EnergyScale)
|
||||
|
||||
self.sv_addr = src_addr
|
||||
self.sv_linked = true
|
||||
self.sv_r_seq_num = nil
|
||||
self.sv_config_err = false
|
||||
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,55 +7,84 @@ local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
local F_CMD = comms.FAC_COMMAND
|
||||
local U_CMD = comms.UNIT_COMMAND
|
||||
|
||||
local PROCESS = types.PROCESS
|
||||
local PRODUCT = types.WASTE_PRODUCT
|
||||
|
||||
local REQUEST_TIMEOUT_MS = 10000
|
||||
|
||||
---@class process_controller
|
||||
local process = {}
|
||||
|
||||
local self = {
|
||||
io = nil, ---@type ioctl
|
||||
comms = nil, ---@type coord_comms
|
||||
---@class coord_control_states
|
||||
local pctl = {
|
||||
io = nil, ---@type ioctl
|
||||
comms = nil, ---@type coord_comms
|
||||
---@class sys_control_states
|
||||
control_states = {
|
||||
---@class coord_auto_config
|
||||
---@class sys_auto_config
|
||||
process = {
|
||||
mode = PROCESS.INACTIVE,
|
||||
mode = PROCESS.INACTIVE, ---@type PROCESS
|
||||
burn_target = 0.0,
|
||||
charge_target = 0.0,
|
||||
gen_target = 0.0,
|
||||
limits = {},
|
||||
waste_product = PRODUCT.PLUTONIUM,
|
||||
limits = {}, ---@type number[]
|
||||
waste_product = PRODUCT.PLUTONIUM, ---@type WASTE_PRODUCT
|
||||
pu_fallback = false,
|
||||
sps_low_power = false
|
||||
},
|
||||
waste_modes = {},
|
||||
priority_groups = {}
|
||||
waste_modes = {}, ---@type WASTE_MODE[]
|
||||
priority_groups = {} ---@type AUTO_GROUP[]
|
||||
},
|
||||
commands = {
|
||||
unit = {}, ---@type process_command_state[][]
|
||||
fac = {} ---@type process_command_state[]
|
||||
}
|
||||
}
|
||||
|
||||
--------------------------
|
||||
-- UNIT COMMAND CONTROL --
|
||||
--------------------------
|
||||
---@class process_command_state
|
||||
---@field active boolean if this command is live
|
||||
---@field timeout integer expiration time of this command request
|
||||
---@field requestors function[] list of callbacks from the requestors
|
||||
|
||||
-- write auto process control to config file
|
||||
---@return boolean saved
|
||||
local function _write_auto_config()
|
||||
-- save config
|
||||
settings.set("ControlStates", pctl.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 saved
|
||||
end
|
||||
|
||||
--#region Core
|
||||
|
||||
-- initialize the process controller
|
||||
---@param iocontrol ioctl iocontrl system
|
||||
---@param coord_comms coord_comms coordinator communications
|
||||
function process.init(iocontrol, coord_comms)
|
||||
self.io = iocontrol
|
||||
self.comms = coord_comms
|
||||
pctl.io = iocontrol
|
||||
pctl.comms = coord_comms
|
||||
|
||||
local ctl_proc = self.control_states.process
|
||||
-- create command handling objects
|
||||
for _, v in pairs(F_CMD) do pctl.commands.fac[v] = { active = false, timeout = 0, requestors = {} } end
|
||||
for i = 1, pctl.io.facility.num_units do
|
||||
pctl.commands.unit[i] = {}
|
||||
for _, v in pairs(U_CMD) do pctl.commands.unit[i][v] = { active = false, timeout = 0, requestors = {} } end
|
||||
end
|
||||
|
||||
for i = 1, self.io.facility.num_units do
|
||||
local ctl_proc = pctl.control_states.process
|
||||
|
||||
for i = 1, pctl.io.facility.num_units do
|
||||
ctl_proc.limits[i] = 0.1
|
||||
end
|
||||
|
||||
local ctrl_states = settings.get("ControlStates", {})
|
||||
local config = ctrl_states.process ---@type coord_auto_config
|
||||
local ctrl_states = settings.get("ControlStates", {}) ---@type sys_control_states
|
||||
local config = ctrl_states.process
|
||||
|
||||
-- facility auto control configuration
|
||||
if type(config) == "table" then
|
||||
@@ -68,123 +97,339 @@ function process.init(iocontrol, coord_comms)
|
||||
ctl_proc.pu_fallback = config.pu_fallback
|
||||
ctl_proc.sps_low_power = config.sps_low_power
|
||||
|
||||
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)
|
||||
pctl.io.facility.ps.publish("process_mode", ctl_proc.mode)
|
||||
pctl.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
|
||||
pctl.io.facility.ps.publish("process_charge_target", pctl.io.energy_convert_from_fe(ctl_proc.charge_target))
|
||||
pctl.io.facility.ps.publish("process_gen_target", pctl.io.energy_convert_from_fe(ctl_proc.gen_target))
|
||||
pctl.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
|
||||
pctl.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback)
|
||||
pctl.io.facility.ps.publish("process_sps_low_power", ctl_proc.sps_low_power)
|
||||
|
||||
for id = 1, math.min(#ctl_proc.limits, self.io.facility.num_units) do
|
||||
local unit = self.io.units[id] ---@type ioctl_unit
|
||||
for id = 1, math.min(#ctl_proc.limits, pctl.io.facility.num_units) do
|
||||
local unit = pctl.io.units[id]
|
||||
unit.unit_ps.publish("burn_limit", ctl_proc.limits[id])
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded auto control settings")
|
||||
|
||||
-- notify supervisor of auto waste config
|
||||
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)
|
||||
pctl.comms.send_fac_command(F_CMD.SET_WASTE_MODE, ctl_proc.waste_product)
|
||||
pctl.comms.send_fac_command(F_CMD.SET_PU_FB, ctl_proc.pu_fallback)
|
||||
pctl.comms.send_fac_command(F_CMD.SET_SPS_LP, ctl_proc.sps_low_power)
|
||||
end
|
||||
|
||||
-- unit waste states
|
||||
local waste_modes = ctrl_states.waste_modes ---@type table|nil
|
||||
local waste_modes = ctrl_states.waste_modes
|
||||
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)
|
||||
pctl.control_states.waste_modes[id] = mode
|
||||
pctl.comms.send_unit_command(U_CMD.SET_WASTE, id, mode)
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded unit waste mode settings")
|
||||
end
|
||||
|
||||
-- unit priority groups
|
||||
local prio_groups = ctrl_states.priority_groups ---@type table|nil
|
||||
local prio_groups = ctrl_states.priority_groups
|
||||
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)
|
||||
pctl.control_states.priority_groups[id] = group
|
||||
pctl.comms.send_unit_command(U_CMD.SET_GROUP, id, group)
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded priority groups settings")
|
||||
end
|
||||
|
||||
-- report to the supervisor all initial configuration data has been sent
|
||||
-- startup resume can occur if needed
|
||||
local p = ctl_proc
|
||||
pctl.comms.send_ready(p.mode, p.burn_target, p.charge_target, p.gen_target, p.limits)
|
||||
end
|
||||
|
||||
-- facility SCRAM command
|
||||
function process.fac_scram()
|
||||
self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL)
|
||||
log.debug("PROCESS: FAC SCRAM ALL")
|
||||
-- create a handle to process control for usage of commands that get acknowledgements
|
||||
function process.create_handle()
|
||||
---@class process_handle
|
||||
local handle = {}
|
||||
|
||||
-- add this handle to the requestors and activate the command if inactive
|
||||
---@param cmd process_command_state
|
||||
---@param ack function
|
||||
local function request(cmd, ack)
|
||||
local new = not cmd.active
|
||||
|
||||
if new then
|
||||
cmd.active = true
|
||||
cmd.timeout = util.time_ms() + REQUEST_TIMEOUT_MS
|
||||
end
|
||||
|
||||
table.insert(cmd.requestors, ack)
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
local function u_request(u_id, cmd_id, ack) return request(pctl.commands.unit[u_id][cmd_id], ack) end
|
||||
local function f_request(cmd_id, ack) return request(pctl.commands.fac[cmd_id], ack) end
|
||||
|
||||
--#region Facility Commands
|
||||
|
||||
-- facility SCRAM command
|
||||
function handle.fac_scram()
|
||||
if f_request(F_CMD.SCRAM_ALL, handle.fac_ack.on_scram) then
|
||||
pctl.comms.send_fac_command(F_CMD.SCRAM_ALL)
|
||||
log.debug("PROCESS: FAC SCRAM ALL")
|
||||
end
|
||||
end
|
||||
|
||||
-- facility alarm acknowledge command
|
||||
function handle.fac_ack_alarms()
|
||||
if f_request(F_CMD.ACK_ALL_ALARMS, handle.fac_ack.on_ack_alarms) then
|
||||
pctl.comms.send_fac_command(F_CMD.ACK_ALL_ALARMS)
|
||||
log.debug("PROCESS: FAC ACK ALL ALARMS")
|
||||
end
|
||||
end
|
||||
|
||||
-- start automatic process control with current settings
|
||||
function handle.process_start()
|
||||
if f_request(F_CMD.START, handle.fac_ack.on_start) then
|
||||
local p = pctl.control_states.process
|
||||
pctl.comms.send_auto_start(p.mode, p.burn_target, p.charge_target, p.gen_target, p.limits)
|
||||
log.debug("PROCESS: START AUTO CTRL")
|
||||
end
|
||||
end
|
||||
|
||||
-- start automatic process control with remote settings that haven't been set on the coordinator
|
||||
---@param mode PROCESS process control mode
|
||||
---@param burn_target number burn rate target
|
||||
---@param charge_target number charge level target
|
||||
---@param gen_target number generation rate target
|
||||
---@param limits number[] unit burn rate limits
|
||||
function handle.process_start_remote(mode, burn_target, charge_target, gen_target, limits)
|
||||
if f_request(F_CMD.START, handle.fac_ack.on_start) then
|
||||
pctl.comms.send_auto_start(mode, burn_target, charge_target, gen_target, limits)
|
||||
log.debug("PROCESS: START AUTO CTRL")
|
||||
end
|
||||
end
|
||||
|
||||
-- stop process control
|
||||
function handle.process_stop()
|
||||
if f_request(F_CMD.STOP, handle.fac_ack.on_stop) then
|
||||
pctl.comms.send_fac_command(F_CMD.STOP)
|
||||
log.debug("PROCESS: STOP AUTO CTRL")
|
||||
end
|
||||
end
|
||||
|
||||
handle.fac_ack = {}
|
||||
|
||||
-- luacheck: no unused args
|
||||
|
||||
-- facility SCRAM ack, override to implement
|
||||
---@param success boolean
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function handle.fac_ack.on_scram(success) end
|
||||
|
||||
-- facility acknowledge all alarms ack, override to implement
|
||||
---@param success boolean
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function handle.fac_ack.on_ack_alarms(success) end
|
||||
|
||||
-- facility auto control start ack, override to implement
|
||||
---@param success boolean
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function handle.fac_ack.on_start(success) end
|
||||
|
||||
-- facility auto control stop ack, override to implement
|
||||
---@param success boolean
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function handle.fac_ack.on_stop(success) end
|
||||
|
||||
-- luacheck: unused args
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Unit Commands
|
||||
|
||||
-- start a reactor
|
||||
---@param id integer unit ID
|
||||
function handle.start(id)
|
||||
if u_request(id, U_CMD.START, handle.unit_ack[id].on_start) then
|
||||
pctl.io.units[id].control_state = true
|
||||
pctl.comms.send_unit_command(U_CMD.START, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
|
||||
end
|
||||
end
|
||||
|
||||
-- SCRAM reactor
|
||||
---@param id integer unit ID
|
||||
function handle.scram(id)
|
||||
if u_request(id, U_CMD.SCRAM, handle.unit_ack[id].on_scram) then
|
||||
pctl.io.units[id].control_state = false
|
||||
pctl.comms.send_unit_command(U_CMD.SCRAM, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
|
||||
end
|
||||
end
|
||||
|
||||
-- reset reactor protection system
|
||||
---@param id integer unit ID
|
||||
function handle.reset_rps(id)
|
||||
if u_request(id, U_CMD.RESET_RPS, handle.unit_ack[id].on_rps_reset) then
|
||||
pctl.comms.send_unit_command(U_CMD.RESET_RPS, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
|
||||
end
|
||||
end
|
||||
|
||||
-- acknowledge all alarms
|
||||
---@param id integer unit ID
|
||||
function handle.ack_all_alarms(id)
|
||||
if u_request(id, U_CMD.ACK_ALL_ALARMS, handle.unit_ack[id].on_ack_alarms) then
|
||||
pctl.comms.send_unit_command(U_CMD.ACK_ALL_ALARMS, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
|
||||
end
|
||||
end
|
||||
|
||||
-- unit command acknowledgement callbacks, indexed by unit ID
|
||||
---@type process_unit_ack[]
|
||||
handle.unit_ack = {}
|
||||
|
||||
for u = 1, pctl.io.facility.num_units do
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
handle.unit_ack[u] = {}
|
||||
|
||||
---@class process_unit_ack
|
||||
local u_ack = handle.unit_ack[u]
|
||||
|
||||
-- luacheck: no unused args
|
||||
|
||||
-- unit start ack, override to implement
|
||||
---@param success boolean
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function u_ack.on_start(success) end
|
||||
|
||||
-- unit SCRAM ack, override to implement
|
||||
---@param success boolean
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function u_ack.on_scram(success) end
|
||||
|
||||
-- unit RPS reset ack, override to implement
|
||||
---@param success boolean
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function u_ack.on_rps_reset(success) end
|
||||
|
||||
-- unit acknowledge all alarms ack, override to implement
|
||||
---@param success boolean
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function u_ack.on_ack_alarms(success) end
|
||||
|
||||
-- luacheck: unused args
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
return handle
|
||||
end
|
||||
|
||||
-- facility alarm acknowledge command
|
||||
function process.fac_ack_alarms()
|
||||
self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_ALARMS)
|
||||
log.debug("PROCESS: FAC ACK ALL ALARMS")
|
||||
-- clear outstanding process commands that have timed out
|
||||
function process.clear_timed_out()
|
||||
local now = util.time_ms()
|
||||
local objs = { pctl.commands.fac, table.unpack(pctl.commands.unit) }
|
||||
|
||||
for _, obj in pairs(objs) do
|
||||
-- cancel expired requests
|
||||
for _, cmd in pairs(obj) do
|
||||
if cmd.active and now > cmd.timeout then
|
||||
cmd.active = false
|
||||
cmd.requestors = {}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- start reactor
|
||||
---@param id integer unit ID
|
||||
function process.start(id)
|
||||
self.io.units[id].control_state = true
|
||||
self.comms.send_unit_command(UNIT_COMMAND.START, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
|
||||
-- get the control states table
|
||||
---@nodiscard
|
||||
function process.get_control_states() return pctl.control_states end
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Command Handling
|
||||
|
||||
-- handle a command acknowledgement
|
||||
---@param cmd_state process_command_state
|
||||
---@param success boolean if the command was successful
|
||||
local function cmd_ack(cmd_state, success)
|
||||
if cmd_state.active then
|
||||
cmd_state.active = false
|
||||
|
||||
-- call all acknowledge callback functions
|
||||
for i = 1, #cmd_state.requestors do
|
||||
cmd_state.requestors[i](success)
|
||||
end
|
||||
|
||||
cmd_state.requestors = {}
|
||||
end
|
||||
end
|
||||
|
||||
-- SCRAM reactor
|
||||
---@param id integer unit ID
|
||||
function process.scram(id)
|
||||
self.io.units[id].control_state = false
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
|
||||
-- handle a facility command acknowledgement
|
||||
---@param command FAC_COMMAND command
|
||||
---@param success boolean if the command was successful
|
||||
function process.fac_ack(command, success)
|
||||
cmd_ack(pctl.commands.fac[command], success)
|
||||
end
|
||||
|
||||
-- reset reactor protection system
|
||||
---@param id integer unit ID
|
||||
function process.reset_rps(id)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.RESET_RPS, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
|
||||
-- handle a unit command acknowledgement
|
||||
---@param unit integer unit ID
|
||||
---@param command UNIT_COMMAND command
|
||||
---@param success boolean if the command was successful
|
||||
function process.unit_ack(unit, command, success)
|
||||
cmd_ack(pctl.commands.unit[unit][command], success)
|
||||
end
|
||||
|
||||
--#region One-Way Commands (no acknowledgements)
|
||||
|
||||
-- set burn rate
|
||||
---@param id integer unit ID
|
||||
---@param rate number burn rate
|
||||
function process.set_rate(id, rate)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate)
|
||||
pctl.comms.send_unit_command(U_CMD.SET_BURN, id, rate)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
|
||||
end
|
||||
|
||||
-- assign a unit to a group
|
||||
---@param unit_id integer unit ID
|
||||
---@param group_id integer|0 group ID or 0 for independent
|
||||
function process.set_group(unit_id, group_id)
|
||||
pctl.comms.send_unit_command(U_CMD.SET_GROUP, unit_id, group_id)
|
||||
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
|
||||
|
||||
pctl.control_states.priority_groups[unit_id] = group_id
|
||||
settings.set("ControlStates", pctl.control_states)
|
||||
|
||||
if not settings.save("/coordinator.settings") then
|
||||
log.error("process.set_group(): failed to save coordinator settings file")
|
||||
end
|
||||
end
|
||||
|
||||
-- set waste mode
|
||||
---@param id integer unit ID
|
||||
---@param mode integer waste mode
|
||||
function process.set_unit_waste(id, mode)
|
||||
-- publish so that if it fails then it gets reset
|
||||
self.io.units[id].unit_ps.publish("U_WasteMode", mode)
|
||||
pctl.io.units[id].unit_ps.publish("U_WasteMode", mode)
|
||||
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
|
||||
pctl.comms.send_unit_command(U_CMD.SET_WASTE, id, mode)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
|
||||
|
||||
self.control_states.waste_modes[id] = mode
|
||||
settings.set("ControlStates", self.control_states)
|
||||
pctl.control_states.waste_modes[id] = mode
|
||||
settings.set("ControlStates", pctl.control_states)
|
||||
|
||||
if not settings.save("/coordinator.settings") then
|
||||
log.error("process.set_unit_waste(): failed to save coordinator settings file")
|
||||
end
|
||||
end
|
||||
|
||||
-- acknowledge all alarms
|
||||
---@param id integer unit ID
|
||||
function process.ack_all_alarms(id)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALL_ALARMS, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
|
||||
end
|
||||
|
||||
-- acknowledge an alarm
|
||||
---@param id integer unit ID
|
||||
---@param alarm integer alarm ID
|
||||
function process.ack_alarm(id, alarm)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALARM, id, alarm)
|
||||
pctl.comms.send_unit_command(U_CMD.ACK_ALARM, id, alarm)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
|
||||
end
|
||||
|
||||
@@ -192,100 +437,48 @@ end
|
||||
---@param id integer unit ID
|
||||
---@param alarm integer alarm ID
|
||||
function process.reset_alarm(id, alarm)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm)
|
||||
pctl.comms.send_unit_command(U_CMD.RESET_ALARM, id, alarm)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
|
||||
end
|
||||
|
||||
-- assign a unit to a group
|
||||
---@param unit_id integer unit ID
|
||||
---@param group_id integer|0 group ID or 0 for independent
|
||||
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))
|
||||
|
||||
self.control_states.priority_groups[unit_id] = group_id
|
||||
settings.set("ControlStates", self.control_states)
|
||||
|
||||
if not settings.save("/coordinator.settings") then
|
||||
log.error("process.set_group(): failed to save coordinator settings file")
|
||||
end
|
||||
end
|
||||
--#endregion
|
||||
|
||||
--------------------------
|
||||
-- AUTO PROCESS CONTROL --
|
||||
--------------------------
|
||||
|
||||
-- write auto process control to config file
|
||||
local function _write_auto_config()
|
||||
-- save config
|
||||
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 saved
|
||||
end
|
||||
|
||||
-- stop automatic process control
|
||||
function process.stop_auto()
|
||||
self.comms.send_fac_command(FAC_COMMAND.STOP)
|
||||
log.debug("PROCESS: STOP AUTO CTL")
|
||||
end
|
||||
|
||||
-- start automatic process control
|
||||
function process.start_auto()
|
||||
self.comms.send_auto_start(self.control_states.process)
|
||||
log.debug("PROCESS: START AUTO CTL")
|
||||
end
|
||||
|
||||
-- set automatic process control waste mode
|
||||
---@param product WASTE_PRODUCT waste product for auto control
|
||||
function process.set_process_waste(product)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, product)
|
||||
|
||||
pctl.comms.send_fac_command(F_CMD.SET_WASTE_MODE, product)
|
||||
log.debug(util.c("PROCESS: SET WASTE ", product))
|
||||
|
||||
-- update config table and save
|
||||
self.control_states.process.waste_product = product
|
||||
_write_auto_config()
|
||||
end
|
||||
|
||||
-- set automatic process control plutonium fallback
|
||||
---@param enabled boolean whether to enable plutonium fallback
|
||||
function process.set_pu_fallback(enabled)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, enabled)
|
||||
|
||||
pctl.comms.send_fac_command(F_CMD.SET_PU_FB, enabled)
|
||||
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
|
||||
|
||||
-- update config table and save
|
||||
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)
|
||||
|
||||
pctl.comms.send_fac_command(F_CMD.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
|
||||
|
||||
-- save process control settings
|
||||
---@param mode PROCESS control mode
|
||||
---@param mode PROCESS process control mode
|
||||
---@param burn_target number burn rate target
|
||||
---@param charge_target number charge target
|
||||
---@param charge_target number charge level target
|
||||
---@param gen_target number generation rate target
|
||||
---@param limits table unit burn rate limits
|
||||
---@param limits number[] unit burn rate limits
|
||||
function process.save(mode, burn_target, charge_target, gen_target, limits)
|
||||
log.debug("PROCESS: SAVE")
|
||||
|
||||
-- update config table
|
||||
local ctl_proc = self.control_states.process
|
||||
local ctl_proc = pctl.control_states.process
|
||||
ctl_proc.mode = mode
|
||||
ctl_proc.burn_target = burn_target
|
||||
ctl_proc.charge_target = charge_target
|
||||
@@ -293,7 +486,7 @@ function process.save(mode, burn_target, charge_target, gen_target, limits)
|
||||
ctl_proc.limits = limits
|
||||
|
||||
-- save config
|
||||
self.io.facility.save_cfg_ack(_write_auto_config())
|
||||
pctl.io.facility.save_cfg_ack(_write_auto_config())
|
||||
end
|
||||
|
||||
-- handle a start command acknowledgement
|
||||
@@ -301,39 +494,57 @@ end
|
||||
function process.start_ack_handle(response)
|
||||
local ack = response[1]
|
||||
|
||||
local ctl_proc = self.control_states.process
|
||||
local ctl_proc = pctl.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
|
||||
for i = 1, math.min(#response[6], pctl.io.facility.num_units) do
|
||||
ctl_proc.limits[i] = response[6][i]
|
||||
|
||||
local unit = self.io.units[i] ---@type ioctl_unit
|
||||
unit.unit_ps.publish("burn_limit", ctl_proc.limits[i])
|
||||
pctl.io.units[i].unit_ps.publish("burn_limit", ctl_proc.limits[i])
|
||||
end
|
||||
|
||||
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)
|
||||
pctl.io.facility.ps.publish("process_mode", ctl_proc.mode)
|
||||
pctl.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
|
||||
pctl.io.facility.ps.publish("process_charge_target", pctl.io.energy_convert_from_fe(ctl_proc.charge_target))
|
||||
pctl.io.facility.ps.publish("process_gen_target", pctl.io.energy_convert_from_fe(ctl_proc.gen_target))
|
||||
|
||||
self.io.facility.start_ack(ack)
|
||||
_write_auto_config()
|
||||
|
||||
process.fac_ack(F_CMD.START, ack)
|
||||
end
|
||||
|
||||
-- record waste product state after attempting to change it
|
||||
---@param response WASTE_PRODUCT supervisor waste product state
|
||||
-- record waste product settting after attempting to change it
|
||||
---@param response WASTE_PRODUCT supervisor waste product settting
|
||||
function process.waste_ack_handle(response)
|
||||
self.control_states.process.waste_product = response
|
||||
self.io.facility.ps.publish("process_waste_product", response)
|
||||
-- update config table and save
|
||||
pctl.control_states.process.waste_product = response
|
||||
_write_auto_config()
|
||||
|
||||
pctl.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
|
||||
-- record plutonium fallback settting after attempting to change it
|
||||
---@param response boolean supervisor plutonium fallback settting
|
||||
function process.pu_fb_ack_handle(response)
|
||||
self.control_states.process.pu_fallback = response
|
||||
self.io.facility.ps.publish("process_pu_fallback", response)
|
||||
-- update config table and save
|
||||
pctl.control_states.process.pu_fallback = response
|
||||
_write_auto_config()
|
||||
|
||||
pctl.io.facility.ps.publish("process_pu_fallback", response)
|
||||
end
|
||||
|
||||
-- record SPS low power settting after attempting to change it
|
||||
---@param response boolean supervisor SPS low power settting
|
||||
function process.sps_lp_ack_handle(response)
|
||||
-- update config table and save
|
||||
pctl.control_states.process.sps_low_power = response
|
||||
_write_auto_config()
|
||||
|
||||
pctl.io.facility.ps.publish("process_sps_low_power", response)
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
return process
|
||||
|
||||
@@ -19,7 +19,7 @@ local unit_view = require("coordinator.ui.layout.unit_view")
|
||||
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
|
||||
|
||||
@@ -30,20 +30,20 @@ local renderer = {}
|
||||
local engine = {
|
||||
color_mode = 1, ---@type COLOR_MODE
|
||||
monitors = nil, ---@type monitors_struct|nil
|
||||
dmesg_window = nil, ---@type table|nil
|
||||
dmesg_window = nil, ---@type Window|nil
|
||||
ui_ready = false,
|
||||
fp_ready = false,
|
||||
ui = {
|
||||
front_panel = nil, ---@type graphics_element|nil
|
||||
main_display = nil, ---@type graphics_element|nil
|
||||
flow_display = nil, ---@type graphics_element|nil
|
||||
unit_displays = {}
|
||||
front_panel = nil, ---@type DisplayBox|nil
|
||||
main_display = nil, ---@type DisplayBox|nil
|
||||
flow_display = nil, ---@type DisplayBox|nil
|
||||
unit_displays = {} ---@type (DisplayBox|nil)[]
|
||||
},
|
||||
disable_flow_view = false
|
||||
}
|
||||
|
||||
-- init a display to the "default", but set text scale to 0.5
|
||||
---@param monitor table monitor
|
||||
---@param monitor Monitor monitor
|
||||
local function _init_display(monitor)
|
||||
monitor.setTextScale(0.5)
|
||||
monitor.setTextColor(colors.white)
|
||||
@@ -64,7 +64,7 @@ local function _init_display(monitor)
|
||||
end
|
||||
|
||||
-- print out that the monitor is too small
|
||||
---@param monitor table monitor
|
||||
---@param monitor Monitor monitor
|
||||
local function _print_too_small(monitor)
|
||||
monitor.setCursorPos(1, 1)
|
||||
monitor.setBackgroundColor(colors.black)
|
||||
@@ -137,7 +137,7 @@ function renderer.try_start_fp()
|
||||
if not engine.fp_ready then
|
||||
-- show front panel view on terminal
|
||||
status, msg = pcall(function ()
|
||||
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root}
|
||||
engine.ui.front_panel = DisplayBox{window=term.current(),fg_bg=style.fp.root}
|
||||
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
|
||||
end)
|
||||
|
||||
@@ -275,7 +275,7 @@ function renderer.fp_ready() return engine.fp_ready end
|
||||
function renderer.ui_ready() return engine.ui_ready end
|
||||
|
||||
-- handle a monitor peripheral being disconnected
|
||||
---@param device table monitor
|
||||
---@param device Monitor monitor
|
||||
---@return boolean is_used if the monitor is one of the configured monitors
|
||||
function renderer.handle_disconnect(device)
|
||||
local is_used = false
|
||||
@@ -326,7 +326,7 @@ end
|
||||
|
||||
-- handle a monitor peripheral being reconnected
|
||||
---@param name string monitor name
|
||||
---@param device table monitor
|
||||
---@param device Monitor monitor
|
||||
---@return boolean is_used if the monitor is one of the configured monitors
|
||||
function renderer.handle_reconnect(name, device)
|
||||
local is_used = false
|
||||
@@ -373,7 +373,7 @@ function renderer.handle_resize(name)
|
||||
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
|
||||
local device = engine.monitors.main ---@type Monitor
|
||||
|
||||
-- this is necessary if the bottom left block was broken and on reconnect
|
||||
_init_display(device)
|
||||
@@ -416,7 +416,7 @@ function renderer.handle_resize(name)
|
||||
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
|
||||
local device = engine.monitors.flow ---@type Monitor
|
||||
|
||||
-- this is necessary if the bottom left block was broken and on reconnect
|
||||
_init_display(device)
|
||||
|
||||
@@ -13,7 +13,7 @@ local self = {
|
||||
nic = nil, ---@type nic
|
||||
config = nil, ---@type crd_config
|
||||
next_id = 0,
|
||||
sessions = {}
|
||||
sessions = {} ---@type pkt_session_struct[]
|
||||
}
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
@@ -89,10 +89,11 @@ end
|
||||
|
||||
-- establish a new API session
|
||||
---@nodiscard
|
||||
---@param source_addr integer
|
||||
---@param version string
|
||||
---@param source_addr integer pocket computer ID
|
||||
---@param i_seq_num integer initial (most recent) sequence number
|
||||
---@param version string pocket version
|
||||
---@return integer session_id
|
||||
function apisessions.establish_session(source_addr, version)
|
||||
function apisessions.establish_session(source_addr, i_seq_num, version)
|
||||
---@class pkt_session_struct
|
||||
local pkt_s = {
|
||||
open = true,
|
||||
@@ -105,7 +106,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, self.config.API_Timeout)
|
||||
pkt_s.instance = pocket.new_session(id, source_addr, i_seq_num, pkt_s.in_queue, pkt_s.out_queue, self.config.API_Timeout)
|
||||
table.insert(self.sessions, pkt_s)
|
||||
|
||||
local mt = {
|
||||
@@ -128,7 +129,7 @@ end
|
||||
---@param timer_event number
|
||||
function apisessions.check_all_watchdogs(timer_event)
|
||||
for i = 1, #self.sessions do
|
||||
local session = self.sessions[i] ---@type pkt_session_struct
|
||||
local session = self.sessions[i]
|
||||
if session.open then
|
||||
local triggered = session.instance.check_wd(timer_event)
|
||||
if triggered then
|
||||
@@ -142,7 +143,7 @@ end
|
||||
-- iterate all the API sessions
|
||||
function apisessions.iterate_all()
|
||||
for i = 1, #self.sessions do
|
||||
local session = self.sessions[i] ---@type pkt_session_struct
|
||||
local session = self.sessions[i]
|
||||
|
||||
if session.open and session.instance.iterate() then
|
||||
_api_handle_outq(session)
|
||||
@@ -167,7 +168,7 @@ end
|
||||
-- close all open connections
|
||||
function apisessions.close_all()
|
||||
for i = 1, #self.sessions do
|
||||
local session = self.sessions[i] ---@type pkt_session_struct
|
||||
local session = self.sessions[i]
|
||||
if session.open then _shutdown(session) end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local process = require("coordinator.process")
|
||||
|
||||
local pocket = {}
|
||||
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
|
||||
local AUTO_GROUP = types.AUTO_GROUP
|
||||
local WASTE_MODE = types.WASTE_MODE
|
||||
|
||||
-- retry time constants in ms
|
||||
-- local INITIAL_WAIT = 1500
|
||||
@@ -32,19 +39,22 @@ local PERIODICS = {
|
||||
---@nodiscard
|
||||
---@param id integer session ID
|
||||
---@param s_addr integer device source address
|
||||
---@param i_seq_num integer initial sequence number
|
||||
---@param in_queue mqueue in message queue
|
||||
---@param out_queue mqueue out message queue
|
||||
---@param timeout number communications timeout
|
||||
function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
local log_header = "pkt_session(" .. id .. "): "
|
||||
function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
local log_tag = "pkt_session(" .. id .. "): "
|
||||
|
||||
local self = {
|
||||
-- connection properties
|
||||
seq_num = 0,
|
||||
r_seq_num = nil,
|
||||
seq_num = i_seq_num + 2, -- next after the establish approval was sent
|
||||
r_seq_num = i_seq_num + 1,
|
||||
connected = true,
|
||||
conn_watchdog = util.new_watchdog(timeout),
|
||||
last_rtt = 0,
|
||||
-- process accessor handle
|
||||
proc_handle = process.create_handle(),
|
||||
-- periodic messages
|
||||
periodics = {
|
||||
last_update = 0,
|
||||
@@ -100,17 +110,33 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- link callback transmissions
|
||||
|
||||
local f_ack = self.proc_handle.fac_ack
|
||||
|
||||
f_ack.on_scram = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.SCRAM_ALL, success }) end
|
||||
f_ack.on_ack_alarms = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.ACK_ALL_ALARMS, success }) end
|
||||
|
||||
f_ack.on_start = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.START, success }) end
|
||||
f_ack.on_stop = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.STOP, success }) end
|
||||
|
||||
for u = 1, iocontrol.get_db().facility.num_units do
|
||||
local u_ack = self.proc_handle.unit_ack[u]
|
||||
u_ack.on_start = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.START, u, success }) end
|
||||
u_ack.on_scram = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.SCRAM, u, success }) end
|
||||
u_ack.on_rps_reset = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.RESET_RPS, u, success }) end
|
||||
u_ack.on_ack_alarms = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.ACK_ALL_ALARMS, u, success }) end
|
||||
end
|
||||
|
||||
-- handle a packet
|
||||
---@param pkt mgmt_frame|crdn_frame
|
||||
local function _handle_packet(pkt)
|
||||
-- check sequence number
|
||||
if self.r_seq_num == nil then
|
||||
self.r_seq_num = pkt.scada_frame.seq_num()
|
||||
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
|
||||
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
|
||||
log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||
return
|
||||
else
|
||||
self.r_seq_num = pkt.scada_frame.seq_num()
|
||||
self.r_seq_num = pkt.scada_frame.seq_num() + 1
|
||||
end
|
||||
|
||||
-- feed watchdog
|
||||
@@ -123,7 +149,108 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
-- handle packet by type
|
||||
if pkt.type == CRDN_TYPE.API_GET_FAC then
|
||||
if pkt.type == CRDN_TYPE.FAC_CMD then
|
||||
if pkt.length >= 1 then
|
||||
local cmd = pkt.data[1]
|
||||
|
||||
if cmd == FAC_COMMAND.SCRAM_ALL then
|
||||
log.info(log_tag .. "FAC SCRAM ALL")
|
||||
self.proc_handle.fac_scram()
|
||||
elseif cmd == FAC_COMMAND.STOP then
|
||||
log.info(log_tag .. "STOP PROCESS CTRL")
|
||||
self.proc_handle.process_stop()
|
||||
elseif cmd == FAC_COMMAND.START then
|
||||
if pkt.length == 6 then
|
||||
log.info(log_tag .. "START PROCESS CTRL")
|
||||
self.proc_handle.process_start_remote(pkt.data[2], pkt.data[3], pkt.data[4], pkt.data[5], pkt.data[6])
|
||||
else
|
||||
log.debug(log_tag .. "CRDN auto start (with configuration) packet length mismatch")
|
||||
end
|
||||
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
|
||||
log.info(log_tag .. "FAC ACK ALL ALARMS")
|
||||
self.proc_handle.fac_ack_alarms()
|
||||
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
|
||||
if pkt.length == 2 then
|
||||
log.info(util.c(log_tag, " SET WASTE ", pkt.data[2]))
|
||||
process.set_process_waste(pkt.data[2])
|
||||
else
|
||||
log.debug(log_tag .. "CRDN set waste mode packet length mismatch")
|
||||
end
|
||||
elseif cmd == FAC_COMMAND.SET_PU_FB then
|
||||
if pkt.length == 2 then
|
||||
log.info(util.c(log_tag, " SET PU FALLBACK ", pkt.data[2]))
|
||||
process.set_pu_fallback(pkt.data[2] == true)
|
||||
else
|
||||
log.debug(log_tag .. "CRDN set pu fallback packet length mismatch")
|
||||
end
|
||||
elseif cmd == FAC_COMMAND.SET_SPS_LP then
|
||||
if pkt.length == 2 then
|
||||
log.info(util.c(log_tag, " SET SPS LOW POWER ", pkt.data[2]))
|
||||
process.set_sps_low_power(pkt.data[2] == true)
|
||||
else
|
||||
log.debug(log_tag .. "CRDN set sps low power packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.debug(log_tag .. "CRDN facility command unknown")
|
||||
end
|
||||
else
|
||||
log.debug(log_tag .. "CRDN facility command packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == CRDN_TYPE.UNIT_CMD then
|
||||
if pkt.length >= 2 then
|
||||
-- get command and unit id
|
||||
local cmd = pkt.data[1]
|
||||
local uid = pkt.data[2]
|
||||
|
||||
-- continue if valid unit id
|
||||
if util.is_int(uid) and uid > 0 and uid <= #db.units then
|
||||
if cmd == UNIT_COMMAND.SCRAM then
|
||||
log.info(util.c(log_tag, "UNIT[", uid, "] SCRAM"))
|
||||
self.proc_handle.scram(uid)
|
||||
elseif cmd == UNIT_COMMAND.START then
|
||||
log.info(util.c(log_tag, "UNIT[", uid, "] START"))
|
||||
self.proc_handle.start(uid)
|
||||
elseif cmd == UNIT_COMMAND.RESET_RPS then
|
||||
log.info(util.c(log_tag, "UNIT[", uid, "] RESET RPS"))
|
||||
self.proc_handle.reset_rps(uid)
|
||||
elseif cmd == UNIT_COMMAND.SET_BURN then
|
||||
if (pkt.length == 3) and (type(pkt.data[3]) == "number") then
|
||||
log.info(util.c(log_tag, "UNIT[", uid, "] SET BURN ", pkt.data[3]))
|
||||
process.set_rate(uid, pkt.data[3])
|
||||
else
|
||||
log.debug(log_tag .. "CRDN unit command burn rate missing option")
|
||||
end
|
||||
elseif cmd == UNIT_COMMAND.SET_WASTE then
|
||||
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and
|
||||
(pkt.data[3] >= WASTE_MODE.AUTO) and (pkt.data[3] <= WASTE_MODE.MANUAL_ANTI_MATTER) then
|
||||
log.info(util.c(log_tag, "UNIT[", id, "] SET WASTE ", pkt.data[3]))
|
||||
process.set_unit_waste(uid, pkt.data[3])
|
||||
else
|
||||
log.debug(log_tag .. "CRDN unit command set waste missing/invalid option")
|
||||
end
|
||||
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
|
||||
log.info(util.c(log_tag, "UNIT[", uid, "] ACK ALL ALARMS"))
|
||||
self.proc_handle.ack_all_alarms(uid)
|
||||
elseif cmd == UNIT_COMMAND.ACK_ALARM then
|
||||
elseif cmd == UNIT_COMMAND.RESET_ALARM then
|
||||
elseif cmd == UNIT_COMMAND.SET_GROUP then
|
||||
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and
|
||||
(pkt.data[3] >= AUTO_GROUP.MANUAL) and (pkt.data[3] <= AUTO_GROUP.BACKUP) then
|
||||
log.info(util.c(log_tag, "UNIT[", uid, "] SET GROUP ", pkt.data[3]))
|
||||
process.set_group(uid, pkt.data[3])
|
||||
else
|
||||
log.debug(log_tag .. "CRDN unit set group missing option")
|
||||
end
|
||||
else
|
||||
log.debug(log_tag .. "CRDN unit command unknown")
|
||||
end
|
||||
else
|
||||
log.debug(log_tag .. "CRDN unit command invalid")
|
||||
end
|
||||
else
|
||||
log.debug(log_tag .. "CRDN unit command packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == CRDN_TYPE.API_GET_FAC then
|
||||
local fac = db.facility
|
||||
|
||||
local data = {
|
||||
@@ -133,33 +260,182 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
{ 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,
|
||||
fac.induction_data_tbl[1] ~= nil, ---@fixme this means nothing
|
||||
fac.sps_data_tbl[1] ~= nil ---@fixme this means nothing
|
||||
}
|
||||
|
||||
_send(CRDN_TYPE.API_GET_FAC, data)
|
||||
elseif pkt.type == CRDN_TYPE.API_GET_FAC_DTL then
|
||||
local fac = db.facility
|
||||
local mtx_sps = fac.induction_ps_tbl[1]
|
||||
|
||||
local units = {}
|
||||
local tank_statuses = {}
|
||||
|
||||
for i = 1, #db.units do
|
||||
local u = db.units[i]
|
||||
units[i] = { u.connected, u.annunciator, u.reactor_data, u.tank_data_tbl }
|
||||
for t = 1, #u.tank_ps_tbl do table.insert(tank_statuses, u.tank_ps_tbl[t].get("computed_status")) end
|
||||
end
|
||||
|
||||
for i = 1, #fac.tank_ps_tbl do table.insert(tank_statuses, fac.tank_ps_tbl[i].get("computed_status")) end
|
||||
|
||||
local matrix_data = {
|
||||
mtx_sps.get("eta_string"),
|
||||
mtx_sps.get("avg_charge"),
|
||||
mtx_sps.get("avg_inflow"),
|
||||
mtx_sps.get("avg_outflow"),
|
||||
mtx_sps.get("is_charging"),
|
||||
mtx_sps.get("is_discharging"),
|
||||
mtx_sps.get("at_max_io")
|
||||
}
|
||||
|
||||
local data = {
|
||||
fac.all_sys_ok,
|
||||
fac.rtu_count,
|
||||
fac.auto_scram,
|
||||
fac.ascram_status,
|
||||
tank_statuses,
|
||||
fac.tank_data_tbl,
|
||||
fac.induction_ps_tbl[1].get("computed_status") or types.IMATRIX_STATE.OFFLINE,
|
||||
fac.induction_data_tbl[1],
|
||||
matrix_data,
|
||||
fac.sps_ps_tbl[1].get("computed_status") or types.SPS_STATE.OFFLINE,
|
||||
fac.sps_data_tbl[1],
|
||||
units
|
||||
}
|
||||
|
||||
_send(CRDN_TYPE.API_GET_FAC_DTL, 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
|
||||
local u = db.units[pkt.data[1]]
|
||||
|
||||
local statuses = { u.unit_ps.get("computed_status") }
|
||||
|
||||
for i = 1, #u.boiler_ps_tbl do table.insert(statuses, u.boiler_ps_tbl[i].get("computed_status")) end
|
||||
for i = 1, #u.turbine_ps_tbl do table.insert(statuses, u.turbine_ps_tbl[i].get("computed_status")) end
|
||||
for i = 1, #u.tank_ps_tbl do table.insert(statuses, u.tank_ps_tbl[i].get("computed_status")) end
|
||||
|
||||
if u then
|
||||
local data = {
|
||||
u.unit_id,
|
||||
u.connected,
|
||||
u.rtu_hw,
|
||||
statuses,
|
||||
u.a_group,
|
||||
u.alarms,
|
||||
u.annunciator,
|
||||
u.reactor_data,
|
||||
u.boiler_data_tbl,
|
||||
u.turbine_data_tbl,
|
||||
u.tank_data_tbl
|
||||
u.tank_data_tbl,
|
||||
u.last_rate_change_ms,
|
||||
u.turbine_flow_stable
|
||||
}
|
||||
|
||||
_send(CRDN_TYPE.API_GET_UNIT, data)
|
||||
end
|
||||
end
|
||||
elseif pkt.type == CRDN_TYPE.API_GET_CTRL then
|
||||
local data = {}
|
||||
|
||||
for i = 1, #db.units do
|
||||
local u = db.units[i]
|
||||
|
||||
data[i] = {
|
||||
u.connected,
|
||||
u.reactor_data.rps_tripped,
|
||||
u.reactor_data.mek_status.status,
|
||||
u.reactor_data.mek_status.temp,
|
||||
u.reactor_data.mek_status.burn_rate,
|
||||
u.reactor_data.mek_status.act_burn_rate,
|
||||
u.reactor_data.mek_struct.max_burn,
|
||||
u.annunciator.AutoControl,
|
||||
u.a_group
|
||||
}
|
||||
end
|
||||
|
||||
_send(CRDN_TYPE.API_GET_CTRL, data)
|
||||
elseif pkt.type == CRDN_TYPE.API_GET_PROC then
|
||||
local data = {}
|
||||
|
||||
local fac = db.facility
|
||||
local proc = process.get_control_states().process
|
||||
|
||||
-- unit data
|
||||
for i = 1, #db.units do
|
||||
local u = db.units[i]
|
||||
|
||||
data[i] = {
|
||||
u.reactor_data.mek_status.status,
|
||||
u.reactor_data.mek_struct.max_burn,
|
||||
proc.limits[i],
|
||||
u.auto_ready,
|
||||
u.auto_degraded,
|
||||
u.annunciator.AutoControl,
|
||||
u.a_group
|
||||
}
|
||||
end
|
||||
|
||||
-- facility data
|
||||
data[#db.units + 1] = {
|
||||
fac.status_lines,
|
||||
{ fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated },
|
||||
fac.auto_scram,
|
||||
fac.ascram_status,
|
||||
{ proc.mode, proc.burn_target, proc.charge_target, proc.gen_target }
|
||||
}
|
||||
|
||||
_send(CRDN_TYPE.API_GET_PROC, data)
|
||||
elseif pkt.type == CRDN_TYPE.API_GET_WASTE then
|
||||
local data = {}
|
||||
|
||||
local fac = db.facility
|
||||
local proc = process.get_control_states().process
|
||||
|
||||
-- unit data
|
||||
for i = 1, #db.units do
|
||||
local u = db.units[i]
|
||||
|
||||
data[i] = {
|
||||
u.waste_mode,
|
||||
u.waste_product,
|
||||
u.num_snas,
|
||||
u.sna_peak_rate,
|
||||
u.sna_max_rate,
|
||||
u.sna_out_rate,
|
||||
u.waste_stats
|
||||
}
|
||||
end
|
||||
|
||||
local process_rate = 0
|
||||
|
||||
if fac.sps_data_tbl[1].state then
|
||||
process_rate = fac.sps_data_tbl[1].state.process_rate
|
||||
end
|
||||
|
||||
-- facility data
|
||||
data[#db.units + 1] = {
|
||||
fac.auto_current_waste_product,
|
||||
fac.auto_pu_fallback_active,
|
||||
fac.auto_sps_disabled,
|
||||
proc.waste_product,
|
||||
proc.pu_fallback,
|
||||
proc.sps_low_power,
|
||||
fac.waste_stats,
|
||||
fac.sps_ps_tbl[1].get("computed_status") or types.SPS_STATE.OFFLINE,
|
||||
process_rate
|
||||
}
|
||||
|
||||
_send(CRDN_TYPE.API_GET_WASTE, data)
|
||||
elseif pkt.type == CRDN_TYPE.API_GET_RAD then
|
||||
local data = {}
|
||||
|
||||
for i = 1, #db.units do data[i] = db.units[i].rad_monitors end
|
||||
data[#db.units + 1] = db.facility.rad_monitors
|
||||
|
||||
_send(CRDN_TYPE.API_GET_RAD, data)
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type)
|
||||
log.debug(log_tag .. "handler received unsupported CRDN packet type " .. pkt.type)
|
||||
end
|
||||
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
---@cast pkt mgmt_frame
|
||||
@@ -172,7 +448,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
self.last_rtt = srv_now - srv_start
|
||||
|
||||
if self.last_rtt > 750 then
|
||||
log.warning(log_header .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||
log.warning(log_tag .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms")
|
||||
@@ -180,13 +456,17 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
|
||||
iocontrol.fp_pkt_rtt(id, self.last_rtt)
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_close()
|
||||
elseif pkt.type == MGMT_TYPE.ESTABLISH then
|
||||
-- something is wrong, kill the session
|
||||
_close()
|
||||
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -211,7 +491,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
function public.close()
|
||||
_close()
|
||||
_send_mgmt(MGMT_TYPE.CLOSE, {})
|
||||
log.info(log_header .. "session closed by server")
|
||||
log.info(log_tag .. "session closed by server")
|
||||
end
|
||||
|
||||
-- iterate the session
|
||||
@@ -242,14 +522,14 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
|
||||
-- max 100ms spent processing queue
|
||||
if util.time() - handle_start > 100 then
|
||||
log.warning(log_header .. "exceeded 100ms queue process limit")
|
||||
log.warning(log_tag .. "exceeded 100ms queue process limit")
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- exit if connection was closed
|
||||
if not self.connected then
|
||||
log.info(log_header .. "session closed by remote host")
|
||||
log.info(log_tag .. "session closed by remote host")
|
||||
return self.connected
|
||||
end
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ local log = require("scada-common.log")
|
||||
local sounder = {}
|
||||
|
||||
local alarm_ctl = {
|
||||
speaker = nil,
|
||||
speaker = nil, ---@type Speaker
|
||||
volume = 0.5,
|
||||
stream = audio.new_stream()
|
||||
}
|
||||
@@ -24,7 +24,7 @@ local function play()
|
||||
end
|
||||
|
||||
-- initialize the annunciator alarm system
|
||||
---@param speaker table speaker peripheral
|
||||
---@param speaker Speaker speaker peripheral
|
||||
---@param volume number speaker volume
|
||||
function sounder.init(speaker, volume)
|
||||
alarm_ctl.speaker = speaker
|
||||
@@ -36,7 +36,7 @@ function sounder.init(speaker, volume)
|
||||
end
|
||||
|
||||
-- reconnect the speaker peripheral
|
||||
---@param speaker table speaker peripheral
|
||||
---@param speaker Speaker speaker peripheral
|
||||
function sounder.reconnect(speaker)
|
||||
alarm_ctl.speaker = speaker
|
||||
alarm_ctl.playing = false
|
||||
@@ -44,7 +44,7 @@ function sounder.reconnect(speaker)
|
||||
end
|
||||
|
||||
-- set alarm tones
|
||||
---@param states table alarm tone commands from supervisor
|
||||
---@param states { [TONE]: boolean } 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
|
||||
|
||||
@@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
local threads = require("coordinator.threads")
|
||||
|
||||
local COORDINATOR_VERSION = "v1.4.6"
|
||||
local COORDINATOR_VERSION = "v1.6.16"
|
||||
|
||||
local CHUNK_LOAD_DELAY_S = 30.0
|
||||
|
||||
@@ -151,8 +151,8 @@ local function main()
|
||||
|
||||
-- core coordinator devices
|
||||
crd_dev = {
|
||||
speaker = ppm.get_device("speaker"),
|
||||
modem = ppm.get_wireless_modem()
|
||||
modem = ppm.get_wireless_modem(),
|
||||
speaker = ppm.get_device("speaker") ---@type Speaker|nil
|
||||
},
|
||||
|
||||
-- system objects
|
||||
|
||||
@@ -6,6 +6,7 @@ local util = require("scada-common.util")
|
||||
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local process = require("coordinator.process")
|
||||
local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
|
||||
@@ -23,7 +24,8 @@ local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
|
||||
local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
|
||||
|
||||
local MQ__RENDER_CMD = {
|
||||
START_MAIN_UI = 1
|
||||
START_MAIN_UI = 1,
|
||||
CLOSE_MAIN_UI = 2
|
||||
}
|
||||
|
||||
local MQ__RENDER_DATA = {
|
||||
@@ -67,6 +69,7 @@ function threads.thread__main(smem)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
---@cast device Modem
|
||||
-- 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
|
||||
@@ -79,7 +82,7 @@ function threads.thread__main(smem)
|
||||
nic.connect(other_modem)
|
||||
else
|
||||
-- close out main UI
|
||||
renderer.close_ui()
|
||||
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
|
||||
|
||||
-- alert user to status
|
||||
log_sys("awaiting comms modem reconnect...")
|
||||
@@ -90,8 +93,10 @@ function threads.thread__main(smem)
|
||||
log_sys("non-comms modem disconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
---@cast device Monitor
|
||||
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, device)
|
||||
elseif type == "speaker" then
|
||||
---@cast device Speaker
|
||||
log_sys("lost alarm sounder speaker")
|
||||
iocontrol.fp_has_speaker(false)
|
||||
end
|
||||
@@ -101,6 +106,7 @@ function threads.thread__main(smem)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
---@cast device Modem
|
||||
if device.isWireless() and not nic.is_connected() then
|
||||
-- reconnected modem
|
||||
log_sys("comms modem reconnected")
|
||||
@@ -112,8 +118,10 @@ function threads.thread__main(smem)
|
||||
log_sys("wired modem reconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
---@cast device Monitor
|
||||
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, { name = param1, device = device })
|
||||
elseif type == "speaker" then
|
||||
---@cast device Speaker
|
||||
log_sys("alarm sounder speaker reconnected")
|
||||
sounder.reconnect(device)
|
||||
iocontrol.fp_has_speaker(true)
|
||||
@@ -147,6 +155,9 @@ function threads.thread__main(smem)
|
||||
apisessions.iterate_all()
|
||||
apisessions.free_all_closed()
|
||||
|
||||
-- clear timed out process commands
|
||||
process.clear_timed_out()
|
||||
|
||||
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))
|
||||
@@ -157,9 +168,9 @@ function threads.thread__main(smem)
|
||||
-- supervisor watchdog timeout
|
||||
log_comms("supervisor server timeout")
|
||||
|
||||
-- close connection, main UI, and stop sounder
|
||||
-- close main UI, connection, and stop sounder
|
||||
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
else
|
||||
-- a non-clock/main watchdog timer event
|
||||
@@ -178,9 +189,9 @@ function threads.thread__main(smem)
|
||||
if coord_comms.handle_packet(packet) then
|
||||
log_comms("supervisor closed connection")
|
||||
|
||||
-- close connection, main UI, and stop sounder
|
||||
-- close main UI, connection, and stop sounder
|
||||
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
end
|
||||
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
|
||||
@@ -244,7 +255,7 @@ function threads.thread__main(smem)
|
||||
return public
|
||||
end
|
||||
|
||||
-- coordinator renderer thread, tasked with long duration re-draws
|
||||
-- coordinator renderer thread, tasked with long duration draws
|
||||
---@nodiscard
|
||||
---@param smem crd_shared_memory
|
||||
function threads.thread__render(smem)
|
||||
@@ -292,6 +303,13 @@ function threads.thread__render(smem)
|
||||
else
|
||||
log_render("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
|
||||
end
|
||||
elseif msg.message == MQ__RENDER_CMD.CLOSE_MAIN_UI then
|
||||
-- close the main UI if it has been drawn
|
||||
if renderer.ui_ready() then
|
||||
log_render("closing main UI...")
|
||||
renderer.close_ui()
|
||||
log_render("main UI closed")
|
||||
end
|
||||
end
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
-- received data
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
local Rectangle = require("graphics.elements.Rectangle")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
|
||||
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
|
||||
local VerticalBar = require("graphics.elements.indicators.VerticalBar")
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
-- new boiler view
|
||||
---@param root graphics_element parent
|
||||
---@param root Container parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param ps psil ps interface
|
||||
@@ -35,10 +35,10 @@ local function new_view(root, x, y, ps)
|
||||
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}
|
||||
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}
|
||||
TextBox{parent=boiler,text="H",x=2,y=5,width=1,fg_bg=text_fg}
|
||||
TextBox{parent=boiler,text="W",x=3,y=5,width=1,fg_bg=text_fg}
|
||||
TextBox{parent=boiler,text="S",x=27,y=5,width=1,fg_bg=text_fg}
|
||||
TextBox{parent=boiler,text="C",x=28,y=5,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}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
local Div = require("graphics.elements.Div")
|
||||
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")
|
||||
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
|
||||
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
|
||||
local PowerIndicator = require("graphics.elements.indicators.PowerIndicator")
|
||||
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
|
||||
local VerticalBar = require("graphics.elements.indicators.VerticalBar")
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
@@ -20,13 +22,12 @@ local border = core.border
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
-- new induction matrix view
|
||||
---@param root graphics_element parent
|
||||
---@param root Container parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param data imatrix_session_db matrix data
|
||||
---@param ps psil ps interface
|
||||
---@param id number? matrix ID
|
||||
local function new_view(root, x, y, data, ps, id)
|
||||
local function new_view(root, x, y, ps, id)
|
||||
local label_fg = style.theme.label_fg
|
||||
local text_fg = style.theme.text_fg
|
||||
local lu_col = style.lu_colors
|
||||
@@ -34,6 +35,8 @@ local function new_view(root, x, y, data, ps, id)
|
||||
local ind_yel = style.ind_yel
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local title = "INDUCTION MATRIX"
|
||||
if type(id) == "number" then title = title .. id end
|
||||
|
||||
@@ -42,30 +45,30 @@ local function new_view(root, x, y, data, ps, id)
|
||||
-- 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}
|
||||
TextBox{parent=matrix,text=" ",width=33,x=1,y=1,fg_bg=cutout_fg_bg}
|
||||
TextBox{parent=matrix,text=title,alignment=ALIGN.CENTER,width=33,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 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}
|
||||
local capacity = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Capacity:",unit=db.energy_label,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: ",unit=db.energy_label,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:",unit=db.energy_label,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: ",unit=db.energy_label,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:",unit=db.energy_label,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: ",unit=db.energy_label,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:",unit=db.energy_label,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: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
|
||||
status.register(ps, "computed_status", status.update)
|
||||
capacity.register(ps, "max_energy", function (val) capacity.update(util.joules_to_fe(val)) end)
|
||||
energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end)
|
||||
capacity.register(ps, "max_energy", function (val) capacity.update(db.energy_convert(val)) end)
|
||||
energy.register(ps, "energy", function (val) energy.update(db.energy_convert(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)
|
||||
input.register(ps, "last_input", function (val) input.update(db.energy_convert(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)
|
||||
output.register(ps, "last_output", function (val) output.update(db.energy_convert(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)
|
||||
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(db.energy_convert(val)) end)
|
||||
|
||||
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}
|
||||
@@ -87,9 +90,10 @@ local function new_view(root, x, y, data, ps, id)
|
||||
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 I/O",x=2,y=20,height=1,width=8,fg_bg=label_fg}
|
||||
TextBox{parent=rect,text="FILL I/O",x=2,y=20,width=8,fg_bg=label_fg}
|
||||
|
||||
local function calc_saturation(val)
|
||||
local data = db.facility.induction_data_tbl[id or 1]
|
||||
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
|
||||
@@ -99,48 +103,9 @@ local function new_view(root, x, y, data, ps, id)
|
||||
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}
|
||||
local eta = TextBox{parent=rect,x=11,y=20,width=20,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)
|
||||
eta.register(ps, "eta_string", eta.set_value)
|
||||
end
|
||||
|
||||
return new_view
|
||||
|
||||
@@ -8,17 +8,17 @@ local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
local Div = require("graphics.elements.Div")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
-- create a pocket list entry
|
||||
---@param parent graphics_element parent
|
||||
---@param parent ListBox parent
|
||||
---@param id integer PKT session ID
|
||||
local function init(parent, id)
|
||||
local s_hi_box = style.fp_theme.highlight_box
|
||||
@@ -28,24 +28,26 @@ local function init(parent, id)
|
||||
|
||||
local ps = iocontrol.get_db().fp.ps
|
||||
|
||||
local term_w, _ = term.getSize()
|
||||
|
||||
-- root div
|
||||
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
|
||||
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2}
|
||||
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=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}
|
||||
TextBox{parent=entry,x=1,y=1,text="",width=8,fg_bg=s_hi_box}
|
||||
local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)}
|
||||
TextBox{parent=entry,x=1,y=3,text="",width=8,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=label_fg}
|
||||
TextBox{parent=entry,x=10,y=2,text="FW:",width=3}
|
||||
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,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=label_fg}
|
||||
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=label_fg}
|
||||
TextBox{parent=entry,x=term_w-16,y=2,text="RTT:",width=4}
|
||||
local pkt_rtt = DataIndicator{parent=entry,x=term_w-11,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
|
||||
TextBox{parent=entry,x=term_w-5,y=2,text="ms",width=4,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)
|
||||
|
||||
|
||||
@@ -8,20 +8,20 @@ local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
local Div = require("graphics.elements.Div")
|
||||
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 RadIndicator = require("graphics.elements.indicators.rad")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
|
||||
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
|
||||
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
|
||||
local RadIndicator = require("graphics.elements.indicators.RadIndicator")
|
||||
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
|
||||
local TriIndicatorLight = require("graphics.elements.indicators.TriIndicatorLight")
|
||||
|
||||
local Checkbox = require("graphics.elements.controls.checkbox")
|
||||
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 Checkbox = require("graphics.elements.controls.Checkbox")
|
||||
local HazardButton = require("graphics.elements.controls.HazardButton")
|
||||
local NumericSpinbox = require("graphics.elements.controls.NumericSpinbox")
|
||||
local RadioButton = require("graphics.elements.controls.RadioButton")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
@@ -33,7 +33,7 @@ local bw_fg_bg = style.bw_fg_bg
|
||||
local period = core.flasher.PERIOD
|
||||
|
||||
-- new process control view
|
||||
---@param root graphics_element parent
|
||||
---@param root Container parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
local function new_view(root, x, y)
|
||||
@@ -56,16 +56,18 @@ local function new_view(root, x, y)
|
||||
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 db = iocontrol.get_db()
|
||||
|
||||
local facility = db.facility
|
||||
local units = db.units
|
||||
|
||||
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}
|
||||
local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,fg_bg=hzd_fg_bg}
|
||||
local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=db.process.fac_scram,fg_bg=hzd_fg_bg}
|
||||
local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=db.process.fac_ack_alarms,fg_bg=hzd_fg_bg}
|
||||
|
||||
facility.scram_ack = scram.on_response
|
||||
facility.ack_alarms_ack = ack_a.on_response
|
||||
db.process.fac_ack.on_scram = scram.on_response
|
||||
db.process.fac_ack.on_ack_alarms = ack_a.on_response
|
||||
|
||||
local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=ind_grn}
|
||||
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=style.ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd}
|
||||
@@ -92,24 +94,24 @@ local function new_view(root, x, y)
|
||||
main.line_break()
|
||||
|
||||
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_flt = IndicatorLight{parent=main,label="Induction Matrix Fault",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)
|
||||
matrix_flt.register(facility.ps, "as_matrix_fault", matrix_flt.update)
|
||||
matrix_fill.register(facility.ps, "as_matrix_fill", matrix_fill.update)
|
||||
unit_crit.register(facility.ps, "as_crit_alarm", unit_crit.update)
|
||||
fac_rad_h.register(facility.ps, "as_radiation", fac_rad_h.update)
|
||||
gen_fault.register(facility.ps, "as_gen_fault", gen_fault.update)
|
||||
|
||||
TextBox{parent=main,y=23,text="Radiation",height=1,width=13,fg_bg=style.label}
|
||||
TextBox{parent=main,y=23,text="Radiation",width=13,fg_bg=style.label}
|
||||
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}
|
||||
TextBox{parent=main,x=15,y=23,text="Linked RTUs",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=s_field}
|
||||
rtu_count.register(facility.ps, "rtu_count", rtu_count.update)
|
||||
|
||||
@@ -129,7 +131,7 @@ local function new_view(root, x, y)
|
||||
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=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}
|
||||
local b_target = NumericSpinbox{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}
|
||||
|
||||
@@ -140,23 +142,23 @@ local function new_view(root, x, y)
|
||||
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=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}
|
||||
local c_target = NumericSpinbox{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="M"..db.energy_label,fg_bg=style.theme.label_fg}
|
||||
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="M"..db.energy_label,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], "avg_charge", function (fe) cur_charge.update(fe / 1000000) end)
|
||||
cur_charge.register(facility.induction_ps_tbl[1], "avg_charge", function (fe) cur_charge.update(db.energy_convert_from_fe(fe) / 1000000) end)
|
||||
|
||||
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur}
|
||||
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
|
||||
|
||||
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=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}
|
||||
local g_target = NumericSpinbox{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="k"..db.energy_label.."/t",fg_bg=style.theme.label_fg}
|
||||
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="k"..db.energy_label.."/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)
|
||||
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(db.energy_convert(j) / 1000)) end)
|
||||
|
||||
-----------------
|
||||
-- unit limits --
|
||||
@@ -175,7 +177,7 @@ local function new_view(root, x, y)
|
||||
local cur_lu = style.theme.disabled
|
||||
|
||||
if i <= facility.num_units then
|
||||
unit = units[i] ---@type ioctl_unit
|
||||
unit = units[i]
|
||||
tag_fg_bg = cpair(colors.black, colors.lightBlue)
|
||||
lim_fg_bg = s_hi_box
|
||||
label_fg = style.theme.label_fg
|
||||
@@ -189,8 +191,8 @@ local function new_view(root, x, y)
|
||||
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=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 lim = NumericSpinbox{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,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}
|
||||
|
||||
@@ -232,7 +234,7 @@ local function new_view(root, x, y)
|
||||
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
|
||||
local unit = units[i]
|
||||
|
||||
ready.register(unit.unit_ps, "U_AutoReady", ready.update)
|
||||
degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update)
|
||||
@@ -249,8 +251,8 @@ local function new_view(root, x, y)
|
||||
mode.register(facility.ps, "process_mode", mode.set_value)
|
||||
|
||||
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.white)}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,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,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)
|
||||
@@ -262,21 +264,22 @@ local function new_view(root, x, y)
|
||||
local limits = {}
|
||||
for i = 1, #rate_limits do limits[i] = rate_limits[i].get_value() end
|
||||
|
||||
process.save(mode.get_value(), b_target.get_value(), c_target.get_value(), g_target.get_value(), limits)
|
||||
process.save(mode.get_value(), b_target.get_value(), db.energy_convert_to_fe(c_target.get_value()),
|
||||
db.energy_convert_to_fe(g_target.get_value()), limits)
|
||||
end
|
||||
|
||||
-- start automatic control after saving process control settings
|
||||
local function _start_auto()
|
||||
_save_cfg()
|
||||
process.start_auto()
|
||||
db.process.process_start()
|
||||
end
|
||||
|
||||
local save = HazardButton{parent=auto_controls,x=2,y=2,text="SAVE",accent=colors.purple,dis_colors=dis_colors,callback=_save_cfg,fg_bg=hzd_fg_bg}
|
||||
local start = HazardButton{parent=auto_controls,x=13,y=2,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=_start_auto,fg_bg=hzd_fg_bg}
|
||||
local stop = HazardButton{parent=auto_controls,x=23,y=2,text="STOP",accent=colors.red,dis_colors=dis_colors,callback=process.stop_auto,fg_bg=hzd_fg_bg}
|
||||
local stop = HazardButton{parent=auto_controls,x=23,y=2,text="STOP",accent=colors.red,dis_colors=dis_colors,callback=db.process.process_stop,fg_bg=hzd_fg_bg}
|
||||
|
||||
facility.start_ack = start.on_response
|
||||
facility.stop_ack = stop.on_response
|
||||
db.process.fac_ack.on_start = start.on_response
|
||||
db.process.fac_ack.on_stop = stop.on_response
|
||||
|
||||
function facility.save_cfg_ack(ack)
|
||||
tcd.dispatch(0.2, function () save.on_response(ack) end)
|
||||
@@ -318,11 +321,11 @@ local function new_view(root, x, y)
|
||||
local waste_status = Div{parent=proc,width=24,height=4,x=57,y=1,}
|
||||
|
||||
for i = 1, facility.num_units do
|
||||
local unit = units[i] ---@type ioctl_unit
|
||||
local unit = units[i]
|
||||
|
||||
TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8,height=1}
|
||||
TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8}
|
||||
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}
|
||||
local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.get_waste().states_abbrv,value=1,min_width=6}
|
||||
|
||||
a_waste.register(unit.unit_ps, "U_AutoWaste", a_waste.update)
|
||||
waste_m.register(unit.unit_ps, "U_WasteProduct", waste_m.update)
|
||||
@@ -332,15 +335,15 @@ local function new_view(root, x, y)
|
||||
|
||||
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}
|
||||
TextBox{parent=waste_sel,text=" ",width=21,x=1,y=1,fg_bg=cutout_fg_bg}
|
||||
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,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}
|
||||
local status = StateIndicator{parent=rect,x=2,y=1,states=style.get_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(style.theme.accent_dark,style.theme.accent_light),select_color=colors.brown}
|
||||
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.get_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)
|
||||
|
||||
|
||||
@@ -6,18 +6,18 @@ local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
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 StateIndicator = require("graphics.elements.indicators.state")
|
||||
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
|
||||
local HorizontalBar = require("graphics.elements.indicators.HorizontalBar")
|
||||
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
-- create new reactor view
|
||||
---@param root graphics_element parent
|
||||
---@param root Container parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param ps psil ps interface
|
||||
@@ -41,10 +41,10 @@ local function new_view(root, x, y, ps)
|
||||
|
||||
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}
|
||||
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}
|
||||
TextBox{parent=reactor_fills,text="FUEL",x=2,y=1,fg_bg=text_fg}
|
||||
TextBox{parent=reactor_fills,text="COOL",x=2,y=2,fg_bg=text_fg}
|
||||
TextBox{parent=reactor_fills,text="HCOOL",x=2,y=4,fg_bg=text_fg}
|
||||
TextBox{parent=reactor_fills,text="WASTE",x=2,y=5,fg_bg=text_fg}
|
||||
|
||||
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}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
local util = require("scada-common.util")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
local Rectangle = require("graphics.elements.Rectangle")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local PowerIndicator = require("graphics.elements.indicators.power")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
|
||||
local PowerIndicator = require("graphics.elements.indicators.PowerIndicator")
|
||||
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
|
||||
local VerticalBar = require("graphics.elements.indicators.VerticalBar")
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
-- new turbine view
|
||||
---@param root graphics_element parent
|
||||
---@param root Container parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param ps psil ps interface
|
||||
@@ -24,21 +24,23 @@ local function new_view(root, x, y, ps)
|
||||
local text_fg = style.theme.text_fg
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local 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}
|
||||
local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",unit=db.energy_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)
|
||||
prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(db.energy_convert(val)) end)
|
||||
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}
|
||||
TextBox{parent=turbine,text="E",x=3,y=5,height=1,width=1,fg_bg=text_fg}
|
||||
TextBox{parent=turbine,text="S",x=2,y=5,width=1,fg_bg=text_fg}
|
||||
TextBox{parent=turbine,text="E",x=3,y=5,width=1,fg_bg=text_fg}
|
||||
|
||||
steam.register(ps, "steam_fill", steam.update)
|
||||
energy.register(ps, "energy_fill", energy.update)
|
||||
|
||||
@@ -11,23 +11,25 @@ local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
local Div = require("graphics.elements.Div")
|
||||
local Rectangle = require("graphics.elements.Rectangle")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local AlarmLight = require("graphics.elements.indicators.alight")
|
||||
local CoreMap = require("graphics.elements.indicators.coremap")
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||
local RadIndicator = require("graphics.elements.indicators.rad")
|
||||
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
|
||||
local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
local AlarmLight = require("graphics.elements.indicators.AlarmLight")
|
||||
local CoreMap = require("graphics.elements.indicators.CoreMap")
|
||||
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
|
||||
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
|
||||
local RadIndicator = require("graphics.elements.indicators.RadIndicator")
|
||||
local TriIndicatorLight = require("graphics.elements.indicators.TriIndicatorLight")
|
||||
local VerticalBar = require("graphics.elements.indicators.VerticalBar")
|
||||
|
||||
local HazardButton = require("graphics.elements.controls.hazard_button")
|
||||
local MultiButton = require("graphics.elements.controls.multi_button")
|
||||
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 HazardButton = require("graphics.elements.controls.HazardButton")
|
||||
local MultiButton = require("graphics.elements.controls.MultiButton")
|
||||
local NumericSpinbox = require("graphics.elements.controls.NumericSpinbox")
|
||||
local PushButton = require("graphics.elements.controls.PushButton")
|
||||
local RadioButton = require("graphics.elements.controls.RadioButton")
|
||||
|
||||
local AUTO_GROUP = types.AUTO_GROUP
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
@@ -40,7 +42,7 @@ local gry_wht = style.gray_white
|
||||
local period = core.flasher.PERIOD
|
||||
|
||||
-- create a unit view
|
||||
---@param parent graphics_element parent
|
||||
---@param parent Container parent
|
||||
---@param id integer
|
||||
local function init(parent, id)
|
||||
local s_hi_box = style.theme.highlight_box
|
||||
@@ -60,7 +62,7 @@ local function init(parent, id)
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
local unit = db.units[id] ---@type ioctl_unit
|
||||
local unit = db.units[id]
|
||||
local f_ps = db.facility.ps
|
||||
|
||||
local main = Div{parent=parent,x=1,y=1}
|
||||
@@ -71,7 +73,7 @@ local function init(parent, id)
|
||||
local b_ps = unit.boiler_ps_tbl
|
||||
local t_ps = unit.turbine_ps_tbl
|
||||
|
||||
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,height=1,fg_bg=style.theme.header}
|
||||
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,fg_bg=style.theme.header}
|
||||
|
||||
-----------------------------
|
||||
-- main stats and core map --
|
||||
@@ -81,20 +83,20 @@ local function init(parent, id)
|
||||
core_map.register(u_ps, "temp", core_map.update)
|
||||
core_map.register(u_ps, "size", function (s) core_map.resize(s[1], s[2]) end)
|
||||
|
||||
TextBox{parent=main,x=12,y=22,text="Heating Rate",height=1,width=12,fg_bg=style.label}
|
||||
TextBox{parent=main,x=12,y=22,text="Heating Rate",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=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}
|
||||
TextBox{parent=main,x=12,y=25,text="Commanded Burn Rate",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=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}
|
||||
TextBox{parent=main,text="C",x=4,y=22,width=1,height=1,fg_bg=style.label}
|
||||
TextBox{parent=main,text="\x1a",x=6,y=24,width=1,height=1,fg_bg=style.label}
|
||||
TextBox{parent=main,text="\x1a",x=6,y=25,width=1,height=1,fg_bg=style.label}
|
||||
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}
|
||||
TextBox{parent=main,text="F",x=2,y=22,width=1,fg_bg=style.label}
|
||||
TextBox{parent=main,text="C",x=4,y=22,width=1,fg_bg=style.label}
|
||||
TextBox{parent=main,text="\x1a",x=6,y=24,width=1,fg_bg=style.label}
|
||||
TextBox{parent=main,text="\x1a",x=6,y=25,width=1,fg_bg=style.label}
|
||||
TextBox{parent=main,text="H",x=8,y=22,width=1,fg_bg=style.label}
|
||||
TextBox{parent=main,text="W",x=10,y=22,width=1,fg_bg=style.label}
|
||||
|
||||
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}
|
||||
@@ -122,20 +124,20 @@ local function init(parent, id)
|
||||
end
|
||||
end)
|
||||
|
||||
TextBox{parent=main,x=32,y=22,text="Core Temp",height=1,width=9,fg_bg=style.label}
|
||||
TextBox{parent=main,x=32,y=22,text="Core Temp",width=9,fg_bg=style.label}
|
||||
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}
|
||||
TextBox{parent=main,x=32,y=25,text="Burn Rate",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=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}
|
||||
TextBox{parent=main,x=32,y=28,text="Damage",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=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}
|
||||
TextBox{parent=main,x=32,y=31,text="Radiation",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=s_field}
|
||||
radiation.register(u_ps, "radiation", radiation.update)
|
||||
|
||||
@@ -144,8 +146,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=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}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,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,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)
|
||||
@@ -205,7 +207,7 @@ local function init(parent, id)
|
||||
|
||||
-- RPS annunciator panel
|
||||
|
||||
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}
|
||||
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,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}
|
||||
|
||||
@@ -233,7 +235,7 @@ local function init(parent, id)
|
||||
|
||||
-- cooling annunciator panel
|
||||
|
||||
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}
|
||||
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,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}
|
||||
@@ -265,11 +267,11 @@ local function init(parent, id)
|
||||
if unit.num_boilers > 0 then
|
||||
if available_space > 0 then _add_space() end
|
||||
|
||||
TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=hc_text}
|
||||
TextBox{parent=rcs_tags,x=1,text="B1",width=2,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}
|
||||
TextBox{parent=rcs_tags,text="B1",width=2,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
|
||||
@@ -281,11 +283,11 @@ local function init(parent, id)
|
||||
_add_space()
|
||||
end
|
||||
|
||||
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=hc_text}
|
||||
TextBox{parent=rcs_tags,text="B2",width=2,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=hc_text}
|
||||
TextBox{parent=rcs_tags,text="B2",width=2,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
|
||||
@@ -294,19 +296,19 @@ local function init(parent, id)
|
||||
|
||||
if available_space > 1 then _add_space() end
|
||||
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=hc_text}
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,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=hc_text}
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,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=hc_text}
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,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=hc_text}
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,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)
|
||||
|
||||
@@ -315,19 +317,19 @@ local function init(parent, id)
|
||||
_add_space()
|
||||
end
|
||||
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=hc_text}
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,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=hc_text}
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,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=hc_text}
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,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=hc_text}
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,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
|
||||
@@ -335,19 +337,19 @@ local function init(parent, id)
|
||||
if unit.num_turbines > 2 then
|
||||
if available_space > 3 then _add_space() end
|
||||
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=hc_text}
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,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=hc_text}
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,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=hc_text}
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,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=hc_text}
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,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
|
||||
@@ -359,7 +361,7 @@ local function init(parent, id)
|
||||
----------------------
|
||||
|
||||
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}
|
||||
local burn_rate = NumericSpinbox{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
|
||||
@@ -373,18 +375,16 @@ local function init(parent, id)
|
||||
local scram = HazardButton{parent=main,x=2,y=32,text="SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=unit.scram,fg_bg=hzd_fg_bg}
|
||||
local reset = HazardButton{parent=main,x=22,y=32,text="RESET",accent=colors.red,dis_colors=dis_colors,callback=unit.reset_rps,fg_bg=hzd_fg_bg}
|
||||
|
||||
unit.start_ack = start.on_response
|
||||
unit.scram_ack = scram.on_response
|
||||
unit.reset_rps_ack = reset.on_response
|
||||
unit.ack_alarms_ack = ack_a.on_response
|
||||
db.process.unit_ack[id].on_start = start.on_response
|
||||
db.process.unit_ack[id].on_scram = scram.on_response
|
||||
db.process.unit_ack[id].on_rps_reset = reset.on_response
|
||||
db.process.unit_ack[id].on_ack_alarms = ack_a.on_response
|
||||
|
||||
local function start_button_en_check()
|
||||
if (unit.reactor_data ~= nil) and (unit.reactor_data.mek_status ~= nil) then
|
||||
local can_start = (not unit.reactor_data.mek_status.status) and
|
||||
(not unit.reactor_data.rps_tripped) and
|
||||
(unit.a_group == 0)
|
||||
if can_start then start.enable() else start.disable() end
|
||||
end
|
||||
local can_start = (not unit.reactor_data.mek_status.status) and
|
||||
(not unit.reactor_data.rps_tripped) and
|
||||
(unit.a_group == AUTO_GROUP.MANUAL)
|
||||
if can_start then start.enable() else start.disable() end
|
||||
end
|
||||
|
||||
start.register(u_ps, "status", start_button_en_check)
|
||||
@@ -394,11 +394,11 @@ 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=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,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}
|
||||
|
||||
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.waste.unit_opts,callback=unit.set_waste,min_width=6}
|
||||
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.get_waste().unit_opts,callback=unit.set_waste,min_width=6}
|
||||
|
||||
waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)
|
||||
|
||||
@@ -474,21 +474,19 @@ local function init(parent, id)
|
||||
|
||||
-- color tags
|
||||
|
||||
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)}
|
||||
TextBox{parent=alarm_panel,x=5,y=13,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.cyan)}
|
||||
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)}
|
||||
TextBox{parent=alarm_panel,x=5,text="\x95",width=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=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,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(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
|
||||
local group = RadioButton{parent=auto_div,options=types.AUTO_GROUP_NAMES,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)
|
||||
|
||||
@@ -499,8 +497,8 @@ local function init(parent, id)
|
||||
|
||||
auto_div.line_break()
|
||||
|
||||
TextBox{parent=auto_div,text="Prio. Group",height=1,width=11,fg_bg=style.label}
|
||||
local auto_grp = TextBox{parent=auto_div,text="Manual",height=1,width=11,fg_bg=s_field}
|
||||
TextBox{parent=auto_div,text="Prio. Group",width=11,fg_bg=style.label}
|
||||
local auto_grp = TextBox{parent=auto_div,text="Manual",width=11,fg_bg=s_field}
|
||||
|
||||
auto_grp.register(u_ps, "auto_group", auto_grp.set_value)
|
||||
|
||||
@@ -523,10 +521,10 @@ local function init(parent, id)
|
||||
|
||||
-- enable/disable controls based on group assignment (start button is separate)
|
||||
burn_rate.register(u_ps, "auto_group_id", function (gid)
|
||||
if gid == 0 then burn_rate.enable() else burn_rate.disable() end
|
||||
if gid == AUTO_GROUP.MANUAL then burn_rate.enable() else burn_rate.disable() end
|
||||
end)
|
||||
set_burn_btn.register(u_ps, "auto_group_id", function (gid)
|
||||
if gid == 0 then set_burn_btn.enable() else set_burn_btn.disable() end
|
||||
if gid == AUTO_GROUP.MANUAL then set_burn_btn.enable() else set_burn_btn.disable() end
|
||||
end)
|
||||
|
||||
-- can't change group if auto is engaged regardless of if this unit is part of auto control
|
||||
|
||||
@@ -2,22 +2,27 @@
|
||||
-- Basic Unit Flow Overview
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
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 Div = require("graphics.elements.Div")
|
||||
local PipeNetwork = require("graphics.elements.PipeNetwork")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
local Rectangle = require("graphics.elements.Rectangle")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
|
||||
|
||||
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
|
||||
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
|
||||
local TriIndicatorLight = require("graphics.elements.indicators.TriIndicatorLight")
|
||||
|
||||
local COOLANT_TYPE = types.COOLANT_TYPE
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
@@ -31,12 +36,12 @@ local wh_gray = style.wh_gray
|
||||
local lg_gray = style.lg_gray
|
||||
|
||||
-- make a new unit flow window
|
||||
---@param parent graphics_element parent
|
||||
---@param parent Container 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)
|
||||
---@param unit_id integer unit index
|
||||
local function make(parent, x, y, wide, unit_id)
|
||||
local s_field = style.theme.field_box
|
||||
|
||||
local text_c = style.text_colors
|
||||
@@ -48,7 +53,13 @@ local function make(parent, x, y, wide, unit)
|
||||
|
||||
local height = 16
|
||||
|
||||
local v_start = 1 + ((unit.unit_id - 1) * 5)
|
||||
local facility = iocontrol.get_db().facility
|
||||
local unit = iocontrol.get_db().units[unit_id]
|
||||
|
||||
local tank_conns = facility.tank_conns
|
||||
local tank_types = facility.tank_fluid_types
|
||||
|
||||
local v_start = 1 + ((unit.unit_id - 1) * 6)
|
||||
local prv_start = 1 + ((unit.unit_id - 1) * 3)
|
||||
local v_fields = { "pu", "po", "pl", "am" }
|
||||
local v_names = {
|
||||
@@ -73,28 +84,39 @@ local function make(parent, x, y, wide, unit)
|
||||
------------------
|
||||
|
||||
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=reactor,y=1,text="FISSION REACTOR",alignment=ALIGN.CENTER}
|
||||
TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=ALIGN.CENTER}
|
||||
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}
|
||||
TextBox{parent=root,x=3,y=5,text="\x19",width=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))
|
||||
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))
|
||||
|
||||
if unit.aux_coolant then
|
||||
local em_water = facility.tank_fluid_types[facility.tank_conns[unit_id]] == COOLANT_TYPE.WATER
|
||||
local offset = util.trinary(unit.has_tank and em_water, 3, 0)
|
||||
table.insert(rc_pipes, pipe(_wide(51, 41) + offset, 0, _wide(51, 41) + offset, 0, colors.blue, true))
|
||||
end
|
||||
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))
|
||||
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))
|
||||
|
||||
if unit.aux_coolant then
|
||||
table.insert(rc_pipes, pipe(8, 0, 8, 0, colors.blue, true))
|
||||
end
|
||||
end
|
||||
|
||||
if unit.has_tank then
|
||||
table.insert(rc_pipes, pipe(emc_x, 1, emc_x, 0, colors.blue, true, true))
|
||||
local is_water = tank_types[tank_conns[unit_id]] == COOLANT_TYPE.WATER
|
||||
-- emergency coolant connection x point
|
||||
local emc_x = util.trinary(is_water and (unit.num_boilers > 0), 42, 3)
|
||||
|
||||
table.insert(rc_pipes, pipe(emc_x, 1, emc_x, 0, util.trinary(is_water, colors.blue, colors.lightBlue), true, true))
|
||||
end
|
||||
|
||||
local prv_yo = math.max(3 - unit.num_turbines, 0)
|
||||
@@ -113,8 +135,8 @@ local function make(parent, x, y, wide, unit)
|
||||
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=boiler,y=1,text="THERMO-ELECTRIC",alignment=ALIGN.CENTER}
|
||||
TextBox{parent=boiler,y=3,text=util.trinary(unit.num_boilers>1,"BOILERS","BOILER"),alignment=ALIGN.CENTER}
|
||||
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}
|
||||
|
||||
@@ -132,13 +154,13 @@ local function make(parent, x, y, wide, unit)
|
||||
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=turbine,y=1,text="STEAM TURBINE",alignment=ALIGN.CENTER}
|
||||
TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=ALIGN.CENTER}
|
||||
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}
|
||||
TextBox{parent=root,x=_wide(125,103),y=ry,text="\x10\x11\x7f",fg_bg=text_c,width=3}
|
||||
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
|
||||
@@ -157,12 +179,12 @@ local function make(parent, x, y, wide, unit)
|
||||
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(64, 53), 1, _wide(95, 81), 1, colors.cyan, 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(48, 43), 4, _wide(71, 61), 4, colors.green, true),
|
||||
pipe(_wide(66, 57), 4, _wide(71, 61), 8, colors.green, true),
|
||||
pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.green, true),
|
||||
pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.green, 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),
|
||||
@@ -172,7 +194,7 @@ local function make(parent, x, y, wide, unit)
|
||||
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}
|
||||
TextBox{parent=waste,x=vx,y=vy,text="\x10\x11",fg_bg=text_c,width=2}
|
||||
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)
|
||||
@@ -181,8 +203,8 @@ local function make(parent, x, y, wide, unit)
|
||||
|
||||
local function _machine(mx, my, name)
|
||||
local l = string.len(name) + 2
|
||||
TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=ALIGN.CENTER,fg_bg=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}
|
||||
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}
|
||||
TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=ALIGN.CENTER,fg_bg=style.theme.header,width=l}
|
||||
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}
|
||||
@@ -209,18 +231,22 @@ local function make(parent, x, y, wide, unit)
|
||||
_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}
|
||||
TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,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=8,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}
|
||||
TextBox{parent=sna_po,y=3,text="PEAK\x1a",width=5,fg_bg=cpair(style.theme.label_dark,colors._INHERIT)}
|
||||
TextBox{parent=sna_po,text="MAX \x1a",width=5,fg_bg=cpair(style.theme.label_dark,colors._INHERIT)}
|
||||
local sna_pk = DataIndicator{parent=sna_po,x=6,y=3,lu_colors=lu_c_d,label="",unit="mB/t",format="%7.2f",value=0,width=17}
|
||||
local sna_max_o = DataIndicator{parent=sna_po,x=6,lu_colors=lu_c_d,label="",unit="mB/t",format="%7.2f",value=0,width=17}
|
||||
local sna_max_i = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="\x1aMAX",unit="mB/t",format="%7.2f",value=0,width=17}
|
||||
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="\x1aIN",unit="mB/t",format="%8.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_max_o.register(unit.unit_ps, "sna_max_rate", sna_max_o.update)
|
||||
sna_max_i.register(unit.unit_ps, "sna_max_rate", function (r) sna_max_i.update(r * 10) end)
|
||||
sna_in.register(unit.unit_ps, "sna_in", sna_in.update)
|
||||
|
||||
return root
|
||||
|
||||
@@ -10,16 +10,16 @@ local reactor_view = require("coordinator.ui.components.reactor")
|
||||
local boiler_view = require("coordinator.ui.components.boiler")
|
||||
local turbine_view = require("coordinator.ui.components.turbine")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local PipeNetwork = require("graphics.elements.pipenet")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
local Div = require("graphics.elements.Div")
|
||||
local PipeNetwork = require("graphics.elements.PipeNetwork")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local pipe = core.pipe
|
||||
|
||||
-- make a new unit overview window
|
||||
---@param parent graphics_element parent
|
||||
---@param parent Container parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param unit ioctl_unit unit database entry
|
||||
@@ -44,7 +44,7 @@ local function make(parent, x, y, unit)
|
||||
local root = Div{parent=parent,x=x,y=y,width=80,height=height}
|
||||
|
||||
-- unit header message
|
||||
TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,height=1,fg_bg=style.theme.header}
|
||||
TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,fg_bg=style.theme.header}
|
||||
|
||||
-------------
|
||||
-- REACTOR --
|
||||
|
||||
@@ -13,17 +13,18 @@ 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 Div = require("graphics.elements.Div")
|
||||
local PipeNetwork = require("graphics.elements.PipeNetwork")
|
||||
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 DataIndicator = require("graphics.elements.indicators.DataIndicator")
|
||||
local HorizontalBar = require("graphics.elements.indicators.HorizontalBar")
|
||||
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
|
||||
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
|
||||
|
||||
local CONTAINER_MODE = types.CONTAINER_MODE
|
||||
local COOLANT_TYPE = types.COOLANT_TYPE
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
@@ -34,7 +35,7 @@ local pipe = core.pipe
|
||||
local wh_gray = style.wh_gray
|
||||
|
||||
-- create new flow view
|
||||
---@param main graphics_element main displaybox
|
||||
---@param main DisplayBox main displaybox
|
||||
local function init(main)
|
||||
local s_hi_bright = style.theme.highlight_box_bright
|
||||
local s_field = style.theme.field_box
|
||||
@@ -45,23 +46,29 @@ local function init(main)
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
|
||||
local tank_defs = facility.tank_defs
|
||||
local tank_list = facility.tank_list
|
||||
local tank_defs = facility.tank_defs
|
||||
local tank_conns = facility.tank_conns
|
||||
local tank_list = facility.tank_list
|
||||
local tank_types = facility.tank_fluid_types
|
||||
|
||||
-- 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}
|
||||
local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=ALIGN.CENTER,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}
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,fg_bg=style.theme.header}
|
||||
|
||||
datetime.register(facility.ps, "date_time", datetime.set_value)
|
||||
|
||||
local po_pipes = {}
|
||||
local water_pipes = {}
|
||||
local emcool_pipes = {}
|
||||
|
||||
-- get the y offset for this unit index
|
||||
---@param idx integer unit index
|
||||
local function y_ofs(idx) return ((idx - 1) * 20) end
|
||||
|
||||
-- get the coolant color
|
||||
---@param idx integer tank index
|
||||
local function c_clr(idx) return util.trinary(tank_types[tank_conns[idx]] == COOLANT_TYPE.WATER, colors.blue, colors.lightBlue) 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
|
||||
@@ -81,12 +88,13 @@ local function init(main)
|
||||
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 color = c_clr(i)
|
||||
|
||||
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))
|
||||
table.insert(emcool_pipes, pipe(2, y, 2, y + 3, color, true))
|
||||
table.insert(emcool_pipes, pipe(2, y, 21, y, color, true))
|
||||
|
||||
local x = util.trinary((tank_types[tank_conns[i]] == COOLANT_TYPE.SODIUM) or (units[i].num_boilers == 0), 45, 84)
|
||||
table.insert(emcool_pipes, pipe(21, y, x, y + 2, color, true, true))
|
||||
end
|
||||
end
|
||||
else
|
||||
@@ -94,17 +102,17 @@ local function init(main)
|
||||
for i = 1, #tank_defs do
|
||||
if tank_defs[i] > 0 then
|
||||
local y = y_ofs(i)
|
||||
local color = c_clr(i)
|
||||
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(1, y, 21, y, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(1, y, 21, y, color, 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))
|
||||
table.insert(emcool_pipes, pipe(2, y, 2, y + 3, color, true))
|
||||
table.insert(emcool_pipes, pipe(2, y, 21, y, color, 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))
|
||||
local x = util.trinary((tank_types[tank_conns[i]] == COOLANT_TYPE.SODIUM) or (units[i].num_boilers == 0), 45, 84)
|
||||
table.insert(emcool_pipes, pipe(21, y, x, y + 2, color, true, true))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -114,13 +122,14 @@ local function init(main)
|
||||
|
||||
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))
|
||||
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, c_clr(first_fdef), true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, c_clr(first_fdef), true))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -130,17 +139,19 @@ local function init(main)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
local color = c_clr(first_fdef)
|
||||
|
||||
if i == 4 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -149,12 +160,12 @@ local function init(main)
|
||||
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))
|
||||
table.insert(emcool_pipes, pipe(0, y_ofs(a), 1, y_ofs(a) + 6, c_clr(a), true))
|
||||
if tank_defs[b] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y_ofs(b) - 13, 1, y_ofs(b), colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y_ofs(b) - 13, 1, y_ofs(b), c_clr(a), 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))
|
||||
table.insert(emcool_pipes, pipe(0, y_ofs(b), 1, y_ofs(b) + 6, c_clr(b), true))
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 4 then
|
||||
@@ -163,17 +174,19 @@ local function init(main)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
local color = c_clr(first_fdef)
|
||||
|
||||
if i == 1 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -183,17 +196,19 @@ local function init(main)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
local color = c_clr(first_fdef)
|
||||
|
||||
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))
|
||||
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -203,17 +218,19 @@ local function init(main)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
local color = c_clr(first_fdef)
|
||||
|
||||
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))
|
||||
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -223,17 +240,19 @@ local function init(main)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
local color = c_clr(first_fdef)
|
||||
|
||||
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))
|
||||
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -241,15 +260,15 @@ local function init(main)
|
||||
end
|
||||
|
||||
local flow_x = 3
|
||||
if #water_pipes > 0 then
|
||||
if #emcool_pipes > 0 then
|
||||
flow_x = 25
|
||||
PipeNetwork{parent=main,x=2,y=3,pipes=water_pipes,bg=style.theme.bg}
|
||||
PipeNetwork{parent=main,x=2,y=3,pipes=emcool_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))
|
||||
unit_flow(main, flow_x, 5 + y_offset, #emcool_pipes == 0, i)
|
||||
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.green, true, true))
|
||||
util.nop()
|
||||
end
|
||||
|
||||
@@ -265,9 +284,9 @@ local function init(main)
|
||||
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}
|
||||
TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=text_col,width=2}
|
||||
|
||||
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", i * 5),colors=style.ind_grn}
|
||||
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", (i * 6) - 1),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)
|
||||
@@ -275,6 +294,35 @@ local function init(main)
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------
|
||||
-- auxiliary coolant valves --
|
||||
------------------------------
|
||||
|
||||
for i = 1, facility.num_units do
|
||||
if units[i].aux_coolant then
|
||||
local vx
|
||||
local vy = 3 + y_ofs(i)
|
||||
|
||||
if #emcool_pipes == 0 then
|
||||
vx = util.trinary(units[i].num_boilers == 0, 36, 79)
|
||||
else
|
||||
local em_water = tank_types[tank_conns[i]] == COOLANT_TYPE.WATER
|
||||
vx = util.trinary(units[i].num_boilers == 0, 58, util.trinary(units[i].has_tank and em_water, 94, 91))
|
||||
end
|
||||
|
||||
PipeNetwork{parent=main,x=vx-6,y=vy,pipes={pipe(0,1,9,0,colors.blue,true)},bg=style.theme.bg}
|
||||
|
||||
TextBox{parent=main,x=vx,y=vy,text="\x10\x11",fg_bg=text_col,width=2}
|
||||
TextBox{parent=main,x=vx+5,y=vy,text="\x1b",fg_bg=cpair(colors.blue,text_col.bkg),width=1}
|
||||
|
||||
local conn = IndicatorLight{parent=main,x=vx-3,y=vy+1,label=util.sprintf("PV%02d-AUX", i * 6),colors=style.ind_grn}
|
||||
local open = IndicatorLight{parent=main,x=vx-3,y=vy+2,label="OPEN",colors=style.ind_wht}
|
||||
|
||||
conn.register(units[i].unit_ps, "V_aux_conn", conn.update)
|
||||
open.register(units[i].unit_ps, "V_aux_state", open.update)
|
||||
end
|
||||
end
|
||||
|
||||
-------------------
|
||||
-- dynamic tanks --
|
||||
-------------------
|
||||
@@ -292,21 +340,23 @@ local function init(main)
|
||||
|
||||
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}
|
||||
TextBox{parent=tank,text=" ",x=1,y=1,fg_bg=style.lg_gray}
|
||||
TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=ALIGN.CENTER,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}
|
||||
TextBox{parent=tank_box,x=2,y=3,text="Fill",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}
|
||||
local is_water = tank_types[i] == COOLANT_TYPE.WATER
|
||||
|
||||
TextBox{parent=tank_box,x=2,y=9,text="In/Out Mode",height=1,width=11,fg_bg=style.label}
|
||||
TextBox{parent=tank_box,x=2,y=6,text=util.trinary(is_water,"Water","Sodium").." Level",width=12,fg_bg=style.label}
|
||||
local level = HorizontalBar{parent=tank_box,x=2,y=7,bar_fg_bg=cpair(util.trinary(is_water,colors.blue,colors.lightBlue),colors.gray),height=1,width=16}
|
||||
|
||||
TextBox{parent=tank_box,x=2,y=9,text="In/Out Mode",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}
|
||||
|
||||
@@ -344,8 +394,8 @@ local function init(main)
|
||||
|
||||
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}
|
||||
TextBox{parent=sps,text=" ",width=24,x=1,y=1,fg_bg=style.lg_gray}
|
||||
TextBox{parent=sps,text="SPS",alignment=ALIGN.CENTER,width=24,fg_bg=wh_gray}
|
||||
|
||||
local sps_box = Rectangle{parent=sps,border=border(1,colors.gray,true),width=24,height=10}
|
||||
|
||||
@@ -353,12 +403,12 @@ local function init(main)
|
||||
|
||||
status.register(facility.sps_ps_tbl[1], "computed_status", status.update)
|
||||
|
||||
TextBox{parent=sps_box,x=2,y=3,text="Input Rate",height=1,width=10,fg_bg=style.label}
|
||||
TextBox{parent=sps_box,x=2,y=3,text="Input Rate",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}
|
||||
TextBox{parent=sps_box,x=2,y=6,text="Production Rate",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)
|
||||
@@ -367,13 +417,13 @@ local function init(main)
|
||||
-- statistics --
|
||||
----------------
|
||||
|
||||
TextBox{parent=main,x=145,y=16,text="RAW WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
|
||||
TextBox{parent=main,x=145,y=16,text="RAW WASTE",alignment=ALIGN.CENTER,width=19,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}
|
||||
TextBox{parent=main,x=145,y=21,text="PROC. WASTE",alignment=ALIGN.CENTER,width=19,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}
|
||||
@@ -383,7 +433,7 @@ local function init(main)
|
||||
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}
|
||||
TextBox{parent=main,x=145,y=28,text="SPENT WASTE",alignment=ALIGN.CENTER,width=19,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}
|
||||
|
||||
|
||||
@@ -14,16 +14,16 @@ local pkt_entry = require("coordinator.ui.components.pkt_entry")
|
||||
|
||||
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 LEDPair = require("graphics.elements.indicators.ledpair")
|
||||
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.RGBLED")
|
||||
|
||||
local LINK_STATE = types.PANEL_LINK_STATE
|
||||
|
||||
@@ -34,12 +34,14 @@ local cpair = core.cpair
|
||||
local led_grn = style.led_grn
|
||||
|
||||
-- create new front panel view
|
||||
---@param panel graphics_element main displaybox
|
||||
---@param panel DisplayBox 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=ALIGN.CENTER,height=1,fg_bg=style.fp_theme.header}
|
||||
local term_w, term_h = term.getSize()
|
||||
|
||||
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,fg_bg=style.fp_theme.header}
|
||||
|
||||
local page_div = Div{parent=panel,x=1,y=3}
|
||||
|
||||
@@ -61,7 +63,7 @@ local function init(panel, num_units)
|
||||
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}}
|
||||
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.fp_ind_bkg}}
|
||||
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
network.register(ps, "link_state", network.update)
|
||||
else
|
||||
@@ -110,7 +112,7 @@ local function init(panel, num_units)
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local comp_id = util.sprintf("(%d)", os.getComputerID())
|
||||
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=style.fp.disabled_fg}
|
||||
TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=style.fp.disabled_fg}
|
||||
|
||||
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
|
||||
|
||||
@@ -131,9 +133,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=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}
|
||||
local about = Div{parent=main_page,width=15,height=2,y=term_h-3,fg_bg=style.fp.disabled_fg}
|
||||
local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
|
||||
local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
|
||||
|
||||
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)
|
||||
@@ -145,8 +147,8 @@ local function init(panel, num_units)
|
||||
-- API page
|
||||
|
||||
local api_page = Div{parent=page_div,x=1,y=1,hidden=true}
|
||||
local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_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
|
||||
local api_list = ListBox{parent=api_page,y=1,height=term_h-2,width=term_w,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} -- padding
|
||||
|
||||
-- assemble page panes
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@ local unit_overview = require("coordinator.ui.components.unit_overview")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
-- create new main view
|
||||
---@param main graphics_element main displaybox
|
||||
---@param main DisplayBox main displaybox
|
||||
local function init(main)
|
||||
local s_header = style.theme.header
|
||||
|
||||
@@ -29,15 +29,16 @@ local function init(main)
|
||||
local units = iocontrol.get_db().units
|
||||
|
||||
-- window header message
|
||||
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=ALIGN.CENTER,height=1,fg_bg=s_header}
|
||||
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=ALIGN.CENTER,fg_bg=s_header}
|
||||
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=style.lg_white,width=12,fg_bg=s_header}
|
||||
-- max length example: "01:23:45 AM - Wednesday, September 28 2022"
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=s_header}
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,fg_bg=s_header}
|
||||
|
||||
ping.register(facility.ps, "sv_ping", ping.update)
|
||||
datetime.register(facility.ps, "date_time", datetime.set_value)
|
||||
|
||||
local uo_1, uo_2, uo_3, uo_4 ---@type graphics_element
|
||||
---@type Div, Div, Div, Div
|
||||
local uo_1, uo_2, uo_3, uo_4
|
||||
|
||||
local cnc_y_start = 3
|
||||
local row_1_height = 0
|
||||
@@ -79,7 +80,7 @@ local function init(main)
|
||||
|
||||
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=string.rep("\x8c", header.get_width()),alignment=ALIGN.CENTER,height=1,fg_bg=style.lg_gray}
|
||||
TextBox{parent=main,y=cnc_bottom_align_start,text=string.rep("\x8c", header.get_width()),alignment=ALIGN.CENTER,fg_bg=style.lg_gray}
|
||||
|
||||
cnc_bottom_align_start = cnc_bottom_align_start + 2
|
||||
|
||||
@@ -87,7 +88,7 @@ local function init(main)
|
||||
|
||||
util.nop()
|
||||
|
||||
imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1])
|
||||
imatrix(main, 131, cnc_bottom_align_start, facility.induction_ps_tbl[1])
|
||||
end
|
||||
|
||||
return init
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
local unit_detail = require("coordinator.ui.components.unit_detail")
|
||||
|
||||
-- create a unit view
|
||||
---@param main graphics_element main displaybox
|
||||
---@param main DisplayBox main displaybox
|
||||
---@param id integer
|
||||
local function init(main, id)
|
||||
unit_detail(main, id)
|
||||
|
||||
@@ -8,15 +8,17 @@ local util = require("scada-common.util")
|
||||
local pgi = {}
|
||||
|
||||
local data = {
|
||||
pkt_list = nil, ---@type nil|graphics_element
|
||||
pkt_entry = nil, ---@type function
|
||||
pkt_list = nil, ---@type ListBox|nil
|
||||
pkt_entry = nil, ---@type function
|
||||
-- session entries
|
||||
s_entries = { pkt = {} }
|
||||
s_entries = {
|
||||
pkt = {} ---@type Div[]
|
||||
}
|
||||
}
|
||||
|
||||
-- link list boxes
|
||||
---@param pkt_list graphics_element pocket list element
|
||||
---@param pkt_entry function pocket entry constructor
|
||||
---@param pkt_list ListBox pocket list element
|
||||
---@param pkt_entry fun(parent: ListBox, id: integer) : Div pocket entry constructor
|
||||
function pgi.link_elements(pkt_list, pkt_entry)
|
||||
data.pkt_list = pkt_list
|
||||
data.pkt_entry = pkt_entry
|
||||
|
||||
@@ -2,16 +2,20 @@
|
||||
-- Graphics Style Options
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local themes = require("graphics.themes")
|
||||
local core = require("graphics.core")
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
|
||||
---@class crd_style
|
||||
local style = {}
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local config = coordinator.config
|
||||
|
||||
-- front panel styling
|
||||
|
||||
style.fp_theme = themes.sandstone
|
||||
@@ -147,236 +151,110 @@ style.gray_white = cpair(colors.gray, colors.white)
|
||||
-- UI COMPONENTS --
|
||||
|
||||
style.reactor = {
|
||||
-- reactor states
|
||||
-- reactor states<br>
|
||||
---@see REACTOR_STATE
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "PLC OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "PLC FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "DISABLED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ACTIVE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.red),
|
||||
text = "SCRAMMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.red),
|
||||
text = "FORCE DISABLED"
|
||||
}
|
||||
{ color = cpair(colors.black, colors.yellow), text = "PLC OFF-LINE" },
|
||||
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
|
||||
{ color = cpair(colors.black, colors.orange), text = "PLC FAULT" },
|
||||
{ color = cpair(colors.white, colors.gray), text = "DISABLED" },
|
||||
{ color = cpair(colors.black, colors.green), text = "ACTIVE" },
|
||||
{ color = cpair(colors.black, colors.red), text = "SCRAMMED" },
|
||||
{ color = cpair(colors.black, colors.red), text = "FORCE DISABLED" }
|
||||
}
|
||||
}
|
||||
|
||||
style.boiler = {
|
||||
-- boiler states
|
||||
-- boiler states<br>
|
||||
---@see BOILER_STATE
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "IDLE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ACTIVE"
|
||||
}
|
||||
{ color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
|
||||
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
|
||||
{ color = cpair(colors.black, colors.orange), text = "RTU FAULT" },
|
||||
{ color = cpair(colors.white, colors.gray), text = "IDLE" },
|
||||
{ color = cpair(colors.black, colors.green), text = "ACTIVE" }
|
||||
}
|
||||
}
|
||||
|
||||
style.turbine = {
|
||||
-- turbine states
|
||||
-- turbine states<br>
|
||||
---@see TURBINE_STATE
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "IDLE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ACTIVE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.red),
|
||||
text = "TRIP"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style.imatrix = {
|
||||
-- induction matrix states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ONLINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "LOW CHARGE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "HIGH CHARGE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style.sps = {
|
||||
-- SPS states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "IDLE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ACTIVE"
|
||||
}
|
||||
{ color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
|
||||
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
|
||||
{ color = cpair(colors.black, colors.orange), text = "RTU FAULT" },
|
||||
{ color = cpair(colors.white, colors.gray), text = "IDLE" },
|
||||
{ color = cpair(colors.black, colors.green), text = "ACTIVE" },
|
||||
{ color = cpair(colors.black, colors.red), text = "TRIP" }
|
||||
}
|
||||
}
|
||||
|
||||
style.dtank = {
|
||||
-- dynamic tank states
|
||||
-- dynamic tank states<br>
|
||||
---@see TANK_STATE
|
||||
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"
|
||||
},
|
||||
{ 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
|
||||
style.imatrix = {
|
||||
-- induction matrix states<br>
|
||||
---@see IMATRIX_STATE
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "PLUTONIUM"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.cyan),
|
||||
text = "POLONIUM"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.purple),
|
||||
text = "ANTI MATTER"
|
||||
}
|
||||
},
|
||||
states_abbrv = {
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "Pu"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.cyan),
|
||||
text = "Po"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.purple),
|
||||
text = "AM"
|
||||
}
|
||||
},
|
||||
-- process radio button options
|
||||
options = { "Plutonium", "Polonium", "Antimatter" },
|
||||
-- unit waste selection
|
||||
unit_opts = {
|
||||
{
|
||||
text = "Auto",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.white, colors.gray)
|
||||
},
|
||||
{
|
||||
text = "Pu",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.black, colors.green)
|
||||
},
|
||||
{
|
||||
text = "Po",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.black, colors.cyan)
|
||||
},
|
||||
{
|
||||
text = "AM",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.black, colors.purple)
|
||||
}
|
||||
{ color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
|
||||
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
|
||||
{ color = cpair(colors.black, colors.orange), text = "RTU FAULT" },
|
||||
{ color = cpair(colors.black, colors.green), text = "ONLINE" },
|
||||
{ color = cpair(colors.black, colors.yellow), text = "LOW CHARGE" },
|
||||
{ color = cpair(colors.black, colors.yellow), text = "HIGH CHARGE" }
|
||||
}
|
||||
}
|
||||
|
||||
style.sps = {
|
||||
-- SPS states<br>
|
||||
---@see SPS_STATE
|
||||
states = {
|
||||
{ color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
|
||||
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
|
||||
{ color = cpair(colors.black, colors.orange), text = "RTU FAULT" },
|
||||
{ color = cpair(colors.white, colors.gray), text = "IDLE" },
|
||||
{ color = cpair(colors.black, colors.green), text = "ACTIVE" }
|
||||
}
|
||||
}
|
||||
|
||||
-- get waste styling, which depends on the configuration
|
||||
---@return { states: { color: color, text: string }, states_abbrv: { color: color, text: string }, options: string[], unit_opts: { text: string, fg_bg: cpair, active_fg_bg:cpair } }
|
||||
function style.get_waste()
|
||||
local pu_color = util.trinary(config.GreenPuPellet, colors.green, colors.cyan)
|
||||
local po_color = util.trinary(config.GreenPuPellet, colors.cyan, colors.green)
|
||||
|
||||
return {
|
||||
-- auto waste processing states
|
||||
states = {
|
||||
{ color = cpair(colors.black, pu_color), text = "PLUTONIUM" },
|
||||
{ color = cpair(colors.black, po_color), text = "POLONIUM" },
|
||||
{ color = cpair(colors.black, colors.purple), text = "ANTI MATTER" }
|
||||
},
|
||||
states_abbrv = {
|
||||
{ color = cpair(colors.black, pu_color), text = "Pu" },
|
||||
{ color = cpair(colors.black, po_color), text = "Po" },
|
||||
{ color = cpair(colors.black, colors.purple), text = "AM" }
|
||||
},
|
||||
-- process radio button options
|
||||
options = { "Plutonium", "Polonium", "Antimatter" },
|
||||
-- unit waste selection
|
||||
unit_opts = {
|
||||
{ text = "Auto", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.white, colors.gray) },
|
||||
{ text = "Pu", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, pu_color) },
|
||||
{ text = "Po", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, po_color) },
|
||||
{ text = "AM", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.purple) }
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
return style
|
||||
|
||||
@@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
|
||||
|
||||
local core = {}
|
||||
|
||||
core.version = "2.2.4"
|
||||
core.version = "2.4.8"
|
||||
|
||||
core.flasher = flasher
|
||||
core.events = events
|
||||
@@ -17,6 +17,8 @@ core.events = events
|
||||
---@enum ALIGN
|
||||
core.ALIGN = { LEFT = 1, CENTER = 2, RIGHT = 3 }
|
||||
|
||||
---@alias Container DisplayBox|Div|ListBox|MultiPane|AppMultiPane|Rectangle
|
||||
|
||||
---@class graphics_border
|
||||
---@field width integer
|
||||
---@field color color
|
||||
@@ -123,15 +125,17 @@ 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)
|
||||
---@param e graphics_base element
|
||||
---@param max_len integer max value length
|
||||
---@param fg_bg cpair enabled fg/bg
|
||||
---@param dis_fg_bg? cpair disabled fg/bg
|
||||
---@param align_right? boolean true to align content right while unfocused
|
||||
function core.new_ifield(e, max_len, fg_bg, dis_fg_bg, align_right)
|
||||
local self = {
|
||||
frame_start = 1,
|
||||
visible_text = e.value,
|
||||
cursor_pos = string.len(e.value) + 1,
|
||||
align_offset = 0,
|
||||
selected_all = false
|
||||
}
|
||||
|
||||
@@ -186,7 +190,12 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
|
||||
e.w_write(string.rep(" ", e.frame.w))
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
local function _write()
|
||||
local function _write(align_r)
|
||||
if align_r and string.len(self.visible_text) <=e.frame.w then
|
||||
self.align_offset = (e.frame.w - string.len(self.visible_text))
|
||||
e.w_set_cur((e.frame.w - string.len(self.visible_text)) + 1, 1)
|
||||
end
|
||||
|
||||
if self.censor then
|
||||
e.w_write(string.rep(self.censor, string.len(self.visible_text)))
|
||||
else
|
||||
@@ -226,15 +235,27 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
|
||||
self.selected_all = false
|
||||
|
||||
-- write text without cursor
|
||||
_write()
|
||||
_write(align_right)
|
||||
end
|
||||
end
|
||||
|
||||
-- move cursor to x
|
||||
-- get an x value to pass to move_cursor taking into account right alignment offset present when unfocused
|
||||
---@param x integer
|
||||
function public.get_cursor_align_shift(x)
|
||||
return math.max(0, x - self.align_offset)
|
||||
end
|
||||
|
||||
-- move cursor to x
|
||||
---@param x integer x position or 0 to jump to the end
|
||||
function public.move_cursor(x)
|
||||
self.selected_all = false
|
||||
self.cursor_pos = math.min(x, string.len(self.visible_text) + 1)
|
||||
|
||||
if x <= 0 then
|
||||
self.cursor_pos = string.len(self.visible_text) + 1
|
||||
else
|
||||
self.cursor_pos = math.min(x, string.len(self.visible_text) + 1)
|
||||
end
|
||||
|
||||
public.show()
|
||||
end
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ local events = core.events
|
||||
|
||||
local element = {}
|
||||
|
||||
---@class graphics_args_generic
|
||||
---@field window? table
|
||||
---@field parent? graphics_element
|
||||
---@class graphics_args
|
||||
---@field window? Window base window to use, only root elements should use this
|
||||
---@field parent? graphics_element parent element, if not a root element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer next line if omitted
|
||||
@@ -23,47 +23,6 @@ local element = {}
|
||||
---@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
|
||||
---|hbar_args
|
||||
---|icon_indicator_args
|
||||
---|indicator_led_args
|
||||
---|indicator_led_pair_args
|
||||
---|indicator_led_rgb_args
|
||||
---|indicator_light_args
|
||||
---|power_indicator_args
|
||||
---|rad_indicator_args
|
||||
---|signal_bar_args
|
||||
---|state_indicator_args
|
||||
---|tristate_indicator_light_args
|
||||
---|vbar_args
|
||||
---|app_multipane_args
|
||||
---|colormap_args
|
||||
---|displaybox_args
|
||||
---|div_args
|
||||
---|listbox_args
|
||||
---|multipane_args
|
||||
---|pipenet_args
|
||||
---|rectangle_args
|
||||
---|textbox_args
|
||||
---|tiling_args
|
||||
|
||||
---@class element_subscription
|
||||
---@field ps psil ps used
|
||||
---@field key string data key
|
||||
@@ -82,22 +41,23 @@ end
|
||||
-- a base graphics element, should not be created on its own
|
||||
---@nodiscard
|
||||
---@param args graphics_args arguments
|
||||
---@param constraint? function apply a dimensional constraint based on proposed dimensions function(frame) -> width, height
|
||||
---@param child_offset_x? integer mouse event offset x
|
||||
---@param child_offset_y? integer mouse event offset y
|
||||
function element.new(args, child_offset_x, child_offset_y)
|
||||
function element.new(args, constraint, 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
|
||||
p_window = nil, ---@type Window
|
||||
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 = {},
|
||||
next_id = 1, -- next child ID
|
||||
subscriptions = {}, ---@type { ps: psil, key: string, func: function }[]
|
||||
button_down = { events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1) },
|
||||
focused = false,
|
||||
mt = {}
|
||||
@@ -107,13 +67,13 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
local protected = {
|
||||
enabled = true,
|
||||
value = nil, ---@type any
|
||||
window = nil, ---@type table
|
||||
content_window = nil, ---@type table|nil
|
||||
window = nil, ---@type Window
|
||||
content_window = nil, ---@type Window|nil
|
||||
mouse_window_shift = { x = 0, y = 0 },
|
||||
fg_bg = core.cpair(colors.white, colors.black),
|
||||
frame = core.gframe(1, 1, 1, 1),
|
||||
children = {},
|
||||
child_id_map = {}
|
||||
children = {}, ---@type graphics_base[]
|
||||
child_id_map = {} ---@type { [element_id]: integer }
|
||||
}
|
||||
|
||||
-- element as string
|
||||
@@ -126,9 +86,9 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
|
||||
setmetatable(public, self.mt)
|
||||
|
||||
-----------------------
|
||||
-- PRIVATE FUNCTIONS --
|
||||
-----------------------
|
||||
------------------------------
|
||||
--#region PRIVATE FUNCTIONS --
|
||||
------------------------------
|
||||
|
||||
-- use tab to jump to the next focusable field
|
||||
---@param reverse boolean
|
||||
@@ -166,10 +126,10 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
end
|
||||
end
|
||||
|
||||
---@param children table
|
||||
---@param children graphics_base[]
|
||||
local function traverse(children)
|
||||
for i = 1, #children do
|
||||
local child = children[i] ---@type graphics_base
|
||||
local child = children[i]
|
||||
handle_element(child.get())
|
||||
if child.get().is_visible() then traverse(child.children) end
|
||||
end
|
||||
@@ -189,9 +149,11 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------
|
||||
-- PROTECTED FUNCTIONS --
|
||||
-------------------------
|
||||
--#endregion
|
||||
|
||||
--------------------------------
|
||||
--#region PROTECTED FUNCTIONS --
|
||||
--------------------------------
|
||||
|
||||
-- prepare the template
|
||||
---@param offset_x integer x offset for mouse events
|
||||
@@ -199,7 +161,7 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
---@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)
|
||||
next_y = util.trinary(args.height == nil and constraint == nil, 1, next_y)
|
||||
|
||||
-- record offsets in case there is a reposition
|
||||
self.offset_x = offset_x
|
||||
@@ -226,6 +188,13 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
local w, h = self.p_window.getSize()
|
||||
f.w = math.min(f.w, w - (f.x - 1))
|
||||
f.h = math.min(f.h, h - (f.y - 1))
|
||||
|
||||
if type(constraint) == "function" then
|
||||
-- constrain per provided constraint function (can only get smaller than available space)
|
||||
w, h = constraint(f)
|
||||
f.w = math.min(f.w, w)
|
||||
f.h = math.min(f.h, h)
|
||||
end
|
||||
end
|
||||
|
||||
-- check frame
|
||||
@@ -277,24 +246,29 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
|
||||
-- alias functions
|
||||
|
||||
-- window set cursor position
|
||||
-- window set cursor position<br>
|
||||
---@see Window.setCursorPos
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
function protected.w_set_cur(x, y) protected.window.setCursorPos(x, y) end
|
||||
|
||||
-- set background color
|
||||
-- set background color<br>
|
||||
---@see Window.setBackgroundColor
|
||||
---@param c color
|
||||
function protected.w_set_bkg(c) protected.window.setBackgroundColor(c) end
|
||||
|
||||
-- set foreground (text) color
|
||||
-- set foreground (text) color<br>
|
||||
---@see Window.setTextColor
|
||||
---@param c color
|
||||
function protected.w_set_fgd(c) protected.window.setTextColor(c) end
|
||||
|
||||
-- write text
|
||||
-- write text<br>
|
||||
---@see Window.write
|
||||
---@param str string
|
||||
function protected.w_write(str) protected.window.write(str) end
|
||||
|
||||
-- blit text
|
||||
-- blit text<br>
|
||||
---@see Window.blit
|
||||
---@param str string
|
||||
---@param fg string
|
||||
---@param bg string
|
||||
@@ -326,8 +300,10 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
|
||||
-- report completion of element instantiation and get the public interface
|
||||
---@nodiscard
|
||||
---@param redraw? boolean true to call redraw as part of completing this element
|
||||
---@return graphics_element element, element_id id
|
||||
function protected.complete()
|
||||
function protected.complete(redraw)
|
||||
if redraw then protected.redraw() end
|
||||
if args.parent ~= nil then args.parent.__child_ready(self.id, public) end
|
||||
return public, self.id
|
||||
end
|
||||
@@ -343,7 +319,7 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
-- focus this element and take away focus from all other elements
|
||||
function protected.take_focus() args.parent.__focus_child(public) end
|
||||
|
||||
-- action handlers --
|
||||
--#region Action Handlers
|
||||
|
||||
-- luacheck: push ignore
|
||||
---@diagnostic disable: unused-local, unused-vararg
|
||||
@@ -392,14 +368,12 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
function protected.handle_paste(text) end
|
||||
|
||||
-- handle data value changes
|
||||
---@vararg any value(s)
|
||||
---@param ... any value(s)
|
||||
function protected.on_update(...) end
|
||||
|
||||
-- callback on control press responses
|
||||
---@param result any
|
||||
function protected.response_callback(result) end
|
||||
--#endregion
|
||||
|
||||
-- accessors and control --
|
||||
--#region Accessors and Control
|
||||
|
||||
-- get value
|
||||
---@nodiscard
|
||||
@@ -418,11 +392,11 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
function protected.set_max(max) end
|
||||
|
||||
-- custom recolor command, varies by element if implemented
|
||||
---@vararg cpair|color color(s)
|
||||
---@param ... cpair|color color(s)
|
||||
function protected.recolor(...) end
|
||||
|
||||
-- custom resize command, varies by element if implemented
|
||||
---@vararg integer sizing
|
||||
---@param ... integer sizing
|
||||
function protected.resize(...) end
|
||||
|
||||
-- luacheck: pop
|
||||
@@ -437,9 +411,13 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
-- stop animations
|
||||
function protected.stop_anim() end
|
||||
|
||||
-----------
|
||||
-- SETUP --
|
||||
-----------
|
||||
--#endregion
|
||||
|
||||
--#endregion
|
||||
|
||||
------------------
|
||||
--#region SETUP --
|
||||
------------------
|
||||
|
||||
-- get the parent window
|
||||
self.p_window = args.window
|
||||
@@ -458,9 +436,11 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
self.id = args.parent.__add_child(args.id, protected)
|
||||
end
|
||||
|
||||
----------------------
|
||||
-- PUBLIC FUNCTIONS --
|
||||
----------------------
|
||||
--#endregion
|
||||
|
||||
-----------------------------
|
||||
--#region PUBLIC FUNCTIONS --
|
||||
-----------------------------
|
||||
|
||||
-- get the window object
|
||||
---@nodiscard
|
||||
@@ -499,9 +479,10 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
end
|
||||
end
|
||||
|
||||
-- ELEMENT TREE --
|
||||
--#region ELEMENT TREE
|
||||
|
||||
-- add a child element
|
||||
---@package
|
||||
---@nodiscard
|
||||
---@param key string|nil id
|
||||
---@param child graphics_base
|
||||
@@ -511,20 +492,24 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
|
||||
self.next_y = child.frame.y + child.frame.h
|
||||
|
||||
local id = key ---@type string|integer|nil
|
||||
local id = key ---@type element_id|nil
|
||||
if id == nil then
|
||||
id = self.next_id
|
||||
self.next_id = self.next_id + 1
|
||||
end
|
||||
|
||||
table.insert(protected.children, child)
|
||||
-- see #539 on GitHub
|
||||
-- using #protected.children after inserting may give the wrong index, since if it inserts in a hole that completes the list then
|
||||
-- the length will jump up to the full length of the list, possibly making two map entries point to the same child
|
||||
protected.child_id_map[id] = #protected.children + 1
|
||||
|
||||
protected.child_id_map[id] = #protected.children
|
||||
table.insert(protected.children, child)
|
||||
|
||||
return id
|
||||
end
|
||||
|
||||
-- remove a child element
|
||||
---@package
|
||||
---@param id element_id id
|
||||
function public.__remove_child(id)
|
||||
local index = protected.child_id_map[id]
|
||||
@@ -536,11 +521,13 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
end
|
||||
|
||||
-- actions to take upon a child element becoming ready (initial draw/construction completed)
|
||||
---@package
|
||||
---@param key element_id id
|
||||
---@param child graphics_element
|
||||
function public.__child_ready(key, child) protected.on_added(key, child) end
|
||||
|
||||
-- focus solely on this child
|
||||
---@package
|
||||
---@param child graphics_element
|
||||
function public.__focus_child(child)
|
||||
if self.is_root then
|
||||
@@ -550,6 +537,7 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
end
|
||||
|
||||
-- a child was focused, used to make sure it is actually visible to the user in the content frame
|
||||
---@package
|
||||
---@param child graphics_element
|
||||
function public.__child_focused(child)
|
||||
protected.on_child_focused(child)
|
||||
@@ -559,8 +547,17 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
-- get a child element
|
||||
---@nodiscard
|
||||
---@param id element_id
|
||||
---@return graphics_element
|
||||
function public.get_child(id) return protected.children[protected.child_id_map[id]].get() end
|
||||
---@return graphics_element element
|
||||
function public.get_child(id) return ({ protected.children[protected.child_id_map[id]].get() })[1] end
|
||||
|
||||
-- get all children
|
||||
---@nodiscard
|
||||
---@return table children table of graphics_element objects
|
||||
function public.get_children()
|
||||
local list = {}
|
||||
for k, v in pairs(protected.children) do list[k] = v.get() end
|
||||
return list
|
||||
end
|
||||
|
||||
-- remove a child element
|
||||
---@param id element_id
|
||||
@@ -577,7 +574,7 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
-- 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
|
||||
local child = protected.children[i].get() ---@type graphics_element
|
||||
child.delete()
|
||||
protected.on_removed(child.get_id())
|
||||
end
|
||||
@@ -598,29 +595,33 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
local elem = child.get().get_element_by_id(id)
|
||||
if elem ~= nil then return elem end
|
||||
end
|
||||
else return protected.children[index].get() end
|
||||
else return ({ protected.children[index].get() })[1] end
|
||||
end
|
||||
|
||||
-- AUTO-PLACEMENT --
|
||||
--#endregion
|
||||
|
||||
--#region AUTO-PLACEMENT
|
||||
|
||||
-- skip a line for automatically placed elements
|
||||
function public.line_break()
|
||||
self.next_y = self.next_y + 1
|
||||
end
|
||||
|
||||
-- PROPERTIES --
|
||||
--#endregion
|
||||
|
||||
-- get element id
|
||||
--#region PROPERTIES
|
||||
|
||||
-- get element ID
|
||||
---@nodiscard
|
||||
---@return element_id
|
||||
function public.get_id() return self.id end
|
||||
|
||||
-- get element x
|
||||
-- get element relative x position
|
||||
---@nodiscard
|
||||
---@return integer x
|
||||
function public.get_x() return protected.frame.x end
|
||||
|
||||
-- get element y
|
||||
-- get element relative y position
|
||||
---@nodiscard
|
||||
---@return integer y
|
||||
function public.get_y() return protected.frame.y end
|
||||
@@ -640,12 +641,12 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
---@return cpair fg_bg
|
||||
function public.get_fg_bg() return protected.fg_bg end
|
||||
|
||||
-- get the element value
|
||||
-- get the element's value
|
||||
---@nodiscard
|
||||
---@return any value
|
||||
function public.get_value() return protected.get_value() end
|
||||
|
||||
-- set the element value
|
||||
-- set the element's value
|
||||
---@param value any new value
|
||||
function public.set_value(value) protected.set_value(value) end
|
||||
|
||||
@@ -707,11 +708,11 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
end
|
||||
|
||||
-- custom recolor command, varies by element if implemented
|
||||
---@vararg cpair|color color(s)
|
||||
---@param ... cpair|color color(s)
|
||||
function public.recolor(...) protected.recolor(...) end
|
||||
|
||||
-- resize attributes of the element value if supported
|
||||
---@vararg number dimensions (element specific)
|
||||
---@param ... number dimensions (element specific)
|
||||
function public.resize(...) protected.resize(...) end
|
||||
|
||||
-- reposition the element window<br>
|
||||
@@ -735,7 +736,9 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
self.bounds.y2 = self.position.y + protected.frame.h - 1
|
||||
end
|
||||
|
||||
-- FUNCTION CALLBACKS --
|
||||
--#endregion
|
||||
|
||||
--#region FUNCTION CALLBACKS
|
||||
|
||||
-- handle a monitor touch or mouse click if this element is visible
|
||||
---@param event mouse_interaction mouse interaction event
|
||||
@@ -797,13 +800,9 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
end
|
||||
|
||||
-- draw the element given new data
|
||||
---@vararg any new data
|
||||
---@param ... any new data
|
||||
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
|
||||
|
||||
-- register a callback with a PSIL, allowing for automatic unregister on delete<br>
|
||||
-- do not use graphics elements directly with PSIL subscribe()
|
||||
---@param ps psil PSIL to subscribe to
|
||||
@@ -814,7 +813,9 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
ps.subscribe(key, func)
|
||||
end
|
||||
|
||||
-- VISIBILITY & ANIMATIONS --
|
||||
--#endregion
|
||||
|
||||
--#region VISIBILITY & ANIMATIONS
|
||||
|
||||
-- check if this element is visible
|
||||
function public.is_visible() return protected.window.isVisible() end
|
||||
@@ -828,6 +829,7 @@ 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 Window.redraw
|
||||
---@see graphics_element.redraw
|
||||
---@see graphics_element.content_redraw
|
||||
---@param clear? boolean true to visibly hide this element (redraws the parent)
|
||||
@@ -879,6 +881,10 @@ function element.new(args, child_offset_x, child_offset_y)
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
--#endregion
|
||||
|
||||
return protected
|
||||
end
|
||||
|
||||
|
||||
@@ -24,15 +24,15 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new app multipane element
|
||||
-- Create a new app multipane container element.
|
||||
---@nodiscard
|
||||
---@param args app_multipane_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function multipane(args)
|
||||
---@return AppMultiPane element, element_id id
|
||||
return function (args)
|
||||
element.assert(type(args.panes) == "table", "panes is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = 1
|
||||
|
||||
@@ -100,10 +100,8 @@ local function multipane(args)
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class AppMultiPane:graphics_element
|
||||
local AppMultiPane, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return AppMultiPane, id
|
||||
end
|
||||
|
||||
return multipane
|
||||
@@ -9,10 +9,10 @@ local element = require("graphics.element")
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new color map
|
||||
-- Create a horizontal reference color map. Primarily used for tuning custom colors.
|
||||
---@param args colormap_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function colormap(args)
|
||||
---@return ColorMap element, element_id id
|
||||
return function (args)
|
||||
local bkg = "008877FFCCEE114455DD9933BBAA2266"
|
||||
local spaces = string.rep(" ", 32)
|
||||
|
||||
@@ -20,7 +20,7 @@ local function colormap(args)
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- draw color map
|
||||
function e.redraw()
|
||||
@@ -28,10 +28,8 @@ local function colormap(args)
|
||||
e.w_blit(spaces, bkg, bkg)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class ColorMap:graphics_element
|
||||
local ColorMap, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return ColorMap, id
|
||||
end
|
||||
|
||||
return colormap
|
||||
@@ -13,13 +13,16 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new root display box
|
||||
-- Create a root display box.
|
||||
---@nodiscard
|
||||
---@param args displaybox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function displaybox(args)
|
||||
---@return DisplayBox element, element_id id
|
||||
return function (args)
|
||||
-- create new graphics element base object
|
||||
return element.new(args).complete()
|
||||
end
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
return displaybox
|
||||
---@class DisplayBox:graphics_element
|
||||
local DisplayBox, id = e.complete()
|
||||
|
||||
return DisplayBox, id
|
||||
end
|
||||
@@ -13,13 +13,16 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new div element
|
||||
-- Create a new div container element.
|
||||
---@nodiscard
|
||||
---@param args div_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function div(args)
|
||||
---@return Div element, element_id id
|
||||
return function (args)
|
||||
-- create new graphics element base object
|
||||
return element.new(args).complete()
|
||||
end
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
return div
|
||||
---@class Div:graphics_element
|
||||
local Div, id = e.complete()
|
||||
|
||||
return Div, id
|
||||
end
|
||||
@@ -29,15 +29,15 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
---@field y integer y position
|
||||
---@field h integer element height
|
||||
|
||||
-- new listbox element
|
||||
-- Create a new scrollable listbox container element.
|
||||
---@nodiscard
|
||||
---@param args listbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function listbox(args)
|
||||
---@return ListBox element, element_id id
|
||||
return function (args)
|
||||
args.can_focus = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- create content window for child elements
|
||||
local scroll_frame = window.create(e.window, 1, 1, e.frame.w - 1, args.scroll_height, false)
|
||||
@@ -334,10 +334,8 @@ local function listbox(args)
|
||||
draw_bar()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class ListBox:graphics_element
|
||||
local ListBox, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return ListBox, id
|
||||
end
|
||||
|
||||
return listbox
|
||||
@@ -14,15 +14,15 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new multipane element
|
||||
-- Create a new multipane container element.
|
||||
---@nodiscard
|
||||
---@param args multipane_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function multipane(args)
|
||||
---@return MultiPane element, element_id id
|
||||
return function (args)
|
||||
element.assert(type(args.panes) == "table", "panes is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = 1
|
||||
|
||||
@@ -41,10 +41,8 @@ local function multipane(args)
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class MultiPane:graphics_element
|
||||
local MultiPane, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return MultiPane, id
|
||||
end
|
||||
|
||||
return multipane
|
||||
@@ -20,10 +20,10 @@ local element = require("graphics.element")
|
||||
---@field fg string foreground blit
|
||||
---@field bg string background blit
|
||||
|
||||
-- new pipe network
|
||||
-- Create a pipe network diagram.
|
||||
---@param args pipenet_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function pipenet(args)
|
||||
---@return PipeNetwork element, element_id id
|
||||
return function (args)
|
||||
element.assert(type(args.pipes) == "table", "pipes is a required field")
|
||||
|
||||
args.width = 0
|
||||
@@ -47,7 +47,7 @@ local function pipenet(args)
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- determine if there are any thin pipes involved
|
||||
local any_thin = false
|
||||
@@ -322,10 +322,8 @@ local function pipenet(args)
|
||||
if any_thin then map_draw() else vector_draw() end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class PipeNetwork:graphics_element
|
||||
local PipeNetwork, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return PipeNetwork, id
|
||||
end
|
||||
|
||||
return pipenet
|
||||
@@ -18,10 +18,10 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new rectangle
|
||||
-- Create a new rectangle container element.
|
||||
---@param args rectangle_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function rectangle(args)
|
||||
---@return Rectangle element, element_id id
|
||||
return function (args)
|
||||
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
|
||||
@@ -45,7 +45,7 @@ local function rectangle(args)
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args, offset_x, offset_y)
|
||||
local e = element.new(args --[[@as graphics_args]], nil, offset_x, offset_y)
|
||||
|
||||
-- create content window for child elements
|
||||
e.content_window = window.create(e.window, 1 + offset_x, 1 + offset_y, e.frame.w - (2 * offset_x), e.frame.h - (2 * offset_y))
|
||||
@@ -191,7 +191,8 @@ local function rectangle(args)
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
---@class Rectangle:graphics_element
|
||||
local Rectangle, id = e.complete()
|
||||
|
||||
return rectangle
|
||||
return Rectangle, id
|
||||
end
|
||||
@@ -10,24 +10,40 @@ local ALIGN = core.ALIGN
|
||||
---@class textbox_args
|
||||
---@field text string text to show
|
||||
---@field alignment? ALIGN text alignment, left by default
|
||||
---@field trim_whitespace? boolean true to trim whitespace before/after lines of text
|
||||
---@field anchor? boolean true to use this as an anchor, making it focusable
|
||||
---@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 height? integer minimum necessary height for wrapped text if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new text box
|
||||
-- Create a new text box element.
|
||||
---@param args textbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function textbox(args)
|
||||
---@return TextBox element, element_id id
|
||||
return function (args)
|
||||
element.assert(type(args.text) == "string", "text is a required field")
|
||||
|
||||
if args.anchor == true then args.can_focus = true end
|
||||
|
||||
-- provide a constraint condition to element creation to prevent an pointlessly tall text box
|
||||
---@param frame graphics_frame
|
||||
local function constrain(frame)
|
||||
local new_height = math.max(1, #util.strwrap(args.text, frame.w))
|
||||
|
||||
if args.height then
|
||||
new_height = math.max(frame.h, new_height)
|
||||
end
|
||||
|
||||
return frame.w, new_height
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]], constrain)
|
||||
|
||||
e.value = args.text
|
||||
|
||||
@@ -42,6 +58,12 @@ local function textbox(args)
|
||||
for i = 1, #lines do
|
||||
if i > e.frame.h then break end
|
||||
|
||||
-- trim leading/trailing whitespace, except on the first line
|
||||
-- leading whitespace on the first line is usually intentional
|
||||
if args.trim_whitespace == true then
|
||||
lines[i] = util.trim(lines[i])
|
||||
end
|
||||
|
||||
local len = string.len(lines[i])
|
||||
|
||||
-- use cursor position to align this line
|
||||
@@ -64,10 +86,15 @@ local function textbox(args)
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
-- change the foreground color of the text
|
||||
---@param c color
|
||||
function e.recolor(c)
|
||||
e.w_set_fgd(c)
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
---@class TextBox:graphics_element
|
||||
local TextBox, id = e.complete(true)
|
||||
|
||||
return TextBox, id
|
||||
end
|
||||
|
||||
return textbox
|
||||
@@ -18,14 +18,14 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new tiling box
|
||||
-- Create a new tiling box element.
|
||||
---@param args tiling_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function tiling(args)
|
||||
---@return Tiling element, element_id id
|
||||
return function (args)
|
||||
element.assert(type(args.fill_c) == "table", "fill_c is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
local fill_a = args.fill_c.blit_a
|
||||
local fill_b = args.fill_c.blit_b
|
||||
@@ -52,7 +52,7 @@ local function tiling(args)
|
||||
element.assert(start_x <= inner_width, "start_x > inner_width")
|
||||
element.assert(start_y <= inner_height, "start_y > inner_height")
|
||||
|
||||
-- draw tiling box
|
||||
-- draw the tiling box
|
||||
function e.redraw()
|
||||
local alternator = true
|
||||
|
||||
@@ -86,10 +86,8 @@ local function tiling(args)
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class Tiling:graphics_element
|
||||
local Tiling, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return Tiling, id
|
||||
end
|
||||
|
||||
return tiling
|
||||
@@ -12,10 +12,10 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new waiting animation element
|
||||
-- Create a new waiting animation element.
|
||||
---@param args waiting_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function waiting(args)
|
||||
---@return Waiting element, element_id id
|
||||
return function (args)
|
||||
local state = 0
|
||||
local run_animation = false
|
||||
|
||||
@@ -23,7 +23,7 @@ local function waiting(args)
|
||||
args.height = 3
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
local blit_fg = e.fg_bg.blit_fgd
|
||||
local blit_bg = e.fg_bg.blit_bkg
|
||||
@@ -103,7 +103,8 @@ local function waiting(args)
|
||||
|
||||
e.start_anim()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
---@class Waiting:graphics_element
|
||||
local Waiting, id = e.complete()
|
||||
|
||||
return waiting
|
||||
return Waiting, id
|
||||
end
|
||||
@@ -20,10 +20,10 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new app button
|
||||
-- Create a new app icon style button control element, like on a mobile device.
|
||||
---@param args app_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function app_button(args)
|
||||
---@return App element, element_id id
|
||||
return function (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")
|
||||
@@ -33,7 +33,7 @@ local function app_button(args)
|
||||
args.width = 7
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- draw the app button
|
||||
local function draw()
|
||||
@@ -123,10 +123,8 @@ local function app_button(args)
|
||||
draw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class App:graphics_element
|
||||
local App, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return App, id
|
||||
end
|
||||
|
||||
return app_button
|
||||
@@ -6,6 +6,7 @@ local element = require("graphics.element")
|
||||
---@class checkbox_args
|
||||
---@field label string checkbox text
|
||||
---@field box_fg_bg cpair colors for checkbox
|
||||
---@field disable_fg_bg? cpair text colors when disabled
|
||||
---@field default? boolean default value
|
||||
---@field callback? function function to call on press
|
||||
---@field parent graphics_element
|
||||
@@ -15,10 +16,10 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new checkbox control
|
||||
-- Create a new checkbox control element.
|
||||
---@param args checkbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function checkbox(args)
|
||||
---@return Checkbox element, element_id id
|
||||
return function (args)
|
||||
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")
|
||||
|
||||
@@ -27,7 +28,7 @@ local function checkbox(args)
|
||||
args.width = 2 + string.len(args.label)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = args.default == true
|
||||
|
||||
@@ -35,20 +36,27 @@ local function checkbox(args)
|
||||
local function draw()
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
local fgd, bkg = args.box_fg_bg.fgd, args.box_fg_bg.bkg
|
||||
|
||||
if (not e.enabled) and type(args.disable_fg_bg) == "table" then
|
||||
fgd = args.disable_fg_bg.bkg
|
||||
bkg = args.disable_fg_bg.fgd
|
||||
end
|
||||
|
||||
if e.value then
|
||||
-- show as selected
|
||||
e.w_set_fgd(args.box_fg_bg.bkg)
|
||||
e.w_set_bkg(args.box_fg_bg.fgd)
|
||||
e.w_set_fgd(bkg)
|
||||
e.w_set_bkg(fgd)
|
||||
e.w_write("\x88")
|
||||
e.w_set_fgd(args.box_fg_bg.fgd)
|
||||
e.w_set_fgd(fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write("\x95")
|
||||
else
|
||||
-- show as unselected
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(args.box_fg_bg.bkg)
|
||||
e.w_set_bkg(bkg)
|
||||
e.w_write("\x88")
|
||||
e.w_set_fgd(args.box_fg_bg.bkg)
|
||||
e.w_set_fgd(bkg)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write("\x95")
|
||||
end
|
||||
@@ -57,16 +65,18 @@ local function checkbox(args)
|
||||
-- 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)
|
||||
elseif (not e.enabled) and type(args.disable_fg_bg) == "table" then
|
||||
e.w_set_fgd(args.disable_fg_bg.fgd)
|
||||
e.w_set_bkg(args.disable_fg_bg.bkg)
|
||||
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
|
||||
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
@@ -98,24 +108,22 @@ local function checkbox(args)
|
||||
draw()
|
||||
end
|
||||
|
||||
-- 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
|
||||
e.redraw()
|
||||
-- handle focus
|
||||
e.on_focused = draw_label
|
||||
e.on_unfocused = draw_label
|
||||
|
||||
return e.complete()
|
||||
-- handle enable
|
||||
e.on_enabled = e.redraw
|
||||
e.on_disabled = e.redraw
|
||||
|
||||
---@class Checkbox:graphics_element
|
||||
local Checkbox, id = e.complete(true)
|
||||
|
||||
return Checkbox, id
|
||||
end
|
||||
|
||||
return checkbox
|
||||
@@ -10,6 +10,7 @@ local element = require("graphics.element")
|
||||
---@field accent color accent color for hazard border
|
||||
---@field dis_colors? cpair text color and border color when disabled
|
||||
---@field callback function function to call on touch
|
||||
---@field timeout? integer override for the default 1.5 second timeout, in seconds
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
@@ -17,10 +18,10 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new hazard button
|
||||
-- Create a new hazard button control element.
|
||||
---@param args hazard_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function hazard_button(args)
|
||||
---@return HazardButton element, element_id id
|
||||
return function (args)
|
||||
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")
|
||||
@@ -28,8 +29,10 @@ local function hazard_button(args)
|
||||
args.height = 3
|
||||
args.width = string.len(args.text) + 4
|
||||
|
||||
local timeout = args.timeout or 1.5
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- draw border
|
||||
---@param accent color accent color
|
||||
@@ -149,20 +152,13 @@ local function hazard_button(args)
|
||||
tcd.abort(on_success)
|
||||
tcd.abort(on_failure)
|
||||
|
||||
-- 1.5 second timeout
|
||||
tcd.dispatch(1.5, on_timeout)
|
||||
-- operation timeout animation
|
||||
tcd.dispatch(timeout, on_timeout)
|
||||
|
||||
args.callback()
|
||||
end
|
||||
end
|
||||
|
||||
-- callback on request response
|
||||
---@param result boolean true for success, false for failure
|
||||
function e.response_callback(result)
|
||||
tcd.abort(on_timeout)
|
||||
if result then on_success() else on_failure(0) end
|
||||
end
|
||||
|
||||
-- set the value (true simulates pressing the button)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
@@ -195,10 +191,15 @@ local function hazard_button(args)
|
||||
draw_border(args.accent)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class HazardButton:graphics_element
|
||||
local HazardButton, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
-- callback for request response
|
||||
---@param success boolean
|
||||
function HazardButton.on_response(success)
|
||||
tcd.abort(on_timeout)
|
||||
if success then on_success() else on_failure(0) end
|
||||
end
|
||||
|
||||
return HazardButton, id
|
||||
end
|
||||
|
||||
return hazard_button
|
||||
@@ -25,10 +25,10 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new multi button (latch selection, exclusively one button at a time)
|
||||
-- Create a new multi button control element (latch selection, exclusively one button at a time).
|
||||
---@param args multi_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function multi_button(args)
|
||||
---@return MultiButton element, element_id id
|
||||
return function (args)
|
||||
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")
|
||||
@@ -52,7 +52,7 @@ local function multi_button(args)
|
||||
args.width = (button_width * #args.options) + #args.options + 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- button state (convert nil to 1 if missing)
|
||||
e.value = args.default or 1
|
||||
@@ -126,10 +126,8 @@ local function multi_button(args)
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class MultiButton:graphics_element
|
||||
local MultiButton, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return MultiButton, id
|
||||
end
|
||||
|
||||
return multi_button
|
||||
@@ -20,10 +20,10 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new spinbox control (minimum value is 0)
|
||||
-- Create a new spinbox control element (minimum value is 0).
|
||||
---@param args spinbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function spinbox(args)
|
||||
---@return NumericSpinbox element, element_id id
|
||||
return function (args)
|
||||
-- properties
|
||||
local digits = {}
|
||||
local wn_prec = args.whole_num_precision
|
||||
@@ -51,7 +51,7 @@ local function spinbox(args)
|
||||
args.height = 3
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- set initial value
|
||||
e.value = args.default or 0
|
||||
@@ -179,10 +179,8 @@ local function spinbox(args)
|
||||
draw_arrows(util.trinary(e.enabled, args.arrow_fg_bg.fgd, args.arrow_disable or colors.lightGray))
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class NumericSpinbox:graphics_element
|
||||
local NumericSpinbox, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return NumericSpinbox, id
|
||||
end
|
||||
|
||||
return spinbox
|
||||
@@ -1,6 +1,7 @@
|
||||
-- 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")
|
||||
@@ -21,14 +22,13 @@ local KEY_CLICK = core.events.KEY_CLICK
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new push button
|
||||
-- Create a new push button control element.
|
||||
---@param args push_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function push_button(args)
|
||||
---@return PushButton element, element_id id
|
||||
return function (args)
|
||||
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")
|
||||
@@ -38,29 +38,40 @@ local function push_button(args)
|
||||
|
||||
-- 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)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
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
|
||||
-- provide a constraint condition to element creation to prefer a single line button
|
||||
---@param frame graphics_frame
|
||||
local function constrain(frame)
|
||||
return frame.w, math.max(1, #util.strwrap(args.text, frame.w))
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args --[[@as graphics_args]], constrain)
|
||||
|
||||
local text_lines = util.strwrap(args.text, e.frame.w)
|
||||
|
||||
-- draw the button
|
||||
function e.redraw()
|
||||
e.window.clear()
|
||||
|
||||
-- write the button text
|
||||
e.w_set_cur(h_pad, v_pad)
|
||||
e.w_write(args.text)
|
||||
for i = 1, #text_lines do
|
||||
if i > e.frame.h then break end
|
||||
|
||||
local len = string.len(text_lines[i])
|
||||
|
||||
-- use cursor position to align this line
|
||||
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.w_set_cur(1, i)
|
||||
end
|
||||
|
||||
e.w_write(text_lines[i])
|
||||
end
|
||||
end
|
||||
|
||||
-- draw the button as pressed (if active_fg_bg set)
|
||||
@@ -109,7 +120,9 @@ local function push_button(args)
|
||||
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()
|
||||
-- visualize click without unfocusing
|
||||
show_unpressed()
|
||||
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_pressed) end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -144,10 +157,8 @@ local function push_button(args)
|
||||
e.on_focused = show_pressed
|
||||
e.on_unfocused = show_unpressed
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class PushButton:graphics_element
|
||||
local PushButton, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return PushButton, id
|
||||
end
|
||||
|
||||
return push_button
|
||||
@@ -23,10 +23,10 @@ local element = require("graphics.element")
|
||||
---@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)
|
||||
-- Create a new 2-dimensional (rows and columns of options) radio button list control element (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)
|
||||
---@return Radio2D element, element_id id
|
||||
return function (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")
|
||||
@@ -70,7 +70,7 @@ local function radio_2d_button(args)
|
||||
args.height = max_rows
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- selected option (convert nil to 1 if missing)
|
||||
e.value = args.default or 1
|
||||
@@ -194,10 +194,8 @@ local function radio_2d_button(args)
|
||||
e.on_enabled = e.redraw
|
||||
e.on_disabled = e.redraw
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class Radio2D:graphics_element
|
||||
local Radio2D, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return Radio2D, id
|
||||
end
|
||||
|
||||
return radio_2d_button
|
||||
@@ -11,6 +11,7 @@ local KEY_CLICK = core.events.KEY_CLICK
|
||||
---@field options table button options
|
||||
---@field radio_colors cpair radio button colors (inner & outer)
|
||||
---@field select_color color color for radio button border when selected
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
---@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
|
||||
@@ -21,10 +22,10 @@ local KEY_CLICK = core.events.KEY_CLICK
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new radio button list (latch selection, exclusively one button at a time)
|
||||
-- Create a new radio button list control element (latch selection, exclusively one button at a time).
|
||||
---@param args radio_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function radio_button(args)
|
||||
---@return RadioButton element, element_id id
|
||||
return function (args)
|
||||
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")
|
||||
@@ -49,7 +50,7 @@ local function radio_button(args)
|
||||
args.height = #args.options -- one line per option
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
local focused_opt = 1
|
||||
|
||||
@@ -64,6 +65,10 @@ local function radio_button(args)
|
||||
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 and args.dis_fg_bg and not e.enabled then
|
||||
outer_color = args.radio_colors.color_a
|
||||
end
|
||||
|
||||
e.w_set_cur(1, i)
|
||||
|
||||
e.w_set_fgd(inner_color)
|
||||
@@ -75,9 +80,14 @@ local function radio_button(args)
|
||||
e.w_write("\x95")
|
||||
|
||||
-- write button text
|
||||
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)
|
||||
if args.dis_fg_bg and not e.enabled then
|
||||
e.w_set_fgd(args.dis_fg_bg.fgd)
|
||||
e.w_set_bkg(args.dis_fg_bg.bkg)
|
||||
elseif i == focused_opt and e.is_focused() then
|
||||
if e.enabled then
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.fgd)
|
||||
end
|
||||
else
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
@@ -139,10 +149,8 @@ local function radio_button(args)
|
||||
e.on_enabled = e.redraw
|
||||
e.on_disabled = e.redraw
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class RadioButton:graphics_element
|
||||
local RadioButton, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return RadioButton, id
|
||||
end
|
||||
|
||||
return radio_button
|
||||
@@ -17,14 +17,14 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new sidebar tab selector
|
||||
-- Create a new sidebar tab selector control element.
|
||||
---@param args sidebar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function sidebar(args)
|
||||
---@return Sidebar element, element_id id
|
||||
return function (args)
|
||||
args.width = 3
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- default to 1st tab
|
||||
e.value = 1
|
||||
@@ -129,8 +129,14 @@ local function sidebar(args)
|
||||
end
|
||||
|
||||
-- update the sidebar navigation options
|
||||
---@param items table sidebar entries
|
||||
---@param items sidebar_entry[] sidebar entries
|
||||
function e.on_update(items)
|
||||
---@class sidebar_entry
|
||||
---@field label string
|
||||
---@field tall boolean
|
||||
---@field color cpair
|
||||
---@field callback function|nil
|
||||
|
||||
local next_y = 1
|
||||
|
||||
tabs = {}
|
||||
@@ -160,9 +166,8 @@ local function sidebar(args)
|
||||
-- element redraw
|
||||
e.redraw = draw
|
||||
|
||||
e.redraw()
|
||||
---@class Sidebar:graphics_element
|
||||
local Sidebar, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return Sidebar, id
|
||||
end
|
||||
|
||||
return sidebar
|
||||
@@ -17,10 +17,10 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new switch button (latch high/low)
|
||||
-- Create a new latching switch button control element.
|
||||
---@param args switch_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function switch_button(args)
|
||||
---@return SwitchButton element, element_id id
|
||||
return function (args)
|
||||
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")
|
||||
@@ -33,7 +33,7 @@ local function switch_button(args)
|
||||
args.width = math.max(text_width, args.min_width)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = args.default or false
|
||||
|
||||
@@ -72,10 +72,8 @@ local function switch_button(args)
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class SwitchButton:graphics_element
|
||||
local SwitchButton, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return SwitchButton, id
|
||||
end
|
||||
|
||||
return switch_button
|
||||
@@ -23,10 +23,10 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new tab selector
|
||||
-- Create a new tab selector control element.
|
||||
---@param args tabbar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function tabbar(args)
|
||||
---@return TabBar element, element_id id
|
||||
return function (args)
|
||||
element.assert(type(args.tabs) == "table", "tabs is a required field")
|
||||
element.assert(#args.tabs > 0, "at least one tab is required")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
@@ -46,7 +46,7 @@ local function tabbar(args)
|
||||
local button_width = math.max(max_width, args.min_width or 0)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
element.assert(e.frame.w >= (button_width * #args.tabs), "width insufficent to display all tabs")
|
||||
|
||||
@@ -120,10 +120,8 @@ local function tabbar(args)
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class TabBar:graphics_element
|
||||
local TabBar, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return TabBar, id
|
||||
end
|
||||
|
||||
return tabbar
|
||||
@@ -17,6 +17,7 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
---@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 align_right? boolean true to align right while unfocused
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
@@ -26,10 +27,10 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new numeric entry field
|
||||
-- Create a new numeric entry field.
|
||||
---@param args number_field_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function number_field(args)
|
||||
---@return NumberField element, element_id id
|
||||
return function (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")
|
||||
|
||||
@@ -37,17 +38,56 @@ local function number_field(args)
|
||||
args.can_focus = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
local has_decimal = false
|
||||
|
||||
args.max_chars = args.max_chars or e.frame.w
|
||||
|
||||
-- determine the format to convert the number to a string
|
||||
local format = "%d"
|
||||
if args.allow_decimal then
|
||||
if args.max_frac_digits then
|
||||
format = "%."..args.max_frac_digits.."f"
|
||||
else format = "%f" end
|
||||
end
|
||||
|
||||
-- set the value to a formatted numeric string<br>
|
||||
-- trims trailing zeros from floating point numbers
|
||||
---@param num number
|
||||
local function _set_value(num)
|
||||
local str = util.sprintf(format, num)
|
||||
|
||||
if args.allow_decimal then
|
||||
local found_nonzero = false
|
||||
local str_table = {}
|
||||
|
||||
for i = #str, 1, -1 do
|
||||
local c = string.sub(str, i, i)
|
||||
|
||||
if found_nonzero then
|
||||
str_table[i] = c
|
||||
else
|
||||
if c == "." then
|
||||
found_nonzero = true
|
||||
elseif c ~= "0" then
|
||||
str_table[i] = c
|
||||
found_nonzero = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
e.value = table.concat(str_table)
|
||||
else
|
||||
e.value = str
|
||||
end
|
||||
end
|
||||
|
||||
-- set initial value
|
||||
e.value = "" .. (args.default or 0)
|
||||
_set_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)
|
||||
local ifield = core.new_ifield(e, args.max_chars, args.fg_bg, args.dis_fg_bg, args.align_right)
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
@@ -55,10 +95,16 @@ local function number_field(args)
|
||||
-- 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
|
||||
local x = event.current.x
|
||||
|
||||
if not e.is_focused() then
|
||||
x = ifield.get_cursor_align_shift(x)
|
||||
end
|
||||
|
||||
e.take_focus()
|
||||
|
||||
if event.type == MOUSE_CLICK.UP then
|
||||
ifield.move_cursor(event.current.x)
|
||||
ifield.move_cursor(x)
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
|
||||
ifield.select_all()
|
||||
@@ -100,7 +146,17 @@ local function number_field(args)
|
||||
-- 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
|
||||
local num, max, min = tonumber(val), tonumber(args.max), tonumber(args.min)
|
||||
|
||||
if max and num > max then
|
||||
_set_value(max)
|
||||
elseif min and num < min then
|
||||
_set_value(min)
|
||||
elseif num then
|
||||
_set_value(num)
|
||||
end
|
||||
|
||||
ifield.set_value(e.value)
|
||||
end
|
||||
|
||||
-- set minimum input value
|
||||
@@ -129,11 +185,9 @@ local function number_field(args)
|
||||
|
||||
-- handle unfocused
|
||||
function e.on_unfocused()
|
||||
local val = tonumber(e.value)
|
||||
local max = tonumber(args.max)
|
||||
local min = tonumber(args.min)
|
||||
local val, max, min = tonumber(e.value), tonumber(args.max), tonumber(args.min)
|
||||
|
||||
if type(val) == "number" then
|
||||
if val then
|
||||
if args.max_int_digits or args.max_frac_digits then
|
||||
local str = e.value
|
||||
local ceil = false
|
||||
@@ -162,17 +216,17 @@ local function number_field(args)
|
||||
|
||||
if parts[2] then parts[2] = "." .. parts[2] else parts[2] = "" end
|
||||
|
||||
val = tonumber((parts[1] or "") .. parts[2])
|
||||
val = tonumber((parts[1] or "") .. parts[2]) or 0
|
||||
end
|
||||
|
||||
if type(args.max) == "number" and val > max then
|
||||
e.value = "" .. max
|
||||
if max and val > max then
|
||||
_set_value(max)
|
||||
ifield.nav_start()
|
||||
elseif type(args.min) == "number" and val < min then
|
||||
e.value = "" .. min
|
||||
elseif min and val < min then
|
||||
_set_value(min)
|
||||
ifield.nav_start()
|
||||
else
|
||||
e.value = "" .. val
|
||||
_set_value(val)
|
||||
ifield.nav_end()
|
||||
end
|
||||
else
|
||||
@@ -188,10 +242,14 @@ local function number_field(args)
|
||||
e.on_disabled = ifield.show
|
||||
e.redraw = ifield.show
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class NumberField:graphics_element
|
||||
local NumberField, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
-- get the numeric value of this field
|
||||
---@return number value the value, or 0 if not a valid number
|
||||
function NumberField.get_numeric()
|
||||
return tonumber(e.value) or 0
|
||||
end
|
||||
|
||||
return NumberField, id
|
||||
end
|
||||
|
||||
return number_field
|
||||
@@ -19,15 +19,15 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new text entry field
|
||||
-- Create a new text entry field.
|
||||
---@param args text_field_args
|
||||
---@return graphics_element element, element_id id, function censor_ctl
|
||||
local function text_field(args)
|
||||
---@return TextField element, element_id id
|
||||
return function (args)
|
||||
args.height = 1
|
||||
args.can_focus = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- set initial value
|
||||
e.value = args.value or ""
|
||||
@@ -95,11 +95,10 @@ local function text_field(args)
|
||||
e.on_disabled = ifield.show
|
||||
e.redraw = ifield.show
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class TextField:graphics_element
|
||||
local TextField, id = e.complete(true)
|
||||
|
||||
local elem, id = e.complete()
|
||||
return elem, id, ifield.censor
|
||||
TextField.censor = ifield.censor
|
||||
|
||||
return TextField, id
|
||||
end
|
||||
|
||||
return text_field
|
||||
@@ -20,11 +20,11 @@ local flasher = require("graphics.flasher")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new alarm indicator light
|
||||
-- Create a new alarm indicator light element.
|
||||
---@nodiscard
|
||||
---@param args alarm_indicator_light
|
||||
---@return graphics_element element, element_id id
|
||||
local function alarm_indicator_light(args)
|
||||
---@return AlarmLight element, element_id id
|
||||
return function (args)
|
||||
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")
|
||||
@@ -49,7 +49,7 @@ local function alarm_indicator_light(args)
|
||||
local c3 = colors.toBlit(args.c3)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = 1
|
||||
|
||||
@@ -113,10 +113,8 @@ local function alarm_indicator_light(args)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class AlarmLight:graphics_element
|
||||
local AlarmLight, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return AlarmLight, id
|
||||
end
|
||||
|
||||
return alarm_indicator_light
|
||||
@@ -13,11 +13,11 @@ local element = require("graphics.element")
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
|
||||
-- new core map box
|
||||
-- Create a new core map diagram indicator element.
|
||||
---@nodiscard
|
||||
---@param args core_map_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function core_map(args)
|
||||
---@return CoreMap element, element_id id
|
||||
return function (args)
|
||||
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")
|
||||
|
||||
@@ -29,7 +29,7 @@ local function core_map(args)
|
||||
args.fg_bg = core.cpair(args.parent.get_fg_bg().fgd, colors.gray)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = 0
|
||||
|
||||
@@ -165,10 +165,8 @@ local function core_map(args)
|
||||
draw_core(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class CoreMap:graphics_element
|
||||
local CoreMap, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return CoreMap, id
|
||||
end
|
||||
|
||||
return core_map
|
||||
@@ -19,11 +19,11 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new data indicator
|
||||
-- Create new data indicator element.
|
||||
---@nodiscard
|
||||
---@param args data_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function data(args)
|
||||
---@return DataIndicator element, element_id id
|
||||
return function (args)
|
||||
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")
|
||||
@@ -32,7 +32,7 @@ local function data(args)
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = args.value
|
||||
|
||||
@@ -94,10 +94,8 @@ local function data(args)
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class DataIndicator:graphics_element
|
||||
local DataIndicator, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return DataIndicator, id
|
||||
end
|
||||
|
||||
return data
|
||||
@@ -17,13 +17,13 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new horizontal bar
|
||||
-- Create a new horizontal fill bar indicator element.
|
||||
---@nodiscard
|
||||
---@param args hbar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function hbar(args)
|
||||
return function (args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = 0.0
|
||||
|
||||
@@ -119,10 +119,8 @@ local function hbar(args)
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class HorizontalBar:graphics_element
|
||||
local HorizontalBar, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return HorizontalBar, id
|
||||
end
|
||||
|
||||
return hbar
|
||||
@@ -18,11 +18,11 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new icon indicator
|
||||
-- Create a new icon indicator element.
|
||||
---@nodiscard
|
||||
---@param args icon_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function icon(args)
|
||||
---@return IconIndicator element, element_id id
|
||||
return function (args)
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.states) == "table", "states is a required field")
|
||||
|
||||
@@ -30,7 +30,7 @@ local function icon(args)
|
||||
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)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = args.value or 1
|
||||
if e.value == true then e.value = 2 end
|
||||
@@ -71,10 +71,8 @@ local function icon(args)
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class IconIndicator:graphics_element
|
||||
local IconIndicator, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return IconIndicator, id
|
||||
end
|
||||
|
||||
return icon
|
||||
@@ -18,11 +18,11 @@ local flasher = require("graphics.flasher")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new indicator light
|
||||
-- Create a new indicator light element.
|
||||
---@nodiscard
|
||||
---@param args indicator_light_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_light(args)
|
||||
---@return IndicatorLight element, element_id id
|
||||
return function (args)
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.colors) == "table", "colors is a required field")
|
||||
|
||||
@@ -36,7 +36,7 @@ local function indicator_light(args)
|
||||
local flash_on = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = false
|
||||
|
||||
@@ -93,10 +93,8 @@ local function indicator_light(args)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class IndicatorLight:graphics_element
|
||||
local IndicatorLight, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return IndicatorLight, id
|
||||
end
|
||||
|
||||
return indicator_light
|
||||
@@ -18,11 +18,11 @@ local flasher = require("graphics.flasher")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new indicator LED
|
||||
-- Create a new indicator LED element.
|
||||
---@nodiscard
|
||||
---@param args indicator_led_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led(args)
|
||||
---@return LED element, element_id id
|
||||
return function (args)
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.colors) == "table", "colors is a required field")
|
||||
|
||||
@@ -36,7 +36,7 @@ local function indicator_led(args)
|
||||
local flash_on = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = false
|
||||
|
||||
@@ -95,10 +95,8 @@ local function indicator_led(args)
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class LED:graphics_element
|
||||
local LED, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return LED, id
|
||||
end
|
||||
|
||||
return indicator_led
|
||||
@@ -20,11 +20,12 @@ local flasher = require("graphics.flasher")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new dual LED indicator light
|
||||
-- Create a new three-state LED indicator light. Two "active" states (colors c1 and c2) and an inactive state (off).<br>
|
||||
-- Values: 1 = off, 2 = c1, 3 = c2
|
||||
---@nodiscard
|
||||
---@param args indicator_led_pair_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led_pair(args)
|
||||
---@return LEDPair element, element_id id
|
||||
return function (args)
|
||||
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")
|
||||
@@ -44,7 +45,7 @@ local function indicator_led_pair(args)
|
||||
local c2 = colors.toBlit(args.c2)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = 1
|
||||
|
||||
@@ -104,10 +105,8 @@ local function indicator_led_pair(args)
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class LEDPair:graphics_element
|
||||
local LEDPair, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return LEDPair, id
|
||||
end
|
||||
|
||||
return indicator_led_pair
|
||||
@@ -6,6 +6,7 @@ local element = require("graphics.element")
|
||||
|
||||
---@class power_indicator_args
|
||||
---@field label string indicator label
|
||||
---@field unit string energy unit
|
||||
---@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)
|
||||
@@ -18,18 +19,20 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new power indicator
|
||||
-- Create a new power indicator. Variant of a data indicator with dynamic energy units.
|
||||
---@nodiscard
|
||||
---@param args power_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function power(args)
|
||||
---@return PowerIndicator element, element_id id
|
||||
return function (args)
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.unit) == "string", "unit 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")
|
||||
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = args.value
|
||||
|
||||
@@ -40,7 +43,7 @@ local function power(args)
|
||||
function e.on_update(value)
|
||||
e.value = value
|
||||
|
||||
local data_str, unit = util.power_format(value, false, args.format)
|
||||
local data_str, unit = util.power_format(value, args.unit, false, args.format)
|
||||
|
||||
-- write data
|
||||
e.w_set_cur(data_start, 1)
|
||||
@@ -53,14 +56,13 @@ local function power(args)
|
||||
end
|
||||
|
||||
-- append per tick if rate is set
|
||||
-- add space to FE so we don't end up with FEE (after having kFE for example)
|
||||
if args.rate == true then
|
||||
unit = unit .. "/t"
|
||||
if unit == "FE/t" then unit = "FE/t " end
|
||||
else
|
||||
if unit == "FE" then unit = "FE " end
|
||||
end
|
||||
|
||||
-- add space to unit so we don't end up with something like FEE after having kFE
|
||||
unit = util.strminw(unit, 5)
|
||||
|
||||
e.w_write(" " .. unit)
|
||||
end
|
||||
|
||||
@@ -80,10 +82,8 @@ local function power(args)
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class PowerIndicator:graphics_element
|
||||
local PowerIndicator, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return PowerIndicator, id
|
||||
end
|
||||
|
||||
return power
|
||||
@@ -13,11 +13,11 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new RGB LED indicator light
|
||||
-- Create a new RGB LED indicator light element.
|
||||
---@nodiscard
|
||||
---@param args indicator_led_rgb_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led_rgb(args)
|
||||
---@return RGBLED element, element_id id
|
||||
return function (args)
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.colors) == "table", "colors is a required field")
|
||||
|
||||
@@ -25,7 +25,7 @@ local function indicator_led_rgb(args)
|
||||
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)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = 1
|
||||
|
||||
@@ -52,10 +52,8 @@ local function indicator_led_rgb(args)
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class RGBLED:graphics_element
|
||||
local RGBLED, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return RGBLED, id
|
||||
end
|
||||
|
||||
return indicator_led_rgb
|
||||
@@ -19,11 +19,11 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new radiation indicator
|
||||
-- Create a new radiation indicator element. Variant of a data indicator using dynamic Sievert unit precision.
|
||||
---@nodiscard
|
||||
---@param args rad_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function rad(args)
|
||||
---@return RadIndicator element, element_id id
|
||||
return function (args)
|
||||
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")
|
||||
@@ -31,7 +31,7 @@ local function rad(args)
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = args.value or types.new_zero_radiation_reading()
|
||||
|
||||
@@ -83,10 +83,8 @@ local function rad(args)
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class RadIndicator:graphics_element
|
||||
local RadIndicator, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return RadIndicator, id
|
||||
end
|
||||
|
||||
return rad
|
||||
@@ -15,16 +15,16 @@ local element = require("graphics.element")
|
||||
---@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
|
||||
-- Create a new signal bar indicator element.
|
||||
---@nodiscard
|
||||
---@param args signal_bar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function signal_bar(args)
|
||||
---@return SignalBar element, element_id id
|
||||
return function (args)
|
||||
args.height = 1
|
||||
args.width = util.trinary(args.compact, 1, 2)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = 0
|
||||
|
||||
@@ -76,10 +76,8 @@ local function signal_bar(args)
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class SignalBar:graphics_element
|
||||
local SignalBar, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return SignalBar, id
|
||||
end
|
||||
|
||||
return signal_bar
|
||||
@@ -20,11 +20,11 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new state indicator
|
||||
-- Create a new state indicator element.
|
||||
---@nodiscard
|
||||
---@param args state_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function state_indicator(args)
|
||||
---@return StateIndicator element, element_id id
|
||||
return function (args)
|
||||
element.assert(type(args.states) == "table", "states is a required field")
|
||||
|
||||
if util.is_int(args.height) then
|
||||
@@ -52,7 +52,7 @@ local function state_indicator(args)
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = args.value or 1
|
||||
|
||||
@@ -74,10 +74,8 @@ local function state_indicator(args)
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class StateIndicator:graphics_element
|
||||
local StateIndicator, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return StateIndicator, id
|
||||
end
|
||||
|
||||
return state_indicator
|
||||
@@ -20,11 +20,11 @@ local flasher = require("graphics.flasher")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new tri-state indicator light
|
||||
-- Create a new tri-state indicator light element.
|
||||
---@nodiscard
|
||||
---@param args tristate_indicator_light_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function tristate_indicator_light(args)
|
||||
---@return TriIndicatorLight element, element_id id
|
||||
return function (args)
|
||||
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")
|
||||
@@ -38,7 +38,7 @@ local function tristate_indicator_light(args)
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = 1
|
||||
|
||||
@@ -102,10 +102,8 @@ local function tristate_indicator_light(args)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class TriIndicatorLight:graphics_element
|
||||
local TriIndicatorLight, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return TriIndicatorLight, id
|
||||
end
|
||||
|
||||
return tristate_indicator_light
|
||||
@@ -15,13 +15,13 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new vertical bar
|
||||
-- Create a new vertical fill bar indicator element.
|
||||
---@nodiscard
|
||||
---@param args vbar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function vbar(args)
|
||||
---@return VerticalBar element, element_id id
|
||||
return function (args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = 0.0
|
||||
|
||||
@@ -98,10 +98,8 @@ local function vbar(args)
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
---@class VerticalBar:graphics_element
|
||||
local VerticalBar, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return VerticalBar, id
|
||||
end
|
||||
|
||||
return vbar
|
||||
@@ -18,7 +18,7 @@ local PERIOD = {
|
||||
flasher.PERIOD = PERIOD
|
||||
|
||||
local active = false
|
||||
local registry = { {}, {}, {} } -- one registry table per period
|
||||
local registry = { {}, {}, {} } ---@type [ function[], function[], function [] ] one registry table per period
|
||||
local callback_counter = 0
|
||||
|
||||
-- blink registered indicators<br>
|
||||
|
||||
432
pocket/config/system.lua
Normal file
432
pocket/config/system.lua
Normal file
@@ -0,0 +1,432 @@
|
||||
local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
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 Checkbox = require("graphics.elements.controls.Checkbox")
|
||||
local PushButton = require("graphics.elements.controls.PushButton")
|
||||
local RadioButton = require("graphics.elements.controls.RadioButton")
|
||||
|
||||
local NumberField = require("graphics.elements.form.NumberField")
|
||||
local TextField = require("graphics.elements.form.TextField")
|
||||
|
||||
local tri = util.trinary
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local RIGHT = core.ALIGN.RIGHT
|
||||
|
||||
local self = {
|
||||
importing_legacy = false,
|
||||
|
||||
show_auth_key = nil, ---@type function
|
||||
show_key_btn = nil, ---@type PushButton
|
||||
auth_key_textbox = nil, ---@type TextBox
|
||||
auth_key_value = ""
|
||||
}
|
||||
|
||||
local system = {}
|
||||
|
||||
-- create the system configuration view
|
||||
---@param tool_ctl _pkt_cfg_tool_ctl
|
||||
---@param main_pane MultiPane
|
||||
---@param cfg_sys [ pkt_config, pkt_config, pkt_config, { [1]: string, [2]: string, [3]: any }[], function ]
|
||||
---@param divs Div[]
|
||||
---@param style { [string]: cpair }
|
||||
---@param exit function
|
||||
function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
local settings_cfg, ini_cfg, tmp_cfg, fields, load_settings = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5]
|
||||
local ui_cfg, net_cfg, log_cfg, summary = divs[1], divs[2], divs[3], divs[4]
|
||||
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local g_lg_fg_bg = style.g_lg_fg_bg
|
||||
local nav_fg_bg = style.nav_fg_bg
|
||||
local btn_act_fg_bg = style.btn_act_fg_bg
|
||||
local btn_dis_fg_bg = style.btn_dis_fg_bg
|
||||
|
||||
--#region Pocket UI
|
||||
|
||||
local ui_c_1 = Div{parent=ui_cfg,x=2,y=4,width=24}
|
||||
local ui_c_2 = Div{parent=ui_cfg,x=2,y=4,width=24}
|
||||
|
||||
local ui_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={ui_c_1,ui_c_2}}
|
||||
|
||||
TextBox{parent=ui_cfg,x=1,y=2,text=" Pocket UI",fg_bg=cpair(colors.black,colors.lime)}
|
||||
|
||||
TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize UI options below."}
|
||||
|
||||
TextBox{parent=ui_c_1,y=4,text="Po/Pu Pellet Color"}
|
||||
TextBox{parent=ui_c_1,x=20,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
|
||||
local pellet_color = RadioButton{parent=ui_c_1,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
|
||||
TextBox{parent=ui_c_1,y=8,height=4,text="In Mekanism 10.4 and later, pellet colors now match gas colors (Cyan Pu/Green Po).",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local function submit_ui_opts()
|
||||
tmp_cfg.GreenPuPellet = pellet_color.get_value() == 1
|
||||
ui_pane.set_value(2)
|
||||
end
|
||||
|
||||
PushButton{parent=ui_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=ui_c_1,x=19,y=15,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=ui_c_2,x=1,y=1,height=3,text="You may customize units below."}
|
||||
|
||||
TextBox{parent=ui_c_2,x=1,y=4,text="Temperature Scale"}
|
||||
local temp_scale = RadioButton{parent=ui_c_2,x=1,y=5,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
|
||||
TextBox{parent=ui_c_2,x=1,y=10,text="Energy Scale"}
|
||||
local energy_scale = RadioButton{parent=ui_c_2,x=1,y=11,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
|
||||
local function submit_ui_units()
|
||||
tmp_cfg.TempScale = temp_scale.get_value()
|
||||
tmp_cfg.EnergyScale = energy_scale.get_value()
|
||||
main_pane.set_value(3)
|
||||
end
|
||||
|
||||
PushButton{parent=ui_c_2,x=1,y=15,text="\x1b Back",callback=function()ui_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=ui_c_2,x=19,y=15,text="Next \x1a",callback=submit_ui_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#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,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=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,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,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,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,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(2)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,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,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,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,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,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,text="Facility Auth Key"}
|
||||
local key, _ = 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) key.censor(tri(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,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(4)
|
||||
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,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=1,text="Configure logging below."}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=3,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,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,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
|
||||
self.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
main_pane.set_value(5)
|
||||
else path_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=log_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(3)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,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 self.importing_legacy then
|
||||
main_pane.set_value(1)
|
||||
tool_ctl.viewing_config = false
|
||||
self.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
else
|
||||
main_pane.set_value(4)
|
||||
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 _, field in ipairs(fields) do
|
||||
local k, v = field[1], tmp_cfg[field[1]]
|
||||
if v == nil then settings.unset(k) else settings.set(k, v) end
|
||||
end
|
||||
|
||||
if settings.save("/pocket.settings") then
|
||||
load_settings(settings_cfg, true)
|
||||
load_settings(ini_cfg)
|
||||
|
||||
try_set(pellet_color, ini_cfg.GreenPuPellet)
|
||||
try_set(temp_scale, ini_cfg.TempScale)
|
||||
try_set(energy_scale, ini_cfg.EnergyScale)
|
||||
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 self.importing_legacy then
|
||||
self.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}
|
||||
self.show_key_btn = PushButton{parent=sum_c_1,x=1,y=13,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_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,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
|
||||
|
||||
--#region Tool Functions
|
||||
|
||||
-- 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(5)
|
||||
self.importing_legacy = true
|
||||
end
|
||||
|
||||
-- expose the auth key on the summary page
|
||||
function self.show_auth_key()
|
||||
self.show_key_btn.disable()
|
||||
self.auth_key_textbox.set_value(self.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
|
||||
|
||||
self.show_key_btn.enable()
|
||||
self.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 = tri(raw == log.MODE.APPEND, "append", "replace")
|
||||
elseif f[1] == "GreenPuPellet" then
|
||||
val = tri(raw, "Green Pu/Cyan Po", "Cyan Pu/Green Po")
|
||||
elseif f[1] == "TempScale" then
|
||||
val = util.strval(types.TEMP_SCALE_NAMES[raw])
|
||||
elseif f[1] == "EnergyScale" then
|
||||
val = util.strval(types.ENERGY_SCALE_NAMES[raw])
|
||||
end
|
||||
|
||||
if val == "nil" then val = "<not set>" end
|
||||
|
||||
local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
|
||||
alternate = not alternate
|
||||
|
||||
if (string.len(val) > val_max_w) or string.find(val, "\n") 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}
|
||||
else
|
||||
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||
end
|
||||
|
||||
if f[1] == "AuthKey" then self.auth_key_textbox = textbox end
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
end
|
||||
|
||||
return system
|
||||
@@ -3,83 +3,78 @@
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local system = require("pocket.config.system")
|
||||
|
||||
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 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 PushButton = require("graphics.elements.controls.PushButton")
|
||||
|
||||
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 = {}
|
||||
local changes = {
|
||||
{ "v0.9.2", { "Added temperature scale options" } },
|
||||
{ "v0.11.3", { "Added energy scale options" } },
|
||||
{ "v0.13.2", { "Added option for Po/Pu pellet green/cyan pairing" } }
|
||||
}
|
||||
|
||||
---@class pkt_configurator
|
||||
local configurator = {}
|
||||
|
||||
local style = {}
|
||||
|
||||
style.root = cpair(colors.black, colors.lightGray)
|
||||
style.header = cpair(colors.white, colors.gray)
|
||||
style.root = cpair(colors.black, colors.lightGray)
|
||||
style.header = cpair(colors.white, colors.gray)
|
||||
|
||||
style.colors = themes.smooth_stone.colors
|
||||
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)
|
||||
style.bw_fg_bg = cpair(colors.black, colors.white)
|
||||
style.g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
|
||||
style.nav_fg_bg = style.bw_fg_bg
|
||||
style.btn_act_fg_bg = cpair(colors.white, colors.gray)
|
||||
style.btn_dis_fg_bg = cpair(colors.lightGray, colors.white)
|
||||
|
||||
---@class _pkt_cfg_tool_ctl
|
||||
local tool_ctl = {
|
||||
launch_startup = false,
|
||||
ask_config = false,
|
||||
has_config = false,
|
||||
viewing_config = false,
|
||||
importing_legacy = false,
|
||||
|
||||
view_cfg = nil, ---@type graphics_element
|
||||
settings_apply = nil, ---@type graphics_element
|
||||
view_cfg = nil, ---@type PushButton
|
||||
settings_apply = nil, ---@type PushButton
|
||||
|
||||
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 = ""
|
||||
load_legacy = nil ---@type function
|
||||
}
|
||||
|
||||
---@class pkt_config
|
||||
local tmp_cfg = {
|
||||
GreenPuPellet = false,
|
||||
TempScale = 1, ---@type TEMP_SCALE
|
||||
EnergyScale = 1, ---@type ENERGY_SCALE
|
||||
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,
|
||||
LogMode = 0, ---@type LOG_MODE
|
||||
LogPath = "",
|
||||
LogDebug = false,
|
||||
}
|
||||
@@ -91,6 +86,9 @@ local settings_cfg = {}
|
||||
|
||||
-- all settings fields, their nice names, and their default values
|
||||
local fields = {
|
||||
{ "GreenPuPellet", "Pellet Colors", false },
|
||||
{ "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN },
|
||||
{ "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE },
|
||||
{ "SVR_Channel", "SVR Channel", 16240 },
|
||||
{ "CRD_Channel", "CRD Channel", 16243 },
|
||||
{ "PKT_Channel", "PKT Channel", 16244 },
|
||||
@@ -116,24 +114,31 @@ local function load_settings(target, raw)
|
||||
end
|
||||
|
||||
-- create the config view
|
||||
---@param display graphics_element
|
||||
---@param display DisplayBox
|
||||
local function config_view(display)
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local g_lg_fg_bg = style.g_lg_fg_bg
|
||||
local nav_fg_bg = style.nav_fg_bg
|
||||
local btn_act_fg_bg = style.btn_act_fg_bg
|
||||
local btn_dis_fg_bg = style.btn_dis_fg_bg
|
||||
|
||||
---@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}
|
||||
TextBox{parent=display,y=1,text="Pocket Configurator",alignment=CENTER,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 ui_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local 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}}
|
||||
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,ui_cfg,net_cfg,log_cfg,summary,changelog}}
|
||||
|
||||
-- Main Page
|
||||
--#region Main Page
|
||||
|
||||
local y_start = 7
|
||||
|
||||
@@ -148,7 +153,7 @@ local function config_view(display)
|
||||
tool_ctl.viewing_config = true
|
||||
tool_ctl.gen_summary(settings_cfg)
|
||||
tool_ctl.settings_apply.hide(true)
|
||||
main_pane.set_value(4)
|
||||
main_pane.set_value(5)
|
||||
end
|
||||
|
||||
if fs.exists("/pocket/config.lua") then
|
||||
@@ -157,348 +162,52 @@ local function config_view(display)
|
||||
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}
|
||||
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=btn_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")
|
||||
local function startup()
|
||||
tool_ctl.launch_startup = true
|
||||
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)}
|
||||
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}
|
||||
local start_btn = PushButton{parent=main_page,x=17,y=18,min_width=9,text="Startup",callback=startup,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
PushButton{parent=main_page,x=2,y=y_start+4,min_width=12,text="Change Log",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
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)}
|
||||
if tool_ctl.ask_config then start_btn.disable() end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- Config Change Log
|
||||
--#region System Configuration
|
||||
|
||||
local settings = { settings_cfg, ini_cfg, tmp_cfg, fields, load_settings }
|
||||
local divs = { ui_cfg, net_cfg, log_cfg, summary }
|
||||
|
||||
system.create(tool_ctl, main_pane, settings, divs, style, exit)
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region 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}
|
||||
TextBox{parent=changelog,x=1,y=2,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}
|
||||
TextBox{parent=c_log,text=change[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=1,text="- ",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
|
||||
--#endregion
|
||||
end
|
||||
|
||||
-- reset terminal screen
|
||||
@@ -532,9 +241,7 @@ function configurator.configure(ask_config)
|
||||
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
|
||||
if 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
|
||||
@@ -559,7 +266,7 @@ function configurator.configure(ask_config)
|
||||
println("configurator error: " .. error)
|
||||
end
|
||||
|
||||
return status, error
|
||||
return status, error, tool_ctl.launch_startup
|
||||
end
|
||||
|
||||
return configurator
|
||||
|
||||
@@ -2,15 +2,21 @@
|
||||
-- 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 psil = require("scada-common.psil")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iorx = require("pocket.iorx")
|
||||
local process = require("pocket.process")
|
||||
|
||||
local ALARM = types.ALARM
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
|
||||
---@todo nominal trip time is ping (0ms to 10ms usually)
|
||||
local ENERGY_SCALE = types.ENERGY_SCALE
|
||||
local ENERGY_UNITS = types.ENERGY_SCALE_UNITS
|
||||
local TEMP_SCALE = types.TEMP_SCALE
|
||||
local TEMP_UNITS = types.TEMP_SCALE_UNITS
|
||||
|
||||
local WARN_TT = 40
|
||||
local HIGH_TT = 80
|
||||
|
||||
@@ -26,200 +32,27 @@ local LINK_STATE = {
|
||||
|
||||
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()
|
||||
version = "unknown", -- pocket version
|
||||
ps = psil.create(), -- pocket PSIL
|
||||
loader_require = { sv = false, api = false }
|
||||
}
|
||||
|
||||
---@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
|
||||
local config = nil ---@type pkt_config
|
||||
local comms = nil ---@type pocket_comms
|
||||
|
||||
-- initialize facility-independent components of pocket iocontrol
|
||||
---@param comms pocket_comms
|
||||
function iocontrol.init_core(comms)
|
||||
iocontrol.alloc_nav()
|
||||
---@param pkt_comms pocket_comms
|
||||
---@param nav pocket_nav
|
||||
---@param cfg pkt_config
|
||||
function iocontrol.init_core(pkt_comms, nav, cfg)
|
||||
comms = pkt_comms
|
||||
config = cfg
|
||||
|
||||
iocontrol.rx = iorx(io)
|
||||
|
||||
io.nav = nav
|
||||
|
||||
---@class pocket_ioctl_diag
|
||||
io.diag = {}
|
||||
@@ -252,47 +85,70 @@ function iocontrol.init_core(comms)
|
||||
|
||||
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
|
||||
tone_buttons = {}, ---@type SwitchButton[]
|
||||
alarm_buttons = {} ---@type Checkbox[]
|
||||
}
|
||||
|
||||
-- computer list
|
||||
io.diag.get_comps = function () comms.diag__get_computers() end
|
||||
|
||||
-- API access
|
||||
---@class pocket_ioctl_api
|
||||
io.api = {
|
||||
get_unit = function (unit) comms.api__get_unit(unit) end
|
||||
get_fac = function () comms.api__get_facility() end,
|
||||
get_unit = function (unit) comms.api__get_unit(unit) end,
|
||||
get_ctrl = function () comms.api__get_control() end,
|
||||
get_proc = function () comms.api__get_process() end,
|
||||
get_waste = function () comms.api__get_waste() end,
|
||||
get_rad = function () comms.api__get_rad() 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)
|
||||
---@param conf facility_conf facility configuration
|
||||
function iocontrol.init_fac(conf)
|
||||
local temp_scale, energy_scale = config.TempScale, config.EnergyScale
|
||||
io.temp_label = TEMP_UNITS[temp_scale]
|
||||
io.energy_label = ENERGY_UNITS[energy_scale]
|
||||
|
||||
-- temperature unit label and conversion function (from Kelvin)
|
||||
if temp_scale == 2 then
|
||||
io.temp_label = "\xb0C"
|
||||
if temp_scale == TEMP_SCALE.CELSIUS then
|
||||
io.temp_convert = function (t) return t - 273.15 end
|
||||
elseif temp_scale == 3 then
|
||||
io.temp_label = "\xb0F"
|
||||
elseif temp_scale == TEMP_SCALE.FAHRENHEIT then
|
||||
io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end
|
||||
elseif temp_scale == 4 then
|
||||
io.temp_label = "\xb0R"
|
||||
elseif temp_scale == TEMP_SCALE.RANKINE then
|
||||
io.temp_convert = function (t) return 1.8 * t end
|
||||
else
|
||||
io.temp_label = "K"
|
||||
io.temp_convert = function (t) return t end
|
||||
end
|
||||
|
||||
-- energy unit label and conversion function (from Joules unless otherwise specified)
|
||||
if energy_scale == ENERGY_SCALE.FE or energy_scale == ENERGY_SCALE.RF then
|
||||
io.energy_convert = util.joules_to_fe_rf
|
||||
io.energy_convert_from_fe = function (t) return t end
|
||||
io.energy_convert_to_fe = function (t) return t end
|
||||
else
|
||||
io.energy_label = "J"
|
||||
io.energy_convert = function (t) return t end
|
||||
io.energy_convert_from_fe = util.fe_rf_to_joules
|
||||
io.energy_convert_to_fe = util.joules_to_fe_rf
|
||||
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,
|
||||
tank_list = conf.cooling.fac_tank_list,
|
||||
tank_conns = conf.cooling.fac_tank_conns,
|
||||
tank_fluid_types = conf.cooling.tank_fluid_types,
|
||||
all_sys_ok = false,
|
||||
rtu_count = 0,
|
||||
|
||||
status_lines = { "", "" },
|
||||
|
||||
auto_ready = false,
|
||||
auto_active = false,
|
||||
auto_ramping = false,
|
||||
@@ -301,7 +157,7 @@ function iocontrol.init_fac(conf, temp_scale)
|
||||
auto_scram = false,
|
||||
---@type ascram_status
|
||||
ascram_status = {
|
||||
matrix_dc = false,
|
||||
matrix_fault = false,
|
||||
matrix_fill = false,
|
||||
crit_alarm = false,
|
||||
radiation = false,
|
||||
@@ -311,22 +167,28 @@ function iocontrol.init_fac(conf, temp_scale)
|
||||
---@type WASTE_PRODUCT
|
||||
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
|
||||
auto_pu_fallback_active = false,
|
||||
auto_sps_disabled = false,
|
||||
waste_stats = { 0, 0, 0, 0, 0, 0 }, -- waste in, pu, po, po pellets, am, spent waste
|
||||
|
||||
radiation = types.new_zero_radiation_reading(),
|
||||
|
||||
start_ack = nil, ---@type fun(success: boolean)
|
||||
stop_ack = nil, ---@type fun(success: boolean)
|
||||
scram_ack = nil, ---@type fun(success: boolean)
|
||||
ack_alarms_ack = nil, ---@type fun(success: boolean)
|
||||
|
||||
ps = psil.create(),
|
||||
|
||||
induction_ps_tbl = {},
|
||||
induction_data_tbl = {},
|
||||
induction_ps_tbl = {}, ---@type psil[]
|
||||
induction_data_tbl = {}, ---@type imatrix_session_db[]
|
||||
|
||||
sps_ps_tbl = {},
|
||||
sps_data_tbl = {},
|
||||
sps_ps_tbl = {}, ---@type psil[]
|
||||
sps_data_tbl = {}, ---@type sps_session_db[]
|
||||
|
||||
tank_ps_tbl = {},
|
||||
tank_data_tbl = {},
|
||||
tank_ps_tbl = {}, ---@type psil[]
|
||||
tank_data_tbl = {}, ---@type dynamicv_session_db[]
|
||||
|
||||
env_d_ps = psil.create(),
|
||||
env_d_data = {}
|
||||
rad_monitors = {} ---@type { radiation: radiation_reading, raw: number }[]
|
||||
}
|
||||
|
||||
-- create induction and SPS tables (currently only 1 of each is supported)
|
||||
@@ -335,92 +197,6 @@ function iocontrol.init_fac(conf, temp_scale)
|
||||
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
|
||||
@@ -430,19 +206,23 @@ function iocontrol.init_fac(conf, temp_scale)
|
||||
end
|
||||
|
||||
-- create unit data structures
|
||||
io.units = {}
|
||||
io.units = {} ---@type pioctl_unit[]
|
||||
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,
|
||||
|
||||
status_lines = { "", "" },
|
||||
|
||||
auto_ready = false,
|
||||
auto_degraded = false,
|
||||
|
||||
control_state = false,
|
||||
burn_rate_cmd = 0.0,
|
||||
radiation = types.new_zero_radiation_reading(),
|
||||
@@ -454,25 +234,43 @@ function iocontrol.init_fac(conf, temp_scale)
|
||||
waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
|
||||
waste_product = types.WASTE_PRODUCT.PLUTONIUM,
|
||||
|
||||
-- auto control group
|
||||
a_group = 0,
|
||||
last_rate_change_ms = 0,
|
||||
turbine_flow_stable = false,
|
||||
waste_stats = { 0, 0, 0 }, -- plutonium, polonium, po pellets
|
||||
|
||||
---@type alarms
|
||||
-- auto control group
|
||||
a_group = types.AUTO_GROUP.MANUAL,
|
||||
|
||||
start = function () process.start(i) end,
|
||||
scram = function () process.scram(i) end,
|
||||
reset_rps = function () process.reset_rps(i) end,
|
||||
ack_alarms = function () process.ack_all_alarms(i) end,
|
||||
set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate
|
||||
|
||||
start_ack = nil, ---@type fun(success: boolean)
|
||||
scram_ack = nil, ---@type fun(success: boolean)
|
||||
reset_rps_ack = nil, ---@type fun(success: boolean)
|
||||
ack_alarms_ack = nil, ---@type fun(success: boolean)
|
||||
|
||||
---@type { [ALARM]: ALARM_STATE }
|
||||
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
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
annunciator = {}, ---@type annunciator
|
||||
|
||||
unit_ps = psil.create(),
|
||||
reactor_data = {}, ---@type reactor_db
|
||||
reactor_data = types.new_reactor_db(),
|
||||
|
||||
boiler_ps_tbl = {},
|
||||
boiler_data_tbl = {},
|
||||
boiler_ps_tbl = {}, ---@type psil[]
|
||||
boiler_data_tbl = {}, ---@type boilerv_session_db[]
|
||||
|
||||
turbine_ps_tbl = {},
|
||||
turbine_data_tbl = {},
|
||||
turbine_ps_tbl = {}, ---@type psil[]
|
||||
turbine_data_tbl = {}, ---@type turbinev_session_db[]
|
||||
|
||||
tank_ps_tbl = {},
|
||||
tank_data_tbl = {}
|
||||
tank_ps_tbl = {}, ---@type psil[]
|
||||
tank_data_tbl = {}, ---@type dynamicv_session_db[]
|
||||
|
||||
rad_monitors = {} ---@type { radiation: radiation_reading, raw: number }[]
|
||||
}
|
||||
|
||||
-- on other facility modes, overwrite unit TANK option with facility tank defs
|
||||
@@ -503,12 +301,15 @@ function iocontrol.init_fac(conf, temp_scale)
|
||||
|
||||
table.insert(io.units, entry)
|
||||
end
|
||||
|
||||
-- pass IO control here since it can't be require'd due to a require loop
|
||||
process.init(io, comms)
|
||||
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
|
||||
---@param sv_addr integer|false|nil supervisor address if linked, nil if unchanged, false if unlinked
|
||||
---@param api_addr integer|false|nil coordinator address if linked, nil if unchanged, false if unlinked
|
||||
function iocontrol.report_link_state(state, sv_addr, api_addr)
|
||||
io.ps.publish("link_state", state)
|
||||
|
||||
@@ -520,12 +321,25 @@ function iocontrol.report_link_state(state, sv_addr, api_addr)
|
||||
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)
|
||||
if sv_addr then
|
||||
io.ps.publish("sv_addr", util.c(sv_addr, ":", config.SVR_Channel))
|
||||
elseif sv_addr == false then
|
||||
io.ps.publish("sv_addr", "unknown (not linked)")
|
||||
end
|
||||
|
||||
if api_addr then
|
||||
io.ps.publish("api_addr", util.c(api_addr, ":", config.CRD_Channel))
|
||||
elseif api_addr == false then
|
||||
io.ps.publish("api_addr", "unknown (not linked)")
|
||||
end
|
||||
end
|
||||
|
||||
-- show the reason the supervisor connection isn't linking
|
||||
function iocontrol.report_svr_link_error(msg) io.ps.publish("svr_link_msg", msg) end
|
||||
|
||||
-- show the reason the coordinator api connection isn't linking
|
||||
function iocontrol.report_crd_link_error(msg) io.ps.publish("api_link_msg", msg) end
|
||||
|
||||
-- determine supervisor connection quality (trip time)
|
||||
---@param trip_time integer
|
||||
function iocontrol.report_svr_tt(trip_time)
|
||||
@@ -552,221 +366,6 @@ function iocontrol.report_crd_tt(trip_time)
|
||||
io.ps.publish("crd_conn_quality", state)
|
||||
end
|
||||
|
||||
-- populate facility data from API_GET_FAC
|
||||
---@param data table
|
||||
---@return boolean valid
|
||||
function iocontrol.record_facility_data(data)
|
||||
local valid = true
|
||||
|
||||
local fac = io.facility
|
||||
|
||||
fac.all_sys_ok = data[1]
|
||||
fac.rtu_count = data[2]
|
||||
fac.radiation = data[3]
|
||||
|
||||
-- auto control
|
||||
if type(data[4]) == "table" and #data[4] == 4 then
|
||||
fac.auto_ready = data[4][1]
|
||||
fac.auto_active = data[4][2]
|
||||
fac.auto_ramping = data[4][3]
|
||||
fac.auto_saturated = data[4][4]
|
||||
end
|
||||
|
||||
-- waste
|
||||
if type(data[5]) == "table" and #data[5] == 2 then
|
||||
fac.auto_current_waste_product = data[5][1]
|
||||
fac.auto_pu_fallback_active = data[5][2]
|
||||
end
|
||||
|
||||
fac.num_tanks = data[6]
|
||||
fac.has_imatrix = data[7]
|
||||
fac.has_sps = data[8]
|
||||
|
||||
return valid
|
||||
end
|
||||
|
||||
-- update unit status data from API_GET_UNIT
|
||||
---@param data table
|
||||
function iocontrol.record_unit_data(data)
|
||||
if type(data[1]) == "number" and io.units[data[1]] then
|
||||
local unit = io.units[data[1]] ---@type pioctl_unit
|
||||
|
||||
unit.connected = data[2]
|
||||
unit.rtu_hw = data[3]
|
||||
unit.alarms = data[4]
|
||||
|
||||
--#region Annunciator
|
||||
|
||||
unit.annunciator = data[5]
|
||||
|
||||
local rcs_disconn, rcs_warn, rcs_hazard = false, false, false
|
||||
|
||||
for key, val in pairs(unit.annunciator) do
|
||||
if key == "BoilerOnline" or key == "TurbineOnline" then
|
||||
-- split up online arrays
|
||||
local every = true
|
||||
for id = 1, #val do
|
||||
every = every and val[id]
|
||||
unit.boiler_ps_tbl[id].publish(key, val[id])
|
||||
end
|
||||
|
||||
if not every then rcs_disconn = true end
|
||||
|
||||
unit.unit_ps.publish("U_" .. key, every)
|
||||
elseif key == "HeatingRateLow" or key == "WaterLevelLow" then
|
||||
-- split up array for all boilers
|
||||
local any = false
|
||||
for id = 1, #val do
|
||||
any = any or val[id]
|
||||
unit.boiler_ps_tbl[id].publish(key, val[id])
|
||||
end
|
||||
|
||||
if key == "HeatingRateLow" and any then
|
||||
rcs_warn = true
|
||||
elseif key == "WaterLevelLow" and any then
|
||||
rcs_hazard = true
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_" .. key, any)
|
||||
elseif key == "SteamDumpOpen" or key == "TurbineOverSpeed" or key == "GeneratorTrip" or key == "TurbineTrip" then
|
||||
-- split up array for all turbines
|
||||
local any = false
|
||||
for id = 1, #val do
|
||||
any = any or val[id]
|
||||
unit.turbine_ps_tbl[id].publish(key, val[id])
|
||||
end
|
||||
|
||||
if key == "GeneratorTrip" and any then
|
||||
rcs_warn = true
|
||||
elseif (key == "TurbineOverSpeed" or key == "TurbineTrip") and any then
|
||||
rcs_hazard = true
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_" .. key, any)
|
||||
else
|
||||
-- non-table fields
|
||||
unit.unit_ps.publish(key, val)
|
||||
end
|
||||
end
|
||||
|
||||
local anc = unit.annunciator
|
||||
rcs_hazard = rcs_hazard or anc.RCPTrip
|
||||
rcs_warn = rcs_warn or anc.RCSFlowLow or anc.CoolantLevelLow or anc.RCSFault or anc.MaxWaterReturnFeed or
|
||||
anc.CoolantFeedMismatch or anc.BoilRateMismatch or anc.SteamFeedMismatch or anc.MaxWaterReturnFeed
|
||||
|
||||
local rcs_status = 4
|
||||
if rcs_hazard then
|
||||
rcs_status = 2
|
||||
elseif rcs_warn then
|
||||
rcs_status = 3
|
||||
elseif rcs_disconn then
|
||||
rcs_status = 1
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_RCS", rcs_status)
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Reactor Data
|
||||
|
||||
unit.reactor_data = data[6]
|
||||
|
||||
local control_status = 1
|
||||
local reactor_status = 1
|
||||
local rps_status = 1
|
||||
|
||||
if unit.connected then
|
||||
-- update RPS status
|
||||
if unit.reactor_data.rps_tripped then
|
||||
control_status = 2
|
||||
rps_status = util.trinary(unit.reactor_data.rps_trip_cause == "manual", 3, 2)
|
||||
else rps_status = 4 end
|
||||
|
||||
-- update reactor/control status
|
||||
if unit.reactor_data.mek_status.status then
|
||||
reactor_status = 4
|
||||
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
|
||||
else
|
||||
if unit.reactor_data.no_reactor then
|
||||
reactor_status = 2
|
||||
elseif not unit.reactor_data.formed or unit.reactor_data.rps_status.force_dis then
|
||||
reactor_status = 3
|
||||
else
|
||||
reactor_status = 4
|
||||
end
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.reactor_data) do
|
||||
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
|
||||
unit.unit_ps.publish(key, val)
|
||||
end
|
||||
end
|
||||
|
||||
if type(unit.reactor_data.rps_status) == "table" then
|
||||
for key, val in pairs(unit.reactor_data.rps_status) do
|
||||
unit.unit_ps.publish(key, val)
|
||||
end
|
||||
end
|
||||
|
||||
if type(unit.reactor_data.mek_status) == "table" then
|
||||
for key, val in pairs(unit.reactor_data.mek_status) do
|
||||
unit.unit_ps.publish(key, val)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_ControlStatus", control_status)
|
||||
unit.unit_ps.publish("U_ReactorStatus", reactor_status)
|
||||
unit.unit_ps.publish("U_RPS", rps_status)
|
||||
|
||||
--#endregion
|
||||
|
||||
unit.boiler_data_tbl = data[7]
|
||||
|
||||
for id = 1, #unit.boiler_data_tbl do
|
||||
local boiler = unit.boiler_data_tbl[id] ---@type boilerv_session_db
|
||||
local ps = unit.boiler_ps_tbl[id] ---@type psil
|
||||
|
||||
local boiler_status = 1
|
||||
|
||||
if unit.rtu_hw.boilers[id].connected then
|
||||
if unit.rtu_hw.boilers[id].faulted then
|
||||
boiler_status = 3
|
||||
elseif boiler.formed then
|
||||
boiler_status = 4
|
||||
else
|
||||
boiler_status = 2
|
||||
end
|
||||
end
|
||||
|
||||
ps.publish("BoilerStatus", boiler_status)
|
||||
end
|
||||
|
||||
unit.turbine_data_tbl = data[8]
|
||||
|
||||
for id = 1, #unit.turbine_data_tbl do
|
||||
local turbine = unit.turbine_data_tbl[id] ---@type turbinev_session_db
|
||||
local ps = unit.turbine_ps_tbl[id] ---@type psil
|
||||
|
||||
local turbine_status = 1
|
||||
|
||||
if unit.rtu_hw.turbines[id].connected then
|
||||
if unit.rtu_hw.turbines[id].faulted then
|
||||
turbine_status = 3
|
||||
elseif turbine.formed then
|
||||
turbine_status = 4
|
||||
else
|
||||
turbine_status = 2
|
||||
end
|
||||
end
|
||||
|
||||
ps.publish("TurbineStatus", turbine_status)
|
||||
end
|
||||
|
||||
unit.tank_data_tbl = data[9]
|
||||
end
|
||||
end
|
||||
|
||||
-- get the IO controller database
|
||||
function iocontrol.get_db() return io end
|
||||
|
||||
|
||||
955
pocket/iorx.lua
Normal file
955
pocket/iorx.lua
Normal file
@@ -0,0 +1,955 @@
|
||||
--
|
||||
-- I/O Control's Data Receive (Rx) Handlers
|
||||
--
|
||||
|
||||
local comms = require("scada-common.comms")
|
||||
local const = require("scada-common.constants")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local DEV_TYPE = comms.DEVICE_TYPE
|
||||
|
||||
local ALARM = types.ALARM
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
|
||||
local BLR_STATE = types.BOILER_STATE
|
||||
local TRB_STATE = types.TURBINE_STATE
|
||||
local TNK_STATE = types.TANK_STATE
|
||||
local MTX_STATE = types.IMATRIX_STATE
|
||||
local SPS_STATE = types.SPS_STATE
|
||||
|
||||
local io ---@type pocket_ioctl
|
||||
local iorx = {} ---@class iorx
|
||||
|
||||
-- populate facility data from API_GET_FAC
|
||||
---@param data table
|
||||
---@return boolean valid
|
||||
function iorx.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
|
||||
|
||||
local function tripped(state) return state == ALARM_STATE.TRIPPED or state == ALARM_STATE.ACKED end
|
||||
|
||||
local function _record_multiblock_status(faulted, data, ps)
|
||||
ps.publish("formed", data.formed)
|
||||
ps.publish("faulted", faulted)
|
||||
|
||||
if data.build then
|
||||
for key, val in pairs(data.build) do ps.publish(key, val) end
|
||||
end
|
||||
|
||||
for key, val in pairs(data.state) do ps.publish(key, val) end
|
||||
for key, val in pairs(data.tanks) do ps.publish(key, val) end
|
||||
end
|
||||
|
||||
-- update unit status data from API_GET_UNIT
|
||||
---@param data table
|
||||
function iorx.record_unit_data(data)
|
||||
local unit = io.units[data[1]]
|
||||
|
||||
unit.connected = data[2]
|
||||
local comp_statuses = data[3]
|
||||
unit.a_group = data[4]
|
||||
unit.alarms = data[5]
|
||||
|
||||
local next_c_stat = 1
|
||||
|
||||
unit.unit_ps.publish("auto_group_id", unit.a_group)
|
||||
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
|
||||
|
||||
--#region Annunciator
|
||||
|
||||
unit.annunciator = data[6]
|
||||
|
||||
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
|
||||
|
||||
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[7]
|
||||
|
||||
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
|
||||
|
||||
reactor_status = 4 -- ok, until proven otherwise
|
||||
|
||||
-- update reactor/control status
|
||||
if unit.reactor_data.mek_status.status then
|
||||
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
|
||||
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
|
||||
|
||||
for key, val in pairs(unit.reactor_data.rps_status) do
|
||||
unit.unit_ps.publish(key, val)
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.reactor_data.mek_struct) do
|
||||
unit.unit_ps.publish(key, val)
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.reactor_data.mek_status) do
|
||||
unit.unit_ps.publish(key, val)
|
||||
end
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_ControlStatus", control_status)
|
||||
unit.unit_ps.publish("U_ReactorStatus", reactor_status)
|
||||
unit.unit_ps.publish("U_ReactorStateStatus", comp_statuses[next_c_stat])
|
||||
unit.unit_ps.publish("U_RPS", rps_status)
|
||||
|
||||
next_c_stat = next_c_stat + 1
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region RTU Devices
|
||||
|
||||
unit.boiler_data_tbl = data[8]
|
||||
|
||||
for id = 1, #unit.boiler_data_tbl do
|
||||
local boiler = unit.boiler_data_tbl[id]
|
||||
local ps = unit.boiler_ps_tbl[id]
|
||||
local c_stat = comp_statuses[next_c_stat]
|
||||
|
||||
local boiler_status = 1
|
||||
|
||||
if c_stat ~= BLR_STATE.OFFLINE then
|
||||
if c_stat == BLR_STATE.FAULT then
|
||||
boiler_status = 3
|
||||
elseif c_stat ~= BLR_STATE.UNFORMED then
|
||||
boiler_status = 4
|
||||
else
|
||||
boiler_status = 2
|
||||
end
|
||||
|
||||
_record_multiblock_status(c_stat == BLR_STATE.FAULT, boiler, ps)
|
||||
end
|
||||
|
||||
ps.publish("BoilerStatus", boiler_status)
|
||||
ps.publish("BoilerStateStatus", c_stat)
|
||||
|
||||
next_c_stat = next_c_stat + 1
|
||||
end
|
||||
|
||||
unit.turbine_data_tbl = data[9]
|
||||
|
||||
for id = 1, #unit.turbine_data_tbl do
|
||||
local turbine = unit.turbine_data_tbl[id]
|
||||
local ps = unit.turbine_ps_tbl[id]
|
||||
local c_stat = comp_statuses[next_c_stat]
|
||||
|
||||
local turbine_status = 1
|
||||
|
||||
if c_stat ~= TRB_STATE.OFFLINE then
|
||||
if c_stat == TRB_STATE.FAULT then
|
||||
turbine_status = 3
|
||||
elseif turbine.formed then
|
||||
turbine_status = 4
|
||||
else
|
||||
turbine_status = 2
|
||||
end
|
||||
|
||||
_record_multiblock_status(c_stat == TRB_STATE.FAULT, turbine, ps)
|
||||
end
|
||||
|
||||
ps.publish("TurbineStatus", turbine_status)
|
||||
ps.publish("TurbineStateStatus", c_stat)
|
||||
|
||||
next_c_stat = next_c_stat + 1
|
||||
end
|
||||
|
||||
unit.tank_data_tbl = data[10]
|
||||
|
||||
for id = 1, #unit.tank_data_tbl do
|
||||
local tank = unit.tank_data_tbl[id]
|
||||
local ps = unit.tank_ps_tbl[id]
|
||||
local c_stat = comp_statuses[next_c_stat]
|
||||
|
||||
local tank_status = 1
|
||||
|
||||
if c_stat ~= TNK_STATE.OFFLINE then
|
||||
if c_stat == TNK_STATE.FAULT then
|
||||
tank_status = 3
|
||||
elseif tank.formed then
|
||||
tank_status = 4
|
||||
else
|
||||
tank_status = 2
|
||||
end
|
||||
|
||||
_record_multiblock_status(c_stat == TNK_STATE.FAULT, tank, ps)
|
||||
end
|
||||
|
||||
ps.publish("DynamicTankStatus", tank_status)
|
||||
ps.publish("DynamicTankStateStatus", c_stat)
|
||||
|
||||
next_c_stat = next_c_stat + 1
|
||||
end
|
||||
|
||||
unit.last_rate_change_ms = data[11]
|
||||
unit.turbine_flow_stable = data[12]
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Status Information Display
|
||||
|
||||
local ecam = {} -- aviation reference :)
|
||||
|
||||
-- local function red(text) return { text = text, color = colors.red } end
|
||||
local function white(text) return { text = text, color = colors.white } end
|
||||
local function blue(text) return { text = text, color = colors.blue } end
|
||||
|
||||
-- if unit.reactor_data.rps_status then
|
||||
-- for k, _ in pairs(unit.alarms) do
|
||||
-- unit.alarms[k] = ALARM_STATE.TRIPPED
|
||||
-- end
|
||||
-- end
|
||||
|
||||
if tripped(unit.alarms[ALARM.ContainmentBreach]) then
|
||||
local items = { white("REACTOR MELTDOWN"), blue("DON HAZMAT SUIT") }
|
||||
table.insert(ecam, { color = colors.red, text = "CONTAINMENT BREACH", help = "ContainmentBreach", items = items })
|
||||
end
|
||||
|
||||
if tripped(unit.alarms[ALARM.ContainmentRadiation]) then
|
||||
local items = {
|
||||
white("RADIATION DETECTED"),
|
||||
blue("DON HAZMAT SUIT"),
|
||||
blue("RESOLVE LEAK"),
|
||||
blue("AWAIT SAFE LEVELS")
|
||||
}
|
||||
|
||||
table.insert(ecam, { color = colors.red, text = "RADIATION LEAK", help = "ContainmentRadiation", items = items })
|
||||
end
|
||||
|
||||
if tripped(unit.alarms[ALARM.CriticalDamage]) then
|
||||
local items = { white("MELTDOWN IMMINENT"), blue("EVACUATE") }
|
||||
table.insert(ecam, { color = colors.red, text = "RCT DAMAGE CRITICAL", help = "CriticalDamage", items = items })
|
||||
end
|
||||
|
||||
if tripped(unit.alarms[ALARM.ReactorLost]) then
|
||||
local items = { white("REACTOR OFF-LINE"), blue("CHECK PLC") }
|
||||
table.insert(ecam, { color = colors.red, text = "REACTOR CONN LOST", help = "ReactorLost", items = items })
|
||||
end
|
||||
|
||||
if tripped(unit.alarms[ALARM.ReactorDamage]) then
|
||||
local items = { white("REACTOR DAMAGED"), blue("CHECK RCS"), blue("AWAIT DMG REDUCED") }
|
||||
table.insert(ecam, { color = colors.red, text = "REACTOR DAMAGE", help = "ReactorDamage", items = items })
|
||||
end
|
||||
|
||||
if tripped(unit.alarms[ALARM.ReactorOverTemp]) then
|
||||
local items = { white("DAMAGING TEMP"), blue("CHECK RCS"), blue("AWAIT COOLDOWN") }
|
||||
table.insert(ecam, { color = colors.red, text = "REACTOR OVER TEMP", help = "ReactorOverTemp", items = items })
|
||||
end
|
||||
|
||||
if tripped(unit.alarms[ALARM.ReactorHighTemp]) then
|
||||
local items = { white("OVER EXPECTED TEMP"), blue("CHECK RCS") }
|
||||
table.insert(ecam, { color = colors.yellow, text = "REACTOR HIGH TEMP", help = "ReactorHighTemp", items = items})
|
||||
end
|
||||
|
||||
if tripped(unit.alarms[ALARM.ReactorWasteLeak]) then
|
||||
local items = { white("AT WASTE CAPACITY"), blue("CHECK WASTE OUTPUT"), blue("KEEP RCT DISABLED") }
|
||||
table.insert(ecam, { color = colors.red, text = "REACTOR WASTE LEAK", help = "ReactorWasteLeak", items = items})
|
||||
end
|
||||
|
||||
if tripped(unit.alarms[ALARM.ReactorHighWaste]) then
|
||||
local items = { blue("CHECK WASTE OUTPUT") }
|
||||
table.insert(ecam, { color = colors.yellow, text = "REACTOR WASTE HIGH", help = "ReactorHighWaste", items = items})
|
||||
end
|
||||
|
||||
if tripped(unit.alarms[ALARM.RPSTransient]) then
|
||||
local items = {}
|
||||
local stat = unit.reactor_data.rps_status
|
||||
|
||||
-- for k, _ in pairs(stat) do stat[k] = true end
|
||||
|
||||
local function insert(cond, key, text, color) if cond[key] then table.insert(items, { text = text, help = key, color = color }) end end
|
||||
|
||||
table.insert(items, white("REACTOR SCRAMMED"))
|
||||
insert(stat, "high_dmg", "HIGH DAMAGE", colors.red)
|
||||
insert(stat, "high_temp", "HIGH TEMPERATURE", colors.red)
|
||||
insert(stat, "low_cool", "CRIT LOW COOLANT")
|
||||
insert(stat, "ex_waste", "EXCESS WASTE")
|
||||
insert(stat, "ex_hcool", "EXCESS HEATED COOL")
|
||||
insert(stat, "no_fuel", "NO FUEL")
|
||||
insert(stat, "fault", "HARDWARE FAULT")
|
||||
insert(stat, "timeout", "SUPERVISOR DISCONN")
|
||||
insert(stat, "manual", "MANUAL SCRAM", colors.white)
|
||||
insert(stat, "automatic", "AUTOMATIC SCRAM")
|
||||
insert(stat, "sys_fail", "NOT FORMED", colors.red)
|
||||
insert(stat, "force_dis", "FORCE DISABLED", colors.red)
|
||||
table.insert(items, blue("RESOLVE PROBLEM"))
|
||||
table.insert(items, blue("RESET RPS"))
|
||||
|
||||
table.insert(ecam, { color = colors.yellow, text = "RPS TRANSIENT", help = "RPSTransient", items = items})
|
||||
end
|
||||
|
||||
if tripped(unit.alarms[ALARM.RCSTransient]) then
|
||||
local items = {}
|
||||
local annunc = unit.annunciator
|
||||
|
||||
-- for k, v in pairs(annunc) do
|
||||
-- if type(v) == "boolean" then annunc[k] = true end
|
||||
-- if type(v) == "table" then
|
||||
-- for a, _ in pairs(v) do
|
||||
-- v[a] = true
|
||||
-- end
|
||||
-- end
|
||||
-- end
|
||||
|
||||
local function insert(cond, key, text, color)
|
||||
if cond == true or (type(cond) == "table" and cond[key]) then table.insert(items, { text = text, help = key, color = color }) end
|
||||
end
|
||||
|
||||
table.insert(items, white("COOLANT PROBLEM"))
|
||||
|
||||
insert(annunc, "RCPTrip", "RCP TRIP", colors.red)
|
||||
insert(annunc, "CoolantLevelLow", "LOW COOLANT")
|
||||
|
||||
if unit.num_boilers == 0 then
|
||||
if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then
|
||||
insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH")
|
||||
end
|
||||
|
||||
if unit.turbine_flow_stable then
|
||||
insert(annunc, "RCSFlowLow", "RCS FLOW LOW")
|
||||
insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH")
|
||||
insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH")
|
||||
end
|
||||
else
|
||||
if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then
|
||||
insert(annunc, "RCSFlowLow", "RCS FLOW LOW")
|
||||
insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH")
|
||||
insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH")
|
||||
end
|
||||
|
||||
if unit.turbine_flow_stable then
|
||||
insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH")
|
||||
end
|
||||
end
|
||||
|
||||
insert(annunc, "MaxWaterReturnFeed", "MAX WTR RTRN FEED")
|
||||
|
||||
for k, v in ipairs(annunc.WaterLevelLow) do insert(v, "WaterLevelLow", "BOILER " .. k .. " WTR LOW", colors.red) end
|
||||
for k, v in ipairs(annunc.HeatingRateLow) do insert(v, "HeatingRateLow", "BOILER " .. k .. " HEAT RATE") end
|
||||
for k, v in ipairs(annunc.TurbineOverSpeed) do insert(v, "TurbineOverSpeed", "TURBINE " .. k .. " OVERSPD", colors.red) end
|
||||
for k, v in ipairs(annunc.GeneratorTrip) do insert(v, "GeneratorTrip", "TURBINE " .. k .. " GEN TRIP") end
|
||||
|
||||
table.insert(items, blue("CHECK COOLING SYS"))
|
||||
|
||||
table.insert(ecam, { color = colors.yellow, text = "RCS TRANSIENT", help = "RCSTransient", items = items})
|
||||
end
|
||||
|
||||
if tripped(unit.alarms[ALARM.TurbineTrip]) then
|
||||
local items = {}
|
||||
|
||||
for k, v in ipairs(unit.annunciator.TurbineTrip) do
|
||||
if v then table.insert(items, { text = "TURBINE " .. k .. " TRIP", help = "TurbineTrip" }) end
|
||||
end
|
||||
|
||||
table.insert(items, blue("CHECK ENERGY OUT"))
|
||||
table.insert(ecam, { color = colors.red, text = "TURBINE TRIP", help = "TurbineTripAlarm", items = items})
|
||||
end
|
||||
|
||||
if not (tripped(unit.alarms[ALARM.ReactorLost]) or unit.connected) then
|
||||
local items = { blue("CHECK PLC") }
|
||||
table.insert(ecam, { color = colors.yellow, text = "REACTOR OFF-LINE", items = items })
|
||||
end
|
||||
|
||||
for k, v in ipairs(unit.annunciator.BoilerOnline) do
|
||||
if not v then
|
||||
local items = { blue("CHECK RTU") }
|
||||
table.insert(ecam, { color = colors.yellow, text = "BOILER " .. k .. " OFF-LINE", items = items})
|
||||
end
|
||||
end
|
||||
|
||||
for k, v in ipairs(unit.annunciator.TurbineOnline) do
|
||||
if not v then
|
||||
local items = { blue("CHECK RTU") }
|
||||
table.insert(ecam, { color = colors.yellow, text = "TURBINE " .. k .. " OFF-LINE", items = items})
|
||||
end
|
||||
end
|
||||
|
||||
-- if no alarms, put some basic status messages in
|
||||
if #ecam == 0 then
|
||||
table.insert(ecam, { color = colors.green, text = "REACTOR " .. util.trinary(unit.reactor_data.mek_status.status, "NOMINAL", "IDLE"), items = {}})
|
||||
|
||||
local plural = util.trinary(unit.num_turbines > 1, "S", "")
|
||||
table.insert(ecam, { color = colors.green, text = "TURBINE" .. plural .. util.trinary(unit.turbine_flow_stable, " STABLE", " STABILIZING"), items = {}})
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_ECAM", textutils.serialize(ecam))
|
||||
|
||||
--#endregion
|
||||
end
|
||||
|
||||
-- update control app with unit data from API_GET_CTRL
|
||||
---@param data table
|
||||
function iorx.record_control_data(data)
|
||||
for u_id = 1, #data do
|
||||
local unit = io.units[u_id]
|
||||
local u_data = data[u_id]
|
||||
|
||||
unit.connected = u_data[1]
|
||||
|
||||
unit.reactor_data.rps_tripped = u_data[2]
|
||||
unit.unit_ps.publish("rps_tripped", u_data[2])
|
||||
unit.reactor_data.mek_status.status = u_data[3]
|
||||
unit.unit_ps.publish("status", u_data[3])
|
||||
unit.reactor_data.mek_status.temp = u_data[4]
|
||||
unit.unit_ps.publish("temp", u_data[4])
|
||||
unit.reactor_data.mek_status.burn_rate = u_data[5]
|
||||
unit.unit_ps.publish("burn_rate", u_data[5])
|
||||
unit.reactor_data.mek_status.act_burn_rate = u_data[6]
|
||||
unit.unit_ps.publish("act_burn_rate", u_data[6])
|
||||
unit.reactor_data.mek_struct.max_burn = u_data[7]
|
||||
unit.unit_ps.publish("max_burn", u_data[7])
|
||||
|
||||
unit.annunciator.AutoControl = u_data[8]
|
||||
unit.unit_ps.publish("AutoControl", u_data[8])
|
||||
|
||||
unit.a_group = u_data[9]
|
||||
unit.unit_ps.publish("auto_group_id", unit.a_group)
|
||||
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
|
||||
|
||||
local control_status = 1
|
||||
|
||||
if unit.connected then
|
||||
if unit.reactor_data.rps_tripped then
|
||||
control_status = 2
|
||||
end
|
||||
|
||||
if unit.reactor_data.mek_status.status then
|
||||
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
|
||||
end
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_ControlStatus", control_status)
|
||||
end
|
||||
end
|
||||
|
||||
-- update process app with unit data from API_GET_PROC
|
||||
---@param data table
|
||||
function iorx.record_process_data(data)
|
||||
-- get unit data
|
||||
for u_id = 1, #io.units do
|
||||
local unit = io.units[u_id]
|
||||
local u_data = data[u_id]
|
||||
|
||||
unit.reactor_data.mek_status.status = u_data[1]
|
||||
unit.reactor_data.mek_struct.max_burn = u_data[2]
|
||||
unit.annunciator.AutoControl = u_data[6]
|
||||
unit.a_group = u_data[7]
|
||||
|
||||
unit.unit_ps.publish("status", u_data[1])
|
||||
unit.unit_ps.publish("max_burn", u_data[2])
|
||||
unit.unit_ps.publish("burn_limit", u_data[3])
|
||||
unit.unit_ps.publish("U_AutoReady", u_data[4])
|
||||
unit.unit_ps.publish("U_AutoDegraded", u_data[5])
|
||||
unit.unit_ps.publish("AutoControl", u_data[6])
|
||||
unit.unit_ps.publish("auto_group_id", unit.a_group)
|
||||
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
|
||||
end
|
||||
|
||||
-- get facility data
|
||||
local fac = io.facility
|
||||
local f_data = data[#io.units + 1]
|
||||
|
||||
fac.status_lines = f_data[1]
|
||||
|
||||
fac.auto_ready = f_data[2][1]
|
||||
fac.auto_active = f_data[2][2]
|
||||
fac.auto_ramping = f_data[2][3]
|
||||
fac.auto_saturated = f_data[2][4]
|
||||
|
||||
fac.auto_scram = f_data[3]
|
||||
fac.ascram_status = f_data[4]
|
||||
|
||||
fac.ps.publish("status_line_1", fac.status_lines[1])
|
||||
fac.ps.publish("status_line_2", fac.status_lines[2])
|
||||
|
||||
fac.ps.publish("auto_ready", fac.auto_ready)
|
||||
fac.ps.publish("auto_active", fac.auto_active)
|
||||
fac.ps.publish("auto_ramping", fac.auto_ramping)
|
||||
fac.ps.publish("auto_saturated", fac.auto_saturated)
|
||||
|
||||
fac.ps.publish("auto_scram", fac.auto_scram)
|
||||
fac.ps.publish("as_matrix_fault", fac.ascram_status.matrix_fault)
|
||||
fac.ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill)
|
||||
fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm)
|
||||
fac.ps.publish("as_radiation", fac.ascram_status.radiation)
|
||||
fac.ps.publish("as_gen_fault", fac.ascram_status.gen_fault)
|
||||
|
||||
fac.ps.publish("process_mode", f_data[5][1])
|
||||
fac.ps.publish("process_burn_target", f_data[5][2])
|
||||
fac.ps.publish("process_charge_target", f_data[5][3])
|
||||
fac.ps.publish("process_gen_target", f_data[5][4])
|
||||
end
|
||||
|
||||
-- update waste app with unit data from API_GET_WASTE
|
||||
---@param data table
|
||||
function iorx.record_waste_data(data)
|
||||
-- get unit data
|
||||
for u_id = 1, #io.units do
|
||||
local unit = io.units[u_id]
|
||||
local u_data = data[u_id]
|
||||
|
||||
unit.waste_mode = u_data[1]
|
||||
unit.waste_product = u_data[2]
|
||||
unit.num_snas = u_data[3]
|
||||
unit.sna_peak_rate = u_data[4]
|
||||
unit.sna_max_rate = u_data[5]
|
||||
unit.sna_out_rate = u_data[6]
|
||||
unit.waste_stats = u_data[7]
|
||||
|
||||
unit.unit_ps.publish("U_AutoWaste", unit.waste_mode == types.WASTE_MODE.AUTO)
|
||||
unit.unit_ps.publish("U_WasteMode", unit.waste_mode)
|
||||
unit.unit_ps.publish("U_WasteProduct", unit.waste_product)
|
||||
|
||||
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)
|
||||
|
||||
unit.unit_ps.publish("pu_rate", unit.waste_stats[1])
|
||||
unit.unit_ps.publish("po_rate", unit.waste_stats[2])
|
||||
unit.unit_ps.publish("po_pl_rate", unit.waste_stats[3])
|
||||
end
|
||||
|
||||
-- get facility data
|
||||
local fac = io.facility
|
||||
local f_data = data[#io.units + 1]
|
||||
|
||||
fac.auto_current_waste_product = f_data[1]
|
||||
fac.auto_pu_fallback_active = f_data[2]
|
||||
fac.auto_sps_disabled = f_data[3]
|
||||
|
||||
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)
|
||||
|
||||
fac.ps.publish("process_waste_product", f_data[4])
|
||||
fac.ps.publish("process_pu_fallback", f_data[5])
|
||||
fac.ps.publish("process_sps_low_power", f_data[6])
|
||||
|
||||
fac.waste_stats = f_data[7]
|
||||
|
||||
fac.ps.publish("burn_sum", fac.waste_stats[1])
|
||||
fac.ps.publish("pu_rate", fac.waste_stats[2])
|
||||
fac.ps.publish("po_rate", fac.waste_stats[3])
|
||||
fac.ps.publish("po_pl_rate", fac.waste_stats[4])
|
||||
fac.ps.publish("po_am_rate", fac.waste_stats[5])
|
||||
fac.ps.publish("spent_waste_rate", fac.waste_stats[6])
|
||||
|
||||
fac.sps_ps_tbl[1].publish("SPSStateStatus", f_data[8])
|
||||
fac.ps.publish("sps_process_rate", f_data[9])
|
||||
end
|
||||
|
||||
-- update facility app with facility and unit data from API_GET_FAC_DTL
|
||||
---@param data table
|
||||
function iorx.record_fac_detail_data(data)
|
||||
local fac = io.facility
|
||||
|
||||
local tank_statuses = data[5]
|
||||
local next_t_stat = 1
|
||||
|
||||
-- annunciator
|
||||
|
||||
fac.all_sys_ok = data[1]
|
||||
fac.rtu_count = data[2]
|
||||
fac.auto_scram = data[3]
|
||||
fac.ascram_status = data[4]
|
||||
|
||||
fac.ps.publish("all_sys_ok", fac.all_sys_ok)
|
||||
fac.ps.publish("rtu_count", fac.rtu_count)
|
||||
fac.ps.publish("auto_scram", fac.auto_scram)
|
||||
fac.ps.publish("as_matrix_fault", fac.ascram_status.matrix_fault)
|
||||
fac.ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill)
|
||||
fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm)
|
||||
fac.ps.publish("as_radiation", fac.ascram_status.radiation)
|
||||
fac.ps.publish("as_gen_fault", fac.ascram_status.gen_fault)
|
||||
|
||||
-- unit data
|
||||
|
||||
local units = data[12]
|
||||
|
||||
for i = 1, io.facility.num_units do
|
||||
local unit = io.units[i]
|
||||
local u_rx = units[i]
|
||||
|
||||
unit.connected = u_rx[1]
|
||||
unit.annunciator = u_rx[2]
|
||||
unit.reactor_data = u_rx[3]
|
||||
|
||||
local control_status = 1
|
||||
if unit.connected then
|
||||
if unit.reactor_data.rps_tripped then control_status = 2 end
|
||||
if unit.reactor_data.mek_status.status then
|
||||
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
|
||||
end
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_ControlStatus", control_status)
|
||||
|
||||
unit.tank_data_tbl = u_rx[4]
|
||||
|
||||
for id = 1, #unit.tank_data_tbl do
|
||||
local tank = unit.tank_data_tbl[id]
|
||||
local ps = unit.tank_ps_tbl[id]
|
||||
local c_stat = tank_statuses[next_t_stat]
|
||||
|
||||
local tank_status = 1
|
||||
|
||||
if c_stat ~= TNK_STATE.OFFLINE then
|
||||
if c_stat == TNK_STATE.FAULT then
|
||||
tank_status = 3
|
||||
elseif tank.formed then
|
||||
tank_status = 4
|
||||
else
|
||||
tank_status = 2
|
||||
end
|
||||
end
|
||||
|
||||
ps.publish("DynamicTankStatus", tank_status)
|
||||
ps.publish("DynamicTankStateStatus", c_stat)
|
||||
|
||||
next_t_stat = next_t_stat + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- facility dynamic tank data
|
||||
|
||||
fac.tank_data_tbl = data[6]
|
||||
|
||||
for id = 1, #fac.tank_data_tbl do
|
||||
local tank = fac.tank_data_tbl[id]
|
||||
local ps = fac.tank_ps_tbl[id]
|
||||
local c_stat = tank_statuses[next_t_stat]
|
||||
|
||||
local tank_status = 1
|
||||
|
||||
if c_stat ~= TNK_STATE.OFFLINE then
|
||||
if c_stat == TNK_STATE.FAULT then
|
||||
tank_status = 3
|
||||
elseif tank.formed then
|
||||
tank_status = 4
|
||||
else
|
||||
tank_status = 2
|
||||
end
|
||||
|
||||
_record_multiblock_status(c_stat == TNK_STATE.FAULT, tank, ps)
|
||||
end
|
||||
|
||||
ps.publish("DynamicTankStatus", tank_status)
|
||||
ps.publish("DynamicTankStateStatus", c_stat)
|
||||
|
||||
next_t_stat = next_t_stat + 1
|
||||
end
|
||||
|
||||
-- induction matrix data
|
||||
|
||||
fac.induction_data_tbl[1] = data[8]
|
||||
|
||||
local matrix = fac.induction_data_tbl[1]
|
||||
local m_ps = fac.induction_ps_tbl[1]
|
||||
local m_stat = data[7]
|
||||
|
||||
local mtx_status = 1
|
||||
|
||||
if m_stat ~= MTX_STATE.OFFLINE then
|
||||
if m_stat == MTX_STATE.FAULT then
|
||||
mtx_status = 3
|
||||
elseif matrix.formed then
|
||||
mtx_status = 4
|
||||
else
|
||||
mtx_status = 2
|
||||
end
|
||||
|
||||
_record_multiblock_status(m_stat == MTX_STATE.FAULT, matrix, m_ps)
|
||||
end
|
||||
|
||||
m_ps.publish("InductionMatrixStatus", mtx_status)
|
||||
m_ps.publish("InductionMatrixStateStatus", m_stat)
|
||||
|
||||
m_ps.publish("eta_string", data[9][1])
|
||||
m_ps.publish("avg_charge", data[9][2])
|
||||
m_ps.publish("avg_inflow", data[9][3])
|
||||
m_ps.publish("avg_outflow", data[9][4])
|
||||
m_ps.publish("is_charging", data[9][5])
|
||||
m_ps.publish("is_discharging", data[9][6])
|
||||
m_ps.publish("at_max_io", data[9][7])
|
||||
|
||||
-- sps data
|
||||
|
||||
fac.sps_data_tbl[1] = data[11]
|
||||
|
||||
local sps = fac.sps_data_tbl[1]
|
||||
local s_ps = fac.sps_ps_tbl[1]
|
||||
local s_stat = data[10]
|
||||
|
||||
local sps_status = 1
|
||||
|
||||
if s_stat ~= SPS_STATE.OFFLINE then
|
||||
if s_stat == SPS_STATE.FAULT then
|
||||
sps_status = 3
|
||||
elseif sps.formed then
|
||||
sps_status = 4
|
||||
else
|
||||
sps_status = 2
|
||||
end
|
||||
|
||||
_record_multiblock_status(s_stat == SPS_STATE.FAULT, sps, s_ps)
|
||||
end
|
||||
|
||||
s_ps.publish("SPSStatus", sps_status)
|
||||
s_ps.publish("SPSStateStatus", s_stat)
|
||||
end
|
||||
|
||||
-- update the radiation monitor app with radiation monitor data from API_GET_RAD
|
||||
---@param data table
|
||||
function iorx.record_radiation_data(data)
|
||||
-- unit radiation monitors
|
||||
|
||||
for u_id = 1, #io.units do
|
||||
local unit = io.units[u_id]
|
||||
local max_rad = 0
|
||||
local connected = {}
|
||||
|
||||
unit.radiation = types.new_zero_radiation_reading()
|
||||
unit.rad_monitors = data[u_id]
|
||||
|
||||
for id, mon in pairs(unit.rad_monitors) do
|
||||
table.insert(connected, id)
|
||||
|
||||
unit.unit_ps.publish("radiation@" .. id, mon.radiation)
|
||||
|
||||
if mon.raw > max_rad then
|
||||
max_rad = mon.raw
|
||||
unit.radiation = mon.radiation
|
||||
end
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("radiation", unit.radiation)
|
||||
unit.unit_ps.publish("radiation_monitors", textutils.serialize(connected))
|
||||
end
|
||||
|
||||
-- facility radiation monitors
|
||||
|
||||
local fac = io.facility
|
||||
|
||||
fac.radiation = types.new_zero_radiation_reading()
|
||||
fac.rad_monitors = data[#io.units + 1]
|
||||
|
||||
local max_rad = 0
|
||||
local connected = {}
|
||||
|
||||
for id, mon in pairs(fac.rad_monitors) do
|
||||
table.insert(connected, id)
|
||||
|
||||
fac.ps.publish("radiation@" .. id, mon.radiation)
|
||||
|
||||
if mon.raw > max_rad then
|
||||
max_rad = mon.raw
|
||||
fac.radiation = mon.radiation
|
||||
end
|
||||
end
|
||||
|
||||
fac.ps.publish("radiation", fac.radiation)
|
||||
fac.ps.publish("radiation_monitors", textutils.serialize(connected))
|
||||
end
|
||||
|
||||
local comp_record = {}
|
||||
|
||||
-- update the computers app with the network data from INFO_LIST_CMP
|
||||
---@param data table
|
||||
function iorx.record_network_data(data)
|
||||
local ps = io.ps
|
||||
local connected = {}
|
||||
local crd_online = false
|
||||
|
||||
ps.publish("comp_online", #data)
|
||||
|
||||
-- add/update connected computers
|
||||
for i = 1, #data do
|
||||
local entry = data[i]
|
||||
local type = entry[1]
|
||||
local id = entry[2]
|
||||
local pfx = "comp_" .. id
|
||||
|
||||
connected[id] = true
|
||||
|
||||
if type == DEV_TYPE.SVR then
|
||||
ps.publish("comp_svr_addr", id)
|
||||
ps.publish("comp_svr_fw", entry[3])
|
||||
elseif type == DEV_TYPE.CRD then
|
||||
crd_online = true
|
||||
ps.publish("comp_crd_addr", id)
|
||||
ps.publish("comp_crd_fw", entry[3])
|
||||
ps.publish("comp_crd_rtt", entry[4])
|
||||
else
|
||||
ps.publish(pfx .. "_type", entry[1])
|
||||
ps.publish(pfx .. "_addr", id)
|
||||
ps.publish(pfx .. "_fw", entry[3])
|
||||
ps.publish(pfx .. "_rtt", entry[4])
|
||||
|
||||
if type == DEV_TYPE.PLC then
|
||||
ps.publish(pfx .. "_unit", entry[5])
|
||||
end
|
||||
|
||||
if not comp_record[id] then
|
||||
comp_record[id] = true
|
||||
|
||||
-- trigger the app to create the new element
|
||||
ps.publish("comp_connect", id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle the coordinator being online or not
|
||||
-- no need to worry about the supervisor since this data is from the supervisor, so it has to be 'online' if received
|
||||
ps.publish("comp_crd_online", crd_online)
|
||||
if not crd_online then
|
||||
ps.publish("comp_crd_addr", "---")
|
||||
ps.publish("comp_crd_fw", "---")
|
||||
ps.publish("comp_crd_rtt", "---")
|
||||
end
|
||||
|
||||
-- reset the published value
|
||||
ps.publish("comp_connect", false)
|
||||
|
||||
-- remove disconnected computers
|
||||
for id, state in pairs(comp_record) do
|
||||
if state and not connected[id] then
|
||||
comp_record[id] = false
|
||||
|
||||
-- trigger the app to delete the element
|
||||
ps.publish("comp_disconnect", id)
|
||||
end
|
||||
end
|
||||
|
||||
-- reset the published value
|
||||
ps.publish("comp_disconnect", false)
|
||||
end
|
||||
|
||||
-- clear the tracked connected computer record
|
||||
function iorx.clear_comp_record() comp_record = {} end
|
||||
|
||||
return function (io_obj)
|
||||
io = io_obj
|
||||
return iorx
|
||||
end
|
||||
@@ -9,12 +9,21 @@ local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
|
||||
local LINK_STATE = iocontrol.LINK_STATE
|
||||
|
||||
local pocket = {}
|
||||
|
||||
local MQ__RENDER_DATA = {
|
||||
LOAD_APP = 1
|
||||
}
|
||||
|
||||
pocket.MQ__RENDER_DATA = MQ__RENDER_DATA
|
||||
|
||||
---@type pkt_config
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
local config = {}
|
||||
|
||||
pocket.config = config
|
||||
@@ -23,6 +32,10 @@ pocket.config = config
|
||||
function pocket.load_config()
|
||||
if not settings.load("/pocket.settings") then return false end
|
||||
|
||||
config.GreenPuPellet = settings.get("GreenPuPellet")
|
||||
config.TempScale = settings.get("TempScale")
|
||||
config.EnergyScale = settings.get("EnergyScale")
|
||||
|
||||
config.SVR_Channel = settings.get("SVR_Channel")
|
||||
config.CRD_Channel = settings.get("CRD_Channel")
|
||||
config.PKT_Channel = settings.get("PKT_Channel")
|
||||
@@ -36,6 +49,12 @@ function pocket.load_config()
|
||||
|
||||
local cfv = util.new_validator()
|
||||
|
||||
cfv.assert_type_bool(config.GreenPuPellet)
|
||||
cfv.assert_type_int(config.TempScale)
|
||||
cfv.assert_range(config.TempScale, 1, 4)
|
||||
cfv.assert_type_int(config.EnergyScale)
|
||||
cfv.assert_range(config.EnergyScale, 1, 3)
|
||||
|
||||
cfv.assert_channel(config.SVR_Channel)
|
||||
cfv.assert_channel(config.CRD_Channel)
|
||||
cfv.assert_channel(config.PKT_Channel)
|
||||
@@ -58,26 +77,332 @@ function pocket.load_config()
|
||||
return cfv.valid()
|
||||
end
|
||||
|
||||
---@enum POCKET_APP_ID
|
||||
local APP_ID = {
|
||||
-- core UI
|
||||
ROOT = 1,
|
||||
LOADER = 2,
|
||||
-- main apps
|
||||
UNITS = 3,
|
||||
FACILITY = 4,
|
||||
CONTROL = 5,
|
||||
PROCESS = 6,
|
||||
WASTE = 7,
|
||||
GUIDE = 8,
|
||||
ABOUT = 9,
|
||||
RADMON = 10,
|
||||
-- diagnostic apps
|
||||
ALARMS = 11,
|
||||
COMPS = 12,
|
||||
-- count
|
||||
NUM_APPS = 12
|
||||
}
|
||||
|
||||
pocket.APP_ID = APP_ID
|
||||
|
||||
---@class nav_tree_page
|
||||
---@field _p nav_tree_page|nil page's parent
|
||||
---@field _c nav_tree_page[] page's children
|
||||
---@field nav_to function function to navigate to this page
|
||||
---@field switcher function|nil function to switch between children
|
||||
---@field tasks function[] tasks to run while viewing this page
|
||||
|
||||
-- initialize the page navigation system
|
||||
---@param smem pkt_shared_memory
|
||||
function pocket.init_nav(smem)
|
||||
local self = {
|
||||
pane = nil, ---@type AppMultiPane|MultiPane|nil
|
||||
sidebar = nil, ---@type Sidebar|nil
|
||||
apps = {}, ---@type pocket_app[]
|
||||
containers = {}, ---@type Container[]
|
||||
help_map = {}, ---@type { [string]: function }
|
||||
help_return = nil, ---@type POCKET_APP_ID|nil
|
||||
loader_return = nil, ---@type POCKET_APP_ID|nil
|
||||
cur_app = APP_ID.ROOT
|
||||
}
|
||||
|
||||
self.cur_page = self.root
|
||||
|
||||
---@class pocket_nav
|
||||
local nav = {}
|
||||
|
||||
-- set the root pane element to switch between apps with
|
||||
---@param root_pane MultiPane
|
||||
function nav.set_pane(root_pane) self.pane = root_pane end
|
||||
|
||||
-- link sidebar element
|
||||
---@param sidebar Sidebar
|
||||
function nav.set_sidebar(sidebar) self.sidebar = sidebar end
|
||||
|
||||
-- register an app
|
||||
---@param app_id POCKET_APP_ID app ID
|
||||
---@param container Container element that contains this app (usually a Div)
|
||||
---@param pane? AppMultiPane|MultiPane multipane if this is a simple paned app, then nav_to must be a number
|
||||
---@param require_sv? boolean true to specifiy if this app should be unloaded when the supervisor connection is lost
|
||||
---@param require_api? boolean true to specifiy if this app should be unloaded when the api connection is lost
|
||||
function nav.register_app(app_id, container, pane, require_sv, require_api)
|
||||
---@class pocket_app
|
||||
local app = {
|
||||
loaded = false,
|
||||
cur_page = nil, ---@type nav_tree_page|nil
|
||||
pane = pane,
|
||||
paned_pages = {}, ---@type nav_tree_page[]
|
||||
sidebar_items = {} ---@type sidebar_entry[]
|
||||
}
|
||||
|
||||
app.load = function () app.loaded = true end
|
||||
app.unload = function () app.loaded = false end
|
||||
|
||||
-- check which connections this requires (for unload)
|
||||
---@return boolean requires_sv, boolean requires_api
|
||||
function app.check_requires() return require_sv or false, require_api or false end
|
||||
|
||||
-- check if any connection is required (for load)
|
||||
function app.requires_conn() return require_sv or require_api or false end
|
||||
|
||||
-- delayed set of the pane if it wasn't ready at the start
|
||||
---@param root_pane AppMultiPane|MultiPane multipane
|
||||
function app.set_root_pane(root_pane)
|
||||
app.pane = root_pane
|
||||
end
|
||||
|
||||
-- configure the sidebar
|
||||
---@param items sidebar_entry[]
|
||||
function app.set_sidebar(items)
|
||||
app.sidebar_items = items
|
||||
-- only modify the sidebar if this app is still open
|
||||
if self.cur_app == app_id then
|
||||
if self.sidebar then self.sidebar.update(items) end
|
||||
end
|
||||
end
|
||||
|
||||
-- function to run on initial load into memory
|
||||
---@param on_load function callback
|
||||
function app.set_load(on_load)
|
||||
app.load = function ()
|
||||
app.loaded = true -- must flag first so it can't be repeatedly attempted
|
||||
on_load()
|
||||
end
|
||||
end
|
||||
|
||||
-- function to run to close out the app
|
||||
---@param on_unload function callback
|
||||
function app.set_unload(on_unload)
|
||||
app.unload = function ()
|
||||
app.loaded = false
|
||||
on_unload()
|
||||
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|nil 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
|
||||
|
||||
-- delete paned pages and clear the current page
|
||||
function app.delete_pages()
|
||||
app.paned_pages = {}
|
||||
app.cur_page = nil
|
||||
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
|
||||
|
||||
-- open an app
|
||||
---@param app_id POCKET_APP_ID
|
||||
---@param on_ready? function
|
||||
function nav.open_app(app_id, on_ready)
|
||||
-- reset help return on navigating out of an app
|
||||
if app_id == APP_ID.ROOT then self.help_return = nil end
|
||||
|
||||
local app = self.apps[app_id]
|
||||
if app then
|
||||
local p_comms = smem.pkt_sys.pocket_comms
|
||||
local req_sv, req_api = app.check_requires()
|
||||
|
||||
if (req_sv and not p_comms.is_sv_linked()) or (req_api and not p_comms.is_api_linked()) then
|
||||
-- report required connction(s)
|
||||
iocontrol.get_db().loader_require = { sv = req_sv, api = req_api }
|
||||
iocontrol.get_db().ps.toggle("loader_reqs")
|
||||
|
||||
-- bring up the app loader
|
||||
self.loader_return = app_id
|
||||
app_id = APP_ID.LOADER
|
||||
app = self.apps[app_id]
|
||||
else self.loader_return = nil end
|
||||
|
||||
if not app.loaded then smem.q.mq_render.push_data(MQ__RENDER_DATA.LOAD_APP, { app_id, on_ready }) 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
|
||||
|
||||
if app.loaded and on_ready then on_ready() end
|
||||
else
|
||||
log.debug("tried to open unknown app")
|
||||
end
|
||||
end
|
||||
|
||||
-- go home (open the home screen app)
|
||||
function nav.go_home() nav.open_app(APP_ID.ROOT) end
|
||||
|
||||
-- open the app that was blocked on connecting
|
||||
function nav.on_loader_connected()
|
||||
if self.loader_return then
|
||||
nav.open_app(self.loader_return)
|
||||
end
|
||||
end
|
||||
|
||||
-- load a given app
|
||||
---@param app_id POCKET_APP_ID
|
||||
function nav.load_app(app_id)
|
||||
self.apps[app_id].load()
|
||||
end
|
||||
|
||||
-- unload api-dependent apps
|
||||
function nav.unload_api()
|
||||
for id, app in pairs(self.apps) do
|
||||
local _, api = app.check_requires()
|
||||
if app.loaded and api then
|
||||
if id == self.cur_app then nav.open_app(APP_ID.ROOT) end
|
||||
app.unload()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- unload supervisor-dependent apps
|
||||
function nav.unload_sv()
|
||||
for id, app in pairs(self.apps) do
|
||||
local sv, _ = app.check_requires()
|
||||
if app.loaded and sv then
|
||||
if id == self.cur_app then nav.open_app(APP_ID.ROOT) end
|
||||
app.unload()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- get a list of the app containers (usually Div elements)
|
||||
function nav.get_containers() return self.containers end
|
||||
|
||||
-- get the currently active page
|
||||
---@return nav_tree_page|nil
|
||||
function nav.get_current_page()
|
||||
return self.apps[self.cur_app].get_current_page()
|
||||
end
|
||||
|
||||
-- attempt to navigate up within the active app, otherwise open home page<br>
|
||||
-- except, this will go back to a prior app if leaving the help app after open_help was used
|
||||
function nav.nav_up()
|
||||
-- return out of help if opened with open_help
|
||||
if self.help_return then
|
||||
nav.open_app(self.help_return)
|
||||
self.help_return = nil
|
||||
return
|
||||
end
|
||||
|
||||
local app = self.apps[self.cur_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")
|
||||
nav.open_app(APP_ID.ROOT)
|
||||
end
|
||||
end
|
||||
|
||||
-- open the help app, to show the reference for a key
|
||||
function nav.open_help(key)
|
||||
self.help_return = self.cur_app
|
||||
|
||||
nav.open_app(APP_ID.GUIDE, function ()
|
||||
if self.help_map[key] then self.help_map[key]() end
|
||||
end)
|
||||
end
|
||||
|
||||
-- link the help map from the guide app
|
||||
---@param map { [string]: function }
|
||||
function nav.link_help(map) self.help_map = map end
|
||||
|
||||
return nav
|
||||
end
|
||||
|
||||
-- pocket coordinator + supervisor communications
|
||||
---@nodiscard
|
||||
---@param version string pocket version
|
||||
---@param nic nic network interface device
|
||||
---@param sv_watchdog watchdog
|
||||
---@param api_watchdog watchdog
|
||||
function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
---@param nav pocket_nav
|
||||
function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
local self = {
|
||||
sv = {
|
||||
linked = false,
|
||||
addr = comms.BROADCAST,
|
||||
seq_num = 0,
|
||||
r_seq_num = nil, ---@type nil|integer
|
||||
seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
|
||||
r_seq_num = nil, ---@type nil|integer
|
||||
last_est_ack = ESTABLISH_ACK.ALLOW
|
||||
},
|
||||
api = {
|
||||
linked = false,
|
||||
addr = comms.BROADCAST,
|
||||
seq_num = 0,
|
||||
r_seq_num = nil, ---@type nil|integer
|
||||
seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
|
||||
r_seq_num = nil, ---@type nil|integer
|
||||
last_est_ack = ESTABLISH_ACK.ALLOW
|
||||
},
|
||||
establish_delay_counter = 0
|
||||
@@ -135,11 +460,13 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
|
||||
-- attempt supervisor connection establishment
|
||||
local function _send_sv_establish()
|
||||
self.sv.r_seq_num = nil
|
||||
_send_sv(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
|
||||
end
|
||||
|
||||
-- attempt coordinator API connection establishment
|
||||
local function _send_api_establish()
|
||||
self.api.r_seq_num = nil
|
||||
_send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT, comms.api_version })
|
||||
end
|
||||
|
||||
@@ -163,6 +490,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
-- close connection to the supervisor
|
||||
function public.close_sv()
|
||||
sv_watchdog.cancel()
|
||||
nav.unload_sv()
|
||||
self.sv.linked = false
|
||||
self.sv.r_seq_num = nil
|
||||
self.sv.addr = comms.BROADCAST
|
||||
@@ -172,6 +500,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
-- close connection to coordinator API server
|
||||
function public.close_api()
|
||||
api_watchdog.cancel()
|
||||
nav.unload_api()
|
||||
self.api.linked = false
|
||||
self.api.r_seq_num = nil
|
||||
self.api.addr = comms.BROADCAST
|
||||
@@ -187,7 +516,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
-- attempt to re-link if any of the dependent links aren't active
|
||||
function public.link_update()
|
||||
if not self.sv.linked then
|
||||
iocontrol.report_link_state(util.trinary(self.api.linked, LINK_STATE.API_LINK_ONLY, LINK_STATE.UNLINKED))
|
||||
if self.api.linked then
|
||||
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY, false, nil)
|
||||
else
|
||||
iocontrol.report_link_state(LINK_STATE.UNLINKED, false, false)
|
||||
end
|
||||
|
||||
if self.establish_delay_counter <= 0 then
|
||||
_send_sv_establish()
|
||||
@@ -196,7 +529,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
self.establish_delay_counter = self.establish_delay_counter - 1
|
||||
end
|
||||
elseif not self.api.linked then
|
||||
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY)
|
||||
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, nil, false)
|
||||
|
||||
if self.establish_delay_counter <= 0 then
|
||||
_send_api_establish()
|
||||
@@ -204,9 +537,6 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
else
|
||||
self.establish_delay_counter = self.establish_delay_counter - 1
|
||||
end
|
||||
else
|
||||
-- linked, all good!
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -229,11 +559,62 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end
|
||||
end
|
||||
|
||||
-- supervisor get connected computers
|
||||
function public.diag__get_computers()
|
||||
if self.sv.linked then _send_sv(MGMT_TYPE.INFO_LIST_CMP, {}) end
|
||||
end
|
||||
|
||||
-- coordinator get facility app data
|
||||
function public.api__get_facility()
|
||||
if self.api.linked then _send_api(CRDN_TYPE.API_GET_FAC_DTL, {}) 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
|
||||
|
||||
-- coordinator get control app data
|
||||
function public.api__get_control()
|
||||
if self.api.linked then _send_api(CRDN_TYPE.API_GET_CTRL, {}) end
|
||||
end
|
||||
|
||||
-- coordinator get process app data
|
||||
function public.api__get_process()
|
||||
if self.api.linked then _send_api(CRDN_TYPE.API_GET_PROC, {}) end
|
||||
end
|
||||
|
||||
-- coordinator get waste app data
|
||||
function public.api__get_waste()
|
||||
if self.api.linked then _send_api(CRDN_TYPE.API_GET_WASTE, {}) end
|
||||
end
|
||||
|
||||
-- coordinator get radiation app data
|
||||
function public.api__get_rad()
|
||||
if self.api.linked then _send_api(CRDN_TYPE.API_GET_RAD, {}) end
|
||||
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_api(CRDN_TYPE.FAC_CMD, { cmd, option })
|
||||
end
|
||||
|
||||
-- send the auto process control configuration with a start command
|
||||
---@param auto_cfg [ PROCESS, number, number, number, number[] ]
|
||||
function public.send_auto_start(auto_cfg)
|
||||
_send_api(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.START, table.unpack(auto_cfg) })
|
||||
end
|
||||
|
||||
-- send a unit command
|
||||
---@param cmd UNIT_COMMAND command
|
||||
---@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_api(CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
|
||||
end
|
||||
|
||||
-- parse a packet
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
@@ -274,7 +655,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
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))
|
||||
log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type, length, packet.length))
|
||||
end
|
||||
return ok
|
||||
end
|
||||
@@ -289,6 +670,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
---@param packet mgmt_frame|crdn_frame|nil
|
||||
function public.handle_packet(packet)
|
||||
local diag = iocontrol.get_db().diag
|
||||
local ps = iocontrol.get_db().ps
|
||||
|
||||
if packet ~= nil then
|
||||
local l_chan = packet.scada_frame.local_channel()
|
||||
@@ -301,16 +683,16 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
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()
|
||||
elseif self.connected and ((self.api.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
|
||||
log.warning("sequence out-of-order (API): last = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
self.api.r_seq_num = packet.scada_frame.seq_num() + 1
|
||||
elseif self.api.r_seq_num ~= packet.scada_frame.seq_num() then
|
||||
log.warning("sequence out-of-order (API): next = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
return
|
||||
elseif self.api.linked and (src_addr ~= self.api.addr) then
|
||||
log.debug("received packet from unknown computer " .. src_addr .. " while linked (API expected " .. self.api.addr ..
|
||||
"); channel in use by another system?")
|
||||
return
|
||||
else
|
||||
self.api.r_seq_num = packet.scada_frame.seq_num()
|
||||
self.api.r_seq_num = packet.scada_frame.seq_num() + 1
|
||||
end
|
||||
|
||||
-- feed watchdog on valid sequence number
|
||||
@@ -319,13 +701,79 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
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 packet.type == CRDN_TYPE.FAC_CMD then
|
||||
-- facility command acknowledgement
|
||||
if packet.length >= 2 then
|
||||
local cmd = packet.data[1]
|
||||
local ack = packet.data[2] == true
|
||||
|
||||
if cmd == FAC_COMMAND.SCRAM_ALL then
|
||||
iocontrol.get_db().facility.scram_ack(ack)
|
||||
elseif cmd == FAC_COMMAND.STOP then
|
||||
iocontrol.get_db().facility.stop_ack(ack)
|
||||
elseif cmd == FAC_COMMAND.START then
|
||||
iocontrol.get_db().facility.start_ack(ack)
|
||||
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
|
||||
iocontrol.get_db().facility.ack_alarms_ack(ack)
|
||||
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
|
||||
elseif cmd == FAC_COMMAND.SET_PU_FB then
|
||||
elseif cmd == FAC_COMMAND.SET_SPS_LP then
|
||||
else
|
||||
log.debug(util.c("received facility command ack with unknown command ", cmd))
|
||||
end
|
||||
else
|
||||
log.debug("SCADA_CRDN facility command ack packet length mismatch")
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.UNIT_CMD then
|
||||
-- unit command acknowledgement
|
||||
if packet.length == 3 then
|
||||
local cmd = packet.data[1]
|
||||
local unit_id = packet.data[2]
|
||||
local ack = packet.data[3] == true
|
||||
|
||||
local unit = iocontrol.get_db().units[unit_id] ---@type pioctl_unit
|
||||
|
||||
if unit ~= nil then
|
||||
if cmd == UNIT_COMMAND.SCRAM then
|
||||
unit.scram_ack(ack)
|
||||
elseif cmd == UNIT_COMMAND.START then
|
||||
unit.start_ack(ack)
|
||||
elseif cmd == UNIT_COMMAND.RESET_RPS then
|
||||
unit.reset_rps_ack(ack)
|
||||
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
|
||||
unit.ack_alarms_ack(ack)
|
||||
else
|
||||
log.debug(util.c("received unsupported unit command ack for command ", cmd))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.API_GET_FAC then
|
||||
if _check_length(packet, 11) then
|
||||
iocontrol.record_facility_data(packet.data)
|
||||
iocontrol.rx.record_facility_data(packet.data)
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.API_GET_FAC_DTL then
|
||||
if _check_length(packet, 12) then
|
||||
iocontrol.rx.record_fac_detail_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)
|
||||
if _check_length(packet, 12) and type(packet.data[1]) == "number" and iocontrol.get_db().units[packet.data[1]] then
|
||||
iocontrol.rx.record_unit_data(packet.data)
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.API_GET_CTRL then
|
||||
if _check_length(packet, #iocontrol.get_db().units) then
|
||||
iocontrol.rx.record_control_data(packet.data)
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.API_GET_PROC then
|
||||
if _check_length(packet, #iocontrol.get_db().units + 1) then
|
||||
iocontrol.rx.record_process_data(packet.data)
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.API_GET_WASTE then
|
||||
if _check_length(packet, #iocontrol.get_db().units + 1) then
|
||||
iocontrol.rx.record_waste_data(packet.data)
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.API_GET_RAD then
|
||||
if _check_length(packet, #iocontrol.get_db().units + 1) then
|
||||
iocontrol.rx.record_radiation_data(packet.data)
|
||||
end
|
||||
else _fail_type(packet) end
|
||||
else
|
||||
@@ -353,6 +801,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
elseif packet.type == MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
api_watchdog.cancel()
|
||||
nav.unload_api()
|
||||
self.api.linked = false
|
||||
self.api.r_seq_num = nil
|
||||
self.api.addr = comms.BROADCAST
|
||||
@@ -371,18 +820,19 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
-- get configuration
|
||||
local conf = { num_units = fac_config[1], cooling = fac_config[2] }
|
||||
|
||||
---@todo unit options
|
||||
iocontrol.init_fac(conf, 1)
|
||||
iocontrol.init_fac(conf)
|
||||
|
||||
log.info("coordinator connection established")
|
||||
self.establish_delay_counter = 0
|
||||
self.api.linked = true
|
||||
self.api.addr = src_addr
|
||||
|
||||
iocontrol.report_crd_link_error("")
|
||||
|
||||
if self.sv.linked then
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, nil, self.api.addr)
|
||||
else
|
||||
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY)
|
||||
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY, nil, self.api.addr)
|
||||
end
|
||||
else
|
||||
log.debug("invalid facility configuration table received from coordinator, establish failed")
|
||||
@@ -390,24 +840,29 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
else
|
||||
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
|
||||
log.info("coordinator connection denied")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
if self.api.last_est_ack ~= est_ack then
|
||||
log.info("coordinator connection denied due to collision")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
if self.api.last_est_ack ~= est_ack then
|
||||
log.info("coordinator comms version mismatch")
|
||||
end
|
||||
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")
|
||||
if self.api.last_est_ack ~= est_ack then
|
||||
if est_ack == ESTABLISH_ACK.DENY then
|
||||
log.info("coordinator connection denied")
|
||||
iocontrol.report_crd_link_error("denied")
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
log.info("coordinator connection denied due to collision")
|
||||
iocontrol.report_crd_link_error("collision")
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
log.info("coordinator comms version mismatch")
|
||||
iocontrol.report_crd_link_error("comms version mismatch")
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_API_VERSION then
|
||||
log.info("coordinator api version mismatch")
|
||||
iocontrol.report_crd_link_error("API version mismatch")
|
||||
else
|
||||
log.debug("coordinator SCADA_MGMT establish packet reply unsupported")
|
||||
iocontrol.report_crd_link_error("unknown reply")
|
||||
end
|
||||
end
|
||||
|
||||
-- unlink
|
||||
self.api.addr = comms.BROADCAST
|
||||
self.api.linked = false
|
||||
end
|
||||
|
||||
self.api.last_est_ack = est_ack
|
||||
@@ -421,16 +876,16 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
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()
|
||||
elseif self.connected and ((self.sv.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
|
||||
log.warning("sequence out-of-order (SVR): last = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
self.sv.r_seq_num = packet.scada_frame.seq_num() + 1
|
||||
elseif self.sv.r_seq_num ~= packet.scada_frame.seq_num() then
|
||||
log.warning("sequence out-of-order (SVR): next = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
return
|
||||
elseif self.sv.linked and (src_addr ~= self.sv.addr) then
|
||||
log.debug("received packet from unknown computer " .. src_addr .. " while linked (SVR expected " .. self.sv.addr ..
|
||||
"); channel in use by another system?")
|
||||
return
|
||||
else
|
||||
self.sv.r_seq_num = packet.scada_frame.seq_num()
|
||||
self.sv.r_seq_num = packet.scada_frame.seq_num() + 1
|
||||
end
|
||||
|
||||
-- feed watchdog on valid sequence number
|
||||
@@ -459,6 +914,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
elseif packet.type == MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
sv_watchdog.cancel()
|
||||
nav.unload_sv()
|
||||
self.sv.linked = false
|
||||
self.sv.r_seq_num = nil
|
||||
self.sv.addr = comms.BROADCAST
|
||||
@@ -466,23 +922,23 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
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)
|
||||
ps.publish("alarm_tone_" .. i, 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")
|
||||
ps.publish("alarm_ready_warn", "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"))
|
||||
ps.publish("alarm_ready_warn", util.trinary(ready, "", "system not idle"))
|
||||
|
||||
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)
|
||||
ps.publish("alarm_tone_" .. i, states[i] == true)
|
||||
end
|
||||
end
|
||||
else
|
||||
@@ -490,13 +946,13 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
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")
|
||||
ps.publish("alarm_ready_warn", "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"))
|
||||
ps.publish("alarm_ready_warn", util.trinary(ready, "", "system not idle"))
|
||||
|
||||
for i = 1, #states do
|
||||
if diag.tone_test.alarm_buttons[i] ~= nil then
|
||||
@@ -506,6 +962,8 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
else
|
||||
log.debug("supervisor SCADA diag alarm set packet length/type mismatch")
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.INFO_LIST_CMP then
|
||||
iocontrol.rx.record_network_data(packet.data)
|
||||
else _fail_type(packet) end
|
||||
elseif packet.type == MGMT_TYPE.ESTABLISH then
|
||||
-- connection with supervisor established
|
||||
@@ -518,25 +976,33 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
self.sv.linked = true
|
||||
self.sv.addr = src_addr
|
||||
|
||||
iocontrol.report_svr_link_error("")
|
||||
|
||||
if self.api.linked then
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, nil)
|
||||
else
|
||||
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
|
||||
log.info("supervisor connection denied")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
if self.sv.last_est_ack ~= est_ack then
|
||||
log.info("supervisor connection denied due to collision")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
if self.sv.last_est_ack ~= est_ack then
|
||||
log.info("supervisor comms version mismatch")
|
||||
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, self.sv.addr, nil)
|
||||
end
|
||||
else
|
||||
log.debug("supervisor SCADA_MGMT establish packet reply unsupported")
|
||||
if self.sv.last_est_ack ~= est_ack then
|
||||
if est_ack == ESTABLISH_ACK.DENY then
|
||||
log.info("supervisor connection denied")
|
||||
iocontrol.report_svr_link_error("denied")
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
log.info("supervisor connection denied due to collision")
|
||||
iocontrol.report_svr_link_error("collision")
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
log.info("supervisor comms version mismatch")
|
||||
iocontrol.report_svr_link_error("comms version mismatch")
|
||||
else
|
||||
log.debug("supervisor SCADA_MGMT establish packet reply unsupported")
|
||||
iocontrol.report_svr_link_error("unknown reply")
|
||||
end
|
||||
end
|
||||
|
||||
-- unlink
|
||||
self.sv.addr = comms.BROADCAST
|
||||
self.sv.linked = false
|
||||
end
|
||||
|
||||
self.sv.last_est_ack = est_ack
|
||||
@@ -559,6 +1025,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
---@nodiscard
|
||||
function public.is_api_linked() return self.api.linked end
|
||||
|
||||
-- check if we are still linked with the supervisor and coordinator
|
||||
---@nodiscard
|
||||
function public.is_linked() return self.sv.linked and self.api.linked end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
|
||||
166
pocket/process.lua
Normal file
166
pocket/process.lua
Normal file
@@ -0,0 +1,166 @@
|
||||
--
|
||||
-- Process Control Management
|
||||
--
|
||||
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local F_CMD = comms.FAC_COMMAND
|
||||
local U_CMD = comms.UNIT_COMMAND
|
||||
|
||||
---@class pocket_process_controller
|
||||
local process = {}
|
||||
|
||||
local self = {
|
||||
io = nil, ---@type ioctl
|
||||
comms = nil ---@type pocket_comms
|
||||
}
|
||||
|
||||
-- initialize the process controller
|
||||
---@param iocontrol pocket_ioctl iocontrl system
|
||||
---@param pocket_comms pocket_comms pocket communications
|
||||
function process.init(iocontrol, pocket_comms)
|
||||
self.io = iocontrol
|
||||
self.comms = pocket_comms
|
||||
end
|
||||
|
||||
------------------------------
|
||||
--#region FACILITY COMMANDS --
|
||||
|
||||
-- facility SCRAM command
|
||||
function process.fac_scram()
|
||||
self.comms.send_fac_command(F_CMD.SCRAM_ALL)
|
||||
log.debug("PROCESS: FAC SCRAM ALL")
|
||||
end
|
||||
|
||||
-- facility alarm acknowledge command
|
||||
function process.fac_ack_alarms()
|
||||
self.comms.send_fac_command(F_CMD.ACK_ALL_ALARMS)
|
||||
log.debug("PROCESS: FAC ACK ALL ALARMS")
|
||||
end
|
||||
|
||||
--#endregion
|
||||
------------------------------
|
||||
|
||||
--------------------------
|
||||
--#region UNIT COMMANDS --
|
||||
|
||||
-- start reactor
|
||||
---@param id integer unit ID
|
||||
function process.start(id)
|
||||
self.io.units[id].control_state = true
|
||||
self.comms.send_unit_command(U_CMD.START, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
|
||||
end
|
||||
|
||||
-- SCRAM reactor
|
||||
---@param id integer unit ID
|
||||
function process.scram(id)
|
||||
self.io.units[id].control_state = false
|
||||
self.comms.send_unit_command(U_CMD.SCRAM, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
|
||||
end
|
||||
|
||||
-- reset reactor protection system
|
||||
---@param id integer unit ID
|
||||
function process.reset_rps(id)
|
||||
self.comms.send_unit_command(U_CMD.RESET_RPS, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
|
||||
end
|
||||
|
||||
-- set burn rate
|
||||
---@param id integer unit ID
|
||||
---@param rate number burn rate
|
||||
function process.set_rate(id, rate)
|
||||
self.comms.send_unit_command(U_CMD.SET_BURN, id, rate)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
|
||||
end
|
||||
|
||||
-- assign a unit to a group
|
||||
---@param unit_id integer unit ID
|
||||
---@param group_id integer|0 group ID or 0 for independent
|
||||
function process.set_group(unit_id, group_id)
|
||||
self.comms.send_unit_command(U_CMD.SET_GROUP, unit_id, group_id)
|
||||
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
|
||||
end
|
||||
|
||||
-- set waste mode
|
||||
---@param id integer unit ID
|
||||
---@param mode integer waste mode
|
||||
function process.set_unit_waste(id, mode)
|
||||
self.comms.send_unit_command(U_CMD.SET_WASTE, id, mode)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
|
||||
end
|
||||
|
||||
-- acknowledge all alarms
|
||||
---@param id integer unit ID
|
||||
function process.ack_all_alarms(id)
|
||||
self.comms.send_unit_command(U_CMD.ACK_ALL_ALARMS, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
|
||||
end
|
||||
|
||||
-- acknowledge an alarm
|
||||
---@param id integer unit ID
|
||||
---@param alarm integer alarm ID
|
||||
function process.ack_alarm(id, alarm)
|
||||
self.comms.send_unit_command(U_CMD.ACK_ALARM, id, alarm)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
|
||||
end
|
||||
|
||||
-- reset an alarm
|
||||
---@param id integer unit ID
|
||||
---@param alarm integer alarm ID
|
||||
function process.reset_alarm(id, alarm)
|
||||
self.comms.send_unit_command(U_CMD.RESET_ALARM, id, alarm)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
|
||||
end
|
||||
|
||||
-- #endregion
|
||||
--------------------------
|
||||
|
||||
---------------------------------
|
||||
--#region AUTO PROCESS CONTROL --
|
||||
|
||||
-- process start command
|
||||
---@param mode PROCESS process control mode
|
||||
---@param burn_target number burn rate target
|
||||
---@param charge_target number charge level target
|
||||
---@param gen_target number generation rate target
|
||||
---@param limits number[] unit burn rate limits
|
||||
function process.process_start(mode, burn_target, charge_target, gen_target, limits)
|
||||
self.comms.send_auto_start({ mode, burn_target, charge_target, gen_target, limits })
|
||||
log.debug("PROCESS: START AUTO CTRL")
|
||||
end
|
||||
|
||||
-- process stop command
|
||||
function process.process_stop()
|
||||
self.comms.send_fac_command(F_CMD.STOP)
|
||||
log.debug("PROCESS: STOP AUTO CTRL")
|
||||
end
|
||||
|
||||
-- set automatic process control waste mode
|
||||
---@param product WASTE_PRODUCT waste product for auto control
|
||||
function process.set_process_waste(product)
|
||||
self.comms.send_fac_command(F_CMD.SET_WASTE_MODE, product)
|
||||
log.debug(util.c("PROCESS: SET WASTE ", product))
|
||||
end
|
||||
|
||||
-- set automatic process control plutonium fallback
|
||||
---@param enabled boolean whether to enable plutonium fallback
|
||||
function process.set_pu_fallback(enabled)
|
||||
self.comms.send_fac_command(F_CMD.SET_PU_FB, enabled)
|
||||
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
|
||||
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(F_CMD.SET_SPS_LP, enabled)
|
||||
log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled))
|
||||
end
|
||||
|
||||
-- #endregion
|
||||
---------------------------------
|
||||
|
||||
return process
|
||||
@@ -8,7 +8,7 @@ local style = require("pocket.ui.style")
|
||||
local core = require("graphics.core")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
local DisplayBox = require("graphics.elements.DisplayBox")
|
||||
|
||||
---@class pocket_renderer
|
||||
local renderer = {}
|
||||
@@ -92,4 +92,20 @@ function renderer.handle_mouse(event)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle a keyboard event
|
||||
---@param event key_interaction|nil
|
||||
function renderer.handle_key(event)
|
||||
if ui.display ~= nil and event ~= nil then
|
||||
ui.display.handle_key(event)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle a paste event
|
||||
---@param text string
|
||||
function renderer.handle_paste(text)
|
||||
if ui.display ~= nil then
|
||||
ui.display.handle_paste(text)
|
||||
end
|
||||
end
|
||||
|
||||
return renderer
|
||||
|
||||
@@ -2,27 +2,37 @@
|
||||
-- SCADA System Access on a Pocket Computer
|
||||
--
|
||||
|
||||
---@diagnostic disable-next-line: lowercase-global
|
||||
pocket = pocket or periphemu -- luacheck: ignore pocket
|
||||
|
||||
local _is_pocket_env = pocket -- luacheck: ignore pocket
|
||||
|
||||
require("/initenv").init_env()
|
||||
|
||||
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 configure = require("pocket.configure")
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local pocket = require("pocket.pocket")
|
||||
local renderer = require("pocket.renderer")
|
||||
local threads = require("pocket.threads")
|
||||
|
||||
local POCKET_VERSION = "v0.9.0-alpha"
|
||||
local POCKET_VERSION = "v1.0.3"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
|
||||
-- check environment (allows Pocket or CraftOS-PC)
|
||||
if not _is_pocket_env then
|
||||
println("You can only use this application on a pocket computer.")
|
||||
return
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
-- get configuration
|
||||
----------------------------------------
|
||||
@@ -72,9 +82,51 @@ local function main()
|
||||
iocontrol.get_db().version = POCKET_VERSION
|
||||
|
||||
----------------------------------------
|
||||
-- setup communications & clocks
|
||||
-- memory allocation
|
||||
----------------------------------------
|
||||
|
||||
-- shared memory across threads
|
||||
---@class pkt_shared_memory
|
||||
local __shared_memory = {
|
||||
-- pocket system state flags
|
||||
---@class pkt_state
|
||||
pkt_state = {
|
||||
ui_ok = false,
|
||||
ui_error = nil,
|
||||
shutdown = false
|
||||
},
|
||||
|
||||
-- core pocket devices
|
||||
pkt_dev = {
|
||||
modem = ppm.get_wireless_modem()
|
||||
},
|
||||
|
||||
-- system objects
|
||||
pkt_sys = {
|
||||
nic = nil, ---@type nic
|
||||
pocket_comms = nil, ---@type pocket_comms
|
||||
sv_wd = nil, ---@type watchdog
|
||||
api_wd = nil, ---@type watchdog
|
||||
nav = nil ---@type pocket_nav
|
||||
},
|
||||
|
||||
-- message queues
|
||||
q = {
|
||||
mq_render = mqueue.new()
|
||||
}
|
||||
}
|
||||
|
||||
local smem_dev = __shared_memory.pkt_dev
|
||||
local smem_sys = __shared_memory.pkt_sys
|
||||
|
||||
local pkt_state = __shared_memory.pkt_state
|
||||
|
||||
----------------------------------------
|
||||
-- setup system
|
||||
----------------------------------------
|
||||
|
||||
smem_sys.nav = pocket.init_nav(__shared_memory)
|
||||
|
||||
-- message authentication init
|
||||
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
|
||||
network.init_mac(config.AuthKey)
|
||||
@@ -83,112 +135,59 @@ local function main()
|
||||
iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED)
|
||||
|
||||
-- get the communications modem
|
||||
local modem = ppm.get_wireless_modem()
|
||||
if modem == nil then
|
||||
if smem_dev.modem == nil then
|
||||
println("startup> wireless modem not found: please craft the pocket computer with a wireless modem")
|
||||
log.fatal("startup> no wireless modem on startup")
|
||||
return
|
||||
end
|
||||
|
||||
-- create connection watchdogs
|
||||
local conn_wd = {
|
||||
sv = util.new_watchdog(config.ConnTimeout),
|
||||
api = util.new_watchdog(config.ConnTimeout)
|
||||
}
|
||||
|
||||
conn_wd.sv.cancel()
|
||||
conn_wd.api.cancel()
|
||||
|
||||
smem_sys.sv_wd = util.new_watchdog(config.ConnTimeout)
|
||||
smem_sys.sv_wd.cancel()
|
||||
smem_sys.api_wd = util.new_watchdog(config.ConnTimeout)
|
||||
smem_sys.api_wd.cancel()
|
||||
log.debug("startup> conn watchdogs created")
|
||||
|
||||
-- create network interface then setup comms
|
||||
local nic = network.nic(modem)
|
||||
local pocket_comms = pocket.comms(POCKET_VERSION, nic, conn_wd.sv, conn_wd.api)
|
||||
smem_sys.nic = network.nic(smem_dev.modem)
|
||||
smem_sys.pocket_comms = pocket.comms(POCKET_VERSION, smem_sys.nic, smem_sys.sv_wd, smem_sys.api_wd, smem_sys.nav)
|
||||
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)
|
||||
iocontrol.init_core(smem_sys.pocket_comms, smem_sys.nav, config)
|
||||
|
||||
----------------------------------------
|
||||
-- start the UI
|
||||
----------------------------------------
|
||||
|
||||
local ui_ok, message = renderer.try_start_ui()
|
||||
if not ui_ok then
|
||||
println(util.c("UI error: ", message))
|
||||
log.error(util.c("startup> GUI render failed with error ", message))
|
||||
else
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
local ui_message
|
||||
pkt_state.ui_ok, ui_message = renderer.try_start_ui()
|
||||
if not pkt_state.ui_ok then
|
||||
println(util.c("UI error: ", ui_message))
|
||||
log.error(util.c("startup> GUI render failed with error ", ui_message))
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
-- main event loop
|
||||
-- start system
|
||||
----------------------------------------
|
||||
|
||||
if ui_ok then
|
||||
-- start connection watchdogs
|
||||
conn_wd.sv.feed()
|
||||
conn_wd.api.feed()
|
||||
log.debug("startup> conn watchdogs started")
|
||||
if pkt_state.ui_ok then
|
||||
-- init threads
|
||||
local main_thread = threads.thread__main(__shared_memory)
|
||||
local render_thread = threads.thread__render(__shared_memory)
|
||||
|
||||
local io_db = iocontrol.get_db()
|
||||
local nav = io_db.nav
|
||||
log.info("startup> completed")
|
||||
|
||||
-- main event loop
|
||||
while true do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
if event == "timer" then
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
|
||||
-- relink if necessary
|
||||
pocket_comms.link_update()
|
||||
|
||||
-- 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
|
||||
log.info("supervisor server timeout")
|
||||
pocket_comms.close_sv()
|
||||
elseif conn_wd.api.is_timer(param1) then
|
||||
-- coordinator watchdog timeout
|
||||
log.info("coordinator api server timeout")
|
||||
pocket_comms.close_api()
|
||||
else
|
||||
-- a non-clock/main watchdog timer event
|
||||
-- notify timer callback dispatcher
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
-- got a packet
|
||||
local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
pocket_comms.handle_packet(packet)
|
||||
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
|
||||
event == "double_click" then
|
||||
-- handle a monitor touch event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
log.info("terminate requested, closing server connections...")
|
||||
pocket_comms.close()
|
||||
log.info("connections closed")
|
||||
break
|
||||
end
|
||||
end
|
||||
-- run threads
|
||||
parallel.waitForAll(main_thread.p_exec, render_thread.p_exec)
|
||||
|
||||
renderer.close_ui()
|
||||
|
||||
if not pkt_state.ui_ok then
|
||||
println(util.c("UI crashed with error: ", pkt_state.ui_error))
|
||||
end
|
||||
else
|
||||
println_ts("UI creation failed")
|
||||
end
|
||||
|
||||
println_ts("exited")
|
||||
|
||||
220
pocket/threads.lua
Normal file
220
pocket/threads.lua
Normal file
@@ -0,0 +1,220 @@
|
||||
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 pocket = require("pocket.pocket")
|
||||
local renderer = require("pocket.renderer")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local threads = {}
|
||||
|
||||
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
|
||||
local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
|
||||
|
||||
local MQ__RENDER_DATA = pocket.MQ__RENDER_DATA
|
||||
|
||||
-- main thread
|
||||
---@nodiscard
|
||||
---@param smem pkt_shared_memory
|
||||
function threads.thread__main(smem)
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
log.debug("main thread start")
|
||||
|
||||
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
|
||||
-- load in from shared memory
|
||||
local pkt_state = smem.pkt_state
|
||||
local pocket_comms = smem.pkt_sys.pocket_comms
|
||||
local sv_wd = smem.pkt_sys.sv_wd
|
||||
local api_wd = smem.pkt_sys.api_wd
|
||||
local nav = smem.pkt_sys.nav
|
||||
|
||||
-- start connection watchdogs
|
||||
sv_wd.feed()
|
||||
api_wd.feed()
|
||||
log.debug("startup> conn watchdogs started")
|
||||
|
||||
-- event loop
|
||||
while true do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
if event == "timer" then
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
|
||||
-- relink if necessary
|
||||
pocket_comms.link_update()
|
||||
|
||||
-- update any tasks for the active page
|
||||
if nav.get_current_page() then
|
||||
local page_tasks = nav.get_current_page().tasks
|
||||
for i = 1, #page_tasks do page_tasks[i]() end
|
||||
end
|
||||
|
||||
loop_clock.start()
|
||||
elseif sv_wd.is_timer(param1) then
|
||||
-- supervisor watchdog timeout
|
||||
log.info("supervisor server timeout")
|
||||
pocket_comms.close_sv()
|
||||
elseif api_wd.is_timer(param1) then
|
||||
-- coordinator watchdog timeout
|
||||
log.info("coordinator api server timeout")
|
||||
pocket_comms.close_api()
|
||||
else
|
||||
-- a non-clock/main watchdog timer event
|
||||
-- notify timer callback dispatcher
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
-- got a packet
|
||||
local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
pocket_comms.handle_packet(packet)
|
||||
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
|
||||
event == "double_click" then
|
||||
-- handle a mouse event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
elseif event == "char" or event == "key" or event == "key_up" then
|
||||
-- handle a keyboard event
|
||||
renderer.handle_key(core.events.new_key_event(event, param1, param2))
|
||||
elseif event == "paste" then
|
||||
-- handle a paste event
|
||||
renderer.handle_paste(param1)
|
||||
end
|
||||
|
||||
-- check for termination request or UI crash
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
log.info("terminate requested, main thread exiting")
|
||||
pkt_state.shutdown = true
|
||||
elseif not pkt_state.ui_ok then
|
||||
pkt_state.shutdown = true
|
||||
log.info("terminating due to fatal UI error")
|
||||
end
|
||||
|
||||
if pkt_state.shutdown then
|
||||
log.info("closing server connections...")
|
||||
pocket_comms.close()
|
||||
log.info("connections 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 pkt_state = smem.pkt_state
|
||||
|
||||
while not pkt_state.shutdown do
|
||||
local status, result = pcall(public.exec)
|
||||
if status == false then
|
||||
log.fatal(util.strval(result))
|
||||
end
|
||||
|
||||
-- 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 pkt_state.shutdown then
|
||||
log.info("main thread restarting now...")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- pocket renderer thread, tasked with long duration draws
|
||||
---@nodiscard
|
||||
---@param smem pkt_shared_memory
|
||||
function threads.thread__render(smem)
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
log.debug("render thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local pkt_state = smem.pkt_state
|
||||
local nav = smem.pkt_sys.nav
|
||||
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 pkt_state.shutdown do
|
||||
local msg = render_queue.pop()
|
||||
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- received a command
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
-- received data
|
||||
local cmd = msg.message ---@type queue_data
|
||||
|
||||
if cmd.key == MQ__RENDER_DATA.LOAD_APP then
|
||||
log.debug("RENDER: load app " .. cmd.val[1])
|
||||
|
||||
local draw_start = util.time_ms()
|
||||
|
||||
pkt_state.ui_ok, pkt_state.ui_error = pcall(function () nav.load_app(cmd.val[1]) end)
|
||||
if not pkt_state.ui_ok then
|
||||
log.fatal(util.c("RENDER: app load failed with error ", pkt_state.ui_error))
|
||||
else
|
||||
log.debug("RENDER: app loaded in " .. (util.time_ms() - draw_start) .. "ms")
|
||||
|
||||
-- call the on loaded function if provided
|
||||
if type(cmd.val[2]) == "function" then cmd.val[2]() end
|
||||
end
|
||||
end
|
||||
elseif msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- received a packet
|
||||
end
|
||||
end
|
||||
|
||||
-- quick yield
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if pkt_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 pkt_state = smem.pkt_state
|
||||
|
||||
while not pkt_state.shutdown do
|
||||
local status, result = pcall(public.exec)
|
||||
if status == false then
|
||||
log.fatal(util.strval(result))
|
||||
end
|
||||
|
||||
if not pkt_state.shutdown then
|
||||
log.info("render thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return threads
|
||||
142
pocket/ui/apps/about.lua
Normal file
142
pocket/ui/apps/about.lua
Normal file
@@ -0,0 +1,142 @@
|
||||
--
|
||||
-- About Page
|
||||
--
|
||||
|
||||
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.PushButton")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
local cpair = core.cpair
|
||||
|
||||
local APP_ID = pocket.APP_ID
|
||||
|
||||
-- create about page view
|
||||
---@param root Container parent
|
||||
local function create_pages(root)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local frame = Div{parent=root,x=1,y=1}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.ABOUT, frame)
|
||||
|
||||
local about_page = app.new_page(nil, 1)
|
||||
local nt_page = app.new_page(about_page, 2)
|
||||
local fw_page = app.new_page(about_page, 3)
|
||||
local hw_page = app.new_page(about_page, 4)
|
||||
|
||||
local about = Div{parent=frame,x=1,y=2}
|
||||
|
||||
TextBox{parent=about,y=1,text="System Information",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=frame,x=1,y=2}
|
||||
TextBox{parent=nt_div,y=1,text="Network Details",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",fg_bg=label}
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
TextBox{parent=nt_div,x=2,text=util.c(os.getComputerID(),":",config.PKT_Channel)}
|
||||
|
||||
nt_div.line_break()
|
||||
TextBox{parent=nt_div,x=2,text="Supervisor Address",fg_bg=label}
|
||||
local sv = TextBox{parent=nt_div,x=2,text=""}
|
||||
|
||||
nt_div.line_break()
|
||||
TextBox{parent=nt_div,x=2,text="Coordinator Address",fg_bg=label}
|
||||
local coord = TextBox{parent=nt_div,x=2,text=""}
|
||||
|
||||
sv.register(db.ps, "sv_addr", sv.set_value)
|
||||
coord.register(db.ps, "api_addr", coord.set_value)
|
||||
|
||||
nt_div.line_break()
|
||||
TextBox{parent=nt_div,x=2,text="Message Authentication",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}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Firmware Versions
|
||||
|
||||
local fw_div = Div{parent=frame,x=1,y=2}
|
||||
TextBox{parent=fw_div,y=1,text="Firmware Versions",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",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=db.version}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="Comms Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=comms.version}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="API Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=comms.api_version}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="Common Lib Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=util.version}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="Graphics Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=core.version}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="Lockbox Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=lockbox.version}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Host Versions
|
||||
|
||||
local hw_div = Div{parent=frame,x=1,y=2}
|
||||
TextBox{parent=hw_div,y=1,text="Host Versions",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",fg_bg=label}
|
||||
TextBox{parent=hw_div,x=2,text=_VERSION}
|
||||
|
||||
hw_div.line_break()
|
||||
TextBox{parent=hw_div,x=2,text="Environment",fg_bg=label}
|
||||
TextBox{parent=hw_div,x=2,text=_HOST,height=6}
|
||||
|
||||
--#endregion
|
||||
|
||||
local root_pane = MultiPane{parent=frame,x=1,y=1,panes={about,nt_div,fw_div,hw_div}}
|
||||
|
||||
app.set_root_pane(root_pane)
|
||||
end
|
||||
|
||||
return create_pages
|
||||
184
pocket/ui/apps/alarm.lua
Normal file
184
pocket/ui/apps/alarm.lua
Normal file
@@ -0,0 +1,184 @@
|
||||
--
|
||||
-- Alarm Test App
|
||||
--
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.Div")
|
||||
local MultiPane = require("graphics.elements.MultiPane")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
|
||||
|
||||
local Checkbox = require("graphics.elements.controls.Checkbox")
|
||||
local PushButton = require("graphics.elements.controls.PushButton")
|
||||
local SwitchButton = require("graphics.elements.controls.SwitchButton")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
local cpair = core.cpair
|
||||
|
||||
local APP_ID = pocket.APP_ID
|
||||
|
||||
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)
|
||||
|
||||
-- create alarm test page view
|
||||
---@param root Container parent
|
||||
local function new_view(root)
|
||||
local db = iocontrol.get_db()
|
||||
local ps = db.ps
|
||||
local ttest = db.diag.tone_test
|
||||
|
||||
local frame = Div{parent=root,x=1,y=1}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.ALARMS, frame, nil, true)
|
||||
|
||||
local main = Div{parent=frame,x=1,y=1}
|
||||
local page_div = Div{parent=main,y=2,width=main.get_width()}
|
||||
|
||||
--#region alarm testing
|
||||
|
||||
local alarm_page = app.new_page(nil, 1)
|
||||
alarm_page.tasks = { db.diag.tone_test.get_tone_states }
|
||||
|
||||
local alarms_div = Div{parent=page_div}
|
||||
|
||||
TextBox{parent=alarms_div,text="Alarm Sounder Tests",alignment=ALIGN.CENTER}
|
||||
|
||||
local alarm_ready_warn = TextBox{parent=alarms_div,y=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
|
||||
alarm_ready_warn.register(ps, "alarm_ready_warn", alarm_ready_warn.set_value)
|
||||
|
||||
local alarm_page_states = Div{parent=alarms_div,x=2,y=3,height=5,width=8}
|
||||
|
||||
TextBox{parent=alarm_page_states,text="States",alignment=ALIGN.CENTER}
|
||||
local ta_1 = IndicatorLight{parent=alarm_page_states,label="1",colors=c_blue_gray}
|
||||
local ta_2 = IndicatorLight{parent=alarm_page_states,label="2",colors=c_blue_gray}
|
||||
local ta_3 = IndicatorLight{parent=alarm_page_states,label="3",colors=c_blue_gray}
|
||||
local ta_4 = IndicatorLight{parent=alarm_page_states,label="4",colors=c_blue_gray}
|
||||
local ta_5 = IndicatorLight{parent=alarm_page_states,x=6,y=2,label="5",colors=c_blue_gray}
|
||||
local ta_6 = IndicatorLight{parent=alarm_page_states,x=6,label="6",colors=c_blue_gray}
|
||||
local ta_7 = IndicatorLight{parent=alarm_page_states,x=6,label="7",colors=c_blue_gray}
|
||||
local ta_8 = IndicatorLight{parent=alarm_page_states,x=6,label="8",colors=c_blue_gray}
|
||||
|
||||
local ta = { ta_1, ta_2, ta_3, ta_4, ta_5, ta_6, ta_7, ta_8 }
|
||||
|
||||
for i = 1, #ta do
|
||||
ta[i].register(ps, "alarm_tone_" .. i, ta[i].update)
|
||||
end
|
||||
|
||||
local alarms = Div{parent=alarms_div,x=11,y=3,height=15,fg_bg=cpair(colors.lightGray,colors.black)}
|
||||
|
||||
TextBox{parent=alarms,text="Alarms (\x13)",alignment=ALIGN.CENTER,fg_bg=alarms_div.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}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region direct tone testing
|
||||
|
||||
local tones_page = app.new_page(nil, 2)
|
||||
tones_page.tasks = { db.diag.tone_test.get_tone_states }
|
||||
|
||||
local tones_div = Div{parent=page_div}
|
||||
|
||||
TextBox{parent=tones_div,text="Alarm Sounder Tests",alignment=ALIGN.CENTER}
|
||||
|
||||
local tone_ready_warn = TextBox{parent=tones_div,y=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
|
||||
tone_ready_warn.register(ps, "alarm_ready_warn", tone_ready_warn.set_value)
|
||||
|
||||
local tone_page_states = Div{parent=tones_div,x=3,y=3,height=5,width=8}
|
||||
|
||||
TextBox{parent=tone_page_states,text="States",alignment=ALIGN.CENTER}
|
||||
local tt_1 = IndicatorLight{parent=tone_page_states,label="1",colors=c_blue_gray}
|
||||
local tt_2 = IndicatorLight{parent=tone_page_states,label="2",colors=c_blue_gray}
|
||||
local tt_3 = IndicatorLight{parent=tone_page_states,label="3",colors=c_blue_gray}
|
||||
local tt_4 = IndicatorLight{parent=tone_page_states,label="4",colors=c_blue_gray}
|
||||
local tt_5 = IndicatorLight{parent=tone_page_states,x=6,y=2,label="5",colors=c_blue_gray}
|
||||
local tt_6 = IndicatorLight{parent=tone_page_states,x=6,label="6",colors=c_blue_gray}
|
||||
local tt_7 = IndicatorLight{parent=tone_page_states,x=6,label="7",colors=c_blue_gray}
|
||||
local tt_8 = IndicatorLight{parent=tone_page_states,x=6,label="8",colors=c_blue_gray}
|
||||
|
||||
local tt = { tt_1, tt_2, tt_3, tt_4, tt_5, tt_6, tt_7, tt_8 }
|
||||
|
||||
for i = 1, #tt do
|
||||
tt[i].register(ps, "alarm_tone_" .. i, tt[i].update)
|
||||
end
|
||||
|
||||
local tones = Div{parent=tones_div,x=14,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)}
|
||||
|
||||
TextBox{parent=tones,text="Tones",alignment=ALIGN.CENTER,fg_bg=tones_div.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}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region info page
|
||||
|
||||
app.new_page(nil, 3)
|
||||
|
||||
local info_div = Div{parent=page_div}
|
||||
|
||||
TextBox{parent=info_div,x=2,y=1,text="This app provides tools to test alarm sounds by alarm and by tone (1-8)."}
|
||||
TextBox{parent=info_div,x=2,y=6,text="The system must be idle (all units stopped with no alarms active) for testing to run."}
|
||||
TextBox{parent=info_div,x=2,y=12,text="Currently, testing will be denied unless you have a Facility Authentication Key set (this will change in the future)."}
|
||||
|
||||
--#endregion
|
||||
|
||||
-- setup multipane
|
||||
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes={alarms_div,tones_div,info_div}}
|
||||
app.set_root_pane(u_pane)
|
||||
|
||||
local list = {
|
||||
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
|
||||
{ label = " \x13 ", color = core.cpair(colors.black, colors.red), callback = function () app.switcher(1) end },
|
||||
{ label = " \x0f ", color = core.cpair(colors.black, colors.yellow), callback = function () app.switcher(2) end },
|
||||
{ label = " ? ", color = core.cpair(colors.black, colors.blue), callback = function () app.switcher(3) end }
|
||||
}
|
||||
|
||||
app.set_sidebar(list)
|
||||
end
|
||||
|
||||
return new_view
|
||||
297
pocket/ui/apps/comps.lua
Normal file
297
pocket/ui/apps/comps.lua
Normal file
@@ -0,0 +1,297 @@
|
||||
--
|
||||
-- Computer List App
|
||||
--
|
||||
|
||||
local comms = require("scada-common.comms")
|
||||
local const = require("scada-common.constants")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.Div")
|
||||
local ListBox = require("graphics.elements.ListBox")
|
||||
local MultiPane = require("graphics.elements.MultiPane")
|
||||
local Rectangle = require("graphics.elements.Rectangle")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local WaitingAnim = require("graphics.elements.animations.Waiting")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
|
||||
|
||||
local DEV_TYPE = comms.DEVICE_TYPE
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local APP_ID = pocket.APP_ID
|
||||
|
||||
local lu_col = style.label_unit_pair
|
||||
local box_label = cpair(colors.lightGray, colors.gray)
|
||||
|
||||
-- new computer list page view
|
||||
---@param root Container parent
|
||||
local function new_view(root)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local frame = Div{parent=root,x=1,y=1}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.COMPS, frame, nil, true, false)
|
||||
|
||||
local load_div = Div{parent=frame,x=1,y=1}
|
||||
local main = Div{parent=frame,x=1,y=1}
|
||||
|
||||
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
|
||||
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.orange,colors._INHERIT)}
|
||||
|
||||
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
|
||||
|
||||
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
|
||||
|
||||
local page_div = nil ---@type Div|nil
|
||||
|
||||
-- load the app (create the elements)
|
||||
local function load()
|
||||
local ps = db.ps
|
||||
|
||||
page_div = Div{parent=main,y=2,width=main.get_width()}
|
||||
|
||||
local panes = {} ---@type Div[]
|
||||
|
||||
-- create all page divs
|
||||
for _ = 1, 4 do
|
||||
local div = Div{parent=page_div}
|
||||
table.insert(panes, div)
|
||||
end
|
||||
|
||||
local last_update = 0
|
||||
-- refresh data callback, every 1s it will re-send the query
|
||||
local function update()
|
||||
if util.time_ms() - last_update >= 1000 then
|
||||
db.diag.get_comps()
|
||||
last_update = util.time_ms()
|
||||
end
|
||||
end
|
||||
|
||||
-- create indicators for the ID, firmware, and RTT
|
||||
---@param pfx string
|
||||
---@param rect Rectangle
|
||||
local function create_common_indicators(pfx, rect)
|
||||
local first = TextBox{parent=rect,text="Computer",fg_bg=box_label}
|
||||
TextBox{parent=rect,text="Firmware",fg_bg=box_label}
|
||||
TextBox{parent=rect,text="RTT (ms)",fg_bg=box_label}
|
||||
|
||||
local y = first.get_y()
|
||||
local addr = TextBox{parent=rect,x=10,y=y,text="---"}
|
||||
local fw = TextBox{parent=rect,x=10,y=y+1,text="---"}
|
||||
local rtt = TextBox{parent=rect,x=10,y=y+2,text="---"}
|
||||
|
||||
addr.register(ps, pfx .. "_addr", function (v) addr.set_value(util.strval(v)) end)
|
||||
fw.register(ps, pfx .. "_fw", function (v) fw.set_value(util.strval(v)) end)
|
||||
|
||||
rtt.register(ps, pfx .. "_rtt", function (value)
|
||||
rtt.set_value(util.strval(value))
|
||||
|
||||
if value == "---" then
|
||||
rtt.recolor(colors.white)
|
||||
elseif value > const.HIGH_RTT then
|
||||
rtt.recolor(colors.red)
|
||||
elseif value > const.WARN_RTT then
|
||||
rtt.recolor(colors.yellow)
|
||||
else
|
||||
rtt.recolor(colors.green)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--#region main computer page
|
||||
|
||||
local m_div = Div{parent=panes[1],x=2,width=main.get_width()-2}
|
||||
|
||||
local main_page = app.new_page(nil, 1)
|
||||
main_page.tasks = { update }
|
||||
|
||||
TextBox{parent=m_div,y=1,text="Connected Computers",alignment=ALIGN.CENTER}
|
||||
|
||||
local conns = DataIndicator{parent=m_div,y=3,lu_colors=lu_col,label="Total Online",unit="",format="%8d",value=0,commas=true,width=21}
|
||||
conns.register(ps, "comp_online", conns.update)
|
||||
|
||||
local svr_div = Div{parent=m_div,y=4,height=6}
|
||||
local svr_rect = Rectangle{parent=svr_div,height=6,width=22,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=svr_rect,text="Supervisor"}
|
||||
TextBox{parent=svr_rect,text="Status",fg_bg=box_label}
|
||||
TextBox{parent=svr_rect,x=10,y=2,text="Online",fg_bg=cpair(colors.green,colors._INHERIT)}
|
||||
TextBox{parent=svr_rect,text="Computer",fg_bg=box_label}
|
||||
TextBox{parent=svr_rect,text="Firmware",fg_bg=box_label}
|
||||
local svr_addr = TextBox{parent=svr_rect,x=10,y=3,text="?"}
|
||||
local svr_fw = TextBox{parent=svr_rect,x=10,y=4,text="?"}
|
||||
|
||||
svr_addr.register(ps, "comp_svr_addr", function (v) svr_addr.set_value(util.strval(v)) end)
|
||||
svr_fw.register(ps, "comp_svr_fw", function (v) svr_fw.set_value(util.strval(v)) end)
|
||||
|
||||
local crd_div = Div{parent=m_div,y=11,height=7}
|
||||
local crd_rect = Rectangle{parent=crd_div,height=7,width=21,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=crd_rect,text="Coordinator"}
|
||||
TextBox{parent=crd_rect,text="Status",fg_bg=box_label}
|
||||
local crd_online = TextBox{parent=crd_rect,x=10,y=2,width=8,text="Off-line",fg_bg=cpair(colors.red,colors._INHERIT)}
|
||||
|
||||
create_common_indicators("comp_crd", crd_rect)
|
||||
|
||||
crd_online.register(ps, "comp_crd_online", function (online)
|
||||
if online then
|
||||
crd_online.recolor(colors.green)
|
||||
crd_online.set_value("Online")
|
||||
else
|
||||
crd_online.recolor(colors.red)
|
||||
crd_online.set_value("Off-line")
|
||||
end
|
||||
end)
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region PLC page
|
||||
|
||||
local p_div = Div{parent=panes[2],width=main.get_width()}
|
||||
|
||||
local plc_page = app.new_page(nil, 2)
|
||||
plc_page.tasks = { update }
|
||||
|
||||
TextBox{parent=p_div,y=1,text="PLC Devices",alignment=ALIGN.CENTER}
|
||||
|
||||
local plc_list = ListBox{parent=p_div,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
|
||||
local plc_elems = {} ---@type graphics_element[]
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region RTU gateway page
|
||||
|
||||
local r_div = Div{parent=panes[3],width=main.get_width()}
|
||||
|
||||
local rtu_page = app.new_page(nil, 3)
|
||||
rtu_page.tasks = { update }
|
||||
|
||||
TextBox{parent=r_div,y=1,text="RTU Gateway Devices",alignment=ALIGN.CENTER}
|
||||
|
||||
local rtu_list = ListBox{parent=r_div,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
|
||||
local rtu_elems = {} ---@type graphics_element[]
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region pocket computer page
|
||||
|
||||
local pk_div = Div{parent=panes[4],width=main.get_width()}
|
||||
|
||||
local pkt_page = app.new_page(nil, 4)
|
||||
pkt_page.tasks = { update }
|
||||
|
||||
TextBox{parent=pk_div,y=1,text="Pocket Devices",alignment=ALIGN.CENTER}
|
||||
|
||||
local pkt_list = ListBox{parent=pk_div,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
|
||||
local pkt_elems = {} ---@type graphics_element[]
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region connect/disconnect management
|
||||
|
||||
ps.subscribe("comp_connect", function (id)
|
||||
if id == false then return end
|
||||
|
||||
local pfx = "comp_" .. id
|
||||
local type = ps.get(pfx .. "_type")
|
||||
|
||||
if type == DEV_TYPE.PLC then
|
||||
plc_elems[id] = Div{parent=plc_list,height=7}
|
||||
local rect = Rectangle{parent=plc_elems[id],height=6,x=2,width=20,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
local title = TextBox{parent=rect,text="PLC (Unit ?)"}
|
||||
title.register(ps, pfx .. "_unit", function (unit) title.set_value("PLC (Unit " .. unit .. ")") end)
|
||||
|
||||
create_common_indicators(pfx, rect)
|
||||
elseif type == DEV_TYPE.RTU then
|
||||
rtu_elems[id] = Div{parent=rtu_list,height=7}
|
||||
local rect = Rectangle{parent=rtu_elems[id],height=6,x=2,width=20,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=rect,text="RTU Gateway"}
|
||||
|
||||
create_common_indicators(pfx, rect)
|
||||
elseif type == DEV_TYPE.PKT then
|
||||
pkt_elems[id] = Div{parent=pkt_list,height=7}
|
||||
local rect = Rectangle{parent=pkt_elems[id],height=6,x=2,width=20,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=rect,text="Pocket Computer"}
|
||||
|
||||
create_common_indicators(pfx, rect)
|
||||
end
|
||||
end)
|
||||
|
||||
ps.subscribe("comp_disconnect", function (id)
|
||||
if id == false then return end
|
||||
|
||||
local type = ps.get("comp_" ..id .. "_type")
|
||||
|
||||
if type == DEV_TYPE.PLC then
|
||||
if plc_elems[id] then plc_elems[id].delete() end
|
||||
plc_elems[id] = nil
|
||||
elseif type == DEV_TYPE.RTU then
|
||||
if rtu_elems[id] then rtu_elems[id].delete() end
|
||||
rtu_elems[id] = nil
|
||||
elseif type == DEV_TYPE.PKT then
|
||||
if pkt_elems[id] then pkt_elems[id].delete() end
|
||||
pkt_elems[id] = nil
|
||||
end
|
||||
end)
|
||||
|
||||
--#endregion
|
||||
|
||||
-- setup multipane
|
||||
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
app.set_root_pane(u_pane)
|
||||
|
||||
-- setup sidebar
|
||||
|
||||
local list = {
|
||||
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
|
||||
{ label = " @ ", color = core.cpair(colors.black, colors.blue), callback = main_page.nav_to },
|
||||
{ label = "PLC", color = core.cpair(colors.black, colors.red), callback = plc_page.nav_to },
|
||||
{ label = "RTU", color = core.cpair(colors.black, colors.orange), callback = rtu_page.nav_to },
|
||||
{ label = "PKT", color = core.cpair(colors.black, colors.lightGray), callback = pkt_page.nav_to }
|
||||
}
|
||||
|
||||
app.set_sidebar(list)
|
||||
|
||||
-- done, show the app
|
||||
main_page.nav_to()
|
||||
load_pane.set_value(2)
|
||||
end
|
||||
|
||||
-- delete the elements and switch back to the loading screen
|
||||
local function unload()
|
||||
if page_div then
|
||||
page_div.delete()
|
||||
page_div = nil
|
||||
end
|
||||
|
||||
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
|
||||
app.delete_pages()
|
||||
|
||||
-- show loading screen
|
||||
load_pane.set_value(1)
|
||||
|
||||
-- clear the list of connected computers so that connections re-appear on reload of this app
|
||||
iocontrol.rx.clear_comp_record()
|
||||
end
|
||||
|
||||
app.set_load(load)
|
||||
app.set_unload(unload)
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
233
pocket/ui/apps/control.lua
Normal file
233
pocket/ui/apps/control.lua
Normal file
@@ -0,0 +1,233 @@
|
||||
--
|
||||
-- Facility & Unit Control App
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local pocket = require("pocket.pocket")
|
||||
local process = require("pocket.process")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.Div")
|
||||
local MultiPane = require("graphics.elements.MultiPane")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local WaitingAnim = require("graphics.elements.animations.Waiting")
|
||||
|
||||
local HazardButton = require("graphics.elements.controls.HazardButton")
|
||||
local PushButton = require("graphics.elements.controls.PushButton")
|
||||
|
||||
local NumberField = require("graphics.elements.form.NumberField")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
|
||||
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
|
||||
|
||||
local AUTO_GROUP = types.AUTO_GROUP
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
local cpair = core.cpair
|
||||
|
||||
local APP_ID = pocket.APP_ID
|
||||
|
||||
local label_fg_bg = style.label
|
||||
local lu_col = style.label_unit_pair
|
||||
local text_fg = style.text_fg
|
||||
|
||||
local mode_states = style.icon_states.mode_states
|
||||
|
||||
local btn_active = cpair(colors.white, colors.black)
|
||||
local hzd_fg_bg = style.hzd_fg_bg
|
||||
local hzd_dis_colors = style.hzd_dis_colors
|
||||
|
||||
-- new unit control page view
|
||||
---@param root Container parent
|
||||
local function new_view(root)
|
||||
local btn_fg_bg = cpair(colors.green, colors.black)
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local frame = Div{parent=root,x=1,y=1}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.CONTROL, frame, nil, false, true)
|
||||
|
||||
local load_div = Div{parent=frame,x=1,y=1}
|
||||
local main = Div{parent=frame,x=1,y=1}
|
||||
|
||||
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
|
||||
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.green,colors._INHERIT)}
|
||||
|
||||
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
|
||||
|
||||
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
|
||||
|
||||
local page_div = nil ---@type Div|nil
|
||||
|
||||
-- set sidebar to display unit-specific fields based on a specified unit
|
||||
local function set_sidebar()
|
||||
local list = {
|
||||
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
|
||||
{ label = "FAC", color = core.cpair(colors.black, colors.orange), callback = function () app.switcher(db.facility.num_units + 1) end }
|
||||
}
|
||||
|
||||
for i = 1, db.facility.num_units do
|
||||
table.insert(list, { label = "U-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = function () app.switcher(i) end })
|
||||
end
|
||||
|
||||
app.set_sidebar(list)
|
||||
end
|
||||
|
||||
-- load the app (create the elements)
|
||||
local function load()
|
||||
page_div = Div{parent=main,y=2,width=main.get_width()}
|
||||
|
||||
local panes = {} ---@type Div[]
|
||||
|
||||
local active_unit = 1
|
||||
|
||||
-- create all page divs
|
||||
for _ = 1, db.facility.num_units + 1 do
|
||||
local div = Div{parent=page_div}
|
||||
table.insert(panes, div)
|
||||
end
|
||||
|
||||
-- previous unit
|
||||
local function prev(x)
|
||||
active_unit = util.trinary(x == 1, db.facility.num_units, x - 1)
|
||||
app.switcher(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)
|
||||
end
|
||||
|
||||
local last_update = 0
|
||||
-- refresh data callback, every 500ms it will re-send the query
|
||||
local function update()
|
||||
if util.time_ms() - last_update >= 500 then
|
||||
db.api.get_ctrl()
|
||||
last_update = util.time_ms()
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, db.facility.num_units do
|
||||
local u_pane = panes[i]
|
||||
local u_div = Div{parent=u_pane,x=2,width=main.get_width()-2}
|
||||
local unit = db.units[i]
|
||||
local u_ps = unit.unit_ps
|
||||
|
||||
local u_page = app.new_page(nil, i)
|
||||
u_page.tasks = { update }
|
||||
|
||||
TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,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 rate = DataIndicator{parent=u_div,y=3,lu_colors=lu_col,label="Burn",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=db.temp_label,format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
|
||||
|
||||
local ctrl = IconIndicator{parent=u_div,x=1,y=6,label="Control State",states=mode_states}
|
||||
|
||||
rate.register(u_ps, "act_burn_rate", rate.update)
|
||||
temp.register(u_ps, "temp", function (t) temp.update(db.temp_convert(t)) end)
|
||||
ctrl.register(u_ps, "U_ControlStatus", ctrl.update)
|
||||
|
||||
u_div.line_break()
|
||||
|
||||
TextBox{parent=u_div,y=8,text="CMD",width=4,fg_bg=label_fg_bg}
|
||||
TextBox{parent=u_div,x=14,y=8,text="mB/t",width=4,fg_bg=label_fg_bg}
|
||||
local burn_cmd = NumberField{parent=u_div,x=5,y=8,width=8,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=style.field,dis_fg_bg=style.field_disable}
|
||||
|
||||
local set_burn = function () unit.set_burn(burn_cmd.get_numeric()) end
|
||||
local set_burn_btn = PushButton{parent=u_div,x=19,y=8,text="SET",min_width=5,fg_bg=cpair(colors.green,colors.black),active_fg_bg=cpair(colors.white,colors.black),dis_fg_bg=style.btn_disable,callback=set_burn}
|
||||
|
||||
-- enable/disable controls based on group assignment (start button is separate)
|
||||
burn_cmd.register(u_ps, "auto_group_id", function (gid)
|
||||
if gid == AUTO_GROUP.MANUAL then burn_cmd.enable() else burn_cmd.disable() end
|
||||
end)
|
||||
set_burn_btn.register(u_ps, "auto_group_id", function (gid)
|
||||
if gid == AUTO_GROUP.MANUAL then set_burn_btn.enable() else set_burn_btn.disable() end
|
||||
end)
|
||||
|
||||
burn_cmd.register(u_ps, "burn_rate", burn_cmd.set_value)
|
||||
burn_cmd.register(u_ps, "max_burn", burn_cmd.set_max)
|
||||
|
||||
local start = HazardButton{parent=u_div,x=2,y=11,text="START",accent=colors.lightBlue,callback=unit.start,timeout=3,fg_bg=hzd_fg_bg,dis_colors=hzd_dis_colors}
|
||||
local ack_a = HazardButton{parent=u_div,x=12,y=11,text="ACK \x13",accent=colors.orange,callback=unit.ack_alarms,timeout=3,fg_bg=hzd_fg_bg,dis_colors=hzd_dis_colors}
|
||||
local scram = HazardButton{parent=u_div,x=2,y=15,text="SCRAM",accent=colors.yellow,callback=unit.scram,timeout=3,fg_bg=hzd_fg_bg,dis_colors=hzd_dis_colors}
|
||||
local reset = HazardButton{parent=u_div,x=12,y=15,text="RESET",accent=colors.red,callback=unit.reset_rps,timeout=3,fg_bg=hzd_fg_bg,dis_colors=hzd_dis_colors}
|
||||
|
||||
unit.start_ack = start.on_response
|
||||
unit.ack_alarms_ack = ack_a.on_response
|
||||
unit.scram_ack = scram.on_response
|
||||
unit.reset_rps_ack = reset.on_response
|
||||
|
||||
local function start_button_en_check()
|
||||
local can_start = (not unit.reactor_data.mek_status.status) and
|
||||
(not unit.reactor_data.rps_tripped) and
|
||||
(unit.a_group == AUTO_GROUP.MANUAL)
|
||||
if can_start then start.enable() else start.disable() end
|
||||
end
|
||||
|
||||
start.register(u_ps, "status", start_button_en_check)
|
||||
start.register(u_ps, "rps_tripped", start_button_en_check)
|
||||
start.register(u_ps, "auto_group_id", start_button_en_check)
|
||||
start.register(u_ps, "AutoControl", start_button_en_check)
|
||||
|
||||
reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
|
||||
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- facility controls
|
||||
|
||||
local f_pane = panes[db.facility.num_units + 1]
|
||||
local f_div = Div{parent=f_pane,x=2,width=main.get_width()-2}
|
||||
|
||||
app.new_page(nil, db.facility.num_units + 1)
|
||||
|
||||
TextBox{parent=f_div,y=1,text="Facility Commands",alignment=ALIGN.CENTER}
|
||||
|
||||
local scram = HazardButton{parent=f_div,x=5,y=6,text="FAC SCRAM",accent=colors.yellow,dis_colors=hzd_dis_colors,callback=process.fac_scram,timeout=3,fg_bg=hzd_fg_bg}
|
||||
local ack_a = HazardButton{parent=f_div,x=7,y=11,text="ACK \x13",accent=colors.orange,dis_colors=hzd_dis_colors,callback=process.fac_ack_alarms,timeout=3,fg_bg=hzd_fg_bg}
|
||||
|
||||
db.facility.scram_ack = scram.on_response
|
||||
db.facility.ack_alarms_ack = ack_a.on_response
|
||||
|
||||
-- setup multipane
|
||||
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
app.set_root_pane(u_pane)
|
||||
|
||||
set_sidebar()
|
||||
|
||||
-- done, show the app
|
||||
load_pane.set_value(2)
|
||||
end
|
||||
|
||||
-- delete the elements and switch back to the loading screen
|
||||
local function unload()
|
||||
if page_div then
|
||||
page_div.delete()
|
||||
page_div = nil
|
||||
end
|
||||
|
||||
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
|
||||
app.delete_pages()
|
||||
|
||||
-- show loading screen
|
||||
load_pane.set_value(1)
|
||||
end
|
||||
|
||||
app.set_load(load)
|
||||
app.set_unload(unload)
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
@@ -1,116 +0,0 @@
|
||||
--
|
||||
-- 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
|
||||
@@ -1,26 +0,0 @@
|
||||
--
|
||||
-- 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
|
||||
258
pocket/ui/apps/facility.lua
Normal file
258
pocket/ui/apps/facility.lua
Normal file
@@ -0,0 +1,258 @@
|
||||
--
|
||||
-- Facility Overview App
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
local dyn_tank = require("pocket.ui.pages.dynamic_tank")
|
||||
local facility_sps = require("pocket.ui.pages.facility_sps")
|
||||
local induction_mtx = require("pocket.ui.pages.facility_matrix")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.Div")
|
||||
local MultiPane = require("graphics.elements.MultiPane")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local WaitingAnim = require("graphics.elements.animations.Waiting")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
|
||||
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
local cpair = core.cpair
|
||||
|
||||
local APP_ID = pocket.APP_ID
|
||||
|
||||
local label_fg_bg = style.label
|
||||
local lu_col = style.label_unit_pair
|
||||
|
||||
local basic_states = style.icon_states.basic_states
|
||||
local mode_states = style.icon_states.mode_states
|
||||
local red_ind_s = style.icon_states.red_ind_s
|
||||
local yel_ind_s = style.icon_states.yel_ind_s
|
||||
local grn_ind_s = style.icon_states.grn_ind_s
|
||||
|
||||
-- new unit page view
|
||||
---@param root Container parent
|
||||
local function new_view(root)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local frame = Div{parent=root,x=1,y=1}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.FACILITY, frame, nil, false, true)
|
||||
|
||||
local load_div = Div{parent=frame,x=1,y=1}
|
||||
local main = Div{parent=frame,x=1,y=1}
|
||||
|
||||
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
|
||||
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.orange,colors._INHERIT)}
|
||||
|
||||
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
|
||||
|
||||
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
|
||||
|
||||
local tank_page_navs = {}
|
||||
local page_div = nil ---@type Div|nil
|
||||
|
||||
-- load the app (create the elements)
|
||||
local function load()
|
||||
local fac = db.facility
|
||||
local f_ps = fac.ps
|
||||
|
||||
page_div = Div{parent=main,y=2,width=main.get_width()}
|
||||
|
||||
local panes = {} ---@type Div[]
|
||||
|
||||
-- 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_fac()
|
||||
last_update = util.time_ms()
|
||||
end
|
||||
end
|
||||
|
||||
--#region facility overview
|
||||
|
||||
local main_pane = Div{parent=page_div}
|
||||
local f_div = Div{parent=main_pane,x=2,width=main.get_width()-2}
|
||||
table.insert(panes, main_pane)
|
||||
|
||||
local fac_page = app.new_page(nil, #panes)
|
||||
fac_page.tasks = { update }
|
||||
|
||||
TextBox{parent=f_div,y=1,text="Facility",alignment=ALIGN.CENTER}
|
||||
|
||||
local mtx_state = IconIndicator{parent=f_div,y=3,label="Matrix Status",states=basic_states}
|
||||
local sps_state = IconIndicator{parent=f_div,label="SPS Status",states=basic_states}
|
||||
mtx_state.register(fac.induction_ps_tbl[1], "InductionMatrixStatus", mtx_state.update)
|
||||
sps_state.register(fac.sps_ps_tbl[1], "SPSStatus", sps_state.update)
|
||||
|
||||
TextBox{parent=f_div,y=6,text="RTU Gateways",fg_bg=label_fg_bg}
|
||||
local rtu_count = DataIndicator{parent=f_div,x=19,y=6,label="",format="%3d",value=0,lu_colors=lu_col,width=3}
|
||||
rtu_count.register(f_ps, "rtu_count", rtu_count.update)
|
||||
|
||||
TextBox{parent=f_div,y=8,text="Induction Matrix",alignment=ALIGN.CENTER}
|
||||
|
||||
local eta = TextBox{parent=f_div,x=1,y=10,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,colors.gray)}
|
||||
eta.register(fac.induction_ps_tbl[1], "eta_string", eta.set_value)
|
||||
|
||||
TextBox{parent=f_div,y=12,text="Unit Statuses",alignment=ALIGN.CENTER}
|
||||
|
||||
f_div.line_break()
|
||||
|
||||
for i = 1, fac.num_units do
|
||||
local ctrl = IconIndicator{parent=f_div,label="U"..i.." Control State",states=mode_states}
|
||||
ctrl.register(db.units[i].unit_ps, "U_ControlStatus", ctrl.update)
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region facility annunciator
|
||||
|
||||
local a_pane = Div{parent=page_div}
|
||||
local a_div = Div{parent=a_pane,x=2,width=main.get_width()-2}
|
||||
table.insert(panes, a_pane)
|
||||
|
||||
local annunc_page = app.new_page(nil, #panes)
|
||||
annunc_page.tasks = { update }
|
||||
|
||||
TextBox{parent=a_div,y=1,text="Annunciator",alignment=ALIGN.CENTER}
|
||||
|
||||
local all_ok = IconIndicator{parent=a_div,y=3,label="Units Online",states=grn_ind_s}
|
||||
local ind_mat = IconIndicator{parent=a_div,label="Induction Matrix",states=grn_ind_s}
|
||||
local sps = IconIndicator{parent=a_div,label="SPS Connected",states=grn_ind_s}
|
||||
|
||||
all_ok.register(f_ps, "all_sys_ok", all_ok.update)
|
||||
ind_mat.register(fac.induction_ps_tbl[1], "InductionMatrixStateStatus", function (status) ind_mat.update(status > 1) end)
|
||||
sps.register(fac.sps_ps_tbl[1], "SPSStateStatus", function (status) sps.update(status > 1) end)
|
||||
|
||||
a_div.line_break()
|
||||
|
||||
local auto_scram = IconIndicator{parent=a_div,label="Automatic SCRAM",states=red_ind_s}
|
||||
local matrix_flt = IconIndicator{parent=a_div,label="Ind. Matrix Fault",states=yel_ind_s}
|
||||
local matrix_fill = IconIndicator{parent=a_div,label="Matrix Charge Hi",states=red_ind_s}
|
||||
local unit_crit = IconIndicator{parent=a_div,label="Unit Crit. Alarm",states=red_ind_s}
|
||||
local fac_rad_h = IconIndicator{parent=a_div,label="FAC Radiation Hi",states=red_ind_s}
|
||||
local gen_fault = IconIndicator{parent=a_div,label="Gen Control Fault",states=yel_ind_s}
|
||||
|
||||
auto_scram.register(f_ps, "auto_scram", auto_scram.update)
|
||||
matrix_flt.register(f_ps, "as_matrix_fault", matrix_flt.update)
|
||||
matrix_fill.register(f_ps, "as_matrix_fill", matrix_fill.update)
|
||||
unit_crit.register(f_ps, "as_crit_alarm", unit_crit.update)
|
||||
fac_rad_h.register(f_ps, "as_radiation", fac_rad_h.update)
|
||||
gen_fault.register(f_ps, "as_gen_fault", gen_fault.update)
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region induction matrix
|
||||
|
||||
local mtx_page_nav = induction_mtx(app, panes, Div{parent=page_div}, fac.induction_ps_tbl[1], update)
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region SPS
|
||||
|
||||
local sps_page_nav = facility_sps(app, panes, Div{parent=page_div}, fac.sps_ps_tbl[1], update)
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region facility tank pages
|
||||
|
||||
local t_pane = Div{parent=page_div}
|
||||
local t_div = Div{parent=t_pane,x=2,width=main.get_width()-2}
|
||||
table.insert(panes, t_pane)
|
||||
|
||||
local tank_page = app.new_page(nil, #panes)
|
||||
tank_page.tasks = { update }
|
||||
|
||||
TextBox{parent=t_div,y=1,text="Facility Tanks",alignment=ALIGN.CENTER}
|
||||
|
||||
local f_tank_id = 1
|
||||
for t = 1, #fac.tank_list do
|
||||
if fac.tank_list[t] == 1 then
|
||||
t_div.line_break()
|
||||
|
||||
local tank = IconIndicator{parent=t_div,x=1,label="Unit Tank "..t.." (U-"..t..")",states=basic_states}
|
||||
tank.register(db.units[t].tank_ps_tbl[1], "DynamicTankStatus", tank.update)
|
||||
|
||||
TextBox{parent=t_div,x=5,text="\x07 Unit "..t,fg_bg=label_fg_bg}
|
||||
elseif fac.tank_list[t] == 2 then
|
||||
tank_page_navs[f_tank_id] = dyn_tank(app, nil, panes, Div{parent=page_div}, t, fac.tank_ps_tbl[f_tank_id], update)
|
||||
|
||||
t_div.line_break()
|
||||
|
||||
local tank = IconIndicator{parent=t_div,x=1,label="Fac. Tank "..f_tank_id.." (F-"..f_tank_id..")",states=basic_states}
|
||||
tank.register(fac.tank_ps_tbl[f_tank_id], "DynamicTankStatus", tank.update)
|
||||
|
||||
local connections = ""
|
||||
for i = 1, #fac.tank_conns do
|
||||
if fac.tank_conns[i] == t then
|
||||
if connections ~= "" then
|
||||
connections = connections .. "\n\x07 Unit " .. i
|
||||
else
|
||||
connections = "\x07 Unit " .. i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
TextBox{parent=t_div,x=5,text=connections,fg_bg=label_fg_bg}
|
||||
|
||||
f_tank_id = f_tank_id + 1
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- setup multipane
|
||||
local f_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
app.set_root_pane(f_pane)
|
||||
|
||||
-- setup sidebar
|
||||
|
||||
local list = {
|
||||
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
|
||||
{ label = "FAC", tall = true, color = core.cpair(colors.black, colors.orange), callback = fac_page.nav_to },
|
||||
{ label = "ANN", color = core.cpair(colors.black, colors.yellow), callback = annunc_page.nav_to },
|
||||
{ label = "MTX", color = core.cpair(colors.black, colors.white), callback = mtx_page_nav },
|
||||
{ label = "SPS", color = core.cpair(colors.black, colors.purple), callback = sps_page_nav },
|
||||
{ label = "TNK", tall = true, color = core.cpair(colors.black, colors.blue), callback = tank_page.nav_to }
|
||||
}
|
||||
|
||||
for i = 1, #fac.tank_data_tbl do
|
||||
table.insert(list, { label = "F-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = tank_page_navs[i] })
|
||||
end
|
||||
|
||||
app.set_sidebar(list)
|
||||
|
||||
-- done, show the app
|
||||
load_pane.set_value(2)
|
||||
end
|
||||
|
||||
-- delete the elements and switch back to the loading screen
|
||||
local function unload()
|
||||
if page_div then
|
||||
page_div.delete()
|
||||
page_div = nil
|
||||
end
|
||||
|
||||
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
|
||||
app.delete_pages()
|
||||
|
||||
-- show loading screen
|
||||
load_pane.set_value(1)
|
||||
end
|
||||
|
||||
app.set_load(load)
|
||||
app.set_unload(unload)
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
331
pocket/ui/apps/guide.lua
Normal file
331
pocket/ui/apps/guide.lua
Normal file
@@ -0,0 +1,331 @@
|
||||
--
|
||||
-- System Guide
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
local log = require("scada-common.log")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local docs = require("pocket.ui.docs")
|
||||
|
||||
local guide_section = require("pocket.ui.pages.guide_section")
|
||||
|
||||
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 WaitingAnim = require("graphics.elements.animations.Waiting")
|
||||
|
||||
local PushButton = require("graphics.elements.controls.PushButton")
|
||||
|
||||
local TextField = require("graphics.elements.form.TextField")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
local cpair = core.cpair
|
||||
|
||||
local APP_ID = pocket.APP_ID
|
||||
|
||||
-- new system guide view
|
||||
---@param root Container parent
|
||||
local function new_view(root)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local frame = Div{parent=root,x=1,y=1}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.GUIDE, frame)
|
||||
|
||||
local load_div = Div{parent=frame,x=1,y=1}
|
||||
local main = Div{parent=frame,x=1,y=1}
|
||||
|
||||
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.cyan,colors._INHERIT)}
|
||||
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
|
||||
local load_text_1 = TextBox{parent=load_div,y=14,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.lightGray,colors._INHERIT)}
|
||||
local load_text_2 = TextBox{parent=load_div,y=15,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.lightGray,colors._INHERIT)}
|
||||
|
||||
-- give more detailed information so the user doesn't give up
|
||||
local function load_text(a, b)
|
||||
if a then load_text_1.set_value(a) end
|
||||
load_text_2.set_value(b or "")
|
||||
end
|
||||
|
||||
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
|
||||
|
||||
local btn_fg_bg = cpair(colors.cyan, colors.black)
|
||||
local btn_active = cpair(colors.white, colors.black)
|
||||
|
||||
app.set_sidebar({{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home }})
|
||||
|
||||
local page_div = nil ---@type Div|nil
|
||||
|
||||
-- load the app (create the elements)
|
||||
local function load()
|
||||
local list = {
|
||||
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
|
||||
{ label = " \x14 ", color = core.cpair(colors.black, colors.cyan), callback = function () app.switcher(1) end },
|
||||
{ label = "__?", color = core.cpair(colors.black, colors.lightGray), callback = function () app.switcher(2) end }
|
||||
}
|
||||
|
||||
app.set_sidebar(list)
|
||||
|
||||
page_div = Div{parent=main,y=2}
|
||||
local p_width = page_div.get_width() - 1
|
||||
|
||||
local main_page = app.new_page(nil, 1)
|
||||
local search_page = app.new_page(main_page, 2)
|
||||
local use_page = app.new_page(main_page, 3)
|
||||
local uis_page = app.new_page(main_page, 4)
|
||||
local fps_page = app.new_page(main_page, 5)
|
||||
local gls_page = app.new_page(main_page, 6)
|
||||
local lnk_page = app.new_page(main_page, 7)
|
||||
|
||||
local home = Div{parent=page_div,x=2}
|
||||
local search = Div{parent=page_div,x=2}
|
||||
local use = Div{parent=page_div,x=2,width=p_width}
|
||||
local uis = Div{parent=page_div,x=2,width=p_width}
|
||||
local fps = Div{parent=page_div,x=2,width=p_width}
|
||||
local gls = Div{parent=page_div,x=2,width=p_width}
|
||||
local lnk = Div{parent=page_div,x=2,width=p_width}
|
||||
local panes = { home, search, use, uis, fps, gls, lnk } ---@type Div[]
|
||||
|
||||
local doc_map = {} ---@type { [string]: function }
|
||||
local search_db = {} ---@type [ string, string, string, function ][]
|
||||
|
||||
local sect_construct_data = { app, page_div, panes, doc_map, search_db, btn_fg_bg, btn_active }
|
||||
|
||||
TextBox{parent=home,y=1,text="cc-mek-scada Guide",alignment=ALIGN.CENTER}
|
||||
|
||||
PushButton{parent=home,y=3,text="Search >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=search_page.nav_to}
|
||||
PushButton{parent=home,y=5,text="System Usage >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=use_page.nav_to}
|
||||
PushButton{parent=home,text="Operator UIs >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=uis_page.nav_to}
|
||||
PushButton{parent=home,text="Front Panels >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fps_page.nav_to}
|
||||
PushButton{parent=home,text="Glossary >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_page.nav_to}
|
||||
PushButton{parent=home,y=10,text="Wiki and Discord >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=lnk_page.nav_to}
|
||||
|
||||
load_text("Search")
|
||||
|
||||
TextBox{parent=search,y=1,text="Search",alignment=ALIGN.CENTER}
|
||||
|
||||
local query_field = TextField{parent=search,x=1,y=3,width=18,fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
local func_ref = {}
|
||||
|
||||
PushButton{parent=search,x=20,y=3,text="GO",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()func_ref.run_search()end}
|
||||
|
||||
local search_results = ListBox{parent=search,x=1,y=5,scroll_height=200,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
|
||||
|
||||
function func_ref.run_search()
|
||||
local query = string.lower(query_field.get_value())
|
||||
local s_results = { {}, {}, {}, {} } ---@type [ string, string, string, function ][][]
|
||||
|
||||
search_results.remove_all()
|
||||
|
||||
if string.len(query) < 2 then
|
||||
TextBox{parent=search_results,text="Search requires at least 2 characters."}
|
||||
return
|
||||
end
|
||||
|
||||
local start = util.time_ms()
|
||||
|
||||
for _, entry in ipairs(search_db) do
|
||||
local s_start, s_end = string.find(entry[1], query, 1, true)
|
||||
|
||||
if s_start == nil then
|
||||
elseif s_start == 1 then
|
||||
if s_end == string.len(entry[1]) then
|
||||
-- best match: full match
|
||||
table.insert(s_results[1], entry)
|
||||
else
|
||||
-- very good match, start of key
|
||||
table.insert(s_results[2], entry)
|
||||
end
|
||||
elseif string.sub(query, s_start - 1, s_start) == " " then
|
||||
-- start of word, good match
|
||||
table.insert(s_results[3], entry)
|
||||
else
|
||||
-- basic match in content
|
||||
table.insert(s_results[4], entry)
|
||||
end
|
||||
end
|
||||
|
||||
local empty = true
|
||||
|
||||
for tier = 1, 4 do
|
||||
for idx = 1, #s_results[tier] do
|
||||
local entry = s_results[tier][idx]
|
||||
TextBox{parent=search_results,text=entry[3].." >",fg_bg=cpair(colors.gray,colors.black)}
|
||||
PushButton{parent=search_results,text=entry[2],alignment=ALIGN.LEFT,fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=entry[4]}
|
||||
|
||||
empty = false
|
||||
end
|
||||
end
|
||||
|
||||
log.debug("App.Guide: search for \"" .. query .. "\" completed in " .. (util.time_ms() - start) .. "ms")
|
||||
|
||||
if empty then
|
||||
TextBox{parent=search_results,text="No results found."}
|
||||
end
|
||||
end
|
||||
|
||||
TextBox{parent=search_results,text="Click 'GO' to search..."}
|
||||
|
||||
util.nop()
|
||||
|
||||
load_text("System Usage")
|
||||
|
||||
TextBox{parent=use,y=1,text="System Usage",alignment=ALIGN.CENTER}
|
||||
PushButton{parent=use,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
|
||||
|
||||
load_text(false, "Connecting Devices")
|
||||
local conn_dev_page = guide_section(sect_construct_data, use_page, "Connecting Devs", docs.usage.conn, 110)
|
||||
load_text(false, "Configuring Devices")
|
||||
local config_dev_page = guide_section(sect_construct_data, use_page, "Configuring Devs", docs.usage.config, 350)
|
||||
load_text(false, "Manual Control")
|
||||
local man_ctrl_page = guide_section(sect_construct_data, use_page, "Manual Control", docs.usage.manual, 100)
|
||||
load_text(false, "Auto Control")
|
||||
local auto_ctrl_page = guide_section(sect_construct_data, use_page, "Auto Control", docs.usage.auto, 200)
|
||||
load_text(false, "Waste Control")
|
||||
local waste_ctrl_page = guide_section(sect_construct_data, use_page, "Waste Control", docs.usage.waste, 120)
|
||||
|
||||
PushButton{parent=use,y=3,text="Connecting Devices >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=conn_dev_page.nav_to}
|
||||
PushButton{parent=use,text="Configuring Devices >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=config_dev_page.nav_to}
|
||||
PushButton{parent=use,text="Manual Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=man_ctrl_page.nav_to}
|
||||
PushButton{parent=use,text="Automatic Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=auto_ctrl_page.nav_to}
|
||||
PushButton{parent=use,text="Waste Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=waste_ctrl_page.nav_to}
|
||||
|
||||
load_text("Operator UIs")
|
||||
|
||||
TextBox{parent=uis,y=1,text="Operator UIs",alignment=ALIGN.CENTER}
|
||||
PushButton{parent=uis,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
|
||||
|
||||
local annunc_page = app.new_page(uis_page, #panes + 1)
|
||||
local annunc_div = Div{parent=page_div,x=2}
|
||||
table.insert(panes, annunc_div)
|
||||
|
||||
local coord_page = app.new_page(uis_page, #panes + 1)
|
||||
local coord_div = Div{parent=page_div,x=2}
|
||||
table.insert(panes, coord_div)
|
||||
|
||||
load_text(false, "Alarms")
|
||||
|
||||
local alarms_page = guide_section(sect_construct_data, uis_page, "Alarms", docs.alarms, 100)
|
||||
|
||||
PushButton{parent=uis,y=3,text="Alarms >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=alarms_page.nav_to}
|
||||
PushButton{parent=uis,text="Annunciators >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=annunc_page.nav_to}
|
||||
PushButton{parent=uis,text="Coordinator UI >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=coord_page.nav_to}
|
||||
|
||||
load_text(false, "Annunciators")
|
||||
|
||||
TextBox{parent=annunc_div,y=1,text="Annunciators",alignment=ALIGN.CENTER}
|
||||
PushButton{parent=annunc_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=uis_page.nav_to}
|
||||
|
||||
local fac_annunc_page = guide_section(sect_construct_data, annunc_page, "Facility", docs.annunc.facility.main_section, 110)
|
||||
local unit_gen_page = guide_section(sect_construct_data, annunc_page, "Unit General", docs.annunc.unit.main_section, 170)
|
||||
local unit_rps_page = guide_section(sect_construct_data, annunc_page, "Unit RPS", docs.annunc.unit.rps_section, 100)
|
||||
local unit_rcs_page = guide_section(sect_construct_data, annunc_page, "Unit RCS", docs.annunc.unit.rcs_section, 170)
|
||||
|
||||
PushButton{parent=annunc_div,y=3,text="Facility General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fac_annunc_page.nav_to}
|
||||
PushButton{parent=annunc_div,text="Unit General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_gen_page.nav_to}
|
||||
PushButton{parent=annunc_div,text="Unit RPS >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_rps_page.nav_to}
|
||||
PushButton{parent=annunc_div,text="Unit RCS >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_rcs_page.nav_to}
|
||||
|
||||
load_text(false, "Coordinator UI")
|
||||
|
||||
TextBox{parent=coord_div,y=1,text="Coordinator UI",alignment=ALIGN.CENTER}
|
||||
PushButton{parent=coord_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=uis_page.nav_to}
|
||||
|
||||
load_text(false, "Main Display")
|
||||
local main_disp_page = guide_section(sect_construct_data, coord_page, "Main Display", docs.c_ui.main, 300)
|
||||
load_text(false, "Flow Display")
|
||||
local flow_disp_page = guide_section(sect_construct_data, coord_page, "Flow Display", docs.c_ui.flow, 210)
|
||||
load_text(false, "Unit Displays")
|
||||
local unit_disp_page = guide_section(sect_construct_data, coord_page, "Unit Displays", docs.c_ui.unit, 150)
|
||||
|
||||
PushButton{parent=coord_div,y=3,text="Main Display >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_disp_page.nav_to}
|
||||
PushButton{parent=coord_div,text="Flow Display >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=flow_disp_page.nav_to}
|
||||
PushButton{parent=coord_div,text="Unit Displays >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_disp_page.nav_to}
|
||||
|
||||
load_text("Front Panels")
|
||||
|
||||
TextBox{parent=fps,y=1,text="Front Panels",alignment=ALIGN.CENTER}
|
||||
PushButton{parent=fps,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
|
||||
|
||||
load_text(false, "Common Items")
|
||||
local fp_common_page = guide_section(sect_construct_data, fps_page, "Common Items", docs.fp.common, 100)
|
||||
load_text(false, "Reactor PLC")
|
||||
local fp_rplc_page = guide_section(sect_construct_data, fps_page, "Reactor PLC", docs.fp.r_plc, 190)
|
||||
load_text(false, "RTU Gateway")
|
||||
local fp_rtu_page = guide_section(sect_construct_data, fps_page, "RTU Gateway", docs.fp.rtu_gw, 100)
|
||||
load_text(false, "Supervisor")
|
||||
local fp_supervisor_page = guide_section(sect_construct_data, fps_page, "Supervisor", docs.fp.supervisor, 160)
|
||||
load_text(false, "Coordinator")
|
||||
local fp_coordinator_page = guide_section(sect_construct_data, fps_page, "Coordinator", docs.fp.coordinator, 80)
|
||||
|
||||
PushButton{parent=fps,y=3,text="Common Items >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_common_page.nav_to}
|
||||
PushButton{parent=fps,text="Reactor PLC >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_rplc_page.nav_to}
|
||||
PushButton{parent=fps,text="RTU Gateway >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_rtu_page.nav_to}
|
||||
PushButton{parent=fps,text="Supervisor >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_supervisor_page.nav_to}
|
||||
PushButton{parent=fps,text="Coordinator >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_coordinator_page.nav_to}
|
||||
|
||||
load_text("Glossary")
|
||||
|
||||
TextBox{parent=gls,y=1,text="Glossary",alignment=ALIGN.CENTER}
|
||||
PushButton{parent=gls,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
|
||||
|
||||
local gls_abbv_page = guide_section(sect_construct_data, gls_page, "Abbreviations", docs.glossary.abbvs, 140)
|
||||
local gls_term_page = guide_section(sect_construct_data, gls_page, "Terminology", docs.glossary.terms, 120)
|
||||
|
||||
PushButton{parent=gls,y=3,text="Abbreviations >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_abbv_page.nav_to}
|
||||
PushButton{parent=gls,text="Terminology >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_term_page.nav_to}
|
||||
|
||||
load_text("Links")
|
||||
|
||||
TextBox{parent=lnk,y=1,text="Wiki and Discord",alignment=ALIGN.CENTER}
|
||||
PushButton{parent=lnk,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
|
||||
|
||||
lnk.line_break()
|
||||
TextBox{parent=lnk,text="GitHub",fg_bg=cpair(colors.lightGray,colors.black)}
|
||||
TextBox{parent=lnk,text="https://github.com/MikaylaFischler/cc-mek-scada"}
|
||||
lnk.line_break()
|
||||
TextBox{parent=lnk,text="Wiki",fg_bg=cpair(colors.lightGray,colors.black)}
|
||||
TextBox{parent=lnk,text="https://github.com/MikaylaFischler/cc-mek-scada/wiki"}
|
||||
lnk.line_break()
|
||||
TextBox{parent=lnk,text="Discord",fg_bg=cpair(colors.lightGray,colors.black)}
|
||||
TextBox{parent=lnk,text="discord.gg/R9NSCkhcwt"}
|
||||
|
||||
-- setup multipane
|
||||
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
app.set_root_pane(u_pane)
|
||||
|
||||
-- link help resources
|
||||
db.nav.link_help(doc_map)
|
||||
|
||||
-- done, show the app
|
||||
load_pane.set_value(2)
|
||||
end
|
||||
|
||||
-- delete the elements and switch back to the loading screen
|
||||
local function unload()
|
||||
if page_div then
|
||||
page_div.delete()
|
||||
page_div = nil
|
||||
end
|
||||
|
||||
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
|
||||
app.delete_pages()
|
||||
|
||||
-- show loading screen
|
||||
load_pane.set_value(1)
|
||||
end
|
||||
|
||||
app.set_load(load)
|
||||
app.set_unload(unload)
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
62
pocket/ui/apps/loader.lua
Normal file
62
pocket/ui/apps/loader.lua
Normal file
@@ -0,0 +1,62 @@
|
||||
--
|
||||
-- Loading Screen App
|
||||
--
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local conn_waiting = require("pocket.ui.components.conn_waiting")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.Div")
|
||||
local MultiPane = require("graphics.elements.MultiPane")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local APP_ID = pocket.APP_ID
|
||||
|
||||
local LINK_STATE = iocontrol.LINK_STATE
|
||||
|
||||
-- create the connecting to SV & API page
|
||||
---@param root Container parent
|
||||
local function create_pages(root)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
db.nav.register_app(APP_ID.LOADER, main).new_page(nil, function () end)
|
||||
|
||||
local conn_sv_wait = conn_waiting(main, 6, false)
|
||||
local conn_api_wait = conn_waiting(main, 6, true)
|
||||
local main_pane = Div{parent=main,x=1,y=2}
|
||||
|
||||
local root_pane = MultiPane{parent=main,x=1,y=1,panes={conn_sv_wait,conn_api_wait,main_pane}}
|
||||
|
||||
local function update()
|
||||
local state = db.ps.get("link_state")
|
||||
|
||||
if state == LINK_STATE.UNLINKED then
|
||||
root_pane.set_value(1)
|
||||
elseif state == LINK_STATE.API_LINK_ONLY then
|
||||
if not db.loader_require.sv then
|
||||
root_pane.set_value(3)
|
||||
db.nav.on_loader_connected()
|
||||
else root_pane.set_value(1) end
|
||||
elseif state == LINK_STATE.SV_LINK_ONLY then
|
||||
if not db.loader_require.api then
|
||||
root_pane.set_value(3)
|
||||
db.nav.on_loader_connected()
|
||||
else root_pane.set_value(2) end
|
||||
else
|
||||
root_pane.set_value(3)
|
||||
db.nav.on_loader_connected()
|
||||
end
|
||||
end
|
||||
|
||||
root_pane.register(db.ps, "link_state", update)
|
||||
root_pane.register(db.ps, "loader_reqs", update)
|
||||
|
||||
TextBox{parent=main_pane,text="Connected!",x=1,y=6,alignment=core.ALIGN.CENTER}
|
||||
end
|
||||
|
||||
return create_pages
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user