Compare commits
766 Commits
v1.6.2-bet
...
v1.8.23-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6343e5956 | ||
|
|
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 | ||
|
|
7130176781 | ||
|
|
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 | ||
|
|
8e19418701 | ||
|
|
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 | ||
|
|
07406ca5fc | ||
|
|
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 | ||
|
|
b0342654e7 | ||
|
|
bee96ed12e | ||
|
|
50bd59781e | ||
|
|
196e0b1daf | ||
|
|
f725eb0eef | ||
|
|
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 | ||
|
|
afc38e7e7a | ||
|
|
41b7a68f3e | ||
|
|
8968ebede3 | ||
|
|
50a9168b06 | ||
|
|
76e85da9d5 | ||
|
|
be560cd532 | ||
|
|
2b0a536292 | ||
|
|
afed6f514d | ||
|
|
3e6a0a8869 | ||
|
|
b99cf19be0 | ||
|
|
7f009f9c86 | ||
|
|
6a8ed311f3 | ||
|
|
c181142f75 | ||
|
|
0cb964a177 | ||
|
|
3cd832ca20 | ||
|
|
0c6c7bf9a5 | ||
|
|
3bfcb1d83c | ||
|
|
4fe6792804 | ||
|
|
25dc47d520 | ||
|
|
f958b0e3b7 | ||
|
|
f621ff2482 | ||
|
|
eb45ff899b | ||
|
|
e91fd2fcaa | ||
|
|
d35b824458 | ||
|
|
165d1497f8 | ||
|
|
50bf057ca6 | ||
|
|
6f768ef6b3 | ||
|
|
826086951e | ||
|
|
35bf56663f | ||
|
|
43b44cc425 | ||
|
|
7b8cea4a5c | ||
|
|
51d4a22532 | ||
|
|
fb85c2f05b | ||
|
|
712d018806 | ||
|
|
00a8d64a88 | ||
|
|
d9efd5b8d2 | ||
|
|
a786404092 | ||
|
|
1bd03c0b1a | ||
|
|
0d8d7aeb15 | ||
|
|
0b60dc9fa4 | ||
|
|
48b91ac808 | ||
|
|
4c9efc78b1 | ||
|
|
3c5857cd68 | ||
|
|
473b0dd10d | ||
|
|
d2df7db18b | ||
|
|
0ca002316f | ||
|
|
9fe34648c2 | ||
|
|
57dc20692b | ||
|
|
f22d699baf | ||
|
|
23b31e0049 | ||
|
|
2b4309afa7 | ||
|
|
89cd5a07f2 | ||
|
|
99213da760 | ||
|
|
f23f69d441 | ||
|
|
a99b7912f8 | ||
|
|
23ca5fb69e | ||
|
|
c243d064ef | ||
|
|
878c3b92e1 | ||
|
|
2aa52d2e2c | ||
|
|
dfc1ee6497 | ||
|
|
a6a1a61954 | ||
|
|
d0b50c834c | ||
|
|
612a06ba98 | ||
|
|
65d43d55c7 | ||
|
|
4fcd375ee2 | ||
|
|
a22f5562cf | ||
|
|
0365ea5e8a | ||
|
|
da300607f1 | ||
|
|
cc3174ee76 | ||
|
|
98c37caecd | ||
|
|
cc50e4a020 | ||
|
|
f734c4307b | ||
|
|
45573be8c7 | ||
|
|
eab1ffbe03 | ||
|
|
1504247b33 | ||
|
|
92a4277f73 | ||
|
|
31a663e86b | ||
|
|
5c0e2c32ee | ||
|
|
3537c59365 | ||
|
|
1d7104ae74 | ||
|
|
4629e1ba2a | ||
|
|
659644865a | ||
|
|
03d2b3f087 | ||
|
|
bea7b91ff1 | ||
|
|
b94c89f4ec | ||
|
|
55e4e5a68b | ||
|
|
e1ad76a00d | ||
|
|
93e4590947 | ||
|
|
2442e7f972 | ||
|
|
bb2c07963b | ||
|
|
44c6352a8c | ||
|
|
968b0a9122 | ||
|
|
19869416af | ||
|
|
598f6f08af | ||
|
|
ca2d8ab7da | ||
|
|
1c0f61b3e0 | ||
|
|
ecd3575643 | ||
|
|
9dc3a09f4d | ||
|
|
7a5d14d67f | ||
|
|
d6175e5cec | ||
|
|
a00a824a7f | ||
|
|
9393632428 | ||
|
|
886bd0d5d5 | ||
|
|
9c1d83fdfc | ||
|
|
bd88244681 | ||
|
|
8dae632b25 | ||
|
|
89d56d3101 | ||
|
|
1f451ff92a | ||
|
|
9d08b51f84 | ||
|
|
047ff5c203 | ||
|
|
895f768e58 | ||
|
|
b3f29566ea | ||
|
|
c0a5c8d504 | ||
|
|
bbe7b52662 | ||
|
|
d5b166dcc6 | ||
|
|
b0aa6d54ac | ||
|
|
ad240ae44c | ||
|
|
79c93f1562 | ||
|
|
5d760a0524 | ||
|
|
6c89b3134c | ||
|
|
3c7fff28c9 | ||
|
|
814043bf04 | ||
|
|
fc7896ebd3 | ||
|
|
48a8eadc55 | ||
|
|
f0f2aadf53 | ||
|
|
ce37a672a3 | ||
|
|
510995b04f | ||
|
|
560061d4ad | ||
|
|
d87e3893f0 | ||
|
|
fc198cd9d2 | ||
|
|
c714e49ad8 | ||
|
|
d1e4ea586e | ||
|
|
4e789ab92d | ||
|
|
0892a57d35 | ||
|
|
1bc4828010 | ||
|
|
fb5a9d5d9e | ||
|
|
adbf1f2f78 | ||
|
|
2f99aaeedb | ||
|
|
a318ffb283 | ||
|
|
a677e994d6 | ||
|
|
f9917b786c | ||
|
|
dbc1f41c5d | ||
|
|
d6185e0183 | ||
|
|
6ef049baa1 | ||
|
|
a4214e8a4f | ||
|
|
45881067df | ||
|
|
c40aa229bf | ||
|
|
51f2bba4d1 | ||
|
|
628a50e1bd | ||
|
|
cdd31508d9 | ||
|
|
83d62991f8 | ||
|
|
f207a950e4 | ||
|
|
0b0051dc2f | ||
|
|
f152c37ea9 | ||
|
|
372fd426d8 | ||
|
|
8347afb6d0 | ||
|
|
10d0a9763a | ||
|
|
96691d773a | ||
|
|
910509d764 | ||
|
|
158cc39b80 | ||
|
|
940f71aa35 | ||
|
|
788cef8f86 | ||
|
|
baaef862ab | ||
|
|
96db709ced | ||
|
|
c47fa5433f | ||
|
|
440b724798 | ||
|
|
126d6eb163 | ||
|
|
8ac46faf36 | ||
|
|
f112746e12 | ||
|
|
76f21e925b | ||
|
|
a330249c7e | ||
|
|
bbc64c8dc2 | ||
|
|
bb062cf397 | ||
|
|
53bb36ce8d | ||
|
|
8237113577 | ||
|
|
02906ae707 | ||
|
|
6d0e777e68 | ||
|
|
36468c4043 | ||
|
|
1cf7375311 | ||
|
|
ca55948286 | ||
|
|
195f59178f | ||
|
|
e416faf313 | ||
|
|
20bff48cfd | ||
|
|
1a9892b291 | ||
|
|
827953c0a1 | ||
|
|
56e69e3a29 | ||
|
|
1984b63837 | ||
|
|
36b12d5dea | ||
|
|
3e83c8e2c6 | ||
|
|
1fa8c03dff | ||
|
|
d6de9c266b | ||
|
|
2142c1b4f7 | ||
|
|
cafba6c67a | ||
|
|
6eccebbe39 | ||
|
|
5e9f03c900 | ||
|
|
7374bb02d1 | ||
|
|
d19794ae4f | ||
|
|
108cf75cad | ||
|
|
34cbb6be39 | ||
|
|
907f27baf8 | ||
|
|
4710fa7cee | ||
|
|
526a54903e | ||
|
|
e1a632dcc7 | ||
|
|
d5d818e625 | ||
|
|
bfa1f4d0c6 | ||
|
|
737e0d72b0 | ||
|
|
6edeb3e3b8 | ||
|
|
fb00e98a5b | ||
|
|
4f952eff83 | ||
|
|
1eede97c08 | ||
|
|
95419562ee | ||
|
|
03de90c3d8 | ||
|
|
99096e0fc9 | ||
|
|
7b85d947c4 | ||
|
|
08e670091a | ||
|
|
1348b632a8 | ||
|
|
8cd5162362 | ||
|
|
1c410a89d8 | ||
|
|
6a931fced4 | ||
|
|
622e2eeb90 | ||
|
|
42cd9fff0c | ||
|
|
2a85a438ba | ||
|
|
338b3b1615 | ||
|
|
363f164f47 | ||
|
|
739f04ece9 | ||
|
|
c6ade68ce2 | ||
|
|
7d60e259e2 | ||
|
|
cd71c6a9c1 | ||
|
|
26fe130609 | ||
|
|
95f87b1b05 | ||
|
|
aebdf3e8df | ||
|
|
0395aa95b6 | ||
|
|
c624baed2c | ||
|
|
1a40321c0f | ||
|
|
5d4fc36256 | ||
|
|
b799d785b9 | ||
|
|
d55442fa53 | ||
|
|
c870b749a4 | ||
|
|
4421cbc0c5 | ||
|
|
b6a3305f23 | ||
|
|
5680260136 | ||
|
|
bc66ea6ecb | ||
|
|
1b20218445 | ||
|
|
466e442353 | ||
|
|
9e6751f47f | ||
|
|
f868923905 | ||
|
|
5d3fd6d939 | ||
|
|
37659d687e | ||
|
|
f23b7e2c2f | ||
|
|
55ccdd63d4 | ||
|
|
5c88890ed4 | ||
|
|
fa0185c9a4 | ||
|
|
e1ed9a8e5e | ||
|
|
9f7e3bc282 | ||
|
|
4ec060ba24 | ||
|
|
b1f1753a8d | ||
|
|
94a62f8c31 | ||
|
|
e6f49f256c | ||
|
|
a048b0aa4a | ||
|
|
b6617c140c | ||
|
|
1fdf012f65 | ||
|
|
8fe0321ac0 | ||
|
|
4a2199fa13 | ||
|
|
69680a53a0 | ||
|
|
785dea6545 | ||
|
|
885932afe1 | ||
|
|
a38ccf3dcc | ||
|
|
d7b1f9cc7e | ||
|
|
6e92097544 | ||
|
|
76403b4ddc | ||
|
|
41ad8d8edb | ||
|
|
68754977b0 | ||
|
|
78ad6d5457 | ||
|
|
cb049ebf41 | ||
|
|
f2f5c3201f | ||
|
|
1ba178eae8 | ||
|
|
838f80c30c | ||
|
|
dc0408881e | ||
|
|
1b5e8cb69c | ||
|
|
9e13a3a467 | ||
|
|
32653c3b8a | ||
|
|
16258a2631 | ||
|
|
4c646249ad | ||
|
|
45c8a8d8a9 | ||
|
|
eff3444834 | ||
|
|
25f68f338c | ||
|
|
7ef363a3c2 | ||
|
|
3065e2bece | ||
|
|
1075d66122 | ||
|
|
d477b33774 | ||
|
|
7b374f8618 | ||
|
|
4869c00c0e | ||
|
|
ff4a5a68d9 | ||
|
|
d77a527b15 | ||
|
|
01caca48dc | ||
|
|
43e545b6ae | ||
|
|
8b65956dcc | ||
|
|
7b522ae120 | ||
|
|
43d134d9ad | ||
|
|
24c787f47d | ||
|
|
f95ac8be8c | ||
|
|
41442012c2 | ||
|
|
6f1195dded | ||
|
|
86e8feaabc | ||
|
|
670ca78a5b | ||
|
|
73ceed0f60 | ||
|
|
8412270772 | ||
|
|
686b47898c | ||
|
|
e03eaf2982 | ||
|
|
8b1775b0af | ||
|
|
8bbd04d133 | ||
|
|
26dc6ff6d1 | ||
|
|
24d190921d | ||
|
|
4fec116e93 | ||
|
|
ef6fdaa3ac | ||
|
|
0160d4c53a | ||
|
|
d2a1951b66 | ||
|
|
5d7a0b266a | ||
|
|
ebabd99f2b | ||
|
|
b5e0183e54 | ||
|
|
d450c9ca3e | ||
|
|
894229831d | ||
|
|
bfa24b3665 | ||
|
|
b1446637ad | ||
|
|
02e9c09daf | ||
|
|
21d5cb3858 | ||
|
|
c0a602385d | ||
|
|
4d4dd4ed39 | ||
|
|
3a5d69d96f | ||
|
|
d38a2dea7c | ||
|
|
560d48084a | ||
|
|
625feb3fd1 | ||
|
|
ed4180a072 | ||
|
|
70831b49d2 | ||
|
|
881a120d34 | ||
|
|
8ab1307b2b | ||
|
|
689d474796 | ||
|
|
18bcfb4014 | ||
|
|
645a5f5137 | ||
|
|
1f9743efd0 | ||
|
|
9cef6e6175 | ||
|
|
70b03896d5 | ||
|
|
f9d0ef60b4 | ||
|
|
09ab60f79d | ||
|
|
d21604ea09 | ||
|
|
611b048cb4 | ||
|
|
a2182d9566 | ||
|
|
7a87499aa4 | ||
|
|
b173b72f21 | ||
|
|
29cc107ea5 | ||
|
|
c24766a4db | ||
|
|
29e910ba3c | ||
|
|
1cb240b1b0 | ||
|
|
1525ed9d60 | ||
|
|
b1c2c4d291 | ||
|
|
5585088e3a | ||
|
|
b9073153b3 | ||
|
|
cb554e5d16 | ||
|
|
71d8b5ba0a | ||
|
|
e4f49e9949 | ||
|
|
3afc765f72 | ||
|
|
048714817e | ||
|
|
f267a4e569 | ||
|
|
cfc6479dd5 | ||
|
|
70f24edb53 | ||
|
|
31df4a7f7e | ||
|
|
ca49cf90b4 | ||
|
|
785dbe9533 | ||
|
|
a9d1bc2b50 | ||
|
|
f7766d8cba | ||
|
|
37f8b85924 | ||
|
|
2ed28cf74d | ||
|
|
17698b7fb4 | ||
|
|
386a33ffd8 | ||
|
|
b7d4468cea | ||
|
|
8b0a5d529e | ||
|
|
d18a93f7d2 | ||
|
|
89d1087b1c | ||
|
|
d9e48f5cac | ||
|
|
56377ef595 | ||
|
|
95c300e450 | ||
|
|
2985898b7e | ||
|
|
57d50e6745 | ||
|
|
3dc1a06969 | ||
|
|
fcba935240 | ||
|
|
9b32bb4675 | ||
|
|
f59f484e7b | ||
|
|
0fe9b391d8 | ||
|
|
97f0191875 | ||
|
|
70db8d782c | ||
|
|
2acd166c3e | ||
|
|
c78f7e173a | ||
|
|
99a0b0a55a | ||
|
|
6e51e70b62 | ||
|
|
fd2abad5cf | ||
|
|
b93c6b7c6e | ||
|
|
8b3f558f68 | ||
|
|
8c5289867c | ||
|
|
d179920565 | ||
|
|
504ee0594f | ||
|
|
a92f182156 | ||
|
|
c5d38a5584 | ||
|
|
9bf07e6c3e | ||
|
|
7656936982 | ||
|
|
f477ad9426 | ||
|
|
59950e9d15 | ||
|
|
11d86d92eb | ||
|
|
1275f61113 | ||
|
|
d17e2b8321 | ||
|
|
ce780c3d72 | ||
|
|
76ab4e17bf | ||
|
|
ac1733c46e | ||
|
|
17731de61b | ||
|
|
d85385c1fe | ||
|
|
e0809f52a6 | ||
|
|
b2c55f9d4b | ||
|
|
ba896ea163 | ||
|
|
1a64591256 | ||
|
|
9ce75eb4bd | ||
|
|
451f804f87 | ||
|
|
724d13510d | ||
|
|
3f01ce7ec5 | ||
|
|
df67795239 | ||
|
|
775d4dc95b | ||
|
|
b3c7263bc4 | ||
|
|
9f8732830c | ||
|
|
1c87ef18a1 | ||
|
|
f111b711c5 | ||
|
|
92d1945bea | ||
|
|
4192ea426c | ||
|
|
7bd8f34773 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ko_fi: mikayla_f
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
labels: "enhancement,feature request"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
6
.github/workflows/check.yml
vendored
6
.github/workflows/check.yml
vendored
@@ -5,19 +5,17 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- latest
|
||||
- devel
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- latest
|
||||
- devel
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3.5.1
|
||||
uses: actions/checkout@v4
|
||||
- name: Luacheck
|
||||
uses: lunarmodules/luacheck@v1.1.0
|
||||
with:
|
||||
@@ -28,4 +26,4 @@ jobs:
|
||||
# --no-max-line-length = Disable warnings for long line lengths
|
||||
# --exclude-files ... = Exclude lockbox library (external) and config files
|
||||
# --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os'
|
||||
args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http parallel periphemu peripheral read rs settings shell term textutils window
|
||||
args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* --globals os _HOST bit colors fs http keys parallel periphemu peripheral read rs settings shell term textutils window
|
||||
|
||||
36
.github/workflows/manifest.yml
vendored
36
.github/workflows/manifest.yml
vendored
@@ -6,7 +6,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- latest
|
||||
- devel
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
@@ -30,54 +29,41 @@ 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
|
||||
- name: Create outputs folders
|
||||
if: success() || failure()
|
||||
shell: bash
|
||||
run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/latest deploy/manifests/devel
|
||||
run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/devel
|
||||
- name: Generate manifest and shields for main branch
|
||||
id: manifest-main
|
||||
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
|
||||
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
|
||||
# Generate manifest for latest branch
|
||||
- name: Checkout latest
|
||||
id: checkout-latest
|
||||
if: success() || failure()
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'latest'
|
||||
clean: false
|
||||
- name: Generate manifest for latest
|
||||
id: manifest-latest
|
||||
if: ${{ (success() || failure()) && steps.checkout-latest.outcome == 'success' }}
|
||||
run: python imgen.py
|
||||
- name: Save latest's manifest
|
||||
if: ${{ (success() || failure()) && steps.manifest-latest.outcome == 'success' }}
|
||||
run: mv install_manifest.json deploy/manifests/latest
|
||||
# Generate manifest for devel branch
|
||||
- name: Checkout devel
|
||||
id: checkout-devel
|
||||
if: success() || failure()
|
||||
uses: actions/checkout@v3
|
||||
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
|
||||
@@ -85,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
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -5,6 +5,7 @@
|
||||
"colors",
|
||||
"fs",
|
||||
"http",
|
||||
"keys",
|
||||
"parallel",
|
||||
"periphemu",
|
||||
"peripheral",
|
||||
@@ -24,6 +25,7 @@
|
||||
},
|
||||
"Lua.hint.setType": true,
|
||||
"Lua.diagnostics.disable": [
|
||||
"duplicate-set-field"
|
||||
"duplicate-set-field",
|
||||
"inject-field"
|
||||
]
|
||||
}
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright © 2022 - 2023 Mikayla Fischler
|
||||
Copyright 2022 - 2024 Mikayla Fischler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
20
README.md
20
README.md
@@ -4,12 +4,11 @@ Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fi
|
||||

|
||||

|
||||

|
||||

|
||||

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

|
||||

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

|
||||

|
||||

|
||||

|
||||

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

|
||||
@@ -34,14 +36,22 @@ Mod Requirements:
|
||||
|
||||
Mod Recommendations:
|
||||
- Advanced Peripherals (adds the capability to detect environmental radiation levels)
|
||||
- Immersive Engineering (provides bundled redstone, though any mod containing bundled redstone will do)
|
||||
|
||||
v10.1+ is required due the complete support of CC:Tweaked added in Mekanism v10.1
|
||||
v10.1+ is required due to the complete support of CC:Tweaked added in Mekanism v10.1
|
||||
|
||||
## Installation
|
||||
|
||||
You can install this on a ComputerCraft computer using either:
|
||||
* `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua`
|
||||
* `pastebin get RGasyTM4 ccmsi.lua`
|
||||
* `pastebin get sqUN6VUb ccmsi.lua`
|
||||
* Off-line (when HTTP is disabled) installation via [release bundles](https://github.com/MikaylaFischler/cc-mek-scada/wiki/Alternative-Installation-Strategies#release-bundles)
|
||||
|
||||
## Contributing
|
||||
|
||||
Please reach out to me via Discord or email (or GitHub in some way) if you are thinking of making any contributions at this time. I started this project as a challenge for myself and have been enjoying having something I can work on in my own way.
|
||||
|
||||
Once this is out of beta I will be more open to contributions, but for now I am hoping to keep them to a minimum as the remaining challenges are ones I am looking forward to solving.
|
||||
|
||||
## [SCADA](https://en.wikipedia.org/wiki/SCADA)
|
||||
> Supervisory control and data acquisition (SCADA) is a control system architecture comprising computers, networked data communications and graphical user interfaces for high-level supervision of machines and processes. It also covers sensors and other devices, such as programmable logic controllers, which interface with process plant or machinery.
|
||||
|
||||
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()
|
||||
@@ -48,6 +48,7 @@ def make_manifest(size):
|
||||
"versions" : {
|
||||
"installer" : get_version("./ccmsi.lua"),
|
||||
"bootloader" : get_version("./startup.lua"),
|
||||
"common" : get_version("./scada-common/util.lua", True),
|
||||
"comms" : get_version("./scada-common/comms.lua", True),
|
||||
"graphics" : get_version("./graphics/core.lua", True),
|
||||
"lockbox" : get_version("./lockbox/init.lua", True),
|
||||
@@ -59,7 +60,7 @@ def make_manifest(size):
|
||||
},
|
||||
"files" : {
|
||||
# common files
|
||||
"system" : [ "initenv.lua", "startup.lua" ],
|
||||
"system" : [ "initenv.lua", "startup.lua", "configure.lua", "LICENSE" ],
|
||||
"common" : list_files("./scada-common"),
|
||||
"graphics" : list_files("./graphics"),
|
||||
"lockbox" : list_files("./lockbox"),
|
||||
@@ -81,7 +82,7 @@ def make_manifest(size):
|
||||
# manifest file estimate
|
||||
"manifest" : size,
|
||||
# common files
|
||||
"system" : os.path.getsize("initenv.lua") + os.path.getsize("startup.lua"),
|
||||
"system" : os.path.getsize("initenv.lua") + os.path.getsize("startup.lua") + os.path.getsize("configure.lua"),
|
||||
"common" : dir_size("./scada-common"),
|
||||
"graphics" : dir_size("./graphics"),
|
||||
"lockbox" : dir_size("./lockbox"),
|
||||
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()
|
||||
758
ccmsi.lua
758
ccmsi.lua
@@ -1,9 +1,7 @@
|
||||
--
|
||||
-- ComputerCraft Mekanism SCADA System Installer Utility
|
||||
--
|
||||
|
||||
--[[
|
||||
Copyright (c) 2023 Mikayla Fischler
|
||||
CC-MEK-SCADA Installer Utility
|
||||
|
||||
Copyright (c) 2023 - 2024 Mikayla Fischler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
@@ -17,23 +15,72 @@ 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.7d"
|
||||
local CCMSI_VERSION = "v1.20"
|
||||
|
||||
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"
|
||||
local install_manifest = manifest_path.."main/install_manifest.json"
|
||||
|
||||
local function red() term.setTextColor(colors.red) end
|
||||
local function orange() term.setTextColor(colors.orange) end
|
||||
local function yellow() term.setTextColor(colors.yellow) end
|
||||
local function green() term.setTextColor(colors.green) end
|
||||
local function cyan() term.setTextColor(colors.cyan) end
|
||||
local function blue() term.setTextColor(colors.blue) end
|
||||
local function white() term.setTextColor(colors.white) end
|
||||
local function lgray() term.setTextColor(colors.lightGray) end
|
||||
@@ -60,19 +107,18 @@ local function ask_y_n(question, default)
|
||||
end
|
||||
|
||||
-- print out a white + blue text message
|
||||
local function pkg_message(message, package) white();print(message .. " ");blue();println(package);white() end
|
||||
local function pkg_message(message, package) white();print(message.." ");blue();println(package);white() end
|
||||
|
||||
-- indicate actions to be taken based on package differences for installs/updates
|
||||
local function show_pkg_change(name, v_local, v_remote)
|
||||
if v_local ~= nil then
|
||||
if v_local ~= v_remote then
|
||||
print("[" .. name .. "] updating ");blue();print(v_local);white();print(" \xbb ");blue();println(v_remote);white()
|
||||
local function show_pkg_change(name, v)
|
||||
if v.v_local ~= nil then
|
||||
if v.v_local ~= v.v_remote then
|
||||
print("["..name.."] updating ");blue();print(v.v_local);white();print(" \xbb ");blue();println(v.v_remote);white()
|
||||
elseif mode == "install" then
|
||||
pkg_message("[" .. name .. "] reinstalling", v_local)
|
||||
pkg_message("["..name.."] reinstalling", v.v_local)
|
||||
end
|
||||
else
|
||||
pkg_message("[" .. name .. "] new install of", v_remote)
|
||||
end
|
||||
else pkg_message("["..name.."] new install of", v.v_remote) end
|
||||
return v.v_local ~= v.v_remote
|
||||
end
|
||||
|
||||
-- read the local manifest file
|
||||
@@ -91,8 +137,8 @@ end
|
||||
local function get_remote_manifest()
|
||||
local response, error = http.get(install_manifest)
|
||||
if response == nil then
|
||||
orange();println("failed to get installation manifest from GitHub, cannot update or install")
|
||||
red();println("HTTP error: " .. error);white()
|
||||
orange();println("Failed to get installation manifest from GitHub, cannot update or install.")
|
||||
red();println("HTTP error: "..error);white()
|
||||
return false, {}
|
||||
end
|
||||
|
||||
@@ -103,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
|
||||
@@ -122,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)
|
||||
@@ -133,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
|
||||
@@ -157,96 +237,118 @@ local function _clean_dir(dir, tree)
|
||||
if tree == nil then tree = {} end
|
||||
local ls = fs.list(dir)
|
||||
for _, val in pairs(ls) do
|
||||
local path = dir .. "/" .. val
|
||||
local path = dir.."/"..val
|
||||
if fs.isDir(path) then
|
||||
_clean_dir(path, tree[val])
|
||||
if #fs.list(path) == 0 then fs.delete(path);println("deleted " .. path) end
|
||||
elseif not _in_array(val, tree) then
|
||||
if #fs.list(path) == 0 then fs.delete(path);println("deleted "..path) end
|
||||
elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then ---@todo remove config.lua on full release
|
||||
fs.delete(path)
|
||||
println("deleted " .. path)
|
||||
println("deleted "..path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- go through app/common directories to delete unused files
|
||||
local function clean(manifest)
|
||||
local root_ext = false
|
||||
local tree = gen_tree(manifest)
|
||||
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")
|
||||
|
||||
lgray()
|
||||
|
||||
local ls = fs.list("/")
|
||||
for _, val in pairs(ls) do
|
||||
if fs.isDir(val) then
|
||||
if tree[val] ~= nil then _clean_dir("/" .. val, tree[val]) end
|
||||
if #fs.list(val) == 0 then fs.delete(val);println("deleted " .. val) end
|
||||
elseif not _in_array(val, tree) then
|
||||
root_ext = true
|
||||
yellow();println(val .. " not used")
|
||||
if fs.isDriveRoot(val) then
|
||||
yellow();println("skipped mount '"..val.."'")
|
||||
elseif fs.isDir(val) then
|
||||
if tree[val] ~= nil then lgray();_clean_dir("/"..val, tree[val])
|
||||
else white(); if ask_y_n("delete the unused directory '"..val.."'") then lgray();_clean_dir("/"..val) end end
|
||||
if #fs.list(val) == 0 then fs.delete(val);lgray();println("deleted empty directory '"..val.."'") end
|
||||
elseif not _in_array(val, tree) and (string.find(val, ".settings") == nil) then
|
||||
white();if ask_y_n("delete the unused file '"..val.."'") then fs.delete(val);lgray();println("deleted "..val) end
|
||||
end
|
||||
end
|
||||
|
||||
white()
|
||||
if root_ext then println("Files in root directory won't be automatically deleted.") end
|
||||
end
|
||||
|
||||
-- get and validate command line options
|
||||
|
||||
println("-- CC Mekanism SCADA Installer " .. CCMSI_VERSION .. " --")
|
||||
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 avilable")
|
||||
println(" check - check latest versions available")
|
||||
yellow()
|
||||
println(" ccmsi check <branch> for target")
|
||||
lgray()
|
||||
println(" install - fresh install, overwrites config")
|
||||
println(" update - update files EXCEPT for config/logs")
|
||||
println(" remove - delete files EXCEPT for config/logs")
|
||||
println(" purge - delete files INCLUDING config/logs")
|
||||
println(" install - fresh install")
|
||||
println(" update - update files")
|
||||
println(" uninstall - delete files INCLUDING config/logs")
|
||||
white();println("<app>");lgray()
|
||||
println(" reactor-plc - reactor PLC firmware")
|
||||
println(" rtu - RTU firmware")
|
||||
println(" supervisor - supervisor server application")
|
||||
println(" coordinator - coordinator application")
|
||||
println(" pocket - pocket application")
|
||||
white();println("<branch>");yellow()
|
||||
println(" second parameter when used with check")
|
||||
lgray();println(" main (default) | latest | devel");white()
|
||||
println(" installer - ccmsi installer (update only)")
|
||||
white();println("<branch>")
|
||||
lgray();println(" main (default) | devel");white()
|
||||
end
|
||||
return
|
||||
else
|
||||
mode = get_opt(opts[1], { "check", "install", "update", "remove", "purge" })
|
||||
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
|
||||
if mode == nil then
|
||||
red();println("Unrecognized mode.");white()
|
||||
return
|
||||
end
|
||||
|
||||
app = get_opt(opts[2], { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket" })
|
||||
app = get_opt(opts[2], { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket", "installer" })
|
||||
if app == nil and mode ~= "check" then
|
||||
red();println("Unrecognized application.");white()
|
||||
return
|
||||
elseif app == "installer" and mode ~= "update" then
|
||||
red();println("Installer app only supports 'update' option.");white()
|
||||
return
|
||||
end
|
||||
|
||||
-- determine target
|
||||
if mode == "check" then target = opts[2] else target = opts[3] end
|
||||
if (target ~= "main") and (target ~= "latest") and (target ~= "devel") then
|
||||
if (target ~= "main") and (target ~= "devel") then
|
||||
if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end
|
||||
target = "main"
|
||||
end
|
||||
|
||||
-- set paths
|
||||
install_manifest = manifest_path .. target .. "/install_manifest.json"
|
||||
repo_path = repo_path .. target .. "/"
|
||||
install_manifest = manifest_path..target.."/install_manifest.json"
|
||||
repo_path = repo_path..target.."/"
|
||||
end
|
||||
|
||||
-- run selected mode
|
||||
|
||||
if mode == "check" then
|
||||
local ok, manifest = get_remote_manifest()
|
||||
if not ok then return end
|
||||
@@ -262,271 +364,342 @@ 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(" (")
|
||||
term.setTextColor(colors.cyan)
|
||||
print(value);white();println(" available)")
|
||||
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 ")
|
||||
term.setTextColor(colors.cyan)
|
||||
print(value);white();println(")")
|
||||
cyan();print(value);white();println(")")
|
||||
end
|
||||
end
|
||||
|
||||
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, manifest = get_remote_manifest()
|
||||
local ok, r_manifest, l_manifest
|
||||
|
||||
local update_installer = app == "installer"
|
||||
ok, r_manifest = get_remote_manifest()
|
||||
if not ok then return end
|
||||
|
||||
local ver = {
|
||||
app = { v_local = nil, v_remote = nil, changed = false },
|
||||
boot = { v_local = nil, v_remote = nil, changed = false },
|
||||
comms = { v_local = nil, v_remote = nil, changed = false },
|
||||
common = { v_local = nil, v_remote = nil, changed = false },
|
||||
graphics = { v_local = nil, v_remote = nil, changed = false },
|
||||
lockbox = { v_local = nil, v_remote = nil, changed = false }
|
||||
}
|
||||
|
||||
-- try to find local versions
|
||||
local local_ok, local_manifest = read_local_manifest()
|
||||
if not local_ok then
|
||||
if mode == "update" then
|
||||
red();println("failed to load local installation information, cannot update");white()
|
||||
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
|
||||
else
|
||||
ver.boot.v_local = local_manifest.versions.bootloader
|
||||
ver.app.v_local = local_manifest.versions[app]
|
||||
ver.comms.v_local = local_manifest.versions.comms
|
||||
ver.graphics.v_local = local_manifest.versions.graphics
|
||||
ver.lockbox.v_local = local_manifest.versions.lockbox
|
||||
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 local_manifest.versions[app] == nil then
|
||||
red();println("another application is already installed, please purge it before installing a new application");white()
|
||||
return
|
||||
end
|
||||
|
||||
local_manifest.versions.installer = CCMSI_VERSION
|
||||
if manifest.versions.installer ~= CCMSI_VERSION then
|
||||
yellow();println("a newer version of the installer is available, it is recommended to download it");white()
|
||||
if 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
|
||||
|
||||
ver.boot.v_remote = manifest.versions.bootloader
|
||||
ver.app.v_remote = manifest.versions[app]
|
||||
ver.comms.v_remote = manifest.versions.comms
|
||||
ver.graphics.v_remote = manifest.versions.graphics
|
||||
ver.lockbox.v_remote = manifest.versions.lockbox
|
||||
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", 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)
|
||||
println("Installer download failed.");white()
|
||||
else
|
||||
local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location
|
||||
handle.write(dl.readAll())
|
||||
handle.close()
|
||||
green();println("Installer updated successfully.");white()
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
elseif update_installer then
|
||||
green();println("Installer already up-to-date.");white()
|
||||
return
|
||||
end
|
||||
|
||||
ver.boot.v_remote = 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
|
||||
println("Installing " .. app .. " files...")
|
||||
elseif mode == "update" then
|
||||
println("Updating " .. app .. " files... (keeping old config.lua)")
|
||||
end
|
||||
white()
|
||||
if mode == "install" then print("Installing ") else print("Updating ") end
|
||||
println(app.." files...");white()
|
||||
|
||||
-- display bootloader version change information
|
||||
show_pkg_change("bootldr", ver.boot.v_local, ver.boot.v_remote)
|
||||
ver.boot.changed = ver.boot.v_local ~= ver.boot.v_remote
|
||||
|
||||
-- display app version change information
|
||||
show_pkg_change(app, ver.app.v_local, ver.app.v_remote)
|
||||
ver.app.changed = ver.app.v_local ~= ver.app.v_remote
|
||||
|
||||
-- display comms version change information
|
||||
show_pkg_change("comms", ver.comms.v_local, ver.comms.v_remote)
|
||||
ver.comms.changed = ver.comms.v_local ~= ver.comms.v_remote
|
||||
ver.boot.changed = show_pkg_change("bootldr", ver.boot)
|
||||
ver.common.changed = show_pkg_change("common", ver.common)
|
||||
ver.comms.changed = show_pkg_change("comms", ver.comms)
|
||||
if ver.comms.changed and ver.comms.v_local ~= nil then
|
||||
print("[comms] ");yellow();println("other devices on the network will require an update");white()
|
||||
end
|
||||
ver.app.changed = show_pkg_change(app, ver.app)
|
||||
ver.graphics.changed = show_pkg_change("graphics", ver.graphics)
|
||||
ver.lockbox.changed = show_pkg_change("lockbox", ver.lockbox)
|
||||
|
||||
-- display graphics version change information
|
||||
show_pkg_change("graphics", ver.graphics.v_local, ver.graphics.v_remote)
|
||||
ver.graphics.changed = ver.graphics.v_local ~= ver.graphics.v_remote
|
||||
-- start install/update
|
||||
|
||||
-- display lockbox version change information
|
||||
show_pkg_change("lockbox", ver.lockbox.v_local, ver.lockbox.v_remote)
|
||||
ver.lockbox.changed = ver.lockbox.v_local ~= ver.lockbox.v_remote
|
||||
local space_req = r_manifest.sizes.manifest
|
||||
local space_avail = fs.getFreeSpace("/")
|
||||
|
||||
local file_list = r_manifest.files
|
||||
local size_list = r_manifest.sizes
|
||||
local deps = r_manifest.depends[app]
|
||||
|
||||
table.insert(deps, app)
|
||||
|
||||
-- helper function to check if a dependency is unchanged
|
||||
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
|
||||
|
||||
local any_change = false
|
||||
|
||||
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
|
||||
|
||||
--------------------------
|
||||
-- START INSTALL/UPDATE --
|
||||
--------------------------
|
||||
|
||||
local space_required = manifest.sizes.manifest
|
||||
local space_available = fs.getFreeSpace("/")
|
||||
|
||||
local single_file_mode = false
|
||||
local file_list = manifest.files
|
||||
local size_list = manifest.sizes
|
||||
local dependencies = manifest.depends[app]
|
||||
local config_file = app .. "/config.lua"
|
||||
|
||||
table.insert(dependencies, app)
|
||||
|
||||
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("WARNING: Insufficient space available for a full download!");white()
|
||||
println("Files can be downloaded one by one, so if you are replacing a current install this will not be a problem unless installation fails.")
|
||||
if mode == "update" then println("If installation still fails, delete this device's log file or uninstall the app (not purge) and try again.") end
|
||||
if not ask_y_n("Do you wish to continue?", false) then
|
||||
println("Operation cancelled.")
|
||||
return
|
||||
end
|
||||
end
|
||||
local single_file_mode = space_avail < space_req
|
||||
|
||||
local success = true
|
||||
|
||||
-- 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.app.changed or ver.comms.changed)
|
||||
elseif dependency == app then return not ver.app.changed
|
||||
else return true end
|
||||
-- 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
|
||||
|
||||
if not single_file_mode then
|
||||
if fs.exists(install_dir) then
|
||||
fs.delete(install_dir)
|
||||
fs.makeDir(install_dir)
|
||||
---@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
|
||||
|
||||
-- download all dependencies
|
||||
for _, dependency in pairs(dependencies) do
|
||||
if mode == "update" and unchanged(dependency) then
|
||||
pkg_message("skipping download of unchanged package", dependency)
|
||||
-- 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]
|
||||
for _, file in pairs(files) do
|
||||
println("GET " .. file)
|
||||
local dl, err = http.get(repo_path .. file)
|
||||
|
||||
if dl == nil then
|
||||
red();println("GET HTTP Error " .. err)
|
||||
success = false
|
||||
break
|
||||
-- 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
|
||||
local handle = fs.open(install_dir .. "/" .. file, "w")
|
||||
handle.write(dl.readAll())
|
||||
handle.close()
|
||||
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)
|
||||
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
|
||||
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
|
||||
if mode == "install" or file ~= config_file then
|
||||
local temp_file = install_dir .. "/" .. file
|
||||
if fs.exists(file) then fs.delete(file) end
|
||||
fs.move(temp_file, file)
|
||||
end
|
||||
local temp_file = install_dir.."/"..file
|
||||
if fs.exists(file) then fs.delete(file) end
|
||||
fs.move(temp_file, file)
|
||||
end
|
||||
end
|
||||
end
|
||||
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
|
||||
if mode == "install" or file ~= config_file then
|
||||
println("GET " .. file)
|
||||
local dl, err = http.get(repo_path .. file)
|
||||
|
||||
if dl == nil then
|
||||
red();println("GET HTTP Error " .. err)
|
||||
success = false
|
||||
break
|
||||
else
|
||||
local handle = fs.open("/" .. file, "w")
|
||||
handle.write(dl.readAll())
|
||||
handle.close()
|
||||
end
|
||||
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 == "remove" or mode == "purge" then
|
||||
elseif mode == "uninstall" then
|
||||
local ok, manifest = read_local_manifest()
|
||||
if not ok then
|
||||
red();println("Error parsing local installation manifest.");white()
|
||||
return
|
||||
elseif mode == "remove" and manifest.versions[app] == nil then
|
||||
red();println(app .. " is not installed, cannot remove.");white()
|
||||
end
|
||||
|
||||
if manifest.versions[app] == nil then
|
||||
red();println("Error: '"..app.."' is not installed.")
|
||||
return
|
||||
end
|
||||
|
||||
orange()
|
||||
if mode == "remove" then
|
||||
println("Removing all " .. app .. " files except for config and log...")
|
||||
elseif mode == "purge" then
|
||||
println("Purging all " .. app .. " files including config and log...")
|
||||
end
|
||||
orange();println("Uninstalling all "..app.." files...")
|
||||
|
||||
-- ask for confirmation
|
||||
if not ask_y_n("Continue", false) then return end
|
||||
@@ -535,77 +708,56 @@ elseif mode == "remove" or mode == "purge" then
|
||||
clean(manifest)
|
||||
|
||||
local file_list = manifest.files
|
||||
local dependencies = manifest.depends[app]
|
||||
local config_file = app .. "/config.lua"
|
||||
local deps = manifest.depends[app]
|
||||
|
||||
table.insert(dependencies, app)
|
||||
table.insert(deps, app)
|
||||
|
||||
-- delete log file if purging
|
||||
-- delete all installed files
|
||||
lgray()
|
||||
if mode == "purge" and fs.exists(config_file) then
|
||||
local 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)
|
||||
|
||||
if not log_deleted then
|
||||
red();println("failed to delete log file")
|
||||
white();println("press any key to continue...")
|
||||
any_key();lgray()
|
||||
end
|
||||
end
|
||||
|
||||
-- delete all files except config unless purging
|
||||
for _, dependency in pairs(dependencies) do
|
||||
local files = file_list[dependency]
|
||||
for _, dep in pairs(deps) do
|
||||
local files = file_list[dep]
|
||||
for _, file in pairs(files) do
|
||||
if mode == "purge" or file ~= config_file then
|
||||
if fs.exists(file) then fs.delete(file);println("deleted " .. file) end
|
||||
end
|
||||
if fs.exists(file) then fs.delete(file);println("deleted "..file) end
|
||||
end
|
||||
|
||||
-- delete folders that we should be deleteing
|
||||
if mode == "purge" or dependency ~= app then
|
||||
local folder = files[1]
|
||||
while true do
|
||||
local dir = fs.getDir(folder)
|
||||
if dir == "" or dir == ".." then break else folder = dir end
|
||||
end
|
||||
local folder = files[1]
|
||||
while true do
|
||||
local dir = fs.getDir(folder)
|
||||
if dir == "" or dir == ".." then break else folder = dir end
|
||||
end
|
||||
|
||||
if fs.isDir(folder) then
|
||||
fs.delete(folder)
|
||||
println("deleted directory " .. folder)
|
||||
end
|
||||
elseif dependency == app then
|
||||
-- delete individual subdirectories so we can leave the config
|
||||
for _, folder in pairs(files) do
|
||||
while true do
|
||||
local dir = fs.getDir(folder)
|
||||
if dir == "" or dir == ".." or dir == app then break else folder = dir end
|
||||
end
|
||||
if fs.isDir(folder) then
|
||||
fs.delete(folder)
|
||||
println("deleted directory "..folder)
|
||||
end
|
||||
end
|
||||
|
||||
if folder ~= app and fs.isDir(folder) then
|
||||
fs.delete(folder);println("deleted app subdirectory " .. folder)
|
||||
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
|
||||
|
||||
-- only delete manifest if purging
|
||||
if mode == "purge" then
|
||||
fs.delete("install_manifest.json")
|
||||
println("deleted install_manifest.json")
|
||||
else
|
||||
-- remove all data from versions list to show nothing is installed
|
||||
manifest.versions = {}
|
||||
local imfile = fs.open("install_manifest.json", "w")
|
||||
imfile.write(textutils.serializeJSON(manifest))
|
||||
imfile.close()
|
||||
if not log_deleted then
|
||||
red();println("Failed to delete log file (it may not exist).");lgray()
|
||||
end
|
||||
|
||||
if fs.exists(settings_file) then
|
||||
fs.delete(settings_file);println("deleted "..settings_file)
|
||||
end
|
||||
|
||||
fs.delete("install_manifest.json")
|
||||
println("deleted install_manifest.json")
|
||||
|
||||
green();println("Done!")
|
||||
end
|
||||
|
||||
|
||||
12
configure.lua
Normal file
12
configure.lua
Normal file
@@ -0,0 +1,12 @@
|
||||
print("CONFIGURE> SCANNING FOR CONFIGURATOR...")
|
||||
|
||||
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")
|
||||
@@ -1,38 +0,0 @@
|
||||
local config = {}
|
||||
|
||||
-- supervisor comms channel
|
||||
config.SVR_CHANNEL = 16240
|
||||
-- coordinator comms channel
|
||||
config.CRD_CHANNEL = 16243
|
||||
-- pocket comms channel
|
||||
config.PKT_CHANNEL = 16244
|
||||
-- max trusted modem message distance (0 to disable check)
|
||||
config.TRUSTED_RANGE = 0
|
||||
-- time in seconds (>= 2) before assuming a remote device is no longer active
|
||||
config.SV_TIMEOUT = 5
|
||||
config.API_TIMEOUT = 5
|
||||
-- facility authentication key (do NOT use one of your passwords)
|
||||
-- this enables verifying that messages are authentic
|
||||
-- all devices on the same network must use the same key
|
||||
-- config.AUTH_KEY = "SCADAfacility123"
|
||||
|
||||
-- expected number of reactor units, used only to require that number of unit monitors
|
||||
config.NUM_UNITS = 4
|
||||
|
||||
-- alarm sounder volume (0.0 to 3.0, 1.0 being standard max volume, this is the option given to to speaker.play())
|
||||
-- note: alarm sine waves are at half saturation, so that multiple will be required to reach full scale
|
||||
config.SOUNDER_VOLUME = 1.0
|
||||
|
||||
-- true for 24 hour time on main view screen
|
||||
config.TIME_24_HOUR = true
|
||||
|
||||
-- log path
|
||||
config.LOG_PATH = "/log.txt"
|
||||
-- log mode
|
||||
-- 0 = APPEND (adds to existing file on start)
|
||||
-- 1 = NEW (replaces existing file on start)
|
||||
config.LOG_MODE = 0
|
||||
-- true to log verbose debug messages
|
||||
config.LOG_DEBUG = false
|
||||
|
||||
return config
|
||||
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=g_lg_fg_bg}
|
||||
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
|
||||
450
coordinator/config/hmi.lua
Normal file
450
coordinator/config/hmi.lua
Normal file
@@ -0,0 +1,450 @@
|
||||
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=3,text="Configure the UI interface options below if you wish to customize formats."}
|
||||
|
||||
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=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=24,y=8,text="Energy Scale"}
|
||||
tool_ctl.energy_scale = RadioButton{parent=crd_c_1,x=24,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.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
|
||||
577
coordinator/config/system.lua
Normal file
577
coordinator/config/system.lua
Normal file
@@ -0,0 +1,577 @@
|
||||
|
||||
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.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] == "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 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
|
||||
387
coordinator/configure.lua
Normal file
387
coordinator/configure.lua
Normal file
@@ -0,0 +1,387 @@
|
||||
--
|
||||
-- Configuration GUI
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local facility = require("coordinator.config.facility")
|
||||
local hmi = require("coordinator.config.hmi")
|
||||
local system = require("coordinator.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 PushButton = require("graphics.elements.controls.PushButton")
|
||||
|
||||
local println = util.println
|
||||
local tri = util.trinary
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local CENTER = core.ALIGN.CENTER
|
||||
|
||||
-- changes to the config data/format to let the user know
|
||||
local changes = {
|
||||
{ "v1.2.4", { "Added temperature scale options" } },
|
||||
{ "v1.2.12", { "Added main UI theme", "Added front panel UI theme", "Added color accessibility modes" } },
|
||||
{ "v1.3.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
|
||||
{ "v1.5.1", { "Added energy scale options" } }
|
||||
}
|
||||
|
||||
---@class crd_configurator
|
||||
local configurator = {}
|
||||
|
||||
local style = {}
|
||||
|
||||
style.root = cpair(colors.black, colors.lightGray)
|
||||
style.header = cpair(colors.white, colors.gray)
|
||||
|
||||
style.colors = themes.smooth_stone.colors
|
||||
|
||||
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 _crd_cfg_tool_ctl
|
||||
local tool_ctl = {
|
||||
sv_cool_conf = nil, ---@type [ integer, integer ][] list of boiler & turbine counts
|
||||
|
||||
launch_startup = false,
|
||||
start_fail = 0,
|
||||
fail_message = "",
|
||||
has_config = false,
|
||||
viewing_config = false,
|
||||
jumped_to_color = false,
|
||||
|
||||
view_cfg = nil, ---@type PushButton
|
||||
color_cfg = nil, ---@type PushButton
|
||||
color_next = nil, ---@type PushButton
|
||||
color_apply = nil, ---@type PushButton
|
||||
settings_apply = nil, ---@type PushButton
|
||||
|
||||
gen_summary = nil, ---@type function
|
||||
load_legacy = nil, ---@type function
|
||||
|
||||
-- settings elements from hmi
|
||||
dis_flow_view = nil, ---@type Checkbox
|
||||
s_vol = nil, ---@type NumberField
|
||||
clock_fmt = nil, ---@type RadioButton
|
||||
temp_scale = nil, ---@type RadioButton
|
||||
energy_scale = nil, ---@type RadioButton
|
||||
|
||||
-- settings elements and functions from facility
|
||||
num_units = nil, ---@type NumberField
|
||||
init_sv_connect_ui = nil, ---@type function
|
||||
is_int_min_max = nil, ---@type function
|
||||
|
||||
update_mon_reqs = nil, ---@type function
|
||||
gen_mon_list = function () end
|
||||
}
|
||||
|
||||
---@class crd_config
|
||||
local tmp_cfg = {
|
||||
UnitCount = 1,
|
||||
SpeakerVolume = 1.0,
|
||||
Time24Hour = true,
|
||||
TempScale = 1, ---@type TEMP_SCALE
|
||||
EnergyScale = 1, ---@type ENERGY_SCALE
|
||||
DisableFlowView = false,
|
||||
MainDisplay = nil, ---@type string
|
||||
FlowDisplay = nil, ---@type string
|
||||
UnitDisplays = {}, ---@type string[]
|
||||
SVR_Channel = nil, ---@type integer
|
||||
CRD_Channel = nil, ---@type integer
|
||||
PKT_Channel = nil, ---@type integer
|
||||
SVR_Timeout = nil, ---@type number
|
||||
API_Timeout = nil, ---@type number
|
||||
TrustedRange = nil, ---@type number
|
||||
AuthKey = nil, ---@type string|nil
|
||||
LogMode = 0, ---@type LOG_MODE
|
||||
LogPath = "",
|
||||
LogDebug = false,
|
||||
MainTheme = 1, ---@type UI_THEME
|
||||
FrontPanelTheme = 1, ---@type FP_THEME
|
||||
ColorMode = 1 ---@type COLOR_MODE
|
||||
}
|
||||
|
||||
---@class crd_config
|
||||
local ini_cfg = {}
|
||||
---@class crd_config
|
||||
local settings_cfg = {}
|
||||
|
||||
-- all settings fields, their nice names, and their default values
|
||||
local fields = {
|
||||
{ "UnitCount", "Number of Reactors", 1 },
|
||||
{ "MainDisplay", "Main Monitor", nil },
|
||||
{ "FlowDisplay", "Flow Monitor", nil },
|
||||
{ "UnitDisplays", "Unit Monitors", {} },
|
||||
{ "SpeakerVolume", "Speaker Volume", 1.0 },
|
||||
{ "Time24Hour", "Use 24-hour Time Format", true },
|
||||
{ "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN },
|
||||
{ "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE },
|
||||
{ "DisableFlowView", "Disable Flow Monitor (legacy, discouraged)", false },
|
||||
{ "SVR_Channel", "SVR Channel", 16240 },
|
||||
{ "CRD_Channel", "CRD Channel", 16243 },
|
||||
{ "PKT_Channel", "PKT Channel", 16244 },
|
||||
{ "SVR_Timeout", "Supervisor Connection Timeout", 5 },
|
||||
{ "API_Timeout", "API Connection Timeout", 5 },
|
||||
{ "TrustedRange", "Trusted Range", 0 },
|
||||
{ "AuthKey", "Facility Auth Key" , ""},
|
||||
{ "LogMode", "Log Mode", log.MODE.APPEND },
|
||||
{ "LogPath", "Log Path", "/log.txt" },
|
||||
{ "LogDebug", "Log Debug Messages", false },
|
||||
{ "MainTheme", "Main UI Theme", themes.UI_THEME.SMOOTH_STONE },
|
||||
{ "FrontPanelTheme", "Front Panel Theme", themes.FP_THEME.SANDSTONE },
|
||||
{ "ColorMode", "Color Mode", themes.COLOR_MODE.STANDARD }
|
||||
}
|
||||
|
||||
-- load tmp_cfg fields from ini_cfg fields for displays
|
||||
local function preset_monitor_fields()
|
||||
tmp_cfg.DisableFlowView = ini_cfg.DisableFlowView
|
||||
|
||||
tmp_cfg.MainDisplay = ini_cfg.MainDisplay
|
||||
tmp_cfg.FlowDisplay = ini_cfg.FlowDisplay
|
||||
for i = 1, ini_cfg.UnitCount do
|
||||
tmp_cfg.UnitDisplays[i] = ini_cfg.UnitDisplays[i]
|
||||
end
|
||||
end
|
||||
|
||||
-- load data from the settings file
|
||||
---@param target crd_config
|
||||
---@param raw boolean? true to not use default values
|
||||
local function load_settings(target, raw)
|
||||
for _, v in pairs(fields) do settings.unset(v[1]) end
|
||||
|
||||
local loaded = settings.load("/coordinator.settings")
|
||||
|
||||
for _, v in pairs(fields) do target[v[1]] = settings.get(v[1], tri(raw, nil, v[3])) end
|
||||
|
||||
return loaded
|
||||
end
|
||||
|
||||
-- create the config view
|
||||
---@param display 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="Coordinator 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 net_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local fac_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local mon_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local spkr_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local crd_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local clr_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local summary = Div{parent=root_pane_div,x=1,y=1}
|
||||
local changelog = Div{parent=root_pane_div,x=1,y=1}
|
||||
|
||||
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,net_cfg,fac_cfg,mon_cfg,spkr_cfg,crd_cfg,log_cfg,clr_cfg,summary,changelog}}
|
||||
|
||||
--#region Main Page
|
||||
|
||||
local y_start = 5
|
||||
|
||||
TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the Coordinator configurator! Please select one of the following options."}
|
||||
|
||||
if tool_ctl.start_fail == 2 then
|
||||
local msg = util.c("Notice: There is a problem with your monitor configuration. ", tool_ctl.fail_message, " Please reconfigure monitors or correct their sizes.")
|
||||
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text=msg,fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
y_start = y_start + 5
|
||||
elseif tool_ctl.start_fail > 0 then
|
||||
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device had no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
y_start = y_start + 5
|
||||
end
|
||||
|
||||
local function view_config()
|
||||
tool_ctl.viewing_config = true
|
||||
tool_ctl.gen_summary(settings_cfg)
|
||||
tool_ctl.settings_apply.hide(true)
|
||||
main_pane.set_value(9)
|
||||
end
|
||||
|
||||
if fs.exists("/coordinator/config.lua") then
|
||||
PushButton{parent=main_page,x=2,y=y_start,min_width=28,text="Import Legacy 'config.lua'",callback=function()tool_ctl.load_legacy()end,fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=btn_act_fg_bg}
|
||||
y_start = y_start + 2
|
||||
end
|
||||
|
||||
PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure System",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
|
||||
local function jump_color()
|
||||
tool_ctl.jumped_to_color = true
|
||||
tool_ctl.color_next.hide(true)
|
||||
tool_ctl.color_apply.show()
|
||||
main_pane.set_value(8)
|
||||
end
|
||||
|
||||
local function startup()
|
||||
tool_ctl.launch_startup = true
|
||||
exit()
|
||||
end
|
||||
|
||||
PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
|
||||
local start_btn = PushButton{parent=main_page,x=42,y=17,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}
|
||||
tool_ctl.color_cfg = PushButton{parent=main_page,x=36,y=y_start,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
PushButton{parent=main_page,x=39,y=y_start+2,min_width=12,text="Change Log",callback=function()main_pane.set_value(10)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
if tool_ctl.start_fail ~= 0 then start_btn.disable() end
|
||||
|
||||
if not tool_ctl.has_config then
|
||||
tool_ctl.view_cfg.disable()
|
||||
tool_ctl.color_cfg.disable()
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
local settings = { settings_cfg, ini_cfg, tmp_cfg, fields, load_settings }
|
||||
|
||||
--#region Facility Configuration
|
||||
|
||||
local fac_pane = facility.create(tool_ctl, main_pane, settings, fac_cfg, style)
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region HMI Configuration
|
||||
|
||||
local mon_pane = hmi.create(tool_ctl, main_pane, settings, { mon_cfg, spkr_cfg, crd_cfg }, style)
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region System Configuration
|
||||
|
||||
local divs = { net_cfg, log_cfg, clr_cfg, summary }
|
||||
local ext = { fac_pane, mon_pane, preset_monitor_fields, exit }
|
||||
|
||||
system.create(tool_ctl, main_pane, settings, divs, ext, style)
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Config Change Log
|
||||
|
||||
local cl = Div{parent=changelog,x=2,y=4,width=49}
|
||||
|
||||
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=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
for _, change in ipairs(changes) do
|
||||
TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
|
||||
for _, v in ipairs(change[2]) do
|
||||
local e = Div{parent=c_log,height=#util.strwrap(v,46)}
|
||||
TextBox{parent=e,y=1,x=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
end
|
||||
|
||||
-- reset terminal screen
|
||||
local function reset_term()
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
end
|
||||
|
||||
-- run the coordinator configurator<br>
|
||||
-- start_fail of 0 is OK (default if not provided), 1 is bad config, 2 is bad monitor config
|
||||
---@param start_code? 0|1|2 indicate error state when called from the startup app
|
||||
---@param message? any string message to display on a start_fail of 2
|
||||
function configurator.configure(start_code, message)
|
||||
tool_ctl.start_fail = start_code or 0
|
||||
tool_ctl.fail_message = util.trinary(type(message) == "string", message, "")
|
||||
|
||||
load_settings(settings_cfg, true)
|
||||
tool_ctl.has_config = load_settings(ini_cfg)
|
||||
|
||||
-- copy in some important values to start with
|
||||
preset_monitor_fields()
|
||||
|
||||
reset_term()
|
||||
|
||||
ppm.mount_all()
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.colors do
|
||||
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||
end
|
||||
|
||||
local status, error = pcall(function ()
|
||||
local display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||
config_view(display)
|
||||
|
||||
tool_ctl.gen_mon_list()
|
||||
|
||||
while true do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
if event == "timer" then
|
||||
tcd.handle(param1)
|
||||
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
|
||||
local m_e = core.events.new_mouse_event(event, param1, param2, param3)
|
||||
if m_e then display.handle_mouse(m_e) end
|
||||
elseif event == "char" or event == "key" or event == "key_up" then
|
||||
local k_e = core.events.new_key_event(event, param1, param2)
|
||||
if k_e then display.handle_key(k_e) end
|
||||
elseif event == "paste" then
|
||||
display.handle_paste(param1)
|
||||
elseif event == "peripheral_detach" then
|
||||
---@diagnostic disable-next-line: discard-returns
|
||||
ppm.handle_unmount(param1)
|
||||
tool_ctl.gen_mon_list()
|
||||
elseif event == "peripheral" then
|
||||
---@diagnostic disable-next-line: discard-returns
|
||||
ppm.mount(param1)
|
||||
tool_ctl.gen_mon_list()
|
||||
elseif event == "monitor_resize" then
|
||||
tool_ctl.gen_mon_list()
|
||||
elseif event == "modem_message" then
|
||||
facility.receive_sv(param1, param2, param3, param4, param5)
|
||||
end
|
||||
|
||||
if event == "terminate" then return end
|
||||
end
|
||||
end)
|
||||
|
||||
-- restore colors
|
||||
for i = 1, #style.colors do
|
||||
local r, g, b = term.nativePaletteColor(style.colors[i].c)
|
||||
term.setPaletteColor(style.colors[i].c, r, g, b)
|
||||
end
|
||||
|
||||
reset_term()
|
||||
if not status then
|
||||
println("configurator error: " .. error)
|
||||
end
|
||||
|
||||
return status, error, tool_ctl.launch_startup
|
||||
end
|
||||
|
||||
return configurator
|
||||
@@ -4,21 +4,18 @@ local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
local types = require("scada-common.types")
|
||||
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local process = require("coordinator.process")
|
||||
|
||||
local apisessions = require("coordinator.session.apisessions")
|
||||
|
||||
local dialog = require("coordinator.ui.dialog")
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||
local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
|
||||
@@ -26,153 +23,169 @@ local LINK_TIMEOUT = 60.0
|
||||
|
||||
local coordinator = {}
|
||||
|
||||
-- request the user to select a monitor
|
||||
---@nodiscard
|
||||
---@param names table available monitors
|
||||
---@return boolean|string|nil
|
||||
local function ask_monitor(names)
|
||||
println("available monitors:")
|
||||
for i = 1, #names do
|
||||
print(" " .. names[i])
|
||||
end
|
||||
println("")
|
||||
println("select a monitor or type c to cancel")
|
||||
---@type crd_config
|
||||
local config = {}
|
||||
|
||||
local iface = dialog.ask_options(names, "c")
|
||||
coordinator.config = config
|
||||
|
||||
if iface ~= false and iface ~= nil then
|
||||
util.filter_table(names, function (x) return x ~= iface end)
|
||||
-- load the coordinator configuration<br>
|
||||
-- status of 0 is OK, 1 is bad config, 2 is bad monitor config
|
||||
---@return 0|1|2 status, nil|monitors_struct|string monitors (or error message)
|
||||
function coordinator.load_config()
|
||||
if not settings.load("/coordinator.settings") then return 1 end
|
||||
|
||||
config.UnitCount = settings.get("UnitCount")
|
||||
config.SpeakerVolume = settings.get("SpeakerVolume")
|
||||
config.Time24Hour = settings.get("Time24Hour")
|
||||
config.TempScale = settings.get("TempScale")
|
||||
config.EnergyScale = settings.get("EnergyScale")
|
||||
|
||||
config.DisableFlowView = settings.get("DisableFlowView")
|
||||
config.MainDisplay = settings.get("MainDisplay")
|
||||
config.FlowDisplay = settings.get("FlowDisplay")
|
||||
config.UnitDisplays = settings.get("UnitDisplays")
|
||||
|
||||
config.SVR_Channel = settings.get("SVR_Channel")
|
||||
config.CRD_Channel = settings.get("CRD_Channel")
|
||||
config.PKT_Channel = settings.get("PKT_Channel")
|
||||
config.SVR_Timeout = settings.get("SVR_Timeout")
|
||||
config.API_Timeout = settings.get("API_Timeout")
|
||||
config.TrustedRange = settings.get("TrustedRange")
|
||||
config.AuthKey = settings.get("AuthKey")
|
||||
|
||||
config.LogMode = settings.get("LogMode")
|
||||
config.LogPath = settings.get("LogPath")
|
||||
config.LogDebug = settings.get("LogDebug")
|
||||
|
||||
config.MainTheme = settings.get("MainTheme")
|
||||
config.FrontPanelTheme = settings.get("FrontPanelTheme")
|
||||
config.ColorMode = settings.get("ColorMode")
|
||||
|
||||
local cfv = util.new_validator()
|
||||
|
||||
cfv.assert_type_int(config.UnitCount)
|
||||
cfv.assert_range(config.UnitCount, 1, 4)
|
||||
cfv.assert_type_bool(config.Time24Hour)
|
||||
cfv.assert_type_int(config.TempScale)
|
||||
cfv.assert_range(config.TempScale, 1, 4)
|
||||
cfv.assert_type_int(config.EnergyScale)
|
||||
cfv.assert_range(config.EnergyScale, 1, 3)
|
||||
|
||||
cfv.assert_type_bool(config.DisableFlowView)
|
||||
cfv.assert_type_table(config.UnitDisplays)
|
||||
|
||||
cfv.assert_type_num(config.SpeakerVolume)
|
||||
cfv.assert_range(config.SpeakerVolume, 0, 3)
|
||||
|
||||
cfv.assert_channel(config.SVR_Channel)
|
||||
cfv.assert_channel(config.CRD_Channel)
|
||||
cfv.assert_channel(config.PKT_Channel)
|
||||
|
||||
cfv.assert_type_num(config.SVR_Timeout)
|
||||
cfv.assert_min(config.SVR_Timeout, 2)
|
||||
cfv.assert_type_num(config.API_Timeout)
|
||||
cfv.assert_min(config.API_Timeout, 2)
|
||||
|
||||
cfv.assert_type_num(config.TrustedRange)
|
||||
cfv.assert_min(config.TrustedRange, 0)
|
||||
cfv.assert_type_str(config.AuthKey)
|
||||
|
||||
if type(config.AuthKey) == "string" then
|
||||
local len = string.len(config.AuthKey)
|
||||
cfv.assert(len == 0 or len >= 8)
|
||||
end
|
||||
|
||||
return iface
|
||||
end
|
||||
cfv.assert_type_int(config.LogMode)
|
||||
cfv.assert_range(config.LogMode, 0, 1)
|
||||
cfv.assert_type_str(config.LogPath)
|
||||
cfv.assert_type_bool(config.LogDebug)
|
||||
|
||||
cfv.assert_type_int(config.MainTheme)
|
||||
cfv.assert_range(config.MainTheme, 1, 2)
|
||||
cfv.assert_type_int(config.FrontPanelTheme)
|
||||
cfv.assert_range(config.FrontPanelTheme, 1, 2)
|
||||
cfv.assert_type_int(config.ColorMode)
|
||||
cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
|
||||
|
||||
-- Monitor Setup
|
||||
|
||||
-- configure monitor layout
|
||||
---@param num_units integer number of units expected
|
||||
---@return boolean success, monitors_struct? monitors
|
||||
function coordinator.configure_monitors(num_units)
|
||||
---@class monitors_struct
|
||||
local monitors = {
|
||||
primary = nil,
|
||||
primary_name = "",
|
||||
unit_displays = {},
|
||||
unit_name_map = {}
|
||||
main = nil, ---@type Monitor|nil
|
||||
main_name = "",
|
||||
flow = nil, ---@type Monitor|nil
|
||||
flow_name = "",
|
||||
unit_displays = {}, ---@type Monitor[]
|
||||
unit_name_map = {} ---@type string[]
|
||||
}
|
||||
|
||||
local monitors_avail = ppm.get_monitor_list()
|
||||
local names = {}
|
||||
local available = {}
|
||||
local mon_cfv = util.new_validator()
|
||||
|
||||
-- get all interface names
|
||||
for iface, _ in pairs(monitors_avail) do
|
||||
table.insert(names, iface)
|
||||
table.insert(available, iface)
|
||||
end
|
||||
local names = {}
|
||||
for iface, _ in pairs(ppm.get_monitor_list()) do table.insert(names, iface) end
|
||||
|
||||
-- we need a certain number of monitors (1 per unit + 1 primary display)
|
||||
local num_displays_needed = num_units + 1
|
||||
if #names < num_displays_needed then
|
||||
local message = "not enough monitors connected (need " .. num_displays_needed .. ")"
|
||||
println(message)
|
||||
log.warning(message)
|
||||
return false
|
||||
end
|
||||
local function setup_monitors()
|
||||
mon_cfv.assert_type_str(config.MainDisplay)
|
||||
if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end
|
||||
mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount)
|
||||
|
||||
-- attempt to load settings
|
||||
if not settings.load("/coord.settings") then
|
||||
log.warning("configure_monitors(): failed to load coordinator settings file (may not exist yet)")
|
||||
else
|
||||
local _primary = settings.get("PRIMARY_DISPLAY")
|
||||
local _unitd = settings.get("UNIT_DISPLAYS")
|
||||
if mon_cfv.valid() then
|
||||
local w, h, _
|
||||
|
||||
-- filter out already assigned monitors
|
||||
util.filter_table(available, function (x) return x ~= _primary end)
|
||||
if type(_unitd) == "table" then
|
||||
util.filter_table(available, function (x) return not util.table_contains(_unitd, x) end)
|
||||
end
|
||||
end
|
||||
|
||||
---------------------
|
||||
-- PRIMARY DISPLAY --
|
||||
---------------------
|
||||
|
||||
local iface_primary_display = settings.get("PRIMARY_DISPLAY") ---@type boolean|string|nil
|
||||
|
||||
if not util.table_contains(names, iface_primary_display) then
|
||||
println("primary display is not connected")
|
||||
local response = dialog.ask_y_n("would you like to change it", true)
|
||||
if response == false then return false end
|
||||
iface_primary_display = nil
|
||||
end
|
||||
|
||||
while iface_primary_display == nil and #available > 0 do
|
||||
-- lets get a monitor
|
||||
iface_primary_display = ask_monitor(available)
|
||||
end
|
||||
|
||||
if type(iface_primary_display) ~= "string" then return false end
|
||||
|
||||
settings.set("PRIMARY_DISPLAY", iface_primary_display)
|
||||
util.filter_table(available, function (x) return x ~= iface_primary_display end)
|
||||
|
||||
monitors.primary = ppm.get_periph(iface_primary_display)
|
||||
monitors.primary_name = iface_primary_display
|
||||
|
||||
-------------------
|
||||
-- UNIT DISPLAYS --
|
||||
-------------------
|
||||
|
||||
local unit_displays = settings.get("UNIT_DISPLAYS")
|
||||
|
||||
if unit_displays == nil then
|
||||
unit_displays = {}
|
||||
for i = 1, num_units do
|
||||
local display = nil
|
||||
|
||||
while display == nil and #available > 0 do
|
||||
-- lets get a monitor
|
||||
println("please select monitor for unit #" .. i)
|
||||
display = ask_monitor(available)
|
||||
if not util.table_contains(names, config.MainDisplay) then
|
||||
return 2, "Main monitor is not connected."
|
||||
end
|
||||
|
||||
if display == false then return false end
|
||||
monitors.main = ppm.get_periph(config.MainDisplay)
|
||||
monitors.main_name = config.MainDisplay
|
||||
|
||||
unit_displays[i] = display
|
||||
end
|
||||
else
|
||||
-- make sure all displays are connected
|
||||
for i = 1, num_units do
|
||||
local display = unit_displays[i]
|
||||
|
||||
if not util.table_contains(names, display) then
|
||||
println("unit #" .. i .. " display is not connected")
|
||||
local response = dialog.ask_y_n("would you like to change it", true)
|
||||
if response == false then return false end
|
||||
display = nil
|
||||
monitors.main.setTextScale(0.5)
|
||||
w, _ = ppm.monitor_block_size(monitors.main.getSize())
|
||||
if w ~= 8 then
|
||||
return 2, util.c("Main monitor width is incorrect (was ", w, ", must be 8).")
|
||||
end
|
||||
|
||||
while display == nil and #available > 0 do
|
||||
-- lets get a monitor
|
||||
display = ask_monitor(available)
|
||||
if not config.DisableFlowView then
|
||||
if not util.table_contains(names, config.FlowDisplay) then
|
||||
return 2, "Flow monitor is not connected."
|
||||
end
|
||||
|
||||
monitors.flow = ppm.get_periph(config.FlowDisplay)
|
||||
monitors.flow_name = config.FlowDisplay
|
||||
|
||||
monitors.flow.setTextScale(0.5)
|
||||
w, _ = ppm.monitor_block_size(monitors.flow.getSize())
|
||||
if w ~= 8 then
|
||||
return 2, util.c("Flow monitor width is incorrect (was ", w, ", must be 8).")
|
||||
end
|
||||
end
|
||||
|
||||
if display == false then return false end
|
||||
for i = 1, config.UnitCount do
|
||||
local display = config.UnitDisplays[i]
|
||||
if type(display) ~= "string" or not util.table_contains(names, display) then
|
||||
return 2, "Unit " .. i .. " monitor is not connected."
|
||||
end
|
||||
|
||||
unit_displays[i] = display
|
||||
end
|
||||
monitors.unit_displays[i] = ppm.get_periph(display)
|
||||
monitors.unit_name_map[i] = display
|
||||
|
||||
monitors.unit_displays[i].setTextScale(0.5)
|
||||
w, h = ppm.monitor_block_size(monitors.unit_displays[i].getSize())
|
||||
if w ~= 4 or h ~= 4 then
|
||||
return 2, util.c("Unit ", i, " monitor size is incorrect (was ", w, " by ", h,", must be 4 by 4).")
|
||||
end
|
||||
end
|
||||
else return 2, "Monitor configuration invalid." end
|
||||
end
|
||||
|
||||
settings.set("UNIT_DISPLAYS", unit_displays)
|
||||
if not settings.save("/coord.settings") then
|
||||
log.warning("configure_monitors(): failed to save coordinator settings file")
|
||||
end
|
||||
if cfv.valid() then
|
||||
local ok, result, message = pcall(setup_monitors)
|
||||
assert(ok, util.c("fatal error while trying to verify monitors: ", result))
|
||||
if result == 2 then return 2, message end
|
||||
else return 1 end
|
||||
|
||||
for i = 1, #unit_displays do
|
||||
monitors.unit_displays[i] = ppm.get_periph(unit_displays[i])
|
||||
monitors.unit_name_map[i] = unit_displays[i]
|
||||
end
|
||||
|
||||
return true, monitors
|
||||
return 0, monitors
|
||||
end
|
||||
|
||||
-- dmesg print wrapper
|
||||
@@ -182,7 +195,7 @@ end
|
||||
---@return function? update, function? done
|
||||
local function log_dmesg(message, dmesg_tag, working)
|
||||
local colors = {
|
||||
GRAPHICS = colors.green,
|
||||
RENDER = colors.green,
|
||||
SYSTEM = colors.cyan,
|
||||
BOOT = colors.blue,
|
||||
COMMS = colors.purple,
|
||||
@@ -196,7 +209,7 @@ local function log_dmesg(message, dmesg_tag, working)
|
||||
end
|
||||
end
|
||||
|
||||
function coordinator.log_graphics(message) log_dmesg(message, "GRAPHICS") end
|
||||
function coordinator.log_render(message) log_dmesg(message, "RENDER") end
|
||||
function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end
|
||||
function coordinator.log_boot(message) log_dmesg(message, "BOOT") end
|
||||
function coordinator.log_comms(message) log_dmesg(message, "COMMS") end
|
||||
@@ -217,17 +230,13 @@ end
|
||||
---@nodiscard
|
||||
---@param version string coordinator version
|
||||
---@param nic nic network interface device
|
||||
---@param crd_channel integer port of configured supervisor
|
||||
---@param svr_channel integer listening port for supervisor replys
|
||||
---@param pkt_channel integer listening port for pocket API
|
||||
---@param range integer trusted device connection range
|
||||
---@param sv_watchdog watchdog
|
||||
function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel, range, sv_watchdog)
|
||||
function coordinator.comms(version, nic, sv_watchdog)
|
||||
local self = {
|
||||
sv_linked = false,
|
||||
sv_addr = comms.BROADCAST,
|
||||
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 = {},
|
||||
@@ -237,19 +246,19 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
est_task_done = nil
|
||||
}
|
||||
|
||||
comms.set_trusted_range(range)
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
comms.set_trusted_range(config.TrustedRange)
|
||||
|
||||
-- configure network channels
|
||||
nic.closeAll()
|
||||
nic.open(crd_channel)
|
||||
nic.open(config.CRD_Channel)
|
||||
|
||||
-- link nic to apisessions
|
||||
apisessions.init(nic)
|
||||
-- pass config to apisessions
|
||||
apisessions.init(nic, config)
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- send a packet to the supervisor
|
||||
---@param msg_type SCADA_MGMT_TYPE|SCADA_CRDN_TYPE
|
||||
---@param msg_type MGMT_TYPE|CRDN_TYPE
|
||||
---@param msg table
|
||||
local function _send_sv(protocol, msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
@@ -266,33 +275,35 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable())
|
||||
|
||||
nic.transmit(svr_channel, crd_channel, s_pkt)
|
||||
nic.transmit(config.SVR_Channel, config.CRD_Channel, s_pkt)
|
||||
self.sv_seq_num = self.sv_seq_num + 1
|
||||
end
|
||||
|
||||
-- send an API establish request response
|
||||
---@param packet scada_packet
|
||||
---@param ack ESTABLISH_ACK
|
||||
local function _send_api_establish_ack(packet, ack)
|
||||
---@param data any?
|
||||
local function _send_api_establish_ack(packet, ack, data)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
|
||||
m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, { ack })
|
||||
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data })
|
||||
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
nic.transmit(pkt_channel, crd_channel, s_pkt)
|
||||
nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt)
|
||||
self.last_api_est_acks[packet.src_addr()] = ack
|
||||
end
|
||||
|
||||
-- attempt connection establishment
|
||||
local function _send_establish()
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRDN })
|
||||
self.sv_r_seq_num = nil
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRD })
|
||||
end
|
||||
|
||||
-- keep alive ack
|
||||
---@param srv_time integer
|
||||
local function _send_keep_alive_ack(srv_time)
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
@@ -309,24 +320,24 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
|
||||
if not self.sv_linked then
|
||||
if self.est_tick_waiting == nil then
|
||||
self.est_start = util.time_s()
|
||||
self.est_start = os.clock()
|
||||
self.est_last = self.est_start
|
||||
|
||||
self.est_tick_waiting, self.est_task_done =
|
||||
coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. svr_channel)
|
||||
coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SVR_Channel)
|
||||
|
||||
_send_establish()
|
||||
else
|
||||
self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (util.time_s() - self.est_start)))
|
||||
self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (os.clock() - self.est_start)))
|
||||
end
|
||||
|
||||
if abort or (util.time_s() - self.est_start) >= LINK_TIMEOUT then
|
||||
if abort or (os.clock() - self.est_start) >= LINK_TIMEOUT then
|
||||
self.est_task_done(false)
|
||||
|
||||
if abort then
|
||||
coordinator.log_comms("supervisor connection attempt cancelled by user")
|
||||
elseif self.sv_config_err then
|
||||
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
|
||||
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
|
||||
elseif not self.sv_linked then
|
||||
if self.last_est_ack == ESTABLISH_ACK.DENY then
|
||||
coordinator.log_comms("supervisor connection attempt denied")
|
||||
@@ -341,11 +352,12 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
|
||||
ok = false
|
||||
elseif self.sv_config_err then
|
||||
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
|
||||
self.est_task_done(false)
|
||||
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
|
||||
ok = false
|
||||
elseif (util.time_s() - self.est_last) > 1.0 then
|
||||
elseif (os.clock() - self.est_last) > 1.0 then
|
||||
_send_establish()
|
||||
self.est_last = util.time_s()
|
||||
self.est_last = os.clock()
|
||||
end
|
||||
elseif self.est_tick_waiting ~= nil then
|
||||
self.est_task_done(true)
|
||||
@@ -364,21 +376,25 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
self.sv_linked = false
|
||||
self.sv_r_seq_num = nil
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {})
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {})
|
||||
end
|
||||
|
||||
-- send a facility command
|
||||
---@param cmd FAC_COMMAND command
|
||||
---@param option any? optional option options for the optional options (like waste mode)
|
||||
function public.send_fac_command(cmd, option)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd, option })
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, { cmd, option })
|
||||
end
|
||||
|
||||
-- send the auto process control configuration with a start command
|
||||
---@param config coord_auto_config configuration
|
||||
function public.send_auto_start(config)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, {
|
||||
FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits
|
||||
---@param 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, mode, burn_target, charge_target, gen_target, limits
|
||||
})
|
||||
end
|
||||
|
||||
@@ -387,7 +403,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
---@param unit integer unit ID
|
||||
---@param option any? optional option options for the optional options (like burn rate)
|
||||
function public.send_unit_command(cmd, unit, option)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
|
||||
end
|
||||
|
||||
-- parse a packet
|
||||
@@ -396,7 +412,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
---@param reply_to integer
|
||||
---@param message any
|
||||
---@param distance integer
|
||||
---@return mgmt_frame|crdn_frame|capi_frame|nil packet
|
||||
---@return mgmt_frame|crdn_frame|nil packet
|
||||
function public.parse_packet(side, sender, reply_to, message, distance)
|
||||
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
|
||||
local pkt = nil
|
||||
@@ -414,12 +430,6 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
if crdn_pkt.decode(s_pkt) then
|
||||
pkt = crdn_pkt.get()
|
||||
end
|
||||
-- get as coordinator API packet
|
||||
elseif s_pkt.protocol() == PROTOCOL.COORD_API then
|
||||
local capi_pkt = comms.capi_packet()
|
||||
if capi_pkt.decode(s_pkt) then
|
||||
pkt = capi_pkt.get()
|
||||
end
|
||||
else
|
||||
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
|
||||
end
|
||||
@@ -429,7 +439,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
end
|
||||
|
||||
-- handle a packet
|
||||
---@param packet mgmt_frame|crdn_frame|capi_frame|nil
|
||||
---@param packet mgmt_frame|crdn_frame|nil
|
||||
---@return boolean close_ui
|
||||
function public.handle_packet(packet)
|
||||
local was_linked = self.sv_linked
|
||||
@@ -440,23 +450,23 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
local src_addr = packet.scada_frame.src_addr()
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
|
||||
if l_chan ~= crd_channel then
|
||||
if l_chan ~= config.CRD_Channel then
|
||||
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
||||
elseif r_chan == pkt_channel then
|
||||
elseif r_chan == config.PKT_Channel then
|
||||
if not self.sv_linked then
|
||||
log.debug("discarding pocket API packet before linked to supervisor")
|
||||
elseif protocol == PROTOCOL.COORD_API then
|
||||
---@cast packet capi_frame
|
||||
elseif protocol == PROTOCOL.SCADA_CRDN then
|
||||
---@cast packet crdn_frame
|
||||
-- look for an associated session
|
||||
local session = apisessions.find_session(src_addr)
|
||||
|
||||
-- API packet
|
||||
-- coordinator packet
|
||||
if session ~= nil then
|
||||
-- pass the packet onto the session handler
|
||||
session.in_queue.push_packet(packet)
|
||||
else
|
||||
-- any other packet should be session related, discard it
|
||||
log.debug("discarding COORD_API packet without a known session")
|
||||
log.debug("discarding SCADA_CRDN packet without a known session")
|
||||
end
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
@@ -467,13 +477,14 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
if session ~= nil then
|
||||
-- pass the packet onto the session handler
|
||||
session.in_queue.push_packet(packet)
|
||||
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
elseif packet.type == MGMT_TYPE.ESTABLISH then
|
||||
-- establish a new session
|
||||
-- validate packet and continue
|
||||
if packet.length == 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
|
||||
local comms_v = packet.data[1]
|
||||
local firmware_v = packet.data[2]
|
||||
if packet.length == 4 then
|
||||
local comms_v = util.strval(packet.data[1])
|
||||
local firmware_v = util.strval(packet.data[2])
|
||||
local dev_type = packet.data[3]
|
||||
local api_v = util.strval(packet.data[4])
|
||||
|
||||
if comms_v ~= comms.version then
|
||||
if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_VERSION then
|
||||
@@ -481,12 +492,19 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
end
|
||||
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
|
||||
elseif api_v ~= comms.api_version then
|
||||
if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_API_VERSION then
|
||||
log.info(util.c("dropping API establish packet with incorrect api version v", api_v, " (expected v", comms.api_version, ")"))
|
||||
end
|
||||
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_API_VERSION)
|
||||
elseif dev_type == DEVICE_TYPE.PKT then
|
||||
-- pocket linking request
|
||||
local id = apisessions.establish_session(src_addr, firmware_v)
|
||||
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))
|
||||
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW)
|
||||
local conf = iocontrol.get_db().facility.conf
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW, { conf.num_units, conf.cooling })
|
||||
else
|
||||
log.debug(util.c("API_ESTABLISH: illegal establish packet for device ", dev_type, " on pocket channel"))
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
|
||||
@@ -502,18 +520,18 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
else
|
||||
log.debug("illegal packet type " .. protocol .. " on pocket channel", true)
|
||||
end
|
||||
elseif r_chan == svr_channel then
|
||||
elseif r_chan == config.SVR_Channel then
|
||||
-- check sequence number
|
||||
if self.sv_r_seq_num == nil then
|
||||
self.sv_r_seq_num = packet.scada_frame.seq_num()
|
||||
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
|
||||
@@ -523,7 +541,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
if protocol == PROTOCOL.SCADA_CRDN then
|
||||
---@cast packet crdn_frame
|
||||
if self.sv_linked then
|
||||
if packet.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then
|
||||
if packet.type == CRDN_TYPE.INITIAL_BUILDS then
|
||||
if packet.length == 2 then
|
||||
-- record builds
|
||||
local fac_builds = iocontrol.record_facility_builds(packet.data[1])
|
||||
@@ -531,40 +549,40 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
|
||||
if fac_builds and unit_builds then
|
||||
-- acknowledge receipt of builds
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.INITIAL_BUILDS, {})
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.INITIAL_BUILDS, {})
|
||||
else
|
||||
log.debug("received invalid INITIAL_BUILDS packet")
|
||||
end
|
||||
else
|
||||
log.debug("INITIAL_BUILDS packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.FAC_BUILDS then
|
||||
elseif packet.type == CRDN_TYPE.FAC_BUILDS then
|
||||
if packet.length == 1 then
|
||||
-- record facility builds
|
||||
if iocontrol.record_facility_builds(packet.data[1]) then
|
||||
-- acknowledge receipt of builds
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_BUILDS, {})
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_BUILDS, {})
|
||||
else
|
||||
log.debug("received invalid FAC_BUILDS packet")
|
||||
end
|
||||
else
|
||||
log.debug("FAC_BUILDS packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.FAC_STATUS then
|
||||
elseif packet.type == CRDN_TYPE.FAC_STATUS then
|
||||
-- update facility status
|
||||
if not iocontrol.update_facility_status(packet.data) then
|
||||
log.debug("received invalid FAC_STATUS packet")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.FAC_CMD then
|
||||
elseif packet.type == CRDN_TYPE.FAC_CMD then
|
||||
-- facility command acknowledgement
|
||||
if packet.length >= 2 then
|
||||
local cmd = packet.data[1]
|
||||
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) })
|
||||
@@ -572,60 +590,56 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
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
|
||||
else
|
||||
log.debug("SCADA_CRDN facility command ack packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.UNIT_BUILDS then
|
||||
elseif packet.type == CRDN_TYPE.UNIT_BUILDS then
|
||||
-- record builds
|
||||
if packet.length == 1 then
|
||||
if iocontrol.record_unit_builds(packet.data[1]) then
|
||||
-- acknowledge receipt of builds
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_BUILDS, {})
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_BUILDS, {})
|
||||
else
|
||||
log.debug("received invalid UNIT_BUILDS packet")
|
||||
end
|
||||
else
|
||||
log.debug("UNIT_BUILDS packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.UNIT_STATUSES then
|
||||
elseif packet.type == CRDN_TYPE.UNIT_STATUSES then
|
||||
-- update statuses
|
||||
if not iocontrol.update_unit_statuses(packet.data) then
|
||||
log.debug("received invalid UNIT_STATUSES packet")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.UNIT_CMD then
|
||||
elseif packet.type == CRDN_TYPE.UNIT_CMD then
|
||||
-- unit command acknowledgement
|
||||
if packet.length == 3 then
|
||||
local cmd = packet.data[1]
|
||||
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))
|
||||
@@ -642,7 +656,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
if self.sv_linked then
|
||||
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
if packet.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if packet.length == 1 then
|
||||
local timestamp = packet.data[1]
|
||||
@@ -660,7 +674,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
else
|
||||
log.debug("SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
|
||||
elseif packet.type == MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
sv_watchdog.cancel()
|
||||
self.sv_addr = comms.BROADCAST
|
||||
@@ -671,43 +685,37 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
else
|
||||
log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
elseif packet.type == MGMT_TYPE.ESTABLISH then
|
||||
-- connection with supervisor established
|
||||
if packet.length == 2 then
|
||||
local est_ack = packet.data[1]
|
||||
local config = packet.data[2]
|
||||
local sv_config = packet.data[2]
|
||||
|
||||
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||
-- reset to disconnected before validating
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
|
||||
if type(config) == "table" and #config > 1 then
|
||||
if type(sv_config) == "table" and #sv_config == 2 then
|
||||
-- get configuration
|
||||
|
||||
---@class facility_conf
|
||||
local conf = {
|
||||
num_units = config[1], ---@type integer
|
||||
defs = {} -- boilers and turbines
|
||||
num_units = sv_config[1], ---@type integer
|
||||
cooling = sv_config[2] ---@type sv_cooling_conf
|
||||
}
|
||||
|
||||
if (#config - 1) == (conf.num_units * 2) then
|
||||
-- record sequence of pairs of [#boilers, #turbines] per unit
|
||||
for i = 2, #config do
|
||||
table.insert(conf.defs, config[i])
|
||||
end
|
||||
|
||||
if conf.num_units == config.UnitCount then
|
||||
-- init io controller
|
||||
iocontrol.init(conf, public)
|
||||
iocontrol.init(conf, public, config.TempScale, 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)
|
||||
else
|
||||
self.sv_config_err = true
|
||||
log.warning("invalid supervisor configuration definitions received, establish failed")
|
||||
log.warning("supervisor config's number of units don't match coordinator's config, establish failed")
|
||||
end
|
||||
else
|
||||
log.debug("invalid supervisor configuration table received, establish failed")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,179 +7,423 @@ 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_auto_config
|
||||
config = {
|
||||
mode = PROCESS.INACTIVE,
|
||||
burn_target = 0.0,
|
||||
charge_target = 0.0,
|
||||
gen_target = 0.0,
|
||||
limits = {},
|
||||
waste_product = PRODUCT.PLUTONIUM,
|
||||
pu_fallback = false
|
||||
local pctl = {
|
||||
io = nil, ---@type ioctl
|
||||
comms = nil, ---@type coord_comms
|
||||
---@class sys_control_states
|
||||
control_states = {
|
||||
---@class sys_auto_config
|
||||
process = {
|
||||
mode = PROCESS.INACTIVE, ---@type PROCESS
|
||||
burn_target = 0.0,
|
||||
charge_target = 0.0,
|
||||
gen_target = 0.0,
|
||||
limits = {}, ---@type number[]
|
||||
waste_product = PRODUCT.PLUTONIUM, ---@type WASTE_PRODUCT
|
||||
pu_fallback = false,
|
||||
sps_low_power = false
|
||||
},
|
||||
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
|
||||
|
||||
for i = 1, self.io.facility.num_units do
|
||||
self.config.limits[i] = 0.1
|
||||
-- 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
|
||||
|
||||
-- load settings
|
||||
if not settings.load("/coord.settings") then
|
||||
log.error("process.init(): failed to load coordinator settings file")
|
||||
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", {}) ---@type sys_control_states
|
||||
local config = ctrl_states.process
|
||||
|
||||
-- facility auto control configuration
|
||||
local config = settings.get("PROCESS") ---@type coord_auto_config|nil
|
||||
if type(config) == "table" then
|
||||
self.config.mode = config.mode
|
||||
self.config.burn_target = config.burn_target
|
||||
self.config.charge_target = config.charge_target
|
||||
self.config.gen_target = config.gen_target
|
||||
self.config.limits = config.limits
|
||||
self.config.waste_product = config.waste_product
|
||||
self.config.pu_fallback = config.pu_fallback
|
||||
ctl_proc.mode = config.mode
|
||||
ctl_proc.burn_target = config.burn_target
|
||||
ctl_proc.charge_target = config.charge_target
|
||||
ctl_proc.gen_target = config.gen_target
|
||||
ctl_proc.limits = config.limits
|
||||
ctl_proc.waste_product = config.waste_product
|
||||
ctl_proc.pu_fallback = config.pu_fallback
|
||||
ctl_proc.sps_low_power = config.sps_low_power
|
||||
|
||||
self.io.facility.ps.publish("process_mode", self.config.mode)
|
||||
self.io.facility.ps.publish("process_burn_target", self.config.burn_target)
|
||||
self.io.facility.ps.publish("process_charge_target", self.config.charge_target)
|
||||
self.io.facility.ps.publish("process_gen_target", self.config.gen_target)
|
||||
self.io.facility.ps.publish("process_waste_product", self.config.waste_product)
|
||||
self.io.facility.ps.publish("process_pu_fallback", self.config.pu_fallback)
|
||||
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(#self.config.limits, self.io.facility.num_units) do
|
||||
local unit = self.io.units[id] ---@type ioctl_unit
|
||||
unit.unit_ps.publish("burn_limit", self.config.limits[id])
|
||||
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 from coord.settings")
|
||||
log.info("PROCESS: loaded auto control settings")
|
||||
|
||||
-- notify supervisor of auto waste config
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, self.config.waste_product)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, self.config.pu_fallback)
|
||||
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 = settings.get("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.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 from coord.settings")
|
||||
log.info("PROCESS: loaded unit waste mode settings")
|
||||
end
|
||||
|
||||
-- unit priority groups
|
||||
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil
|
||||
local prio_groups = ctrl_states.priority_groups
|
||||
if type(prio_groups) == "table" then
|
||||
for id, group in pairs(prio_groups) do
|
||||
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 from coord.settings")
|
||||
log.info("PROCESS: loaded priority groups settings")
|
||||
end
|
||||
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
|
||||
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))
|
||||
|
||||
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil
|
||||
pctl.control_states.waste_modes[id] = mode
|
||||
settings.set("ControlStates", pctl.control_states)
|
||||
|
||||
if type(waste_mode) ~= "table" then waste_mode = {} end
|
||||
|
||||
waste_mode[id] = mode
|
||||
|
||||
settings.set("WASTE_MODES", waste_mode)
|
||||
|
||||
if not settings.save("/coord.settings") then
|
||||
if not settings.save("/coordinator.settings") then
|
||||
log.error("process.set_unit_waste(): failed to save coordinator settings file")
|
||||
end
|
||||
end
|
||||
|
||||
-- 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
|
||||
|
||||
@@ -187,106 +431,56 @@ 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))
|
||||
|
||||
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil
|
||||
|
||||
if type(prio_groups) ~= "table" then prio_groups = {} end
|
||||
|
||||
prio_groups[unit_id] = group_id
|
||||
|
||||
settings.set("PRIORITY_GROUPS", prio_groups)
|
||||
|
||||
if not settings.save("/coord.settings") then
|
||||
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()
|
||||
-- attempt to load settings
|
||||
if not settings.load("/coord.settings") then
|
||||
log.warning("process._write_auto_config(): failed to load coordinator settings file")
|
||||
end
|
||||
|
||||
-- save config
|
||||
settings.set("PROCESS", self.config)
|
||||
local saved = settings.save("/coord.settings")
|
||||
|
||||
if not saved then
|
||||
log.warning("process._write_auto_config(): failed to save coordinator settings file")
|
||||
end
|
||||
|
||||
return not not 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.config)
|
||||
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.config.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))
|
||||
end
|
||||
|
||||
-- update config table and save
|
||||
self.config.pu_fallback = enabled
|
||||
_write_auto_config()
|
||||
-- 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)
|
||||
pctl.comms.send_fac_command(F_CMD.SET_SPS_LP, enabled)
|
||||
log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled))
|
||||
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
|
||||
self.config.mode = mode
|
||||
self.config.burn_target = burn_target
|
||||
self.config.charge_target = charge_target
|
||||
self.config.gen_target = gen_target
|
||||
self.config.limits = limits
|
||||
local ctl_proc = pctl.control_states.process
|
||||
ctl_proc.mode = mode
|
||||
ctl_proc.burn_target = burn_target
|
||||
ctl_proc.charge_target = charge_target
|
||||
ctl_proc.gen_target = gen_target
|
||||
ctl_proc.limits = limits
|
||||
|
||||
-- save config
|
||||
self.io.facility.save_cfg_ack(_write_auto_config())
|
||||
pctl.io.facility.save_cfg_ack(_write_auto_config())
|
||||
end
|
||||
|
||||
-- handle a start command acknowledgement
|
||||
@@ -294,38 +488,57 @@ end
|
||||
function process.start_ack_handle(response)
|
||||
local ack = response[1]
|
||||
|
||||
self.config.mode = response[2]
|
||||
self.config.burn_target = response[3]
|
||||
self.config.charge_target = response[4]
|
||||
self.config.gen_target = response[5]
|
||||
local ctl_proc = 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
|
||||
self.config.limits[i] = response[6][i]
|
||||
|
||||
local unit = self.io.units[i] ---@type ioctl_unit
|
||||
unit.unit_ps.publish("burn_limit", self.config.limits[i])
|
||||
for i = 1, math.min(#response[6], pctl.io.facility.num_units) do
|
||||
ctl_proc.limits[i] = response[6][i]
|
||||
pctl.io.units[i].unit_ps.publish("burn_limit", ctl_proc.limits[i])
|
||||
end
|
||||
|
||||
self.io.facility.ps.publish("process_mode", self.config.mode)
|
||||
self.io.facility.ps.publish("process_burn_target", self.config.burn_target)
|
||||
self.io.facility.ps.publish("process_charge_target", self.config.charge_target)
|
||||
self.io.facility.ps.publish("process_gen_target", self.config.gen_target)
|
||||
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.config.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.config.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
|
||||
|
||||
@@ -2,39 +2,48 @@
|
||||
-- Graphics Rendering Control
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
local style = require("coordinator.ui.style")
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
|
||||
local panel_view = require("coordinator.ui.layout.front_panel")
|
||||
local main_view = require("coordinator.ui.layout.main_view")
|
||||
local unit_view = require("coordinator.ui.layout.unit_view")
|
||||
local flow_view = require("coordinator.ui.layout.flow_view")
|
||||
local panel_view = require("coordinator.ui.layout.front_panel")
|
||||
local main_view = require("coordinator.ui.layout.main_view")
|
||||
local unit_view = require("coordinator.ui.layout.unit_view")
|
||||
|
||||
local flasher = require("graphics.flasher")
|
||||
local core = require("graphics.core")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
local DisplayBox = require("graphics.elements.DisplayBox")
|
||||
|
||||
local log_render = coordinator.log_render
|
||||
|
||||
---@class coord_renderer
|
||||
local renderer = {}
|
||||
|
||||
-- render engine
|
||||
local engine = {
|
||||
color_mode = 1, ---@type COLOR_MODE
|
||||
monitors = nil, ---@type monitors_struct|nil
|
||||
dmesg_window = nil, ---@type table|nil
|
||||
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
|
||||
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)
|
||||
@@ -43,9 +52,34 @@ local function _init_display(monitor)
|
||||
monitor.setCursorPos(1, 1)
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.colors do
|
||||
monitor.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||
for i = 1, #style.theme.colors do
|
||||
monitor.setPaletteColor(style.theme.colors[i].c, style.theme.colors[i].hex)
|
||||
end
|
||||
|
||||
-- apply color mode
|
||||
local c_mode_overrides = style.theme.color_modes[engine.color_mode]
|
||||
for i = 1, #c_mode_overrides do
|
||||
monitor.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
|
||||
end
|
||||
end
|
||||
|
||||
-- print out that the monitor is too small
|
||||
---@param monitor Monitor monitor
|
||||
local function _print_too_small(monitor)
|
||||
monitor.setCursorPos(1, 1)
|
||||
monitor.setBackgroundColor(colors.black)
|
||||
monitor.setTextColor(colors.red)
|
||||
monitor.clear()
|
||||
monitor.write("monitor too small")
|
||||
end
|
||||
|
||||
-- apply renderer configurations
|
||||
---@param config crd_config
|
||||
function renderer.configure(config)
|
||||
style.set_themes(config.MainTheme, config.FrontPanelTheme, config.ColorMode)
|
||||
|
||||
engine.color_mode = config.ColorMode
|
||||
engine.disable_flow_view = config.DisableFlowView
|
||||
end
|
||||
|
||||
-- link to the monitor peripherals
|
||||
@@ -54,14 +88,16 @@ function renderer.set_displays(monitors)
|
||||
engine.monitors = monitors
|
||||
|
||||
-- report to front panel as connected
|
||||
iocontrol.fp_monitor_state(0, true)
|
||||
iocontrol.fp_monitor_state("main", engine.monitors.main ~= nil)
|
||||
iocontrol.fp_monitor_state("flow", engine.monitors.flow ~= nil)
|
||||
for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end
|
||||
end
|
||||
|
||||
-- init all displays in use by the renderer
|
||||
function renderer.init_displays()
|
||||
-- init primary monitor
|
||||
_init_display(engine.monitors.primary)
|
||||
-- init main and flow monitors
|
||||
_init_display(engine.monitors.main)
|
||||
if not engine.disable_flow_view then _init_display(engine.monitors.flow) end
|
||||
|
||||
-- init unit displays
|
||||
for _, monitor in ipairs(engine.monitors.unit_displays) do
|
||||
@@ -75,56 +111,48 @@ function renderer.init_displays()
|
||||
term.setCursorPos(1, 1)
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.fp.colors do
|
||||
term.setPaletteColor(style.fp.colors[i].c, style.fp.colors[i].hex)
|
||||
end
|
||||
end
|
||||
|
||||
-- check main display width
|
||||
---@nodiscard
|
||||
---@return boolean width_okay
|
||||
function renderer.validate_main_display_width()
|
||||
local w, _ = engine.monitors.primary.getSize()
|
||||
return w == 164
|
||||
end
|
||||
|
||||
-- check display sizes
|
||||
---@nodiscard
|
||||
---@return boolean valid all unit display dimensions OK
|
||||
function renderer.validate_unit_display_sizes()
|
||||
local valid = true
|
||||
|
||||
for id, monitor in ipairs(engine.monitors.unit_displays) do
|
||||
local w, h = monitor.getSize()
|
||||
if w ~= 79 or h ~= 52 then
|
||||
log.warning(util.c("RENDERER: unit ", id, " display resolution not 79 wide by 52 tall: ", w, ", ", h))
|
||||
valid = false
|
||||
end
|
||||
for i = 1, #style.fp_theme.colors do
|
||||
term.setPaletteColor(style.fp_theme.colors[i].c, style.fp_theme.colors[i].hex)
|
||||
end
|
||||
|
||||
return valid
|
||||
-- apply color mode
|
||||
local c_mode_overrides = style.fp_theme.color_modes[engine.color_mode]
|
||||
for i = 1, #c_mode_overrides do
|
||||
term.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
|
||||
end
|
||||
end
|
||||
|
||||
-- initialize the dmesg output window
|
||||
function renderer.init_dmesg()
|
||||
local disp_x, disp_y = engine.monitors.primary.getSize()
|
||||
engine.dmesg_window = window.create(engine.monitors.primary, 1, 1, disp_x, disp_y)
|
||||
local disp_w, disp_h = engine.monitors.main.getSize()
|
||||
engine.dmesg_window = window.create(engine.monitors.main, 1, 1, disp_w, disp_h)
|
||||
log.direct_dmesg(engine.dmesg_window)
|
||||
end
|
||||
|
||||
-- start the coordinator front panel
|
||||
function renderer.start_fp()
|
||||
-- try to start the front panel
|
||||
---@return boolean success, any error_msg
|
||||
function renderer.try_start_fp()
|
||||
local status, msg = true, nil
|
||||
|
||||
if not engine.fp_ready then
|
||||
-- show front panel view on terminal
|
||||
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root}
|
||||
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
|
||||
status, msg = pcall(function ()
|
||||
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root}
|
||||
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
|
||||
end)
|
||||
|
||||
-- start flasher callback task
|
||||
flasher.run()
|
||||
|
||||
-- report front panel as ready
|
||||
engine.fp_ready = true
|
||||
if status then
|
||||
-- start flasher callback task and report ready
|
||||
flasher.run()
|
||||
engine.fp_ready = true
|
||||
else
|
||||
-- report fail and close front panel
|
||||
msg = core.extract_assert_msg(msg)
|
||||
renderer.close_fp()
|
||||
end
|
||||
end
|
||||
|
||||
return status, msg
|
||||
end
|
||||
|
||||
-- close out the front panel
|
||||
@@ -144,9 +172,9 @@ function renderer.close_fp()
|
||||
engine.fp_ready = false
|
||||
|
||||
-- restore colors
|
||||
for i = 1, #style.colors do
|
||||
local r, g, b = term.nativePaletteColor(style.colors[i].c)
|
||||
term.setPaletteColor(style.colors[i].c, r, g, b)
|
||||
for i = 1, #style.fp_theme.colors do
|
||||
local r, g, b = term.nativePaletteColor(style.fp_theme.colors[i].c)
|
||||
term.setPaletteColor(style.fp_theme.colors[i].c, r, g, b)
|
||||
end
|
||||
|
||||
-- reset terminal
|
||||
@@ -157,30 +185,50 @@ function renderer.close_fp()
|
||||
end
|
||||
end
|
||||
|
||||
-- start the coordinator GUI
|
||||
function renderer.start_ui()
|
||||
-- try to start the main GUI
|
||||
---@return boolean success, any error_msg
|
||||
function renderer.try_start_ui()
|
||||
local status, msg = true, nil
|
||||
|
||||
if not engine.ui_ready then
|
||||
-- hide dmesg
|
||||
engine.dmesg_window.setVisible(false)
|
||||
|
||||
-- show main view on main monitor
|
||||
if engine.monitors.primary ~= nil then
|
||||
engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root}
|
||||
main_view(engine.ui.main_display)
|
||||
status, msg = pcall(function ()
|
||||
-- show main view on main monitor
|
||||
if engine.monitors.main ~= nil then
|
||||
engine.ui.main_display = DisplayBox{window=engine.monitors.main,fg_bg=style.root}
|
||||
main_view(engine.ui.main_display)
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- show flow view on flow monitor
|
||||
if engine.monitors.flow ~= nil then
|
||||
engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root}
|
||||
flow_view(engine.ui.flow_display)
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- show unit views on unit displays
|
||||
for idx, display in pairs(engine.monitors.unit_displays) do
|
||||
engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
|
||||
unit_view(engine.ui.unit_displays[idx], idx)
|
||||
util.nop()
|
||||
end
|
||||
end)
|
||||
|
||||
if status then
|
||||
-- start flasher callback task and report ready
|
||||
flasher.run()
|
||||
engine.ui_ready = true
|
||||
else
|
||||
-- report fail and close ui
|
||||
msg = core.extract_assert_msg(msg)
|
||||
renderer.close_ui()
|
||||
end
|
||||
|
||||
-- show unit views on unit displays
|
||||
for idx, display in pairs(engine.monitors.unit_displays) do
|
||||
engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
|
||||
unit_view(engine.ui.unit_displays[idx], idx)
|
||||
end
|
||||
|
||||
-- start flasher callback task
|
||||
flasher.run()
|
||||
|
||||
-- report ui as ready
|
||||
engine.ui_ready = true
|
||||
end
|
||||
|
||||
return status, msg
|
||||
end
|
||||
|
||||
-- close out the UI
|
||||
@@ -192,6 +240,7 @@ function renderer.close_ui()
|
||||
|
||||
-- delete element trees
|
||||
if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end
|
||||
if engine.ui.flow_display ~= nil then engine.ui.flow_display.delete() end
|
||||
for _, display in pairs(engine.ui.unit_displays) do display.delete() end
|
||||
|
||||
-- report ui as not ready
|
||||
@@ -199,11 +248,17 @@ function renderer.close_ui()
|
||||
|
||||
-- clear root UI elements
|
||||
engine.ui.main_display = nil
|
||||
engine.ui.flow_display = nil
|
||||
engine.ui.unit_displays = {}
|
||||
|
||||
-- clear unit monitors
|
||||
for _, monitor in ipairs(engine.monitors.unit_displays) do monitor.clear() end
|
||||
|
||||
if not engine.disable_flow_view then
|
||||
-- clear flow monitor
|
||||
engine.monitors.flow.clear()
|
||||
end
|
||||
|
||||
-- re-draw dmesg
|
||||
engine.dmesg_window.setVisible(true)
|
||||
engine.dmesg_window.redraw()
|
||||
@@ -220,37 +275,48 @@ 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
|
||||
|
||||
if engine.monitors ~= nil then
|
||||
if engine.monitors.primary == device then
|
||||
if engine.ui.main_display ~= nil then
|
||||
-- delete element tree and clear root UI elements
|
||||
engine.ui.main_display.delete()
|
||||
end
|
||||
if not engine.monitors then return false end
|
||||
|
||||
is_used = true
|
||||
engine.monitors.primary = nil
|
||||
engine.ui.main_display = nil
|
||||
if engine.monitors.main == device then
|
||||
if engine.ui.main_display ~= nil then
|
||||
-- delete element tree and clear root UI elements
|
||||
engine.ui.main_display.delete()
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state(0, false)
|
||||
else
|
||||
for idx, monitor in pairs(engine.monitors.unit_displays) do
|
||||
if monitor == device then
|
||||
if engine.ui.unit_displays[idx] ~= nil then
|
||||
engine.ui.unit_displays[idx].delete()
|
||||
end
|
||||
is_used = true
|
||||
engine.monitors.main = nil
|
||||
engine.ui.main_display = nil
|
||||
|
||||
is_used = true
|
||||
engine.monitors.unit_displays[idx] = nil
|
||||
engine.ui.unit_displays[idx] = nil
|
||||
iocontrol.fp_monitor_state("main", false)
|
||||
elseif engine.monitors.flow == device then
|
||||
if engine.ui.flow_display ~= nil then
|
||||
-- delete element tree and clear root UI elements
|
||||
engine.ui.flow_display.delete()
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state(idx, false)
|
||||
break
|
||||
is_used = true
|
||||
engine.monitors.flow = nil
|
||||
engine.ui.flow_display = nil
|
||||
|
||||
iocontrol.fp_monitor_state("flow", false)
|
||||
else
|
||||
for idx, monitor in pairs(engine.monitors.unit_displays) do
|
||||
if monitor == device then
|
||||
if engine.ui.unit_displays[idx] ~= nil then
|
||||
engine.ui.unit_displays[idx].delete()
|
||||
end
|
||||
|
||||
is_used = true
|
||||
engine.monitors.unit_displays[idx] = nil
|
||||
engine.ui.unit_displays[idx] = nil
|
||||
|
||||
iocontrol.fp_monitor_state(idx, false)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -260,46 +326,34 @@ 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
|
||||
|
||||
if engine.monitors ~= nil then
|
||||
if engine.monitors.primary_name == name then
|
||||
is_used = true
|
||||
_init_display(device)
|
||||
engine.monitors.primary = device
|
||||
if not engine.monitors then return false end
|
||||
|
||||
local disp_x, disp_y = engine.monitors.primary.getSize()
|
||||
engine.dmesg_window.reposition(1, 1, disp_x, disp_y, engine.monitors.primary)
|
||||
-- note: handle_resize is a more adaptive way of re-initializing a connected monitor
|
||||
-- since it can handle a monitor being reconnected that isn't the right size
|
||||
|
||||
if engine.ui_ready and (engine.ui.main_display == nil) then
|
||||
engine.dmesg_window.setVisible(false)
|
||||
if engine.monitors.main_name == name then
|
||||
is_used = true
|
||||
engine.monitors.main = device
|
||||
|
||||
engine.ui.main_display = DisplayBox{window=device,fg_bg=style.root}
|
||||
main_view(engine.ui.main_display)
|
||||
else
|
||||
engine.dmesg_window.setVisible(true)
|
||||
engine.dmesg_window.redraw()
|
||||
end
|
||||
renderer.handle_resize(name)
|
||||
elseif engine.monitors.flow_name == name then
|
||||
is_used = true
|
||||
engine.monitors.flow = device
|
||||
|
||||
iocontrol.fp_monitor_state(0, true)
|
||||
else
|
||||
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
if monitor == name then
|
||||
is_used = true
|
||||
_init_display(device)
|
||||
engine.monitors.unit_displays[idx] = device
|
||||
renderer.handle_resize(name)
|
||||
else
|
||||
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
if monitor == name then
|
||||
is_used = true
|
||||
engine.monitors.unit_displays[idx] = device
|
||||
|
||||
if engine.ui_ready and (engine.ui.unit_displays[idx] == nil) then
|
||||
engine.ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
|
||||
unit_view(engine.ui.unit_displays[idx], idx)
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state(idx, true)
|
||||
break
|
||||
end
|
||||
renderer.handle_resize(name)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -307,6 +361,142 @@ function renderer.handle_reconnect(name, device)
|
||||
return is_used
|
||||
end
|
||||
|
||||
-- handle a monitor being resized<br>
|
||||
-- returns if this monitor is assigned + if the assigned screen still fits
|
||||
---@param name string monitor name
|
||||
---@return boolean is_used, boolean is_ok
|
||||
function renderer.handle_resize(name)
|
||||
local is_used = false
|
||||
local is_ok = true
|
||||
local ui = engine.ui
|
||||
|
||||
if not engine.monitors then return false, false end
|
||||
|
||||
if engine.monitors.main_name == name and engine.monitors.main then
|
||||
local device = engine.monitors.main ---@type Monitor
|
||||
|
||||
-- this is necessary if the bottom left block was broken and on reconnect
|
||||
_init_display(device)
|
||||
|
||||
is_used = true
|
||||
|
||||
-- resize dmesg window if needed, but don't make it thinner
|
||||
local disp_w, disp_h = engine.monitors.main.getSize()
|
||||
local dmsg_w, _ = engine.dmesg_window.getSize()
|
||||
engine.dmesg_window.reposition(1, 1, math.max(disp_w, dmsg_w), disp_h, engine.monitors.main)
|
||||
|
||||
if ui.main_display then
|
||||
ui.main_display.delete()
|
||||
ui.main_display = nil
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state("main", true)
|
||||
|
||||
engine.dmesg_window.setVisible(not engine.ui_ready)
|
||||
|
||||
if engine.ui_ready then
|
||||
local draw_start = util.time_ms()
|
||||
local ok = pcall(function ()
|
||||
ui.main_display = DisplayBox{window=device,fg_bg=style.root}
|
||||
main_view(ui.main_display)
|
||||
end)
|
||||
|
||||
if ok then
|
||||
log_render("main view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
|
||||
else
|
||||
if ui.main_display then
|
||||
ui.main_display.delete()
|
||||
ui.main_display = nil
|
||||
end
|
||||
|
||||
_print_too_small(device)
|
||||
|
||||
iocontrol.fp_monitor_state("main", false)
|
||||
is_ok = false
|
||||
end
|
||||
else engine.dmesg_window.redraw() end
|
||||
elseif engine.monitors.flow_name == name and engine.monitors.flow then
|
||||
local device = engine.monitors.flow ---@type Monitor
|
||||
|
||||
-- this is necessary if the bottom left block was broken and on reconnect
|
||||
_init_display(device)
|
||||
|
||||
is_used = true
|
||||
|
||||
if ui.flow_display then
|
||||
ui.flow_display.delete()
|
||||
ui.flow_display = nil
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state("flow", true)
|
||||
|
||||
if engine.ui_ready then
|
||||
local draw_start = util.time_ms()
|
||||
local ok = pcall(function ()
|
||||
ui.flow_display = DisplayBox{window=device,fg_bg=style.root}
|
||||
flow_view(ui.flow_display)
|
||||
end)
|
||||
|
||||
if ok then
|
||||
log_render("flow view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
|
||||
else
|
||||
if ui.flow_display then
|
||||
ui.flow_display.delete()
|
||||
ui.flow_display = nil
|
||||
end
|
||||
|
||||
_print_too_small(device)
|
||||
|
||||
iocontrol.fp_monitor_state("flow", false)
|
||||
is_ok = false
|
||||
end
|
||||
end
|
||||
else
|
||||
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
local device = engine.monitors.unit_displays[idx]
|
||||
|
||||
if monitor == name and device then
|
||||
-- this is necessary if the bottom left block was broken and on reconnect
|
||||
_init_display(device)
|
||||
|
||||
is_used = true
|
||||
|
||||
if ui.unit_displays[idx] then
|
||||
ui.unit_displays[idx].delete()
|
||||
ui.unit_displays[idx] = nil
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state(idx, true)
|
||||
|
||||
if engine.ui_ready then
|
||||
local draw_start = util.time_ms()
|
||||
local ok = pcall(function ()
|
||||
ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
|
||||
unit_view(ui.unit_displays[idx], idx)
|
||||
end)
|
||||
|
||||
if ok then
|
||||
log_render("unit " .. idx .. " view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
|
||||
else
|
||||
if ui.unit_displays[idx] then
|
||||
ui.unit_displays[idx].delete()
|
||||
ui.unit_displays[idx] = nil
|
||||
end
|
||||
|
||||
_print_too_small(device)
|
||||
|
||||
iocontrol.fp_monitor_state(idx, false)
|
||||
is_ok = false
|
||||
end
|
||||
end
|
||||
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return is_used, is_ok
|
||||
end
|
||||
|
||||
-- handle a touch event
|
||||
---@param event mouse_interaction|nil
|
||||
@@ -315,14 +505,15 @@ function renderer.handle_mouse(event)
|
||||
if engine.fp_ready and event.monitor == "terminal" then
|
||||
engine.ui.front_panel.handle_mouse(event)
|
||||
elseif engine.ui_ready then
|
||||
if event.monitor == engine.monitors.primary_name then
|
||||
engine.ui.main_display.handle_mouse(event)
|
||||
if event.monitor == engine.monitors.main_name then
|
||||
if engine.ui.main_display then engine.ui.main_display.handle_mouse(event) end
|
||||
elseif event.monitor == engine.monitors.flow_name then
|
||||
if engine.ui.flow_display then engine.ui.flow_display.handle_mouse(event) end
|
||||
else
|
||||
for id, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
if event.monitor == monitor then
|
||||
local layout = engine.ui.unit_displays[id] ---@type graphics_element
|
||||
layout.handle_mouse(event)
|
||||
break
|
||||
local display = engine.ui.unit_displays[id]
|
||||
if event.monitor == monitor and display then
|
||||
if display then display.handle_mouse(event) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,6 @@ local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local config = require("coordinator.config")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local pocket = require("coordinator.session.pocket")
|
||||
@@ -11,9 +10,10 @@ local pocket = require("coordinator.session.pocket")
|
||||
local apisessions = {}
|
||||
|
||||
local self = {
|
||||
nic = nil,
|
||||
nic = nil, ---@type nic
|
||||
config = nil, ---@type crd_config
|
||||
next_id = 0,
|
||||
sessions = {}
|
||||
sessions = {} ---@type pkt_session_struct[]
|
||||
}
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
@@ -32,7 +32,7 @@ local function _api_handle_outq(session)
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- handle a packet to be sent
|
||||
self.nic.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message)
|
||||
self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message)
|
||||
elseif msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- handle instruction/notification
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
@@ -42,8 +42,8 @@ local function _api_handle_outq(session)
|
||||
|
||||
-- max 100ms spent processing queue
|
||||
if util.time() - handle_start > 100 then
|
||||
log.warning("[API] out queue handler exceeded 100ms queue process limit")
|
||||
log.warning(util.c("[API] offending session: ", session))
|
||||
log.warning("API: out queue handler exceeded 100ms queue process limit")
|
||||
log.warning(util.c("API: offending session: ", session))
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -59,19 +59,21 @@ local function _shutdown(session)
|
||||
while session.out_queue.ready() do
|
||||
local msg = session.out_queue.pop()
|
||||
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then
|
||||
self.nic.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message)
|
||||
self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message)
|
||||
end
|
||||
end
|
||||
|
||||
log.debug(util.c("[API] closed session ", session))
|
||||
log.debug(util.c("API: closed session ", session))
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
-- initialize apisessions
|
||||
---@param nic nic
|
||||
function apisessions.init(nic)
|
||||
---@param nic nic network interface
|
||||
---@param config crd_config coordinator config
|
||||
function apisessions.init(nic, config)
|
||||
self.nic = nic
|
||||
self.config = config
|
||||
end
|
||||
|
||||
-- find a session by remote port
|
||||
@@ -87,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,
|
||||
@@ -103,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, 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 = {
|
||||
@@ -114,7 +117,7 @@ function apisessions.establish_session(source_addr, version)
|
||||
setmetatable(pkt_s, mt)
|
||||
|
||||
iocontrol.fp_pkt_connected(id, version, source_addr)
|
||||
log.debug(util.c("[API] established new session: ", pkt_s))
|
||||
log.debug(util.c("API: established new session: ", pkt_s))
|
||||
|
||||
self.next_id = id + 1
|
||||
|
||||
@@ -126,11 +129,11 @@ 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
|
||||
log.debug(util.c("[API] watchdog closing session ", session, "..."))
|
||||
log.debug(util.c("API: watchdog closing session ", session, "..."))
|
||||
_shutdown(session)
|
||||
end
|
||||
end
|
||||
@@ -140,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)
|
||||
@@ -156,7 +159,7 @@ function apisessions.free_all_closed()
|
||||
|
||||
---@param session pkt_session_struct
|
||||
local on_delete = function (session)
|
||||
log.debug(util.c("[API] free'ing closed session ", session))
|
||||
log.debug(util.c("API: free'ing closed session ", session))
|
||||
end
|
||||
|
||||
util.filter_table(self.sessions, f, on_delete)
|
||||
@@ -165,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 CAPI_TYPE = comms.CAPI_TYPE
|
||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||
local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
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,
|
||||
@@ -72,22 +82,22 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
iocontrol.fp_pkt_disconnected(id)
|
||||
end
|
||||
|
||||
-- send a CAPI packet
|
||||
-----@param msg_type CAPI_TYPE
|
||||
-----@param msg table
|
||||
-- local function _send(msg_type, msg)
|
||||
-- local s_pkt = comms.scada_packet()
|
||||
-- local c_pkt = comms.capi_packet()
|
||||
-- send a CRDN packet
|
||||
---@param msg_type CRDN_TYPE
|
||||
---@param msg table
|
||||
local function _send(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local c_pkt = comms.crdn_packet()
|
||||
|
||||
-- c_pkt.make(msg_type, msg)
|
||||
-- s_pkt.make(self.seq_num, PROTOCOL.COORD_API, c_pkt.raw_sendable())
|
||||
c_pkt.make(msg_type, msg)
|
||||
s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable())
|
||||
|
||||
-- out_queue.push_packet(s_pkt)
|
||||
-- self.seq_num = self.seq_num + 1
|
||||
-- end
|
||||
out_queue.push_packet(s_pkt)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- send a SCADA management packet
|
||||
---@param msg_type SCADA_MGMT_TYPE
|
||||
---@param msg_type MGMT_TYPE
|
||||
---@param msg table
|
||||
local function _send_mgmt(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
@@ -100,34 +110,282 @@ 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|capi_frame
|
||||
---@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
|
||||
self.conn_watchdog.feed()
|
||||
|
||||
-- process packet
|
||||
if pkt.scada_frame.protocol() == PROTOCOL.COORD_API then
|
||||
---@cast pkt capi_frame
|
||||
if pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
---@cast pkt crdn_frame
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
-- handle packet by type
|
||||
if pkt.type == nil then
|
||||
if pkt.type == CRDN_TYPE.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 = {
|
||||
fac.all_sys_ok,
|
||||
fac.rtu_count,
|
||||
fac.radiation,
|
||||
{ fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated },
|
||||
{ fac.auto_current_waste_product, fac.auto_pu_fallback_active },
|
||||
util.table_len(fac.tank_data_tbl),
|
||||
fac.induction_data_tbl[1] ~= nil,
|
||||
fac.sps_data_tbl[1] ~= nil,
|
||||
}
|
||||
|
||||
_send(CRDN_TYPE.API_GET_FAC, data)
|
||||
elseif pkt.type == CRDN_TYPE.API_GET_UNIT then
|
||||
if pkt.length == 1 and type(pkt.data[1]) == "number" then
|
||||
local u = db.units[pkt.data[1]]
|
||||
|
||||
if u then
|
||||
local data = {
|
||||
u.unit_id,
|
||||
u.connected,
|
||||
u.rtu_hw,
|
||||
u.a_group,
|
||||
u.alarms,
|
||||
u.annunciator,
|
||||
u.reactor_data,
|
||||
u.boiler_data_tbl,
|
||||
u.turbine_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_status,
|
||||
process_rate
|
||||
}
|
||||
|
||||
_send(CRDN_TYPE.API_GET_WASTE, data)
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported CAPI 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
|
||||
if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
if pkt.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive reply
|
||||
if pkt.length == 2 then
|
||||
local srv_start = pkt.data[1]
|
||||
@@ -136,7 +394,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")
|
||||
@@ -144,13 +402,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 == SCADA_MGMT_TYPE.CLOSE then
|
||||
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
|
||||
@@ -174,8 +436,8 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
-- close the connection
|
||||
function public.close()
|
||||
_close()
|
||||
_send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
|
||||
log.info(log_header .. "session closed by server")
|
||||
_send_mgmt(MGMT_TYPE.CLOSE, {})
|
||||
log.info(log_tag .. "session closed by server")
|
||||
end
|
||||
|
||||
-- iterate the session
|
||||
@@ -206,14 +468,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
|
||||
|
||||
@@ -229,7 +491,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
|
||||
periodics.keep_alive = periodics.keep_alive + elapsed
|
||||
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
|
||||
_send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() })
|
||||
_send_mgmt(MGMT_TYPE.KEEP_ALIVE, { util.time() })
|
||||
periodics.keep_alive = 0
|
||||
end
|
||||
|
||||
|
||||
@@ -2,468 +2,78 @@
|
||||
-- Alarm Sounder
|
||||
--
|
||||
|
||||
local audio = require("scada-common.audio")
|
||||
local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local ALARM = types.ALARM
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
|
||||
---@class sounder
|
||||
local sounder = {}
|
||||
|
||||
-- note: max samples = 0x20000 (128 * 1024 samples)
|
||||
|
||||
local _2_PI = 2 * math.pi -- 2 whole pies, hope you're hungry
|
||||
local _DRATE = 48000 -- 48kHz audio
|
||||
local _MAX_VAL = 127 / 2 -- max signed integer in this 8-bit audio
|
||||
local _05s_SAMPLES = 24000 -- half a second worth of samples
|
||||
|
||||
local test_alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
|
||||
|
||||
local alarm_ctl = {
|
||||
speaker = nil,
|
||||
speaker = nil, ---@type Speaker
|
||||
volume = 0.5,
|
||||
playing = false,
|
||||
num_active = 0,
|
||||
next_block = 1,
|
||||
-- split audio up into 0.5s samples so specific components can be ended quicker
|
||||
quad_buffer = { {}, {}, {}, {} }
|
||||
stream = audio.new_stream()
|
||||
}
|
||||
|
||||
-- sounds modeled after https://www.e2s.com/references-and-guidelines/listen-and-download-alarm-tones
|
||||
|
||||
local T_340Hz_Int_2Hz = 1
|
||||
local T_544Hz_440Hz_Alt = 2
|
||||
local T_660Hz_Int_125ms = 3
|
||||
local T_745Hz_Int_1Hz = 4
|
||||
local T_800Hz_Int = 5
|
||||
local T_800Hz_1000Hz_Alt = 6
|
||||
local T_1000Hz_Int = 7
|
||||
local T_1800Hz_Int_4Hz = 8
|
||||
|
||||
local TONES = {
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 340Hz @ 2Hz Intermittent
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 544Hz 100mS / 440Hz 400mS Alternating
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 660Hz @ 125ms On 125ms Off
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 745Hz @ 1Hz Intermittent
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 800Hz @ 0.25s On 1.75s Off
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 800/1000Hz @ 0.25s Alternating
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 1KHz 1s on, 1s off Intermittent
|
||||
{ active = false, component = { {}, {}, {}, {} } } -- 1.8KHz @ 4Hz Intermittent
|
||||
}
|
||||
|
||||
-- calculate how many samples are in the given number of milliseconds
|
||||
---@nodiscard
|
||||
---@param ms integer milliseconds
|
||||
---@return integer samples
|
||||
local function ms_to_samples(ms) return math.floor(ms * 48) end
|
||||
|
||||
--#region Tone Generation (the Maths)
|
||||
|
||||
-- 340Hz @ 2Hz Intermittent
|
||||
local function gen_tone_1()
|
||||
local t, dt = 0, _2_PI * 340 / _DRATE
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
TONES[1].component[1][i] = val
|
||||
TONES[1].component[3][i] = val
|
||||
TONES[1].component[2][i] = 0
|
||||
TONES[1].component[4][i] = 0
|
||||
t = (t + dt) % _2_PI
|
||||
end
|
||||
end
|
||||
|
||||
-- 544Hz 100mS / 440Hz 400mS Alternating
|
||||
local function gen_tone_2()
|
||||
local t1, dt1 = 0, _2_PI * 544 / _DRATE
|
||||
local t2, dt2 = 0, _2_PI * 440 / _DRATE
|
||||
local alternate_at = ms_to_samples(100)
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local value
|
||||
|
||||
if i <= alternate_at then
|
||||
value = math.floor(math.sin(t1) * _MAX_VAL)
|
||||
t1 = (t1 + dt1) % _2_PI
|
||||
else
|
||||
value = math.floor(math.sin(t2) * _MAX_VAL)
|
||||
t2 = (t2 + dt2) % _2_PI
|
||||
end
|
||||
|
||||
TONES[2].component[1][i] = value
|
||||
TONES[2].component[2][i] = value
|
||||
TONES[2].component[3][i] = value
|
||||
TONES[2].component[4][i] = value
|
||||
end
|
||||
end
|
||||
|
||||
-- 660Hz @ 125ms On 125ms Off
|
||||
local function gen_tone_3()
|
||||
local elapsed_samples = 0
|
||||
local alternate_after = ms_to_samples(125)
|
||||
local alternate_at = alternate_after
|
||||
local mode = true
|
||||
|
||||
local t, dt = 0, _2_PI * 660 / _DRATE
|
||||
|
||||
for set = 1, 4 do
|
||||
for i = 1, _05s_SAMPLES do
|
||||
if mode then
|
||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
TONES[3].component[set][i] = val
|
||||
t = (t + dt) % _2_PI
|
||||
else
|
||||
t = 0
|
||||
TONES[3].component[set][i] = 0
|
||||
end
|
||||
|
||||
if elapsed_samples == alternate_at then
|
||||
mode = not mode
|
||||
alternate_at = elapsed_samples + alternate_after
|
||||
end
|
||||
|
||||
elapsed_samples = elapsed_samples + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 745Hz @ 1Hz Intermittent
|
||||
local function gen_tone_4()
|
||||
local t, dt = 0, _2_PI * 745 / _DRATE
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
TONES[4].component[1][i] = val
|
||||
TONES[4].component[3][i] = val
|
||||
TONES[4].component[2][i] = 0
|
||||
TONES[4].component[4][i] = 0
|
||||
t = (t + dt) % _2_PI
|
||||
end
|
||||
end
|
||||
|
||||
-- 800Hz @ 0.25s On 1.75s Off
|
||||
local function gen_tone_5()
|
||||
local t, dt = 0, _2_PI * 800 / _DRATE
|
||||
local stop_at = ms_to_samples(250)
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
|
||||
if i > stop_at then
|
||||
TONES[5].component[1][i] = val
|
||||
else
|
||||
TONES[5].component[1][i] = 0
|
||||
end
|
||||
|
||||
TONES[5].component[2][i] = 0
|
||||
TONES[5].component[3][i] = 0
|
||||
TONES[5].component[4][i] = 0
|
||||
|
||||
t = (t + dt) % _2_PI
|
||||
end
|
||||
end
|
||||
|
||||
-- 1000/800Hz @ 0.25s Alternating
|
||||
local function gen_tone_6()
|
||||
local t1, dt1 = 0, _2_PI * 1000 / _DRATE
|
||||
local t2, dt2 = 0, _2_PI * 800 / _DRATE
|
||||
|
||||
local alternate_at = ms_to_samples(250)
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val
|
||||
if i <= alternate_at then
|
||||
val = math.floor(math.sin(t1) * _MAX_VAL)
|
||||
t1 = (t1 + dt1) % _2_PI
|
||||
else
|
||||
val = math.floor(math.sin(t2) * _MAX_VAL)
|
||||
t2 = (t2 + dt2) % _2_PI
|
||||
end
|
||||
|
||||
TONES[6].component[1][i] = val
|
||||
TONES[6].component[2][i] = val
|
||||
TONES[6].component[3][i] = val
|
||||
TONES[6].component[4][i] = val
|
||||
end
|
||||
end
|
||||
|
||||
-- 1KHz 1s on, 1s off Intermittent
|
||||
local function gen_tone_7()
|
||||
local t, dt = 0, _2_PI * 1000 / _DRATE
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
TONES[7].component[1][i] = val
|
||||
TONES[7].component[2][i] = val
|
||||
TONES[7].component[3][i] = 0
|
||||
TONES[7].component[4][i] = 0
|
||||
t = (t + dt) % _2_PI
|
||||
end
|
||||
end
|
||||
|
||||
-- 1800Hz @ 4Hz Intermittent
|
||||
local function gen_tone_8()
|
||||
local t, dt = 0, _2_PI * 1800 / _DRATE
|
||||
|
||||
local off_at = ms_to_samples(250)
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val = 0
|
||||
|
||||
if i <= off_at then
|
||||
val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
t = (t + dt) % _2_PI
|
||||
end
|
||||
|
||||
TONES[8].component[1][i] = val
|
||||
TONES[8].component[2][i] = val
|
||||
TONES[8].component[3][i] = val
|
||||
TONES[8].component[4][i] = val
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- hard audio limiter
|
||||
---@nodiscard
|
||||
---@param output number output level
|
||||
---@return number limited -128.0 to 127.0
|
||||
local function limit(output)
|
||||
return math.max(-128, math.min(127, output))
|
||||
end
|
||||
|
||||
-- zero the alarm audio buffer
|
||||
local function zero()
|
||||
for i = 1, 4 do
|
||||
for s = 1, _05s_SAMPLES do alarm_ctl.quad_buffer[i][s] = 0 end
|
||||
end
|
||||
end
|
||||
|
||||
-- add an alarm to the output buffer
|
||||
---@param alarm_idx integer tone ID
|
||||
local function add(alarm_idx)
|
||||
alarm_ctl.num_active = alarm_ctl.num_active + 1
|
||||
TONES[alarm_idx].active = true
|
||||
|
||||
for i = 1, 4 do
|
||||
for s = 1, _05s_SAMPLES do
|
||||
alarm_ctl.quad_buffer[i][s] = limit(alarm_ctl.quad_buffer[i][s] + TONES[alarm_idx].component[i][s])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- start audio or continue audio on buffer empty
|
||||
---@return boolean success successfully added buffer to audio output
|
||||
local function play()
|
||||
if not alarm_ctl.playing then
|
||||
alarm_ctl.playing = true
|
||||
alarm_ctl.next_block = 1
|
||||
|
||||
return sounder.continue()
|
||||
else
|
||||
return true
|
||||
end
|
||||
else return true end
|
||||
end
|
||||
|
||||
-- initialize the annunciator alarm system
|
||||
---@param speaker table speaker peripheral
|
||||
---@param speaker Speaker speaker peripheral
|
||||
---@param volume number speaker volume
|
||||
function sounder.init(speaker, volume)
|
||||
alarm_ctl.speaker = speaker
|
||||
alarm_ctl.speaker.stop()
|
||||
|
||||
alarm_ctl.volume = volume
|
||||
alarm_ctl.playing = false
|
||||
alarm_ctl.num_active = 0
|
||||
alarm_ctl.next_block = 1
|
||||
alarm_ctl.stream.stop()
|
||||
|
||||
zero()
|
||||
|
||||
-- generate tones
|
||||
gen_tone_1()
|
||||
gen_tone_2()
|
||||
gen_tone_3()
|
||||
gen_tone_4()
|
||||
gen_tone_5()
|
||||
gen_tone_6()
|
||||
gen_tone_7()
|
||||
gen_tone_8()
|
||||
audio.generate_tones()
|
||||
end
|
||||
|
||||
-- reconnect the speaker peripheral
|
||||
---@param speaker table speaker peripheral
|
||||
---@param speaker Speaker speaker peripheral
|
||||
function sounder.reconnect(speaker)
|
||||
alarm_ctl.speaker = speaker
|
||||
alarm_ctl.playing = false
|
||||
alarm_ctl.next_block = 1
|
||||
alarm_ctl.num_active = 0
|
||||
for id = 1, #TONES do TONES[id].active = false end
|
||||
alarm_ctl.stream.stop()
|
||||
end
|
||||
|
||||
-- check alarm state to enable/disable alarms
|
||||
---@param units table|nil unit list or nil to use test mode
|
||||
function sounder.eval(units)
|
||||
local changed = false
|
||||
local any_active = false
|
||||
local new_states = { false, false, false, false, false, false, false, false }
|
||||
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
|
||||
-- set alarm tones
|
||||
---@param states { [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
|
||||
|
||||
if units ~= nil then
|
||||
-- check all alarms for all units
|
||||
for i = 1, #units do
|
||||
local unit = units[i] ---@type ioctl_unit
|
||||
for id = 1, #unit.alarms do
|
||||
alarms[id] = alarms[id] or (unit.alarms[id] == ALARM_STATE.TRIPPED)
|
||||
end
|
||||
end
|
||||
else
|
||||
alarms = test_alarms
|
||||
end
|
||||
|
||||
-- containment breach is worst case CRITICAL alarm, this takes priority
|
||||
if alarms[ALARM.ContainmentBreach] then
|
||||
new_states[T_1800Hz_Int_4Hz] = true
|
||||
else
|
||||
-- critical damage is highest priority CRITICAL level alarm
|
||||
if alarms[ALARM.CriticalDamage] then
|
||||
new_states[T_660Hz_Int_125ms] = true
|
||||
else
|
||||
-- EMERGENCY level alarms + URGENT over temp
|
||||
if alarms[ALARM.ReactorDamage] or alarms[ALARM.ReactorOverTemp] or alarms[ALARM.ReactorWasteLeak] then
|
||||
new_states[T_544Hz_440Hz_Alt] = true
|
||||
-- URGENT level turbine trip
|
||||
elseif alarms[ALARM.TurbineTrip] then
|
||||
new_states[T_745Hz_Int_1Hz] = true
|
||||
-- URGENT level reactor lost
|
||||
elseif alarms[ALARM.ReactorLost] then
|
||||
new_states[T_340Hz_Int_2Hz] = true
|
||||
-- TIMELY level alarms
|
||||
elseif alarms[ALARM.ReactorHighTemp] or alarms[ALARM.ReactorHighWaste] or alarms[ALARM.RCSTransient] then
|
||||
new_states[T_800Hz_Int] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- check RPS transient URGENT level alarm
|
||||
if alarms[ALARM.RPSTransient] then
|
||||
new_states[T_1000Hz_Int] = true
|
||||
-- disable really painful audio combination
|
||||
new_states[T_340Hz_Int_2Hz] = false
|
||||
end
|
||||
end
|
||||
|
||||
-- radiation is a big concern, always play this CRITICAL level alarm if active
|
||||
if alarms[ALARM.ContainmentRadiation] then
|
||||
new_states[T_800Hz_1000Hz_Alt] = true
|
||||
-- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled
|
||||
-- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one
|
||||
if new_states[T_1000Hz_Int] and alarms[ALARM.ReactorLost] then new_states[T_340Hz_Int_2Hz] = true end
|
||||
-- it sounds *really* bad if this is in conjunction with these other tones, so disable them
|
||||
new_states[T_745Hz_Int_1Hz] = false
|
||||
new_states[T_800Hz_Int] = false
|
||||
new_states[T_1000Hz_Int] = false
|
||||
end
|
||||
|
||||
-- check if any changed, check if any active, update active flags
|
||||
for id = 1, #TONES do
|
||||
if new_states[id] ~= TONES[id].active then
|
||||
TONES[id].active = new_states[id]
|
||||
changed = true
|
||||
end
|
||||
|
||||
if TONES[id].active then any_active = true end
|
||||
end
|
||||
|
||||
-- zero and re-add tones if changed
|
||||
if changed then
|
||||
zero()
|
||||
|
||||
for id = 1, #TONES do
|
||||
if TONES[id].active then add(id) end
|
||||
end
|
||||
end
|
||||
|
||||
if any_active then play() else sounder.stop() end
|
||||
-- re-compute output if needed, then play audio if available
|
||||
if alarm_ctl.stream.is_recompute_needed() then alarm_ctl.stream.compute_buffer() end
|
||||
if alarm_ctl.stream.any_active() then play() else sounder.stop() end
|
||||
end
|
||||
|
||||
-- stop all audio and clear output buffer
|
||||
function sounder.stop()
|
||||
alarm_ctl.playing = false
|
||||
alarm_ctl.speaker.stop()
|
||||
alarm_ctl.next_block = 1
|
||||
alarm_ctl.num_active = 0
|
||||
for id = 1, #TONES do TONES[id].active = false end
|
||||
zero()
|
||||
alarm_ctl.stream.stop()
|
||||
end
|
||||
|
||||
-- continue audio on buffer empty
|
||||
---@return boolean success successfully added buffer to audio output
|
||||
function sounder.continue()
|
||||
local success = false
|
||||
|
||||
if alarm_ctl.playing then
|
||||
if alarm_ctl.speaker ~= nil and #alarm_ctl.quad_buffer[alarm_ctl.next_block] > 0 then
|
||||
local success = alarm_ctl.speaker.playAudio(alarm_ctl.quad_buffer[alarm_ctl.next_block], alarm_ctl.volume)
|
||||
|
||||
alarm_ctl.next_block = alarm_ctl.next_block + 1
|
||||
if alarm_ctl.next_block > 4 then alarm_ctl.next_block = 1 end
|
||||
|
||||
if not success then
|
||||
log.debug("SOUNDER: error playing audio")
|
||||
end
|
||||
|
||||
return success
|
||||
else
|
||||
return false
|
||||
end
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
--#region Test Functions
|
||||
|
||||
function sounder.test_1() add(1) play() end -- play tone T_340Hz_Int_2Hz
|
||||
function sounder.test_2() add(2) play() end -- play tone T_544Hz_440Hz_Alt
|
||||
function sounder.test_3() add(3) play() end -- play tone T_660Hz_Int_125ms
|
||||
function sounder.test_4() add(4) play() end -- play tone T_745Hz_Int_1Hz
|
||||
function sounder.test_5() add(5) play() end -- play tone T_800Hz_Int
|
||||
function sounder.test_6() add(6) play() end -- play tone T_800Hz_1000Hz_Alt
|
||||
function sounder.test_7() add(7) play() end -- play tone T_1000Hz_Int
|
||||
function sounder.test_8() add(8) play() end -- play tone T_1800Hz_Int_4Hz
|
||||
|
||||
function sounder.test_breach(active) test_alarms[ALARM.ContainmentBreach] = active end ---@param active boolean
|
||||
function sounder.test_rad(active) test_alarms[ALARM.ContainmentRadiation] = active end ---@param active boolean
|
||||
function sounder.test_lost(active) test_alarms[ALARM.ReactorLost] = active end ---@param active boolean
|
||||
function sounder.test_crit(active) test_alarms[ALARM.CriticalDamage] = active end ---@param active boolean
|
||||
function sounder.test_dmg(active) test_alarms[ALARM.ReactorDamage] = active end ---@param active boolean
|
||||
function sounder.test_overtemp(active) test_alarms[ALARM.ReactorOverTemp] = active end ---@param active boolean
|
||||
function sounder.test_hightemp(active) test_alarms[ALARM.ReactorHighTemp] = active end ---@param active boolean
|
||||
function sounder.test_wasteleak(active) test_alarms[ALARM.ReactorWasteLeak] = active end ---@param active boolean
|
||||
function sounder.test_highwaste(active) test_alarms[ALARM.ReactorHighWaste] = active end ---@param active boolean
|
||||
function sounder.test_rps(active) test_alarms[ALARM.RPSTransient] = active end ---@param active boolean
|
||||
function sounder.test_rcs(active) test_alarms[ALARM.RCSTransient] = active end ---@param active boolean
|
||||
function sounder.test_turbinet(active) test_alarms[ALARM.TurbineTrip] = active end ---@param active boolean
|
||||
|
||||
-- power rescaling limiter test
|
||||
function sounder.test_power_scale()
|
||||
local start = util.time_ms()
|
||||
|
||||
zero()
|
||||
|
||||
for id = 1, #TONES do
|
||||
if TONES[id].active then
|
||||
for i = 1, 4 do
|
||||
for s = 1, _05s_SAMPLES do
|
||||
alarm_ctl.quad_buffer[i][s] = limit(alarm_ctl.quad_buffer[i][s] +
|
||||
(TONES[id].component[i][s] / math.sqrt(alarm_ctl.num_active)))
|
||||
end
|
||||
end
|
||||
if alarm_ctl.speaker ~= nil and alarm_ctl.stream.has_next_block() then
|
||||
success = alarm_ctl.speaker.playAudio(alarm_ctl.stream.get_next_block(), alarm_ctl.volume)
|
||||
if not success then log.error("SOUNDER: error playing audio") end
|
||||
end
|
||||
end
|
||||
|
||||
log.debug("SOUNDER: power rescale test took " .. (util.time_ms() - start) .. "ms")
|
||||
return success
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
return sounder
|
||||
|
||||
@@ -7,59 +7,92 @@ require("/initenv").init_env()
|
||||
local comms = require("scada-common.comms")
|
||||
local crash = require("scada-common.crash")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local network = require("scada-common.network")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local config = require("coordinator.config")
|
||||
local configure = require("coordinator.configure")
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
local threads = require("coordinator.threads")
|
||||
|
||||
local apisessions = require("coordinator.session.apisessions")
|
||||
local COORDINATOR_VERSION = "v1.5.16"
|
||||
|
||||
local COORDINATOR_VERSION = "v0.21.2"
|
||||
local CHUNK_LOAD_DELAY_S = 30.0
|
||||
|
||||
local println = util.println
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
|
||||
local log_graphics = coordinator.log_graphics
|
||||
local log_sys = coordinator.log_sys
|
||||
local log_boot = coordinator.log_boot
|
||||
local log_comms = coordinator.log_comms
|
||||
local log_render = coordinator.log_render
|
||||
local log_sys = coordinator.log_sys
|
||||
local log_boot = coordinator.log_boot
|
||||
local log_comms = coordinator.log_comms
|
||||
local log_crypto = coordinator.log_crypto
|
||||
|
||||
----------------------------------------
|
||||
-- config validation
|
||||
-- get configuration
|
||||
----------------------------------------
|
||||
|
||||
local cfv = util.new_validator()
|
||||
-- mount connected devices (required for monitor setup)
|
||||
ppm.mount_all()
|
||||
|
||||
cfv.assert_channel(config.SVR_CHANNEL)
|
||||
cfv.assert_channel(config.CRD_CHANNEL)
|
||||
cfv.assert_channel(config.PKT_CHANNEL)
|
||||
cfv.assert_type_int(config.TRUSTED_RANGE)
|
||||
cfv.assert_type_num(config.SV_TIMEOUT)
|
||||
cfv.assert_min(config.SV_TIMEOUT, 2)
|
||||
cfv.assert_type_num(config.API_TIMEOUT)
|
||||
cfv.assert_min(config.API_TIMEOUT, 2)
|
||||
cfv.assert_type_int(config.NUM_UNITS)
|
||||
cfv.assert_type_num(config.SOUNDER_VOLUME)
|
||||
cfv.assert_type_bool(config.TIME_24_HOUR)
|
||||
cfv.assert_type_str(config.LOG_PATH)
|
||||
cfv.assert_type_int(config.LOG_MODE)
|
||||
local wait_on_load = true
|
||||
local loaded, monitors = coordinator.load_config()
|
||||
|
||||
assert(cfv.valid(), "bad config file: missing/invalid fields")
|
||||
-- if the computer just started, its chunk may have just loaded (...or the user rebooted)
|
||||
-- if monitor config failed, maybe an adjacent chunk containing all or part of a monitor has not loaded yet, so keep trying
|
||||
while wait_on_load and loaded == 2 and os.clock() < CHUNK_LOAD_DELAY_S do
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
println("There was a monitor configuration problem at boot.\n")
|
||||
println("Startup will keep trying every 2s in case of chunk load delays.\n")
|
||||
println(util.sprintf("The configurator will be started in %ds if all attempts fail.\n", math.max(0, CHUNK_LOAD_DELAY_S - os.clock())))
|
||||
println("(click to skip to the configurator)")
|
||||
|
||||
local timer_id = util.start_timer(2)
|
||||
|
||||
while true do
|
||||
local event, param1 = util.pull_event()
|
||||
if event == "timer" and param1 == timer_id then
|
||||
-- remount and re-attempt
|
||||
ppm.mount_all()
|
||||
loaded, monitors = coordinator.load_config()
|
||||
break
|
||||
elseif event == "mouse_click" or event == "terminate" then
|
||||
wait_on_load = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if loaded ~= 0 then
|
||||
-- try to reconfigure (user action)
|
||||
local success, error = configure.configure(loaded, monitors)
|
||||
if success then
|
||||
loaded, monitors = coordinator.load_config()
|
||||
if loaded ~= 0 then
|
||||
println(util.trinary(loaded == 2, "monitor configuration invalid", "failed to load a valid configuration") .. ", please reconfigure")
|
||||
return
|
||||
end
|
||||
else
|
||||
println("configuration error: " .. error)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- passed checks, good now
|
||||
---@cast monitors monitors_struct
|
||||
|
||||
local config = coordinator.config
|
||||
|
||||
----------------------------------------
|
||||
-- log init
|
||||
----------------------------------------
|
||||
|
||||
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
|
||||
log.init(config.LogPath, config.LogMode, config.LogDebug)
|
||||
|
||||
log.info("========================================")
|
||||
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
|
||||
@@ -67,6 +100,7 @@ log.info("========================================")
|
||||
println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<")
|
||||
|
||||
crash.set_env("coordinator", COORDINATOR_VERSION)
|
||||
crash.dbg_log_env()
|
||||
|
||||
----------------------------------------
|
||||
-- main application
|
||||
@@ -77,49 +111,73 @@ local function main()
|
||||
-- system startup
|
||||
----------------------------------------
|
||||
|
||||
-- mount connected devices
|
||||
ppm.mount_all()
|
||||
-- log mounts now since mounting was done before logging was ready
|
||||
ppm.log_mounts()
|
||||
|
||||
-- report versions/init fp PSIL
|
||||
iocontrol.init_fp(COORDINATOR_VERSION, comms.version)
|
||||
|
||||
-- setup monitors
|
||||
local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS)
|
||||
if not configured or monitors == nil then
|
||||
println("startup> monitor setup failed")
|
||||
log.fatal("monitor configuration failed")
|
||||
return
|
||||
end
|
||||
|
||||
-- init renderer
|
||||
renderer.configure(config)
|
||||
renderer.set_displays(monitors)
|
||||
renderer.init_displays()
|
||||
|
||||
if not renderer.validate_main_display_width() then
|
||||
println("startup> main display must be 8 blocks wide")
|
||||
log.fatal("main display not wide enough")
|
||||
return
|
||||
elseif not renderer.validate_unit_display_sizes() then
|
||||
println("startup> one or more unit display dimensions incorrect; they must be 4x4 blocks")
|
||||
log.fatal("unit display dimensions incorrect")
|
||||
return
|
||||
end
|
||||
|
||||
renderer.init_dmesg()
|
||||
|
||||
-- lets get started!
|
||||
log.info("monitors ready, dmesg output incoming...")
|
||||
|
||||
log_graphics("displays connected and reset")
|
||||
log_render("displays connected and reset")
|
||||
log_sys("system start on " .. os.date("%c"))
|
||||
log_boot("starting " .. COORDINATOR_VERSION)
|
||||
|
||||
----------------------------------------
|
||||
-- memory allocation
|
||||
----------------------------------------
|
||||
|
||||
-- shared memory across threads
|
||||
---@class crd_shared_memory
|
||||
local __shared_memory = {
|
||||
-- time and date format for display
|
||||
date_format = util.trinary(config.Time24Hour, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y"),
|
||||
|
||||
-- coordinator system state flags
|
||||
---@class crd_state
|
||||
crd_state = {
|
||||
fp_ok = false,
|
||||
ui_ok = true, -- default true, used to abort on fail
|
||||
link_fail = false,
|
||||
shutdown = false
|
||||
},
|
||||
|
||||
-- core coordinator devices
|
||||
crd_dev = {
|
||||
modem = ppm.get_wireless_modem(),
|
||||
speaker = ppm.get_device("speaker") ---@type Speaker|nil
|
||||
},
|
||||
|
||||
-- system objects
|
||||
crd_sys = {
|
||||
nic = nil, ---@type nic
|
||||
coord_comms = nil, ---@type coord_comms
|
||||
conn_watchdog = nil ---@type watchdog
|
||||
},
|
||||
|
||||
-- message queues
|
||||
q = {
|
||||
mq_render = mqueue.new()
|
||||
}
|
||||
}
|
||||
|
||||
local smem_dev = __shared_memory.crd_dev
|
||||
local smem_sys = __shared_memory.crd_sys
|
||||
|
||||
local crd_state = __shared_memory.crd_state
|
||||
|
||||
----------------------------------------
|
||||
-- setup alarm sounder subsystem
|
||||
----------------------------------------
|
||||
|
||||
local speaker = ppm.get_device("speaker")
|
||||
if speaker == nil then
|
||||
if smem_dev.speaker == nil then
|
||||
log_boot("annunciator alarm speaker not found")
|
||||
println("startup> speaker not found")
|
||||
log.fatal("no annunciator alarm speaker found")
|
||||
@@ -127,7 +185,7 @@ local function main()
|
||||
else
|
||||
local sounder_start = util.time_ms()
|
||||
log_boot("annunciator alarm speaker connected")
|
||||
sounder.init(speaker, config.SOUNDER_VOLUME)
|
||||
sounder.init(smem_dev.speaker, config.SpeakerVolume)
|
||||
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
|
||||
log_sys("annunciator alarm configured")
|
||||
iocontrol.fp_has_speaker(true)
|
||||
@@ -138,14 +196,13 @@ local function main()
|
||||
----------------------------------------
|
||||
|
||||
-- message authentication init
|
||||
if type(config.AUTH_KEY) == "string" then
|
||||
local init_time = network.init_mac(config.AUTH_KEY)
|
||||
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
|
||||
local init_time = network.init_mac(config.AuthKey)
|
||||
log_crypto("HMAC init took " .. init_time .. "ms")
|
||||
end
|
||||
|
||||
-- get the communications modem
|
||||
local modem = ppm.get_wireless_modem()
|
||||
if modem == nil then
|
||||
if smem_dev.modem == nil then
|
||||
log_comms("wireless modem not found")
|
||||
println("startup> wireless modem not found")
|
||||
log.fatal("no wireless modem on startup")
|
||||
@@ -156,238 +213,54 @@ local function main()
|
||||
end
|
||||
|
||||
-- create connection watchdog
|
||||
local conn_watchdog = util.new_watchdog(config.SV_TIMEOUT)
|
||||
conn_watchdog.cancel()
|
||||
smem_sys.conn_watchdog = util.new_watchdog(config.SVR_Timeout)
|
||||
smem_sys.conn_watchdog.cancel()
|
||||
log.debug("startup> conn watchdog created")
|
||||
|
||||
-- create network interface then setup comms
|
||||
local nic = network.nic(modem)
|
||||
local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, config.CRD_CHANNEL, config.SVR_CHANNEL,
|
||||
config.PKT_CHANNEL, config.TRUSTED_RANGE, conn_watchdog)
|
||||
smem_sys.nic = network.nic(smem_dev.modem)
|
||||
smem_sys.coord_comms = coordinator.comms(COORDINATOR_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
|
||||
log.debug("startup> comms init")
|
||||
log_comms("comms initialized")
|
||||
|
||||
-- base loop clock (2Hz, 10 ticks)
|
||||
local MAIN_CLOCK = 0.5
|
||||
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||
|
||||
----------------------------------------
|
||||
-- start front panel & UI start function
|
||||
-- start front panel
|
||||
----------------------------------------
|
||||
|
||||
log_graphics("starting front panel UI...")
|
||||
log_render("starting front panel UI...")
|
||||
|
||||
local fp_ok, fp_message = pcall(renderer.start_fp)
|
||||
if not fp_ok then
|
||||
renderer.close_fp()
|
||||
log_graphics(util.c("front panel UI error: ", fp_message))
|
||||
local fp_message
|
||||
crd_state.fp_ok, fp_message = renderer.try_start_fp()
|
||||
if not crd_state.fp_ok then
|
||||
log_render(util.c("front panel UI error: ", fp_message))
|
||||
println_ts("front panel UI creation failed")
|
||||
log.fatal(util.c("front panel GUI render failed with error ", fp_message))
|
||||
return
|
||||
else log_graphics("front panel ready") end
|
||||
|
||||
-- start up the main UI
|
||||
---@return boolean ui_ok started ok
|
||||
local function start_main_ui()
|
||||
log_graphics("starting main UI...")
|
||||
|
||||
local draw_start = util.time_ms()
|
||||
|
||||
local ui_ok, ui_message = pcall(renderer.start_ui)
|
||||
if not ui_ok then
|
||||
renderer.close_ui()
|
||||
log_graphics(util.c("main UI error: ", ui_message))
|
||||
log.fatal(util.c("main GUI render failed with error ", ui_message))
|
||||
else
|
||||
log_graphics("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
|
||||
end
|
||||
|
||||
return ui_ok
|
||||
end
|
||||
else log_render("front panel ready") end
|
||||
|
||||
----------------------------------------
|
||||
-- main event loop
|
||||
-- start system
|
||||
----------------------------------------
|
||||
|
||||
local link_failed = false
|
||||
local ui_ok = true
|
||||
local date_format = util.trinary(config.TIME_24_HOUR, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y")
|
||||
-- init threads
|
||||
local main_thread = threads.thread__main(__shared_memory)
|
||||
local render_thread = threads.thread__render(__shared_memory)
|
||||
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
log.info("startup> completed")
|
||||
|
||||
log_sys("system started successfully")
|
||||
|
||||
-- main event loop
|
||||
while true do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
if event == "peripheral_detach" then
|
||||
local type, device = ppm.handle_unmount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
-- we only really care if this is our wireless modem
|
||||
-- if it is another modem, handle other peripheral losses separately
|
||||
if nic.is_modem(device) then
|
||||
nic.disconnect()
|
||||
log_sys("comms modem disconnected")
|
||||
|
||||
local other_modem = ppm.get_wireless_modem()
|
||||
if other_modem then
|
||||
log_sys("found another wireless modem, using it for comms")
|
||||
nic.connect(other_modem)
|
||||
else
|
||||
-- close out main UI
|
||||
renderer.close_ui()
|
||||
|
||||
-- alert user to status
|
||||
log_sys("awaiting comms modem reconnect...")
|
||||
|
||||
iocontrol.fp_has_modem(false)
|
||||
end
|
||||
else
|
||||
log_sys("non-comms modem disconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
if renderer.handle_disconnect(device) then
|
||||
log_sys("lost a configured monitor")
|
||||
else
|
||||
log_sys("lost an unused monitor")
|
||||
end
|
||||
elseif type == "speaker" then
|
||||
log_sys("lost alarm sounder speaker")
|
||||
iocontrol.fp_has_speaker(false)
|
||||
end
|
||||
end
|
||||
elseif event == "peripheral" then
|
||||
local type, device = ppm.mount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
if device.isWireless() and not nic.is_connected() then
|
||||
-- reconnected modem
|
||||
log_sys("comms modem reconnected")
|
||||
nic.connect(device)
|
||||
iocontrol.fp_has_modem(true)
|
||||
elseif device.isWireless() then
|
||||
log.info("unused wireless modem reconnected")
|
||||
else
|
||||
log_sys("wired modem reconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
if renderer.handle_reconnect(param1, device) then
|
||||
log_sys(util.c("configured monitor ", param1, " reconnected"))
|
||||
else
|
||||
log_sys(util.c("unused monitor ", param1, " connected"))
|
||||
end
|
||||
elseif type == "speaker" then
|
||||
log_sys("alarm sounder speaker reconnected")
|
||||
sounder.reconnect(device)
|
||||
iocontrol.fp_has_speaker(true)
|
||||
end
|
||||
end
|
||||
elseif event == "timer" then
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
|
||||
-- toggle heartbeat
|
||||
iocontrol.heartbeat()
|
||||
|
||||
-- maintain connection
|
||||
if nic.is_connected() then
|
||||
local ok, start_ui = coord_comms.try_connect()
|
||||
if not ok then
|
||||
link_failed = true
|
||||
log_sys("supervisor connection failed, shutting down...")
|
||||
log.fatal("failed to connect to supervisor")
|
||||
break
|
||||
elseif start_ui then
|
||||
log_sys("supervisor connected, proceeding to main UI start")
|
||||
ui_ok = start_main_ui()
|
||||
if not ui_ok then break end
|
||||
end
|
||||
end
|
||||
|
||||
-- iterate sessions
|
||||
apisessions.iterate_all()
|
||||
|
||||
-- free any closed sessions
|
||||
apisessions.free_all_closed()
|
||||
|
||||
-- update date and time string for main display
|
||||
if coord_comms.is_linked() then
|
||||
iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format))
|
||||
end
|
||||
|
||||
loop_clock.start()
|
||||
elseif conn_watchdog.is_timer(param1) then
|
||||
-- supervisor watchdog timeout
|
||||
log_comms("supervisor server timeout")
|
||||
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
else
|
||||
-- a non-clock/main watchdog timer event
|
||||
|
||||
-- check API watchdogs
|
||||
apisessions.check_all_watchdogs(param1)
|
||||
|
||||
-- notify timer callback dispatcher
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
-- got a packet
|
||||
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
|
||||
-- handle then check if it was a disconnect
|
||||
if coord_comms.handle_packet(packet) then
|
||||
log_comms("supervisor closed connection")
|
||||
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
end
|
||||
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
|
||||
event == "mouse_drag" or event == "mouse_scroll" then
|
||||
-- handle a mouse event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
elseif event == "speaker_audio_empty" then
|
||||
-- handle speaker buffer emptied
|
||||
sounder.continue()
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
-- handle supervisor connection
|
||||
coord_comms.try_connect(true)
|
||||
|
||||
if coord_comms.is_linked() then
|
||||
log_comms("terminate requested, closing supervisor connection...")
|
||||
else link_failed = true end
|
||||
|
||||
coord_comms.close()
|
||||
log_comms("supervisor connection closed")
|
||||
|
||||
-- handle API sessions
|
||||
log_comms("closing api sessions...")
|
||||
apisessions.close_all()
|
||||
log_comms("api sessions closed")
|
||||
break
|
||||
end
|
||||
end
|
||||
-- run threads
|
||||
parallel.waitForAll(main_thread.p_exec, render_thread.p_exec)
|
||||
|
||||
renderer.close_ui()
|
||||
renderer.close_fp()
|
||||
sounder.stop()
|
||||
log_sys("system shutdown")
|
||||
|
||||
if link_failed then println_ts("failed to connect to supervisor") end
|
||||
if not ui_ok then println_ts("main UI creation failed") end
|
||||
if crd_state.link_fail then println_ts("failed to connect to supervisor") end
|
||||
if not crd_state.ui_ok then println_ts("main UI creation failed") end
|
||||
|
||||
-- close on error exit (such as UI error)
|
||||
if smem_sys.coord_comms.is_linked() then smem_sys.coord_comms.close() end
|
||||
|
||||
println_ts("exited")
|
||||
log.info("exited")
|
||||
|
||||
373
coordinator/threads.lua
Normal file
373
coordinator/threads.lua
Normal file
@@ -0,0 +1,373 @@
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local process = require("coordinator.process")
|
||||
local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
|
||||
local apisessions = require("coordinator.session.apisessions")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local log_render = coordinator.log_render
|
||||
local log_sys = coordinator.log_sys
|
||||
local log_comms = coordinator.log_comms
|
||||
|
||||
local threads = {}
|
||||
|
||||
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
|
||||
local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
|
||||
|
||||
local MQ__RENDER_CMD = {
|
||||
START_MAIN_UI = 1
|
||||
}
|
||||
|
||||
local MQ__RENDER_DATA = {
|
||||
MON_CONNECT = 1,
|
||||
MON_DISCONNECT = 2,
|
||||
MON_RESIZE = 3
|
||||
}
|
||||
|
||||
-- main thread
|
||||
---@nodiscard
|
||||
---@param smem crd_shared_memory
|
||||
function threads.thread__main(smem)
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
iocontrol.fp_rt_status("main", true)
|
||||
log.debug("main thread start")
|
||||
|
||||
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
|
||||
log_sys("system started successfully")
|
||||
|
||||
-- load in from shared memory
|
||||
local crd_state = smem.crd_state
|
||||
local nic = smem.crd_sys.nic
|
||||
local coord_comms = smem.crd_sys.coord_comms
|
||||
local conn_watchdog = smem.crd_sys.conn_watchdog
|
||||
|
||||
-- event loop
|
||||
while true do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
if event == "peripheral_detach" then
|
||||
local type, device = ppm.handle_unmount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
---@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
|
||||
nic.disconnect()
|
||||
log_sys("comms modem disconnected")
|
||||
|
||||
local other_modem = ppm.get_wireless_modem()
|
||||
if other_modem then
|
||||
log_sys("found another wireless modem, using it for comms")
|
||||
nic.connect(other_modem)
|
||||
else
|
||||
-- close out main UI
|
||||
renderer.close_ui()
|
||||
|
||||
-- alert user to status
|
||||
log_sys("awaiting comms modem reconnect...")
|
||||
|
||||
iocontrol.fp_has_modem(false)
|
||||
end
|
||||
else
|
||||
log_sys("non-comms modem disconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
---@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
|
||||
end
|
||||
elseif event == "peripheral" then
|
||||
local type, device = ppm.mount(param1)
|
||||
|
||||
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")
|
||||
nic.connect(device)
|
||||
iocontrol.fp_has_modem(true)
|
||||
elseif device.isWireless() then
|
||||
log.info("unused wireless modem reconnected")
|
||||
else
|
||||
log_sys("wired modem reconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
---@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)
|
||||
end
|
||||
end
|
||||
elseif event == "monitor_resize" then
|
||||
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_RESIZE, param1)
|
||||
elseif event == "timer" then
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
|
||||
-- toggle heartbeat
|
||||
iocontrol.heartbeat()
|
||||
|
||||
-- maintain connection
|
||||
if nic.is_connected() then
|
||||
local ok, start_ui = coord_comms.try_connect()
|
||||
if not ok then
|
||||
crd_state.link_fail = true
|
||||
crd_state.shutdown = true
|
||||
log_sys("supervisor connection failed, shutting down...")
|
||||
log.fatal("failed to connect to supervisor")
|
||||
break
|
||||
elseif start_ui then
|
||||
log_sys("supervisor connected, dispatching main UI start")
|
||||
smem.q.mq_render.push_command(MQ__RENDER_CMD.START_MAIN_UI)
|
||||
end
|
||||
end
|
||||
|
||||
-- iterate sessions and free any closed ones
|
||||
apisessions.iterate_all()
|
||||
apisessions.free_all_closed()
|
||||
|
||||
-- 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))
|
||||
end
|
||||
|
||||
loop_clock.start()
|
||||
elseif conn_watchdog.is_timer(param1) then
|
||||
-- supervisor watchdog timeout
|
||||
log_comms("supervisor server timeout")
|
||||
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
else
|
||||
-- a non-clock/main watchdog timer event
|
||||
|
||||
-- check API watchdogs
|
||||
apisessions.check_all_watchdogs(param1)
|
||||
|
||||
-- notify timer callback dispatcher
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
-- got a packet
|
||||
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
|
||||
-- handle then check if it was a disconnect
|
||||
if coord_comms.handle_packet(packet) then
|
||||
log_comms("supervisor closed connection")
|
||||
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
end
|
||||
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
|
||||
event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
|
||||
-- handle a mouse event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
elseif event == "speaker_audio_empty" then
|
||||
-- handle speaker buffer emptied
|
||||
sounder.continue()
|
||||
end
|
||||
|
||||
-- check for termination request or UI crash
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
crd_state.shutdown = true
|
||||
log.info("terminate requested, main thread exiting")
|
||||
elseif not crd_state.ui_ok then
|
||||
crd_state.shutdown = true
|
||||
log.info("terminating due to fatal UI error")
|
||||
end
|
||||
|
||||
if crd_state.shutdown then
|
||||
-- handle closing supervisor connection
|
||||
coord_comms.try_connect(true)
|
||||
|
||||
if coord_comms.is_linked() then
|
||||
log_comms("closing supervisor connection...")
|
||||
else crd_state.link_fail = true end
|
||||
|
||||
coord_comms.close()
|
||||
log_comms("supervisor connection closed")
|
||||
|
||||
-- handle API sessions
|
||||
log_comms("closing api sessions...")
|
||||
apisessions.close_all()
|
||||
log_comms("api sessions closed")
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- execute the thread in a protected mode, retrying it on return if not shutting down
|
||||
function public.p_exec()
|
||||
local crd_state = smem.crd_state
|
||||
|
||||
while not crd_state.shutdown do
|
||||
local status, result = pcall(public.exec)
|
||||
if status == false then
|
||||
log.fatal(util.strval(result))
|
||||
end
|
||||
|
||||
iocontrol.fp_rt_status("main", false)
|
||||
|
||||
-- if status is true, then we are probably exiting, so this won't matter
|
||||
-- this thread cannot be slept because it will miss events (namely "terminate")
|
||||
if not crd_state.shutdown then
|
||||
log.info("main thread restarting now...")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- coordinator renderer thread, tasked with long duration draws
|
||||
---@nodiscard
|
||||
---@param smem crd_shared_memory
|
||||
function threads.thread__render(smem)
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
iocontrol.fp_rt_status("render", true)
|
||||
log.debug("render thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local crd_state = smem.crd_state
|
||||
local render_queue = smem.q.mq_render
|
||||
|
||||
local last_update = util.time()
|
||||
|
||||
-- thread loop
|
||||
while true do
|
||||
-- check for messages in the message queue
|
||||
while render_queue.ready() and not crd_state.shutdown do
|
||||
local msg = render_queue.pop()
|
||||
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- received a command
|
||||
if msg.message == MQ__RENDER_CMD.START_MAIN_UI then
|
||||
-- stop the UI if it was already started
|
||||
-- this may occur on a quick supervisor disconnect -> connect
|
||||
if renderer.ui_ready() then
|
||||
log_render("closing main UI before executing new request to start")
|
||||
renderer.close_ui()
|
||||
end
|
||||
|
||||
-- start up the main UI
|
||||
log_render("starting main UI...")
|
||||
|
||||
local draw_start = util.time_ms()
|
||||
|
||||
local ui_message
|
||||
crd_state.ui_ok, ui_message = renderer.try_start_ui()
|
||||
if not crd_state.ui_ok then
|
||||
log_render(util.c("main UI error: ", ui_message))
|
||||
log.fatal(util.c("main GUI render failed with error ", ui_message))
|
||||
else
|
||||
log_render("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
|
||||
end
|
||||
end
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
-- received data
|
||||
local cmd = msg.message ---@type queue_data
|
||||
|
||||
if cmd.key == MQ__RENDER_DATA.MON_CONNECT then
|
||||
-- monitor connected
|
||||
if renderer.handle_reconnect(cmd.val.name, cmd.val.device) then
|
||||
log_sys(util.c("configured monitor ", cmd.val.name, " reconnected"))
|
||||
else
|
||||
log_sys(util.c("unused monitor ", cmd.val.name, " connected"))
|
||||
end
|
||||
elseif cmd.key == MQ__RENDER_DATA.MON_DISCONNECT then
|
||||
-- monitor disconnected
|
||||
if renderer.handle_disconnect(cmd.val) then
|
||||
log_sys("lost a configured monitor")
|
||||
else
|
||||
log_sys("lost an unused monitor")
|
||||
end
|
||||
elseif cmd.key == MQ__RENDER_DATA.MON_RESIZE then
|
||||
-- monitor resized
|
||||
local is_used, is_ok = renderer.handle_resize(cmd.val)
|
||||
if is_used then
|
||||
log_sys(util.c("configured monitor ", cmd.val, " resized, ", util.trinary(is_ok, "display fits", "display does not fit")))
|
||||
end
|
||||
end
|
||||
elseif msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- received a packet
|
||||
end
|
||||
end
|
||||
|
||||
-- quick yield
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if crd_state.shutdown then
|
||||
log.info("render thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
-- delay before next check
|
||||
last_update = util.adaptive_delay(RENDER_SLEEP, last_update)
|
||||
end
|
||||
end
|
||||
|
||||
-- execute the thread in a protected mode, retrying it on return if not shutting down
|
||||
function public.p_exec()
|
||||
local crd_state = smem.crd_state
|
||||
|
||||
while not crd_state.shutdown do
|
||||
local status, result = pcall(public.exec)
|
||||
if status == false then
|
||||
log.fatal(util.strval(result))
|
||||
end
|
||||
|
||||
iocontrol.fp_rt_status("render", false)
|
||||
|
||||
if not crd_state.shutdown then
|
||||
log.info("render thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return threads
|
||||
@@ -1,40 +1,44 @@
|
||||
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
|
||||
local function new_view(root, x, y, ps)
|
||||
local boiler = Rectangle{parent=root,border=border(1, colors.gray, true),width=31,height=7,x=x,y=y}
|
||||
local text_fg = style.theme.text_fg
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
local text_fg_bg = cpair(colors.black, colors.lightGray)
|
||||
local lu_col = cpair(colors.gray, colors.gray)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local boiler = Rectangle{parent=root,border=border(1,colors.gray,true),width=31,height=7,x=x,y=y}
|
||||
|
||||
local status = StateIndicator{parent=boiler,x=9,y=1,states=style.boiler.states,value=1,min_width=12}
|
||||
local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg}
|
||||
local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg}
|
||||
local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=lu_col,label="Temp:",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=22,fg_bg=text_fg}
|
||||
local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg}
|
||||
|
||||
status.register(ps, "computed_status", status.update)
|
||||
temp.register(ps, "temperature", temp.update)
|
||||
temp.register(ps, "temperature", function (t) temp.update(db.temp_convert(t)) end)
|
||||
boil_r.register(ps, "boil_rate", boil_r.update)
|
||||
|
||||
TextBox{parent=boiler,text="H",x=2,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=boiler,text="W",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=boiler,text="S",x=27,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=boiler,text="C",x=28,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=boiler,text="H",x=2,y=5,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,96 +1,150 @@
|
||||
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 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
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
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 label_fg = style.theme.label_fg
|
||||
local text_fg = style.theme.text_fg
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
local ind_yel = style.ind_yel
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local title = "INDUCTION MATRIX"
|
||||
if type(id) == "number" then title = title .. id end
|
||||
|
||||
local matrix = Div{parent=root,fg_bg=style.root,width=33,height=24,x=x,y=y}
|
||||
|
||||
TextBox{parent=matrix,text=" ",width=33,height=1,x=1,y=1,fg_bg=cpair(colors.lightGray,colors.gray)}
|
||||
TextBox{parent=matrix,text=title,alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.gray)}
|
||||
-- black has low contrast with dark gray, so if background is black use white instead
|
||||
local cutout_fg_bg = cpair(util.trinary(style.theme.bg == colors.black, colors.white, style.theme.bg), colors.gray)
|
||||
|
||||
TextBox{parent=matrix,text=" ",width=33,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 text_fg_bg = cpair(colors.black, colors.lightGray)
|
||||
local label_fg_bg = cpair(colors.gray, colors.lightGray)
|
||||
local lu_col = cpair(colors.gray, colors.gray)
|
||||
|
||||
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
|
||||
local energy = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
local capacity = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Capacity:",format="%8.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
local input = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="Input: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
|
||||
local output = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Output: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
|
||||
|
||||
local avg_chg = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Avg. Chg:",format="%8.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
local avg_in = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="Avg. In: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
|
||||
local avg_out = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Avg. Out:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
|
||||
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
|
||||
local capacity = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Capacity:",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)
|
||||
energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end)
|
||||
capacity.register(ps, "max_energy", function (val) capacity.update(util.joules_to_fe(val)) end)
|
||||
input.register(ps, "last_input", function (val) input.update(util.joules_to_fe(val)) end)
|
||||
output.register(ps, "last_output", function (val) output.update(util.joules_to_fe(val)) end)
|
||||
|
||||
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(db.energy_convert(val)) end)
|
||||
avg_in.register(ps, "avg_inflow", avg_in.update)
|
||||
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(db.energy_convert(val)) end)
|
||||
|
||||
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill:",unit="%",format="%8.2f",value=0,width=18,fg_bg=text_fg_bg}
|
||||
|
||||
local cells = DataIndicator{parent=rect,x=11,y=14,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg_bg}
|
||||
local providers = DataIndicator{parent=rect,x=11,y=15,lu_colors=lu_col,label="Providers:",format="%7d",value=0,width=18,fg_bg=text_fg_bg}
|
||||
|
||||
TextBox{parent=rect,text="Transfer Capacity",x=11,y=17,height=1,width=17,fg_bg=label_fg_bg}
|
||||
local trans_cap = PowerIndicator{parent=rect,x=19,y=18,lu_colors=lu_col,label="",format="%5.2f",rate=true,value=0,width=12,fg_bg=text_fg_bg}
|
||||
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill: ",format="%7.2f",unit="%",value=0,width=20,fg_bg=text_fg}
|
||||
local cells = DataIndicator{parent=rect,x=11,y=13,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg}
|
||||
local providers = DataIndicator{parent=rect,x=11,y=14,lu_colors=lu_col,label="Providers:",format="%7d",value=0,width=18,fg_bg=text_fg}
|
||||
|
||||
fill.register(ps, "energy_fill", function (val) fill.update(val * 100) end)
|
||||
cells.register(ps, "cells", cells.update)
|
||||
providers.register(ps, "providers", providers.update)
|
||||
fill.register(ps, "energy_fill", function (val) fill.update(val * 100) end)
|
||||
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end)
|
||||
|
||||
local chging = IndicatorLight{parent=rect,x=11,y=16,label="Charging",colors=ind_wht}
|
||||
local dischg = IndicatorLight{parent=rect,x=11,y=17,label="Discharging",colors=ind_wht}
|
||||
local max_io = IndicatorLight{parent=rect,x=11,y=18,label="Max I/O Rate",colors=ind_yel}
|
||||
|
||||
chging.register(ps, "is_charging", chging.update)
|
||||
dischg.register(ps, "is_discharging", dischg.update)
|
||||
max_io.register(ps, "at_max_io", max_io.update)
|
||||
|
||||
local charge = VerticalBar{parent=rect,x=2,y=2,fg_bg=cpair(colors.green,colors.gray),height=17,width=4}
|
||||
local in_cap = VerticalBar{parent=rect,x=7,y=12,fg_bg=cpair(colors.red,colors.gray),height=7,width=1}
|
||||
local out_cap = VerticalBar{parent=rect,x=9,y=12,fg_bg=cpair(colors.blue,colors.gray),height=7,width=1}
|
||||
|
||||
TextBox{parent=rect,text="FILL",x=2,y=20,height=1,width=4,fg_bg=text_fg_bg}
|
||||
TextBox{parent=rect,text="I/O",x=7,y=20,height=1,width=3,fg_bg=text_fg_bg}
|
||||
TextBox{parent=rect,text="FILL I/O",x=2,y=20,width=8,fg_bg=label_fg}
|
||||
|
||||
local function calc_saturation(val)
|
||||
if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then
|
||||
return val / data.build.transfer_cap
|
||||
else
|
||||
return 0
|
||||
end
|
||||
else return 0 end
|
||||
end
|
||||
|
||||
charge.register(ps, "energy_fill", charge.update)
|
||||
in_cap.register(ps, "last_input", function (val) in_cap.update(calc_saturation(val)) end)
|
||||
out_cap.register(ps, "last_output", function (val) out_cap.update(calc_saturation(val)) end)
|
||||
|
||||
local eta = TextBox{parent=rect,x=11,y=20,width=20,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=style.theme.field_box}
|
||||
|
||||
eta.register(ps, "eta_ms", function (eta_ms)
|
||||
local str, pre = "", util.trinary(eta_ms >= 0, "Full in ", "Empty in ")
|
||||
|
||||
local seconds = math.abs(eta_ms) / 1000
|
||||
local minutes = seconds / 60
|
||||
local hours = minutes / 60
|
||||
local days = hours / 24
|
||||
|
||||
if math.abs(eta_ms) < 1000 or (eta_ms ~= eta_ms) then
|
||||
-- really small or NaN
|
||||
str = "No ETA"
|
||||
elseif days < 1000 then
|
||||
days = math.floor(days)
|
||||
hours = math.floor(hours % 24)
|
||||
minutes = math.floor(minutes % 60)
|
||||
seconds = math.floor(seconds % 60)
|
||||
|
||||
if days > 0 then
|
||||
str = days .. "d"
|
||||
elseif hours > 0 then
|
||||
str = hours .. "h " .. minutes .. "m"
|
||||
elseif minutes > 0 then
|
||||
str = minutes .. "m " .. seconds .. "s"
|
||||
elseif seconds > 0 then
|
||||
str = seconds .. "s"
|
||||
end
|
||||
|
||||
str = pre .. str
|
||||
else
|
||||
local years = math.floor(days / 365.25)
|
||||
|
||||
if years <= 99999999 then
|
||||
str = pre .. years .. "y"
|
||||
else
|
||||
str = pre .. "eras"
|
||||
end
|
||||
end
|
||||
|
||||
eta.set_value(str)
|
||||
end)
|
||||
end
|
||||
|
||||
return new_view
|
||||
|
||||
@@ -4,41 +4,48 @@
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
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 TEXT_ALIGN = core.TEXT_ALIGN
|
||||
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
|
||||
local s_hi_bright = style.fp_theme.highlight_box_bright
|
||||
|
||||
local label_fg = style.fp.label_fg
|
||||
|
||||
local ps = iocontrol.get_db().fp.ps
|
||||
|
||||
-- root div
|
||||
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
|
||||
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)}
|
||||
local 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=cpair(colors.black,colors.lightGray)}
|
||||
local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)}
|
||||
TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
|
||||
TextBox{parent=entry,x=1,y=1,text="",width=8,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=cpair(colors.lightGray,colors.white)}
|
||||
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=cpair(colors.lightGray,colors.white)}
|
||||
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4}
|
||||
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,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,55 +8,71 @@ 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 TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
|
||||
local period = core.flasher.PERIOD
|
||||
|
||||
-- new process control view
|
||||
---@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)
|
||||
local s_hi_box = style.theme.highlight_box
|
||||
local s_field = style.theme.field_box
|
||||
|
||||
local lu_cpair = style.lu_colors
|
||||
local hzd_fg_bg = style.hzd_fg_bg
|
||||
local dis_colors = style.dis_colors
|
||||
local arrow_fg_bg = cpair(style.theme.label, s_hi_box.bkg)
|
||||
|
||||
local ind_grn = style.ind_grn
|
||||
local ind_yel = style.ind_yel
|
||||
local ind_red = style.ind_red
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
assert(root.get_height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
local black = cpair(colors.black, colors.black)
|
||||
local blk_brn = cpair(colors.black, colors.brown)
|
||||
local blk_pur = cpair(colors.black, colors.purple)
|
||||
|
||||
local bw_fg_bg = cpair(colors.black, colors.white)
|
||||
local hzd_fg_bg = cpair(colors.white, colors.gray)
|
||||
local lu_cpair = cpair(colors.gray, colors.gray)
|
||||
local dis_colors = cpair(colors.white, colors.lightGray)
|
||||
local 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=cpair(colors.green,colors.red)}
|
||||
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
|
||||
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=cpair(colors.green,colors.gray)}
|
||||
local sps = IndicatorLight{parent=main,label="SPS Connected",colors=cpair(colors.green,colors.gray)}
|
||||
local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=ind_grn}
|
||||
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=style.ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd}
|
||||
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=ind_grn}
|
||||
local sps = IndicatorLight{parent=main,label="SPS Connected",colors=ind_grn}
|
||||
|
||||
all_ok.register(facility.ps, "all_sys_ok", all_ok.update)
|
||||
rad_mon.register(facility.ps, "rad_computed_status", rad_mon.update)
|
||||
@@ -65,10 +81,10 @@ local function new_view(root, x, y)
|
||||
|
||||
main.line_break()
|
||||
|
||||
local auto_ready = IndicatorLight{parent=main,label="Configured Units Ready",colors=cpair(colors.green,colors.red)}
|
||||
local auto_act = IndicatorLight{parent=main,label="Process Active",colors=cpair(colors.green,colors.gray)}
|
||||
local auto_ramp = IndicatorLight{parent=main,label="Process Ramping",colors=cpair(colors.white,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local auto_sat = IndicatorLight{parent=main,label="Min/Max Burn Rate",colors=cpair(colors.yellow,colors.gray)}
|
||||
local auto_ready = IndicatorLight{parent=main,label="Configured Units Ready",colors=ind_grn}
|
||||
local auto_act = IndicatorLight{parent=main,label="Process Active",colors=ind_grn}
|
||||
local auto_ramp = IndicatorLight{parent=main,label="Process Ramping",colors=ind_wht,flash=true,period=period.BLINK_250_MS}
|
||||
local auto_sat = IndicatorLight{parent=main,label="Min/Max Burn Rate",colors=ind_yel}
|
||||
|
||||
auto_ready.register(facility.ps, "auto_ready", auto_ready.update)
|
||||
auto_act.register(facility.ps, "auto_active", auto_act.update)
|
||||
@@ -77,26 +93,26 @@ local function new_view(root, x, y)
|
||||
|
||||
main.line_break()
|
||||
|
||||
local auto_scram = IndicatorLight{parent=main,label="Automatic SCRAM",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local matrix_dc = IndicatorLight{parent=main,label="Matrix Disconnected",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local matrix_fill = IndicatorLight{parent=main,label="Matrix Charge High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local unit_crit = IndicatorLight{parent=main,label="Unit Critical Alarm",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local fac_rad_h = IndicatorLight{parent=main,label="Facility Radiation High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local gen_fault = IndicatorLight{parent=main,label="Gen. Control Fault",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local auto_scram = IndicatorLight{parent=main,label="Automatic SCRAM",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local matrix_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}
|
||||
local radiation = RadIndicator{parent=main,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
|
||||
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}
|
||||
local rtu_count = DataIndicator{parent=main,x=15,y=24,label="",format="%11d",value=0,lu_colors=lu_cpair,width=11,fg_bg=bw_fg_bg}
|
||||
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)
|
||||
|
||||
---------------------
|
||||
@@ -111,38 +127,38 @@ local function new_view(root, x, y)
|
||||
|
||||
local targets = Div{parent=proc,width=31,height=24,x=1,y=1}
|
||||
|
||||
local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
|
||||
local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=blk_pur}
|
||||
TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2}
|
||||
|
||||
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
|
||||
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
|
||||
TextBox{parent=burn_target,x=18,y=2,text="mB/t"}
|
||||
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
|
||||
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=s_hi_box}
|
||||
local b_target = 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}
|
||||
|
||||
b_target.register(facility.ps, "process_burn_target", b_target.set_value)
|
||||
burn_sum.register(facility.ps, "burn_sum", burn_sum.update)
|
||||
|
||||
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
|
||||
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=blk_pur}
|
||||
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
|
||||
|
||||
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
|
||||
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
|
||||
TextBox{parent=chg_target,x=18,y=2,text="MFE"}
|
||||
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
|
||||
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=s_hi_box}
|
||||
local c_target = 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], "energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end)
|
||||
cur_charge.register(facility.induction_ps_tbl[1], "avg_charge", function (fe) cur_charge.update(db.energy_convert_from_fe(fe) / 1000000) end)
|
||||
|
||||
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
|
||||
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur}
|
||||
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
|
||||
|
||||
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
|
||||
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
|
||||
TextBox{parent=gen_target,x=18,y=2,text="kFE/t"}
|
||||
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
|
||||
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=s_hi_box}
|
||||
local g_target = 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 --
|
||||
@@ -154,18 +170,18 @@ local function new_view(root, x, y)
|
||||
|
||||
for i = 1, 4 do
|
||||
local unit
|
||||
local tag_fg_bg = cpair(colors.gray,colors.white)
|
||||
local lim_fg_bg = cpair(colors.lightGray,colors.white)
|
||||
local ctl_fg = colors.lightGray
|
||||
local cur_fg_bg = cpair(colors.lightGray,colors.white)
|
||||
local cur_lu = colors.lightGray
|
||||
local tag_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
|
||||
local lim_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
|
||||
local label_fg = style.theme.disabled_fg
|
||||
local cur_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
|
||||
local cur_lu = style.theme.disabled
|
||||
|
||||
if i <= facility.num_units then
|
||||
unit = units[i] ---@type ioctl_unit
|
||||
tag_fg_bg = cpair(colors.black,colors.lightBlue)
|
||||
lim_fg_bg = bw_fg_bg
|
||||
ctl_fg = colors.gray
|
||||
cur_fg_bg = cpair(colors.black,colors.brown)
|
||||
unit = units[i]
|
||||
tag_fg_bg = cpair(colors.black, colors.lightBlue)
|
||||
lim_fg_bg = s_hi_box
|
||||
label_fg = style.theme.label_fg
|
||||
cur_fg_bg = blk_brn
|
||||
cur_lu = colors.black
|
||||
end
|
||||
|
||||
@@ -174,9 +190,9 @@ local function new_view(root, x, y)
|
||||
local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
|
||||
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2}
|
||||
|
||||
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(ctl_fg,colors.white)}
|
||||
local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=lim_fg_bg}
|
||||
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1}
|
||||
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=s_hi_box}
|
||||
local lim = 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}
|
||||
|
||||
@@ -198,14 +214,14 @@ local function new_view(root, x, y)
|
||||
local stat_div = Div{parent=proc,width=22,height=24,x=57,y=6}
|
||||
|
||||
for i = 1, 4 do
|
||||
local tag_fg_bg = cpair(colors.gray,colors.white)
|
||||
local ind_fg_bg = cpair(colors.lightGray,colors.white)
|
||||
local ind_off = colors.lightGray
|
||||
local tag_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
|
||||
local ind_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
|
||||
local ind_off = style.theme.disabled
|
||||
|
||||
if i <= facility.num_units then
|
||||
tag_fg_bg = cpair(colors.black,colors.cyan)
|
||||
ind_fg_bg = bw_fg_bg
|
||||
ind_off = colors.gray
|
||||
tag_fg_bg = cpair(colors.black, colors.cyan)
|
||||
ind_fg_bg = cpair(style.theme.text, s_hi_box.bkg)
|
||||
ind_off = style.ind_hi_box_bg
|
||||
end
|
||||
|
||||
local _y = ((i - 1) * 5) + 1
|
||||
@@ -214,11 +230,11 @@ local function new_view(root, x, y)
|
||||
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Status",width=7,height=2}
|
||||
|
||||
local lights = Div{parent=stat_div,x=9,y=_y,width=14,height=4,fg_bg=ind_fg_bg}
|
||||
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(colors.green,ind_off)}
|
||||
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(colors.red,ind_off),flash=true,period=period.BLINK_250_MS}
|
||||
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(ind_grn.fgd,ind_off)}
|
||||
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(ind_red.fgd,ind_off),flash=true,period=period.BLINK_250_MS}
|
||||
|
||||
if i <= facility.num_units then
|
||||
local unit = units[i] ---@type ioctl_unit
|
||||
local unit = units[i]
|
||||
|
||||
ready.register(unit.unit_ps, "U_AutoReady", ready.update)
|
||||
degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update)
|
||||
@@ -230,39 +246,40 @@ local function new_view(root, x, y)
|
||||
-------------------------
|
||||
|
||||
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
|
||||
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.white,colors.black),radio_bg=colors.purple}
|
||||
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
|
||||
|
||||
mode.register(facility.ps, "process_mode", mode.set_value)
|
||||
|
||||
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,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)
|
||||
|
||||
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=cpair(colors.gray,colors.white)}
|
||||
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=s_hi_box}
|
||||
|
||||
-- save the automatic process control configuration without starting
|
||||
local function _save_cfg()
|
||||
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)
|
||||
@@ -304,10 +321,10 @@ 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}
|
||||
local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=cpair(colors.white,colors.gray)}
|
||||
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}
|
||||
|
||||
a_waste.register(unit.unit_ps, "U_AutoWaste", a_waste.update)
|
||||
@@ -316,40 +333,36 @@ local function new_view(root, x, y)
|
||||
|
||||
local waste_sel = Div{parent=proc,width=21,height=24,x=81,y=1}
|
||||
|
||||
TextBox{parent=waste_sel,text=" ",width=21,height=1,x=1,y=1,fg_bg=cpair(colors.black,colors.brown)}
|
||||
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=TEXT_ALIGN.CENTER,width=21,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.brown)}
|
||||
local cutout_fg_bg = cpair(style.theme.bg, colors.brown)
|
||||
|
||||
TextBox{parent=waste_sel,text=" ",width=21,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}
|
||||
|
||||
status.register(facility.ps, "current_waste_product", status.update)
|
||||
|
||||
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.white,colors.black),radio_bg=colors.brown}
|
||||
local pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.green,colors.black)}
|
||||
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.brown}
|
||||
|
||||
waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value)
|
||||
pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value)
|
||||
|
||||
local fb_active = IndicatorLight{parent=rect,x=2,y=9,label="Fallback Active",colors=cpair(colors.white,colors.gray)}
|
||||
local fb_active = IndicatorLight{parent=rect,x=2,y=7,label="Fallback Active",colors=ind_wht}
|
||||
local sps_disabled = IndicatorLight{parent=rect,x=2,y=8,label="SPS Disabled LC",colors=ind_yel}
|
||||
|
||||
fb_active.register(facility.ps, "pu_fallback_active", fb_active.update)
|
||||
sps_disabled.register(facility.ps, "sps_disabled_low_power", sps_disabled.update)
|
||||
|
||||
TextBox{parent=rect,x=2,y=11,text="Plutonium Rate",height=1,width=17,fg_bg=style.label}
|
||||
local pu_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
|
||||
local pu_fallback = Checkbox{parent=rect,x=2,y=10,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.brown,style.theme.checkbox_bg)}
|
||||
|
||||
TextBox{parent=rect,x=2,y=14,text="Polonium Rate",height=1,width=17,fg_bg=style.label}
|
||||
local po_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
|
||||
TextBox{parent=rect,x=2,y=12,height=3,text="Switch to Pu when SNAs cannot keep up with waste.",fg_bg=style.label}
|
||||
|
||||
TextBox{parent=rect,x=2,y=17,text="Antimatter Rate",height=1,width=17,fg_bg=style.label}
|
||||
local am_rate = DataIndicator{parent=rect,x=2,label="",unit="\xb5B/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
|
||||
local lc_sps = Checkbox{parent=rect,x=2,y=16,label="Low Charge SPS",callback=process.set_sps_low_power,box_fg_bg=cpair(colors.brown,style.theme.checkbox_bg)}
|
||||
|
||||
pu_rate.register(facility.ps, "pu_rate", pu_rate.update)
|
||||
po_rate.register(facility.ps, "po_rate", po_rate.update)
|
||||
am_rate.register(facility.ps, "am_rate", am_rate.update)
|
||||
TextBox{parent=rect,x=2,y=18,height=3,text="Use SPS at low charge, otherwise switches to Po.",fg_bg=style.label}
|
||||
|
||||
local sna_count = DataIndicator{parent=rect,x=2,y=20,label="Linked SNAs:",format="%4d",value=0,lu_colors=lu_cpair,width=17}
|
||||
|
||||
sna_count.register(facility.ps, "sna_count", sna_count.update)
|
||||
pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value)
|
||||
lc_sps.register(facility.ps, "process_sps_low_power", lc_sps.set_value)
|
||||
end
|
||||
|
||||
return new_view
|
||||
|
||||
@@ -1,48 +1,52 @@
|
||||
local types = require("scada-common.types")
|
||||
|
||||
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 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
|
||||
local function new_view(root, x, y, ps)
|
||||
local reactor = Rectangle{parent=root,border=border(1, colors.gray, true),width=30,height=7,x=x,y=y}
|
||||
local text_fg = style.theme.text_fg
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
local text_fg_bg = cpair(colors.black, colors.lightGray)
|
||||
local lu_col = cpair(colors.gray, colors.gray)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local reactor = Rectangle{parent=root,border=border(1,colors.gray,true),width=30,height=7,x=x,y=y}
|
||||
|
||||
local status = StateIndicator{parent=reactor,x=6,y=1,states=style.reactor.states,value=1,min_width=16}
|
||||
local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit="K",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
local heating_r = DataIndicator{parent=reactor,x=2,y=5,lu_colors=lu_col,label="Heating:",unit="mB/t",format="%12.0f",value=0,commas=true,width=26,fg_bg=text_fg_bg}
|
||||
local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
|
||||
local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.2f",value=0,width=26,fg_bg=text_fg}
|
||||
local heating_r = DataIndicator{parent=reactor,x=2,y=5,lu_colors=lu_col,label="Heating:",unit="mB/t",format="%12.0f",value=0,commas=true,width=26,fg_bg=text_fg}
|
||||
|
||||
status.register(ps, "computed_status", status.update)
|
||||
core_temp.register(ps, "temp", core_temp.update)
|
||||
core_temp.register(ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end)
|
||||
burn_r.register(ps, "act_burn_rate", burn_r.update)
|
||||
heating_r.register(ps, "heating_rate", heating_r.update)
|
||||
|
||||
local reactor_fills = Rectangle{parent=root,border=border(1, colors.gray, true),width=24,height=7,x=(x + 29),y=y}
|
||||
|
||||
TextBox{parent=reactor_fills,text="FUEL",x=2,y=1,height=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=reactor_fills,text="COOL",x=2,y=2,height=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=reactor_fills,text="HCOOL",x=2,y=4,height=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=reactor_fills,text="WASTE",x=2,y=5,height=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=reactor_fills,text="FUEL",x=2,y=1,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(colors.black,colors.gray),height=1,width=14}
|
||||
local fuel = HorizontalBar{parent=reactor_fills,x=8,y=1,show_percent=true,bar_fg_bg=cpair(style.theme.fuel_color,colors.gray),height=1,width=14}
|
||||
local ccool = HorizontalBar{parent=reactor_fills,x=8,y=2,show_percent=true,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=14}
|
||||
local hcool = HorizontalBar{parent=reactor_fills,x=8,y=4,show_percent=true,bar_fg_bg=cpair(colors.white,colors.gray),height=1,width=14}
|
||||
local waste = HorizontalBar{parent=reactor_fills,x=8,y=5,show_percent=true,bar_fg_bg=cpair(colors.brown,colors.gray),height=1,width=14}
|
||||
|
||||
@@ -1,44 +1,46 @@
|
||||
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
|
||||
local function new_view(root, x, y, ps)
|
||||
local turbine = Rectangle{parent=root,border=border(1, colors.gray, true),width=23,height=7,x=x,y=y}
|
||||
local text_fg = style.theme.text_fg
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
local text_fg_bg = cpair(colors.black, colors.lightGray)
|
||||
local lu_col = cpair(colors.gray, colors.gray)
|
||||
local 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_bg}
|
||||
local flow_rate = DataIndicator{parent=turbine,x=5,y=4,lu_colors=lu_col,label="",unit="mB/t",format="%10.0f",value=0,commas=true,width=16,fg_bg=text_fg_bg}
|
||||
local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",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)
|
||||
flow_rate.register(ps, "flow_rate", flow_rate.update)
|
||||
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_bg}
|
||||
TextBox{parent=turbine,text="E",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=turbine,text="S",x=2,y=5,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)
|
||||
|
||||
@@ -2,43 +2,68 @@
|
||||
-- Reactor Unit SCADA Coordinator GUI
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
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 TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local AUTO_GROUP = types.AUTO_GROUP
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local gry_wht = style.gray_white
|
||||
|
||||
local period = core.flasher.PERIOD
|
||||
|
||||
-- create a unit view
|
||||
---@param parent graphics_element parent
|
||||
---@param parent Container parent
|
||||
---@param id integer
|
||||
local function init(parent, id)
|
||||
local unit = iocontrol.get_db().units[id] ---@type ioctl_unit
|
||||
local f_ps = iocontrol.get_db().facility.ps
|
||||
local s_hi_box = style.theme.highlight_box
|
||||
local s_hi_bright = style.theme.highlight_box_bright
|
||||
local s_field = style.theme.field_box
|
||||
|
||||
local hc_text = style.hc_text
|
||||
local lu_cpair = style.lu_colors
|
||||
local hzd_fg_bg = style.hzd_fg_bg
|
||||
local dis_colors = style.dis_colors
|
||||
local arrow_fg_bg = cpair(style.theme.label, s_hi_box.bkg)
|
||||
|
||||
local ind_bkg = style.ind_bkg
|
||||
local ind_grn = style.ind_grn
|
||||
local ind_yel = style.ind_yel
|
||||
local ind_red = style.ind_red
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
local unit = db.units[id]
|
||||
local f_ps = db.facility.ps
|
||||
|
||||
local main = Div{parent=parent,x=1,y=1}
|
||||
|
||||
@@ -48,11 +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=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
|
||||
local bw_fg_bg = cpair(colors.black, colors.white)
|
||||
local hzd_fg_bg = cpair(colors.white, colors.gray)
|
||||
local lu_cpair = cpair(colors.gray, colors.gray)
|
||||
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,fg_bg=style.theme.header}
|
||||
|
||||
-----------------------------
|
||||
-- main stats and core map --
|
||||
@@ -62,22 +83,22 @@ 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}
|
||||
local heating_r = DataIndicator{parent=main,x=12,label="",format="%14.0f",value=0,unit="mB/t",commas=true,lu_colors=lu_cpair,width=19,fg_bg=bw_fg_bg}
|
||||
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}
|
||||
local burn_r = DataIndicator{parent=main,x=12,label="",format="%14.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=19,fg_bg=bw_fg_bg}
|
||||
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(colors.black,colors.gray),height=4,width=1}
|
||||
local fuel = VerticalBar{parent=main,x=2,y=23,fg_bg=cpair(style.theme.fuel_color,colors.gray),height=4,width=1}
|
||||
local ccool = VerticalBar{parent=main,x=4,y=23,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1}
|
||||
local hcool = VerticalBar{parent=main,x=8,y=23,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
|
||||
local waste = VerticalBar{parent=main,x=10,y=23,fg_bg=cpair(colors.brown,colors.gray),height=4,width=1}
|
||||
@@ -88,7 +109,7 @@ local function init(parent, id)
|
||||
waste.register(u_ps, "waste_fill", waste.update)
|
||||
|
||||
ccool.register(u_ps, "ccool_type", function (type)
|
||||
if type == "mekanism:sodium" then
|
||||
if type == types.FLUID.SODIUM then
|
||||
ccool.recolor(cpair(colors.lightBlue, colors.gray))
|
||||
else
|
||||
ccool.recolor(cpair(colors.blue, colors.gray))
|
||||
@@ -96,27 +117,28 @@ local function init(parent, id)
|
||||
end)
|
||||
|
||||
hcool.register(u_ps, "hcool_type", function (type)
|
||||
if type == "mekanism:superheated_sodium" then
|
||||
if type == types.FLUID.SUPERHEATED_SODIUM then
|
||||
hcool.recolor(cpair(colors.orange, colors.gray))
|
||||
else
|
||||
hcool.recolor(cpair(colors.white, colors.gray))
|
||||
end
|
||||
end)
|
||||
|
||||
TextBox{parent=main,x=32,y=22,text="Core Temp",height=1,width=9,fg_bg=style.label}
|
||||
local core_temp = DataIndicator{parent=main,x=32,label="",format="%11.2f",value=0,unit="K",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
|
||||
core_temp.register(u_ps, "temp", core_temp.update)
|
||||
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}
|
||||
local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
|
||||
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}
|
||||
local damage_p = DataIndicator{parent=main,x=32,label="",format="%11.0f",value=0,unit="%",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
|
||||
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}
|
||||
local radiation = RadIndicator{parent=main,x=32,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
|
||||
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)
|
||||
|
||||
-------------------
|
||||
@@ -124,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=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,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)
|
||||
@@ -139,9 +161,9 @@ local function init(parent, id)
|
||||
local annunciator = Div{parent=main,width=23,height=18,x=22,y=3}
|
||||
|
||||
-- connectivity
|
||||
local plc_online = IndicatorLight{parent=annunciator,label="PLC Online",colors=cpair(colors.green,colors.red)}
|
||||
local plc_hbeat = IndicatorLight{parent=annunciator,label="PLC Heartbeat",colors=cpair(colors.white,colors.gray)}
|
||||
local rad_mon = TriIndicatorLight{parent=annunciator,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
|
||||
local plc_online = IndicatorLight{parent=annunciator,label="PLC Online",colors=cpair(ind_grn.fgd,ind_red.fgd)}
|
||||
local plc_hbeat = IndicatorLight{parent=annunciator,label="PLC Heartbeat",colors=ind_wht}
|
||||
local rad_mon = TriIndicatorLight{parent=annunciator,label="Radiation Monitor",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd}
|
||||
|
||||
plc_online.register(u_ps, "PLCOnline", plc_online.update)
|
||||
plc_hbeat.register(u_ps, "PLCHeartbeat", plc_hbeat.update)
|
||||
@@ -150,25 +172,25 @@ local function init(parent, id)
|
||||
annunciator.line_break()
|
||||
|
||||
-- operating state
|
||||
local r_active = IndicatorLight{parent=annunciator,label="Active",colors=cpair(colors.green,colors.gray)}
|
||||
local r_auto = IndicatorLight{parent=annunciator,label="Automatic Control",colors=cpair(colors.white,colors.gray)}
|
||||
local r_active = IndicatorLight{parent=annunciator,label="Active",colors=ind_grn}
|
||||
local r_auto = IndicatorLight{parent=annunciator,label="Automatic Control",colors=ind_wht}
|
||||
|
||||
r_active.register(u_ps, "status", r_active.update)
|
||||
r_auto.register(u_ps, "AutoControl", r_auto.update)
|
||||
|
||||
-- main unit transient/warning annunciator panel
|
||||
local r_scram = IndicatorLight{parent=annunciator,label="Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
|
||||
local r_mscrm = IndicatorLight{parent=annunciator,label="Manual Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
|
||||
local r_ascrm = IndicatorLight{parent=annunciator,label="Auto Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
|
||||
local rad_wrn = IndicatorLight{parent=annunciator,label="Radiation Warning",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=cpair(colors.red,colors.gray)}
|
||||
local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=cpair(colors.red,colors.gray)}
|
||||
local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_wloc = IndicatorLight{parent=annunciator,label="Waste Line Occlusion",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_hsrt = IndicatorLight{parent=annunciator,label="Startup Rate High",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_scram = IndicatorLight{parent=annunciator,label="Reactor SCRAM",colors=ind_red}
|
||||
local r_mscrm = IndicatorLight{parent=annunciator,label="Manual Reactor SCRAM",colors=ind_red}
|
||||
local r_ascrm = IndicatorLight{parent=annunciator,label="Auto Reactor SCRAM",colors=ind_red}
|
||||
local rad_wrn = IndicatorLight{parent=annunciator,label="Radiation Warning",colors=ind_yel}
|
||||
local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=ind_red}
|
||||
local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=ind_yel}
|
||||
local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=ind_yel}
|
||||
local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=ind_red}
|
||||
local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=ind_yel}
|
||||
local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=ind_yel}
|
||||
local r_wloc = IndicatorLight{parent=annunciator,label="Waste Line Occlusion",colors=ind_yel}
|
||||
local r_hsrt = IndicatorLight{parent=annunciator,label="Startup Rate High",colors=ind_yel}
|
||||
|
||||
r_scram.register(u_ps, "ReactorSCRAM", r_scram.update)
|
||||
r_mscrm.register(u_ps, "ManualReactorSCRAM", r_mscrm.update)
|
||||
@@ -185,20 +207,20 @@ local function init(parent, id)
|
||||
|
||||
-- RPS annunciator panel
|
||||
|
||||
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=8}
|
||||
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,x=46,y=8}
|
||||
local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9}
|
||||
local rps_annunc = Div{parent=rps,width=31,height=10,x=2,y=1}
|
||||
|
||||
local rps_trp = IndicatorLight{parent=rps_annunc,label="RPS Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local rps_dmg = IndicatorLight{parent=rps_annunc,label="Damage Level High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local rps_exh = IndicatorLight{parent=rps_annunc,label="Excess Heated Coolant",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_exw = IndicatorLight{parent=rps_annunc,label="Excess Waste",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_tmp = IndicatorLight{parent=rps_annunc,label="Core Temperature High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local rps_nof = IndicatorLight{parent=rps_annunc,label="No Fuel",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_loc = IndicatorLight{parent=rps_annunc,label="Coolant Level Low Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_flt = IndicatorLight{parent=rps_annunc,label="PPM Fault",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local rps_tmo = IndicatorLight{parent=rps_annunc,label="Connection Timeout",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local rps_sfl = IndicatorLight{parent=rps_annunc,label="System Failure",colors=cpair(colors.orange,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local rps_trp = IndicatorLight{parent=rps_annunc,label="RPS Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local rps_dmg = IndicatorLight{parent=rps_annunc,label="Damage Level High",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local rps_exh = IndicatorLight{parent=rps_annunc,label="Excess Heated Coolant",colors=ind_yel}
|
||||
local rps_exw = IndicatorLight{parent=rps_annunc,label="Excess Waste",colors=ind_yel}
|
||||
local rps_tmp = IndicatorLight{parent=rps_annunc,label="Core Temperature High",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local rps_nof = IndicatorLight{parent=rps_annunc,label="No Fuel",colors=ind_yel}
|
||||
local rps_loc = IndicatorLight{parent=rps_annunc,label="Coolant Level Low Low",colors=ind_yel}
|
||||
local rps_flt = IndicatorLight{parent=rps_annunc,label="PPM Fault",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
|
||||
local rps_tmo = IndicatorLight{parent=rps_annunc,label="Connection Timeout",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
|
||||
local rps_sfl = IndicatorLight{parent=rps_annunc,label="System Failure",colors=ind_red,flash=true,period=period.BLINK_500_MS}
|
||||
|
||||
rps_trp.register(u_ps, "rps_tripped", rps_trp.update)
|
||||
rps_dmg.register(u_ps, "high_dmg", rps_dmg.update)
|
||||
@@ -213,17 +235,17 @@ local function init(parent, id)
|
||||
|
||||
-- cooling annunciator panel
|
||||
|
||||
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=22}
|
||||
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,x=46,y=22}
|
||||
local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23}
|
||||
local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1}
|
||||
local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7}
|
||||
|
||||
local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=colors.gray,c2=colors.white,c3=colors.green}
|
||||
local c_cfm = IndicatorLight{parent=rcs_annunc,label="Coolant Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=ind_yel}
|
||||
local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=ind_bkg,c2=ind_wht.fgd,c3=ind_grn.fgd}
|
||||
local c_cfm = IndicatorLight{parent=rcs_annunc,label="Coolant Feed Mismatch",colors=ind_yel}
|
||||
local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=ind_yel}
|
||||
local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=ind_yel}
|
||||
local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=ind_yel}
|
||||
|
||||
c_flt.register(u_ps, "RCSFault", c_flt.update)
|
||||
c_emg.register(u_ps, "EmergencyCoolant", c_emg.update)
|
||||
@@ -242,15 +264,15 @@ local function init(parent, id)
|
||||
|
||||
-- boiler annunciator panel(s)
|
||||
|
||||
if available_space > 0 then _add_space() end
|
||||
|
||||
if unit.num_boilers > 0 then
|
||||
TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=cpair(colors.red,colors.gray)}
|
||||
b1_wll.register(b_ps[1], "WasterLevelLow", b1_wll.update)
|
||||
if available_space > 0 then _add_space() end
|
||||
|
||||
TextBox{parent=rcs_tags,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
TextBox{parent=rcs_tags,x=1,text="B1",width=2,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,fg_bg=hc_text}
|
||||
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
|
||||
b1_hr.register(b_ps[1], "HeatingRateLow", b1_hr.update)
|
||||
end
|
||||
if unit.num_boilers > 1 then
|
||||
@@ -261,12 +283,12 @@ local function init(parent, id)
|
||||
_add_space()
|
||||
end
|
||||
|
||||
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=cpair(colors.red,colors.gray)}
|
||||
b2_wll.register(b_ps[2], "WasterLevelLow", b2_wll.update)
|
||||
TextBox{parent=rcs_tags,text="B2",width=2,fg_bg=hc_text}
|
||||
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
|
||||
b2_wll.register(b_ps[2], "WaterLevelLow", b2_wll.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
TextBox{parent=rcs_tags,text="B2",width=2,fg_bg=hc_text}
|
||||
local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
|
||||
b2_hr.register(b_ps[2], "HeatingRateLow", b2_hr.update)
|
||||
end
|
||||
|
||||
@@ -274,20 +296,20 @@ local function init(parent, id)
|
||||
|
||||
if available_space > 1 then _add_space() end
|
||||
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
|
||||
local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
|
||||
t1_sdo.register(t_ps[1], "SteamDumpOpen", t1_sdo.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
|
||||
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
|
||||
t1_tos.register(t_ps[1], "TurbineOverSpeed", t1_tos.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
|
||||
local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
|
||||
t1_gtrp.register(t_ps[1], "GeneratorTrip", t1_gtrp.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
|
||||
local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
t1_trp.register(t_ps[1], "TurbineTrip", t1_trp.update)
|
||||
|
||||
if unit.num_turbines > 1 then
|
||||
@@ -295,55 +317,55 @@ local function init(parent, id)
|
||||
_add_space()
|
||||
end
|
||||
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
|
||||
local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
|
||||
t2_sdo.register(t_ps[2], "SteamDumpOpen", t2_sdo.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
|
||||
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
|
||||
t2_tos.register(t_ps[2], "TurbineOverSpeed", t2_tos.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
|
||||
local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
|
||||
t2_gtrp.register(t_ps[2], "GeneratorTrip", t2_gtrp.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
|
||||
local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
t2_trp.register(t_ps[2], "TurbineTrip", t2_trp.update)
|
||||
end
|
||||
|
||||
if unit.num_turbines > 2 then
|
||||
if available_space > 3 then _add_space() end
|
||||
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
|
||||
local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
|
||||
t3_sdo.register(t_ps[3], "SteamDumpOpen", t3_sdo.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
|
||||
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
|
||||
t3_tos.register(t_ps[3], "TurbineOverSpeed", t3_tos.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
|
||||
local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
|
||||
t3_gtrp.register(t_ps[3], "GeneratorTrip", t3_gtrp.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
|
||||
local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update)
|
||||
end
|
||||
|
||||
util.nop()
|
||||
|
||||
----------------------
|
||||
-- reactor controls --
|
||||
----------------------
|
||||
|
||||
local dis_colors = cpair(colors.white, colors.lightGray)
|
||||
|
||||
local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=cpair(colors.gray,colors.white)}
|
||||
local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
|
||||
TextBox{parent=burn_control,x=9,y=2,text="mB/t"}
|
||||
local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=s_hi_box}
|
||||
local burn_rate = 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
|
||||
local set_burn_btn = PushButton{parent=burn_control,x=14,y=2,text="SET",min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),dis_fg_bg=dis_colors,callback=set_burn}
|
||||
local set_burn_btn = PushButton{parent=burn_control,x=14,y=2,text="SET",min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=style.wh_gray,dis_fg_bg=dis_colors,callback=set_burn}
|
||||
|
||||
burn_rate.register(u_ps, "burn_rate", burn_rate.set_value)
|
||||
burn_rate.register(u_ps, "max_burn", burn_rate.set_max)
|
||||
@@ -353,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)
|
||||
@@ -374,7 +394,7 @@ local function init(parent, id)
|
||||
|
||||
reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
|
||||
|
||||
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=48}
|
||||
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=ALIGN.CENTER,width=33,x=46,y=48}
|
||||
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
|
||||
local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1}
|
||||
|
||||
@@ -386,22 +406,22 @@ local function init(parent, id)
|
||||
-- alarm management --
|
||||
----------------------
|
||||
|
||||
local alarm_panel = Div{parent=main,x=2,y=36,width=29,height=16,fg_bg=bw_fg_bg}
|
||||
local alarm_panel = Div{parent=main,x=2,y=36,width=29,height=16,fg_bg=s_hi_bright}
|
||||
|
||||
local a_brc = AlarmLight{parent=alarm_panel,x=6,y=2,label="Containment Breach",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rad = AlarmLight{parent=alarm_panel,x=6,label="Containment Radiation",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||
local a_dmg = AlarmLight{parent=alarm_panel,x=6,label="Critical Damage",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||
local a_brc = AlarmLight{parent=alarm_panel,x=6,y=2,label="Containment Breach",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rad = AlarmLight{parent=alarm_panel,x=6,label="Containment Radiation",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
|
||||
local a_dmg = AlarmLight{parent=alarm_panel,x=6,label="Critical Damage",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
|
||||
alarm_panel.line_break()
|
||||
local a_rcl = AlarmLight{parent=alarm_panel,x=6,label="Reactor Lost",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rcd = AlarmLight{parent=alarm_panel,x=6,label="Reactor Damage",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rot = AlarmLight{parent=alarm_panel,x=6,label="Reactor Over Temp",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rht = AlarmLight{parent=alarm_panel,x=6,label="Reactor High Temp",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS}
|
||||
local a_rwl = AlarmLight{parent=alarm_panel,x=6,label="Reactor Waste Leak",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rwh = AlarmLight{parent=alarm_panel,x=6,label="Reactor Waste High",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS}
|
||||
local a_rcl = AlarmLight{parent=alarm_panel,x=6,label="Reactor Lost",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rcd = AlarmLight{parent=alarm_panel,x=6,label="Reactor Damage",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rot = AlarmLight{parent=alarm_panel,x=6,label="Reactor Over Temp",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rht = AlarmLight{parent=alarm_panel,x=6,label="Reactor High Temp",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_500_MS}
|
||||
local a_rwl = AlarmLight{parent=alarm_panel,x=6,label="Reactor Waste Leak",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rwh = AlarmLight{parent=alarm_panel,x=6,label="Reactor Waste High",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_500_MS}
|
||||
alarm_panel.line_break()
|
||||
local a_rps = AlarmLight{parent=alarm_panel,x=6,label="RPS Transient",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS}
|
||||
local a_clt = AlarmLight{parent=alarm_panel,x=6,label="RCS Transient",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS}
|
||||
local a_tbt = AlarmLight{parent=alarm_panel,x=6,label="Turbine Trip",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
|
||||
local a_rps = AlarmLight{parent=alarm_panel,x=6,label="RPS Transient",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_500_MS}
|
||||
local a_clt = AlarmLight{parent=alarm_panel,x=6,label="RCS Transient",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_500_MS}
|
||||
local a_tbt = AlarmLight{parent=alarm_panel,x=6,label="Turbine Trip",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
|
||||
|
||||
a_brc.register(u_ps, "Alarm_1", a_brc.update)
|
||||
a_rad.register(u_ps, "Alarm_2", a_rad.update)
|
||||
@@ -454,40 +474,38 @@ local function init(parent, id)
|
||||
|
||||
-- color tags
|
||||
|
||||
TextBox{parent=alarm_panel,x=5,y=13,text="\x95",width=1,height=1,fg_bg=cpair(colors.white,colors.cyan)}
|
||||
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,height=1,fg_bg=cpair(colors.white,colors.blue)}
|
||||
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,height=1,fg_bg=cpair(colors.white,colors.blue)}
|
||||
TextBox{parent=alarm_panel,x=5,y=13,text="\x95",width=1,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=TEXT_ALIGN.CENTER,width=13,height=1,x=32,y=36}
|
||||
TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,x=32,y=36}
|
||||
local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37}
|
||||
local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1}
|
||||
|
||||
local ctl_opts = { "Manual", "Primary", "Secondary", "Tertiary", "Backup" }
|
||||
|
||||
local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.blue,colors.white),radio_bg=colors.gray}
|
||||
local group = RadioButton{parent=auto_div,options=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)
|
||||
|
||||
auto_div.line_break()
|
||||
|
||||
local function set_group() unit.set_group(group.get_value() - 1) end
|
||||
local set_grp_btn = PushButton{parent=auto_div,text="SET",x=4,min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),dis_fg_bg=cpair(colors.gray,colors.white),callback=set_group}
|
||||
local set_grp_btn = PushButton{parent=auto_div,text="SET",x=4,min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=style.wh_gray,dis_fg_bg=gry_wht,callback=set_group}
|
||||
|
||||
auto_div.line_break()
|
||||
|
||||
TextBox{parent=auto_div,text="Prio. Group",height=1,width=11,fg_bg=style.label}
|
||||
local auto_grp = TextBox{parent=auto_div,text="Manual",height=1,width=11,fg_bg=bw_fg_bg}
|
||||
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)
|
||||
|
||||
auto_div.line_break()
|
||||
|
||||
local a_rdy = IndicatorLight{parent=auto_div,label="Ready",x=2,colors=cpair(colors.green,colors.gray)}
|
||||
local a_stb = IndicatorLight{parent=auto_div,label="Standby",x=2,colors=cpair(colors.white,colors.gray),flash=true,period=period.BLINK_1000_MS}
|
||||
local a_rdy = IndicatorLight{parent=auto_div,label="Ready",x=2,colors=ind_grn}
|
||||
local a_stb = IndicatorLight{parent=auto_div,label="Standby",x=2,colors=ind_wht,flash=true,period=period.BLINK_1000_MS}
|
||||
|
||||
a_rdy.register(u_ps, "U_AutoReady", a_rdy.update)
|
||||
|
||||
@@ -503,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
|
||||
|
||||
229
coordinator/ui/components/unit_flow.lua
Normal file
229
coordinator/ui/components/unit_flow.lua
Normal file
@@ -0,0 +1,229 @@
|
||||
--
|
||||
-- Basic Unit Flow Overview
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.Div")
|
||||
local PipeNetwork = require("graphics.elements.PipeNetwork")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local Rectangle = require("graphics.elements.Rectangle")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
|
||||
|
||||
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
|
||||
local TriIndicatorLight = require("graphics.elements.indicators.TriIndicatorLight")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local sprintf = util.sprintf
|
||||
|
||||
local border = core.border
|
||||
local cpair = core.cpair
|
||||
local pipe = core.pipe
|
||||
|
||||
local wh_gray = style.wh_gray
|
||||
local lg_gray = style.lg_gray
|
||||
|
||||
-- make a new unit flow window
|
||||
---@param parent 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)
|
||||
local s_field = style.theme.field_box
|
||||
|
||||
local text_c = style.text_colors
|
||||
local lu_c = style.lu_colors
|
||||
local lu_c_d = style.lu_colors_dark
|
||||
|
||||
local ind_grn = style.ind_grn
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local height = 16
|
||||
|
||||
local v_start = 1 + ((unit.unit_id - 1) * 5)
|
||||
local prv_start = 1 + ((unit.unit_id - 1) * 3)
|
||||
local v_fields = { "pu", "po", "pl", "am" }
|
||||
local v_names = {
|
||||
sprintf("PV%02d-PU", v_start),
|
||||
sprintf("PV%02d-PO", v_start + 1),
|
||||
sprintf("PV%02d-PL", v_start + 2),
|
||||
sprintf("PV%02d-AM", v_start + 3),
|
||||
sprintf("PRV%02d", prv_start),
|
||||
sprintf("PRV%02d", prv_start + 1),
|
||||
sprintf("PRV%02d", prv_start + 2)
|
||||
}
|
||||
|
||||
assert(parent.get_height() >= (y + height), "flow display not of sufficient vertical resolution (add an additional row of monitors) " .. y .. "," .. parent.get_height())
|
||||
|
||||
local function _wide(a, b) return util.trinary(wide, a, b) end
|
||||
|
||||
-- bounding box div
|
||||
local root = Div{parent=parent,x=x,y=y,width=_wide(136, 114),height=height}
|
||||
|
||||
------------------
|
||||
-- COOLING LOOP --
|
||||
------------------
|
||||
|
||||
local reactor = Rectangle{parent=root,x=1,y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
|
||||
TextBox{parent=reactor,y=1,text="FISSION REACTOR",alignment=ALIGN.CENTER}
|
||||
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,fg_bg=lg_gray}
|
||||
|
||||
local rc_pipes = {}
|
||||
|
||||
local emc_x = 42 -- emergency coolant connection x point
|
||||
|
||||
if unit.num_boilers > 0 then
|
||||
table.insert(rc_pipes, pipe(0, 1, _wide(28, 19), 1, colors.lightBlue, true))
|
||||
table.insert(rc_pipes, pipe(0, 3, _wide(28, 19), 3, colors.orange, true))
|
||||
table.insert(rc_pipes, pipe(_wide(46 ,39), 1, _wide(72,58), 1, colors.blue, true))
|
||||
table.insert(rc_pipes, pipe(_wide(46,39), 3, _wide(72,58), 3, colors.white, true))
|
||||
else
|
||||
emc_x = 3
|
||||
table.insert(rc_pipes, pipe(0, 1, _wide(72,58), 1, colors.blue, true))
|
||||
table.insert(rc_pipes, pipe(0, 3, _wide(72,58), 3, colors.white, true))
|
||||
end
|
||||
|
||||
if unit.has_tank then
|
||||
table.insert(rc_pipes, pipe(emc_x, 1, emc_x, 0, colors.blue, true, true))
|
||||
end
|
||||
|
||||
local prv_yo = math.max(3 - unit.num_turbines, 0)
|
||||
for i = 1, unit.num_turbines do
|
||||
local py = 2 * (i - 1) + prv_yo
|
||||
table.insert(rc_pipes, pipe(_wide(92, 78), py, _wide(104, 83), py, colors.white, true))
|
||||
end
|
||||
|
||||
PipeNetwork{parent=root,x=20,y=1,pipes=rc_pipes,bg=style.theme.bg}
|
||||
|
||||
if unit.num_boilers > 0 then
|
||||
local cc_rate = DataIndicator{parent=root,x=_wide(25,22),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
|
||||
local hc_rate = DataIndicator{parent=root,x=_wide(25,22),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
|
||||
|
||||
cc_rate.register(unit.unit_ps, "boiler_boil_sum", function (sum) cc_rate.update(sum * 10) end)
|
||||
hc_rate.register(unit.unit_ps, "heating_rate", hc_rate.update)
|
||||
|
||||
local boiler = Rectangle{parent=root,x=_wide(47,40),y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
|
||||
TextBox{parent=boiler,y=1,text="THERMO-ELECTRIC",alignment=ALIGN.CENTER}
|
||||
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}
|
||||
|
||||
local wt_rate = DataIndicator{parent=root,x=_wide(71,61),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
|
||||
local st_rate = DataIndicator{parent=root,x=_wide(71,61),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
|
||||
|
||||
wt_rate.register(unit.unit_ps, "turbine_flow_sum", wt_rate.update)
|
||||
st_rate.register(unit.unit_ps, "boiler_boil_sum", st_rate.update)
|
||||
else
|
||||
local wt_rate = DataIndicator{parent=root,x=28,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
|
||||
local st_rate = DataIndicator{parent=root,x=28,y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
|
||||
|
||||
wt_rate.register(unit.unit_ps, "turbine_flow_sum", wt_rate.update)
|
||||
st_rate.register(unit.unit_ps, "heating_rate", st_rate.update)
|
||||
end
|
||||
|
||||
local turbine = Rectangle{parent=root,x=_wide(93,79),y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
|
||||
TextBox{parent=turbine,y=1,text="STEAM TURBINE",alignment=ALIGN.CENTER}
|
||||
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}
|
||||
local state = TriIndicatorLight{parent=root,x=_wide(129,107),y=ry,label=v_names[i+4],c1=style.ind_bkg,c2=style.ind_yel.fgd,c3=style.ind_red.fgd}
|
||||
state.register(unit.turbine_ps_tbl[i], "SteamDumpOpen", state.update)
|
||||
end
|
||||
|
||||
----------------------
|
||||
-- WASTE PROCESSING --
|
||||
----------------------
|
||||
|
||||
local waste = Div{parent=root,x=3,y=6}
|
||||
|
||||
local waste_c = style.theme.fuel_color
|
||||
|
||||
local waste_pipes = {
|
||||
pipe(0, 0, _wide(19, 16), 1, colors.brown, true),
|
||||
pipe(_wide(14, 13), 1, _wide(19, 17), 5, colors.brown, true),
|
||||
pipe(_wide(22, 19), 1, _wide(49, 45), 1, colors.brown, true),
|
||||
pipe(_wide(22, 19), 5, _wide(28, 24), 5, colors.brown, true),
|
||||
|
||||
pipe(_wide(64, 53), 1, _wide(95, 81), 1, colors.green, true),
|
||||
|
||||
pipe(_wide(48, 43), 4, _wide(71, 61), 4, colors.cyan, true),
|
||||
pipe(_wide(66, 57), 4, _wide(71, 61), 8, colors.cyan, true),
|
||||
pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.cyan, true),
|
||||
pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.cyan, true),
|
||||
|
||||
pipe(_wide(108, 94), 1, _wide(132, 110), 6, waste_c, true, true),
|
||||
pipe(_wide(108, 94), 4, _wide(111, 95), 1, waste_c, true, true),
|
||||
pipe(_wide(132, 110), 6, _wide(130, 108), 6, waste_c, true, true)
|
||||
}
|
||||
|
||||
PipeNetwork{parent=waste,x=1,y=1,pipes=waste_pipes,bg=style.theme.bg}
|
||||
|
||||
local function _valve(vx, vy, n)
|
||||
TextBox{parent=waste,x=vx,y=vy,text="\x10\x11",fg_bg=text_c,width=2}
|
||||
local conn = IndicatorLight{parent=waste,x=vx-3,y=vy+1,label=v_names[n],colors=ind_grn}
|
||||
local open = IndicatorLight{parent=waste,x=vx-3,y=vy+2,label="OPEN",colors=ind_wht}
|
||||
conn.register(unit.unit_ps, util.c("V_", v_fields[n], "_conn"), conn.update)
|
||||
open.register(unit.unit_ps, util.c("V_", v_fields[n], "_state"), open.update)
|
||||
end
|
||||
|
||||
local function _machine(mx, my, name)
|
||||
local l = string.len(name) + 2
|
||||
TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=ALIGN.CENTER,fg_bg=cpair(style.theme.bg,style.theme.header.bkg),width=l}
|
||||
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}
|
||||
local pu_rate = DataIndicator{parent=waste,x=_wide(82,70),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=s_field}
|
||||
local po_rate = DataIndicator{parent=waste,x=_wide(52,45),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
|
||||
local popl_rate = DataIndicator{parent=waste,x=_wide(82,70),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
|
||||
local poam_rate = DataIndicator{parent=waste,x=_wide(82,70),y=10,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
|
||||
local spent_rate = DataIndicator{parent=waste,x=_wide(117,98),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%8.3f",value=0,width=13,fg_bg=s_field}
|
||||
|
||||
waste_rate.register(unit.unit_ps, "act_burn_rate", waste_rate.update)
|
||||
pu_rate.register(unit.unit_ps, "pu_rate", pu_rate.update)
|
||||
po_rate.register(unit.unit_ps, "po_rate", po_rate.update)
|
||||
popl_rate.register(unit.unit_ps, "po_pl_rate", popl_rate.update)
|
||||
poam_rate.register(unit.unit_ps, "po_am_rate", poam_rate.update)
|
||||
spent_rate.register(unit.unit_ps, "ws_rate", spent_rate.update)
|
||||
|
||||
_valve(_wide(21, 18), 2, 1)
|
||||
_valve(_wide(21, 18), 6, 2)
|
||||
_valve(_wide(73, 62), 5, 3)
|
||||
_valve(_wide(73, 62), 9, 4)
|
||||
|
||||
_machine(_wide(51, 45), 1, "CENTRIFUGE \x1a");
|
||||
_machine(_wide(97, 83), 1, "PRC [Pu] \x1a");
|
||||
_machine(_wide(97, 83), 4, "PRC [Po] \x1a");
|
||||
_machine(_wide(116, 94), 6, "SPENT WASTE \x1b")
|
||||
|
||||
TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
|
||||
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=style.theme.highlight_box_bright}
|
||||
local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn}
|
||||
local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c_d,label="CNT",unit="",format="%2d",value=0,width=7}
|
||||
local sna_pk = DataIndicator{parent=sna_po,y=3,lu_colors=lu_c_d,label="PEAK",unit="mB/t",format="%7.2f",value=0,width=17}
|
||||
local sna_max = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="MAX",unit="mB/t",format="%8.2f",value=0,width=17}
|
||||
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="IN",unit="mB/t",format="%9.2f",value=0,width=17}
|
||||
|
||||
sna_act.register(unit.unit_ps, "po_rate", function (r) sna_act.update(r > 0) end)
|
||||
sna_cnt.register(unit.unit_ps, "sna_count", sna_cnt.update)
|
||||
sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.update)
|
||||
sna_max.register(unit.unit_ps, "sna_max_rate", sna_max.update)
|
||||
sna_in.register(unit.unit_ps, "sna_in", sna_in.update)
|
||||
|
||||
return root
|
||||
end
|
||||
|
||||
return make
|
||||
@@ -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 TEXT_ALIGN = core.TEXT_ALIGN
|
||||
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
|
||||
@@ -34,7 +34,7 @@ local function make(parent, x, y, unit)
|
||||
|
||||
if num_boilers == 0 and num_turbines == 1 then
|
||||
height = 9
|
||||
elseif num_boilers == 1 and num_turbines <= 2 then
|
||||
elseif num_boilers <= 1 and num_turbines <= 2 then
|
||||
height = 17
|
||||
end
|
||||
|
||||
@@ -44,7 +44,7 @@ local function make(parent, x, y, unit)
|
||||
local root = Div{parent=parent,x=x,y=y,width=80,height=height}
|
||||
|
||||
-- unit header message
|
||||
TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,fg_bg=style.theme.header}
|
||||
|
||||
-------------
|
||||
-- REACTOR --
|
||||
@@ -66,7 +66,7 @@ local function make(parent, x, y, unit)
|
||||
table.insert(coolant_pipes, pipe(2, 0, 11, 11, colors.orange))
|
||||
end
|
||||
|
||||
PipeNetwork{parent=root,x=4,y=10,pipes=coolant_pipes,bg=colors.lightGray}
|
||||
PipeNetwork{parent=root,x=4,y=10,pipes=coolant_pipes,bg=style.theme.bg}
|
||||
end
|
||||
|
||||
-------------
|
||||
@@ -164,10 +164,10 @@ local function make(parent, x, y, unit)
|
||||
table.insert(steam_pipes_b, pipe(0, 18, 2, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
|
||||
end
|
||||
|
||||
PipeNetwork{parent=root,x=47,y=11,pipes=steam_pipes_a,bg=colors.lightGray}
|
||||
PipeNetwork{parent=root,x=47,y=11,pipes=steam_pipes_a,bg=style.theme.bg}
|
||||
end
|
||||
|
||||
PipeNetwork{parent=root,x=54,y=3,pipes=steam_pipes_b,bg=colors.lightGray}
|
||||
PipeNetwork{parent=root,x=54,y=3,pipes=steam_pipes_b,bg=style.theme.bg}
|
||||
|
||||
return root
|
||||
end
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
local completion = require("cc.completion")
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local print = util.print
|
||||
|
||||
local dialog = {}
|
||||
|
||||
-- ask the user yes or no
|
||||
---@nodiscard
|
||||
---@param question string
|
||||
---@param default boolean
|
||||
---@return boolean|nil
|
||||
function dialog.ask_y_n(question, default)
|
||||
print(question)
|
||||
|
||||
if default == true then
|
||||
print(" (Y/n)? ")
|
||||
else
|
||||
print(" (y/N)? ")
|
||||
end
|
||||
|
||||
local response = read(nil, nil)
|
||||
|
||||
if response == "" then
|
||||
return default
|
||||
elseif response == "Y" or response == "y" then
|
||||
return true
|
||||
elseif response == "N" or response == "n" then
|
||||
return false
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
-- ask the user for an input within a set of options
|
||||
---@nodiscard
|
||||
---@param options table
|
||||
---@param cancel string
|
||||
---@return boolean|string|nil
|
||||
function dialog.ask_options(options, cancel)
|
||||
print("> ")
|
||||
local response = read(nil, nil, function(text) return completion.choice(text, options) end)
|
||||
|
||||
if response == cancel then return false end
|
||||
|
||||
if util.table_contains(options, response) then
|
||||
return response
|
||||
else return nil end
|
||||
end
|
||||
|
||||
return dialog
|
||||
391
coordinator/ui/layout/flow_view.lua
Normal file
391
coordinator/ui/layout/flow_view.lua
Normal file
@@ -0,0 +1,391 @@
|
||||
--
|
||||
-- Flow Monitor GUI
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local unit_flow = require("coordinator.ui.components.unit_flow")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.Div")
|
||||
local PipeNetwork = require("graphics.elements.PipeNetwork")
|
||||
local Rectangle = require("graphics.elements.Rectangle")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
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 ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
local pipe = core.pipe
|
||||
|
||||
local wh_gray = style.wh_gray
|
||||
|
||||
-- create new flow view
|
||||
---@param main DisplayBox main displaybox
|
||||
local function init(main)
|
||||
local s_hi_bright = style.theme.highlight_box_bright
|
||||
local s_field = style.theme.field_box
|
||||
local text_col = style.text_colors
|
||||
local lu_col = style.lu_colors
|
||||
local lu_c_d = style.lu_colors_dark
|
||||
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
|
||||
local tank_defs = facility.tank_defs
|
||||
local tank_list = facility.tank_list
|
||||
|
||||
-- window header message
|
||||
local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=ALIGN.CENTER,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,fg_bg=style.theme.header}
|
||||
|
||||
datetime.register(facility.ps, "date_time", datetime.set_value)
|
||||
|
||||
local po_pipes = {}
|
||||
local water_pipes = {}
|
||||
|
||||
-- get the y offset for this unit index
|
||||
---@param idx integer unit index
|
||||
local function y_ofs(idx) return ((idx - 1) * 20) end
|
||||
|
||||
-- determinte facility tank start/end from the definitions list
|
||||
---@param start_idx integer start index of table iteration
|
||||
---@param end_idx integer end index of table iteration
|
||||
local function find_fdef(start_idx, end_idx)
|
||||
local first, last = 4, 0
|
||||
for i = start_idx, end_idx do
|
||||
if tank_defs[i] == 2 then
|
||||
last = i
|
||||
if i < first then first = i end
|
||||
end
|
||||
end
|
||||
return first, last
|
||||
end
|
||||
|
||||
if facility.tank_mode == 0 or facility.tank_mode == 8 then
|
||||
-- (0) tanks belong to reactor units OR (8) 4 total facility tanks (A B C D)
|
||||
for i = 1, facility.num_units do
|
||||
if units[i].has_tank then
|
||||
local y = y_ofs(i)
|
||||
table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true))
|
||||
table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true))
|
||||
|
||||
local x = util.trinary(units[i].num_boilers == 0, 45, 84)
|
||||
table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true))
|
||||
end
|
||||
end
|
||||
else
|
||||
-- setup connections for units with emergency coolant, always the same
|
||||
for i = 1, #tank_defs do
|
||||
if tank_defs[i] > 0 then
|
||||
local y = y_ofs(i)
|
||||
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(1, y, 21, y, colors.blue, true))
|
||||
else
|
||||
table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true))
|
||||
table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true))
|
||||
end
|
||||
|
||||
local x = util.trinary(units[i].num_boilers == 0, 45, 84)
|
||||
table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true))
|
||||
end
|
||||
end
|
||||
|
||||
if facility.tank_mode == 1 then
|
||||
-- (1) 1 total facility tank (A A A A)
|
||||
local first_fdef, last_fdef = find_fdef(1, #tank_defs)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 2 then
|
||||
-- (2) 2 total facility tanks (A A A B)
|
||||
local first_fdef, last_fdef = find_fdef(1, math.min(3, #tank_defs))
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 4 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 3 then
|
||||
-- (3) 2 total facility tanks (A A B B)
|
||||
for _, a in pairs({ 1, 3 }) do
|
||||
local b = a + 1
|
||||
if tank_defs[a] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y_ofs(a), 1, y_ofs(a) + 6, colors.blue, true))
|
||||
if tank_defs[b] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y_ofs(b) - 13, 1, y_ofs(b), colors.blue, true))
|
||||
end
|
||||
elseif tank_defs[b] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y_ofs(b), 1, y_ofs(b) + 6, colors.blue, true))
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 4 then
|
||||
-- (4) 2 total facility tanks (A B B B)
|
||||
local first_fdef, last_fdef = find_fdef(2, #tank_defs)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 1 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 5 then
|
||||
-- (5) 3 total facility tanks (A A B C)
|
||||
local first_fdef, last_fdef = find_fdef(1, math.min(2, #tank_defs))
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 3 or i == 4 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 6 then
|
||||
-- (6) 3 total facility tanks (A B B C)
|
||||
local first_fdef, last_fdef = find_fdef(2, math.min(3, #tank_defs))
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 1 or i == 4 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 7 then
|
||||
-- (7) 3 total facility tanks (A B C C)
|
||||
local first_fdef, last_fdef = find_fdef(3, #tank_defs)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 1 or i == 2 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local flow_x = 3
|
||||
if #water_pipes > 0 then
|
||||
flow_x = 25
|
||||
PipeNetwork{parent=main,x=2,y=3,pipes=water_pipes,bg=style.theme.bg}
|
||||
end
|
||||
|
||||
for i = 1, facility.num_units do
|
||||
local y_offset = y_ofs(i)
|
||||
unit_flow(main, flow_x, 5 + y_offset, #water_pipes == 0, units[i])
|
||||
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.cyan, true, true))
|
||||
util.nop()
|
||||
end
|
||||
|
||||
PipeNetwork{parent=main,x=139,y=15,pipes=po_pipes,bg=style.theme.bg}
|
||||
|
||||
-----------------
|
||||
-- tank valves --
|
||||
-----------------
|
||||
|
||||
local next_f_id = 1
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
if tank_defs[i] > 0 then
|
||||
local vy = 3 + y_ofs(i)
|
||||
|
||||
TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=text_col,width=2}
|
||||
|
||||
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", i * 5),colors=style.ind_grn}
|
||||
local open = IndicatorLight{parent=main,x=9,y=vy+2,label="OPEN",colors=style.ind_wht}
|
||||
|
||||
conn.register(units[i].unit_ps, "V_emc_conn", conn.update)
|
||||
open.register(units[i].unit_ps, "V_emc_state", open.update)
|
||||
end
|
||||
end
|
||||
|
||||
-------------------
|
||||
-- dynamic tanks --
|
||||
-------------------
|
||||
|
||||
for i = 1, #tank_list do
|
||||
if tank_list[i] > 0 then
|
||||
local id = "U-" .. i
|
||||
local f_id = next_f_id
|
||||
if tank_list[i] == 2 then
|
||||
id = "F-" .. next_f_id
|
||||
next_f_id = next_f_id + 1
|
||||
end
|
||||
|
||||
local y_offset = y_ofs(i)
|
||||
|
||||
local tank = Div{parent=main,x=3,y=7+y_offset,width=20,height=14}
|
||||
|
||||
TextBox{parent=tank,text=" ",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",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",width=11,fg_bg=style.label}
|
||||
local level = HorizontalBar{parent=tank_box,x=2,y=7,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=16}
|
||||
|
||||
TextBox{parent=tank_box,x=2,y=9,text="In/Out Mode",width=11,fg_bg=style.label}
|
||||
local can_fill = IndicatorLight{parent=tank_box,x=2,y=10,label="FILL",colors=style.ind_wht}
|
||||
local can_empty = IndicatorLight{parent=tank_box,x=10,y=10,label="EMPTY",colors=style.ind_wht}
|
||||
|
||||
local function _can_fill(mode)
|
||||
can_fill.update((mode == CONTAINER_MODE.BOTH) or (mode == CONTAINER_MODE.FILL))
|
||||
end
|
||||
|
||||
local function _can_empty(mode)
|
||||
can_empty.update((mode == CONTAINER_MODE.BOTH) or (mode == CONTAINER_MODE.EMPTY))
|
||||
end
|
||||
|
||||
if tank_list[i] == 1 then
|
||||
status.register(units[i].tank_ps_tbl[1], "computed_status", status.update)
|
||||
tank_pcnt.register(units[i].tank_ps_tbl[1], "fill", function (f) tank_pcnt.update(f * 100) end)
|
||||
tank_amnt.register(units[i].tank_ps_tbl[1], "stored", function (sto) tank_amnt.update(sto.amount) end)
|
||||
level.register(units[i].tank_ps_tbl[1], "fill", level.update)
|
||||
can_fill.register(units[i].tank_ps_tbl[1], "container_mode", _can_fill)
|
||||
can_empty.register(units[i].tank_ps_tbl[1], "container_mode", _can_empty)
|
||||
else
|
||||
status.register(facility.tank_ps_tbl[f_id], "computed_status", status.update)
|
||||
tank_pcnt.register(facility.tank_ps_tbl[f_id], "fill", function (f) tank_pcnt.update(f * 100) end)
|
||||
tank_amnt.register(facility.tank_ps_tbl[f_id], "stored", function (sto) tank_amnt.update(sto.amount) end)
|
||||
level.register(facility.tank_ps_tbl[f_id], "fill", level.update)
|
||||
can_fill.register(facility.tank_ps_tbl[f_id], "container_mode", _can_fill)
|
||||
can_empty.register(facility.tank_ps_tbl[f_id], "container_mode", _can_empty)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
util.nop()
|
||||
|
||||
---------
|
||||
-- SPS --
|
||||
---------
|
||||
|
||||
local sps = Div{parent=main,x=140,y=3,height=12}
|
||||
|
||||
TextBox{parent=sps,text=" ",width=24,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}
|
||||
|
||||
local status = StateIndicator{parent=sps_box,x=5,y=1,states=style.sps.states,value=1,min_width=14}
|
||||
|
||||
status.register(facility.sps_ps_tbl[1], "computed_status", status.update)
|
||||
|
||||
TextBox{parent=sps_box,x=2,y=3,text="Input Rate",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",width=15,fg_bg=style.label}
|
||||
local sps_rate = DataIndicator{parent=sps_box,x=2,label="",format="%15d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=20,fg_bg=s_field}
|
||||
|
||||
sps_rate.register(facility.sps_ps_tbl[1], "process_rate", function (r) sps_rate.update(r * 1000) end)
|
||||
|
||||
----------------
|
||||
-- statistics --
|
||||
----------------
|
||||
|
||||
TextBox{parent=main,x=145,y=16,text="RAW WASTE",alignment=ALIGN.CENTER,width=19,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,fg_bg=wh_gray}
|
||||
local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=s_hi_bright}
|
||||
local pu = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17}
|
||||
local po = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="Po",unit="mB/t",format="%9.2f",value=0,width=17}
|
||||
local popl = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="PoPl",unit="mB/t",format="%7.2f",value=0,width=17}
|
||||
|
||||
pu.register(facility.ps, "pu_rate", pu.update)
|
||||
po.register(facility.ps, "po_rate", po.update)
|
||||
popl.register(facility.ps, "po_pl_rate", popl.update)
|
||||
|
||||
TextBox{parent=main,x=145,y=28,text="SPENT WASTE",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
|
||||
local sp_waste = Rectangle{parent=main,x=145,y=29,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=s_hi_bright}
|
||||
local sum_sp_waste = DataIndicator{parent=sp_waste,lu_colors=lu_c_d,label="SUM",unit="mB/t",format="%8.3f",value=0,width=17}
|
||||
|
||||
sum_sp_waste.register(facility.ps, "spent_waste_rate", sum_sp_waste.update)
|
||||
end
|
||||
|
||||
return init
|
||||
@@ -2,39 +2,44 @@
|
||||
-- Coordinator Front Panel GUI
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
local style = require("coordinator.ui.style")
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local pkt_entry = require("coordinator.ui.components.pkt_entry")
|
||||
local pkt_entry = require("coordinator.ui.components.pkt_entry")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local ListBox = require("graphics.elements.listbox")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
local Div = require("graphics.elements.Div")
|
||||
local ListBox = require("graphics.elements.ListBox")
|
||||
local MultiPane = require("graphics.elements.MultiPane")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local TabBar = require("graphics.elements.controls.tabbar")
|
||||
local TabBar = require("graphics.elements.controls.TabBar")
|
||||
|
||||
local LED = require("graphics.elements.indicators.led")
|
||||
local RGBLED = require("graphics.elements.indicators.ledrgb")
|
||||
local LED = require("graphics.elements.indicators.LED")
|
||||
local LEDPair = require("graphics.elements.indicators.LEDPair")
|
||||
local RGBLED = require("graphics.elements.indicators.RGBLED")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local LINK_STATE = types.PANEL_LINK_STATE
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local led_grn = style.led_grn
|
||||
|
||||
-- create new front panel view
|
||||
---@param panel graphics_element main displaybox
|
||||
---@param 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=TEXT_ALIGN.CENTER,height=1,fg_bg=style.fp.header}
|
||||
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}
|
||||
|
||||
@@ -47,36 +52,78 @@ local function init(panel, num_units)
|
||||
local system = Div{parent=main_page,width=14,height=17,x=2,y=2}
|
||||
|
||||
local status = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
|
||||
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)}
|
||||
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=led_grn}
|
||||
status.update(true)
|
||||
system.line_break()
|
||||
|
||||
heartbeat.register(ps, "heartbeat", heartbeat.update)
|
||||
|
||||
local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)}
|
||||
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
|
||||
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
local modem = LED{parent=system,label="MODEM",colors=led_grn}
|
||||
|
||||
if not style.colorblind then
|
||||
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,style.fp_ind_bkg}}
|
||||
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
network.register(ps, "link_state", network.update)
|
||||
else
|
||||
local nt_lnk = LEDPair{parent=system,label="NT LINKED",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
|
||||
local nt_ver = LEDPair{parent=system,label="NT VERSION",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
|
||||
|
||||
nt_lnk.register(ps, "link_state", function (state)
|
||||
local value = 2
|
||||
|
||||
if state == LINK_STATE.DISCONNECTED then
|
||||
value = 1
|
||||
elseif state == LINK_STATE.LINKED then
|
||||
value = 3
|
||||
end
|
||||
|
||||
nt_lnk.update(value)
|
||||
end)
|
||||
|
||||
nt_ver.register(ps, "link_state", function (state)
|
||||
local value = 3
|
||||
|
||||
if state == LINK_STATE.BAD_VERSION then
|
||||
value = 2
|
||||
elseif state == LINK_STATE.DISCONNECTED then
|
||||
value = 1
|
||||
end
|
||||
|
||||
nt_ver.update(value)
|
||||
end)
|
||||
end
|
||||
|
||||
system.line_break()
|
||||
|
||||
modem.register(ps, "has_modem", modem.update)
|
||||
network.register(ps, "link_state", network.update)
|
||||
|
||||
local speaker = LED{parent=system,label="SPEAKER",colors=cpair(colors.green,colors.green_off)}
|
||||
local speaker = LED{parent=system,label="SPEAKER",colors=led_grn}
|
||||
speaker.register(ps, "has_speaker", speaker.update)
|
||||
|
||||
system.line_break()
|
||||
|
||||
local rt_main = LED{parent=system,label="RT MAIN",colors=led_grn}
|
||||
local rt_render = LED{parent=system,label="RT RENDER",colors=led_grn}
|
||||
|
||||
rt_main.register(ps, "routine__main", rt_main.update)
|
||||
rt_render.register(ps, "routine__render", rt_render.update)
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local comp_id = util.sprintf("(%d)", os.getComputerID())
|
||||
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)}
|
||||
TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=style.fp.disabled_fg}
|
||||
|
||||
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
|
||||
|
||||
local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=cpair(colors.green,colors.green_off)}
|
||||
local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=led_grn}
|
||||
main_monitor.register(ps, "main_monitor", main_monitor.update)
|
||||
|
||||
local flow_monitor = LED{parent=monitors,label="FLOW MONITOR",colors=led_grn}
|
||||
flow_monitor.register(ps, "flow_monitor", flow_monitor.update)
|
||||
|
||||
monitors.line_break()
|
||||
|
||||
for i = 1, num_units do
|
||||
local unit_monitor = LED{parent=monitors,label="UNIT "..i.." MONITOR",colors=cpair(colors.green,colors.green_off)}
|
||||
local unit_monitor = LED{parent=monitors,label="UNIT "..i.." MONITOR",colors=led_grn}
|
||||
unit_monitor.register(ps, "unit_monitor_" .. i, unit_monitor.update)
|
||||
end
|
||||
|
||||
@@ -84,9 +131,9 @@ local function init(panel, num_units)
|
||||
-- about footer
|
||||
--
|
||||
|
||||
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=cpair(colors.lightGray,colors.ivory)}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
|
||||
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,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)
|
||||
@@ -98,8 +145,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=cpair(colors.black,colors.ivory),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,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} -- padding
|
||||
|
||||
-- assemble page panes
|
||||
|
||||
@@ -108,11 +155,11 @@ local function init(panel, num_units)
|
||||
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
|
||||
local tabs = {
|
||||
{ name = "CRD", color = cpair(colors.black, colors.ivory) },
|
||||
{ name = "API", color = cpair(colors.black, colors.ivory) },
|
||||
{ name = "CRD", color = style.fp.text },
|
||||
{ name = "API", color = style.fp.text },
|
||||
}
|
||||
|
||||
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=cpair(colors.black,colors.white)}
|
||||
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=style.fp_theme.highlight_box_bright}
|
||||
|
||||
-- link pocket API list management to PGI
|
||||
pgi.link_elements(api_list, pkt_entry)
|
||||
|
||||
@@ -14,30 +14,31 @@ 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 TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
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
|
||||
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
|
||||
-- window header message
|
||||
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=cpair(colors.lightGray, colors.white),width=12,fg_bg=style.header}
|
||||
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=ALIGN.CENTER,fg_bg=s_header}
|
||||
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=style.lg_white,width=12,fg_bg=s_header}
|
||||
-- max length example: "01:23:45 AM - Wednesday, September 28 2022"
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,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
|
||||
@@ -55,6 +56,8 @@ local function init(main)
|
||||
|
||||
cnc_y_start = cnc_y_start + row_1_height + 1
|
||||
|
||||
util.nop()
|
||||
|
||||
if facility.num_units >= 3 then
|
||||
-- base offset 3, spacing 1, max height of units 1 and 2
|
||||
local row_2_offset = cnc_y_start
|
||||
@@ -66,23 +69,25 @@ local function init(main)
|
||||
uo_4 = unit_overview(main, 84, row_2_offset, units[4])
|
||||
cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.get_height() + 1)
|
||||
end
|
||||
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- command & control
|
||||
|
||||
cnc_y_start = cnc_y_start
|
||||
|
||||
-- induction matrix and process control interfaces are 24 tall + space needed for divider
|
||||
local cnc_bottom_align_start = main.get_height() - 26
|
||||
|
||||
assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||
|
||||
TextBox{parent=main,y=cnc_bottom_align_start,text=util.strrep("\x8c", header.get_width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=cpair(colors.lightGray,colors.gray)}
|
||||
TextBox{parent=main,y=cnc_bottom_align_start,text=string.rep("\x8c", header.get_width()),alignment=ALIGN.CENTER,fg_bg=style.lg_gray}
|
||||
|
||||
cnc_bottom_align_start = cnc_bottom_align_start + 2
|
||||
|
||||
process_ctl(main, 2, cnc_bottom_align_start)
|
||||
|
||||
util.nop()
|
||||
|
||||
imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1])
|
||||
end
|
||||
|
||||
|
||||
@@ -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,73 +2,149 @@
|
||||
-- Graphics Style Options
|
||||
--
|
||||
|
||||
local core = require("graphics.core")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
---@class crd_style
|
||||
local style = {}
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
-- GLOBAL --
|
||||
|
||||
-- add color mappings for front panel
|
||||
colors.ivory = colors.pink
|
||||
colors.yellow_hc = colors.purple
|
||||
colors.red_off = colors.brown
|
||||
colors.yellow_off = colors.magenta
|
||||
colors.green_off = colors.lime
|
||||
|
||||
-- front panel styling
|
||||
|
||||
style.fp = {}
|
||||
style.fp_theme = themes.sandstone
|
||||
style.fp = themes.get_fp_style(style.fp_theme)
|
||||
|
||||
style.fp.root = cpair(colors.black, colors.ivory)
|
||||
style.fp.header = cpair(colors.black, colors.lightGray)
|
||||
|
||||
style.fp.colors = {
|
||||
{ c = colors.red, hex = 0xdf4949 }, -- RED ON
|
||||
{ c = colors.orange, hex = 0xffb659 },
|
||||
{ c = colors.yellow, hex = 0xf9fb53 }, -- YELLOW ON
|
||||
{ c = colors.lime, hex = 0x16665a }, -- GREEN OFF
|
||||
{ c = colors.green, hex = 0x6be551 }, -- GREEN ON
|
||||
{ c = colors.cyan, hex = 0x34bac8 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.purple, hex = 0xb156ee }, -- YELLOW HIGH CONTRAST
|
||||
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
|
||||
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
|
||||
-- { c = colors.white, hex = 0xdcd9ca },
|
||||
{ c = colors.lightGray, hex = 0xb1b8b3 },
|
||||
{ c = colors.gray, hex = 0x575757 },
|
||||
-- { c = colors.black, hex = 0x191919 },
|
||||
{ c = colors.brown, hex = 0x672223 } -- RED OFF
|
||||
}
|
||||
style.led_grn = cpair(colors.green, colors.green_off)
|
||||
|
||||
-- main GUI styling
|
||||
|
||||
style.root = cpair(colors.black, colors.lightGray)
|
||||
style.header = cpair(colors.white, colors.gray)
|
||||
style.label = cpair(colors.gray, colors.lightGray)
|
||||
---@class theme
|
||||
local smooth_stone = {
|
||||
text = colors.black,
|
||||
text_inv = colors.white,
|
||||
label = colors.gray,
|
||||
label_dark = colors.gray,
|
||||
disabled = colors.lightGray,
|
||||
bg = colors.lightGray,
|
||||
checkbox_bg = colors.black,
|
||||
accent_light = colors.white,
|
||||
accent_dark = colors.gray,
|
||||
|
||||
style.colors = {
|
||||
{ c = colors.red, hex = 0xdf4949 },
|
||||
{ c = colors.orange, hex = 0xffb659 },
|
||||
{ c = colors.yellow, hex = 0xfffc79 },
|
||||
{ c = colors.lime, hex = 0x80ff80 },
|
||||
{ c = colors.green, hex = 0x4aee8a },
|
||||
{ c = colors.cyan, hex = 0x34bac8 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.purple, hex = 0xb156ee },
|
||||
{ c = colors.pink, hex = 0xf26ba2 },
|
||||
{ c = colors.magenta, hex = 0xf9488a },
|
||||
-- { c = colors.white, hex = 0xf0f0f0 },
|
||||
{ c = colors.lightGray, hex = 0xcacaca },
|
||||
{ c = colors.gray, hex = 0x575757 },
|
||||
-- { c = colors.black, hex = 0x191919 },
|
||||
-- { c = colors.brown, hex = 0x7f664c }
|
||||
fuel_color = colors.black,
|
||||
|
||||
header = cpair(colors.white, colors.gray),
|
||||
|
||||
text_fg = cpair(colors.black, colors._INHERIT),
|
||||
label_fg = cpair(colors.gray, colors._INHERIT),
|
||||
disabled_fg = cpair(colors.lightGray, colors._INHERIT),
|
||||
|
||||
highlight_box = cpair(colors.black, colors.white),
|
||||
highlight_box_bright = cpair(colors.black, colors.white),
|
||||
field_box = cpair(colors.black, colors.white),
|
||||
|
||||
colors = themes.smooth_stone.colors,
|
||||
|
||||
-- color re-mappings for assistive modes
|
||||
color_modes = themes.smooth_stone.color_modes
|
||||
}
|
||||
|
||||
-- MAIN LAYOUT --
|
||||
---@type theme
|
||||
local deepslate = {
|
||||
text = colors.white,
|
||||
text_inv = colors.black,
|
||||
label = colors.lightGray,
|
||||
label_dark = colors.gray,
|
||||
disabled = colors.gray,
|
||||
bg = colors.black,
|
||||
checkbox_bg = colors.gray,
|
||||
accent_light = colors.gray,
|
||||
accent_dark = colors.lightGray,
|
||||
|
||||
fuel_color = colors.lightGray,
|
||||
|
||||
header = cpair(colors.white, colors.gray),
|
||||
|
||||
text_fg = cpair(colors.white, colors._INHERIT),
|
||||
label_fg = cpair(colors.lightGray, colors._INHERIT),
|
||||
disabled_fg = cpair(colors.gray, colors._INHERIT),
|
||||
|
||||
highlight_box = cpair(colors.white, colors.gray),
|
||||
highlight_box_bright = cpair(colors.black, colors.lightGray),
|
||||
field_box = cpair(colors.white, colors.gray),
|
||||
|
||||
colors = themes.deepslate.colors,
|
||||
|
||||
-- color re-mappings for assistive modes
|
||||
color_modes = themes.deepslate.color_modes
|
||||
}
|
||||
|
||||
style.theme = smooth_stone
|
||||
|
||||
-- set themes per configurations
|
||||
---@param main UI_THEME main UI theme
|
||||
---@param fp FP_THEME front panel theme
|
||||
---@param color_mode COLOR_MODE the color mode to use
|
||||
function style.set_themes(main, fp, color_mode)
|
||||
local colorblind = color_mode ~= themes.COLOR_MODE.STANDARD and color_mode ~= themes.COLOR_MODE.STD_ON_BLACK
|
||||
local gray_ind_off = color_mode == themes.COLOR_MODE.STANDARD or color_mode == themes.COLOR_MODE.BLUE_IND
|
||||
|
||||
style.ind_bkg = colors.gray
|
||||
style.fp_ind_bkg = util.trinary(gray_ind_off, colors.gray, colors.black)
|
||||
style.ind_hi_box_bg = util.trinary(gray_ind_off, colors.gray, colors.black)
|
||||
|
||||
if main == themes.UI_THEME.SMOOTH_STONE then
|
||||
style.theme = smooth_stone
|
||||
style.ind_bkg = util.trinary(gray_ind_off, colors.gray, colors.black)
|
||||
elseif main == themes.UI_THEME.DEEPSLATE then
|
||||
style.theme = deepslate
|
||||
style.ind_hi_box_bg = util.trinary(gray_ind_off, colors.lightGray, colors.black)
|
||||
end
|
||||
|
||||
style.colorblind = colorblind
|
||||
|
||||
style.root = cpair(style.theme.text, style.theme.bg)
|
||||
style.label = cpair(style.theme.label, style.theme.bg)
|
||||
|
||||
-- high contrast text (also tags)
|
||||
style.hc_text = cpair(style.theme.text, style.theme.text_inv)
|
||||
-- text on default background
|
||||
style.text_colors = cpair(style.theme.text, style.theme.bg)
|
||||
-- label & unit colors
|
||||
style.lu_colors = cpair(style.theme.label, style.theme.label)
|
||||
-- label & unit colors (darker if set)
|
||||
style.lu_colors_dark = cpair(style.theme.label_dark, style.theme.label_dark)
|
||||
|
||||
style.ind_grn = cpair(util.trinary(colorblind, colors.blue, colors.green), style.ind_bkg)
|
||||
style.ind_yel = cpair(colors.yellow, style.ind_bkg)
|
||||
style.ind_red = cpair(colors.red, style.ind_bkg)
|
||||
style.ind_wht = cpair(colors.white, style.ind_bkg)
|
||||
|
||||
if fp == themes.FP_THEME.SANDSTONE then
|
||||
style.fp_theme = themes.sandstone
|
||||
elseif fp == themes.FP_THEME.BASALT then
|
||||
style.fp_theme = themes.basalt
|
||||
end
|
||||
|
||||
style.fp = themes.get_fp_style(style.fp_theme)
|
||||
end
|
||||
|
||||
-- COMMON COLOR PAIRS --
|
||||
|
||||
style.wh_gray = cpair(colors.white, colors.gray)
|
||||
|
||||
style.bw_fg_bg = cpair(colors.black, colors.white)
|
||||
|
||||
style.hzd_fg_bg = style.wh_gray
|
||||
style.dis_colors = cpair(colors.white, colors.lightGray)
|
||||
|
||||
style.lg_gray = cpair(colors.lightGray, colors.gray)
|
||||
style.lg_white = cpair(colors.lightGray, colors.white)
|
||||
style.gray_white = cpair(colors.gray, colors.white)
|
||||
|
||||
-- UI COMPONENTS --
|
||||
|
||||
style.reactor = {
|
||||
-- reactor states
|
||||
@@ -206,7 +282,7 @@ style.sps = {
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.gray),
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "IDLE"
|
||||
},
|
||||
{
|
||||
@@ -216,6 +292,36 @@ style.sps = {
|
||||
}
|
||||
}
|
||||
|
||||
style.dtank = {
|
||||
-- dynamic tank states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ONLINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "LOW FILL"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "FILLED"
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
style.waste = {
|
||||
-- auto waste processing states
|
||||
states = {
|
||||
|
||||
@@ -7,19 +7,17 @@ local flasher = require("graphics.flasher")
|
||||
|
||||
local core = {}
|
||||
|
||||
core.version = "1.0.2"
|
||||
core.version = "2.4.6"
|
||||
|
||||
core.flasher = flasher
|
||||
core.events = events
|
||||
|
||||
-- Core Types
|
||||
|
||||
---@enum TEXT_ALIGN
|
||||
core.TEXT_ALIGN = {
|
||||
LEFT = 1,
|
||||
CENTER = 2,
|
||||
RIGHT = 3
|
||||
}
|
||||
---@enum ALIGN
|
||||
core.ALIGN = { LEFT = 1, CENTER = 2, RIGHT = 3 }
|
||||
|
||||
---@alias Container DisplayBox|Div|ListBox|MultiPane|AppMultiPane|Rectangle
|
||||
|
||||
---@class graphics_border
|
||||
---@field width integer
|
||||
@@ -35,11 +33,7 @@ core.TEXT_ALIGN = {
|
||||
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
|
||||
---@return graphics_border
|
||||
function core.border(width, color, even)
|
||||
return {
|
||||
width = width,
|
||||
color = color,
|
||||
even = even or false -- convert nil to false
|
||||
}
|
||||
return { width = width, color = color, even = even or false }
|
||||
end
|
||||
|
||||
---@class graphics_frame
|
||||
@@ -56,12 +50,7 @@ end
|
||||
---@param h integer
|
||||
---@return graphics_frame
|
||||
function core.gframe(x, y, w, h)
|
||||
return {
|
||||
x = x,
|
||||
y = y,
|
||||
w = w,
|
||||
h = h
|
||||
}
|
||||
return { x = x, y = y, w = w, h = h }
|
||||
end
|
||||
|
||||
---@class cpair
|
||||
@@ -74,6 +63,9 @@ end
|
||||
---@field blit_fgd string
|
||||
---@field blit_bkg string
|
||||
|
||||
-- add inherited flag, 3 isn't a pure color so it wouldn't be used
|
||||
colors._INHERIT = 3
|
||||
|
||||
-- create a new color pair definition
|
||||
---@nodiscard
|
||||
---@param a color
|
||||
@@ -82,15 +74,9 @@ end
|
||||
function core.cpair(a, b)
|
||||
return {
|
||||
-- color pairs
|
||||
color_a = a,
|
||||
color_b = b,
|
||||
blit_a = colors.toBlit(a),
|
||||
blit_b = colors.toBlit(b),
|
||||
color_a = a, color_b = b, blit_a = colors.toBlit(a), blit_b = colors.toBlit(b),
|
||||
-- aliases
|
||||
fgd = a,
|
||||
bkg = b,
|
||||
blit_fgd = colors.toBlit(a),
|
||||
blit_bkg = colors.toBlit(b)
|
||||
fgd = a, bkg = b, blit_fgd = colors.toBlit(a), blit_bkg = colors.toBlit(b)
|
||||
}
|
||||
end
|
||||
|
||||
@@ -130,4 +116,234 @@ function core.pipe(x1, y1, x2, y2, color, thin, align_tr)
|
||||
}
|
||||
end
|
||||
|
||||
-- Assertion Handling
|
||||
|
||||
-- extract the custom element assert message, dropping the path to the element file
|
||||
function core.extract_assert_msg(msg)
|
||||
return string.sub(msg, (string.find(msg, "@") or 0) + 1)
|
||||
end
|
||||
|
||||
-- Interactive Field Manager
|
||||
|
||||
---@param e graphics_base 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
|
||||
}
|
||||
|
||||
-- update visible text
|
||||
local function _update_visible()
|
||||
self.visible_text = string.sub(e.value, self.frame_start, self.frame_start + math.min(string.len(e.value), e.frame.w) - 1)
|
||||
end
|
||||
|
||||
-- try shifting frame left
|
||||
local function _try_lshift()
|
||||
if self.frame_start > 1 then
|
||||
self.frame_start = self.frame_start - 1
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- try shifting frame right
|
||||
local function _try_rshift()
|
||||
if (self.frame_start + e.frame.w - 1) <= string.len(e.value) then
|
||||
self.frame_start = self.frame_start + 1
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
---@class ifield
|
||||
local public = {}
|
||||
|
||||
-- censor the display (for private info, for example) with the provided character<br>
|
||||
-- disable by passing no argument
|
||||
---@param censor string? character to hide data with
|
||||
function public.censor(censor)
|
||||
if type(censor) == "string" and string.len(censor) == 1 then
|
||||
self.censor = censor
|
||||
else self.censor = nil end
|
||||
public.show()
|
||||
end
|
||||
|
||||
-- show the field
|
||||
function public.show()
|
||||
_update_visible()
|
||||
|
||||
if e.enabled then
|
||||
e.w_set_bkg(fg_bg.bkg)
|
||||
e.w_set_fgd(fg_bg.fgd)
|
||||
elseif dis_fg_bg ~= nil then
|
||||
e.w_set_bkg(dis_fg_bg.bkg)
|
||||
e.w_set_fgd(dis_fg_bg.fgd)
|
||||
end
|
||||
|
||||
-- clear and print
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(string.rep(" ", e.frame.w))
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
local function _write(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
|
||||
e.w_write(self.visible_text)
|
||||
end
|
||||
end
|
||||
|
||||
if e.is_focused() and e.enabled then
|
||||
-- write text with cursor
|
||||
if self.selected_all then
|
||||
e.w_set_bkg(fg_bg.fgd)
|
||||
e.w_set_fgd(fg_bg.bkg)
|
||||
_write()
|
||||
elseif self.cursor_pos >= (string.len(self.visible_text) + 1) then
|
||||
-- write text with cursor at the end, no need to blit
|
||||
_write()
|
||||
e.w_set_fgd(colors.lightGray)
|
||||
e.w_write("_")
|
||||
else
|
||||
local a, b = "", ""
|
||||
|
||||
if self.cursor_pos <= string.len(self.visible_text) then
|
||||
a = fg_bg.blit_bkg
|
||||
b = fg_bg.blit_fgd
|
||||
end
|
||||
|
||||
local b_fgd = string.rep(fg_bg.blit_fgd, self.cursor_pos - 1) .. a .. string.rep(fg_bg.blit_fgd, string.len(self.visible_text) - self.cursor_pos)
|
||||
local b_bkg = string.rep(fg_bg.blit_bkg, self.cursor_pos - 1) .. b .. string.rep(fg_bg.blit_bkg, string.len(self.visible_text) - self.cursor_pos)
|
||||
|
||||
if self.censor then
|
||||
e.w_blit(string.rep(self.censor, string.len(self.visible_text)), b_fgd, b_bkg)
|
||||
else
|
||||
e.w_blit(self.visible_text, b_fgd, b_bkg)
|
||||
end
|
||||
end
|
||||
else
|
||||
self.selected_all = false
|
||||
|
||||
-- write text without cursor
|
||||
_write(align_right)
|
||||
end
|
||||
end
|
||||
|
||||
-- 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
|
||||
|
||||
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
|
||||
|
||||
-- select all text
|
||||
function public.select_all()
|
||||
self.selected_all = true
|
||||
public.show()
|
||||
end
|
||||
|
||||
-- set field value
|
||||
---@param val string
|
||||
function public.set_value(val)
|
||||
e.value = string.sub(val, 1, math.min(max_len, string.len(val)))
|
||||
public.nav_end()
|
||||
end
|
||||
|
||||
-- try to insert a character if there is space
|
||||
---@param char string
|
||||
function public.try_insert_char(char)
|
||||
-- limit length
|
||||
if string.len(e.value) >= max_len then return end
|
||||
|
||||
-- replace if selected all, insert otherwise
|
||||
if self.selected_all then
|
||||
self.selected_all = false
|
||||
self.cursor_pos = 2
|
||||
self.frame_start = 1
|
||||
|
||||
e.value = char
|
||||
public.show()
|
||||
else
|
||||
e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 2) .. char .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value))
|
||||
_update_visible()
|
||||
public.nav_right()
|
||||
end
|
||||
end
|
||||
|
||||
-- remove charcter before cursor if there is anything to remove, or delete all if selected all
|
||||
function public.backspace()
|
||||
if self.selected_all then
|
||||
self.selected_all = false
|
||||
e.value = ""
|
||||
self.cursor_pos = 1
|
||||
self.frame_start = 1
|
||||
public.show()
|
||||
else
|
||||
if self.frame_start + self.cursor_pos > 2 then
|
||||
e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 3) .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value))
|
||||
if self.cursor_pos > 1 then
|
||||
self.cursor_pos = self.cursor_pos - 1
|
||||
public.show()
|
||||
elseif _try_lshift() then public.show() end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- move cursor left by one
|
||||
function public.nav_left()
|
||||
if self.cursor_pos > 1 then
|
||||
self.cursor_pos = self.cursor_pos - 1
|
||||
public.show()
|
||||
elseif _try_lshift() then public.show() end
|
||||
end
|
||||
|
||||
-- move cursor right by one
|
||||
function public.nav_right()
|
||||
if self.cursor_pos < math.min(string.len(self.visible_text) + 1, e.frame.w) then
|
||||
self.cursor_pos = self.cursor_pos + 1
|
||||
public.show()
|
||||
elseif _try_rshift() then public.show() end
|
||||
end
|
||||
|
||||
-- move cursor to the start
|
||||
function public.nav_start()
|
||||
self.cursor_pos = 1
|
||||
self.frame_start = 1
|
||||
public.show()
|
||||
end
|
||||
|
||||
-- move cursor to the end
|
||||
function public.nav_end()
|
||||
self.frame_start = math.max(1, string.len(e.value) - e.frame.w + 2)
|
||||
_update_visible()
|
||||
self.cursor_pos = string.len(self.visible_text) + 1
|
||||
public.show()
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return core
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
107
graphics/elements/AppMultiPane.lua
Normal file
107
graphics/elements/AppMultiPane.lua
Normal file
@@ -0,0 +1,107 @@
|
||||
-- App Page Multi-Pane Display Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
local events = require("graphics.events")
|
||||
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class app_multipane_args
|
||||
---@field panes table panes to swap between
|
||||
---@field nav_colors cpair on/off colors (a/b respectively) for page navigator
|
||||
---@field scroll_nav boolean? true to allow scrolling to change the active pane
|
||||
---@field drag_nav boolean? true to allow mouse dragging to change the active pane (on mouse up)
|
||||
---@field callback function? function to call when pane is changed by mouse interaction
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- Create a new app multipane container element.
|
||||
---@nodiscard
|
||||
---@param args app_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 --[[@as graphics_args]])
|
||||
|
||||
e.value = 1
|
||||
|
||||
local nav_x_start = math.floor((e.frame.w / 2) - (#args.panes / 2)) + 1
|
||||
local nav_x_end = math.floor((e.frame.w / 2) - (#args.panes / 2)) + #args.panes
|
||||
|
||||
-- show the selected pane
|
||||
function e.redraw()
|
||||
for i = 1, #args.panes do args.panes[i].hide() end
|
||||
args.panes[e.value].show()
|
||||
|
||||
-- draw page indicator dots
|
||||
for i = 1, #args.panes do
|
||||
e.w_set_cur(nav_x_start + (i - 1), e.frame.h)
|
||||
e.w_set_fgd(util.trinary(i == e.value, args.nav_colors.color_a, args.nav_colors.color_b))
|
||||
e.w_write("\x07")
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
local initial = e.value
|
||||
|
||||
if e.enabled then
|
||||
if event.current.y == e.frame.h and event.current.x >= nav_x_start and event.current.x <= nav_x_end then
|
||||
local id = event.current.x - nav_x_start + 1
|
||||
|
||||
if event.type == MOUSE_CLICK.TAP then
|
||||
e.set_value(id)
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
e.set_value(id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if args.scroll_nav then
|
||||
if event.type == events.MOUSE_CLICK.SCROLL_DOWN then
|
||||
e.set_value(e.value + 1)
|
||||
elseif event.type == events.MOUSE_CLICK.SCROLL_UP then
|
||||
e.set_value(e.value - 1)
|
||||
end
|
||||
end
|
||||
|
||||
if args.drag_nav then
|
||||
local x1, x2 = event.initial.x, event.current.x
|
||||
if event.type == events.MOUSE_CLICK.UP and e.in_frame_bounds(x1, event.initial.y) and e.in_frame_bounds(x1, event.current.y) then
|
||||
if x2 > x1 then
|
||||
e.set_value(e.value - 1)
|
||||
elseif x2 < x1 then
|
||||
e.set_value(e.value + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if e.value ~= initial and type(args.callback) == "function" then args.callback(e.value) end
|
||||
end
|
||||
|
||||
-- select which pane is shown
|
||||
---@param value integer pane to show
|
||||
function e.set_value(value)
|
||||
if (e.value ~= value) and (value > 0) and (value <= #args.panes) then
|
||||
e.value = value
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
---@class AppMultiPane:graphics_element
|
||||
local AppMultiPane, id = e.complete(true)
|
||||
|
||||
return AppMultiPane, id
|
||||
end
|
||||
@@ -1,7 +1,5 @@
|
||||
-- Color Map Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class colormap_args
|
||||
@@ -11,24 +9,27 @@ 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 = util.spaces(32)
|
||||
local spaces = string.rep(" ", 32)
|
||||
|
||||
args.width = 32
|
||||
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
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(spaces, bkg, bkg)
|
||||
function e.redraw()
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(spaces, bkg, bkg)
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
---@class ColorMap:graphics_element
|
||||
local ColorMap, id = e.complete(true)
|
||||
|
||||
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
|
||||
@@ -1,11 +1,13 @@
|
||||
-- Scroll-able List Box Display Graphics Element
|
||||
|
||||
-- local log = require("scada-common.log")
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class listbox_args
|
||||
---@field scroll_height integer height of internal scrolling container (must fit all elements vertically tiled)
|
||||
@@ -28,13 +30,15 @@ local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
---@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)
|
||||
@@ -63,34 +67,34 @@ local function listbox(args)
|
||||
|
||||
-- draw up/down arrows
|
||||
if pressed_arrow == 1 then
|
||||
e.window.setTextColor(active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(active_fg_bg.bkg)
|
||||
e.window.setCursorPos(e.frame.w, 1)
|
||||
e.window.write("\x1e")
|
||||
e.window.setTextColor(nav_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(nav_fg_bg.bkg)
|
||||
e.window.setCursorPos(e.frame.w, e.frame.h)
|
||||
e.window.write("\x1f")
|
||||
e.w_set_fgd(active_fg_bg.fgd)
|
||||
e.w_set_bkg(active_fg_bg.bkg)
|
||||
e.w_set_cur(e.frame.w, 1)
|
||||
e.w_write("\x1e")
|
||||
e.w_set_fgd(nav_fg_bg.fgd)
|
||||
e.w_set_bkg(nav_fg_bg.bkg)
|
||||
e.w_set_cur(e.frame.w, e.frame.h)
|
||||
e.w_write("\x1f")
|
||||
elseif pressed_arrow == -1 then
|
||||
e.window.setTextColor(nav_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(nav_fg_bg.bkg)
|
||||
e.window.setCursorPos(e.frame.w, 1)
|
||||
e.window.write("\x1e")
|
||||
e.window.setTextColor(active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(active_fg_bg.bkg)
|
||||
e.window.setCursorPos(e.frame.w, e.frame.h)
|
||||
e.window.write("\x1f")
|
||||
e.w_set_fgd(nav_fg_bg.fgd)
|
||||
e.w_set_bkg(nav_fg_bg.bkg)
|
||||
e.w_set_cur(e.frame.w, 1)
|
||||
e.w_write("\x1e")
|
||||
e.w_set_fgd(active_fg_bg.fgd)
|
||||
e.w_set_bkg(active_fg_bg.bkg)
|
||||
e.w_set_cur(e.frame.w, e.frame.h)
|
||||
e.w_write("\x1f")
|
||||
else
|
||||
e.window.setTextColor(nav_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(nav_fg_bg.bkg)
|
||||
e.window.setCursorPos(e.frame.w, 1)
|
||||
e.window.write("\x1e")
|
||||
e.window.setCursorPos(e.frame.w, e.frame.h)
|
||||
e.window.write("\x1f")
|
||||
e.w_set_fgd(nav_fg_bg.fgd)
|
||||
e.w_set_bkg(nav_fg_bg.bkg)
|
||||
e.w_set_cur(e.frame.w, 1)
|
||||
e.w_write("\x1e")
|
||||
e.w_set_cur(e.frame.w, e.frame.h)
|
||||
e.w_write("\x1f")
|
||||
end
|
||||
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- render the scroll bar and re-cacluate height & bounds
|
||||
@@ -115,23 +119,23 @@ local function listbox(args)
|
||||
for i = 2, e.frame.h - 1 do
|
||||
if (i >= offset and i < (bar_height + offset)) and (bar_height ~= max_bar_height) then
|
||||
if args.nav_fg_bg ~= nil then
|
||||
e.window.setBackgroundColor(args.nav_fg_bg.fgd)
|
||||
e.w_set_bkg(args.nav_fg_bg.fgd)
|
||||
else
|
||||
e.window.setBackgroundColor(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.fgd)
|
||||
end
|
||||
else
|
||||
if args.nav_fg_bg ~= nil then
|
||||
e.window.setBackgroundColor(args.nav_fg_bg.bkg)
|
||||
e.w_set_bkg(args.nav_fg_bg.bkg)
|
||||
else
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
end
|
||||
|
||||
e.window.setCursorPos(e.frame.w, i)
|
||||
e.window.write(" ")
|
||||
e.w_set_cur(e.frame.w, i)
|
||||
if e.is_focused() then e.w_write("\x7f") else e.w_write(" ") end
|
||||
end
|
||||
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- update item y positions and move elements
|
||||
@@ -149,6 +153,7 @@ local function listbox(args)
|
||||
next_y = next_y + item.h + item_pad
|
||||
item.e.reposition(1, item.y)
|
||||
item.e.show()
|
||||
-- log.debug("iterated " .. item.e.get_id())
|
||||
end
|
||||
|
||||
content_height = next_y
|
||||
@@ -158,6 +163,9 @@ local function listbox(args)
|
||||
scroll_frame.reposition(1, 1 + scroll_offset)
|
||||
scroll_frame.setVisible(true)
|
||||
|
||||
-- shift mouse events
|
||||
e.mouse_window_shift.y = scroll_offset
|
||||
|
||||
draw_bar()
|
||||
end
|
||||
|
||||
@@ -204,6 +212,7 @@ local function listbox(args)
|
||||
---@param child graphics_element child element
|
||||
function e.on_added(id, child)
|
||||
table.insert(list, { id = id, e = child, y = 0, h = child.get_height() })
|
||||
-- log.debug("added child " .. id .. " into slot " .. #list)
|
||||
update_positions()
|
||||
end
|
||||
|
||||
@@ -213,36 +222,68 @@ local function listbox(args)
|
||||
for idx, elem in ipairs(list) do
|
||||
if elem.id == id then
|
||||
table.remove(list, idx)
|
||||
-- log.debug("removed child " .. id .. " from slot " .. idx)
|
||||
update_positions()
|
||||
return
|
||||
end
|
||||
end
|
||||
-- log.debug("failed to remove child " .. id)
|
||||
end
|
||||
|
||||
-- handle focus
|
||||
e.on_focused = draw_bar
|
||||
e.on_unfocused = draw_bar
|
||||
|
||||
-- handle a child in the list being focused, make sure it is visible
|
||||
function e.on_child_focused(child)
|
||||
for i = 1, #list do
|
||||
local item = list[i] ---@type listbox_item
|
||||
if item.e == child then
|
||||
if (item.y + scroll_offset) <= 0 then
|
||||
scroll_offset = 1 - item.y
|
||||
update_positions()
|
||||
draw_bar()
|
||||
elseif (item.y + scroll_offset) == 1 then
|
||||
-- do nothing, it's right at the top (if the bottom doesn't fit we can't easily fix that)
|
||||
elseif ((item.h + item.y - 1) + scroll_offset) > e.frame.h then
|
||||
scroll_offset = 1 - ((item.h + item.y) - e.frame.h)
|
||||
update_positions()
|
||||
draw_bar()
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled then
|
||||
if event.type == CLICK_TYPE.TAP then
|
||||
if event.type == MOUSE_CLICK.TAP then
|
||||
if event.current.x == e.frame.w then
|
||||
if event.current.y == 1 or event.current.y < bar_bounds[1] then
|
||||
draw_arrows(1)
|
||||
scroll_up()
|
||||
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
|
||||
if event.current.y == 1 then
|
||||
draw_arrows(1)
|
||||
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
|
||||
end
|
||||
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
|
||||
draw_arrows(-1)
|
||||
scroll_down()
|
||||
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
|
||||
if event.current.y == e.frame.h then
|
||||
draw_arrows(-1)
|
||||
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif event.type == CLICK_TYPE.DOWN then
|
||||
elseif event.type == MOUSE_CLICK.DOWN then
|
||||
if event.current.x == e.frame.w then
|
||||
if event.current.y == 1 or event.current.y < bar_bounds[1] then
|
||||
draw_arrows(1)
|
||||
scroll_up()
|
||||
if event.current.y == 1 then draw_arrows(1) end
|
||||
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
|
||||
draw_arrows(-1)
|
||||
scroll_down()
|
||||
if event.current.y == e.frame.h then draw_arrows(-1) end
|
||||
else
|
||||
-- clicked on bar
|
||||
holding_bar = true
|
||||
@@ -250,10 +291,10 @@ local function listbox(args)
|
||||
mouse_last_y = event.current.y
|
||||
end
|
||||
end
|
||||
elseif event.type == CLICK_TYPE.UP then
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
holding_bar = false
|
||||
draw_arrows(0)
|
||||
elseif event.type == CLICK_TYPE.DRAG then
|
||||
elseif event.type == MOUSE_CLICK.DRAG then
|
||||
if holding_bar then
|
||||
-- if mouse is within vertical frame, including the grip point
|
||||
if event.current.y > (1 + bar_grip_pos) and event.current.y <= ((e.frame.h - bar_height) + bar_grip_pos) then
|
||||
@@ -266,18 +307,40 @@ local function listbox(args)
|
||||
mouse_last_y = event.current.y
|
||||
end
|
||||
end
|
||||
elseif event.type == CLICK_TYPE.SCROLL_DOWN then
|
||||
elseif event.type == MOUSE_CLICK.SCROLL_DOWN then
|
||||
scroll_down()
|
||||
elseif event.type == CLICK_TYPE.SCROLL_UP then
|
||||
elseif event.type == MOUSE_CLICK.SCROLL_UP then
|
||||
scroll_up()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
draw_arrows(0)
|
||||
draw_bar()
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
|
||||
if event.key == keys.up then
|
||||
scroll_up()
|
||||
elseif event.key == keys.down then
|
||||
scroll_down()
|
||||
elseif event.key == keys.home then
|
||||
scroll_offset = 0
|
||||
update_positions()
|
||||
elseif event.key == keys["end"] then
|
||||
scroll_offset = max_down_scroll
|
||||
update_positions()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
draw_arrows(0)
|
||||
draw_bar()
|
||||
end
|
||||
|
||||
---@class ListBox:graphics_element
|
||||
local ListBox, id = e.complete(true)
|
||||
|
||||
return ListBox, id
|
||||
end
|
||||
|
||||
return listbox
|
||||
@@ -14,30 +14,35 @@ 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)
|
||||
assert(type(args.panes) == "table", "graphics.elements.multipane: panes is a required field")
|
||||
---@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
|
||||
|
||||
-- show the selected pane
|
||||
function e.redraw()
|
||||
for i = 1, #args.panes do args.panes[i].hide() end
|
||||
args.panes[e.value].show()
|
||||
end
|
||||
|
||||
-- select which pane is shown
|
||||
---@param value integer pane to show
|
||||
function e.set_value(value)
|
||||
if (e.value ~= value) and (value > 0) and (value <= #args.panes) then
|
||||
e.value = value
|
||||
|
||||
for i = 1, #args.panes do args.panes[i].hide() end
|
||||
args.panes[value].show()
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
e.set_value(1)
|
||||
---@class MultiPane:graphics_element
|
||||
local MultiPane, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return MultiPane, id
|
||||
end
|
||||
|
||||
return multipane
|
||||
329
graphics/elements/PipeNetwork.lua
Normal file
329
graphics/elements/PipeNetwork.lua
Normal file
@@ -0,0 +1,329 @@
|
||||
-- Pipe Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class pipenet_args
|
||||
---@field pipes table pipe list
|
||||
---@field bg? color background color
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
---@class _pipe_map_entry
|
||||
---@field atr boolean align top right (or bottom left for false)
|
||||
---@field thin boolean thin pipe or not
|
||||
---@field fg string foreground blit
|
||||
---@field bg string background blit
|
||||
|
||||
-- Create a pipe network diagram.
|
||||
---@param args 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
|
||||
args.height = 0
|
||||
|
||||
for i = 1, #args.pipes do
|
||||
local pipe = args.pipes[i] ---@type pipe
|
||||
|
||||
local true_w = pipe.w + math.min(pipe.x1, pipe.x2)
|
||||
local true_h = pipe.h + math.min(pipe.y1, pipe.y2)
|
||||
|
||||
if true_w > args.width then args.width = true_w end
|
||||
if true_h > args.height then args.height = true_h end
|
||||
end
|
||||
|
||||
args.x = args.x or 1
|
||||
args.y = args.y or 1
|
||||
|
||||
if args.bg ~= nil then
|
||||
args.fg_bg = core.cpair(args.bg, args.bg)
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- determine if there are any thin pipes involved
|
||||
local any_thin = false
|
||||
for p = 1, #args.pipes do
|
||||
any_thin = args.pipes[p].thin
|
||||
if any_thin then break end
|
||||
end
|
||||
|
||||
-- draw all pipes by drawing out lines
|
||||
local function vector_draw()
|
||||
for p = 1, #args.pipes do
|
||||
local pipe = args.pipes[p] ---@type pipe
|
||||
|
||||
local x = 1 + pipe.x1
|
||||
local y = 1 + pipe.y1
|
||||
|
||||
local x_step = util.trinary(pipe.x1 >= pipe.x2, -1, 1)
|
||||
local y_step = util.trinary(pipe.y1 >= pipe.y2, -1, 1)
|
||||
|
||||
if pipe.thin then
|
||||
x_step = util.trinary(pipe.x1 == pipe.x2, 0, x_step)
|
||||
y_step = util.trinary(pipe.y1 == pipe.y2, 0, y_step)
|
||||
end
|
||||
|
||||
e.w_set_cur(x, y)
|
||||
|
||||
local c = core.cpair(pipe.color, e.fg_bg.bkg)
|
||||
|
||||
if pipe.align_tr then
|
||||
-- cross width then height
|
||||
for i = 1, pipe.w do
|
||||
if pipe.thin then
|
||||
if i == pipe.w then
|
||||
-- corner
|
||||
if y_step > 0 then
|
||||
e.w_blit("\x93", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.w_blit("\x8e", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
if i == pipe.w and y_step > 0 then
|
||||
-- corner
|
||||
e.w_blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.w_blit("\x8f", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
x = x + x_step
|
||||
e.w_set_cur(x, y)
|
||||
end
|
||||
|
||||
-- back up one
|
||||
x = x - x_step
|
||||
|
||||
for _ = 1, pipe.h - 1 do
|
||||
y = y + y_step
|
||||
e.w_set_cur(x, y)
|
||||
|
||||
if pipe.thin then
|
||||
e.w_blit("\x95", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.w_blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- cross height then width
|
||||
for i = 1, pipe.h do
|
||||
if pipe.thin then
|
||||
if i == pipe.h then
|
||||
-- corner
|
||||
if y_step < 0 then
|
||||
e.w_blit("\x97", c.blit_bkg, c.blit_fgd)
|
||||
elseif y_step > 0 then
|
||||
e.w_blit("\x8d", c.blit_fgd, c.blit_bkg)
|
||||
else
|
||||
e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.w_blit("\x95", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
if i == pipe.h and y_step < 0 then
|
||||
-- corner
|
||||
e.w_blit("\x83", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.w_blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
end
|
||||
|
||||
y = y + y_step
|
||||
e.w_set_cur(x, y)
|
||||
end
|
||||
|
||||
-- back up one
|
||||
y = y - y_step
|
||||
|
||||
for _ = 1, pipe.w - 1 do
|
||||
x = x + x_step
|
||||
e.w_set_cur(x, y)
|
||||
|
||||
if pipe.thin then
|
||||
e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
else
|
||||
e.w_blit("\x83", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- draw a particular map cell
|
||||
---@param map table 2D cell map
|
||||
---@param x integer x coord
|
||||
---@param y integer y coord
|
||||
local function draw_map_cell(map, x, y)
|
||||
local entry = map[x][y] ---@type _pipe_map_entry already confirmed not false
|
||||
local char
|
||||
local invert = false
|
||||
|
||||
local function check(cx, cy)
|
||||
return (map[cx] ~= nil) and (map[cx][cy] ~= nil) and (map[cx][cy] ~= false) and (map[cx][cy].fg == entry.fg)
|
||||
end
|
||||
|
||||
if entry.thin then
|
||||
if check(x - 1, y) then -- if left
|
||||
if check(x, y - 1) then -- if above
|
||||
if check(x + 1, y) then -- if right
|
||||
if check(x, y + 1) then -- if below
|
||||
char = util.trinary(entry.atr, "\x91", "\x9d")
|
||||
invert = entry.atr
|
||||
else -- not below
|
||||
char = util.trinary(entry.atr, "\x8e", "\x8d")
|
||||
end
|
||||
else -- not right
|
||||
if check(x, y + 1) then -- if below
|
||||
char = util.trinary(entry.atr, "\x91", "\x95")
|
||||
invert = entry.atr
|
||||
else -- not below
|
||||
char = util.trinary(entry.atr, "\x8e", "\x85")
|
||||
end
|
||||
end
|
||||
elseif check(x, y + 1) then-- not above, if below
|
||||
if check(x + 1, y) then -- if right
|
||||
char = util.trinary(entry.atr, "\x93", "\x9c")
|
||||
invert = entry.atr
|
||||
else -- not right
|
||||
char = util.trinary(entry.atr, "\x93", "\x94")
|
||||
invert = entry.atr
|
||||
end
|
||||
else -- not above, not below
|
||||
char = "\x8c"
|
||||
end
|
||||
elseif check(x + 1, y) then -- not left, if right
|
||||
if check(x, y - 1) then -- if above
|
||||
if check(x, y + 1) then -- if below
|
||||
char = util.trinary(entry.atr, "\x95", "\x9d")
|
||||
invert = entry.atr
|
||||
else -- not below
|
||||
char = util.trinary(entry.atr, "\x8a", "\x8d")
|
||||
end
|
||||
else -- not above
|
||||
if check(x, y + 1) then -- if below
|
||||
char = util.trinary(entry.atr, "\x97", "\x9c")
|
||||
invert = entry.atr
|
||||
else -- not below
|
||||
char = "\x8c"
|
||||
end
|
||||
end
|
||||
else -- not left, not right
|
||||
char = "\x95"
|
||||
invert = entry.atr
|
||||
end
|
||||
else
|
||||
if check(x, y - 1) then -- above
|
||||
-- not below and (if left or right)
|
||||
if (not check(x, y + 1)) and (check(x - 1, y) or check(x + 1, y)) then
|
||||
char = util.trinary(entry.atr, "\x8f", " ")
|
||||
invert = not entry.atr
|
||||
else -- not below w/ sides only
|
||||
char = " "
|
||||
invert = true
|
||||
end
|
||||
elseif check(x, y + 1) then -- not above, if below
|
||||
-- if left or right
|
||||
if (check(x - 1, y) or check(x + 1, y)) then
|
||||
char = "\x83"
|
||||
invert = true
|
||||
else -- not left or right
|
||||
char = " "
|
||||
invert = true
|
||||
end
|
||||
else -- not above, not below
|
||||
char = util.trinary(entry.atr, "\x8f", "\x83")
|
||||
invert = not entry.atr
|
||||
end
|
||||
end
|
||||
|
||||
e.w_set_cur(x, y)
|
||||
|
||||
if invert then
|
||||
e.w_blit(char, entry.bg, entry.fg)
|
||||
else
|
||||
e.w_blit(char, entry.fg, entry.bg)
|
||||
end
|
||||
end
|
||||
|
||||
-- draw all pipes by assembling and marking up a 2D map<br>
|
||||
-- this is an easy way to check adjacent blocks, which is required to properly draw thin pipes
|
||||
local function map_draw()
|
||||
local map = {}
|
||||
|
||||
for x = 1, args.width do
|
||||
table.insert(map, {})
|
||||
for _ = 1, args.height do table.insert(map[x], false) end
|
||||
end
|
||||
|
||||
-- build map
|
||||
for p = 1, #args.pipes do
|
||||
local pipe = args.pipes[p] ---@type pipe
|
||||
|
||||
local x = 1 + pipe.x1
|
||||
local y = 1 + pipe.y1
|
||||
|
||||
local x_step = util.trinary(pipe.x1 >= pipe.x2, -1, 1)
|
||||
local y_step = util.trinary(pipe.y1 >= pipe.y2, -1, 1)
|
||||
|
||||
local entry = { atr = pipe.align_tr, thin = pipe.thin, fg = colors.toBlit(pipe.color), bg = e.fg_bg.blit_bkg }
|
||||
|
||||
if pipe.align_tr then
|
||||
-- cross width then height
|
||||
for _ = 1, pipe.w do
|
||||
map[x][y] = entry
|
||||
x = x + x_step
|
||||
end
|
||||
|
||||
x = x - x_step -- back up one
|
||||
|
||||
for _ = 1, pipe.h do
|
||||
map[x][y] = entry
|
||||
y = y + y_step
|
||||
end
|
||||
else
|
||||
-- cross height then width
|
||||
for _ = 1, pipe.h do
|
||||
map[x][y] = entry
|
||||
y = y + y_step
|
||||
end
|
||||
|
||||
y = y - y_step -- back up one
|
||||
|
||||
for _ = 1, pipe.w do
|
||||
map[x][y] = entry
|
||||
x = x + x_step
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- render
|
||||
for x = 1, args.width do
|
||||
for y = 1, args.height do
|
||||
if map[x][y] ~= false then draw_map_cell(map, x, y) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
if any_thin then map_draw() else vector_draw() end
|
||||
end
|
||||
|
||||
---@class PipeNetwork:graphics_element
|
||||
local PipeNetwork, id = e.complete(true)
|
||||
|
||||
return PipeNetwork, id
|
||||
end
|
||||
198
graphics/elements/Rectangle.lua
Normal file
198
graphics/elements/Rectangle.lua
Normal file
@@ -0,0 +1,198 @@
|
||||
-- Rectangle Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class rectangle_args
|
||||
---@field border? graphics_border
|
||||
---@field thin? boolean true to use extra thin even borders
|
||||
---@field even_inner? boolean true to make the inner area of a border even
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- Create a new rectangle container element.
|
||||
---@param args 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
|
||||
if args.thin == true then
|
||||
args.border.width = 1
|
||||
args.border.even = true
|
||||
end
|
||||
|
||||
-- offset children
|
||||
local offset_x = 0
|
||||
local offset_y = 0
|
||||
if args.border ~= nil then
|
||||
offset_x = args.border.width
|
||||
offset_y = args.border.width
|
||||
|
||||
-- slightly different y offset if the border is set to even
|
||||
if args.border.even then
|
||||
local width_x2 = (2 * args.border.width)
|
||||
offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args --[[@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))
|
||||
e.content_window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.content_window.setTextColor(e.fg_bg.fgd)
|
||||
e.content_window.clear()
|
||||
|
||||
-- draw bordered box if requested
|
||||
-- element constructor will have drawn basic colored rectangle regardless
|
||||
if args.border ~= nil then
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
local border_width = offset_x
|
||||
local border_height = offset_y
|
||||
local border_blit = colors.toBlit(args.border.color)
|
||||
local width_x2 = border_width * 2
|
||||
local inner_width = e.frame.w - width_x2
|
||||
|
||||
-- check dimensions
|
||||
element.assert(width_x2 <= e.frame.w, "border too thick for width")
|
||||
element.assert(width_x2 <= e.frame.h, "border too thick for height")
|
||||
|
||||
-- form the basic line strings and top/bottom blit strings
|
||||
local spaces = util.spaces(e.frame.w)
|
||||
local blit_fg = string.rep(e.fg_bg.blit_fgd, e.frame.w)
|
||||
local blit_fg_sides = blit_fg
|
||||
local blit_bg_sides = ""
|
||||
local blit_bg_top_bot = string.rep(border_blit, e.frame.w)
|
||||
|
||||
-- partial bars
|
||||
local p_a, p_b, p_s
|
||||
if args.thin == true then
|
||||
if args.even_inner == true then
|
||||
p_a = "\x9c" .. string.rep("\x8c", inner_width) .. "\x93"
|
||||
p_b = "\x8d" .. string.rep("\x8c", inner_width) .. "\x8e"
|
||||
else
|
||||
p_a = "\x97" .. string.rep("\x83", inner_width) .. "\x94"
|
||||
p_b = "\x8a" .. string.rep("\x8f", inner_width) .. "\x85"
|
||||
end
|
||||
|
||||
p_s = "\x95" .. util.spaces(inner_width) .. "\x95"
|
||||
else
|
||||
if args.even_inner == true then
|
||||
p_a = string.rep("\x83", inner_width + width_x2)
|
||||
p_b = string.rep("\x8f", inner_width + width_x2)
|
||||
else
|
||||
p_a = util.spaces(border_width) .. string.rep("\x8f", inner_width) .. util.spaces(border_width)
|
||||
p_b = util.spaces(border_width) .. string.rep("\x83", inner_width) .. util.spaces(border_width)
|
||||
end
|
||||
|
||||
p_s = spaces
|
||||
end
|
||||
|
||||
local p_inv_fg = string.rep(border_blit, border_width) .. string.rep(e.fg_bg.blit_bkg, inner_width) ..
|
||||
string.rep(border_blit, border_width)
|
||||
local p_inv_bg = string.rep(e.fg_bg.blit_bkg, border_width) .. string.rep(border_blit, inner_width) ..
|
||||
string.rep(e.fg_bg.blit_bkg, border_width)
|
||||
|
||||
if args.thin == true then
|
||||
p_inv_fg = e.fg_bg.blit_bkg .. string.rep(e.fg_bg.blit_bkg, inner_width) .. string.rep(border_blit, border_width)
|
||||
p_inv_bg = border_blit .. string.rep(border_blit, inner_width) .. string.rep(e.fg_bg.blit_bkg, border_width)
|
||||
|
||||
blit_fg_sides = border_blit .. string.rep(e.fg_bg.blit_bkg, inner_width) .. e.fg_bg.blit_bkg
|
||||
end
|
||||
|
||||
-- form the body blit strings (sides are border, inside is normal)
|
||||
for x = 1, e.frame.w do
|
||||
-- edges get border color, center gets normal
|
||||
if x <= border_width or x > (e.frame.w - border_width) then
|
||||
if args.thin and x == 1 then
|
||||
blit_bg_sides = blit_bg_sides .. e.fg_bg.blit_bkg
|
||||
else
|
||||
blit_bg_sides = blit_bg_sides .. border_blit
|
||||
end
|
||||
else
|
||||
blit_bg_sides = blit_bg_sides .. e.fg_bg.blit_bkg
|
||||
end
|
||||
end
|
||||
|
||||
-- draw rectangle with borders
|
||||
function e.redraw()
|
||||
for y = 1, e.frame.h do
|
||||
e.w_set_cur(1, y)
|
||||
-- top border
|
||||
if y <= border_height then
|
||||
-- partial pixel fill
|
||||
if args.border.even and y == border_height then
|
||||
if args.thin == true then
|
||||
e.w_blit(p_a, p_inv_bg, p_inv_fg)
|
||||
else
|
||||
local _fg = util.trinary(args.even_inner == true, string.rep(e.fg_bg.blit_bkg, e.frame.w), p_inv_bg)
|
||||
local _bg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
|
||||
|
||||
if width_x2 % 3 == 1 then
|
||||
e.w_blit(p_b, _fg, _bg)
|
||||
elseif width_x2 % 3 == 2 then
|
||||
e.w_blit(p_a, _fg, _bg)
|
||||
else
|
||||
-- skip line
|
||||
e.w_blit(spaces, blit_fg, blit_bg_sides)
|
||||
end
|
||||
end
|
||||
else
|
||||
e.w_blit(spaces, blit_fg, blit_bg_top_bot)
|
||||
end
|
||||
-- bottom border
|
||||
elseif y > (e.frame.h - border_width) then
|
||||
-- partial pixel fill
|
||||
if args.border.even and y == ((e.frame.h - border_width) + 1) then
|
||||
if args.thin == true then
|
||||
if args.even_inner == true then
|
||||
e.w_blit(p_b, blit_bg_top_bot, string.rep(e.fg_bg.blit_bkg, e.frame.w))
|
||||
else
|
||||
e.w_blit(p_b, string.rep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
|
||||
end
|
||||
else
|
||||
local _fg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
|
||||
local _bg = util.trinary(args.even_inner == true, string.rep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
|
||||
|
||||
if width_x2 % 3 == 1 then
|
||||
e.w_blit(p_a, _fg, _bg)
|
||||
elseif width_x2 % 3 == 2 then
|
||||
e.w_blit(p_b, _fg, _bg)
|
||||
else
|
||||
-- skip line
|
||||
e.w_blit(spaces, blit_fg, blit_bg_sides)
|
||||
end
|
||||
end
|
||||
else
|
||||
e.w_blit(spaces, blit_fg, blit_bg_top_bot)
|
||||
end
|
||||
else
|
||||
if args.thin == true then
|
||||
e.w_blit(p_s, blit_fg_sides, blit_bg_sides)
|
||||
else
|
||||
e.w_blit(p_s, blit_fg, blit_bg_sides)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw of border
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
---@class Rectangle:graphics_element
|
||||
local Rectangle, id = e.complete()
|
||||
|
||||
return Rectangle, id
|
||||
end
|
||||
93
graphics/elements/TextBox.lua
Normal file
93
graphics/elements/TextBox.lua
Normal file
@@ -0,0 +1,93 @@
|
||||
-- Text Box Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
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 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
|
||||
|
||||
-- Create a new text box element.
|
||||
---@param args 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 --[[@as graphics_args]], constrain)
|
||||
|
||||
e.value = args.text
|
||||
|
||||
local alignment = args.alignment or ALIGN.LEFT
|
||||
|
||||
-- draw textbox
|
||||
function e.redraw()
|
||||
e.window.clear()
|
||||
|
||||
local lines = util.strwrap(e.value, e.frame.w)
|
||||
|
||||
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
|
||||
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(lines[i])
|
||||
end
|
||||
end
|
||||
|
||||
-- set the string value and re-draw the text
|
||||
---@param val string value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
---@class TextBox:graphics_element
|
||||
local TextBox, id = e.complete(true)
|
||||
|
||||
return TextBox, id
|
||||
end
|
||||
93
graphics/elements/Tiling.lua
Normal file
93
graphics/elements/Tiling.lua
Normal file
@@ -0,0 +1,93 @@
|
||||
-- "Basketweave" Tiling Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class tiling_args
|
||||
---@field fill_c cpair colors to fill with
|
||||
---@field even? boolean whether to account for rectangular pixels
|
||||
---@field border_c? color optional frame color
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- Create a new tiling box element.
|
||||
---@param args 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 --[[@as graphics_args]])
|
||||
|
||||
local fill_a = args.fill_c.blit_a
|
||||
local fill_b = args.fill_c.blit_b
|
||||
|
||||
local even = args.even == true
|
||||
|
||||
local start_x = 1
|
||||
local start_y = 1
|
||||
local inner_width = math.floor(e.frame.w / util.trinary(even, 2, 1))
|
||||
local inner_height = e.frame.h
|
||||
|
||||
-- border
|
||||
if args.border_c ~= nil then
|
||||
start_x = 1 + util.trinary(even, 2, 1)
|
||||
start_y = 2
|
||||
|
||||
inner_width = math.floor((e.frame.w - 2 * util.trinary(even, 2, 1)) / util.trinary(even, 2, 1))
|
||||
inner_height = e.frame.h - 2
|
||||
end
|
||||
|
||||
-- check dimensions
|
||||
element.assert(inner_width > 0, "inner_width <= 0")
|
||||
element.assert(inner_height > 0, "inner_height <= 0")
|
||||
element.assert(start_x <= inner_width, "start_x > inner_width")
|
||||
element.assert(start_y <= inner_height, "start_y > inner_height")
|
||||
|
||||
-- draw the tiling box
|
||||
function e.redraw()
|
||||
local alternator = true
|
||||
|
||||
if args.border_c ~= nil then
|
||||
e.w_set_bkg(args.border_c)
|
||||
e.window.clear()
|
||||
end
|
||||
|
||||
-- draw pattern
|
||||
for y = start_y, inner_height + (start_y - 1) do
|
||||
e.w_set_cur(start_x, y)
|
||||
for _ = 1, inner_width do
|
||||
if alternator then
|
||||
if even then
|
||||
e.w_blit(" ", "00", fill_a .. fill_a)
|
||||
else
|
||||
e.w_blit(" ", "0", fill_a)
|
||||
end
|
||||
else
|
||||
if even then
|
||||
e.w_blit(" ", "00", fill_b .. fill_b)
|
||||
else
|
||||
e.w_blit(" ", "0", fill_b)
|
||||
end
|
||||
end
|
||||
|
||||
alternator = not alternator
|
||||
end
|
||||
|
||||
if inner_width % 2 == 0 then alternator = not alternator end
|
||||
end
|
||||
end
|
||||
|
||||
---@class Tiling:graphics_element
|
||||
local Tiling, id = e.complete(true)
|
||||
|
||||
return Tiling, id
|
||||
end
|
||||
@@ -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
|
||||
@@ -36,49 +36,49 @@ local function waiting(args)
|
||||
|
||||
if state >= 0 and state < 7 then
|
||||
-- top
|
||||
e.window.setCursorPos(1 + math.floor(state / 2), 1)
|
||||
e.w_set_cur(1 + math.floor(state / 2), 1)
|
||||
if state % 2 == 0 then
|
||||
e.window.blit("\x8f", blit_fg, blit_bg)
|
||||
e.w_blit("\x8f", blit_fg, blit_bg)
|
||||
else
|
||||
e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
|
||||
e.w_blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
|
||||
end
|
||||
|
||||
-- bottom
|
||||
e.window.setCursorPos(4 - math.ceil(state / 2), 3)
|
||||
e.w_set_cur(4 - math.ceil(state / 2), 3)
|
||||
if state % 2 == 0 then
|
||||
e.window.blit("\x8f", blit_fg, blit_bg)
|
||||
e.w_blit("\x8f", blit_fg, blit_bg)
|
||||
else
|
||||
e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
|
||||
e.w_blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
|
||||
end
|
||||
else
|
||||
local st = state - 7
|
||||
|
||||
-- right
|
||||
if st % 3 == 0 then
|
||||
e.window.setCursorPos(4, 1 + math.floor(st / 3))
|
||||
e.window.blit("\x83", blit_bg, blit_fg)
|
||||
e.w_set_cur(4, 1 + math.floor(st / 3))
|
||||
e.w_blit("\x83", blit_bg, blit_fg)
|
||||
elseif st % 3 == 1 then
|
||||
e.window.setCursorPos(4, 1 + math.floor(st / 3))
|
||||
e.window.blit("\x8f", blit_bg, blit_fg)
|
||||
e.window.setCursorPos(4, 2 + math.floor(st / 3))
|
||||
e.window.blit("\x83", blit_fg, blit_bg)
|
||||
e.w_set_cur(4, 1 + math.floor(st / 3))
|
||||
e.w_blit("\x8f", blit_bg, blit_fg)
|
||||
e.w_set_cur(4, 2 + math.floor(st / 3))
|
||||
e.w_blit("\x83", blit_fg, blit_bg)
|
||||
else
|
||||
e.window.setCursorPos(4, 2 + math.floor(st / 3))
|
||||
e.window.blit("\x8f", blit_fg, blit_bg)
|
||||
e.w_set_cur(4, 2 + math.floor(st / 3))
|
||||
e.w_blit("\x8f", blit_fg, blit_bg)
|
||||
end
|
||||
|
||||
-- left
|
||||
if st % 3 == 0 then
|
||||
e.window.setCursorPos(1, 3 - math.floor(st / 3))
|
||||
e.window.blit("\x83", blit_fg, blit_bg)
|
||||
e.window.setCursorPos(1, 2 - math.floor(st / 3))
|
||||
e.window.blit("\x8f", blit_bg, blit_fg)
|
||||
e.w_set_cur(1, 3 - math.floor(st / 3))
|
||||
e.w_blit("\x83", blit_fg, blit_bg)
|
||||
e.w_set_cur(1, 2 - math.floor(st / 3))
|
||||
e.w_blit("\x8f", blit_bg, blit_fg)
|
||||
elseif st % 3 == 1 then
|
||||
e.window.setCursorPos(1, 2 - math.floor(st / 3))
|
||||
e.window.blit("\x83", blit_bg, blit_fg)
|
||||
e.w_set_cur(1, 2 - math.floor(st / 3))
|
||||
e.w_blit("\x83", blit_bg, blit_fg)
|
||||
else
|
||||
e.window.setCursorPos(1, 2 - math.floor(st / 3))
|
||||
e.window.blit("\x8f", blit_fg, blit_bg)
|
||||
e.w_set_cur(1, 2 - math.floor(st / 3))
|
||||
e.w_blit("\x8f", blit_fg, blit_bg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
130
graphics/elements/controls/App.lua
Normal file
130
graphics/elements/controls/App.lua
Normal file
@@ -0,0 +1,130 @@
|
||||
-- App Button Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class app_button_args
|
||||
---@field text string app icon text
|
||||
---@field title string app title text
|
||||
---@field callback function function to call on touch
|
||||
---@field app_fg_bg cpair app icon foreground/background colors
|
||||
---@field active_fg_bg? cpair foreground/background colors when pressed
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- Create a new app icon style button control element, like on a mobile device.
|
||||
---@param args 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")
|
||||
element.assert(type(args.app_fg_bg) == "table", "app_fg_bg is a required field")
|
||||
|
||||
args.height = 4
|
||||
args.width = 7
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- draw the app button
|
||||
local function draw()
|
||||
local fgd = args.app_fg_bg.fgd
|
||||
local bkg = args.app_fg_bg.bkg
|
||||
|
||||
if e.value then
|
||||
fgd = args.active_fg_bg.fgd
|
||||
bkg = args.active_fg_bg.bkg
|
||||
end
|
||||
|
||||
-- draw icon
|
||||
e.w_set_cur(2, 1)
|
||||
e.w_set_fgd(fgd)
|
||||
e.w_set_bkg(bkg)
|
||||
e.w_write("\x9f\x83\x83\x83")
|
||||
e.w_set_fgd(bkg)
|
||||
e.w_set_bkg(fgd)
|
||||
e.w_write("\x90")
|
||||
e.w_set_fgd(fgd)
|
||||
e.w_set_bkg(bkg)
|
||||
e.w_set_cur(2, 2)
|
||||
e.w_write("\x95 ")
|
||||
e.w_set_fgd(bkg)
|
||||
e.w_set_bkg(fgd)
|
||||
e.w_write("\x95")
|
||||
e.w_set_cur(2, 3)
|
||||
e.w_write("\x82\x8f\x8f\x8f\x81")
|
||||
|
||||
-- write the icon text
|
||||
e.w_set_cur(4, 2)
|
||||
e.w_set_fgd(fgd)
|
||||
e.w_set_bkg(bkg)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
-- draw the app button as pressed (if active_fg_bg set)
|
||||
local function show_pressed()
|
||||
if e.enabled and args.active_fg_bg ~= nil then
|
||||
e.value = true
|
||||
e.w_set_fgd(args.active_fg_bg.fgd)
|
||||
e.w_set_bkg(args.active_fg_bg.bkg)
|
||||
draw()
|
||||
end
|
||||
end
|
||||
|
||||
-- draw the app button as unpressed (if active_fg_bg set)
|
||||
local function show_unpressed()
|
||||
if e.enabled and args.active_fg_bg ~= nil then
|
||||
e.value = false
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
draw()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled then
|
||||
if event.type == MOUSE_CLICK.TAP then
|
||||
show_pressed()
|
||||
-- show as unpressed in 0.25 seconds
|
||||
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
|
||||
args.callback()
|
||||
elseif event.type == MOUSE_CLICK.DOWN then
|
||||
show_pressed()
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
show_unpressed()
|
||||
if e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
args.callback()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value (true simulates pressing the app button)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
|
||||
end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
e.w_set_cur(math.floor((e.frame.w - string.len(args.title)) / 2) + 1, 4)
|
||||
e.w_write(args.title)
|
||||
draw()
|
||||
end
|
||||
|
||||
---@class App:graphics_element
|
||||
local App, id = e.complete(true)
|
||||
|
||||
return App, id
|
||||
end
|
||||
129
graphics/elements/controls/Checkbox.lua
Normal file
129
graphics/elements/controls/Checkbox.lua
Normal file
@@ -0,0 +1,129 @@
|
||||
-- Checkbox Graphics Element
|
||||
|
||||
local core = require("graphics.core")
|
||||
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
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- Create a new checkbox control element.
|
||||
---@param args 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")
|
||||
|
||||
args.can_focus = true
|
||||
args.height = 1
|
||||
args.width = 2 + string.len(args.label)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = args.default == true
|
||||
|
||||
-- show the button state
|
||||
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(bkg)
|
||||
e.w_set_bkg(fgd)
|
||||
e.w_write("\x88")
|
||||
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(bkg)
|
||||
e.w_write("\x88")
|
||||
e.w_set_fgd(bkg)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write("\x95")
|
||||
end
|
||||
end
|
||||
|
||||
-- write label text
|
||||
local function draw_label()
|
||||
if e.enabled and e.is_focused() then
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.fgd)
|
||||
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_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
e.value = not e.value
|
||||
draw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == core.events.KEY_CLICK.DOWN then
|
||||
if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then
|
||||
e.value = not e.value
|
||||
draw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw()
|
||||
end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
draw()
|
||||
draw_label()
|
||||
end
|
||||
|
||||
-- handle focus
|
||||
e.on_focused = draw_label
|
||||
e.on_unfocused = draw_label
|
||||
|
||||
-- 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
|
||||
205
graphics/elements/controls/HazardButton.lua
Normal file
205
graphics/elements/controls/HazardButton.lua
Normal file
@@ -0,0 +1,205 @@
|
||||
-- Hazard-bordered Button Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class hazard_button_args
|
||||
---@field text string text to show on button
|
||||
---@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
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- Create a new hazard button control element.
|
||||
---@param args 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")
|
||||
|
||||
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 --[[@as graphics_args]])
|
||||
|
||||
-- draw border
|
||||
---@param accent color accent color
|
||||
local function draw_border(accent)
|
||||
-- top
|
||||
e.w_set_fgd(accent)
|
||||
e.w_set_bkg(args.fg_bg.bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write("\x99" .. string.rep("\x89", args.width - 2) .. "\x99")
|
||||
|
||||
-- center left
|
||||
e.w_set_cur(1, 2)
|
||||
e.w_set_fgd(args.fg_bg.bkg)
|
||||
e.w_set_bkg(accent)
|
||||
e.w_write("\x99")
|
||||
|
||||
-- center right
|
||||
e.w_set_fgd(args.fg_bg.bkg)
|
||||
e.w_set_bkg(accent)
|
||||
e.w_set_cur(args.width, 2)
|
||||
e.w_write("\x99")
|
||||
|
||||
-- bottom
|
||||
e.w_set_fgd(accent)
|
||||
e.w_set_bkg(args.fg_bg.bkg)
|
||||
e.w_set_cur(1, 3)
|
||||
e.w_write("\x99" .. string.rep("\x98", args.width - 2) .. "\x99")
|
||||
end
|
||||
|
||||
-- on request timeout: recursively calls itself to double flash button text
|
||||
---@param n integer call count
|
||||
local function on_timeout(n)
|
||||
-- start at 0
|
||||
if n == nil then n = 0 end
|
||||
|
||||
if n == 0 then
|
||||
-- go back off
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
if n >= 4 then
|
||||
-- done
|
||||
elseif n % 2 == 0 then
|
||||
-- toggle text color on after 0.25 seconds
|
||||
tcd.dispatch(0.25, function ()
|
||||
e.w_set_fgd(args.accent)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
on_timeout(n + 1)
|
||||
on_timeout(n + 1)
|
||||
end)
|
||||
elseif n % 1 then
|
||||
-- toggle text color off after 0.25 seconds
|
||||
tcd.dispatch(0.25, function ()
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
on_timeout(n + 1)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- blink routine for success indication
|
||||
local function on_success()
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
-- blink routine for failure indication
|
||||
---@param n integer call count
|
||||
local function on_failure(n)
|
||||
-- start at 0
|
||||
if n == nil then n = 0 end
|
||||
|
||||
if n == 0 then
|
||||
-- go back off
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
if n >= 2 then
|
||||
-- done
|
||||
elseif n % 2 == 0 then
|
||||
-- toggle text color on after 0.5 seconds
|
||||
tcd.dispatch(0.5, function ()
|
||||
e.w_set_fgd(args.accent)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
on_failure(n + 1)
|
||||
end)
|
||||
elseif n % 1 then
|
||||
-- toggle text color off after 0.25 seconds
|
||||
tcd.dispatch(0.25, function ()
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
on_failure(n + 1)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
-- change text color to indicate clicked
|
||||
e.w_set_fgd(args.accent)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
|
||||
-- abort any other callbacks
|
||||
tcd.abort(on_timeout)
|
||||
tcd.abort(on_success)
|
||||
tcd.abort(on_failure)
|
||||
|
||||
-- operation timeout animation
|
||||
tcd.dispatch(timeout, on_timeout)
|
||||
|
||||
args.callback()
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value (true simulates pressing the button)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
|
||||
end
|
||||
|
||||
-- show the button as disabled
|
||||
function e.on_disabled()
|
||||
if args.dis_colors then
|
||||
draw_border(args.dis_colors.color_a)
|
||||
e.w_set_fgd(args.dis_colors.color_b)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
end
|
||||
|
||||
-- show the button as enabled
|
||||
function e.on_enabled()
|
||||
draw_border(args.accent)
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
-- write the button text and draw border
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
draw_border(args.accent)
|
||||
end
|
||||
|
||||
---@class HazardButton:graphics_element
|
||||
local HazardButton, id = e.complete(true)
|
||||
|
||||
-- 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
|
||||
@@ -25,17 +25,15 @@ 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)
|
||||
assert(type(args.options) == "table", "graphics.elements.controls.multi_button: options is a required field")
|
||||
assert(#args.options > 0, "graphics.elements.controls.multi_button: at least one option is required")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.multi_button: callback is a required field")
|
||||
assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0),
|
||||
"graphics.elements.controls.multi_button: default must be nil or a number > 0")
|
||||
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||
"graphics.elements.controls.multi_button: min_width must be nil or a number > 0")
|
||||
---@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")
|
||||
element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
|
||||
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
@@ -54,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
|
||||
@@ -71,23 +69,23 @@ local function multi_button(args)
|
||||
end
|
||||
|
||||
-- show the button state
|
||||
local function draw()
|
||||
function e.redraw()
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type button_option
|
||||
|
||||
e.window.setCursorPos(opt._start_x, 1)
|
||||
e.w_set_cur(opt._start_x, 1)
|
||||
|
||||
if e.value == i then
|
||||
-- show as pressed
|
||||
e.window.setTextColor(opt.active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(opt.active_fg_bg.bkg)
|
||||
e.w_set_fgd(opt.active_fg_bg.fgd)
|
||||
e.w_set_bkg(opt.active_fg_bg.bkg)
|
||||
else
|
||||
-- show as unpressed
|
||||
e.window.setTextColor(opt.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(opt.fg_bg.bkg)
|
||||
e.w_set_fgd(opt.fg_bg.fgd)
|
||||
e.w_set_bkg(opt.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.window.write(util.pad(opt.text, button_width))
|
||||
e.w_write(util.pad(opt.text, button_width))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -115,7 +113,7 @@ local function multi_button(args)
|
||||
-- tap always has identical coordinates, so this always passes for taps
|
||||
if button_ini == button_cur and button_cur ~= nil then
|
||||
e.value = button_cur
|
||||
draw()
|
||||
e.redraw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
@@ -125,13 +123,11 @@ local function multi_button(args)
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw()
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
---@class MultiButton:graphics_element
|
||||
local MultiButton, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return MultiButton, id
|
||||
end
|
||||
|
||||
return multi_button
|
||||
@@ -20,17 +20,17 @@ 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
|
||||
local fr_prec = args.fractional_precision
|
||||
|
||||
assert(util.is_int(wn_prec), "graphics.element.controls.spinbox_numeric: whole number precision must be an integer")
|
||||
assert(util.is_int(fr_prec), "graphics.element.controls.spinbox_numeric: fractional precision must be an integer")
|
||||
element.assert(util.is_int(wn_prec), "whole number precision must be an integer")
|
||||
element.assert(util.is_int(fr_prec), "fractional precision must be an integer")
|
||||
|
||||
local fmt, fmt_init ---@type string, string
|
||||
|
||||
@@ -44,36 +44,34 @@ local function spinbox(args)
|
||||
|
||||
local dec_point_x = args.whole_num_precision + 1
|
||||
|
||||
assert(type(args.arrow_fg_bg) == "table", "graphics.element.spinbox_numeric: arrow_fg_bg is a required field")
|
||||
element.assert(type(args.arrow_fg_bg) == "table", "arrow_fg_bg is a required field")
|
||||
|
||||
-- determine widths
|
||||
args.width = wn_prec + fr_prec + util.trinary(fr_prec > 0, 1, 0)
|
||||
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
|
||||
|
||||
-- draw the arrows
|
||||
local function draw_arrows(color)
|
||||
e.window.setBackgroundColor(args.arrow_fg_bg.bkg)
|
||||
e.window.setTextColor(color)
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write(util.strrep("\x1e", wn_prec))
|
||||
e.window.setCursorPos(1, 3)
|
||||
e.window.write(util.strrep("\x1f", wn_prec))
|
||||
e.w_set_bkg(args.arrow_fg_bg.bkg)
|
||||
e.w_set_fgd(color)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(string.rep("\x1e", wn_prec))
|
||||
e.w_set_cur(1, 3)
|
||||
e.w_write(string.rep("\x1f", wn_prec))
|
||||
if fr_prec > 0 then
|
||||
e.window.setCursorPos(1 + wn_prec, 1)
|
||||
e.window.write(" " .. util.strrep("\x1e", fr_prec))
|
||||
e.window.setCursorPos(1 + wn_prec, 3)
|
||||
e.window.write(" " .. util.strrep("\x1f", fr_prec))
|
||||
e.w_set_cur(1 + wn_prec, 1)
|
||||
e.w_write(" " .. string.rep("\x1e", fr_prec))
|
||||
e.w_set_cur(1 + wn_prec, 3)
|
||||
e.w_write(" " .. string.rep("\x1f", fr_prec))
|
||||
end
|
||||
end
|
||||
|
||||
draw_arrows(args.arrow_fg_bg.fgd)
|
||||
|
||||
-- populate digits from current value
|
||||
local function set_digits()
|
||||
local initial_str = util.sprintf(fmt_init, e.value)
|
||||
@@ -119,35 +117,29 @@ local function spinbox(args)
|
||||
end
|
||||
|
||||
-- draw
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setCursorPos(1, 2)
|
||||
e.window.write(util.sprintf(fmt, e.value))
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_cur(1, 2)
|
||||
e.w_write(util.sprintf(fmt, e.value))
|
||||
end
|
||||
|
||||
-- init with the default value
|
||||
show_num()
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- only handle if on an increment or decrement arrow
|
||||
if e.enabled and core.events.was_clicked(event.type) and
|
||||
(event.current.x ~= dec_point_x) and (event.current.y ~= 2) then
|
||||
if event.current.x == event.initial.x and event.current.y == event.initial.y then
|
||||
local idx = util.trinary(event.current.x > dec_point_x, event.current.x - 1, event.current.x)
|
||||
if digits[idx] ~= nil then
|
||||
if event.current.y == 1 then
|
||||
-- increment
|
||||
digits[idx] = digits[idx] + 1
|
||||
elseif event.current.y == 3 then
|
||||
-- decrement
|
||||
digits[idx] = digits[idx] - 1
|
||||
end
|
||||
|
||||
update_value()
|
||||
show_num()
|
||||
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) and
|
||||
(event.current.x ~= dec_point_x) and (event.current.y ~= 2) and
|
||||
(event.current.x == event.initial.x) and (event.current.y == event.initial.y) then
|
||||
local idx = util.trinary(event.current.x > dec_point_x, event.current.x - 1, event.current.x)
|
||||
if digits[idx] ~= nil then
|
||||
if event.current.y == 1 then
|
||||
digits[idx] = digits[idx] + 1
|
||||
elseif event.current.y == 3 then
|
||||
digits[idx] = digits[idx] - 1
|
||||
end
|
||||
|
||||
update_value()
|
||||
show_num()
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -176,20 +168,19 @@ local function spinbox(args)
|
||||
end
|
||||
|
||||
-- enable this input
|
||||
function e.enable()
|
||||
draw_arrows(args.arrow_fg_bg.fgd)
|
||||
end
|
||||
function e.on_enabled() draw_arrows(args.arrow_fg_bg.fgd) end
|
||||
|
||||
-- disable this input
|
||||
function e.disable()
|
||||
draw_arrows(args.arrow_disable or colors.lightGray)
|
||||
function e.on_disabled() draw_arrows(args.arrow_disable or colors.lightGray) end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
show_num()
|
||||
draw_arrows(util.trinary(e.enabled, args.arrow_fg_bg.fgd, args.arrow_disable or colors.lightGray))
|
||||
end
|
||||
|
||||
-- default to zero, init digits table
|
||||
e.value = 0
|
||||
set_digits()
|
||||
---@class NumericSpinbox:graphics_element
|
||||
local NumericSpinbox, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return NumericSpinbox, id
|
||||
end
|
||||
|
||||
return spinbox
|
||||
164
graphics/elements/controls/PushButton.lua
Normal file
164
graphics/elements/controls/PushButton.lua
Normal file
@@ -0,0 +1,164 @@
|
||||
-- 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")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
|
||||
---@class push_button_args
|
||||
---@field text string button text
|
||||
---@field callback function function to call on touch
|
||||
---@field min_width? integer text length if omitted
|
||||
---@field alignment? ALIGN text align if min width > length
|
||||
---@field active_fg_bg? cpair foreground/background colors when pressed
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- Create a new push button control element.
|
||||
---@param args 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")
|
||||
|
||||
local text_width = string.len(args.text)
|
||||
local alignment = args.alignment or ALIGN.CENTER
|
||||
|
||||
-- set automatic settings
|
||||
args.can_focus = true
|
||||
args.min_width = args.min_width or 0
|
||||
args.width = math.max(text_width, args.min_width)
|
||||
|
||||
-- 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()
|
||||
|
||||
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)
|
||||
local function show_pressed()
|
||||
if e.enabled and args.active_fg_bg ~= nil then
|
||||
e.value = true
|
||||
e.w_set_fgd(args.active_fg_bg.fgd)
|
||||
e.w_set_bkg(args.active_fg_bg.bkg)
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- draw the button as unpressed (if active_fg_bg set)
|
||||
local function show_unpressed()
|
||||
if e.enabled and args.active_fg_bg ~= nil then
|
||||
e.value = false
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled then
|
||||
if event.type == MOUSE_CLICK.TAP then
|
||||
show_pressed()
|
||||
-- show as unpressed in 0.25 seconds
|
||||
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
|
||||
args.callback()
|
||||
elseif event.type == MOUSE_CLICK.DOWN then
|
||||
show_pressed()
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
show_unpressed()
|
||||
if e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
args.callback()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == KEY_CLICK.DOWN then
|
||||
if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then
|
||||
args.callback()
|
||||
-- visualize click without unfocusing
|
||||
show_unpressed()
|
||||
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_pressed) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value (true simulates pressing the button)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
|
||||
end
|
||||
|
||||
-- show butten as enabled
|
||||
function e.on_enabled()
|
||||
if args.dis_fg_bg ~= nil then
|
||||
e.value = false
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- show button as disabled
|
||||
function e.on_disabled()
|
||||
if args.dis_fg_bg ~= nil then
|
||||
e.value = false
|
||||
e.w_set_fgd(args.dis_fg_bg.fgd)
|
||||
e.w_set_bkg(args.dis_fg_bg.bkg)
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle focus
|
||||
e.on_focused = show_pressed
|
||||
e.on_unfocused = show_unpressed
|
||||
|
||||
---@class PushButton:graphics_element
|
||||
local PushButton, id = e.complete(true)
|
||||
|
||||
return PushButton, id
|
||||
end
|
||||
201
graphics/elements/controls/Radio2D.lua
Normal file
201
graphics/elements/controls/Radio2D.lua
Normal file
@@ -0,0 +1,201 @@
|
||||
-- 2D Radio Button Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class radio_2d_args
|
||||
---@field rows integer
|
||||
---@field columns integer
|
||||
---@field options table
|
||||
---@field radio_colors cpair radio button colors (inner & outer)
|
||||
---@field select_color? color color for radio button when selected
|
||||
---@field color_map? table colors for each radio button when selected
|
||||
---@field disable_color? color color for radio button when disabled
|
||||
---@field disable_fg_bg? cpair text colors when disabled
|
||||
---@field default? integer default state, defaults to options[1]
|
||||
---@field callback? function function to call on touch
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- 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 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")
|
||||
element.assert(type(args.radio_colors) == "table", "radio_colors is a required field")
|
||||
element.assert(type(args.select_color) == "number" or type(args.color_map) == "table", "select_color or color_map is required")
|
||||
element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
|
||||
|
||||
local array = {}
|
||||
local col_widths = {}
|
||||
|
||||
local next_idx = 1
|
||||
local total_width = 0
|
||||
local max_rows = 1
|
||||
|
||||
local focused_opt = 1
|
||||
local focus_x, focus_y = 1, 1
|
||||
|
||||
-- build table to display
|
||||
for col = 1, args.columns do
|
||||
local max_width = 0
|
||||
array[col] = {}
|
||||
|
||||
for row = 1, args.rows do
|
||||
local len = string.len(args.options[next_idx])
|
||||
if len > max_width then max_width = len end
|
||||
if row > max_rows then max_rows = row end
|
||||
|
||||
table.insert(array[col], { text = args.options[next_idx], id = next_idx, x_1 = 1 + total_width, x_2 = 2 + total_width + len })
|
||||
|
||||
next_idx = next_idx + 1
|
||||
if next_idx > #args.options then break end
|
||||
end
|
||||
|
||||
table.insert(col_widths, max_width + 3)
|
||||
total_width = total_width + max_width + 3
|
||||
if next_idx > #args.options then break end
|
||||
end
|
||||
|
||||
args.can_focus = true
|
||||
args.width = total_width
|
||||
args.height = max_rows
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- selected option (convert nil to 1 if missing)
|
||||
e.value = args.default or 1
|
||||
|
||||
-- draw the element
|
||||
function e.redraw()
|
||||
local col_x = 1
|
||||
|
||||
local radio_color_b = util.trinary(type(args.disable_color) == "number" and not e.enabled, args.disable_color, args.radio_colors.color_b)
|
||||
|
||||
for col = 1, #array do
|
||||
for row = 1, #array[col] do
|
||||
local opt = array[col][row]
|
||||
local select_color = args.select_color
|
||||
|
||||
if type(args.color_map) == "table" and args.color_map[opt.id] then
|
||||
select_color = args.color_map[opt.id]
|
||||
end
|
||||
|
||||
local inner_color = util.trinary((e.value == opt.id) and e.enabled, radio_color_b, args.radio_colors.color_a)
|
||||
local outer_color = util.trinary((e.value == opt.id) and e.enabled, select_color, radio_color_b)
|
||||
|
||||
e.w_set_cur(col_x, row)
|
||||
|
||||
e.w_set_fgd(inner_color)
|
||||
e.w_set_bkg(outer_color)
|
||||
e.w_write("\x88")
|
||||
|
||||
e.w_set_fgd(outer_color)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write("\x95")
|
||||
|
||||
if opt.id == focused_opt then
|
||||
focus_x, focus_y = row, col
|
||||
end
|
||||
|
||||
-- write button text
|
||||
if opt.id == focused_opt and e.is_focused() and e.enabled then
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.fgd)
|
||||
elseif type(args.disable_fg_bg) == "table" and not e.enabled then
|
||||
e.w_set_fgd(args.disable_fg_bg.fgd)
|
||||
e.w_set_bkg(args.disable_fg_bg.bkg)
|
||||
else
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.w_write(opt.text)
|
||||
end
|
||||
|
||||
col_x = col_x + col_widths[col]
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) and (event.initial.y == event.current.y) then
|
||||
-- determine what was pressed
|
||||
for _, row in ipairs(array) do
|
||||
local elem = row[event.current.y]
|
||||
if elem ~= nil and event.initial.x >= elem.x_1 and event.initial.x <= elem.x_2 and event.current.x >= elem.x_1 and event.current.x <= elem.x_2 then
|
||||
e.value = elem.id
|
||||
focused_opt = elem.id
|
||||
e.redraw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == core.events.KEY_CLICK.DOWN or event.type == core.events.KEY_CLICK.HELD then
|
||||
if event.type == core.events.KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then
|
||||
e.value = focused_opt
|
||||
e.redraw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
elseif event.key == keys.down then
|
||||
if focused_opt < #args.options then
|
||||
focused_opt = focused_opt + 1
|
||||
e.redraw()
|
||||
end
|
||||
elseif event.key == keys.up then
|
||||
if focused_opt > 1 then
|
||||
focused_opt = focused_opt - 1
|
||||
e.redraw()
|
||||
end
|
||||
elseif event.key == keys.right then
|
||||
if array[focus_y + 1] and array[focus_y + 1][focus_x] then
|
||||
focused_opt = array[focus_y + 1][focus_x].id
|
||||
else focused_opt = array[1][focus_x].id end
|
||||
e.redraw()
|
||||
elseif event.key == keys.left then
|
||||
if array[focus_y - 1] and array[focus_y - 1][focus_x] then
|
||||
focused_opt = array[focus_y - 1][focus_x].id
|
||||
e.redraw()
|
||||
elseif array[#array][focus_x] then
|
||||
focused_opt = array[#array][focus_x].id
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
if type(val) == "number" and val > 0 and val <= #args.options then
|
||||
e.value = val
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle focus & enable
|
||||
e.on_focused = e.redraw
|
||||
e.on_unfocused = e.redraw
|
||||
e.on_enabled = e.redraw
|
||||
e.on_disabled = e.redraw
|
||||
|
||||
---@class Radio2D:graphics_element
|
||||
local Radio2D, id = e.complete(true)
|
||||
|
||||
return Radio2D, id
|
||||
end
|
||||
156
graphics/elements/controls/RadioButton.lua
Normal file
156
graphics/elements/controls/RadioButton.lua
Normal file
@@ -0,0 +1,156 @@
|
||||
-- Radio Button Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
|
||||
---@class radio_button_args
|
||||
---@field options table button options
|
||||
---@field 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
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- Create a new radio button list control element (latch selection, exclusively one button at a time).
|
||||
---@param args 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")
|
||||
element.assert(type(args.select_color) == "number", "select_color is a required field")
|
||||
element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
|
||||
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
|
||||
|
||||
-- determine widths
|
||||
local max_width = 1
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type string
|
||||
if string.len(opt) > max_width then
|
||||
max_width = string.len(opt)
|
||||
end
|
||||
end
|
||||
|
||||
local button_text_width = math.max(max_width, args.min_width or 0)
|
||||
|
||||
-- set automatic args
|
||||
args.can_focus = true
|
||||
args.width = button_text_width + 2
|
||||
args.height = #args.options -- one line per option
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
local focused_opt = 1
|
||||
|
||||
-- button state (convert nil to 1 if missing)
|
||||
e.value = args.default or 1
|
||||
|
||||
-- show the button state
|
||||
function e.redraw()
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type string
|
||||
|
||||
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)
|
||||
e.w_set_bkg(outer_color)
|
||||
e.w_write("\x88")
|
||||
|
||||
e.w_set_fgd(outer_color)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write("\x95")
|
||||
|
||||
-- write button text
|
||||
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)
|
||||
end
|
||||
|
||||
e.w_write(opt)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) and
|
||||
(event.initial.y == event.current.y) and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
-- determine what was pressed
|
||||
if args.options[event.current.y] ~= nil then
|
||||
e.value = event.current.y
|
||||
focused_opt = e.value
|
||||
e.redraw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
|
||||
if event.type == KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then
|
||||
e.value = focused_opt
|
||||
e.redraw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
elseif event.key == keys.down then
|
||||
if focused_opt < #args.options then
|
||||
focused_opt = focused_opt + 1
|
||||
e.redraw()
|
||||
end
|
||||
elseif event.key == keys.up then
|
||||
if focused_opt > 1 then
|
||||
focused_opt = focused_opt - 1
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
if type(val) == "number" and val > 0 and val <= #args.options then
|
||||
e.value = val
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle focus & enable
|
||||
e.on_focused = e.redraw
|
||||
e.on_unfocused = e.redraw
|
||||
e.on_enabled = e.redraw
|
||||
e.on_disabled = e.redraw
|
||||
|
||||
---@class RadioButton:graphics_element
|
||||
local RadioButton, id = e.complete(true)
|
||||
|
||||
return RadioButton, id
|
||||
end
|
||||
173
graphics/elements/controls/Sidebar.lua
Normal file
173
graphics/elements/controls/Sidebar.lua
Normal file
@@ -0,0 +1,173 @@
|
||||
-- Sidebar Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class sidebar_args
|
||||
---@field parent graphics_element
|
||||
---@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
|
||||
|
||||
-- Create a new sidebar tab selector control element.
|
||||
---@param args 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 --[[@as graphics_args]])
|
||||
|
||||
-- default to 1st tab
|
||||
e.value = 1
|
||||
|
||||
local was_pressed = false
|
||||
local tabs = {}
|
||||
|
||||
-- show the button state
|
||||
---@param pressed? boolean if the currently selected tab should appear as actively pressed
|
||||
---@param pressed_idx? integer optional index to show as held (that is not yet selected)
|
||||
local function draw(pressed, pressed_idx)
|
||||
pressed = util.trinary(pressed == nil, was_pressed, pressed)
|
||||
was_pressed = pressed
|
||||
pressed_idx = pressed_idx or e.value
|
||||
|
||||
-- clear
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
for y = 1, e.frame.h do
|
||||
e.w_set_cur(1, y)
|
||||
e.w_write(" ")
|
||||
end
|
||||
|
||||
-- draw tabs
|
||||
for i = 1, #tabs do
|
||||
local tab = tabs[i] ---@type sidebar_tab
|
||||
local y = tab.y_start
|
||||
|
||||
e.w_set_cur(1, y)
|
||||
|
||||
if pressed and i == pressed_idx then
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
else
|
||||
e.w_set_fgd(tab.color.fgd)
|
||||
e.w_set_bkg(tab.color.bkg)
|
||||
end
|
||||
|
||||
if tab.tall then
|
||||
e.w_write(" ")
|
||||
e.w_set_cur(1, y + 1)
|
||||
end
|
||||
|
||||
e.w_write(tab.label)
|
||||
|
||||
if tab.tall then
|
||||
e.w_set_cur(1, y + 2)
|
||||
e.w_write(" ")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- determine which tab was pressed
|
||||
---@param y integer y coordinate
|
||||
local function find_tab(y)
|
||||
for i = 1, #tabs do
|
||||
local tab = tabs[i] ---@type sidebar_tab
|
||||
|
||||
if y >= tab.y_start and y <= tab.y_end then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- determine what was pressed
|
||||
if e.enabled then
|
||||
local cur_idx = find_tab(event.current.y)
|
||||
local ini_idx = find_tab(event.initial.y)
|
||||
local tab = tabs[cur_idx]
|
||||
|
||||
-- handle press if a callback was provided
|
||||
if tab ~= nil and type(tab.callback) == "function" then
|
||||
if event.type == MOUSE_CLICK.TAP then
|
||||
e.value = cur_idx
|
||||
draw(true)
|
||||
-- show as unpressed in 0.25 seconds
|
||||
tcd.dispatch(0.25, function () draw(false) end)
|
||||
tab.callback()
|
||||
elseif event.type == MOUSE_CLICK.DOWN then
|
||||
draw(true, cur_idx)
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
e.value = cur_idx
|
||||
draw(false)
|
||||
tab.callback()
|
||||
else draw(false) end
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
draw(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw(false)
|
||||
end
|
||||
|
||||
-- update the sidebar navigation options
|
||||
---@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 = {}
|
||||
|
||||
for i = 1, #items do
|
||||
local item = items[i]
|
||||
local height = util.trinary(item.tall, 3, 1)
|
||||
|
||||
---@class sidebar_tab
|
||||
local entry = {
|
||||
y_start = next_y, ---@type integer
|
||||
y_end = next_y + height - 1, ---@type integer
|
||||
tall = item.tall, ---@type boolean
|
||||
label = item.label, ---@type string
|
||||
color = item.color, ---@type cpair
|
||||
callback = item.callback ---@type function|nil
|
||||
}
|
||||
|
||||
next_y = next_y + height
|
||||
|
||||
tabs[i] = entry
|
||||
end
|
||||
|
||||
draw()
|
||||
end
|
||||
|
||||
-- element redraw
|
||||
e.redraw = draw
|
||||
|
||||
---@class Sidebar:graphics_element
|
||||
local Sidebar, id = e.complete(true)
|
||||
|
||||
return Sidebar, id
|
||||
end
|
||||
79
graphics/elements/controls/SwitchButton.lua
Normal file
79
graphics/elements/controls/SwitchButton.lua
Normal file
@@ -0,0 +1,79 @@
|
||||
-- Button Graphics Element
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class switch_button_args
|
||||
---@field text string button text
|
||||
---@field callback function function to call on touch
|
||||
---@field default? boolean default state, defaults to off (false)
|
||||
---@field min_width? integer text length + 2 if omitted
|
||||
---@field active_fg_bg cpair foreground/background colors when pressed
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- Create a new latching switch button control element.
|
||||
---@param args 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")
|
||||
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
|
||||
|
||||
local text_width = string.len(args.text)
|
||||
|
||||
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 --[[@as graphics_args]])
|
||||
|
||||
e.value = args.default or false
|
||||
|
||||
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1
|
||||
local v_pad = math.floor(e.frame.h / 2) + 1
|
||||
|
||||
-- show the button state
|
||||
function e.redraw()
|
||||
if e.value then
|
||||
e.w_set_fgd(args.active_fg_bg.fgd)
|
||||
e.w_set_bkg(args.active_fg_bg.bkg)
|
||||
else
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.window.clear()
|
||||
e.w_set_cur(h_pad, v_pad)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
e.value = not e.value
|
||||
e.redraw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value (does not call the callback)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
---@class SwitchButton:graphics_element
|
||||
local SwitchButton, id = e.complete(true)
|
||||
|
||||
return SwitchButton, id
|
||||
end
|
||||
@@ -23,17 +23,15 @@ 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)
|
||||
assert(type(args.tabs) == "table", "graphics.elements.controls.tabbar: tabs is a required field")
|
||||
assert(#args.tabs > 0, "graphics.elements.controls.tabbar: at least one tab is required")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.tabbar: callback is a required field")
|
||||
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||
"graphics.elements.controls.tabbar: min_width must be nil or a number > 0")
|
||||
---@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")
|
||||
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
|
||||
|
||||
-- always 1 tall
|
||||
args.height = 1
|
||||
|
||||
-- determine widths
|
||||
@@ -48,9 +46,9 @@ 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]])
|
||||
|
||||
assert(e.frame.w >= (button_width * #args.tabs), "graphics.elements.controls.tabbar: width insufficent to display all tabs")
|
||||
element.assert(e.frame.w >= (button_width * #args.tabs), "width insufficent to display all tabs")
|
||||
|
||||
-- default to 1st tab
|
||||
e.value = 1
|
||||
@@ -67,21 +65,21 @@ local function tabbar(args)
|
||||
end
|
||||
|
||||
-- show the tab state
|
||||
local function draw()
|
||||
function e.redraw()
|
||||
for i = 1, #args.tabs do
|
||||
local tab = args.tabs[i] ---@type tabbar_tab
|
||||
|
||||
e.window.setCursorPos(tab._start_x, 1)
|
||||
e.w_set_cur(tab._start_x, 1)
|
||||
|
||||
if e.value == i then
|
||||
e.window.setTextColor(tab.color.fgd)
|
||||
e.window.setBackgroundColor(tab.color.bkg)
|
||||
e.w_set_fgd(tab.color.fgd)
|
||||
e.w_set_bkg(tab.color.bkg)
|
||||
else
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.window.write(util.pad(tab.name, button_width))
|
||||
e.w_write(util.pad(tab.name, button_width))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -100,7 +98,7 @@ local function tabbar(args)
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- determine what was pressed
|
||||
if e.enabled and core.events.was_clicked(event.type) then
|
||||
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
-- a button may have been pressed, which one was it?
|
||||
local tab_ini = which_tab(event.initial.x)
|
||||
local tab_cur = which_tab(event.current.x)
|
||||
@@ -109,7 +107,7 @@ local function tabbar(args)
|
||||
-- tap always has identical coordinates, so this always passes for taps
|
||||
if tab_ini == tab_cur and tab_cur ~= nil then
|
||||
e.value = tab_cur
|
||||
draw()
|
||||
e.redraw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
@@ -119,13 +117,11 @@ local function tabbar(args)
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw()
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
---@class TabBar:graphics_element
|
||||
local TabBar, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return TabBar, id
|
||||
end
|
||||
|
||||
return tabbar
|
||||
@@ -1,85 +0,0 @@
|
||||
-- Checkbox Graphics Element
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class checkbox_args
|
||||
---@field label string checkbox text
|
||||
---@field box_fg_bg cpair colors for checkbox
|
||||
---@field callback function function to call on press
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new checkbox control
|
||||
---@param args checkbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function checkbox(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.controls.checkbox: label is a required field")
|
||||
assert(type(args.box_fg_bg) == "table", "graphics.elements.controls.checkbox: box_fg_bg is a required field")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.checkbox: callback is a required field")
|
||||
|
||||
args.height = 1
|
||||
args.width = 3 + string.len(args.label)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = false
|
||||
|
||||
-- show the button state
|
||||
local function draw()
|
||||
e.window.setCursorPos(1, 1)
|
||||
|
||||
if e.value then
|
||||
-- show as selected
|
||||
e.window.setTextColor(args.box_fg_bg.bkg)
|
||||
e.window.setBackgroundColor(args.box_fg_bg.fgd)
|
||||
e.window.write("\x88")
|
||||
e.window.setTextColor(args.box_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.write("\x95")
|
||||
else
|
||||
-- show as unselected
|
||||
e.window.setTextColor(e.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(args.box_fg_bg.bkg)
|
||||
e.window.write("\x88")
|
||||
e.window.setTextColor(args.box_fg_bg.bkg)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.write("\x95")
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) then
|
||||
e.value = not e.value
|
||||
draw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw()
|
||||
end
|
||||
|
||||
-- write label text
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.write(args.label)
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return checkbox
|
||||
@@ -1,205 +0,0 @@
|
||||
-- Hazard-bordered Button Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class hazard_button_args
|
||||
---@field text string text to show on button
|
||||
---@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 parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new hazard button
|
||||
---@param args hazard_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function hazard_button(args)
|
||||
assert(type(args.text) == "string", "graphics.elements.controls.hazard_button: text is a required field")
|
||||
assert(type(args.accent) == "number", "graphics.elements.controls.hazard_button: accent is a required field")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.hazard_button: callback is a required field")
|
||||
|
||||
-- static dimensions
|
||||
args.height = 3
|
||||
args.width = string.len(args.text) + 4
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- write the button text
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
|
||||
-- draw border
|
||||
---@param accent color accent color
|
||||
local function draw_border(accent)
|
||||
-- top
|
||||
e.window.setTextColor(accent)
|
||||
e.window.setBackgroundColor(args.fg_bg.bkg)
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write("\x99" .. util.strrep("\x89", args.width - 2) .. "\x99")
|
||||
|
||||
-- center left
|
||||
e.window.setCursorPos(1, 2)
|
||||
e.window.setTextColor(args.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(accent)
|
||||
e.window.write("\x99")
|
||||
|
||||
-- center right
|
||||
e.window.setTextColor(args.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(accent)
|
||||
e.window.setCursorPos(args.width, 2)
|
||||
e.window.write("\x99")
|
||||
|
||||
-- bottom
|
||||
e.window.setTextColor(accent)
|
||||
e.window.setBackgroundColor(args.fg_bg.bkg)
|
||||
e.window.setCursorPos(1, 3)
|
||||
e.window.write("\x99" .. util.strrep("\x98", args.width - 2) .. "\x99")
|
||||
end
|
||||
|
||||
-- on request timeout: recursively calls itself to double flash button text
|
||||
---@param n integer call count
|
||||
local function on_timeout(n)
|
||||
-- start at 0
|
||||
if n == nil then n = 0 end
|
||||
|
||||
if n == 0 then
|
||||
-- go back off
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
end
|
||||
|
||||
if n >= 4 then
|
||||
-- done
|
||||
elseif n % 2 == 0 then
|
||||
-- toggle text color on after 0.25 seconds
|
||||
tcd.dispatch(0.25, function ()
|
||||
e.window.setTextColor(args.accent)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
on_timeout(n + 1)
|
||||
on_timeout(n + 1)
|
||||
end)
|
||||
elseif n % 1 then
|
||||
-- toggle text color off after 0.25 seconds
|
||||
tcd.dispatch(0.25, function ()
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
on_timeout(n + 1)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- blink routine for success indication
|
||||
local function on_success()
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
end
|
||||
|
||||
-- blink routine for failure indication
|
||||
---@param n integer call count
|
||||
local function on_failure(n)
|
||||
-- start at 0
|
||||
if n == nil then n = 0 end
|
||||
|
||||
if n == 0 then
|
||||
-- go back off
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
end
|
||||
|
||||
if n >= 2 then
|
||||
-- done
|
||||
elseif n % 2 == 0 then
|
||||
-- toggle text color on after 0.5 seconds
|
||||
tcd.dispatch(0.5, function ()
|
||||
e.window.setTextColor(args.accent)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
on_failure(n + 1)
|
||||
end)
|
||||
elseif n % 1 then
|
||||
-- toggle text color off after 0.25 seconds
|
||||
tcd.dispatch(0.25, function ()
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
on_failure(n + 1)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled then
|
||||
if core.events.was_clicked(event.type) then
|
||||
-- change text color to indicate clicked
|
||||
e.window.setTextColor(args.accent)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
|
||||
-- abort any other callbacks
|
||||
tcd.abort(on_timeout)
|
||||
tcd.abort(on_success)
|
||||
tcd.abort(on_failure)
|
||||
|
||||
-- 1.5 second timeout
|
||||
tcd.dispatch(1.5, on_timeout)
|
||||
|
||||
-- call the touch callback
|
||||
args.callback()
|
||||
end
|
||||
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)
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
|
||||
end
|
||||
|
||||
-- show the button as disabled
|
||||
function e.disable()
|
||||
if args.dis_colors then
|
||||
draw_border(args.dis_colors.color_a)
|
||||
e.window.setTextColor(args.dis_colors.color_b)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
end
|
||||
end
|
||||
|
||||
-- show the button as enabled
|
||||
function e.enable()
|
||||
draw_border(args.accent)
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
end
|
||||
|
||||
-- initial draw of border
|
||||
draw_border(args.accent)
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return hazard_button
|
||||
@@ -1,127 +0,0 @@
|
||||
-- Button Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
|
||||
---@class push_button_args
|
||||
---@field text string button text
|
||||
---@field callback function function to call on touch
|
||||
---@field min_width? integer text length if omitted
|
||||
---@field active_fg_bg? cpair foreground/background colors when pressed
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new push button
|
||||
---@param args push_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function push_button(args)
|
||||
assert(type(args.text) == "string", "graphics.elements.controls.push_button: text is a required field")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.push_button: callback is a required field")
|
||||
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||
"graphics.elements.controls.push_button: min_width must be nil or a number > 0")
|
||||
|
||||
local text_width = string.len(args.text)
|
||||
|
||||
-- single line height, calculate width
|
||||
args.height = 1
|
||||
args.min_width = args.min_width or 0
|
||||
args.width = math.max(text_width, args.min_width)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1
|
||||
local v_pad = math.floor(e.frame.h / 2) + 1
|
||||
|
||||
-- draw the button
|
||||
local function draw()
|
||||
e.window.clear()
|
||||
|
||||
-- write the button text
|
||||
e.window.setCursorPos(h_pad, v_pad)
|
||||
e.window.write(args.text)
|
||||
end
|
||||
|
||||
-- draw the button as pressed (if active_fg_bg set)
|
||||
local function show_pressed()
|
||||
if e.enabled and args.active_fg_bg ~= nil then
|
||||
e.value = true
|
||||
e.window.setTextColor(args.active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(args.active_fg_bg.bkg)
|
||||
draw()
|
||||
end
|
||||
end
|
||||
|
||||
-- draw the button as unpressed (if active_fg_bg set)
|
||||
local function show_unpressed()
|
||||
if e.enabled and args.active_fg_bg ~= nil then
|
||||
e.value = false
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
draw()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled then
|
||||
if event.type == CLICK_TYPE.TAP then
|
||||
show_pressed()
|
||||
-- show as unpressed in 0.25 seconds
|
||||
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
|
||||
args.callback()
|
||||
elseif event.type == CLICK_TYPE.DOWN then
|
||||
show_pressed()
|
||||
elseif event.type == CLICK_TYPE.UP then
|
||||
show_unpressed()
|
||||
if e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
args.callback()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value (true simulates pressing the button)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
|
||||
end
|
||||
|
||||
-- show butten as enabled
|
||||
function e.enable()
|
||||
if args.dis_fg_bg ~= nil then
|
||||
e.value = false
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
draw()
|
||||
end
|
||||
end
|
||||
|
||||
-- show button as disabled
|
||||
function e.disable()
|
||||
if args.dis_fg_bg ~= nil then
|
||||
e.value = false
|
||||
e.window.setTextColor(args.dis_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(args.dis_fg_bg.bkg)
|
||||
draw()
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return push_button
|
||||
@@ -1,110 +0,0 @@
|
||||
-- Radio Button Graphics Element
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class radio_button_args
|
||||
---@field options table button options
|
||||
---@field callback function function to call on touch
|
||||
---@field radio_colors cpair colors for radio button center dot when active (a) or inactive (b)
|
||||
---@field radio_bg color background color of radio button
|
||||
---@field default? integer default state, defaults to options[1]
|
||||
---@field min_width? integer text length + 2 if omitted
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new radio button list (latch selection, exclusively one button at a time)
|
||||
---@param args radio_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function radio_button(args)
|
||||
assert(type(args.options) == "table", "graphics.elements.controls.radio_button: options is a required field")
|
||||
assert(#args.options > 0, "graphics.elements.controls.radio_button: at least one option is required")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.radio_button: callback is a required field")
|
||||
assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0),
|
||||
"graphics.elements.controls.radio_button: default must be nil or a number > 0")
|
||||
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||
"graphics.elements.controls.radio_button: min_width must be nil or a number > 0")
|
||||
|
||||
-- one line per option
|
||||
args.height = #args.options
|
||||
|
||||
-- determine widths
|
||||
local max_width = 1
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type string
|
||||
if string.len(opt) > max_width then
|
||||
max_width = string.len(opt)
|
||||
end
|
||||
end
|
||||
|
||||
local button_text_width = math.max(max_width, args.min_width or 0)
|
||||
|
||||
args.width = button_text_width + 2
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- button state (convert nil to 1 if missing)
|
||||
e.value = args.default or 1
|
||||
|
||||
-- show the button state
|
||||
local function draw()
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type string
|
||||
|
||||
e.window.setCursorPos(1, i)
|
||||
|
||||
if e.value == i then
|
||||
-- show as selected
|
||||
e.window.setTextColor(args.radio_colors.color_a)
|
||||
e.window.setBackgroundColor(args.radio_bg)
|
||||
else
|
||||
-- show as unselected
|
||||
e.window.setTextColor(args.radio_colors.color_b)
|
||||
e.window.setBackgroundColor(args.radio_bg)
|
||||
end
|
||||
|
||||
e.window.write("\x88")
|
||||
|
||||
e.window.setTextColor(args.radio_bg)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.write("\x95")
|
||||
|
||||
-- write button text
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.write(opt)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) and (event.initial.y == event.current.y) then
|
||||
-- determine what was pressed
|
||||
if args.options[event.current.y] ~= nil then
|
||||
e.value = event.current.y
|
||||
draw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return radio_button
|
||||
@@ -1,122 +0,0 @@
|
||||
-- Sidebar Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
|
||||
---@class sidebar_tab
|
||||
---@field char string character identifier
|
||||
---@field color cpair tab colors (fg/bg)
|
||||
|
||||
---@class sidebar_args
|
||||
---@field tabs table sidebar tab options
|
||||
---@field callback function function to call on tab change
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 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 sidebar tab selector
|
||||
---@param args sidebar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function sidebar(args)
|
||||
assert(type(args.tabs) == "table", "graphics.elements.controls.sidebar: tabs is a required field")
|
||||
assert(#args.tabs > 0, "graphics.elements.controls.sidebar: at least one tab is required")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.sidebar: callback is a required field")
|
||||
|
||||
-- always 3 wide
|
||||
args.width = 3
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
assert(e.frame.h >= (#args.tabs * 3), "graphics.elements.controls.sidebar: height insufficent to display all tabs")
|
||||
|
||||
-- default to 1st tab
|
||||
e.value = 1
|
||||
|
||||
-- show the button state
|
||||
---@param pressed boolean if the currently selected tab should appear as actively pressed
|
||||
---@param pressed_idx? integer optional index to show as held (that is not yet selected)
|
||||
local function draw(pressed, pressed_idx)
|
||||
pressed_idx = pressed_idx or e.value
|
||||
|
||||
for i = 1, #args.tabs do
|
||||
local tab = args.tabs[i] ---@type sidebar_tab
|
||||
|
||||
local y = ((i - 1) * 3) + 1
|
||||
|
||||
e.window.setCursorPos(1, y)
|
||||
|
||||
if pressed and i == pressed_idx then
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
else
|
||||
e.window.setTextColor(tab.color.fgd)
|
||||
e.window.setBackgroundColor(tab.color.bkg)
|
||||
end
|
||||
|
||||
e.window.write(" ")
|
||||
e.window.setCursorPos(1, y + 1)
|
||||
if e.value == i then
|
||||
-- show as selected
|
||||
e.window.write(" " .. tab.char .. "\x10")
|
||||
else
|
||||
-- show as unselected
|
||||
e.window.write(" " .. tab.char .. " ")
|
||||
end
|
||||
e.window.setCursorPos(1, y + 2)
|
||||
e.window.write(" ")
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- determine what was pressed
|
||||
if e.enabled then
|
||||
local cur_idx = math.ceil(event.current.y / 3)
|
||||
local ini_idx = math.ceil(event.initial.y / 3)
|
||||
|
||||
if args.tabs[cur_idx] ~= nil then
|
||||
if event.type == CLICK_TYPE.TAP then
|
||||
e.value = cur_idx
|
||||
draw(true)
|
||||
-- show as unpressed in 0.25 seconds
|
||||
tcd.dispatch(0.25, function () draw(false) end)
|
||||
args.callback(e.value)
|
||||
elseif event.type == CLICK_TYPE.DOWN then
|
||||
draw(true, cur_idx)
|
||||
elseif event.type == CLICK_TYPE.UP then
|
||||
if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
e.value = cur_idx
|
||||
draw(false)
|
||||
args.callback(e.value)
|
||||
else draw(false) end
|
||||
end
|
||||
elseif event.type == CLICK_TYPE.UP then
|
||||
draw(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw(false)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw(false)
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return sidebar
|
||||
@@ -1,93 +0,0 @@
|
||||
-- Button Graphics Element
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class switch_button_args
|
||||
---@field text string button text
|
||||
---@field callback function function to call on touch
|
||||
---@field default? boolean default state, defaults to off (false)
|
||||
---@field min_width? integer text length + 2 if omitted
|
||||
---@field active_fg_bg cpair foreground/background colors when pressed
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new switch button (latch high/low)
|
||||
---@param args switch_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function switch_button(args)
|
||||
assert(type(args.text) == "string", "graphics.elements.controls.switch_button: text is a required field")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.switch_button: callback is a required field")
|
||||
assert(type(args.active_fg_bg) == "table", "graphics.elements.controls.switch_button: active_fg_bg is a required field")
|
||||
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||
"graphics.elements.controls.switch_button: min_width must be nil or a number > 0")
|
||||
|
||||
local text_width = string.len(args.text)
|
||||
|
||||
-- single line height, calculate width
|
||||
args.height = 1
|
||||
args.min_width = args.min_width or 0
|
||||
args.width = math.max(text_width, args.min_width)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- button state (convert nil to false if missing)
|
||||
e.value = args.default or false
|
||||
|
||||
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1
|
||||
local v_pad = math.floor(e.frame.h / 2) + 1
|
||||
|
||||
-- show the button state
|
||||
local function draw_state()
|
||||
if e.value then
|
||||
-- show as pressed
|
||||
e.window.setTextColor(args.active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(args.active_fg_bg.bkg)
|
||||
else
|
||||
-- show as unpressed
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- clear to redraw background
|
||||
e.window.clear()
|
||||
|
||||
-- write the button text
|
||||
e.window.setCursorPos(h_pad, v_pad)
|
||||
e.window.write(args.text)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw_state()
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) then
|
||||
-- toggle state
|
||||
e.value = not e.value
|
||||
draw_state()
|
||||
|
||||
-- call the touch callback with state
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
-- set state
|
||||
e.value = val
|
||||
draw_state()
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return switch_button
|
||||
255
graphics/elements/form/NumberField.lua
Normal file
255
graphics/elements/form/NumberField.lua
Normal file
@@ -0,0 +1,255 @@
|
||||
-- Numeric Value Entry Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class number_field_args
|
||||
---@field default? number default value, defaults to 0
|
||||
---@field min? number minimum, enforced on unfocus
|
||||
---@field max? number maximum, enforced on unfocus
|
||||
---@field max_chars? integer maximum number of characters, defaults to width
|
||||
---@field max_int_digits? integer maximum number of integer digits, enforced on unfocus
|
||||
---@field max_frac_digits? integer maximum number of fractional digits, enforced on unfocus
|
||||
---@field allow_decimal? boolean true to allow decimals
|
||||
---@field allow_negative? boolean true to allow negative numbers
|
||||
---@field 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
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- Create a new numeric entry field.
|
||||
---@param args 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")
|
||||
|
||||
args.height = 1
|
||||
args.can_focus = true
|
||||
|
||||
-- create new graphics element base object
|
||||
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
|
||||
_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, args.align_right)
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- only handle if on an increment or decrement arrow
|
||||
if e.enabled and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
if core.events.was_clicked(event.type) then
|
||||
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(x)
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
|
||||
ifield.select_all()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == KEY_CLICK.CHAR and string.len(e.value) < args.max_chars then
|
||||
if tonumber(event.name) then
|
||||
if e.value == 0 then e.value = "" end
|
||||
ifield.try_insert_char(event.name)
|
||||
end
|
||||
elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
|
||||
if (event.key == keys.backspace or event.key == keys.delete) and (string.len(e.value) > 0) then
|
||||
ifield.backspace()
|
||||
has_decimal = string.find(e.value, "%.") ~= nil
|
||||
elseif (event.key == keys.period or event.key == keys.numPadDecimal) and (not has_decimal) and args.allow_decimal then
|
||||
has_decimal = true
|
||||
ifield.try_insert_char(".")
|
||||
elseif (event.key == keys.minus or event.key == keys.numPadSubtract) and (string.len(e.value) == 0) and args.allow_negative then
|
||||
ifield.set_value("-")
|
||||
elseif event.key == keys.left then
|
||||
ifield.nav_left()
|
||||
elseif event.key == keys.right then
|
||||
ifield.nav_right()
|
||||
elseif event.key == keys.a and event.ctrl then
|
||||
ifield.select_all()
|
||||
elseif event.key == keys.home or event.key == keys.up then
|
||||
ifield.nav_start()
|
||||
elseif event.key == keys["end"] or event.key == keys.down then
|
||||
ifield.nav_end()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value (must be a number)
|
||||
---@param val number number to show
|
||||
function e.set_value(val)
|
||||
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
|
||||
---@param min integer minimum allowed value
|
||||
function e.set_min(min)
|
||||
args.min = min
|
||||
e.on_unfocused()
|
||||
end
|
||||
|
||||
-- set maximum input value
|
||||
---@param max integer maximum allowed value
|
||||
function e.set_max(max)
|
||||
args.max = max
|
||||
e.on_unfocused()
|
||||
end
|
||||
|
||||
-- replace text with pasted text if its a number
|
||||
---@param text string string pasted
|
||||
function e.handle_paste(text)
|
||||
if tonumber(text) then
|
||||
ifield.set_value("" .. tonumber(text))
|
||||
else
|
||||
ifield.set_value("0")
|
||||
end
|
||||
end
|
||||
|
||||
-- handle unfocused
|
||||
function e.on_unfocused()
|
||||
local val, max, min = tonumber(e.value), tonumber(args.max), tonumber(args.min)
|
||||
|
||||
if val then
|
||||
if args.max_int_digits or args.max_frac_digits then
|
||||
local str = e.value
|
||||
local ceil = false
|
||||
|
||||
if string.find(str, "-") then str = string.sub(e.value, 2) end
|
||||
local parts = util.strtok(str, ".")
|
||||
|
||||
if parts[1] and args.max_int_digits then
|
||||
if string.len(parts[1]) > args.max_int_digits then
|
||||
parts[1] = string.rep("9", args.max_int_digits)
|
||||
ceil = true
|
||||
end
|
||||
end
|
||||
|
||||
if args.allow_decimal and args.max_frac_digits then
|
||||
if ceil then
|
||||
parts[2] = string.rep("9", args.max_frac_digits)
|
||||
elseif parts[2] and (string.len(parts[2]) > args.max_frac_digits) then
|
||||
-- add a half of the highest precision fractional value in order to round using floor
|
||||
local scaled = math.fmod(val, 1) * (10 ^ (args.max_frac_digits))
|
||||
local value = math.floor(scaled + 0.5)
|
||||
local unscaled = value * (10 ^ (-args.max_frac_digits))
|
||||
parts[2] = string.sub(tostring(unscaled), 3) -- remove starting "0."
|
||||
end
|
||||
end
|
||||
|
||||
if parts[2] then parts[2] = "." .. parts[2] else parts[2] = "" end
|
||||
|
||||
val = tonumber((parts[1] or "") .. parts[2]) or 0
|
||||
end
|
||||
|
||||
if max and val > max then
|
||||
_set_value(max)
|
||||
ifield.nav_start()
|
||||
elseif min and val < min then
|
||||
_set_value(min)
|
||||
ifield.nav_start()
|
||||
else
|
||||
_set_value(val)
|
||||
ifield.nav_end()
|
||||
end
|
||||
else
|
||||
e.value = ""
|
||||
end
|
||||
|
||||
ifield.show()
|
||||
end
|
||||
|
||||
-- handle focus (not unfocus), enable, and redraw with show()
|
||||
e.on_focused = ifield.show
|
||||
e.on_enabled = ifield.show
|
||||
e.on_disabled = ifield.show
|
||||
e.redraw = ifield.show
|
||||
|
||||
---@class NumberField:graphics_element
|
||||
local NumberField, id = e.complete(true)
|
||||
|
||||
-- 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
|
||||
104
graphics/elements/form/TextField.lua
Normal file
104
graphics/elements/form/TextField.lua
Normal file
@@ -0,0 +1,104 @@
|
||||
-- Text Value Entry Graphics Element
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class text_field_args
|
||||
---@field value? string initial value
|
||||
---@field max_len? integer maximum string length
|
||||
---@field censor? string character to replace text with when printing to screen
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- Create a new text entry field.
|
||||
---@param args 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 --[[@as graphics_args]])
|
||||
|
||||
-- set initial value
|
||||
e.value = args.value or ""
|
||||
|
||||
-- make an interactive field manager
|
||||
local ifield = core.new_ifield(e, args.max_len or e.frame.w, args.fg_bg, args.dis_fg_bg)
|
||||
|
||||
ifield.censor(args.censor)
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- only handle if on an increment or decrement arrow
|
||||
if e.enabled and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
if core.events.was_clicked(event.type) then
|
||||
e.take_focus()
|
||||
|
||||
if event.type == MOUSE_CLICK.UP then
|
||||
ifield.move_cursor(event.current.x)
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
|
||||
ifield.select_all()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == KEY_CLICK.CHAR then
|
||||
ifield.try_insert_char(event.name)
|
||||
elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
|
||||
if (event.key == keys.backspace or event.key == keys.delete) then
|
||||
ifield.backspace()
|
||||
elseif event.key == keys.left then
|
||||
ifield.nav_left()
|
||||
elseif event.key == keys.right then
|
||||
ifield.nav_right()
|
||||
elseif event.key == keys.a and event.ctrl then
|
||||
ifield.select_all()
|
||||
elseif event.key == keys.home or event.key == keys.up then
|
||||
ifield.nav_start()
|
||||
elseif event.key == keys["end"] or event.key == keys.down then
|
||||
ifield.nav_end()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val string string to set
|
||||
function e.set_value(val)
|
||||
ifield.set_value(val)
|
||||
end
|
||||
|
||||
-- replace text with pasted text
|
||||
---@param text string string to set
|
||||
function e.handle_paste(text)
|
||||
ifield.set_value(text)
|
||||
end
|
||||
|
||||
-- handle focus, enable, and redraw with show()
|
||||
e.on_focused = ifield.show
|
||||
e.on_unfocused = ifield.show
|
||||
e.on_enabled = ifield.show
|
||||
e.on_disabled = ifield.show
|
||||
e.redraw = ifield.show
|
||||
|
||||
---@class TextField:graphics_element
|
||||
local TextField, id = e.complete(true)
|
||||
|
||||
TextField.censor = ifield.censor
|
||||
|
||||
return TextField, id
|
||||
end
|
||||
@@ -20,18 +20,18 @@ 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)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.alight: label is a required field")
|
||||
assert(type(args.c1) == "number", "graphics.elements.indicators.alight: c1 is a required field")
|
||||
assert(type(args.c2) == "number", "graphics.elements.indicators.alight: c2 is a required field")
|
||||
assert(type(args.c3) == "number", "graphics.elements.indicators.alight: c3 is a required field")
|
||||
---@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")
|
||||
element.assert(type(args.c3) == "number", "c3 is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.alight: period is a required field if flash is enabled")
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
@@ -49,21 +49,23 @@ 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
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
if e.value == 2 then
|
||||
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
else
|
||||
if e.value == 3 then
|
||||
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -76,7 +78,7 @@ local function alarm_indicator_light(args)
|
||||
local was_off = e.value ~= 2
|
||||
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if args.flash then
|
||||
if was_off and (new_state == 2) then
|
||||
@@ -87,17 +89,17 @@ local function alarm_indicator_light(args)
|
||||
flasher.stop(flash_callback)
|
||||
|
||||
if new_state == 3 then
|
||||
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
elseif new_state == 2 then
|
||||
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
elseif new_state == 3 then
|
||||
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -105,11 +107,14 @@ local function alarm_indicator_light(args)
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(1)
|
||||
e.window.write(args.label)
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(e.value)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
---@class AlarmLight:graphics_element
|
||||
local AlarmLight, id = e.complete(true)
|
||||
|
||||
return AlarmLight, id
|
||||
end
|
||||
|
||||
return alarm_indicator_light
|
||||
@@ -13,13 +13,13 @@ 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)
|
||||
assert(util.is_int(args.reactor_l), "graphics.elements.indicators.coremap: reactor_l is a required field")
|
||||
assert(util.is_int(args.reactor_w), "graphics.elements.indicators.coremap: reactor_w is a required field")
|
||||
---@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")
|
||||
|
||||
-- require max dimensions
|
||||
args.width = 18
|
||||
@@ -29,7 +29,9 @@ 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
|
||||
|
||||
local alternator = true
|
||||
|
||||
@@ -47,25 +49,25 @@ local function core_map(args)
|
||||
|
||||
-- create coordinate grid and frame
|
||||
local function draw_frame()
|
||||
e.window.setTextColor(colors.white)
|
||||
e.w_set_fgd(colors.white)
|
||||
|
||||
for x = 0, (inner_width - 1) do
|
||||
e.window.setCursorPos(x + start_x, 1)
|
||||
e.window.write(util.sprintf("%X", x))
|
||||
e.w_set_cur(x + start_x, 1)
|
||||
e.w_write(util.sprintf("%X", x))
|
||||
end
|
||||
|
||||
for y = 0, (inner_height - 1) do
|
||||
e.window.setCursorPos(1, y + start_y)
|
||||
e.window.write(util.sprintf("%X", y))
|
||||
e.w_set_cur(1, y + start_y)
|
||||
e.w_write(util.sprintf("%X", y))
|
||||
end
|
||||
|
||||
-- even out bottom edge
|
||||
e.window.setTextColor(e.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(args.parent.get_fg_bg().bkg)
|
||||
e.window.setCursorPos(1, e.frame.h)
|
||||
e.window.write(util.strrep("\x8f", e.frame.w))
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(args.parent.get_fg_bg().bkg)
|
||||
e.w_set_cur(1, e.frame.h)
|
||||
e.w_write(string.rep("\x8f", e.frame.w))
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- draw the core
|
||||
@@ -102,13 +104,13 @@ local function core_map(args)
|
||||
|
||||
-- draw pattern
|
||||
for y = start_y, inner_height + (start_y - 1) do
|
||||
e.window.setCursorPos(start_x, y)
|
||||
e.w_set_cur(start_x, y)
|
||||
for _ = 1, inner_width do
|
||||
if alternator then
|
||||
i = i + 1
|
||||
e.window.blit("\x07", text_c, back_c)
|
||||
e.w_blit("\x07", text_c, back_c)
|
||||
else
|
||||
e.window.blit("\x07", "7", "8")
|
||||
e.w_blit("\x07", "7", "8")
|
||||
end
|
||||
|
||||
alternator = not alternator
|
||||
@@ -157,13 +159,14 @@ local function core_map(args)
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial (one-time except for resize()) frame draw
|
||||
draw_frame()
|
||||
-- redraw both frame and core
|
||||
function e.redraw()
|
||||
draw_frame()
|
||||
draw_core(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.on_update(0)
|
||||
---@class CoreMap:graphics_element
|
||||
local CoreMap, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return CoreMap, id
|
||||
end
|
||||
|
||||
return core_map
|
||||
@@ -19,30 +19,22 @@ 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)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.data: label is a required field")
|
||||
assert(type(args.format) == "string", "graphics.elements.indicators.data: format is a required field")
|
||||
assert(args.value ~= nil, "graphics.elements.indicators.data: value is a required field")
|
||||
assert(util.is_int(args.width), "graphics.elements.indicators.data: width is a required field")
|
||||
---@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")
|
||||
element.assert(util.is_int(args.width), "width is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- label color
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_a)
|
||||
end
|
||||
|
||||
-- write label
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write(args.label)
|
||||
e.value = args.value
|
||||
|
||||
local value_color = e.fg_bg.fgd
|
||||
local label_len = string.len(args.label)
|
||||
@@ -60,25 +52,25 @@ local function data(args)
|
||||
e.value = value
|
||||
|
||||
-- clear old data and label
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.write(util.spaces(clear_width))
|
||||
e.w_set_cur(data_start, 1)
|
||||
e.w_write(util.spaces(clear_width))
|
||||
|
||||
-- write data
|
||||
local data_str = util.sprintf(args.format, value)
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.setTextColor(value_color)
|
||||
e.w_set_cur(data_start, 1)
|
||||
e.w_set_fgd(value_color)
|
||||
if args.commas then
|
||||
e.window.write(util.comma_format(data_str))
|
||||
e.w_write(util.comma_format(data_str))
|
||||
else
|
||||
e.window.write(data_str)
|
||||
e.w_write(data_str)
|
||||
end
|
||||
|
||||
-- write label
|
||||
if args.unit ~= nil then
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_b)
|
||||
e.w_set_fgd(args.lu_colors.color_b)
|
||||
end
|
||||
e.window.write(" " .. args.unit)
|
||||
e.w_write(" " .. args.unit)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -93,10 +85,17 @@ local function data(args)
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial value draw
|
||||
e.on_update(args.value)
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(args.label)
|
||||
|
||||
return e.complete()
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
---@class DataIndicator:graphics_element
|
||||
local DataIndicator, id = e.complete(true)
|
||||
|
||||
return DataIndicator, id
|
||||
end
|
||||
|
||||
return data
|
||||
@@ -17,21 +17,22 @@ 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)
|
||||
-- properties/state
|
||||
local last_num_bars = -1
|
||||
|
||||
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
|
||||
|
||||
-- bar width is width - 5 characters for " 100%" if showing percent
|
||||
local bar_width = util.trinary(args.show_percent, e.frame.w - 5, e.frame.w)
|
||||
|
||||
assert(bar_width > 0, "graphics.elements.indicators.hbar: too small for bar")
|
||||
element.assert(bar_width > 0, "too small for bar")
|
||||
|
||||
local last_num_bars = -1
|
||||
|
||||
-- determine bar colors
|
||||
local bar_bkg = e.fg_bg.blit_bkg
|
||||
@@ -87,16 +88,16 @@ local function hbar(args)
|
||||
|
||||
-- draw bar
|
||||
for y = 1, e.frame.h do
|
||||
e.window.setCursorPos(1, y)
|
||||
e.w_set_cur(1, y)
|
||||
-- intentionally swapped fgd/bkg since we use spaces as fill, but they are the opposite
|
||||
e.window.blit(spaces, bkg, fgd)
|
||||
e.w_blit(spaces, bkg, fgd)
|
||||
end
|
||||
end
|
||||
|
||||
-- update percentage
|
||||
if args.show_percent then
|
||||
e.window.setCursorPos(bar_width + 2, math.max(1, math.ceil(e.frame.h / 2)))
|
||||
e.window.write(util.sprintf("%3.0f%%", fraction * 100))
|
||||
e.w_set_cur(bar_width + 2, math.max(1, math.ceil(e.frame.h / 2)))
|
||||
e.w_write(util.sprintf("%3.0f%%", fraction * 100))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -105,22 +106,21 @@ local function hbar(args)
|
||||
function e.recolor(bar_fg_bg)
|
||||
bar_bkg = bar_fg_bg.blit_bkg
|
||||
bar_fgd = bar_fg_bg.blit_fgd
|
||||
|
||||
-- re-draw
|
||||
last_num_bars = 0
|
||||
if type(e.value) == "number" then
|
||||
e.on_update(e.value)
|
||||
end
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- set the percentage value
|
||||
---@param val number 0.0 to 1.0
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initialize to 0
|
||||
e.on_update(0)
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
last_num_bars = -1
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
---@class HorizontalBar:graphics_element
|
||||
local HorizontalBar, id = e.complete(true)
|
||||
|
||||
return HorizontalBar, id
|
||||
end
|
||||
|
||||
return hbar
|
||||
@@ -1,7 +1,5 @@
|
||||
-- Icon Indicator Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class icon_sym_color
|
||||
@@ -11,7 +9,7 @@ local element = require("graphics.element")
|
||||
---@class icon_indicator_args
|
||||
---@field label string indicator label
|
||||
---@field states table state color and symbol table
|
||||
---@field value? integer default state, defaults to 1
|
||||
---@field value? integer|boolean default state, defaults to 1 (true = 2, false = 1)
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
@@ -20,22 +18,22 @@ 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)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.icon: label is a required field")
|
||||
assert(type(args.states) == "table", "graphics.elements.indicators.icon: states is a required field")
|
||||
---@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")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = args.value or 1
|
||||
if e.value == true then e.value = 2 end
|
||||
|
||||
-- state blit strings
|
||||
local state_blit_cmds = {}
|
||||
@@ -44,32 +42,37 @@ local function icon(args)
|
||||
|
||||
table.insert(state_blit_cmds, {
|
||||
text = " " .. sym_color.symbol .. " ",
|
||||
fgd = util.strrep(sym_color.color.blit_fgd, 3),
|
||||
bkg = util.strrep(sym_color.color.blit_bkg, 3)
|
||||
fgd = string.rep(sym_color.color.blit_fgd, 3),
|
||||
bkg = string.rep(sym_color.color.blit_bkg, 3)
|
||||
})
|
||||
end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.window.setCursorPos(5, 1)
|
||||
e.window.write(args.label)
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
---@param new_state integer|boolean indicator state
|
||||
function e.on_update(new_state)
|
||||
new_state = new_state or 1
|
||||
if new_state == true then new_state = 2 end
|
||||
|
||||
local blit_cmd = state_blit_cmds[new_state]
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val integer indicator state
|
||||
---@param val integer|boolean indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial icon draw
|
||||
e.on_update(args.value or 1)
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
e.w_set_cur(5, 1)
|
||||
e.w_write(args.label)
|
||||
|
||||
return e.complete()
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
---@class IconIndicator:graphics_element
|
||||
local IconIndicator, id = e.complete(true)
|
||||
|
||||
return IconIndicator, id
|
||||
end
|
||||
|
||||
return icon
|
||||
@@ -18,38 +18,36 @@ 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)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.light: label is a required field")
|
||||
assert(type(args.colors) == "table", "graphics.elements.indicators.light: colors is a required field")
|
||||
---@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")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.light: period is a required field if flash is enabled")
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
local flash_on = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = false
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
e.window.blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
@@ -61,8 +59,8 @@ local function indicator_light(args)
|
||||
flash_on = true
|
||||
flasher.start(flash_callback, args.period)
|
||||
else
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -73,8 +71,8 @@ local function indicator_light(args)
|
||||
flasher.stop(flash_callback)
|
||||
end
|
||||
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
-- on state change
|
||||
@@ -88,12 +86,15 @@ local function indicator_light(args)
|
||||
---@param val boolean indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(false)
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(false)
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
---@class IndicatorLight:graphics_element
|
||||
local IndicatorLight, id = e.complete(true)
|
||||
|
||||
return IndicatorLight, id
|
||||
end
|
||||
|
||||
return indicator_light
|
||||
@@ -18,38 +18,36 @@ 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)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.led: label is a required field")
|
||||
assert(type(args.colors) == "table", "graphics.elements.indicators.led: colors is a required field")
|
||||
---@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")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.led: period is a required field if flash is enabled")
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
local flash_on = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = false
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
@@ -61,8 +59,8 @@ local function indicator_led(args)
|
||||
flash_on = true
|
||||
flasher.start(flash_callback, args.period)
|
||||
else
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -73,8 +71,8 @@ local function indicator_led(args)
|
||||
flasher.stop(flash_callback)
|
||||
end
|
||||
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
-- on state change
|
||||
@@ -88,14 +86,17 @@ local function indicator_led(args)
|
||||
---@param val boolean indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(false)
|
||||
if string.len(args.label) > 0 then
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(e.value)
|
||||
if string.len(args.label) > 0 then
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
---@class LED:graphics_element
|
||||
local LED, id = e.complete(true)
|
||||
|
||||
return indicator_led
|
||||
return LED, id
|
||||
end
|
||||
@@ -20,52 +20,47 @@ 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)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.ledpair: label is a required field")
|
||||
assert(type(args.off) == "number", "graphics.elements.indicators.ledpair: off is a required field")
|
||||
assert(type(args.c1) == "number", "graphics.elements.indicators.ledpair: c1 is a required field")
|
||||
assert(type(args.c2) == "number", "graphics.elements.indicators.ledpair: c2 is a required field")
|
||||
---@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")
|
||||
element.assert(type(args.c2) == "number", "c2 is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.ledpair: period is a required field if flash is enabled")
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
local flash_on = true
|
||||
|
||||
-- blit translations
|
||||
local co = colors.toBlit(args.off)
|
||||
local c1 = colors.toBlit(args.c1)
|
||||
local c2 = colors.toBlit(args.c2)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- init value for initial check in on_update
|
||||
e.value = 1
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
if e.value == 2 then
|
||||
e.window.blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
elseif e.value == 3 then
|
||||
e.window.blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
@@ -77,7 +72,7 @@ local function indicator_led_pair(args)
|
||||
local was_off = e.value <= 1
|
||||
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if args.flash then
|
||||
if was_off and (new_state > 1) then
|
||||
@@ -86,15 +81,14 @@ local function indicator_led_pair(args)
|
||||
elseif new_state <= 1 then
|
||||
flash_on = false
|
||||
flasher.stop(flash_callback)
|
||||
|
||||
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
elseif new_state == 2 then
|
||||
e.window.blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
elseif new_state == 3 then
|
||||
e.window.blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -102,14 +96,17 @@ local function indicator_led_pair(args)
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(1)
|
||||
if string.len(args.label) > 0 then
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(e.value)
|
||||
if string.len(args.label) > 0 then
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
---@class LEDPair:graphics_element
|
||||
local LEDPair, id = e.complete(true)
|
||||
|
||||
return indicator_led_pair
|
||||
return LEDPair, id
|
||||
end
|
||||
89
graphics/elements/indicators/PowerIndicator.lua
Normal file
89
graphics/elements/indicators/PowerIndicator.lua
Normal file
@@ -0,0 +1,89 @@
|
||||
-- Power Indicator Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
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)
|
||||
---@field value number default value
|
||||
---@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 length
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- Create a new power indicator. Variant of a data indicator with dynamic energy units.
|
||||
---@nodiscard
|
||||
---@param args power_indicator_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 --[[@as graphics_args]])
|
||||
|
||||
e.value = args.value
|
||||
|
||||
local data_start = 0
|
||||
|
||||
-- on state change
|
||||
---@param value any new value
|
||||
function e.on_update(value)
|
||||
e.value = value
|
||||
|
||||
local data_str, unit = util.power_format(value, args.unit, false, args.format)
|
||||
|
||||
-- write data
|
||||
e.w_set_cur(data_start, 1)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_write(util.comma_format(data_str))
|
||||
|
||||
-- write unit
|
||||
if args.lu_colors ~= nil then
|
||||
e.w_set_fgd(args.lu_colors.color_b)
|
||||
end
|
||||
|
||||
-- append per tick if rate is set
|
||||
if args.rate == true then
|
||||
unit = unit .. "/t"
|
||||
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
|
||||
|
||||
-- set the value
|
||||
---@param val any new value
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(args.label)
|
||||
|
||||
data_start = string.len(args.label) + 2
|
||||
if string.len(args.label) == 0 then data_start = 1 end
|
||||
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
---@class PowerIndicator:graphics_element
|
||||
local PowerIndicator, id = e.complete(true)
|
||||
|
||||
return PowerIndicator, id
|
||||
end
|
||||
@@ -13,33 +13,29 @@ 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)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.ledrgb: label is a required field")
|
||||
assert(type(args.colors) == "table", "graphics.elements.indicators.ledrgb: colors is a required field")
|
||||
---@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")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- init value for initial check in on_update
|
||||
e.value = 1
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
if type(args.colors[new_state]) == "number" then
|
||||
e.window.blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,14 +43,17 @@ local function indicator_led_rgb(args)
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(1)
|
||||
if string.len(args.label) > 0 then
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(e.value)
|
||||
if string.len(args.label) > 0 then
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
---@class RGBLED:graphics_element
|
||||
local RGBLED, id = e.complete(true)
|
||||
|
||||
return indicator_led_rgb
|
||||
return RGBLED, id
|
||||
end
|
||||
@@ -10,7 +10,7 @@ local element = require("graphics.element")
|
||||
---@field format string data format (lua string format)
|
||||
---@field commas? boolean whether to use commas if a number is given (default to false)
|
||||
---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
|
||||
---@field value any default value
|
||||
---@field value? radiation_reading default value
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
@@ -19,29 +19,21 @@ 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)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.rad: label is a required field")
|
||||
assert(type(args.format) == "string", "graphics.elements.indicators.rad: format is a required field")
|
||||
assert(util.is_int(args.width), "graphics.elements.indicators.rad: width is a required field")
|
||||
---@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")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
-- label color
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_a)
|
||||
end
|
||||
|
||||
-- write label
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write(args.label)
|
||||
e.value = args.value or types.new_zero_radiation_reading()
|
||||
|
||||
local label_len = string.len(args.label)
|
||||
local data_start = 1
|
||||
@@ -58,34 +50,41 @@ local function rad(args)
|
||||
e.value = value.radiation
|
||||
|
||||
-- clear old data and label
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.write(util.spaces(clear_width))
|
||||
e.w_set_cur(data_start, 1)
|
||||
e.w_write(util.spaces(clear_width))
|
||||
|
||||
-- write data
|
||||
local data_str = util.sprintf(args.format, e.value)
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.w_set_cur(data_start, 1)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
if args.commas then
|
||||
e.window.write(util.comma_format(data_str))
|
||||
e.w_write(util.comma_format(data_str))
|
||||
else
|
||||
e.window.write(data_str)
|
||||
e.w_write(data_str)
|
||||
end
|
||||
|
||||
-- write unit
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_b)
|
||||
e.w_set_fgd(args.lu_colors.color_b)
|
||||
end
|
||||
e.window.write(" " .. value.unit)
|
||||
e.w_write(" " .. value.unit)
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val any new value
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial value draw
|
||||
e.on_update(types.new_zero_radiation_reading())
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(args.label)
|
||||
|
||||
return e.complete()
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
---@class RadIndicator:graphics_element
|
||||
local RadIndicator, id = e.complete(true)
|
||||
|
||||
return RadIndicator, id
|
||||
end
|
||||
|
||||
return rad
|
||||
83
graphics/elements/indicators/SignalBar.lua
Normal file
83
graphics/elements/indicators/SignalBar.lua
Normal file
@@ -0,0 +1,83 @@
|
||||
-- Signal Bars Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class signal_bar_args
|
||||
---@field compact? boolean true to use a single character (works better against edges that extend out colors)
|
||||
---@field colors_low_med? cpair color a for low signal quality, color b for medium signal quality
|
||||
---@field disconnect_color? color color for the 'x' on disconnect
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors (foreground is used for high signal quality)
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- Create a new signal bar indicator element.
|
||||
---@nodiscard
|
||||
---@param args 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 --[[@as graphics_args]])
|
||||
|
||||
e.value = 0
|
||||
|
||||
local blit_bkg = args.fg_bg.blit_bkg
|
||||
local blit_0, blit_1, blit_2, blit_3 = args.fg_bg.blit_fgd, args.fg_bg.blit_fgd, args.fg_bg.blit_fgd, args.fg_bg.blit_fgd
|
||||
|
||||
if type(args.colors_low_med) == "table" then
|
||||
blit_1 = args.colors_low_med.blit_a or blit_1
|
||||
blit_2 = args.colors_low_med.blit_b or blit_2
|
||||
end
|
||||
|
||||
if util.is_int(args.disconnect_color) then blit_0 = colors.toBlit(args.disconnect_color) end
|
||||
|
||||
-- on state change (0 = offline, 1 through 3 = low to high signal)
|
||||
---@param new_state integer signal state
|
||||
function e.on_update(new_state)
|
||||
e.value = new_state
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- set signal state (0 = offline, 1 through 3 = low to high signal)
|
||||
---@param val integer signal state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- draw label and signal bar
|
||||
function e.redraw()
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if args.compact then
|
||||
if e.value == 1 then
|
||||
e.w_blit("\x90", blit_1, blit_bkg)
|
||||
elseif e.value == 2 then
|
||||
e.w_blit("\x94", blit_2, blit_bkg)
|
||||
elseif e.value == 3 then
|
||||
e.w_blit("\x95", blit_3, blit_bkg)
|
||||
else
|
||||
e.w_blit("x", blit_0, blit_bkg)
|
||||
end
|
||||
else
|
||||
if e.value == 1 then
|
||||
e.w_blit("\x9f ", blit_bkg .. blit_bkg, blit_1 .. blit_bkg)
|
||||
elseif e.value == 2 then
|
||||
e.w_blit("\x9f\x94", blit_bkg .. blit_2, blit_2 .. blit_bkg)
|
||||
elseif e.value == 3 then
|
||||
e.w_blit("\x9f\x81", blit_bkg .. blit_bkg, blit_3 .. blit_3)
|
||||
else
|
||||
e.w_blit(" x", blit_0 .. blit_0, blit_bkg .. blit_bkg)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@class SignalBar:graphics_element
|
||||
local SignalBar, id = e.complete(true)
|
||||
|
||||
return SignalBar, id
|
||||
end
|
||||
@@ -20,21 +20,17 @@ 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)
|
||||
assert(type(args.states) == "table", "graphics.elements.indicators.state: states is a required field")
|
||||
---@return StateIndicator element, element_id id
|
||||
return function (args)
|
||||
element.assert(type(args.states) == "table", "states is a required field")
|
||||
|
||||
-- determine height
|
||||
if util.is_int(args.height) then
|
||||
assert(args.height % 2 == 1, "graphics.elements.indicators.state: height should be an odd number")
|
||||
else
|
||||
args.height = 1
|
||||
end
|
||||
element.assert(args.height % 2 == 1, "height should be an odd number")
|
||||
else args.height = 1 end
|
||||
|
||||
-- initial guess at width
|
||||
args.width = args.min_width or 1
|
||||
|
||||
-- state blit strings
|
||||
@@ -42,7 +38,6 @@ local function state_indicator(args)
|
||||
for i = 1, #args.states do
|
||||
local state_def = args.states[i] ---@type state_text_color
|
||||
|
||||
-- re-determine width
|
||||
if string.len(state_def.text) > args.width then
|
||||
args.width = string.len(state_def.text)
|
||||
end
|
||||
@@ -51,31 +46,36 @@ local function state_indicator(args)
|
||||
|
||||
table.insert(state_blit_cmds, {
|
||||
text = text,
|
||||
fgd = util.strrep(state_def.color.blit_fgd, string.len(text)),
|
||||
bkg = util.strrep(state_def.color.blit_bkg, string.len(text))
|
||||
fgd = string.rep(state_def.color.blit_fgd, string.len(text)),
|
||||
bkg = string.rep(state_def.color.blit_bkg, string.len(text))
|
||||
})
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = args.value or 1
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
local blit_cmd = state_blit_cmds[e.value]
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
local blit_cmd = state_blit_cmds[new_state]
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial draw
|
||||
e.on_update(args.value or 1)
|
||||
---@class StateIndicator:graphics_element
|
||||
local StateIndicator, id = e.complete(true)
|
||||
|
||||
return e.complete()
|
||||
return StateIndicator, id
|
||||
end
|
||||
|
||||
return state_indicator
|
||||
@@ -20,52 +20,46 @@ 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)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.trilight: label is a required field")
|
||||
assert(type(args.c1) == "number", "graphics.elements.indicators.trilight: c1 is a required field")
|
||||
assert(type(args.c2) == "number", "graphics.elements.indicators.trilight: c2 is a required field")
|
||||
assert(type(args.c3) == "number", "graphics.elements.indicators.trilight: c3 is a required field")
|
||||
---@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")
|
||||
element.assert(type(args.c3) == "number", "c3 is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.trilight: period is a required field if flash is enabled")
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = 1
|
||||
|
||||
local flash_on = true
|
||||
|
||||
-- blit translations
|
||||
local c1 = colors.toBlit(args.c1)
|
||||
local c2 = colors.toBlit(args.c2)
|
||||
local c3 = colors.toBlit(args.c3)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- init value for initial check in on_update
|
||||
e.value = 1
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
if e.value == 2 then
|
||||
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
elseif e.value == 3 then
|
||||
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
@@ -77,7 +71,7 @@ local function tristate_indicator_light(args)
|
||||
local was_off = e.value <= 1
|
||||
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if args.flash then
|
||||
if was_off and (new_state > 1) then
|
||||
@@ -87,14 +81,14 @@ local function tristate_indicator_light(args)
|
||||
flash_on = false
|
||||
flasher.stop(flash_callback)
|
||||
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
elseif new_state == 2 then
|
||||
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
elseif new_state == 3 then
|
||||
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -102,11 +96,14 @@ local function tristate_indicator_light(args)
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(1)
|
||||
e.window.write(args.label)
|
||||
-- draw light and label
|
||||
function e.redraw()
|
||||
e.on_update(1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
---@class TriIndicatorLight:graphics_element
|
||||
local TriIndicatorLight, id = e.complete(true)
|
||||
|
||||
return TriIndicatorLight, id
|
||||
end
|
||||
|
||||
return tristate_indicator_light
|
||||
@@ -15,23 +15,23 @@ 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)
|
||||
-- properties/state
|
||||
---@return VerticalBar element, element_id id
|
||||
return function (args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args --[[@as graphics_args]])
|
||||
|
||||
e.value = 0.0
|
||||
|
||||
local last_num_bars = -1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- blit strings
|
||||
local fgd = util.strrep(e.fg_bg.blit_fgd, e.frame.w)
|
||||
local bkg = util.strrep(e.fg_bg.blit_bkg, e.frame.w)
|
||||
local fgd = string.rep(e.fg_bg.blit_fgd, e.frame.w)
|
||||
local bkg = string.rep(e.fg_bg.blit_bkg, e.frame.w)
|
||||
local spaces = util.spaces(e.frame.w)
|
||||
local one_third = util.strrep("\x8f", e.frame.w)
|
||||
local two_thirds = util.strrep("\x83", e.frame.w)
|
||||
local one_third = string.rep("\x8f", e.frame.w)
|
||||
local two_thirds = string.rep("\x83", e.frame.w)
|
||||
|
||||
-- handle data changes
|
||||
---@param fraction number 0.0 to 1.0
|
||||
@@ -52,55 +52,54 @@ local function vbar(args)
|
||||
if num_bars ~= last_num_bars then
|
||||
last_num_bars = num_bars
|
||||
|
||||
-- start bottom up
|
||||
local y = e.frame.h
|
||||
|
||||
-- start at base of vertical bar
|
||||
e.window.setCursorPos(1, y)
|
||||
e.w_set_cur(1, y)
|
||||
|
||||
-- fill percentage
|
||||
for _ = 1, num_bars / 3 do
|
||||
e.window.blit(spaces, bkg, fgd)
|
||||
e.w_blit(spaces, bkg, fgd)
|
||||
y = y - 1
|
||||
e.window.setCursorPos(1, y)
|
||||
e.w_set_cur(1, y)
|
||||
end
|
||||
|
||||
-- add fractional bar if needed
|
||||
if num_bars % 3 == 1 then
|
||||
e.window.blit(one_third, bkg, fgd)
|
||||
e.w_blit(one_third, bkg, fgd)
|
||||
y = y - 1
|
||||
elseif num_bars % 3 == 2 then
|
||||
e.window.blit(two_thirds, bkg, fgd)
|
||||
e.w_blit(two_thirds, bkg, fgd)
|
||||
y = y - 1
|
||||
end
|
||||
|
||||
-- fill the rest blank
|
||||
while y > 0 do
|
||||
e.window.setCursorPos(1, y)
|
||||
e.window.blit(spaces, fgd, bkg)
|
||||
e.w_set_cur(1, y)
|
||||
e.w_blit(spaces, fgd, bkg)
|
||||
y = y - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- change bar color
|
||||
---@param fg_bg cpair new bar colors
|
||||
function e.recolor(fg_bg)
|
||||
fgd = util.strrep(fg_bg.blit_fgd, e.frame.w)
|
||||
bkg = util.strrep(fg_bg.blit_bkg, e.frame.w)
|
||||
|
||||
-- re-draw
|
||||
last_num_bars = 0
|
||||
if type(e.value) == "number" then
|
||||
e.on_update(e.value)
|
||||
end
|
||||
end
|
||||
|
||||
-- set the percentage value
|
||||
---@param val number 0.0 to 1.0
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
last_num_bars = -1
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
return vbar
|
||||
-- change bar color
|
||||
---@param fg_bg cpair new bar colors
|
||||
function e.recolor(fg_bg)
|
||||
fgd = string.rep(fg_bg.blit_fgd, e.frame.w)
|
||||
bkg = string.rep(fg_bg.blit_bkg, e.frame.w)
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
---@class VerticalBar:graphics_element
|
||||
local VerticalBar, id = e.complete(true)
|
||||
|
||||
return VerticalBar, id
|
||||
end
|
||||
@@ -1,86 +0,0 @@
|
||||
-- Power Indicator Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class power_indicator_args
|
||||
---@field label string indicator label
|
||||
---@field format string power format override (lua string format)
|
||||
---@field rate boolean? whether to append /t to the end (power per tick)
|
||||
---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
|
||||
---@field value any default value
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width integer length
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new power indicator
|
||||
---@nodiscard
|
||||
---@param args power_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function power(args)
|
||||
assert(args.value ~= nil, "graphics.elements.indicators.power: value is a required field")
|
||||
assert(util.is_int(args.width), "graphics.elements.indicators.power: width is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- label color
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_a)
|
||||
end
|
||||
|
||||
-- write label
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write(args.label)
|
||||
|
||||
local data_start = string.len(args.label) + 2
|
||||
if string.len(args.label) == 0 then data_start = 1 end
|
||||
|
||||
-- on state change
|
||||
---@param value any new value
|
||||
function e.on_update(value)
|
||||
e.value = value
|
||||
|
||||
local data_str, unit = util.power_format(value, false, args.format)
|
||||
|
||||
-- write data
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.write(util.comma_format(data_str))
|
||||
|
||||
-- write unit
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_b)
|
||||
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
|
||||
|
||||
e.window.write(" " .. unit)
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val any new value
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial value draw
|
||||
e.on_update(args.value)
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return power
|
||||
@@ -1,148 +0,0 @@
|
||||
-- Pipe Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class pipenet_args
|
||||
---@field pipes table pipe list
|
||||
---@field bg? color background color
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new pipe network
|
||||
---@param args pipenet_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function pipenet(args)
|
||||
assert(type(args.pipes) == "table", "graphics.elements.indicators.pipenet: pipes is a required field")
|
||||
|
||||
args.width = 0
|
||||
args.height = 0
|
||||
|
||||
-- determine width/height
|
||||
for i = 1, #args.pipes do
|
||||
local pipe = args.pipes[i] ---@type pipe
|
||||
|
||||
local true_w = pipe.w + math.min(pipe.x1, pipe.x2)
|
||||
local true_h = pipe.h + math.min(pipe.y1, pipe.y2)
|
||||
|
||||
if true_w > args.width then args.width = true_w end
|
||||
if true_h > args.height then args.height = true_h end
|
||||
end
|
||||
|
||||
args.x = args.x or 1
|
||||
args.y = args.y or 1
|
||||
|
||||
if args.bg ~= nil then
|
||||
args.fg_bg = core.cpair(args.bg, args.bg)
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- draw all pipes
|
||||
for p = 1, #args.pipes do
|
||||
local pipe = args.pipes[p] ---@type pipe
|
||||
|
||||
local x = 1 + pipe.x1
|
||||
local y = 1 + pipe.y1
|
||||
|
||||
local x_step = util.trinary(pipe.x1 >= pipe.x2, -1, 1)
|
||||
local y_step = util.trinary(pipe.y1 >= pipe.y2, -1, 1)
|
||||
|
||||
e.window.setCursorPos(x, y)
|
||||
|
||||
local c = core.cpair(pipe.color, e.fg_bg.bkg)
|
||||
|
||||
if pipe.align_tr then
|
||||
-- cross width then height
|
||||
for i = 1, pipe.w do
|
||||
if pipe.thin then
|
||||
if i == pipe.w then
|
||||
-- corner
|
||||
if y_step > 0 then
|
||||
e.window.blit("\x93", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit("\x8e", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
if i == pipe.w and y_step > 0 then
|
||||
-- corner
|
||||
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit("\x8f", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
x = x + x_step
|
||||
e.window.setCursorPos(x, y)
|
||||
end
|
||||
|
||||
-- back up one
|
||||
x = x - x_step
|
||||
|
||||
for _ = 1, pipe.h - 1 do
|
||||
y = y + y_step
|
||||
e.window.setCursorPos(x, y)
|
||||
|
||||
if pipe.thin then
|
||||
e.window.blit("\x95", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- cross height then width
|
||||
for i = 1, pipe.h do
|
||||
if pipe.thin then
|
||||
if i == pipe.h then
|
||||
-- corner
|
||||
if y_step < 0 then
|
||||
e.window.blit("\x97", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit("\x8d", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.window.blit("\x95", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
if i == pipe.h and y_step < 0 then
|
||||
-- corner
|
||||
e.window.blit("\x83", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
end
|
||||
|
||||
y = y + y_step
|
||||
e.window.setCursorPos(x, y)
|
||||
end
|
||||
|
||||
-- back up one
|
||||
y = y - y_step
|
||||
|
||||
for _ = 1, pipe.w - 1 do
|
||||
x = x + x_step
|
||||
e.window.setCursorPos(x, y)
|
||||
|
||||
if pipe.thin then
|
||||
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
else
|
||||
e.window.blit("\x83", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return pipenet
|
||||
@@ -1,192 +0,0 @@
|
||||
-- Rectangle Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class rectangle_args
|
||||
---@field border? graphics_border
|
||||
---@field thin? boolean true to use extra thin even borders
|
||||
---@field even_inner? boolean true to make the inner area of a border even
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new rectangle
|
||||
---@param args rectangle_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function rectangle(args)
|
||||
assert(args.border ~= nil or args.thin ~= true, "graphics.elements.rectangle: thin requires border to be provided")
|
||||
|
||||
-- if thin, then width will always need to be 1
|
||||
if args.thin == true then
|
||||
args.border.width = 1
|
||||
args.border.even = true
|
||||
end
|
||||
|
||||
-- offset children
|
||||
local offset_x = 0
|
||||
local offset_y = 0
|
||||
if args.border ~= nil then
|
||||
offset_x = args.border.width
|
||||
offset_y = args.border.width
|
||||
|
||||
-- slightly different y offset if the border is set to even
|
||||
if args.border.even then
|
||||
local width_x2 = (2 * args.border.width)
|
||||
offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args, offset_x, offset_y)
|
||||
|
||||
-- create content window for child elements
|
||||
e.content_window = window.create(e.window, 1 + offset_x, 1 + offset_y, e.frame.w - (2 * offset_x), e.frame.h - (2 * offset_y))
|
||||
e.content_window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.content_window.setTextColor(e.fg_bg.fgd)
|
||||
e.content_window.clear()
|
||||
|
||||
-- draw bordered box if requested
|
||||
-- element constructor will have drawn basic colored rectangle regardless
|
||||
if args.border ~= nil then
|
||||
e.window.setCursorPos(1, 1)
|
||||
|
||||
local border_width = offset_x
|
||||
local border_height = offset_y
|
||||
local border_blit = colors.toBlit(args.border.color)
|
||||
local width_x2 = border_width * 2
|
||||
local inner_width = e.frame.w - width_x2
|
||||
|
||||
-- check dimensions
|
||||
assert(width_x2 <= e.frame.w, "graphics.elements.rectangle: border too thick for width")
|
||||
assert(width_x2 <= e.frame.h, "graphics.elements.rectangle: border too thick for height")
|
||||
|
||||
-- form the basic line strings and top/bottom blit strings
|
||||
local spaces = util.spaces(e.frame.w)
|
||||
local blit_fg = util.strrep(e.fg_bg.blit_fgd, e.frame.w)
|
||||
local blit_fg_sides = blit_fg
|
||||
local blit_bg_sides = ""
|
||||
local blit_bg_top_bot = util.strrep(border_blit, e.frame.w)
|
||||
|
||||
-- partial bars
|
||||
local p_a, p_b, p_s
|
||||
if args.thin == true then
|
||||
if args.even_inner == true then
|
||||
p_a = "\x9c" .. util.strrep("\x8c", inner_width) .. "\x93"
|
||||
p_b = "\x8d" .. util.strrep("\x8c", inner_width) .. "\x8e"
|
||||
else
|
||||
p_a = "\x97" .. util.strrep("\x83", inner_width) .. "\x94"
|
||||
p_b = "\x8a" .. util.strrep("\x8f", inner_width) .. "\x85"
|
||||
end
|
||||
|
||||
p_s = "\x95" .. util.spaces(inner_width) .. "\x95"
|
||||
else
|
||||
if args.even_inner == true then
|
||||
p_a = util.strrep("\x83", inner_width + width_x2)
|
||||
p_b = util.strrep("\x8f", inner_width + width_x2)
|
||||
else
|
||||
p_a = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width)
|
||||
p_b = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width)
|
||||
end
|
||||
|
||||
p_s = spaces
|
||||
end
|
||||
|
||||
local p_inv_fg = util.strrep(border_blit, border_width) .. util.strrep(e.fg_bg.blit_bkg, inner_width) ..
|
||||
util.strrep(border_blit, border_width)
|
||||
local p_inv_bg = util.strrep(e.fg_bg.blit_bkg, border_width) .. util.strrep(border_blit, inner_width) ..
|
||||
util.strrep(e.fg_bg.blit_bkg, border_width)
|
||||
|
||||
if args.thin == true then
|
||||
p_inv_fg = e.fg_bg.blit_bkg .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. util.strrep(border_blit, border_width)
|
||||
p_inv_bg = border_blit .. util.strrep(border_blit, inner_width) .. util.strrep(e.fg_bg.blit_bkg, border_width)
|
||||
|
||||
blit_fg_sides = border_blit .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. e.fg_bg.blit_bkg
|
||||
end
|
||||
|
||||
-- form the body blit strings (sides are border, inside is normal)
|
||||
for x = 1, e.frame.w do
|
||||
-- edges get border color, center gets normal
|
||||
if x <= border_width or x > (e.frame.w - border_width) then
|
||||
if args.thin and x == 1 then
|
||||
blit_bg_sides = blit_bg_sides .. e.fg_bg.blit_bkg
|
||||
else
|
||||
blit_bg_sides = blit_bg_sides .. border_blit
|
||||
end
|
||||
else
|
||||
blit_bg_sides = blit_bg_sides .. e.fg_bg.blit_bkg
|
||||
end
|
||||
end
|
||||
|
||||
-- draw rectangle with borders
|
||||
for y = 1, e.frame.h do
|
||||
e.window.setCursorPos(1, y)
|
||||
-- top border
|
||||
if y <= border_height then
|
||||
-- partial pixel fill
|
||||
if args.border.even and y == border_height then
|
||||
if args.thin == true then
|
||||
e.window.blit(p_a, p_inv_bg, p_inv_fg)
|
||||
else
|
||||
local _fg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), p_inv_bg)
|
||||
local _bg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
|
||||
|
||||
if width_x2 % 3 == 1 then
|
||||
e.window.blit(p_b, _fg, _bg)
|
||||
elseif width_x2 % 3 == 2 then
|
||||
e.window.blit(p_a, _fg, _bg)
|
||||
else
|
||||
-- skip line
|
||||
e.window.blit(spaces, blit_fg, blit_bg_sides)
|
||||
end
|
||||
end
|
||||
else
|
||||
e.window.blit(spaces, blit_fg, blit_bg_top_bot)
|
||||
end
|
||||
-- bottom border
|
||||
elseif y > (e.frame.h - border_width) then
|
||||
-- partial pixel fill
|
||||
if args.border.even and y == ((e.frame.h - border_width) + 1) then
|
||||
if args.thin == true then
|
||||
if args.even_inner == true then
|
||||
e.window.blit(p_b, blit_bg_top_bot, util.strrep(e.fg_bg.blit_bkg, e.frame.w))
|
||||
else
|
||||
e.window.blit(p_b, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
|
||||
end
|
||||
else
|
||||
local _fg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
|
||||
local _bg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
|
||||
|
||||
if width_x2 % 3 == 1 then
|
||||
e.window.blit(p_a, _fg, _bg)
|
||||
elseif width_x2 % 3 == 2 then
|
||||
e.window.blit(p_b, _fg, _bg)
|
||||
else
|
||||
-- skip line
|
||||
e.window.blit(spaces, blit_fg, blit_bg_sides)
|
||||
end
|
||||
end
|
||||
else
|
||||
e.window.blit(spaces, blit_fg, blit_bg_top_bot)
|
||||
end
|
||||
else
|
||||
if args.thin == true then
|
||||
e.window.blit(p_s, blit_fg_sides, blit_bg_sides)
|
||||
else
|
||||
e.window.blit(p_s, blit_fg, blit_bg_sides)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return rectangle
|
||||
@@ -1,71 +0,0 @@
|
||||
-- Text Box Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
---@class textbox_args
|
||||
---@field text string text to show
|
||||
---@field alignment? TEXT_ALIGN text alignment, left by default
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new text box
|
||||
---@param args textbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function textbox(args)
|
||||
assert(type(args.text) == "string", "graphics.elements.textbox: text is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local alignment = args.alignment or TEXT_ALIGN.LEFT
|
||||
|
||||
-- draw textbox
|
||||
|
||||
local function display_text(text)
|
||||
e.value = text
|
||||
|
||||
local lines = util.strwrap(text, e.frame.w)
|
||||
|
||||
for i = 1, #lines do
|
||||
if i > e.frame.h then break end
|
||||
|
||||
local len = string.len(lines[i])
|
||||
|
||||
-- use cursor position to align this line
|
||||
if alignment == TEXT_ALIGN.CENTER then
|
||||
e.window.setCursorPos(math.floor((e.frame.w - len) / 2) + 1, i)
|
||||
elseif alignment == TEXT_ALIGN.RIGHT then
|
||||
e.window.setCursorPos((e.frame.w - len) + 1, i)
|
||||
else
|
||||
e.window.setCursorPos(1, i)
|
||||
end
|
||||
|
||||
e.window.write(lines[i])
|
||||
end
|
||||
end
|
||||
|
||||
display_text(args.text)
|
||||
|
||||
-- set the string value and re-draw the text
|
||||
---@param val string value
|
||||
function e.set_value(val)
|
||||
e.window.clear()
|
||||
display_text(val)
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return textbox
|
||||
@@ -1,88 +0,0 @@
|
||||
-- "Basketweave" Tiling Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class tiling_args
|
||||
---@field fill_c cpair colors to fill with
|
||||
---@field even? boolean whether to account for rectangular pixels
|
||||
---@field border_c? color optional frame color
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new tiling box
|
||||
---@param args tiling_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function tiling(args)
|
||||
assert(type(args.fill_c) == "table", "graphics.elements.tiling: fill_c is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- draw tiling box
|
||||
|
||||
local fill_a = args.fill_c.blit_a
|
||||
local fill_b = args.fill_c.blit_b
|
||||
|
||||
local even = args.even == true
|
||||
|
||||
local start_x = 1
|
||||
local start_y = 1
|
||||
local inner_width = math.floor(e.frame.w / util.trinary(even, 2, 1))
|
||||
local inner_height = e.frame.h
|
||||
local alternator = true
|
||||
|
||||
-- border
|
||||
if args.border_c ~= nil then
|
||||
e.window.setBackgroundColor(args.border_c)
|
||||
e.window.clear()
|
||||
|
||||
start_x = 1 + util.trinary(even, 2, 1)
|
||||
start_y = 2
|
||||
|
||||
inner_width = math.floor((e.frame.w - 2 * util.trinary(even, 2, 1)) / util.trinary(even, 2, 1))
|
||||
inner_height = e.frame.h - 2
|
||||
end
|
||||
|
||||
-- check dimensions
|
||||
assert(inner_width > 0, "graphics.elements.tiling: inner_width <= 0")
|
||||
assert(inner_height > 0, "graphics.elements.tiling: inner_height <= 0")
|
||||
assert(start_x <= inner_width, "graphics.elements.tiling: start_x > inner_width")
|
||||
assert(start_y <= inner_height, "graphics.elements.tiling: start_y > inner_height")
|
||||
|
||||
-- create pattern
|
||||
for y = start_y, inner_height + (start_y - 1) do
|
||||
e.window.setCursorPos(start_x, y)
|
||||
for _ = 1, inner_width do
|
||||
if alternator then
|
||||
if even then
|
||||
e.window.blit(" ", "00", fill_a .. fill_a)
|
||||
else
|
||||
e.window.blit(" ", "0", fill_a)
|
||||
end
|
||||
else
|
||||
if even then
|
||||
e.window.blit(" ", "00", fill_b .. fill_b)
|
||||
else
|
||||
e.window.blit(" ", "0", fill_b)
|
||||
end
|
||||
end
|
||||
|
||||
alternator = not alternator
|
||||
end
|
||||
|
||||
if inner_width % 2 == 0 then alternator = not alternator end
|
||||
end
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return tiling
|
||||
@@ -4,46 +4,77 @@
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local DOUBLE_CLICK_MS = 500
|
||||
|
||||
local events = {}
|
||||
|
||||
---@enum CLICK_BUTTON
|
||||
events.CLICK_BUTTON = {
|
||||
local CLICK_BUTTON = {
|
||||
GENERIC = 0,
|
||||
LEFT_BUTTON = 1,
|
||||
RIGHT_BUTTON = 2,
|
||||
MID_BUTTON = 3
|
||||
}
|
||||
|
||||
---@enum CLICK_TYPE
|
||||
events.CLICK_TYPE = {
|
||||
events.CLICK_BUTTON = CLICK_BUTTON
|
||||
|
||||
---@enum MOUSE_CLICK
|
||||
local MOUSE_CLICK = {
|
||||
TAP = 1, -- screen tap (complete click)
|
||||
DOWN = 2, -- button down
|
||||
UP = 3, -- button up (completed a click)
|
||||
DRAG = 4, -- mouse dragged
|
||||
SCROLL_DOWN = 5, -- scroll down
|
||||
SCROLL_UP = 6 -- scroll up
|
||||
SCROLL_UP = 6, -- scroll up
|
||||
DOUBLE_CLICK = 7 -- double left click
|
||||
}
|
||||
|
||||
events.MOUSE_CLICK = MOUSE_CLICK
|
||||
|
||||
---@enum KEY_CLICK
|
||||
local KEY_CLICK = {
|
||||
DOWN = 1,
|
||||
HELD = 2,
|
||||
UP = 3,
|
||||
CHAR = 4
|
||||
}
|
||||
|
||||
events.KEY_CLICK = KEY_CLICK
|
||||
|
||||
-- create a new 2D coordinate
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return coordinate_2d
|
||||
local function _coord2d(x, y) return { x = x, y = y } end
|
||||
|
||||
events.new_coord_2d = _coord2d
|
||||
|
||||
---@class mouse_interaction
|
||||
---@field monitor string
|
||||
---@field button CLICK_BUTTON
|
||||
---@field type CLICK_TYPE
|
||||
---@field type MOUSE_CLICK
|
||||
---@field initial coordinate_2d
|
||||
---@field current coordinate_2d
|
||||
|
||||
---@class key_interaction
|
||||
---@field type KEY_CLICK
|
||||
---@field key number key code
|
||||
---@field name string key character name
|
||||
---@field shift boolean shift held
|
||||
---@field ctrl boolean ctrl held
|
||||
---@field alt boolean alt held
|
||||
|
||||
local handler = {
|
||||
-- left, right, middle button down tracking
|
||||
button_down = {
|
||||
_coord2d(0, 0),
|
||||
_coord2d(0, 0),
|
||||
_coord2d(0, 0)
|
||||
}
|
||||
button_down = { _coord2d(0, 0), _coord2d(0, 0), _coord2d(0, 0) },
|
||||
-- keyboard modifiers
|
||||
shift = false,
|
||||
alt = false,
|
||||
ctrl = false,
|
||||
-- double click tracking
|
||||
dc_start = 0,
|
||||
dc_step = 1,
|
||||
dc_coord = _coord2d(0, 0)
|
||||
}
|
||||
|
||||
-- create a new monitor touch mouse interaction event
|
||||
@@ -55,8 +86,8 @@ local handler = {
|
||||
local function _monitor_touch(monitor, x, y)
|
||||
return {
|
||||
monitor = monitor,
|
||||
button = events.CLICK_BUTTON.GENERIC,
|
||||
type = events.CLICK_TYPE.TAP,
|
||||
button = CLICK_BUTTON.GENERIC,
|
||||
type = MOUSE_CLICK.TAP,
|
||||
initial = _coord2d(x, y),
|
||||
current = _coord2d(x, y)
|
||||
}
|
||||
@@ -65,7 +96,7 @@ end
|
||||
-- create a new mouse button mouse interaction event
|
||||
---@nodiscard
|
||||
---@param button CLICK_BUTTON mouse button
|
||||
---@param type CLICK_TYPE click type
|
||||
---@param type MOUSE_CLICK click type
|
||||
---@param x1 integer initial x
|
||||
---@param y1 integer initial y
|
||||
---@param x2 integer current x
|
||||
@@ -83,14 +114,14 @@ end
|
||||
|
||||
-- create a new generic mouse interaction event
|
||||
---@nodiscard
|
||||
---@param type CLICK_TYPE
|
||||
---@param type MOUSE_CLICK
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return mouse_interaction
|
||||
function events.mouse_generic(type, x, y)
|
||||
return {
|
||||
monitor = "",
|
||||
button = events.CLICK_BUTTON.GENERIC,
|
||||
button = CLICK_BUTTON.GENERIC,
|
||||
type = type,
|
||||
initial = _coord2d(x, y),
|
||||
current = _coord2d(x, y)
|
||||
@@ -115,8 +146,8 @@ end
|
||||
|
||||
-- check if an event qualifies as a click (tap or up)
|
||||
---@nodiscard
|
||||
---@param t CLICK_TYPE
|
||||
function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events.CLICK_TYPE.UP end
|
||||
---@param t MOUSE_CLICK
|
||||
function events.was_clicked(t) return t == MOUSE_CLICK.TAP or t == MOUSE_CLICK.UP end
|
||||
|
||||
-- create a new mouse event to pass onto graphics renderer<br>
|
||||
-- supports: mouse_click, mouse_up, mouse_drag, mouse_scroll, and monitor_touch
|
||||
@@ -126,35 +157,97 @@ function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events.
|
||||
---@param y integer y coordinate
|
||||
---@return mouse_interaction|nil
|
||||
function events.new_mouse_event(event_type, opt, x, y)
|
||||
local h = handler
|
||||
|
||||
if event_type == "mouse_click" then
|
||||
---@cast opt 1|2|3
|
||||
handler.button_down[opt] = _coord2d(x, y)
|
||||
return _mouse_event(opt, events.CLICK_TYPE.DOWN, x, y, x, y)
|
||||
|
||||
local init = true
|
||||
|
||||
if opt == 1 and (h.dc_step % 2) == 1 then
|
||||
if h.dc_step ~= 1 and h.dc_coord.x == x and h.dc_coord.y == y and (util.time_ms() - h.dc_start) < DOUBLE_CLICK_MS then
|
||||
init = false
|
||||
h.dc_step = h.dc_step + 1
|
||||
end
|
||||
end
|
||||
|
||||
if init then
|
||||
h.dc_start = util.time_ms()
|
||||
h.dc_coord = _coord2d(x, y)
|
||||
h.dc_step = 2
|
||||
end
|
||||
|
||||
h.button_down[opt] = _coord2d(x, y)
|
||||
return _mouse_event(opt, MOUSE_CLICK.DOWN, x, y, x, y)
|
||||
elseif event_type == "mouse_up" then
|
||||
---@cast opt 1|2|3
|
||||
local initial = handler.button_down[opt] ---@type coordinate_2d
|
||||
return _mouse_event(opt, events.CLICK_TYPE.UP, initial.x, initial.y, x, y)
|
||||
|
||||
if opt == 1 and (h.dc_step % 2) == 0 and h.dc_coord.x == x and h.dc_coord.y == y and
|
||||
(util.time_ms() - h.dc_start) < DOUBLE_CLICK_MS then
|
||||
if h.dc_step == 4 then
|
||||
util.push_event("double_click", 1, x, y)
|
||||
h.dc_step = 1
|
||||
else h.dc_step = h.dc_step + 1 end
|
||||
else h.dc_step = 1 end
|
||||
|
||||
local initial = h.button_down[opt] ---@type coordinate_2d
|
||||
return _mouse_event(opt, MOUSE_CLICK.UP, initial.x, initial.y, x, y)
|
||||
elseif event_type == "monitor_touch" then
|
||||
---@cast opt string
|
||||
return _monitor_touch(opt, x, y)
|
||||
elseif event_type == "mouse_drag" then
|
||||
---@cast opt 1|2|3
|
||||
local initial = handler.button_down[opt] ---@type coordinate_2d
|
||||
return _mouse_event(opt, events.CLICK_TYPE.DRAG, initial.x, initial.y, x, y)
|
||||
local initial = h.button_down[opt] ---@type coordinate_2d
|
||||
return _mouse_event(opt, MOUSE_CLICK.DRAG, initial.x, initial.y, x, y)
|
||||
elseif event_type == "mouse_scroll" then
|
||||
---@cast opt 1|-1
|
||||
local scroll_direction = util.trinary(opt == 1, events.CLICK_TYPE.SCROLL_DOWN, events.CLICK_TYPE.SCROLL_UP)
|
||||
return _mouse_event(events.CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y)
|
||||
local scroll_direction = util.trinary(opt == 1, MOUSE_CLICK.SCROLL_DOWN, MOUSE_CLICK.SCROLL_UP)
|
||||
return _mouse_event(CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y)
|
||||
elseif event_type == "double_click" then
|
||||
return _mouse_event(CLICK_BUTTON.LEFT_BUTTON, MOUSE_CLICK.DOUBLE_CLICK, x, y, x, y)
|
||||
end
|
||||
end
|
||||
|
||||
-- create a new key event to pass onto graphics renderer<br>
|
||||
-- create a new keyboard interaction event
|
||||
---@nodiscard
|
||||
---@param click_type KEY_CLICK key click type
|
||||
---@param key integer|string keyboard key code or character for 'char' event
|
||||
---@return key_interaction
|
||||
local function _key_event(click_type, key)
|
||||
local name = key
|
||||
if type(key) == "number" then name = keys.getName(key) end
|
||||
return { type = click_type, key = key, name = name, shift = handler.shift, ctrl = handler.ctrl, alt = handler.alt }
|
||||
end
|
||||
|
||||
-- create a new keyboard event to pass onto graphics renderer<br>
|
||||
-- supports: char, key, and key_up
|
||||
---@param event_type os_event
|
||||
function events.new_key_event(event_type)
|
||||
---@param event_type os_event OS event to handle
|
||||
---@param key integer keyboard key code
|
||||
---@param held boolean? if the key is being held (for 'key' event)
|
||||
---@return key_interaction|nil
|
||||
function events.new_key_event(event_type, key, held)
|
||||
if event_type == "char" then
|
||||
return _key_event(KEY_CLICK.CHAR, key)
|
||||
elseif event_type == "key" then
|
||||
if key == keys.leftShift or key == keys.rightShift then
|
||||
handler.shift = true
|
||||
elseif key == keys.leftCtrl or key == keys.rightCtrl then
|
||||
handler.ctrl = true
|
||||
elseif key == keys.leftAlt or key == keys.rightAlt then
|
||||
handler.alt = true
|
||||
else
|
||||
return _key_event(util.trinary(held, KEY_CLICK.HELD, KEY_CLICK.DOWN), key)
|
||||
end
|
||||
elseif event_type == "key_up" then
|
||||
if key == keys.leftShift or key == keys.rightShift then
|
||||
handler.shift = false
|
||||
elseif key == keys.leftCtrl or key == keys.rightCtrl then
|
||||
handler.ctrl = false
|
||||
elseif key == keys.leftAlt or key == keys.rightAlt then
|
||||
handler.alt = false
|
||||
else
|
||||
return _key_event(KEY_CLICK.UP, key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user