Compare commits
1172 Commits
v0.0.7-pre
...
v1.8.10-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0aa6d54ac | ||
|
|
ad240ae44c | ||
|
|
79c93f1562 | ||
|
|
3c7fff28c9 | ||
|
|
d1e4ea586e | ||
|
|
4e789ab92d | ||
|
|
0892a57d35 | ||
|
|
1bc4828010 | ||
|
|
fb5a9d5d9e | ||
|
|
adbf1f2f78 | ||
|
|
2f99aaeedb | ||
|
|
f9917b786c | ||
|
|
d6185e0183 | ||
|
|
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 | ||
|
|
bfa1f4d0c6 | ||
|
|
737e0d72b0 | ||
|
|
6edeb3e3b8 | ||
|
|
fb00e98a5b | ||
|
|
4f952eff83 | ||
|
|
1eede97c08 | ||
|
|
95419562ee | ||
|
|
7b85d947c4 | ||
|
|
08e670091a | ||
|
|
1348b632a8 | ||
|
|
8cd5162362 | ||
|
|
1c410a89d8 | ||
|
|
6a931fced4 | ||
|
|
622e2eeb90 | ||
|
|
42cd9fff0c | ||
|
|
2a85a438ba | ||
|
|
338b3b1615 | ||
|
|
363f164f47 | ||
|
|
739f04ece9 | ||
|
|
c6ade68ce2 | ||
|
|
7d60e259e2 | ||
|
|
cd71c6a9c1 | ||
|
|
26fe130609 | ||
|
|
95f87b1b05 | ||
|
|
aebdf3e8df | ||
|
|
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 | ||
|
|
bdbb3071b3 | ||
|
|
def02a94d2 | ||
|
|
681bb0963e | ||
|
|
8f7d7c3ead | ||
|
|
c0f45cfb8b | ||
|
|
455653074a | ||
|
|
1202289fab | ||
|
|
acb7b5b4cb | ||
|
|
9bd79dacad | ||
|
|
c544d140bf | ||
|
|
353cb3622b | ||
|
|
b54f15bad6 | ||
|
|
4d9783beca | ||
|
|
5529774b0e | ||
|
|
2a541ef3fe | ||
|
|
e1b4d72ef8 | ||
|
|
6a0992c7a4 | ||
|
|
cff7c724be | ||
|
|
47bda73afe | ||
|
|
8daedc109c | ||
|
|
a164c18a50 | ||
|
|
4d663ada8d | ||
|
|
084a153a79 | ||
|
|
4ed6ec1c63 | ||
|
|
d3c2ba7bee | ||
|
|
55ff9dad4b | ||
|
|
0d6022f5e3 | ||
|
|
8b136d78a8 | ||
|
|
a5214730ef | ||
|
|
9f3ad3caf0 | ||
|
|
9bb2a99be5 | ||
|
|
65ace26258 | ||
|
|
61d975d13f | ||
|
|
1d7d6e9817 | ||
|
|
a2e0999cea | ||
|
|
1edee7f64b | ||
|
|
df61ec2c62 | ||
|
|
bf7a316b04 | ||
|
|
96c4444184 | ||
|
|
59eac62c33 | ||
|
|
ab193db153 | ||
|
|
7d65bba589 | ||
|
|
dcef5a96f0 | ||
|
|
ba0900ac65 | ||
|
|
8f54e95519 | ||
|
|
7b9824b6f9 | ||
|
|
b6835fc7d1 | ||
|
|
bc5a94cd3b | ||
|
|
2a3d868402 | ||
|
|
b998634da1 | ||
|
|
5225380523 | ||
|
|
0e7ea7102c | ||
|
|
8924ba4e99 | ||
|
|
a8071db08e | ||
|
|
fb3c7ded06 | ||
|
|
f6b0a49904 | ||
|
|
bfbbfb164b | ||
|
|
57763702ff | ||
|
|
f469754bb7 | ||
|
|
336662de62 | ||
|
|
9073009eb0 | ||
|
|
ffac6996ed | ||
|
|
da3c92b3bf | ||
|
|
712c7a8f3b | ||
|
|
737afe586d | ||
|
|
d69796b607 | ||
|
|
1cdf66a8c3 | ||
|
|
282c7db3eb | ||
|
|
a02529b9f7 | ||
|
|
af38025f50 | ||
|
|
b28e4d1e95 | ||
|
|
75dfa3ae73 | ||
|
|
4a3455fa60 | ||
|
|
a2fa6570dc | ||
|
|
aef8281ad6 | ||
|
|
d42327a20d | ||
|
|
49db75f34d | ||
|
|
bc87030491 | ||
|
|
9266d7d8e1 | ||
|
|
ef5567ad46 | ||
|
|
302f3d913f | ||
|
|
650b9c1811 | ||
|
|
543ac8c9fe | ||
|
|
7f19f76c0b | ||
|
|
8d76c86309 | ||
|
|
a4be6a6dde | ||
|
|
8b926a0978 | ||
|
|
775ffc8094 | ||
|
|
13a8435f6c | ||
|
|
5d6dda5619 | ||
|
|
f8221ad0f1 | ||
|
|
193aeed6df | ||
|
|
8c87cb3e26 | ||
|
|
1548cd706d | ||
|
|
996272e108 | ||
|
|
ef673bdf1b | ||
|
|
7aa236e987 | ||
|
|
35d857a5f4 | ||
|
|
c2c87ec6c6 | ||
|
|
5ce54d78e1 | ||
|
|
c05a312f6c | ||
|
|
86325d9527 | ||
|
|
5074ca89f0 | ||
|
|
c22b048608 | ||
|
|
7ae3014e06 | ||
|
|
8fa37cc9be | ||
|
|
2c730fbdc2 | ||
|
|
5c21140025 | ||
|
|
7859e5ea4c | ||
|
|
0b5ee8eabc | ||
|
|
1decd88415 | ||
|
|
f1b1f0b75a | ||
|
|
f37f2f009f | ||
|
|
15b071378c | ||
|
|
5ba06dcdaf | ||
|
|
f4e7137eb3 | ||
|
|
cf881548d7 | ||
|
|
0a6fd35f93 | ||
|
|
671f8b55bc | ||
|
|
e16b0d237e | ||
|
|
55dab6d675 | ||
|
|
0f5ae9a756 | ||
|
|
cdff7af431 | ||
|
|
c536b823e7 | ||
|
|
b20d42ff38 | ||
|
|
63147bfab5 | ||
|
|
360609df1f | ||
|
|
9a5fc92c86 | ||
|
|
337fca7e7c | ||
|
|
38fc7189ba | ||
|
|
0b939be412 | ||
|
|
351842c9a1 | ||
|
|
8d248408d4 | ||
|
|
2427561dc5 | ||
|
|
b4932b33b6 | ||
|
|
24a7275543 | ||
|
|
529371a0fd | ||
|
|
69df5edbeb | ||
|
|
153a83e569 | ||
|
|
ef1ec220a4 | ||
|
|
8f2e9fe319 | ||
|
|
86ad2a1069 | ||
|
|
494dc437a5 | ||
|
|
deec1ff1df | ||
|
|
4c35233289 | ||
|
|
de9cb3bd3a | ||
|
|
270726e276 | ||
|
|
dbd74afbe6 | ||
|
|
37a91986e5 | ||
|
|
a892c0cf41 | ||
|
|
b7d90872d5 | ||
|
|
82ab85daa5 | ||
|
|
f9aa75a105 | ||
|
|
e313b77abc | ||
|
|
a14ffea6f0 | ||
|
|
43a0ff86d7 | ||
|
|
ece7c0fe9a | ||
|
|
97cee58e5a | ||
|
|
4aba79f232 | ||
|
|
b8c81e2e70 | ||
|
|
142f2c363a | ||
|
|
de99169db8 | ||
|
|
d5446f970b | ||
|
|
792cb46ce6 | ||
|
|
86615b03ff | ||
|
|
d5fe790c86 | ||
|
|
beda7624f4 | ||
|
|
82e3fa494c | ||
|
|
466902371a | ||
|
|
e763af9981 | ||
|
|
b2115fd077 | ||
|
|
36bd2c5e08 | ||
|
|
f6610489c2 | ||
|
|
e159dbb850 | ||
|
|
513c72ea79 | ||
|
|
a81fd49604 | ||
|
|
b430a22f08 | ||
|
|
a220713385 | ||
|
|
fac9a8d104 | ||
|
|
0783c4c01f | ||
|
|
676dfc8c22 | ||
|
|
50c0a4a3eb | ||
|
|
032284e90d | ||
|
|
3a0d677c16 | ||
|
|
2c2f936232 | ||
|
|
4ef1915137 | ||
|
|
40fa0de7a3 | ||
|
|
b8a8da1ac4 | ||
|
|
e26dc905f8 | ||
|
|
c7edd8c487 | ||
|
|
d3249da102 | ||
|
|
0e1f23efe8 | ||
|
|
5a139c2dd6 | ||
|
|
30ba8bdccf | ||
|
|
b2e21cb6d9 | ||
|
|
8064b33a36 | ||
|
|
7e33f22577 | ||
|
|
464451c378 | ||
|
|
0778a442b1 | ||
|
|
2c7b98ba42 | ||
|
|
ff9a18a019 | ||
|
|
81005d3e2c | ||
|
|
d7e2884634 | ||
|
|
43e708aa0d | ||
|
|
783c4936cc | ||
|
|
c75f08a9f7 | ||
|
|
e1da8b59d3 | ||
|
|
706fb5ea74 | ||
|
|
419ca2e6ef | ||
|
|
4c8723eb32 | ||
|
|
5db517cedc | ||
|
|
e9788abde7 | ||
|
|
be077aa1fb | ||
|
|
d143015cc7 | ||
|
|
df45f6c984 | ||
|
|
f6fe99a5fd | ||
|
|
a843c8eb79 | ||
|
|
a614b97d02 | ||
|
|
eca303e289 | ||
|
|
ccdc31ed87 | ||
|
|
c49ad63d6a | ||
|
|
7929318096 | ||
|
|
2371a75130 | ||
|
|
fee54db43e | ||
|
|
b48c956354 | ||
|
|
449e393b73 | ||
|
|
d295c2b3c3 | ||
|
|
438ab55f4f | ||
|
|
46607dd690 | ||
|
|
33c570075c | ||
|
|
93776a0421 | ||
|
|
14dc814925 | ||
|
|
a7ba0e43e8 | ||
|
|
e9290540f5 | ||
|
|
b35bf98dec | ||
|
|
59512bb0cf | ||
|
|
64449c6674 | ||
|
|
5bcd885f53 | ||
|
|
ba70aa31dc | ||
|
|
d9ec3d7825 | ||
|
|
9b9ce7eae1 | ||
|
|
e2a3252d8a | ||
|
|
c0547fe463 | ||
|
|
36b86a4825 | ||
|
|
37dd52b12b | ||
|
|
6b8b38b8cb | ||
|
|
2b23dac1fe | ||
|
|
76f6cca42d | ||
|
|
ab9e487a2d | ||
|
|
982fded31d | ||
|
|
a8e0538804 | ||
|
|
8c42a05bbd | ||
|
|
60a3fc8c37 | ||
|
|
83cc4d3067 | ||
|
|
fb31afc89c | ||
|
|
36c8a9ccfa | ||
|
|
f108db9cfc | ||
|
|
f48266e27c | ||
|
|
5c333c2a07 | ||
|
|
df0ee7c4f7 | ||
|
|
c987d14d8d | ||
|
|
075a0280ac | ||
|
|
4b1c982292 | ||
|
|
e276a99cb3 | ||
|
|
3ae39b2455 | ||
|
|
fc9d86f23e | ||
|
|
b325992a0d | ||
|
|
04d73cdcd3 | ||
|
|
0c0055d5ae | ||
|
|
4ef73a8580 | ||
|
|
fa88392438 | ||
|
|
6e95755db4 | ||
|
|
5b1f304467 | ||
|
|
a2ea6438b5 | ||
|
|
c9b67f68dd | ||
|
|
d624690b6b | ||
|
|
527f3446a1 | ||
|
|
27a697c27e | ||
|
|
6bd7dd0271 | ||
|
|
67872a1053 | ||
|
|
4aad591d3a | ||
|
|
9bc4f0f7a6 | ||
|
|
d642f28fa9 | ||
|
|
ccc0aa18ff | ||
|
|
6ea530635f | ||
|
|
c2132ea7eb | ||
|
|
40b11dbfd3 | ||
|
|
6e1edce8e7 | ||
|
|
efef4a845f | ||
|
|
91f72ace24 | ||
|
|
0f735d049e | ||
|
|
18e4e309a7 | ||
|
|
55362d4e66 | ||
|
|
8b1e7cb933 | ||
|
|
66deabcf5d | ||
|
|
2a681d1d37 | ||
|
|
9eddab2c23 | ||
|
|
83dc1064f7 | ||
|
|
c9f1bddb36 | ||
|
|
85a9532962 | ||
|
|
edb5d8b96f | ||
|
|
0279ecdec9 | ||
|
|
9a500d53d8 | ||
|
|
5b7a11d157 | ||
|
|
57ccb73efe | ||
|
|
d494abe8af | ||
|
|
2e9f52dc89 | ||
|
|
8c236eca85 | ||
|
|
be8e8d767c | ||
|
|
94fb02a46b | ||
|
|
3586d335a6 | ||
|
|
f7828dd05b | ||
|
|
d01a6d548f | ||
|
|
b12f3206e2 | ||
|
|
0e5113918c | ||
|
|
11115633cf | ||
|
|
58cf383c91 | ||
|
|
3f15ae6b6f | ||
|
|
0d7fde635d | ||
|
|
ae3315e4a0 | ||
|
|
523d478739 | ||
|
|
2b8f71fc43 | ||
|
|
b150072234 | ||
|
|
fbb992ff12 | ||
|
|
523ac91c3b | ||
|
|
bd1625c42e | ||
|
|
7508acb1a7 | ||
|
|
6eee0d0c72 | ||
|
|
446fff04da | ||
|
|
4f285cf2b5 | ||
|
|
16d6372d7b | ||
|
|
b7895080cb | ||
|
|
38ac552613 | ||
|
|
225ed7baa1 | ||
|
|
4340518ecf | ||
|
|
79494f0587 | ||
|
|
ce0198f389 | ||
|
|
82ea35168b | ||
|
|
424097973d | ||
|
|
7247d8a828 | ||
|
|
a07086907e | ||
|
|
7c64a66dd3 | ||
|
|
6e0dde3f30 | ||
|
|
34cac6a8b8 | ||
|
|
e2d2a0f1dc | ||
|
|
8df67245c5 | ||
|
|
1be57aaf13 | ||
|
|
c4f6c1b289 | ||
|
|
c9526ba601 | ||
|
|
00263b2feb | ||
|
|
d74a2db8e9 | ||
|
|
632e96c8b3 | ||
|
|
279a40e335 | ||
|
|
e6632c3bd9 | ||
|
|
950ad2931f | ||
|
|
bc38a9ea27 | ||
|
|
960c016f4c | ||
|
|
fa6524d934 | ||
|
|
726d15b48f | ||
|
|
df57e1859e | ||
|
|
0493f572a2 | ||
|
|
1dea5b1b7a | ||
|
|
72eb2432cc | ||
|
|
052b2f3848 | ||
|
|
35dfd61df1 | ||
|
|
cc5ea0dbb0 | ||
|
|
caa6cc81b1 | ||
|
|
9f95801bfc | ||
|
|
c18e7ef4d0 | ||
|
|
5e65ca636e | ||
|
|
2babd67198 | ||
|
|
199ce53f52 | ||
|
|
8ebdf2686b | ||
|
|
9d5a55bf58 | ||
|
|
655213e174 | ||
|
|
1fe2acb5c5 | ||
|
|
ef27da8daf | ||
|
|
5751c320b1 | ||
|
|
2affe1b31c | ||
|
|
ccd9f4b6cc | ||
|
|
fdf75350c0 | ||
|
|
9784b4e165 | ||
|
|
4d40d08a7a | ||
|
|
42ff61a8a1 | ||
|
|
ff1bd02739 | ||
|
|
da9eead2d5 | ||
|
|
44d5cec1f8 | ||
|
|
37f7319494 | ||
|
|
ee739c214d | ||
|
|
07ee792163 | ||
|
|
678dafa62f | ||
|
|
6c09772a74 | ||
|
|
1d3a1672c8 | ||
|
|
1100051585 | ||
|
|
c77993d3a0 | ||
|
|
3e74d6c998 | ||
|
|
b5c70b0d37 | ||
|
|
ba8bfb6e14 | ||
|
|
a117d5ee97 | ||
|
|
72791d042b | ||
|
|
53e4576547 | ||
|
|
2e78aa895d | ||
|
|
eb8aab175f | ||
|
|
5721231ffd | ||
|
|
846f9685ad | ||
|
|
fe71615c12 | ||
|
|
e9562a140c | ||
|
|
e808ee2be0 | ||
|
|
8abac3fdcb | ||
|
|
4145949ba7 | ||
|
|
b7d4bc3a5b | ||
|
|
a1c1125d54 | ||
|
|
41838ee340 | ||
|
|
6fe257d1d7 | ||
|
|
ca2983506e | ||
|
|
93a0dedcb1 | ||
|
|
a591cab338 | ||
|
|
a633f5b4c3 | ||
|
|
6517f78c1c | ||
|
|
03f0216d51 | ||
|
|
41913441d5 | ||
|
|
2a99d1d385 | ||
|
|
52603e3579 | ||
|
|
c23ddaf5ea | ||
|
|
6bdde02268 | ||
|
|
5224dcbd25 | ||
|
|
9475700930 | ||
|
|
4030fdc5c9 | ||
|
|
518ee8272a | ||
|
|
e1d7c7b1c0 | ||
|
|
9c27ac7ae6 | ||
|
|
afb3b0957e | ||
|
|
d4ae18eee7 | ||
|
|
f68c38ccee | ||
|
|
5628df56a2 | ||
|
|
3685e25713 | ||
|
|
657cd15c59 | ||
|
|
29793ba7c4 | ||
|
|
9c32074b56 | ||
|
|
c93a386e74 | ||
|
|
6fcd18e17a | ||
|
|
7c39e8c72b | ||
|
|
9761228b8e | ||
|
|
e679b5a25a | ||
|
|
1a01bec7e4 | ||
|
|
f940c136bf | ||
|
|
8e28dbf2a6 | ||
|
|
8b65bf4852 | ||
|
|
ffeff86507 | ||
|
|
af57c3b1fc | ||
|
|
c221ffa129 | ||
|
|
83cf645da4 | ||
|
|
bc63a06b09 | ||
|
|
806b217d58 | ||
|
|
aaab34f1a8 | ||
|
|
2851331fda | ||
|
|
1828920873 | ||
|
|
c620310e51 | ||
|
|
54264f5149 | ||
|
|
d87dfb9ebd | ||
|
|
004c960e4d | ||
|
|
57bac57e3f | ||
|
|
b2be3ef5fc | ||
|
|
a02fb6f691 | ||
|
|
307bf6e2c8 | ||
|
|
d202a49011 | ||
|
|
93286174d4 | ||
|
|
788fae44aa | ||
|
|
2f55ad76f2 | ||
|
|
1bf8fe557c | ||
|
|
6d5af98310 | ||
|
|
ab757e14a7 | ||
|
|
bfa87815fa | ||
|
|
77dc7ec0c9 | ||
|
|
5dfbe650c6 | ||
|
|
529951f998 | ||
|
|
573c263548 | ||
|
|
d4da6a7f3a | ||
|
|
9d60777223 | ||
|
|
62ac993dae | ||
|
|
c02479b52e | ||
|
|
1b553ad495 | ||
|
|
7a90ea7e4e | ||
|
|
4f7775ccb6 | ||
|
|
50be7f9ca2 | ||
|
|
a87e557d2d | ||
|
|
36557fc345 | ||
|
|
d0d20b1299 | ||
|
|
88c34d8bca | ||
|
|
3267e7ff13 | ||
|
|
6686d8ea62 | ||
|
|
c47e0044b1 | ||
|
|
265368f9b2 | ||
|
|
70d9da847e | ||
|
|
1bf21564f9 | ||
|
|
cd6bb7376d | ||
|
|
e0ab2ade89 | ||
|
|
10c53ac4b3 | ||
|
|
d9be5ccb47 | ||
|
|
c14fc048a1 | ||
|
|
4275c9d408 | ||
|
|
98c826e762 | ||
|
|
dcf275784c | ||
|
|
6f3405949d | ||
|
|
33695b2ed6 | ||
|
|
350370a084 | ||
|
|
17954ef3d0 | ||
|
|
c5ba95449f | ||
|
|
3621f53c45 | ||
|
|
e084ae1eea | ||
|
|
49605e5966 | ||
|
|
0f6b3fdd98 | ||
|
|
c2ac7fc973 | ||
|
|
b53d2d6694 | ||
|
|
117784500a | ||
|
|
397e311f1b | ||
|
|
e456d34468 | ||
|
|
4359cc3e63 | ||
|
|
473763fd27 | ||
|
|
621adbbcbc | ||
|
|
564b89d19c | ||
|
|
17fce01ff5 | ||
|
|
f36b0c7e37 | ||
|
|
5a8bba5108 | ||
|
|
c3f7407689 | ||
|
|
d38e5ca5ec | ||
|
|
eadf5c488a | ||
|
|
c985e90ec3 | ||
|
|
c80d861b28 | ||
|
|
395c1ff9ce | ||
|
|
8dac59fba4 | ||
|
|
7f011369c4 | ||
|
|
3c2f631451 | ||
|
|
02c3c5c53c | ||
|
|
252c48a02c | ||
|
|
6b23a32744 | ||
|
|
826114e5bf | ||
|
|
17dd35e6de | ||
|
|
42c2b1bda1 | ||
|
|
f5c703a8b3 | ||
|
|
2918608326 | ||
|
|
14b24678f9 | ||
|
|
f4f36b020b | ||
|
|
f1a50990f2 | ||
|
|
01a364b5cf | ||
|
|
fc14141321 | ||
|
|
9b21a971fe | ||
|
|
1afafba501 | ||
|
|
d6a201a45f | ||
|
|
41cc6b9acc | ||
|
|
2aedc015c8 | ||
|
|
c3d6d900a1 | ||
|
|
525dedb830 | ||
|
|
88bf4d5653 | ||
|
|
6643c7e6ed | ||
|
|
bd1ab11686 | ||
|
|
8704d845bd | ||
|
|
6f61203db3 | ||
|
|
5a96818c97 | ||
|
|
b25ebdf959 | ||
|
|
4b60c038f4 | ||
|
|
ea17ba41fe | ||
|
|
39672fedb4 | ||
|
|
1444008479 | ||
|
|
409e8083a7 | ||
|
|
335e0f5ee9 | ||
|
|
9bd220cbb2 | ||
|
|
33159bc677 | ||
|
|
bd33240515 | ||
|
|
f6708ca988 | ||
|
|
ed0982a832 | ||
|
|
7ad115bc03 | ||
|
|
3048fbed8b | ||
|
|
35c408883a | ||
|
|
20a1fab611 | ||
|
|
ef73c52417 | ||
|
|
01caf3d914 | ||
|
|
f32cdf5563 | ||
|
|
1188d2f7df | ||
|
|
e137953f93 | ||
|
|
316b255a04 | ||
|
|
6397f29d4f | ||
|
|
47599b8ff6 | ||
|
|
e54d5b3d85 | ||
|
|
cf6f0e3153 | ||
|
|
d3f28a6882 | ||
|
|
15595ca81b | ||
|
|
5a3897572d | ||
|
|
e4b7f807fe | ||
|
|
9bd2229e27 | ||
|
|
27038f64f7 | ||
|
|
6980e73658 | ||
|
|
ea9e9288f7 | ||
|
|
7f007e032d | ||
|
|
971657c3d2 | ||
|
|
b628472d81 | ||
|
|
2e4a533148 | ||
|
|
13513a9ce6 | ||
|
|
3593493c98 | ||
|
|
7dbc5594b0 | ||
|
|
89437b2be9 | ||
|
|
4488a0594f | ||
|
|
3004902ce5 | ||
|
|
0950fc045d | ||
|
|
dc867095fd | ||
|
|
1fa87132d6 | ||
|
|
11e4d89b1d | ||
|
|
307883e6e7 | ||
|
|
1dad4bcf77 | ||
|
|
bc844d21bd | ||
|
|
d8bbe4b459 | ||
|
|
6f645579f8 | ||
|
|
ac607f9dc6 | ||
|
|
15bc816d7e | ||
|
|
254e85f3ed | ||
|
|
9d107da8d9 | ||
|
|
b99f57e480 | ||
|
|
2ac9bab92e | ||
|
|
29c4c39d23 | ||
|
|
8002698dd0 | ||
|
|
ce227a175a | ||
|
|
8ea75b9501 | ||
|
|
285026c1fa | ||
|
|
8b307ea030 | ||
|
|
b75d482f4a | ||
|
|
ebcc911b81 | ||
|
|
0bc0decbf2 | ||
|
|
1c819779c7 | ||
|
|
d6c8eb4d56 | ||
|
|
81345f5325 | ||
|
|
f0c97e8b70 | ||
|
|
5068e47590 | ||
|
|
c764506999 | ||
|
|
6d97d45227 | ||
|
|
e443beec19 | ||
|
|
0f7e77b0cb | ||
|
|
27a86cc893 | ||
|
|
07574aa116 | ||
|
|
dcb517d1cb | ||
|
|
1242c5a81c | ||
|
|
5cba8ff9f1 | ||
|
|
fc7b83a18a | ||
|
|
3bb95eb441 | ||
|
|
341df1a739 | ||
|
|
ccc5220ca8 | ||
|
|
e52b76aa24 | ||
|
|
43d5c0f8ad | ||
|
|
4ec07ca053 | ||
|
|
1705d8993e | ||
|
|
309ba06f8a | ||
|
|
e65a1bf6e1 | ||
|
|
ff5b163c1d | ||
|
|
706bf4d3ba | ||
|
|
4d16d64cdc | ||
|
|
6df0a1d149 | ||
|
|
51111f707f | ||
|
|
214f2d9028 | ||
|
|
78ddd4d782 | ||
|
|
7d7eecaa5e | ||
|
|
4d7d3be93b | ||
|
|
ffc997b84e | ||
|
|
4b6a1c5902 | ||
|
|
0cf81040fb | ||
|
|
9fb6b7a880 | ||
|
|
a93f0a4452 | ||
|
|
26c6010ce0 | ||
|
|
3b16d783d3 | ||
|
|
940ddf0d00 | ||
|
|
3f4fb63029 | ||
|
|
61965f295d | ||
|
|
44d30ae583 | ||
|
|
6a168c884d | ||
|
|
dd553125d6 | ||
|
|
62d5490dc8 | ||
|
|
790571b6fc | ||
|
|
cc856d4d80 | ||
|
|
6184078c3f | ||
|
|
9c034c366b | ||
|
|
31ede51c42 | ||
|
|
0eff8a3e6a | ||
|
|
136b09d7f2 | ||
|
|
bdd8af1873 | ||
|
|
11b89b679d | ||
|
|
530a40b0aa | ||
|
|
4834dbf781 | ||
|
|
374bfb7a19 | ||
|
|
94931ef5a2 | ||
|
|
bc6453de2b | ||
|
|
533d398f9d | ||
|
|
d3a926b25a | ||
|
|
6b74db70bd | ||
|
|
282d3fcd99 | ||
|
|
72da718015 | ||
|
|
bf0e92d6e4 | ||
|
|
c53ddf1638 | ||
|
|
45f5843598 | ||
|
|
fc39588b2e | ||
|
|
635e7b7f59 | ||
|
|
13fcf265b7 | ||
|
|
8b43c81fc0 | ||
|
|
e624dd431b | ||
|
|
969abca95d | ||
|
|
9695e94608 | ||
|
|
b985362757 | ||
|
|
0d090fe9e2 | ||
|
|
95c4d51e67 | ||
|
|
c6987f6f67 | ||
|
|
5ad14205f3 | ||
|
|
02541184bd | ||
|
|
bced8bf566 | ||
|
|
faac421b63 | ||
|
|
22a6159520 | ||
|
|
f4e397ebb1 | ||
|
|
87de804a9e | ||
|
|
e3a4ed5363 | ||
|
|
3c688bfafa | ||
|
|
6e1ece8183 | ||
|
|
168341db39 | ||
|
|
d7e38d6393 | ||
|
|
cd0d7aa5a3 | ||
|
|
25558df22d | ||
|
|
679d98c8bf | ||
|
|
469ee29b5a | ||
|
|
96e535fdc4 | ||
|
|
4aab75b842 | ||
|
|
17a46ae642 | ||
|
|
d0b2820160 | ||
|
|
b7e5ced2e8 | ||
|
|
eaabe51537 | ||
|
|
83fa41bbd0 | ||
|
|
89be79192f | ||
|
|
c4df8eabf9 | ||
|
|
b575899d46 | ||
|
|
7bcb260712 | ||
|
|
1cb5a0789e | ||
|
|
8b7ef47aad | ||
|
|
e253a7b4ff | ||
|
|
fe5059dd51 | ||
|
|
25c6b311f5 | ||
|
|
665b33fa05 | ||
|
|
635c70cffd | ||
|
|
dc1c1db5e6 | ||
|
|
e2f7318922 | ||
|
|
c76200b0e3 | ||
|
|
62b4b63f4a | ||
|
|
574b85e177 | ||
|
|
e3e370d3ab | ||
|
|
1ac4de65a9 | ||
|
|
c19a58380c | ||
|
|
4bc50e4bad | ||
|
|
5ce3f84dfa | ||
|
|
b280201446 | ||
|
|
76c81395b7 | ||
|
|
7ff0e25711 | ||
|
|
cd46c69a66 | ||
|
|
b76871aa07 | ||
|
|
479194b589 | ||
|
|
3fe47f99a9 | ||
|
|
aeda38fa01 | ||
|
|
10aa34a8e8 | ||
|
|
e1135eac01 | ||
|
|
c805b6e0c5 | ||
|
|
3587352219 | ||
|
|
84e7ad43bc | ||
|
|
e833176c65 | ||
|
|
ef1fdc7f39 | ||
|
|
07e9101ac7 | ||
|
|
4d5cbcf475 | ||
|
|
d688f9a1c6 | ||
|
|
67ec8fbd91 | ||
|
|
aff166e27d | ||
|
|
f14d715070 | ||
|
|
7f0f423450 | ||
|
|
f067da31b4 | ||
|
|
fe3b8e6f88 | ||
|
|
46a27a3f3a | ||
|
|
82726520b8 | ||
|
|
d40937b467 | ||
|
|
f996b9414a | ||
|
|
146e0bf569 | ||
|
|
67a93016c0 | ||
|
|
14377e7348 | ||
|
|
8c4598e7a6 | ||
|
|
71be6aca1a | ||
|
|
ccf06956f9 | ||
|
|
1ba5c7f828 | ||
|
|
68011d6734 | ||
|
|
f7f723829c | ||
|
|
19a4b3c0ef | ||
|
|
3ef2902829 | ||
|
|
b861d3f668 | ||
|
|
e119c11204 | ||
|
|
b1998b61bc | ||
|
|
0fc49d312d | ||
|
|
c46a7b2486 | ||
|
|
1744527a41 | ||
|
|
074f6448e1 | ||
|
|
74168707c6 | ||
|
|
86b0d155fa | ||
|
|
416255f41a | ||
|
|
fa19af308d | ||
|
|
852161317d | ||
|
|
3285f829f6 | ||
|
|
812d10f374 | ||
|
|
cd289ffb1e | ||
|
|
89ff502964 | ||
|
|
b25d95eeb7 | ||
|
|
554f09c817 | ||
|
|
912011bfed | ||
|
|
78a1073e2a | ||
|
|
6daf6df2d0 | ||
|
|
1bf0d352a1 | ||
|
|
17d0213d58 | ||
|
|
f7c11febe5 | ||
|
|
991c855c11 | ||
|
|
b10a8d9479 | ||
|
|
0c132f6e43 | ||
|
|
4842f9cb0d | ||
|
|
04f8dc7d75 | ||
|
|
3da7b74cfb | ||
|
|
b89724ad59 | ||
|
|
a3920ec2d8 | ||
|
|
6a5e0243be | ||
|
|
91079eeb78 | ||
|
|
2278469a8b | ||
|
|
377cf8e6fc | ||
|
|
7d9a664d38 | ||
|
|
a6e1134dc3 | ||
|
|
6d6953d795 | ||
|
|
0c5eb77cba | ||
|
|
ba5975f29b | ||
|
|
2a21d7d0be | ||
|
|
945b761fc2 | ||
|
|
203d868aeb | ||
|
|
28b1c03e03 | ||
|
|
b085baf91b | ||
|
|
03f9284f30 | ||
|
|
7e7e98ff6b | ||
|
|
ba1dd1b50e | ||
|
|
895750ea14 | ||
|
|
c47f45ea46 | ||
|
|
f24b214229 | ||
|
|
5b32f83890 | ||
|
|
dbf7377c02 | ||
|
|
13b0fcf65f | ||
|
|
02763c9cb3 | ||
|
|
34fc625602 | ||
|
|
ed997d53e1 | ||
|
|
7c2d89e70f | ||
|
|
a77946ce2c | ||
|
|
36fb4587a1 | ||
|
|
013656bc4d | ||
|
|
5eaeb50000 | ||
|
|
2ee503946c | ||
|
|
be73b17d46 | ||
|
|
60674ec95c | ||
|
|
74ae57324b | ||
|
|
1e23a2fd67 | ||
|
|
5642e3283d | ||
|
|
6e1e4c4685 | ||
|
|
a9d4458103 | ||
|
|
17874c4658 | ||
|
|
ac4ca3e56e | ||
|
|
5cff346cb5 | ||
|
|
a0b2c1f3e2 | ||
|
|
ea84563bb4 | ||
|
|
3c67ee08a8 | ||
|
|
1c6244d235 | ||
|
|
9cd0079d9e | ||
|
|
d6a68ee3d9 | ||
|
|
8429cbfd6e | ||
|
|
14cb7f96fc | ||
|
|
ffca88845b | ||
|
|
c6722c4cbe | ||
|
|
b3a2cfabc6 | ||
|
|
018b228976 | ||
|
|
00a81ab4f0 | ||
|
|
e47b4d7959 | ||
|
|
4dfdb218e2 | ||
|
|
78cbb9e67d | ||
|
|
c78db71b14 | ||
|
|
3b492ead92 | ||
|
|
ab49322fec | ||
|
|
26cce3a46a | ||
|
|
0dac25d9e7 |
16
.devcontainer/devcontainer.json
Normal file
16
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/universal:2",
|
||||
"features": {
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"sumneko.lua",
|
||||
"jackmacwindows.vscode-computercraft",
|
||||
"ms-python.python",
|
||||
"Catppuccin.catppuccin-vsc-icons",
|
||||
"melishev.feather-vscode"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ko_fi: mikayla_f
|
||||
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots and Logs**
|
||||
If applicable, add screenshots to help explain your problem. Please include a text snippet from the log.txt files if possible, otherwise include a screenshot.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: "enhancement,feature request"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
29
.github/workflows/check.yml
vendored
Normal file
29
.github/workflows/check.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Lua Checks
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- devel
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- devel
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3.5.1
|
||||
- name: Luacheck
|
||||
uses: lunarmodules/luacheck@v1.1.0
|
||||
with:
|
||||
# Argument Explanations
|
||||
# -i 121 = Setting a read-only global variable
|
||||
# 512 = Loop can be executed at most once
|
||||
# 542 = An empty if branch
|
||||
# --no-max-line-length = Disable warnings for long line lengths
|
||||
# --exclude-files ... = Exclude lockbox library (external) and config files
|
||||
# --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os'
|
||||
args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http keys parallel periphemu peripheral read rs settings shell term textutils window
|
||||
79
.github/workflows/manifest.yml
vendored
Normal file
79
.github/workflows/manifest.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
# Deploy installation manifests and shields versions
|
||||
name: Deploy Installation Data
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- devel
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v3.1.3
|
||||
# Generate manifest + shields files for main branch
|
||||
- name: Checkout main
|
||||
id: checkout-main
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'main'
|
||||
clean: false
|
||||
- name: Create outputs folders
|
||||
if: success() || failure()
|
||||
shell: bash
|
||||
run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/devel
|
||||
- name: Generate manifest and shields for main branch
|
||||
id: manifest-main
|
||||
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
|
||||
run: python imgen.py shields
|
||||
- name: Save main's manifest
|
||||
if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }}
|
||||
run: mv install_manifest.json deploy/manifests/main
|
||||
# Generate manifest for devel branch
|
||||
- name: Checkout devel
|
||||
id: checkout-devel
|
||||
if: success() || failure()
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'devel'
|
||||
clean: false
|
||||
- name: Generate manifest for devel
|
||||
id: manifest-devel
|
||||
if: ${{ (success() || failure()) && steps.checkout-devel.outcome == 'success' }}
|
||||
run: python imgen.py
|
||||
- name: Save devel's manifest
|
||||
if: ${{ (success() || failure()) && steps.manifest-devel.outcome == 'success' }}
|
||||
run: mv install_manifest.json deploy/manifests/devel
|
||||
# All artifacts ready now, upload deploy directory
|
||||
- name: Upload artifacts
|
||||
id: upload-artifacts
|
||||
if: ${{ (success() || failure()) && (steps.manifest-main.outcome == 'success' || steps.manifest-latest.outcome == 'success' || steps.manifest-devel.outcome == 'success') }}
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
# Upload manifest JSON
|
||||
path: 'deploy/'
|
||||
- name: Deploy to GitHub Pages
|
||||
if: ${{ (success() || failure()) && steps.upload-artifacts.outcome == 'success' }}
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
_notes/
|
||||
/*program.sh
|
||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"sumneko.lua",
|
||||
"jackmacwindows.vscode-computercraft",
|
||||
"ms-python.python"
|
||||
]
|
||||
}
|
||||
31
.vscode/settings.json
vendored
Normal file
31
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"Lua.diagnostics.globals": [
|
||||
"_HOST",
|
||||
"bit",
|
||||
"colors",
|
||||
"fs",
|
||||
"http",
|
||||
"keys",
|
||||
"parallel",
|
||||
"periphemu",
|
||||
"peripheral",
|
||||
"read",
|
||||
"rs",
|
||||
"settings",
|
||||
"shell",
|
||||
"term",
|
||||
"textutils",
|
||||
"window"
|
||||
],
|
||||
"Lua.diagnostics.severity": {
|
||||
"unused-local": "Information",
|
||||
"unused-vararg": "Information",
|
||||
"unused-function": "Warning",
|
||||
"unused-label": "Information"
|
||||
},
|
||||
"Lua.hint.setType": true,
|
||||
"Lua.diagnostics.disable": [
|
||||
"duplicate-set-field",
|
||||
"inject-field"
|
||||
]
|
||||
}
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 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
|
||||
|
||||
98
README.md
98
README.md
@@ -1,2 +1,96 @@
|
||||
# cc-mek-reactor-controller
|
||||
Configurable ComputerCraft multi-reactor control for Mekanism with a GUI, automatic safety features, waste processing control, and more!
|
||||
# cc-mek-scada
|
||||
Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fission reactors with a GUI, automatic safety features, waste processing control, and more!
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

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

|
||||
|
||||
## Released Component Versions
|
||||
|
||||

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

|
||||

|
||||

|
||||

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

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Requirements
|
||||
|
||||
Mod Requirements:
|
||||
- CC: Tweaked
|
||||
- Mekanism v10.1+
|
||||
|
||||
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 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 sqUN6VUb ccmsi.lua`
|
||||
|
||||
## [SCADA](https://en.wikipedia.org/wiki/SCADA)
|
||||
> Supervisory control and data acquisition (SCADA) is a control system architecture comprising computers, networked data communications and graphical user interfaces for high-level supervision of machines and processes. It also covers sensors and other devices, such as programmable logic controllers, which interface with process plant or machinery.
|
||||
|
||||
This project implements concepts of a SCADA system in ComputerCraft (because why not? ..okay don't answer that). I recommend reviewing that linked wikipedia page on SCADA if you *want* to understand the concepts used here.
|
||||
|
||||

|
||||
|
||||
SCADA and industrial automation terminology is used throughout the project, such as:
|
||||
- Supervisory Computer: Gathers data and controls the process
|
||||
- Coordinating Computer: Used as the HMI component, user requests high-level processing operations
|
||||
- RTU: Remote Terminal Unit
|
||||
- PLC: Programmable Logic Controller
|
||||
|
||||
## ComputerCraft Architecture
|
||||
|
||||
### Coordinator Server
|
||||
|
||||
There can only be one of these. This server acts as a hybrid of levels 3 & 4 in the SCADA diagram above. In addition to viewing status and controlling processes with advanced monitors, it can host access for one or more Pocket computers.
|
||||
|
||||
### Supervisory Computers
|
||||
|
||||
There should be one of these per facility system. Currently, that means only one. In the future, multiple supervisors would provide the capability of coordinating between multiple facilities (like a fission facility, fusion facility, etc).
|
||||
|
||||
### RTUs
|
||||
|
||||
RTUs are effectively basic connections between a device and the SCADA system with no internal logic providing the system with I/O capabilities. A single Advanced Computer can represent multiple RTUs as instead I am modeling an RTU as the wired modems connected to that computer rather than the computer itself. Each RTU is referenced separately with an identifier in the modbus communications (see Communications section), so a single computer can distribute instructions to multiple devices. This should save on having a pile of computers everywhere (but if you want to have that, no one's stopping you).
|
||||
|
||||
The RTU control code is relatively unique, as instead of having instructions be decoded simply, due to using modbus, I implemented a generalized RTU interface. To fulfill this, each type of I/O operation is linked to a function rather than implementing the logic itself. For example, to connect an input register to a turbine `getFlowRate()` call, the function reference itself is passed to the `connect_input_reg()` function. A call to `read_input_reg()` on that register address will call the `turbine.getFlowRate()` function and return the result.
|
||||
|
||||
### PLCs
|
||||
|
||||
PLCs are advanced devices that allow for both reporting and control to/from the SCADA system in addition to programed behaviors independent of the SCADA system. Currently there is only one type of PLC, and that is the reactor PLC. This is responsible for reporting on and controlling the reactor as a part of the SCADA system, and independently regulating the safety of the reactor. It checks the status for multiple hazard scenarios and shuts down the reactor if any condition is met.
|
||||
|
||||
There can and should only be one of these per reactor. A single Advanced Computer will act as the PLC, with either a direct connection (physical contact) or a wired modem connection to the reactor logic port.
|
||||
|
||||
## Communications
|
||||
|
||||
A vaguely-modbus [modbus](https://en.wikipedia.org/wiki/Modbus) communication protocol is used for communication with RTUs. Useful terminology for you to know:
|
||||
- Discrete Inputs: Single Bit Read-Only (digital inputs)
|
||||
- Coils: Single Bit Read/Write (digital I/O)
|
||||
- Input Registers: Multi-Byte Read-Only (analog inputs)
|
||||
- Holding Registers: Multi-Byte Read/Write (analog I/O)
|
||||
|
||||
### Security
|
||||
|
||||
HMAC message authentication is available as a configuration option to prevent replay attacks and generally prevent control or false data reporting within a system's network. This is done utilizing the [lua-lockbox](https://github.com/somesocks/lua-lockbox) project.
|
||||
|
||||
The other, simpler security feature is to enforce a maximum authorized transmission range, which is also a configurable feature on each device.
|
||||
|
||||
599
ccmsi.lua
Normal file
599
ccmsi.lua
Normal file
@@ -0,0 +1,599 @@
|
||||
--[[
|
||||
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,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
]]--
|
||||
|
||||
local function println(message) print(tostring(message)) end
|
||||
local function print(message) term.write(tostring(message)) end
|
||||
|
||||
local CCMSI_VERSION = "v1.14"
|
||||
|
||||
local install_dir = "/.install-cache"
|
||||
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
|
||||
local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/"
|
||||
|
||||
local opts = { ... }
|
||||
local mode, app, target
|
||||
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
|
||||
|
||||
-- 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
|
||||
|
||||
-- print out a white + blue text message
|
||||
local function pkg_message(message, package) white();print(message.." ");blue();println(package);white() end
|
||||
|
||||
-- indicate actions to be taken based on package differences for installs/updates
|
||||
local function show_pkg_change(name, v)
|
||||
if v.v_local ~= nil then
|
||||
if v.v_local ~= v.v_remote then
|
||||
print("["..name.."] updating ");blue();print(v.v_local);white();print(" \xbb ");blue();println(v.v_remote);white()
|
||||
elseif mode == "install" then
|
||||
pkg_message("["..name.."] reinstalling", v.v_local)
|
||||
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
|
||||
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
|
||||
|
||||
-- get the manifest from GitHub
|
||||
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()
|
||||
return false, {}
|
||||
end
|
||||
|
||||
local ok, manifest = pcall(function () return textutils.unserializeJSON(response.readAll()) end)
|
||||
if not ok then red();println("error parsing remote installation manifest");white() end
|
||||
|
||||
return ok, manifest
|
||||
end
|
||||
|
||||
-- record the local installation manifest
|
||||
local function write_install_manifest(manifest, dependencies)
|
||||
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
|
||||
end
|
||||
end
|
||||
if key == app or key == "comms" or is_dependency then versions[key] = value end
|
||||
end
|
||||
|
||||
manifest.versions = versions
|
||||
|
||||
local imfile = fs.open("install_manifest.json", "w")
|
||||
imfile.write(textutils.serializeJSON(manifest))
|
||||
imfile.close()
|
||||
end
|
||||
|
||||
-- recursively build a tree out of the file manifest
|
||||
local function gen_tree(manifest)
|
||||
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 = {}, {}
|
||||
|
||||
-- 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 = {}
|
||||
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 tree = gen_tree(manifest)
|
||||
|
||||
table.insert(tree, "install_manifest.json")
|
||||
table.insert(tree, "ccmsi.lua")
|
||||
table.insert(tree, "log.txt") ---@fixme fix after migration to settings files?
|
||||
|
||||
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 Installer "..CCMSI_VERSION.." --")
|
||||
|
||||
if #opts == 0 or opts[1] == "help" then
|
||||
println("usage: ccmsi <mode> <app> <branch>")
|
||||
println("<mode>")
|
||||
lgray()
|
||||
println(" check - check latest versions available")
|
||||
yellow()
|
||||
println(" ccmsi check <branch> for target")
|
||||
lgray()
|
||||
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")
|
||||
println(" installer - ccmsi installer (update only)")
|
||||
white();println("<branch>")
|
||||
lgray();println(" main (default) | devel");white()
|
||||
return
|
||||
else
|
||||
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
|
||||
if mode == nil then
|
||||
red();println("Unrecognized mode.");white()
|
||||
return
|
||||
end
|
||||
|
||||
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 ~= "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.."/"
|
||||
end
|
||||
|
||||
-- run selected mode
|
||||
|
||||
if mode == "check" then
|
||||
local ok, manifest = get_remote_manifest()
|
||||
if not ok then return end
|
||||
|
||||
local local_ok, local_manifest = read_local_manifest()
|
||||
if not local_ok then
|
||||
yellow();println("failed to load local installation information");white()
|
||||
local_manifest = { versions = { installer = CCMSI_VERSION } }
|
||||
else
|
||||
local_manifest.versions.installer = CCMSI_VERSION
|
||||
end
|
||||
|
||||
-- list all versions
|
||||
for key, value in pairs(manifest.versions) do
|
||||
term.setTextColor(colors.purple)
|
||||
print(string.format("%-14s", "["..key.."]"))
|
||||
if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then
|
||||
blue();print(local_manifest.versions[key])
|
||||
if value ~= local_manifest.versions[key] then
|
||||
white();print(" (")
|
||||
cyan();print(value);white();println(" available)")
|
||||
else green();println(" (up to date)") end
|
||||
else
|
||||
lgray();print("not installed");white();print(" (latest ")
|
||||
cyan();print(value);white();println(")")
|
||||
end
|
||||
end
|
||||
|
||||
if manifest.versions.installer ~= local_manifest.versions.installer then
|
||||
yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white()
|
||||
end
|
||||
elseif mode == "install" or mode == "update" then
|
||||
local update_installer = app == "installer"
|
||||
local ok, manifest = get_remote_manifest()
|
||||
if not ok then return end
|
||||
|
||||
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, lmnf = read_local_manifest()
|
||||
if not local_ok then
|
||||
if mode == "update" then
|
||||
red();println("Failed to load local installation information, cannot update.");white()
|
||||
return
|
||||
end
|
||||
elseif not update_installer then
|
||||
ver.boot.v_local = lmnf.versions.bootloader
|
||||
ver.app.v_local = lmnf.versions[app]
|
||||
ver.comms.v_local = lmnf.versions.comms
|
||||
ver.common.v_local = lmnf.versions.common
|
||||
ver.graphics.v_local = lmnf.versions.graphics
|
||||
ver.lockbox.v_local = lmnf.versions.lockbox
|
||||
|
||||
if lmnf.versions[app] == nil then
|
||||
red();println("Another application is already installed, please uninstall it before installing a new application.");white()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if manifest.versions.installer ~= CCMSI_VERSION then
|
||||
if not update_installer then yellow();println("A different version of the installer is available, it is recommended to update to it.");white() end
|
||||
if update_installer or ask_y_n("Would you like to update now") then
|
||||
lgray();println("GET ccmsi.lua")
|
||||
local dl, err = http.get(repo_path.."ccmsi.lua")
|
||||
|
||||
if dl == nil then
|
||||
red();println("HTTP Error "..err)
|
||||
println("Installer download failed.");white()
|
||||
else
|
||||
local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location
|
||||
handle.write(dl.readAll())
|
||||
handle.close()
|
||||
green();println("Installer updated successfully.");white()
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
elseif update_installer then
|
||||
green();println("Installer already up-to-date.");white()
|
||||
return
|
||||
end
|
||||
|
||||
ver.boot.v_remote = manifest.versions.bootloader
|
||||
ver.app.v_remote = manifest.versions[app]
|
||||
ver.comms.v_remote = manifest.versions.comms
|
||||
ver.common.v_remote = manifest.versions.common
|
||||
ver.graphics.v_remote = manifest.versions.graphics
|
||||
ver.lockbox.v_remote = manifest.versions.lockbox
|
||||
|
||||
green()
|
||||
if mode == "install" then print("Installing ") else print("Updating ") end
|
||||
println(app.." files...");white()
|
||||
|
||||
ver.boot.changed = show_pkg_change("bootldr", ver.boot)
|
||||
ver.common.changed = show_pkg_change("common", ver.common)
|
||||
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)
|
||||
|
||||
-- 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]
|
||||
|
||||
table.insert(dependencies, app)
|
||||
|
||||
for _, dependency in pairs(dependencies) do
|
||||
local size = size_list[dependency]
|
||||
space_required = space_required + size
|
||||
end
|
||||
|
||||
-- check space constraints
|
||||
if space_available < space_required then
|
||||
single_file_mode = true
|
||||
yellow();println("NOTICE: Insufficient space available for a full cached download!");white()
|
||||
lgray();println("Files can instead be downloaded one by one. If you are replacing a current install this may corrupt your install ONLY if it fails (such as a sudden network issue). If that occurs, you can still try again.")
|
||||
if mode == "update" then println("If installation still fails, delete this device's log file and/or any unrelated files you have on this computer then try again.") end
|
||||
white();
|
||||
if not ask_y_n("Do you wish to continue", false) then
|
||||
println("Operation cancelled.")
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local success = true
|
||||
|
||||
-- helper function to check if a dependency is unchanged
|
||||
local function unchanged(dependency)
|
||||
if dependency == "system" then return not ver.boot.changed
|
||||
elseif dependency == "graphics" then return not ver.graphics.changed
|
||||
elseif dependency == "lockbox" then return not ver.lockbox.changed
|
||||
elseif dependency == "common" then return not (ver.common.changed or ver.comms.changed)
|
||||
elseif dependency == app then return not ver.app.changed
|
||||
else return true end
|
||||
end
|
||||
|
||||
if not single_file_mode then
|
||||
if fs.exists(install_dir) then fs.delete(install_dir);fs.makeDir(install_dir) end
|
||||
|
||||
-- download all dependencies
|
||||
for _, dependency in pairs(dependencies) do
|
||||
if mode == "update" and unchanged(dependency) then
|
||||
pkg_message("skipping download of unchanged package", dependency)
|
||||
else
|
||||
pkg_message("downloading package", dependency)
|
||||
lgray()
|
||||
|
||||
local files = file_list[dependency]
|
||||
for _, file in pairs(files) do
|
||||
println("GET "..file)
|
||||
local dl, err = http.get(repo_path..file)
|
||||
|
||||
if dl == nil then
|
||||
red();println("HTTP Error "..err)
|
||||
success = false
|
||||
break
|
||||
else
|
||||
local handle = fs.open(install_dir.."/"..file, "w")
|
||||
handle.write(dl.readAll())
|
||||
handle.close()
|
||||
end
|
||||
end
|
||||
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)
|
||||
else
|
||||
pkg_message("installing package", dependency)
|
||||
lgray()
|
||||
|
||||
local files = file_list[dependency]
|
||||
for _, file in pairs(files) do
|
||||
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)
|
||||
|
||||
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
|
||||
else
|
||||
-- go through all files and replace one by one
|
||||
for _, dependency in pairs(dependencies) do
|
||||
if mode == "update" and unchanged(dependency) then
|
||||
pkg_message("skipping install of unchanged package", dependency)
|
||||
else
|
||||
pkg_message("installing package", dependency)
|
||||
lgray()
|
||||
|
||||
local files = file_list[dependency]
|
||||
for _, file in pairs(files) do
|
||||
println("GET "..file)
|
||||
local dl, err = http.get(repo_path..file)
|
||||
|
||||
if dl == nil then
|
||||
red();println("HTTP Error "..err)
|
||||
success = false
|
||||
break
|
||||
else
|
||||
local handle = fs.open("/"..file, "w")
|
||||
handle.write(dl.readAll())
|
||||
handle.close()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if success then
|
||||
write_install_manifest(manifest, dependencies)
|
||||
green()
|
||||
if mode == "install" then
|
||||
println("Installation completed successfully.")
|
||||
else println("Update completed successfully.") end
|
||||
white();println("Ready to clean up unused files, press any key to continue...")
|
||||
any_key();clean(manifest)
|
||||
white();println("Done.")
|
||||
else
|
||||
red()
|
||||
if mode == "install" then
|
||||
println("Installation failed, files may have been skipped.")
|
||||
else println("Update failed, files may have been skipped.") end
|
||||
end
|
||||
end
|
||||
elseif mode == "uninstall" then
|
||||
local ok, manifest = read_local_manifest()
|
||||
if not ok then
|
||||
red();println("Error parsing local installation manifest.");white()
|
||||
return
|
||||
end
|
||||
|
||||
if manifest.versions[app] == nil then
|
||||
red();println("Error: '"..app.."' is not installed.")
|
||||
return
|
||||
end
|
||||
|
||||
orange();println("Uninstalling all "..app.." files...")
|
||||
|
||||
-- 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[app]
|
||||
|
||||
table.insert(dependencies, app)
|
||||
|
||||
-- delete log file
|
||||
local log_deleted = false
|
||||
local settings_file = app..".settings"
|
||||
local legacy_config_file = app.."/config.lua"
|
||||
|
||||
lgray()
|
||||
if fs.exists(legacy_config_file) then
|
||||
log_deleted = pcall(function ()
|
||||
local config = require(app..".config")
|
||||
if fs.exists(config.LOG_PATH) then
|
||||
fs.delete(config.LOG_PATH)
|
||||
println("deleted log file "..config.LOG_PATH)
|
||||
end
|
||||
end)
|
||||
elseif fs.exists(settings_file) and settings.load(settings_file) then
|
||||
local log = settings.get("LogPath")
|
||||
if log ~= nil and fs.exists(log) then
|
||||
log_deleted = true
|
||||
fs.delete(log)
|
||||
println("deleted log file "..log)
|
||||
end
|
||||
end
|
||||
|
||||
if not log_deleted then
|
||||
red();println("Failed to delete log file.")
|
||||
white();println("press any key to continue...")
|
||||
any_key();lgray()
|
||||
end
|
||||
|
||||
-- delete all installed files
|
||||
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
|
||||
|
||||
if fs.exists(legacy_config_file) then
|
||||
fs.delete(legacy_config_file);println("deleted "..legacy_config_file)
|
||||
end
|
||||
|
||||
if fs.exists(settings_file) then
|
||||
fs.delete(settings_file);println("deleted "..settings_file)
|
||||
end
|
||||
|
||||
fs.delete("install_manifest.json")
|
||||
println("deleted install_manifest.json")
|
||||
|
||||
green();println("Done!")
|
||||
end
|
||||
|
||||
white()
|
||||
11
configure.lua
Normal file
11
configure.lua
Normal file
@@ -0,0 +1,11 @@
|
||||
print("CONFIGURE> SCANNING FOR CONFIGURATOR...")
|
||||
|
||||
if fs.exists("reactor-plc/configure.lua") then require("reactor-plc.configure").configure()
|
||||
elseif fs.exists("rtu/configure.lua") then require("rtu.configure").configure()
|
||||
elseif fs.exists("supervisor/configure.lua") then require("supervisor.configure").configure()
|
||||
elseif fs.exists("coordinator/configure.lua") then require("coordinator.configure").configure()
|
||||
elseif fs.exists("pocket/configure.lua") then require("pocket.configure").configure()
|
||||
else
|
||||
print("CONFIGURE> NO CONFIGURATOR FOUND")
|
||||
print("CONFIGURE> EXIT")
|
||||
end
|
||||
1322
coordinator/configure.lua
Normal file
1322
coordinator/configure.lua
Normal file
File diff suppressed because it is too large
Load Diff
749
coordinator/coordinator.lua
Normal file
749
coordinator/coordinator.lua
Normal file
@@ -0,0 +1,749 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
local types = require("scada-common.types")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local process = require("coordinator.process")
|
||||
|
||||
local apisessions = require("coordinator.session.apisessions")
|
||||
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
|
||||
local LINK_TIMEOUT = 60.0
|
||||
|
||||
local coordinator = {}
|
||||
|
||||
---@type crd_config
|
||||
local config = {}
|
||||
|
||||
coordinator.config = config
|
||||
|
||||
-- load the coordinator configuration<br>
|
||||
-- status of 0 is OK, 1 is bad config, 2 is bad monitor config
|
||||
---@return 0|1|2 status, nil|monitors_struct|string monitors (or error message)
|
||||
function coordinator.load_config()
|
||||
if not settings.load("/coordinator.settings") then return 1 end
|
||||
|
||||
config.UnitCount = settings.get("UnitCount")
|
||||
config.SpeakerVolume = settings.get("SpeakerVolume")
|
||||
config.Time24Hour = settings.get("Time24Hour")
|
||||
config.TempScale = settings.get("TempScale")
|
||||
|
||||
config.DisableFlowView = settings.get("DisableFlowView")
|
||||
config.MainDisplay = settings.get("MainDisplay")
|
||||
config.FlowDisplay = settings.get("FlowDisplay")
|
||||
config.UnitDisplays = settings.get("UnitDisplays")
|
||||
|
||||
config.SVR_Channel = settings.get("SVR_Channel")
|
||||
config.CRD_Channel = settings.get("CRD_Channel")
|
||||
config.PKT_Channel = settings.get("PKT_Channel")
|
||||
config.SVR_Timeout = settings.get("SVR_Timeout")
|
||||
config.API_Timeout = settings.get("API_Timeout")
|
||||
config.TrustedRange = settings.get("TrustedRange")
|
||||
config.AuthKey = settings.get("AuthKey")
|
||||
|
||||
config.LogMode = settings.get("LogMode")
|
||||
config.LogPath = settings.get("LogPath")
|
||||
config.LogDebug = settings.get("LogDebug")
|
||||
|
||||
local cfv = util.new_validator()
|
||||
|
||||
cfv.assert_type_int(config.UnitCount)
|
||||
cfv.assert_range(config.UnitCount, 1, 4)
|
||||
cfv.assert_type_bool(config.Time24Hour)
|
||||
cfv.assert_type_int(config.TempScale)
|
||||
cfv.assert_range(config.TempScale, 1, 4)
|
||||
|
||||
cfv.assert_type_bool(config.DisableFlowView)
|
||||
cfv.assert_type_table(config.UnitDisplays)
|
||||
|
||||
cfv.assert_type_num(config.SpeakerVolume)
|
||||
cfv.assert_range(config.SpeakerVolume, 0, 3)
|
||||
|
||||
cfv.assert_channel(config.SVR_Channel)
|
||||
cfv.assert_channel(config.CRD_Channel)
|
||||
cfv.assert_channel(config.PKT_Channel)
|
||||
|
||||
cfv.assert_type_num(config.SVR_Timeout)
|
||||
cfv.assert_min(config.SVR_Timeout, 2)
|
||||
cfv.assert_type_num(config.API_Timeout)
|
||||
cfv.assert_min(config.API_Timeout, 2)
|
||||
|
||||
cfv.assert_type_num(config.TrustedRange)
|
||||
cfv.assert_min(config.TrustedRange, 0)
|
||||
cfv.assert_type_str(config.AuthKey)
|
||||
|
||||
if type(config.AuthKey) == "string" then
|
||||
local len = string.len(config.AuthKey)
|
||||
cfv.assert_eq(len == 0 or len >= 8, true)
|
||||
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)
|
||||
|
||||
-- Monitor Setup
|
||||
|
||||
---@class monitors_struct
|
||||
local monitors = {
|
||||
main = nil, ---@type table|nil
|
||||
main_name = "",
|
||||
flow = nil, ---@type table|nil
|
||||
flow_name = "",
|
||||
unit_displays = {},
|
||||
unit_name_map = {}
|
||||
}
|
||||
|
||||
local mon_cfv = util.new_validator()
|
||||
|
||||
-- get all interface names
|
||||
local names = {}
|
||||
for iface, _ in pairs(ppm.get_monitor_list()) do table.insert(names, iface) 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)
|
||||
|
||||
if mon_cfv.valid() then
|
||||
local w, h, _
|
||||
|
||||
if not util.table_contains(names, config.MainDisplay) then
|
||||
return 2, "Main monitor is not connected."
|
||||
end
|
||||
|
||||
monitors.main = ppm.get_periph(config.MainDisplay)
|
||||
monitors.main_name = config.MainDisplay
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
return 0, monitors
|
||||
end
|
||||
|
||||
-- dmesg print wrapper
|
||||
---@param message string message
|
||||
---@param dmesg_tag string tag
|
||||
---@param working? boolean to use dmesg_working
|
||||
---@return function? update, function? done
|
||||
local function log_dmesg(message, dmesg_tag, working)
|
||||
local colors = {
|
||||
GRAPHICS = colors.green,
|
||||
SYSTEM = colors.cyan,
|
||||
BOOT = colors.blue,
|
||||
COMMS = colors.purple,
|
||||
CRYPTO = colors.yellow
|
||||
}
|
||||
|
||||
if working then
|
||||
return log.dmesg_working(message, dmesg_tag, colors[dmesg_tag])
|
||||
else
|
||||
log.dmesg(message, dmesg_tag, colors[dmesg_tag])
|
||||
end
|
||||
end
|
||||
|
||||
function coordinator.log_graphics(message) log_dmesg(message, "GRAPHICS") 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
|
||||
function coordinator.log_crypto(message) log_dmesg(message, "CRYPTO") end
|
||||
|
||||
-- log a message for communications connecting, providing access to progress indication control functions
|
||||
---@nodiscard
|
||||
---@param message string
|
||||
---@return function update, function done
|
||||
function coordinator.log_comms_connecting(message)
|
||||
local update, done = log_dmesg(message, "COMMS", true)
|
||||
---@cast update function
|
||||
---@cast done function
|
||||
return update, done
|
||||
end
|
||||
|
||||
-- coordinator communications
|
||||
---@nodiscard
|
||||
---@param version string coordinator version
|
||||
---@param nic nic network interface device
|
||||
---@param sv_watchdog 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_config_err = false,
|
||||
last_est_ack = ESTABLISH_ACK.ALLOW,
|
||||
last_api_est_acks = {},
|
||||
est_start = 0,
|
||||
est_last = 0,
|
||||
est_tick_waiting = nil,
|
||||
est_task_done = nil
|
||||
}
|
||||
|
||||
comms.set_trusted_range(config.TrustedRange)
|
||||
|
||||
-- configure network channels
|
||||
nic.closeAll()
|
||||
nic.open(config.CRD_Channel)
|
||||
|
||||
-- pass config to apisessions
|
||||
apisessions.init(nic, config)
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- send a packet to the supervisor
|
||||
---@param msg_type MGMT_TYPE|CRDN_TYPE
|
||||
---@param msg table
|
||||
local function _send_sv(protocol, msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local pkt ---@type mgmt_packet|crdn_packet
|
||||
|
||||
if protocol == PROTOCOL.SCADA_MGMT then
|
||||
pkt = comms.mgmt_packet()
|
||||
elseif protocol == PROTOCOL.SCADA_CRDN then
|
||||
pkt = comms.crdn_packet()
|
||||
else
|
||||
return
|
||||
end
|
||||
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable())
|
||||
|
||||
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)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
|
||||
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack })
|
||||
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
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, 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, MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
---@class coord_comms
|
||||
local public = {}
|
||||
|
||||
-- try to connect to the supervisor if not already linked
|
||||
---@param abort boolean? true to print out cancel info if not linked (use on program terminate)
|
||||
---@return boolean ok, boolean start_ui
|
||||
function public.try_connect(abort)
|
||||
local ok = true
|
||||
local start_ui = false
|
||||
|
||||
if not self.sv_linked then
|
||||
if self.est_tick_waiting == nil then
|
||||
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 " .. config.SVR_Channel)
|
||||
|
||||
_send_establish()
|
||||
else
|
||||
self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (os.clock() - self.est_start)))
|
||||
end
|
||||
|
||||
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 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")
|
||||
elseif self.last_est_ack == ESTABLISH_ACK.COLLISION then
|
||||
coordinator.log_comms("supervisor connection failed due to collision")
|
||||
elseif self.last_est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
coordinator.log_comms("supervisor connection failed due to version mismatch")
|
||||
else
|
||||
coordinator.log_comms("supervisor connection failed with no valid response")
|
||||
end
|
||||
end
|
||||
|
||||
ok = false
|
||||
elseif self.sv_config_err then
|
||||
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
|
||||
ok = false
|
||||
elseif (os.clock() - self.est_last) > 1.0 then
|
||||
_send_establish()
|
||||
self.est_last = os.clock()
|
||||
end
|
||||
elseif self.est_tick_waiting ~= nil then
|
||||
self.est_task_done(true)
|
||||
self.est_tick_waiting = nil
|
||||
self.est_task_done = nil
|
||||
start_ui = true
|
||||
end
|
||||
|
||||
return ok, start_ui
|
||||
end
|
||||
|
||||
-- close the connection to the server
|
||||
function public.close()
|
||||
sv_watchdog.cancel()
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.sv_linked = false
|
||||
self.sv_r_seq_num = nil
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
_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, CRDN_TYPE.FAC_CMD, { cmd, option })
|
||||
end
|
||||
|
||||
-- send the auto process control configuration with a start command
|
||||
---@param auto_cfg coord_auto_config configuration
|
||||
function public.send_auto_start(auto_cfg)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
|
||||
FAC_COMMAND.START, auto_cfg.mode, auto_cfg.burn_target, auto_cfg.charge_target, auto_cfg.gen_target, auto_cfg.limits
|
||||
})
|
||||
end
|
||||
|
||||
-- send a unit command
|
||||
---@param cmd UNIT_COMMAND command
|
||||
---@param unit integer unit ID
|
||||
---@param option any? optional option options for the optional options (like burn rate)
|
||||
function public.send_unit_command(cmd, unit, option)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
|
||||
end
|
||||
|
||||
-- parse a packet
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
---@param reply_to integer
|
||||
---@param message any
|
||||
---@param distance integer
|
||||
---@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
|
||||
|
||||
if s_pkt then
|
||||
-- get as SCADA management packet
|
||||
if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
pkt = mgmt_pkt.get()
|
||||
end
|
||||
-- get as coordinator packet
|
||||
elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
local crdn_pkt = comms.crdn_packet()
|
||||
if crdn_pkt.decode(s_pkt) then
|
||||
pkt = crdn_pkt.get()
|
||||
end
|
||||
else
|
||||
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
|
||||
end
|
||||
end
|
||||
|
||||
return pkt
|
||||
end
|
||||
|
||||
-- handle a packet
|
||||
---@param packet mgmt_frame|crdn_frame|nil
|
||||
---@return boolean close_ui
|
||||
function public.handle_packet(packet)
|
||||
local was_linked = self.sv_linked
|
||||
|
||||
if packet ~= nil then
|
||||
local l_chan = packet.scada_frame.local_channel()
|
||||
local r_chan = packet.scada_frame.remote_channel()
|
||||
local src_addr = packet.scada_frame.src_addr()
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
|
||||
if l_chan ~= config.CRD_Channel then
|
||||
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
||||
elseif r_chan == config.PKT_Channel then
|
||||
if not self.sv_linked then
|
||||
log.debug("discarding pocket API packet before linked to supervisor")
|
||||
elseif protocol == PROTOCOL.SCADA_CRDN then
|
||||
---@cast packet crdn_frame
|
||||
-- look for an associated session
|
||||
local session = apisessions.find_session(src_addr)
|
||||
|
||||
-- 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 SCADA_CRDN packet without a known session")
|
||||
end
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
-- look for an associated session
|
||||
local session = apisessions.find_session(src_addr)
|
||||
|
||||
-- SCADA management packet
|
||||
if session ~= nil then
|
||||
-- pass the packet onto the session handler
|
||||
session.in_queue.push_packet(packet)
|
||||
elseif packet.type == MGMT_TYPE.ESTABLISH then
|
||||
-- establish a new session
|
||||
-- validate packet and continue
|
||||
if packet.length == 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
|
||||
local comms_v = packet.data[1]
|
||||
local firmware_v = packet.data[2]
|
||||
local dev_type = packet.data[3]
|
||||
|
||||
if comms_v ~= comms.version then
|
||||
if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_VERSION then
|
||||
log.info(util.c("dropping API establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
|
||||
end
|
||||
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
|
||||
elseif dev_type == DEVICE_TYPE.PKT then
|
||||
-- pocket linking request
|
||||
local id = apisessions.establish_session(src_addr, firmware_v)
|
||||
coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id))
|
||||
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW)
|
||||
else
|
||||
log.debug(util.c("API_ESTABLISH: illegal establish packet for device ", dev_type, " on pocket channel"))
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
|
||||
end
|
||||
else
|
||||
log.debug("invalid establish packet (on API listening channel)")
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
|
||||
end
|
||||
else
|
||||
-- any other packet should be session related, discard it
|
||||
log.debug(util.c("discarding pocket SCADA_MGMT packet without a known session from computer ", src_addr))
|
||||
end
|
||||
else
|
||||
log.debug("illegal packet type " .. protocol .. " on pocket channel", true)
|
||||
end
|
||||
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())
|
||||
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()
|
||||
end
|
||||
|
||||
-- feed watchdog on valid sequence number
|
||||
sv_watchdog.feed()
|
||||
|
||||
-- handle packet
|
||||
if protocol == PROTOCOL.SCADA_CRDN then
|
||||
---@cast packet crdn_frame
|
||||
if self.sv_linked 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])
|
||||
local unit_builds = iocontrol.record_unit_builds(packet.data[2])
|
||||
|
||||
if fac_builds and unit_builds then
|
||||
-- acknowledge receipt of 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 == 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, 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 == 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 == CRDN_TYPE.FAC_CMD then
|
||||
-- facility command acknowledgement
|
||||
if packet.length >= 2 then
|
||||
local cmd = packet.data[1]
|
||||
local ack = packet.data[2] == true
|
||||
|
||||
if cmd == FAC_COMMAND.SCRAM_ALL then
|
||||
iocontrol.get_db().facility.scram_ack(ack)
|
||||
elseif cmd == FAC_COMMAND.STOP then
|
||||
iocontrol.get_db().facility.stop_ack(ack)
|
||||
elseif cmd == FAC_COMMAND.START then
|
||||
if packet.length == 7 then
|
||||
process.start_ack_handle({ table.unpack(packet.data, 2) })
|
||||
else
|
||||
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)
|
||||
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])
|
||||
else
|
||||
log.debug(util.c("received facility command ack with unknown command ", cmd))
|
||||
end
|
||||
else
|
||||
log.debug("SCADA_CRDN facility command ack packet length mismatch")
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.UNIT_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, 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 == 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 == 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
|
||||
|
||||
if unit ~= nil then
|
||||
if cmd == UNIT_COMMAND.SCRAM then
|
||||
unit.scram_ack(ack)
|
||||
elseif cmd == UNIT_COMMAND.START then
|
||||
unit.start_ack(ack)
|
||||
elseif cmd == UNIT_COMMAND.RESET_RPS then
|
||||
unit.reset_rps_ack(ack)
|
||||
elseif cmd == UNIT_COMMAND.SET_BURN then
|
||||
unit.set_burn_ack(ack)
|
||||
elseif cmd == UNIT_COMMAND.SET_WASTE then
|
||||
unit.set_waste_ack(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
|
||||
else
|
||||
log.debug(util.c("received unit command ack with unknown command ", cmd))
|
||||
end
|
||||
else
|
||||
log.debug(util.c("received unit command ack with unknown unit ", unit_id))
|
||||
end
|
||||
else
|
||||
log.debug("SCADA_CRDN unit command ack packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.debug("received unknown SCADA_CRDN packet type " .. packet.type)
|
||||
end
|
||||
else
|
||||
log.debug("discarding SCADA_CRDN packet before linked")
|
||||
end
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
if self.sv_linked 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]
|
||||
local trip_time = util.time() - timestamp
|
||||
|
||||
if trip_time > 750 then
|
||||
log.warning("coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug("coordinator RTT = " .. trip_time .. "ms")
|
||||
|
||||
iocontrol.get_db().facility.ps.publish("sv_ping", trip_time)
|
||||
|
||||
_send_keep_alive_ack(timestamp)
|
||||
else
|
||||
log.debug("SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
sv_watchdog.cancel()
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.sv_linked = false
|
||||
self.sv_r_seq_num = nil
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
log.info("server connection closed by remote host")
|
||||
else
|
||||
log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.ESTABLISH then
|
||||
-- connection with supervisor established
|
||||
if packet.length == 2 then
|
||||
local est_ack = packet.data[1]
|
||||
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(sv_config) == "table" and #sv_config == 2 then
|
||||
-- get configuration
|
||||
|
||||
---@class facility_conf
|
||||
local conf = {
|
||||
num_units = sv_config[1], ---@type integer
|
||||
cooling = sv_config[2] ---@type sv_cooling_conf
|
||||
}
|
||||
|
||||
if conf.num_units == config.UnitCount then
|
||||
-- init io controller
|
||||
iocontrol.init(conf, public, config.TempScale)
|
||||
|
||||
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("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")
|
||||
end
|
||||
else
|
||||
log.debug("SCADA_MGMT establish packet reply (len = 2) unsupported")
|
||||
end
|
||||
|
||||
self.last_est_ack = est_ack
|
||||
elseif packet.length == 1 then
|
||||
local est_ack = packet.data[1]
|
||||
|
||||
if est_ack == ESTABLISH_ACK.DENY then
|
||||
if self.last_est_ack ~= est_ack then
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DENIED)
|
||||
log.info("supervisor connection denied")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
if self.last_est_ack ~= est_ack then
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.COLLISION)
|
||||
log.warning("supervisor connection denied due to collision")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
if self.last_est_ack ~= est_ack then
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.BAD_VERSION)
|
||||
log.warning("supervisor comms version mismatch")
|
||||
end
|
||||
else
|
||||
log.debug("SCADA_MGMT establish packet reply (len = 1) unsupported")
|
||||
end
|
||||
|
||||
self.last_est_ack = est_ack
|
||||
else
|
||||
log.debug("SCADA_MGMT establish packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.debug("discarding non-link SCADA_MGMT packet before linked")
|
||||
end
|
||||
else
|
||||
log.debug("illegal packet type " .. protocol .. " on supervisor listening channel", true)
|
||||
end
|
||||
else
|
||||
log.debug("received packet for unknown channel " .. r_chan, true)
|
||||
end
|
||||
end
|
||||
|
||||
return was_linked and not self.sv_linked
|
||||
end
|
||||
|
||||
-- check if the coordinator is still linked to the supervisor
|
||||
---@nodiscard
|
||||
function public.is_linked() return self.sv_linked end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return coordinator
|
||||
1272
coordinator/iocontrol.lua
Normal file
1272
coordinator/iocontrol.lua
Normal file
File diff suppressed because it is too large
Load Diff
323
coordinator/process.lua
Normal file
323
coordinator/process.lua
Normal file
@@ -0,0 +1,323 @@
|
||||
--
|
||||
-- Process Control Management
|
||||
--
|
||||
|
||||
local comms = require("scada-common.comms")
|
||||
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 PROCESS = types.PROCESS
|
||||
local PRODUCT = types.WASTE_PRODUCT
|
||||
|
||||
---@class process_controller
|
||||
local process = {}
|
||||
|
||||
local self = {
|
||||
io = nil, ---@type ioctl
|
||||
comms = nil, ---@type coord_comms
|
||||
---@class coord_control_states
|
||||
control_states = {
|
||||
---@class coord_auto_config
|
||||
process = {
|
||||
mode = PROCESS.INACTIVE,
|
||||
burn_target = 0.0,
|
||||
charge_target = 0.0,
|
||||
gen_target = 0.0,
|
||||
limits = {},
|
||||
waste_product = PRODUCT.PLUTONIUM,
|
||||
pu_fallback = false
|
||||
},
|
||||
waste_modes = {},
|
||||
priority_groups = {}
|
||||
}
|
||||
}
|
||||
|
||||
--------------------------
|
||||
-- UNIT COMMAND CONTROL --
|
||||
--------------------------
|
||||
|
||||
-- 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
|
||||
|
||||
local ctl_proc = self.control_states.process
|
||||
|
||||
for i = 1, self.io.facility.num_units do
|
||||
ctl_proc.limits[i] = 0.1
|
||||
end
|
||||
|
||||
local ctrl_states = settings.get("ControlStates", {})
|
||||
local config = ctrl_states.process ---@type coord_auto_config
|
||||
|
||||
-- facility auto control configuration
|
||||
if type(config) == "table" then
|
||||
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
|
||||
|
||||
self.io.facility.ps.publish("process_mode", ctl_proc.mode)
|
||||
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
|
||||
self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target)
|
||||
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target)
|
||||
self.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
|
||||
self.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback)
|
||||
|
||||
for id = 1, math.min(#ctl_proc.limits, self.io.facility.num_units) do
|
||||
local unit = self.io.units[id] ---@type ioctl_unit
|
||||
unit.unit_ps.publish("burn_limit", ctl_proc.limits[id])
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded auto control settings")
|
||||
|
||||
-- notify supervisor of auto waste config
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, ctl_proc.waste_product)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, ctl_proc.pu_fallback)
|
||||
end
|
||||
|
||||
-- unit waste states
|
||||
local waste_modes = ctrl_states.waste_modes ---@type table|nil
|
||||
if type(waste_modes) == "table" then
|
||||
for id, mode in pairs(waste_modes) do
|
||||
self.control_states.waste_modes[id] = mode
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded unit waste mode settings")
|
||||
end
|
||||
|
||||
-- unit priority groups
|
||||
local prio_groups = ctrl_states.priority_groups ---@type table|nil
|
||||
if type(prio_groups) == "table" then
|
||||
for id, group in pairs(prio_groups) do
|
||||
self.control_states.priority_groups[id] = group
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group)
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded priority groups settings")
|
||||
end
|
||||
end
|
||||
|
||||
-- facility SCRAM command
|
||||
function process.fac_scram()
|
||||
self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL)
|
||||
log.debug("PROCESS: FAC SCRAM ALL")
|
||||
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")
|
||||
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"))
|
||||
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"))
|
||||
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"))
|
||||
end
|
||||
|
||||
-- set burn rate
|
||||
---@param id integer unit ID
|
||||
---@param rate number burn rate
|
||||
function process.set_rate(id, rate)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
|
||||
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)
|
||||
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
|
||||
|
||||
self.control_states.waste_modes[id] = mode
|
||||
settings.set("ControlStates", self.control_states)
|
||||
|
||||
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)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
|
||||
end
|
||||
|
||||
-- reset an alarm
|
||||
---@param id integer unit ID
|
||||
---@param alarm integer alarm ID
|
||||
function process.reset_alarm(id, alarm)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
|
||||
end
|
||||
|
||||
-- assign a unit to a group
|
||||
---@param unit_id integer unit ID
|
||||
---@param group_id integer|0 group ID or 0 for independent
|
||||
function process.set_group(unit_id, group_id)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, unit_id, group_id)
|
||||
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
|
||||
|
||||
self.control_states.priority_groups[unit_id] = group_id
|
||||
settings.set("ControlStates", self.control_states)
|
||||
|
||||
if not settings.save("/coordinator.settings") then
|
||||
log.error("process.set_group(): failed to save coordinator settings file")
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------
|
||||
-- AUTO PROCESS CONTROL --
|
||||
--------------------------
|
||||
|
||||
-- write auto process control to config file
|
||||
local function _write_auto_config()
|
||||
-- save config
|
||||
settings.set("ControlStates", self.control_states)
|
||||
local saved = settings.save("/coordinator.settings")
|
||||
if not saved then
|
||||
log.warning("process._write_auto_config(): failed to save coordinator settings file")
|
||||
end
|
||||
|
||||
return saved
|
||||
end
|
||||
|
||||
-- stop automatic process control
|
||||
function process.stop_auto()
|
||||
self.comms.send_fac_command(FAC_COMMAND.STOP)
|
||||
log.debug("PROCESS: STOP AUTO CTL")
|
||||
end
|
||||
|
||||
-- start automatic process control
|
||||
function process.start_auto()
|
||||
self.comms.send_auto_start(self.control_states.process)
|
||||
log.debug("PROCESS: START AUTO CTL")
|
||||
end
|
||||
|
||||
-- set automatic process control waste mode
|
||||
---@param product WASTE_PRODUCT waste product for auto control
|
||||
function process.set_process_waste(product)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, product)
|
||||
|
||||
log.debug(util.c("PROCESS: SET WASTE ", product))
|
||||
|
||||
-- update config table and save
|
||||
self.control_states.process.waste_product = product
|
||||
_write_auto_config()
|
||||
end
|
||||
|
||||
-- set automatic process control plutonium fallback
|
||||
---@param enabled boolean whether to enable plutonium fallback
|
||||
function process.set_pu_fallback(enabled)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, enabled)
|
||||
|
||||
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
|
||||
|
||||
-- update config table and save
|
||||
self.control_states.process.pu_fallback = enabled
|
||||
_write_auto_config()
|
||||
end
|
||||
|
||||
-- save process control settings
|
||||
---@param mode PROCESS control mode
|
||||
---@param burn_target number burn rate target
|
||||
---@param charge_target number charge target
|
||||
---@param gen_target number generation rate target
|
||||
---@param limits table unit burn rate limits
|
||||
function process.save(mode, burn_target, charge_target, gen_target, limits)
|
||||
log.debug("PROCESS: SAVE")
|
||||
|
||||
-- update config table
|
||||
local ctl_proc = self.control_states.process
|
||||
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())
|
||||
end
|
||||
|
||||
-- handle a start command acknowledgement
|
||||
---@param response table ack and configuration reply
|
||||
function process.start_ack_handle(response)
|
||||
local ack = response[1]
|
||||
|
||||
local ctl_proc = self.control_states.process
|
||||
ctl_proc.mode = response[2]
|
||||
ctl_proc.burn_target = response[3]
|
||||
ctl_proc.charge_target = response[4]
|
||||
ctl_proc.gen_target = response[5]
|
||||
|
||||
for i = 1, math.min(#response[6], self.io.facility.num_units) do
|
||||
ctl_proc.limits[i] = response[6][i]
|
||||
|
||||
local unit = self.io.units[i] ---@type ioctl_unit
|
||||
unit.unit_ps.publish("burn_limit", ctl_proc.limits[i])
|
||||
end
|
||||
|
||||
self.io.facility.ps.publish("process_mode", ctl_proc.mode)
|
||||
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
|
||||
self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target)
|
||||
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target)
|
||||
|
||||
self.io.facility.start_ack(ack)
|
||||
end
|
||||
|
||||
-- record waste product state after attempting to change it
|
||||
---@param response WASTE_PRODUCT supervisor waste product state
|
||||
function process.waste_ack_handle(response)
|
||||
self.control_states.process.waste_product = response
|
||||
self.io.facility.ps.publish("process_waste_product", response)
|
||||
end
|
||||
|
||||
-- record plutonium fallback state after attempting to change it
|
||||
---@param response boolean supervisor plutonium fallback state
|
||||
function process.pu_fb_ack_handle(response)
|
||||
self.control_states.process.pu_fallback = response
|
||||
self.io.facility.ps.publish("process_pu_fallback", response)
|
||||
end
|
||||
|
||||
return process
|
||||
491
coordinator/renderer.lua
Normal file
491
coordinator/renderer.lua
Normal file
@@ -0,0 +1,491 @@
|
||||
--
|
||||
-- Graphics Rendering Control
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
|
||||
local flow_view = require("coordinator.ui.layout.flow_view")
|
||||
local panel_view = require("coordinator.ui.layout.front_panel")
|
||||
local main_view = require("coordinator.ui.layout.main_view")
|
||||
local unit_view = require("coordinator.ui.layout.unit_view")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
|
||||
---@class coord_renderer
|
||||
local renderer = {}
|
||||
|
||||
-- render engine
|
||||
local engine = {
|
||||
monitors = nil, ---@type monitors_struct|nil
|
||||
dmesg_window = nil, ---@type table|nil
|
||||
ui_ready = false,
|
||||
fp_ready = false,
|
||||
ui = {
|
||||
front_panel = nil, ---@type graphics_element|nil
|
||||
main_display = nil, ---@type graphics_element|nil
|
||||
flow_display = nil, ---@type graphics_element|nil
|
||||
unit_displays = {}
|
||||
},
|
||||
disable_flow_view = false
|
||||
}
|
||||
|
||||
-- init a display to the "default", but set text scale to 0.5
|
||||
---@param monitor table monitor
|
||||
local function _init_display(monitor)
|
||||
monitor.setTextScale(0.5)
|
||||
monitor.setTextColor(colors.white)
|
||||
monitor.setBackgroundColor(colors.black)
|
||||
monitor.clear()
|
||||
monitor.setCursorPos(1, 1)
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.colors do
|
||||
monitor.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||
end
|
||||
end
|
||||
|
||||
-- print out that the monitor is too small
|
||||
---@param monitor table monitor
|
||||
local function _print_too_small(monitor)
|
||||
monitor.setCursorPos(1, 1)
|
||||
monitor.setBackgroundColor(colors.black)
|
||||
monitor.setTextColor(colors.red)
|
||||
monitor.clear()
|
||||
monitor.write("monitor too small")
|
||||
end
|
||||
|
||||
-- disable the flow view
|
||||
---@param disable boolean
|
||||
function renderer.legacy_disable_flow_view(disable)
|
||||
engine.disable_flow_view = disable
|
||||
end
|
||||
|
||||
-- link to the monitor peripherals
|
||||
---@param monitors monitors_struct
|
||||
function renderer.set_displays(monitors)
|
||||
engine.monitors = monitors
|
||||
|
||||
-- report to front panel as connected
|
||||
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 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
|
||||
_init_display(monitor)
|
||||
end
|
||||
|
||||
-- init terminal
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
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
|
||||
|
||||
-- initialize the dmesg output window
|
||||
function renderer.init_dmesg()
|
||||
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
|
||||
|
||||
-- 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
|
||||
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)
|
||||
|
||||
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
|
||||
function renderer.close_fp()
|
||||
if engine.fp_ready then
|
||||
if not engine.ui_ready then
|
||||
-- stop blinking indicators
|
||||
flasher.clear()
|
||||
end
|
||||
|
||||
-- disable PGI
|
||||
pgi.unlink()
|
||||
|
||||
-- hide to stop animation callbacks and clear root UI elements
|
||||
engine.ui.front_panel.hide()
|
||||
engine.ui.front_panel = nil
|
||||
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)
|
||||
end
|
||||
|
||||
-- reset terminal
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- 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)
|
||||
|
||||
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)
|
||||
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)
|
||||
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
|
||||
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
|
||||
end
|
||||
|
||||
return status, msg
|
||||
end
|
||||
|
||||
-- close out the UI
|
||||
function renderer.close_ui()
|
||||
if not engine.fp_ready then
|
||||
-- stop blinking indicators
|
||||
flasher.clear()
|
||||
end
|
||||
|
||||
-- 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
|
||||
engine.ui_ready = false
|
||||
|
||||
-- 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
|
||||
|
||||
-- re-draw dmesg
|
||||
engine.dmesg_window.setVisible(true)
|
||||
engine.dmesg_window.redraw()
|
||||
end
|
||||
|
||||
-- is the front panel ready?
|
||||
---@nodiscard
|
||||
---@return boolean ready
|
||||
function renderer.fp_ready() return engine.fp_ready end
|
||||
|
||||
-- is the UI ready?
|
||||
---@nodiscard
|
||||
---@return boolean ready
|
||||
function renderer.ui_ready() return engine.ui_ready end
|
||||
|
||||
-- handle a monitor peripheral being disconnected
|
||||
---@param device table monitor
|
||||
---@return boolean is_used if the monitor is one of the configured monitors
|
||||
function renderer.handle_disconnect(device)
|
||||
local is_used = false
|
||||
|
||||
if not engine.monitors then return false end
|
||||
|
||||
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
|
||||
|
||||
is_used = true
|
||||
engine.monitors.main = nil
|
||||
engine.ui.main_display = nil
|
||||
|
||||
iocontrol.fp_monitor_state("main", false)
|
||||
elseif engine.monitors.flow == device then
|
||||
if engine.ui.flow_display ~= nil then
|
||||
-- delete element tree and clear root UI elements
|
||||
engine.ui.flow_display.delete()
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
return is_used
|
||||
end
|
||||
|
||||
-- handle a monitor peripheral being reconnected
|
||||
---@param name string monitor name
|
||||
---@param device table 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 not engine.monitors then return false end
|
||||
|
||||
-- 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.monitors.main_name == name then
|
||||
is_used = true
|
||||
engine.monitors.main = device
|
||||
|
||||
renderer.handle_resize(name)
|
||||
elseif engine.monitors.flow_name == name then
|
||||
is_used = true
|
||||
engine.monitors.flow = device
|
||||
|
||||
renderer.handle_resize(name)
|
||||
else
|
||||
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
if monitor == name then
|
||||
is_used = true
|
||||
engine.monitors.unit_displays[idx] = device
|
||||
|
||||
renderer.handle_resize(name)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return is_used
|
||||
end
|
||||
|
||||
-- handle a monitor being resized<br>
|
||||
-- returns if this monitor is assigned + if the assigned screen still fits
|
||||
---@param name string monitor name
|
||||
---@return boolean is_used, boolean is_ok
|
||||
function renderer.handle_resize(name)
|
||||
local is_used = false
|
||||
local is_ok = true
|
||||
local ui = engine.ui
|
||||
|
||||
if not engine.monitors then return false, false end
|
||||
|
||||
if engine.monitors.main_name == name and engine.monitors.main then
|
||||
local device = engine.monitors.main ---@type table
|
||||
|
||||
-- this is necessary if the bottom left block was broken and on reconnect
|
||||
_init_display(device)
|
||||
|
||||
is_used = true
|
||||
|
||||
-- resize dmesg window if needed, but don't make it thinner
|
||||
local disp_w, disp_h = engine.monitors.main.getSize()
|
||||
local dmsg_w, _ = engine.dmesg_window.getSize()
|
||||
engine.dmesg_window.reposition(1, 1, math.max(disp_w, dmsg_w), disp_h, engine.monitors.main)
|
||||
|
||||
if ui.main_display then
|
||||
ui.main_display.delete()
|
||||
ui.main_display = nil
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state("main", true)
|
||||
|
||||
engine.dmesg_window.setVisible(not engine.ui_ready)
|
||||
|
||||
if engine.ui_ready then
|
||||
local ok = pcall(function ()
|
||||
ui.main_display = DisplayBox{window=device,fg_bg=style.root}
|
||||
main_view(ui.main_display)
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
if ui.main_display then
|
||||
ui.main_display.delete()
|
||||
ui.main_display = nil
|
||||
end
|
||||
|
||||
_print_too_small(device)
|
||||
|
||||
iocontrol.fp_monitor_state("main", false)
|
||||
is_ok = false
|
||||
end
|
||||
else engine.dmesg_window.redraw() end
|
||||
elseif engine.monitors.flow_name == name and engine.monitors.flow then
|
||||
local device = engine.monitors.flow ---@type table
|
||||
|
||||
-- this is necessary if the bottom left block was broken and on reconnect
|
||||
_init_display(device)
|
||||
|
||||
is_used = true
|
||||
|
||||
if ui.flow_display then
|
||||
ui.flow_display.delete()
|
||||
ui.flow_display = nil
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state("flow", true)
|
||||
|
||||
if engine.ui_ready then
|
||||
engine.dmesg_window.setVisible(false)
|
||||
|
||||
local ok = pcall(function ()
|
||||
ui.flow_display = DisplayBox{window=device,fg_bg=style.root}
|
||||
flow_view(ui.flow_display)
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
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
|
||||
engine.dmesg_window.setVisible(false)
|
||||
|
||||
local ok = pcall(function ()
|
||||
ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
|
||||
unit_view(ui.unit_displays[idx], idx)
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
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
|
||||
function renderer.handle_mouse(event)
|
||||
if event ~= nil then
|
||||
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.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
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return renderer
|
||||
177
coordinator/session/apisessions.lua
Normal file
177
coordinator/session/apisessions.lua
Normal file
@@ -0,0 +1,177 @@
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local pocket = require("coordinator.session.pocket")
|
||||
|
||||
local apisessions = {}
|
||||
|
||||
local self = {
|
||||
nic = nil, ---@type nic
|
||||
config = nil, ---@type crd_config
|
||||
next_id = 0,
|
||||
sessions = {}
|
||||
}
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- handle a session output queue
|
||||
---@param session pkt_session_struct
|
||||
local function _api_handle_outq(session)
|
||||
-- record handler start time
|
||||
local handle_start = util.time()
|
||||
|
||||
-- process output queue
|
||||
while session.out_queue.ready() do
|
||||
-- get a new message to process
|
||||
local msg = session.out_queue.pop()
|
||||
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- handle a packet to be sent
|
||||
self.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
|
||||
-- instruction/notification with body
|
||||
end
|
||||
end
|
||||
|
||||
-- max 100ms spent processing queue
|
||||
if util.time() - handle_start > 100 then
|
||||
log.warning("API: out queue handler exceeded 100ms queue process limit")
|
||||
log.warning(util.c("API: offending session: ", session))
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- cleanly close a session
|
||||
---@param session pkt_session_struct
|
||||
local function _shutdown(session)
|
||||
session.open = false
|
||||
session.instance.close()
|
||||
|
||||
-- send packets in out queue (namely the close packet)
|
||||
while session.out_queue.ready() do
|
||||
local msg = session.out_queue.pop()
|
||||
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then
|
||||
self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message)
|
||||
end
|
||||
end
|
||||
|
||||
log.debug(util.c("API: closed session ", session))
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
-- initialize apisessions
|
||||
---@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
|
||||
---@nodiscard
|
||||
---@param source_addr integer
|
||||
---@return pkt_session_struct|nil
|
||||
function apisessions.find_session(source_addr)
|
||||
for i = 1, #self.sessions do
|
||||
if self.sessions[i].s_addr == source_addr then return self.sessions[i] end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- establish a new API session
|
||||
---@nodiscard
|
||||
---@param source_addr integer
|
||||
---@param version string
|
||||
---@return integer session_id
|
||||
function apisessions.establish_session(source_addr, version)
|
||||
---@class pkt_session_struct
|
||||
local pkt_s = {
|
||||
open = true,
|
||||
version = version,
|
||||
s_addr = source_addr,
|
||||
in_queue = mqueue.new(),
|
||||
out_queue = mqueue.new(),
|
||||
instance = nil ---@type pkt_session
|
||||
}
|
||||
|
||||
local id = self.next_id
|
||||
|
||||
pkt_s.instance = pocket.new_session(id, source_addr, pkt_s.in_queue, pkt_s.out_queue, self.config.API_Timeout)
|
||||
table.insert(self.sessions, pkt_s)
|
||||
|
||||
local mt = {
|
||||
---@param s pkt_session_struct
|
||||
__tostring = function (s) return util.c("PKT [", id, "] (@", s.s_addr, ")") end
|
||||
}
|
||||
|
||||
setmetatable(pkt_s, mt)
|
||||
|
||||
iocontrol.fp_pkt_connected(id, version, source_addr)
|
||||
log.debug(util.c("API: established new session: ", pkt_s))
|
||||
|
||||
self.next_id = id + 1
|
||||
|
||||
-- success
|
||||
return pkt_s.instance.get_id()
|
||||
end
|
||||
|
||||
-- attempt to identify which session's watchdog timer fired
|
||||
---@param timer_event number
|
||||
function apisessions.check_all_watchdogs(timer_event)
|
||||
for i = 1, #self.sessions do
|
||||
local session = self.sessions[i] ---@type pkt_session_struct
|
||||
if session.open then
|
||||
local triggered = session.instance.check_wd(timer_event)
|
||||
if triggered then
|
||||
log.debug(util.c("API: watchdog closing session ", session, "..."))
|
||||
_shutdown(session)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- iterate all the API sessions
|
||||
function apisessions.iterate_all()
|
||||
for i = 1, #self.sessions do
|
||||
local session = self.sessions[i] ---@type pkt_session_struct
|
||||
|
||||
if session.open and session.instance.iterate() then
|
||||
_api_handle_outq(session)
|
||||
else
|
||||
session.open = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- delete all closed sessions
|
||||
function apisessions.free_all_closed()
|
||||
local f = function (session) return session.open end
|
||||
|
||||
---@param session pkt_session_struct
|
||||
local on_delete = function (session)
|
||||
log.debug(util.c("API: free'ing closed session ", session))
|
||||
end
|
||||
|
||||
util.filter_table(self.sessions, f, on_delete)
|
||||
end
|
||||
|
||||
-- close all open connections
|
||||
function apisessions.close_all()
|
||||
for i = 1, #self.sessions do
|
||||
local session = self.sessions[i] ---@type pkt_session_struct
|
||||
if session.open then _shutdown(session) end
|
||||
end
|
||||
|
||||
apisessions.free_all_closed()
|
||||
end
|
||||
|
||||
return apisessions
|
||||
251
coordinator/session/pocket.lua
Normal file
251
coordinator/session/pocket.lua
Normal file
@@ -0,0 +1,251 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local pocket = {}
|
||||
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
-- local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
|
||||
-- retry time constants in ms
|
||||
-- local INITIAL_WAIT = 1500
|
||||
-- local RETRY_PERIOD = 1000
|
||||
|
||||
local API_S_CMDS = {
|
||||
}
|
||||
|
||||
local API_S_DATA = {
|
||||
}
|
||||
|
||||
pocket.API_S_CMDS = API_S_CMDS
|
||||
pocket.API_S_DATA = API_S_DATA
|
||||
|
||||
local PERIODICS = {
|
||||
KEEP_ALIVE = 2000
|
||||
}
|
||||
|
||||
-- pocket API session
|
||||
---@nodiscard
|
||||
---@param id integer session ID
|
||||
---@param s_addr integer device source address
|
||||
---@param in_queue mqueue in message queue
|
||||
---@param out_queue mqueue out message queue
|
||||
---@param timeout number communications timeout
|
||||
function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
local log_header = "pkt_session(" .. id .. "): "
|
||||
|
||||
local self = {
|
||||
-- connection properties
|
||||
seq_num = 0,
|
||||
r_seq_num = nil,
|
||||
connected = true,
|
||||
conn_watchdog = util.new_watchdog(timeout),
|
||||
last_rtt = 0,
|
||||
-- periodic messages
|
||||
periodics = {
|
||||
last_update = 0,
|
||||
keep_alive = 0
|
||||
},
|
||||
-- when to next retry one of these requests
|
||||
retry_times = {
|
||||
},
|
||||
-- command acknowledgements
|
||||
acks = {
|
||||
},
|
||||
-- session database
|
||||
---@class api_db
|
||||
sDB = {
|
||||
}
|
||||
}
|
||||
|
||||
---@class pkt_session
|
||||
local public = {}
|
||||
|
||||
-- mark this pocket session as closed, stop watchdog
|
||||
local function _close()
|
||||
self.conn_watchdog.cancel()
|
||||
self.connected = false
|
||||
iocontrol.fp_pkt_disconnected(id)
|
||||
end
|
||||
|
||||
-- send a CRDN packet
|
||||
-----@param msg_type CRDN_TYPE
|
||||
-----@param msg table
|
||||
-- local function _send(msg_type, msg)
|
||||
-- local s_pkt = comms.scada_packet()
|
||||
-- local c_pkt = comms.crdn_packet()
|
||||
|
||||
-- c_pkt.make(msg_type, msg)
|
||||
-- s_pkt.make(self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable())
|
||||
|
||||
-- out_queue.push_packet(s_pkt)
|
||||
-- self.seq_num = self.seq_num + 1
|
||||
-- end
|
||||
|
||||
-- send a SCADA management packet
|
||||
---@param msg_type MGMT_TYPE
|
||||
---@param msg table
|
||||
local function _send_mgmt(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
|
||||
m_pkt.make(msg_type, msg)
|
||||
s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
out_queue.push_packet(s_pkt)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- handle a packet
|
||||
---@param pkt mgmt_frame|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())
|
||||
return
|
||||
else
|
||||
self.r_seq_num = pkt.scada_frame.seq_num()
|
||||
end
|
||||
|
||||
-- feed watchdog
|
||||
self.conn_watchdog.feed()
|
||||
|
||||
-- process packet
|
||||
if pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
---@cast pkt crdn_frame
|
||||
|
||||
-- handle packet by type
|
||||
if pkt.type == nil then
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type)
|
||||
end
|
||||
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
---@cast pkt mgmt_frame
|
||||
if pkt.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive reply
|
||||
if pkt.length == 2 then
|
||||
local srv_start = pkt.data[1]
|
||||
-- local api_send = pkt.data[2]
|
||||
local srv_now = util.time()
|
||||
self.last_rtt = srv_now - srv_start
|
||||
|
||||
if self.last_rtt > 750 then
|
||||
log.warning(log_header .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms")
|
||||
-- log.debug(log_header .. "PKT TT = " .. (srv_now - api_send) .. "ms")
|
||||
|
||||
iocontrol.fp_pkt_rtt(id, self.last_rtt)
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_close()
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
-- get the session ID
|
||||
---@nodiscard
|
||||
function public.get_id() return id end
|
||||
|
||||
-- get the session database
|
||||
---@nodiscard
|
||||
function public.get_db() return self.sDB end
|
||||
|
||||
-- check if a timer matches this session's watchdog
|
||||
---@nodiscard
|
||||
function public.check_wd(timer)
|
||||
return self.conn_watchdog.is_timer(timer) and self.connected
|
||||
end
|
||||
|
||||
-- close the connection
|
||||
function public.close()
|
||||
_close()
|
||||
_send_mgmt(MGMT_TYPE.CLOSE, {})
|
||||
log.info(log_header .. "session closed by server")
|
||||
end
|
||||
|
||||
-- iterate the session
|
||||
---@nodiscard
|
||||
---@return boolean connected
|
||||
function public.iterate()
|
||||
if self.connected then
|
||||
------------------
|
||||
-- handle queue --
|
||||
------------------
|
||||
|
||||
local handle_start = util.time()
|
||||
|
||||
while in_queue.ready() and self.connected do
|
||||
-- get a new message to process
|
||||
local message = in_queue.pop()
|
||||
|
||||
if message ~= nil then
|
||||
if message.qtype == mqueue.TYPE.PACKET then
|
||||
-- handle a packet
|
||||
_handle_packet(message.message)
|
||||
elseif message.qtype == mqueue.TYPE.COMMAND then
|
||||
-- handle instruction
|
||||
elseif message.qtype == mqueue.TYPE.DATA then
|
||||
-- instruction with body
|
||||
end
|
||||
end
|
||||
|
||||
-- max 100ms spent processing queue
|
||||
if util.time() - handle_start > 100 then
|
||||
log.warning(log_header .. "exceeded 100ms queue process limit")
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- exit if connection was closed
|
||||
if not self.connected then
|
||||
log.info(log_header .. "session closed by remote host")
|
||||
return self.connected
|
||||
end
|
||||
|
||||
----------------------
|
||||
-- update periodics --
|
||||
----------------------
|
||||
|
||||
local elapsed = util.time() - self.periodics.last_update
|
||||
|
||||
local periodics = self.periodics
|
||||
|
||||
-- keep alive
|
||||
|
||||
periodics.keep_alive = periodics.keep_alive + elapsed
|
||||
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
|
||||
_send_mgmt(MGMT_TYPE.KEEP_ALIVE, { util.time() })
|
||||
periodics.keep_alive = 0
|
||||
end
|
||||
|
||||
self.periodics.last_update = util.time()
|
||||
|
||||
---------------------
|
||||
-- attempt retries --
|
||||
---------------------
|
||||
|
||||
-- local rtimes = self.retry_times
|
||||
end
|
||||
|
||||
return self.connected
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return pocket
|
||||
79
coordinator/sounder.lua
Normal file
79
coordinator/sounder.lua
Normal file
@@ -0,0 +1,79 @@
|
||||
--
|
||||
-- Alarm Sounder
|
||||
--
|
||||
|
||||
local audio = require("scada-common.audio")
|
||||
local log = require("scada-common.log")
|
||||
|
||||
---@class sounder
|
||||
local sounder = {}
|
||||
|
||||
local alarm_ctl = {
|
||||
speaker = nil,
|
||||
volume = 0.5,
|
||||
stream = audio.new_stream()
|
||||
}
|
||||
|
||||
-- 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
|
||||
return sounder.continue()
|
||||
else return true end
|
||||
end
|
||||
|
||||
-- initialize the annunciator alarm system
|
||||
---@param speaker table 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.stream.stop()
|
||||
|
||||
audio.generate_tones()
|
||||
end
|
||||
|
||||
-- reconnect the speaker peripheral
|
||||
---@param speaker table speaker peripheral
|
||||
function sounder.reconnect(speaker)
|
||||
alarm_ctl.speaker = speaker
|
||||
alarm_ctl.playing = false
|
||||
alarm_ctl.stream.stop()
|
||||
end
|
||||
|
||||
-- set alarm tones
|
||||
---@param states table alarm tone commands from supervisor
|
||||
function sounder.set(states)
|
||||
-- set tone states
|
||||
for id = 1, #states do alarm_ctl.stream.set_active(id, states[id]) end
|
||||
|
||||
-- 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.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.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
|
||||
|
||||
return success
|
||||
end
|
||||
|
||||
return sounder
|
||||
426
coordinator/startup.lua
Normal file
426
coordinator/startup.lua
Normal file
@@ -0,0 +1,426 @@
|
||||
--
|
||||
-- Nuclear Generation Facility SCADA Coordinator
|
||||
--
|
||||
|
||||
require("/initenv").init_env()
|
||||
|
||||
local comms = require("scada-common.comms")
|
||||
local crash = require("scada-common.crash")
|
||||
local log = require("scada-common.log")
|
||||
local network = require("scada-common.network")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local 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 apisessions = require("coordinator.session.apisessions")
|
||||
|
||||
local COORDINATOR_VERSION = "v1.2.11"
|
||||
|
||||
local CHUNK_LOAD_DELAY_S = 30.0
|
||||
|
||||
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_crypto = coordinator.log_crypto
|
||||
|
||||
----------------------------------------
|
||||
-- get configuration
|
||||
----------------------------------------
|
||||
|
||||
-- mount connected devices (required for monitor setup)
|
||||
ppm.mount_all()
|
||||
|
||||
local wait_on_load = true
|
||||
local loaded, monitors = coordinator.load_config()
|
||||
|
||||
-- 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.LogPath, config.LogMode, config.LogDebug)
|
||||
|
||||
log.info("========================================")
|
||||
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
|
||||
log.info("========================================")
|
||||
println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<")
|
||||
|
||||
crash.set_env("coordinator", COORDINATOR_VERSION)
|
||||
|
||||
----------------------------------------
|
||||
-- main application
|
||||
----------------------------------------
|
||||
|
||||
local function main()
|
||||
----------------------------------------
|
||||
-- system startup
|
||||
----------------------------------------
|
||||
|
||||
-- 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)
|
||||
|
||||
-- init renderer
|
||||
renderer.legacy_disable_flow_view(config.DisableFlowView)
|
||||
renderer.set_displays(monitors)
|
||||
renderer.init_displays()
|
||||
renderer.init_dmesg()
|
||||
|
||||
-- lets get started!
|
||||
log.info("monitors ready, dmesg output incoming...")
|
||||
|
||||
log_graphics("displays connected and reset")
|
||||
log_sys("system start on " .. os.date("%c"))
|
||||
log_boot("starting " .. COORDINATOR_VERSION)
|
||||
|
||||
----------------------------------------
|
||||
-- setup alarm sounder subsystem
|
||||
----------------------------------------
|
||||
|
||||
local speaker = ppm.get_device("speaker")
|
||||
if speaker == nil then
|
||||
log_boot("annunciator alarm speaker not found")
|
||||
println("startup> speaker not found")
|
||||
log.fatal("no annunciator alarm speaker found")
|
||||
return
|
||||
else
|
||||
local sounder_start = util.time_ms()
|
||||
log_boot("annunciator alarm speaker connected")
|
||||
sounder.init(speaker, config.SpeakerVolume)
|
||||
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
|
||||
log_sys("annunciator alarm configured")
|
||||
iocontrol.fp_has_speaker(true)
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
-- setup communications
|
||||
----------------------------------------
|
||||
|
||||
-- message authentication init
|
||||
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
|
||||
log_comms("wireless modem not found")
|
||||
println("startup> wireless modem not found")
|
||||
log.fatal("no wireless modem on startup")
|
||||
return
|
||||
else
|
||||
log_comms("wireless modem connected")
|
||||
iocontrol.fp_has_modem(true)
|
||||
end
|
||||
|
||||
-- create connection watchdog
|
||||
local conn_watchdog = util.new_watchdog(config.SVR_Timeout)
|
||||
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, 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
|
||||
----------------------------------------
|
||||
|
||||
log_graphics("starting front panel UI...")
|
||||
|
||||
local fp_ok, fp_message = renderer.try_start_fp()
|
||||
if not fp_ok then
|
||||
log_graphics(util.c("front panel UI error: ", fp_message))
|
||||
println_ts("front panel UI creation failed")
|
||||
log.fatal(util.c("front panel GUI render failed with error ", fp_message))
|
||||
return
|
||||
else log_graphics("front panel ready") end
|
||||
|
||||
-- start up the main UI
|
||||
---@return boolean ui_ok started ok
|
||||
local function start_main_ui()
|
||||
log_graphics("starting main UI...")
|
||||
|
||||
local draw_start = util.time_ms()
|
||||
|
||||
local ui_ok, ui_message = renderer.try_start_ui()
|
||||
if not ui_ok then
|
||||
log_graphics(util.c("main UI error: ", ui_message))
|
||||
log.fatal(util.c("main GUI render failed with error ", ui_message))
|
||||
else
|
||||
log_graphics("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
|
||||
end
|
||||
|
||||
return ui_ok
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
-- main event loop
|
||||
----------------------------------------
|
||||
|
||||
local link_failed = false
|
||||
local ui_ok = true
|
||||
local date_format = util.trinary(config.Time24Hour, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y")
|
||||
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
|
||||
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 == "monitor_resize" then
|
||||
local is_used, is_ok = renderer.handle_resize(param1)
|
||||
if is_used then
|
||||
log_sys(util.c("configured monitor ", param1, " resized, ", util.trinary(is_ok, "display still fits", "display no longer fits")))
|
||||
end
|
||||
elseif event == "timer" then
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
|
||||
-- toggle heartbeat
|
||||
iocontrol.heartbeat()
|
||||
|
||||
-- maintain connection
|
||||
if nic.is_connected() then
|
||||
local ok, start_ui = coord_comms.try_connect()
|
||||
if not ok then
|
||||
link_failed = true
|
||||
log_sys("supervisor connection failed, shutting down...")
|
||||
log.fatal("failed to connect to supervisor")
|
||||
break
|
||||
elseif start_ui then
|
||||
log_sys("supervisor connected, proceeding to main UI start")
|
||||
ui_ok = start_main_ui()
|
||||
if not ui_ok then break end
|
||||
end
|
||||
end
|
||||
|
||||
-- iterate sessions
|
||||
apisessions.iterate_all()
|
||||
|
||||
-- free any closed sessions
|
||||
apisessions.free_all_closed()
|
||||
|
||||
-- update date and time string for main display
|
||||
if coord_comms.is_linked() then
|
||||
iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format))
|
||||
end
|
||||
|
||||
loop_clock.start()
|
||||
elseif conn_watchdog.is_timer(param1) then
|
||||
-- supervisor watchdog timeout
|
||||
log_comms("supervisor server timeout")
|
||||
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
else
|
||||
-- a non-clock/main watchdog timer event
|
||||
|
||||
-- check API watchdogs
|
||||
apisessions.check_all_watchdogs(param1)
|
||||
|
||||
-- notify timer callback dispatcher
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
-- got a packet
|
||||
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
|
||||
-- handle then check if it was a disconnect
|
||||
if coord_comms.handle_packet(packet) then
|
||||
log_comms("supervisor closed connection")
|
||||
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
end
|
||||
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
|
||||
event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
|
||||
-- handle a mouse event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
elseif event == "speaker_audio_empty" then
|
||||
-- handle speaker buffer emptied
|
||||
sounder.continue()
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
-- handle supervisor connection
|
||||
coord_comms.try_connect(true)
|
||||
|
||||
if coord_comms.is_linked() then
|
||||
log_comms("terminate requested, closing supervisor connection...")
|
||||
else link_failed = true end
|
||||
|
||||
coord_comms.close()
|
||||
log_comms("supervisor connection closed")
|
||||
|
||||
-- handle API sessions
|
||||
log_comms("closing api sessions...")
|
||||
apisessions.close_all()
|
||||
log_comms("api sessions closed")
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
-- close on error exit (such as UI error)
|
||||
if coord_comms.is_linked() then coord_comms.close() end
|
||||
|
||||
println_ts("exited")
|
||||
log.info("exited")
|
||||
end
|
||||
|
||||
if not xpcall(main, crash.handler) then
|
||||
pcall(renderer.close_ui)
|
||||
pcall(renderer.close_fp)
|
||||
pcall(sounder.stop)
|
||||
crash.exit()
|
||||
else
|
||||
log.close()
|
||||
end
|
||||
54
coordinator/ui/components/boiler.lua
Normal file
54
coordinator/ui/components/boiler.lua
Normal file
@@ -0,0 +1,54 @@
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
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 cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local text_fg_bg = style.text_colors
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
-- new boiler view
|
||||
---@param root graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param ps psil ps interface
|
||||
local function new_view(root, x, y, ps)
|
||||
local 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=db.temp_label,format="%10.2f",value=0,commas=true,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}
|
||||
|
||||
status.register(ps, "computed_status", status.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}
|
||||
|
||||
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}
|
||||
local steam = VerticalBar{parent=boiler,x=27,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
|
||||
local ccool = VerticalBar{parent=boiler,x=28,y=1,fg_bg=cpair(colors.lightBlue,colors.gray),height=4,width=1}
|
||||
|
||||
hcool.register(ps, "hcool_fill", hcool.update)
|
||||
water.register(ps, "water_fill", water.update)
|
||||
steam.register(ps, "steam_fill", steam.update)
|
||||
ccool.register(ps, "ccool_fill", ccool.update)
|
||||
end
|
||||
|
||||
return new_view
|
||||
95
coordinator/ui/components/imatrix.lua
Normal file
95
coordinator/ui/components/imatrix.lua
Normal file
@@ -0,0 +1,95 @@
|
||||
local util = require("scada-common.util")
|
||||
|
||||
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 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 cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local text_fg_bg = style.text_colors
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
-- new induction matrix view
|
||||
---@param root graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@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 title = "INDUCTION MATRIX"
|
||||
if type(id) == "number" then title = title .. id end
|
||||
|
||||
local matrix = Div{parent=root,fg_bg=style.root,width=33,height=24,x=x,y=y}
|
||||
|
||||
TextBox{parent=matrix,text=" ",width=33,height=1,x=1,y=1,fg_bg=style.lg_gray}
|
||||
TextBox{parent=matrix,text=title,alignment=ALIGN.CENTER,width=33,height=1,x=1,y=2,fg_bg=style.lg_gray}
|
||||
|
||||
local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3}
|
||||
|
||||
local label_fg_bg = cpair(colors.gray, colors.lightGray)
|
||||
|
||||
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
|
||||
local energy = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
local capacity = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Capacity:",format="%8.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
local input = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="Input: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
|
||||
local output = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Output: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
|
||||
|
||||
local avg_chg = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Avg. Chg:",format="%8.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
local avg_in = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="Avg. In: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
|
||||
local avg_out = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Avg. Out:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
|
||||
|
||||
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)
|
||||
|
||||
avg_chg.register(ps, "avg_charge", avg_chg.update)
|
||||
avg_in.register(ps, "avg_inflow", avg_in.update)
|
||||
avg_out.register(ps, "avg_outflow", avg_out.update)
|
||||
|
||||
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill:",unit="%",format="%8.2f",value=0,width=18,fg_bg=text_fg_bg}
|
||||
|
||||
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}
|
||||
|
||||
cells.register(ps, "cells", cells.update)
|
||||
providers.register(ps, "providers", providers.update)
|
||||
fill.register(ps, "energy_fill", function (val) fill.update(val * 100) end)
|
||||
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end)
|
||||
|
||||
local charge = VerticalBar{parent=rect,x=2,y=2,fg_bg=cpair(colors.green,colors.gray),height=17,width=4}
|
||||
local in_cap = VerticalBar{parent=rect,x=7,y=12,fg_bg=cpair(colors.red,colors.gray),height=7,width=1}
|
||||
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}
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
charge.register(ps, "energy_fill", charge.update)
|
||||
in_cap.register(ps, "last_input", function (val) in_cap.update(calc_saturation(val)) end)
|
||||
out_cap.register(ps, "last_output", function (val) out_cap.update(calc_saturation(val)) end)
|
||||
end
|
||||
|
||||
return new_view
|
||||
53
coordinator/ui/components/pkt_entry.lua
Normal file
53
coordinator/ui/components/pkt_entry.lua
Normal file
@@ -0,0 +1,53 @@
|
||||
--
|
||||
-- Pocket Connection Entry
|
||||
--
|
||||
|
||||
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 DataIndicator = require("graphics.elements.indicators.data")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local text_fg_bg = style.text_colors
|
||||
local lg_wh = style.lg_white
|
||||
|
||||
-- create a pocket list entry
|
||||
---@param parent graphics_element parent
|
||||
---@param id integer PKT session ID
|
||||
local function init(parent, id)
|
||||
local ps = iocontrol.get_db().fp.ps
|
||||
|
||||
-- root div
|
||||
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
|
||||
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.bw_fg_bg}
|
||||
|
||||
local ps_prefix = "pkt_" .. id .. "_"
|
||||
|
||||
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=text_fg_bg}
|
||||
local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,height=1,fg_bg=text_fg_bg,nav_active=cpair(colors.gray,colors.black)}
|
||||
TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=text_fg_bg}
|
||||
pkt_addr.register(ps, ps_prefix .. "addr", pkt_addr.set_value)
|
||||
|
||||
TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1}
|
||||
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=lg_wh}
|
||||
pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value)
|
||||
|
||||
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1}
|
||||
local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=lg_wh}
|
||||
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=lg_wh}
|
||||
pkt_rtt.register(ps, ps_prefix .. "rtt", pkt_rtt.update)
|
||||
pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor)
|
||||
|
||||
return root
|
||||
end
|
||||
|
||||
return init
|
||||
366
coordinator/ui/components/process_ctl.lua
Normal file
366
coordinator/ui/components/process_ctl.lua
Normal file
@@ -0,0 +1,366 @@
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local process = require("coordinator.process")
|
||||
|
||||
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 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 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 ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local lu_cpair = style.lu_colors
|
||||
local hzd_fg_bg = style.hzd_fg_bg
|
||||
local dis_colors = style.dis_colors
|
||||
|
||||
local gry_wht = style.gray_white
|
||||
|
||||
local ind_grn = style.ind_grn
|
||||
local ind_yel = style.ind_yel
|
||||
local ind_red = style.ind_red
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local period = core.flasher.PERIOD
|
||||
|
||||
-- new process control view
|
||||
---@param root graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
local function new_view(root, x, y)
|
||||
assert(root.get_height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||
|
||||
local black = cpair(colors.black, colors.black)
|
||||
local blk_brn = cpair(colors.black, colors.brown)
|
||||
local blk_pur = cpair(colors.black, colors.purple)
|
||||
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
|
||||
local 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}
|
||||
|
||||
facility.scram_ack = scram.on_response
|
||||
facility.ack_alarms_ack = ack_a.on_response
|
||||
|
||||
local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=ind_grn}
|
||||
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=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)
|
||||
ind_mat.register(facility.induction_ps_tbl[1], "computed_status", function (status) ind_mat.update(status > 1) end)
|
||||
sps.register(facility.sps_ps_tbl[1], "computed_status", function (status) sps.update(status > 1) end)
|
||||
|
||||
main.line_break()
|
||||
|
||||
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)
|
||||
auto_ramp.register(facility.ps, "auto_ramping", auto_ramp.update)
|
||||
auto_sat.register(facility.ps, "auto_saturated", auto_sat.update)
|
||||
|
||||
main.line_break()
|
||||
|
||||
local auto_scram = IndicatorLight{parent=main,label="Automatic SCRAM",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local matrix_dc = IndicatorLight{parent=main,label="Matrix Disconnected",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
|
||||
local matrix_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_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}
|
||||
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}
|
||||
rtu_count.register(facility.ps, "rtu_count", rtu_count.update)
|
||||
|
||||
---------------------
|
||||
-- process control --
|
||||
---------------------
|
||||
|
||||
local proc = Div{parent=main,width=103,height=24,x=27,y=1}
|
||||
|
||||
-----------------------------
|
||||
-- process control targets --
|
||||
-----------------------------
|
||||
|
||||
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=blk_pur}
|
||||
TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2}
|
||||
|
||||
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=gry_wht}
|
||||
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=burn_target,x=18,y=2,text="mB/t"}
|
||||
local burn_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=blk_pur}
|
||||
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
|
||||
|
||||
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=gry_wht}
|
||||
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=chg_target,x=18,y=2,text="MFE"}
|
||||
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
|
||||
|
||||
c_target.register(facility.ps, "process_charge_target", c_target.set_value)
|
||||
cur_charge.register(facility.induction_ps_tbl[1], "energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end)
|
||||
|
||||
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur}
|
||||
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
|
||||
|
||||
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=gry_wht}
|
||||
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=gen_target,x=18,y=2,text="kFE/t"}
|
||||
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
|
||||
|
||||
g_target.register(facility.ps, "process_gen_target", g_target.set_value)
|
||||
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end)
|
||||
|
||||
-----------------
|
||||
-- unit limits --
|
||||
-----------------
|
||||
|
||||
local limit_div = Div{parent=proc,width=21,height=19,x=34,y=6}
|
||||
|
||||
local rate_limits = {}
|
||||
|
||||
for i = 1, 4 do
|
||||
local unit
|
||||
local tag_fg_bg = gry_wht
|
||||
local lim_fg_bg = style.lg_white
|
||||
local ctl_fg = colors.lightGray
|
||||
local cur_fg_bg = style.lg_white
|
||||
local cur_lu = colors.lightGray
|
||||
|
||||
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 = blk_brn
|
||||
cur_lu = colors.black
|
||||
end
|
||||
|
||||
local _y = ((i - 1) * 5) + 1
|
||||
|
||||
local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
|
||||
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2}
|
||||
|
||||
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(ctl_fg,colors.white)}
|
||||
local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=lim_fg_bg}
|
||||
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1}
|
||||
|
||||
local 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}
|
||||
|
||||
if i <= facility.num_units then
|
||||
rate_limits[i] = lim
|
||||
rate_limits[i].register(unit.unit_ps, "max_burn", rate_limits[i].set_max)
|
||||
rate_limits[i].register(unit.unit_ps, "burn_limit", rate_limits[i].set_value)
|
||||
|
||||
cur_burn.register(unit.unit_ps, "act_burn_rate", cur_burn.update)
|
||||
else
|
||||
lim.disable()
|
||||
end
|
||||
end
|
||||
|
||||
-------------------
|
||||
-- unit statuses --
|
||||
-------------------
|
||||
|
||||
local stat_div = Div{parent=proc,width=22,height=24,x=57,y=6}
|
||||
|
||||
for i = 1, 4 do
|
||||
local tag_fg_bg = gry_wht
|
||||
local ind_fg_bg = style.lg_white
|
||||
local ind_off = colors.lightGray
|
||||
|
||||
if i <= facility.num_units then
|
||||
tag_fg_bg = cpair(colors.black, colors.cyan)
|
||||
ind_fg_bg = bw_fg_bg
|
||||
ind_off = colors.gray
|
||||
end
|
||||
|
||||
local _y = ((i - 1) * 5) + 1
|
||||
|
||||
local unit_tag = Div{parent=stat_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.." 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}
|
||||
|
||||
if i <= facility.num_units then
|
||||
local unit = units[i] ---@type ioctl_unit
|
||||
|
||||
ready.register(unit.unit_ps, "U_AutoReady", ready.update)
|
||||
degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update)
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------
|
||||
-- controls and status --
|
||||
-------------------------
|
||||
|
||||
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
|
||||
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.gray,colors.white),select_color=colors.purple}
|
||||
|
||||
mode.register(facility.ps, "process_mode", mode.set_value)
|
||||
|
||||
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=gry_wht}
|
||||
|
||||
stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value)
|
||||
stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value)
|
||||
|
||||
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=gry_wht}
|
||||
|
||||
-- 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)
|
||||
end
|
||||
|
||||
-- start automatic control after saving process control settings
|
||||
local function _start_auto()
|
||||
_save_cfg()
|
||||
process.start_auto()
|
||||
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}
|
||||
|
||||
facility.start_ack = start.on_response
|
||||
facility.stop_ack = stop.on_response
|
||||
|
||||
function facility.save_cfg_ack(ack)
|
||||
tcd.dispatch(0.2, function () save.on_response(ack) end)
|
||||
end
|
||||
|
||||
start.register(facility.ps, "auto_ready", function (ready)
|
||||
if ready and (not facility.auto_active) then start.enable() else start.disable() end
|
||||
end)
|
||||
|
||||
-- REGISTER_NOTE: for optimization/brevity, due to not deleting anything but the whole element tree when it comes
|
||||
-- to the process control display and coordinator GUI as a whole, child elements will not directly be registered here
|
||||
-- (preventing garbage collection until the parent 'proc' is deleted)
|
||||
proc.register(facility.ps, "auto_active", function (active)
|
||||
if active then
|
||||
b_target.disable()
|
||||
c_target.disable()
|
||||
g_target.disable()
|
||||
|
||||
mode.disable()
|
||||
start.disable()
|
||||
|
||||
for i = 1, #rate_limits do rate_limits[i].disable() end
|
||||
else
|
||||
b_target.enable()
|
||||
c_target.enable()
|
||||
g_target.enable()
|
||||
|
||||
mode.enable()
|
||||
if facility.auto_ready then start.enable() end
|
||||
|
||||
for i = 1, #rate_limits do rate_limits[i].enable() end
|
||||
end
|
||||
end)
|
||||
|
||||
------------------------------
|
||||
-- waste production control --
|
||||
------------------------------
|
||||
|
||||
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
|
||||
|
||||
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=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)
|
||||
waste_m.register(unit.unit_ps, "U_WasteProduct", waste_m.update)
|
||||
end
|
||||
|
||||
local waste_sel = Div{parent=proc,width=21,height=24,x=81,y=1}
|
||||
|
||||
TextBox{parent=waste_sel,text=" ",width=21,height=1,x=1,y=1,fg_bg=blk_brn}
|
||||
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.brown)}
|
||||
|
||||
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
|
||||
local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17}
|
||||
|
||||
status.register(facility.ps, "current_waste_product", status.update)
|
||||
|
||||
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.gray,colors.white),select_color=colors.brown}
|
||||
local pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.green,colors.black)}
|
||||
|
||||
waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value)
|
||||
pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value)
|
||||
|
||||
local fb_active = IndicatorLight{parent=rect,x=2,y=9,label="Fallback Active",colors=ind_wht}
|
||||
|
||||
fb_active.register(facility.ps, "pu_fallback_active", fb_active.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}
|
||||
|
||||
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=17,text="Antimatter Rate",height=1,width=17,fg_bg=style.label}
|
||||
local am_rate = DataIndicator{parent=rect,x=2,label="",unit="\xb5B/t",format="%12d",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
return new_view
|
||||
76
coordinator/ui/components/reactor.lua
Normal file
76
coordinator/ui/components/reactor.lua
Normal file
@@ -0,0 +1,76 @@
|
||||
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 DataIndicator = require("graphics.elements.indicators.data")
|
||||
local HorizontalBar = require("graphics.elements.indicators.hbar")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local text_fg_bg = style.text_colors
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
-- create new reactor view
|
||||
---@param root graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param ps psil ps interface
|
||||
local function new_view(root, x, y, ps)
|
||||
local 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=db.temp_label,format="%10.2f",value=0,commas=true,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}
|
||||
|
||||
status.register(ps, "computed_status", status.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}
|
||||
|
||||
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 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}
|
||||
|
||||
ccool.register(ps, "ccool_type", function (type)
|
||||
if type == types.FLUID.SODIUM then
|
||||
ccool.recolor(cpair(colors.lightBlue, colors.gray))
|
||||
else
|
||||
ccool.recolor(cpair(colors.blue, colors.gray))
|
||||
end
|
||||
end)
|
||||
|
||||
hcool.register(ps, "hcool_type", function (type)
|
||||
if type == types.FLUID.SUPERHEATED_SODIUM then
|
||||
hcool.recolor(cpair(colors.orange, colors.gray))
|
||||
else
|
||||
hcool.recolor(cpair(colors.white, colors.gray))
|
||||
end
|
||||
end)
|
||||
|
||||
fuel.register(ps, "fuel_fill", fuel.update)
|
||||
ccool.register(ps, "ccool_fill", ccool.update)
|
||||
hcool.register(ps, "hcool_fill", hcool.update)
|
||||
waste.register(ps, "waste_fill", waste.update)
|
||||
end
|
||||
|
||||
return new_view
|
||||
47
coordinator/ui/components/turbine.lua
Normal file
47
coordinator/ui/components/turbine.lua
Normal file
@@ -0,0 +1,47 @@
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
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 cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local text_fg_bg = style.text_colors
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
-- new turbine view
|
||||
---@param root graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param ps psil ps interface
|
||||
local function new_view(root, x, y, ps)
|
||||
local 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}
|
||||
|
||||
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, "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}
|
||||
|
||||
steam.register(ps, "steam_fill", steam.update)
|
||||
energy.register(ps, "energy_fill", energy.update)
|
||||
end
|
||||
|
||||
return new_view
|
||||
531
coordinator/ui/components/unit_detail.lua
Normal file
531
coordinator/ui/components/unit_detail.lua
Normal file
@@ -0,0 +1,531 @@
|
||||
--
|
||||
-- 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 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 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 ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local lu_cpair = style.lu_colors
|
||||
local hzd_fg_bg = style.hzd_fg_bg
|
||||
local dis_colors = style.dis_colors
|
||||
|
||||
local gry_wht = style.gray_white
|
||||
|
||||
local ind_grn = style.ind_grn
|
||||
local ind_yel = style.ind_yel
|
||||
local ind_red = style.ind_red
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local period = core.flasher.PERIOD
|
||||
|
||||
-- create a unit view
|
||||
---@param parent graphics_element parent
|
||||
---@param id integer
|
||||
local function init(parent, id)
|
||||
local db = iocontrol.get_db()
|
||||
local unit = db.units[id] ---@type ioctl_unit
|
||||
local f_ps = db.facility.ps
|
||||
|
||||
local main = Div{parent=parent,x=1,y=1}
|
||||
|
||||
if unit == nil then return main end
|
||||
|
||||
local u_ps = unit.unit_ps
|
||||
local b_ps = unit.boiler_ps_tbl
|
||||
local t_ps = unit.turbine_ps_tbl
|
||||
|
||||
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
|
||||
-----------------------------
|
||||
-- main stats and core map --
|
||||
-----------------------------
|
||||
|
||||
local core_map = CoreMap{parent=main,x=2,y=3,reactor_l=18,reactor_w=18}
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
|
||||
local fuel = VerticalBar{parent=main,x=2,y=23,fg_bg=cpair(colors.black,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}
|
||||
|
||||
fuel.register(u_ps, "fuel_fill", fuel.update)
|
||||
ccool.register(u_ps, "ccool_fill", ccool.update)
|
||||
hcool.register(u_ps, "hcool_fill", hcool.update)
|
||||
waste.register(u_ps, "waste_fill", waste.update)
|
||||
|
||||
ccool.register(u_ps, "ccool_type", function (type)
|
||||
if type == types.FLUID.SODIUM then
|
||||
ccool.recolor(cpair(colors.lightBlue, colors.gray))
|
||||
else
|
||||
ccool.recolor(cpair(colors.blue, colors.gray))
|
||||
end
|
||||
end)
|
||||
|
||||
hcool.register(u_ps, "hcool_type", function (type)
|
||||
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 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=bw_fg_bg}
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
radiation.register(u_ps, "radiation", radiation.update)
|
||||
|
||||
-------------------
|
||||
-- system status --
|
||||
-------------------
|
||||
|
||||
local u_stat = Rectangle{parent=main,border=border(1,colors.gray,true),thin=true,width=33,height=4,x=46,y=3,fg_bg=bw_fg_bg}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=ALIGN.CENTER,fg_bg=gry_wht}
|
||||
|
||||
stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value)
|
||||
stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value)
|
||||
|
||||
-----------------
|
||||
-- annunciator --
|
||||
-----------------
|
||||
|
||||
-- annunciator colors (generally) per IAEA-TECDOC-812 recommendations
|
||||
|
||||
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=ind_wht}
|
||||
local rad_mon = TriIndicatorLight{parent=annunciator,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
|
||||
|
||||
plc_online.register(u_ps, "PLCOnline", plc_online.update)
|
||||
plc_hbeat.register(u_ps, "PLCHeartbeat", plc_hbeat.update)
|
||||
rad_mon.register(u_ps, "RadiationMonitor", rad_mon.update)
|
||||
|
||||
annunciator.line_break()
|
||||
|
||||
-- operating state
|
||||
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=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)
|
||||
r_ascrm.register(u_ps, "AutoReactorSCRAM", r_ascrm.update)
|
||||
rad_wrn.register(u_ps, "RadiationWarning", rad_wrn.update)
|
||||
r_rtrip.register(u_ps, "RCPTrip", r_rtrip.update)
|
||||
r_cflow.register(u_ps, "RCSFlowLow", r_cflow.update)
|
||||
r_clow.register(u_ps, "CoolantLevelLow", r_clow.update)
|
||||
r_temp.register(u_ps, "ReactorTempHigh", r_temp.update)
|
||||
r_rhdt.register(u_ps, "ReactorHighDeltaT", r_rhdt.update)
|
||||
r_firl.register(u_ps, "FuelInputRateLow", r_firl.update)
|
||||
r_wloc.register(u_ps, "WasteLineOcclusion", r_wloc.update)
|
||||
r_hsrt.register(u_ps, "HighStartupRate", r_hsrt.update)
|
||||
|
||||
-- RPS annunciator panel
|
||||
|
||||
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=8}
|
||||
local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9}
|
||||
local rps_annunc = Div{parent=rps,width=31,height=10,x=2,y=1}
|
||||
|
||||
local rps_trp = IndicatorLight{parent=rps_annunc,label="RPS Trip",colors=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=cpair(colors.orange,colors.gray),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)
|
||||
rps_exh.register(u_ps, "ex_hcool", rps_exh.update)
|
||||
rps_exw.register(u_ps, "ex_waste", rps_exw.update)
|
||||
rps_tmp.register(u_ps, "high_temp", rps_tmp.update)
|
||||
rps_nof.register(u_ps, "no_fuel", rps_nof.update)
|
||||
rps_loc.register(u_ps, "low_cool", rps_loc.update)
|
||||
rps_flt.register(u_ps, "fault", rps_flt.update)
|
||||
rps_tmo.register(u_ps, "timeout", rps_tmo.update)
|
||||
rps_sfl.register(u_ps, "sys_fail", rps_sfl.update)
|
||||
|
||||
-- cooling annunciator panel
|
||||
|
||||
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=22}
|
||||
local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23}
|
||||
local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1}
|
||||
local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7}
|
||||
|
||||
local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=ind_yel}
|
||||
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=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)
|
||||
c_cfm.register(u_ps, "CoolantFeedMismatch", c_cfm.update)
|
||||
c_brm.register(u_ps, "BoilRateMismatch", c_brm.update)
|
||||
c_sfm.register(u_ps, "SteamFeedMismatch", c_sfm.update)
|
||||
c_mwrf.register(u_ps, "MaxWaterReturnFeed", c_mwrf.update)
|
||||
|
||||
local available_space = 16 - (unit.num_boilers * 2 + unit.num_turbines * 4)
|
||||
|
||||
local function _add_space()
|
||||
-- if we have some extra space, add padding
|
||||
rcs_tags.line_break()
|
||||
rcs_annunc.line_break()
|
||||
end
|
||||
|
||||
-- boiler annunciator panel(s)
|
||||
|
||||
if available_space > 0 then _add_space() end
|
||||
|
||||
if unit.num_boilers > 0 then
|
||||
TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
|
||||
b1_wll.register(b_ps[1], "WaterLevelLow", b1_wll.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
|
||||
b1_hr.register(b_ps[1], "HeatingRateLow", b1_hr.update)
|
||||
end
|
||||
if unit.num_boilers > 1 then
|
||||
-- note, can't (shouldn't for sure...) have 0 turbines
|
||||
if (available_space > 2 and unit.num_turbines == 1) or
|
||||
(available_space > 3 and unit.num_turbines == 2) or
|
||||
(available_space > 4) then
|
||||
_add_space()
|
||||
end
|
||||
|
||||
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=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=ind_yel}
|
||||
b2_hr.register(b_ps[2], "HeatingRateLow", b2_hr.update)
|
||||
end
|
||||
|
||||
-- turbine annunciator panels
|
||||
|
||||
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}
|
||||
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=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=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=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
|
||||
if (available_space > 2 and unit.num_turbines == 2) or available_space > 3 then
|
||||
_add_space()
|
||||
end
|
||||
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
||||
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=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=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=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}
|
||||
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=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=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=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update)
|
||||
end
|
||||
|
||||
----------------------
|
||||
-- reactor controls --
|
||||
----------------------
|
||||
|
||||
local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=gry_wht}
|
||||
local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=burn_control,x=9,y=2,text="mB/t"}
|
||||
|
||||
local set_burn = function () unit.set_burn(burn_rate.get_value()) end
|
||||
local set_burn_btn = PushButton{parent=burn_control,x=14,y=2,text="SET",min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=style.wh_gray,dis_fg_bg=dis_colors,callback=set_burn}
|
||||
|
||||
burn_rate.register(u_ps, "burn_rate", burn_rate.set_value)
|
||||
burn_rate.register(u_ps, "max_burn", burn_rate.set_max)
|
||||
|
||||
local start = HazardButton{parent=main,x=2,y=28,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=unit.start,fg_bg=hzd_fg_bg}
|
||||
local ack_a = HazardButton{parent=main,x=12,y=32,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=unit.ack_alarms,fg_bg=hzd_fg_bg}
|
||||
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
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
start.register(u_ps, "status", start_button_en_check)
|
||||
start.register(u_ps, "rps_tripped", start_button_en_check)
|
||||
start.register(u_ps, "auto_group_id", start_button_en_check)
|
||||
start.register(u_ps, "AutoControl", start_button_en_check)
|
||||
|
||||
reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
|
||||
|
||||
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=48}
|
||||
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
|
||||
local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1}
|
||||
|
||||
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.waste.unit_opts,callback=unit.set_waste,min_width=6}
|
||||
|
||||
waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)
|
||||
|
||||
----------------------
|
||||
-- alarm management --
|
||||
----------------------
|
||||
|
||||
local alarm_panel = Div{parent=main,x=2,y=36,width=29,height=16,fg_bg=bw_fg_bg}
|
||||
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
|
||||
a_brc.register(u_ps, "Alarm_1", a_brc.update)
|
||||
a_rad.register(u_ps, "Alarm_2", a_rad.update)
|
||||
a_dmg.register(u_ps, "Alarm_4", a_dmg.update)
|
||||
|
||||
a_rcl.register(u_ps, "Alarm_3", a_rcl.update)
|
||||
a_rcd.register(u_ps, "Alarm_5", a_rcd.update)
|
||||
a_rot.register(u_ps, "Alarm_6", a_rot.update)
|
||||
a_rht.register(u_ps, "Alarm_7", a_rht.update)
|
||||
a_rwl.register(u_ps, "Alarm_8", a_rwl.update)
|
||||
a_rwh.register(u_ps, "Alarm_9", a_rwh.update)
|
||||
|
||||
a_rps.register(u_ps, "Alarm_10", a_rps.update)
|
||||
a_clt.register(u_ps, "Alarm_11", a_clt.update)
|
||||
a_tbt.register(u_ps, "Alarm_12", a_tbt.update)
|
||||
|
||||
-- ack's and resets
|
||||
|
||||
local c = unit.alarm_callbacks
|
||||
local ack_fg_bg = cpair(colors.black, colors.orange)
|
||||
local rst_fg_bg = cpair(colors.black, colors.lime)
|
||||
local active_fg_bg = cpair(colors.white, colors.gray)
|
||||
|
||||
PushButton{parent=alarm_panel,x=2,y=2,text="\x13",callback=c.c_breach.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=4,y=2,text="R",callback=c.c_breach.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=2,y=3,text="\x13",callback=c.radiation.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=4,y=3,text="R",callback=c.radiation.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=2,y=4,text="\x13",callback=c.dmg_crit.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=4,y=4,text="R",callback=c.dmg_crit.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
|
||||
|
||||
PushButton{parent=alarm_panel,x=2,y=6,text="\x13",callback=c.r_lost.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=4,y=6,text="R",callback=c.r_lost.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=2,y=7,text="\x13",callback=c.damage.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=4,y=7,text="R",callback=c.damage.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=2,y=8,text="\x13",callback=c.over_temp.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=4,y=8,text="R",callback=c.over_temp.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=2,y=9,text="\x13",callback=c.high_temp.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=4,y=9,text="R",callback=c.high_temp.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=2,y=10,text="\x13",callback=c.waste_leak.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=4,y=10,text="R",callback=c.waste_leak.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=2,y=11,text="\x13",callback=c.waste_high.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=4,y=11,text="R",callback=c.waste_high.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
|
||||
|
||||
PushButton{parent=alarm_panel,x=2,y=13,text="\x13",callback=c.rps_trans.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=4,y=13,text="R",callback=c.rps_trans.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=2,y=14,text="\x13",callback=c.rcs_trans.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=4,y=14,text="R",callback=c.rcs_trans.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=2,y=15,text="\x13",callback=c.t_trip.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
|
||||
PushButton{parent=alarm_panel,x=4,y=15,text="R",callback=c.t_trip.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
|
||||
|
||||
-- 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)}
|
||||
|
||||
--------------------------------
|
||||
-- automatic control settings --
|
||||
--------------------------------
|
||||
|
||||
TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,height=1,x=32,y=36}
|
||||
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.gray,colors.white),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=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}
|
||||
|
||||
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=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)
|
||||
|
||||
-- update standby indicator
|
||||
a_stb.register(u_ps, "status", function (active)
|
||||
a_stb.update(unit.annunciator.AutoControl and (not active))
|
||||
end)
|
||||
a_stb.register(u_ps, "AutoControl", function (auto_active)
|
||||
if auto_active then
|
||||
a_stb.update(unit.reactor_data.mek_status.status == false)
|
||||
else a_stb.update(false) end
|
||||
end)
|
||||
|
||||
-- enable/disable controls based on group assignment (start button is separate)
|
||||
burn_rate.register(u_ps, "auto_group_id", function (gid)
|
||||
if gid == 0 then burn_rate.enable() else burn_rate.disable() end
|
||||
end)
|
||||
set_burn_btn.register(u_ps, "auto_group_id", function (gid)
|
||||
if gid == 0 then set_burn_btn.enable() else set_burn_btn.disable() end
|
||||
end)
|
||||
|
||||
-- can't change group if auto is engaged regardless of if this unit is part of auto control
|
||||
set_grp_btn.register(f_ps, "auto_active", function (auto_active)
|
||||
if auto_active then set_grp_btn.disable() else set_grp_btn.enable() end
|
||||
end)
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return init
|
||||
223
coordinator/ui/components/unit_flow.lua
Normal file
223
coordinator/ui/components/unit_flow.lua
Normal file
@@ -0,0 +1,223 @@
|
||||
--
|
||||
-- Basic Unit Flow Overview
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local PipeNetwork = require("graphics.elements.pipenet")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
|
||||
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local sprintf = util.sprintf
|
||||
|
||||
local border = core.border
|
||||
local pipe = core.pipe
|
||||
|
||||
local wh_gray = style.wh_gray
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local text_c = style.text_colors
|
||||
local lu_c = style.lu_colors
|
||||
local lg_gray = style.lg_gray
|
||||
|
||||
local ind_grn = style.ind_grn
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
-- make a new unit flow window
|
||||
---@param parent graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@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 height = 16
|
||||
|
||||
local v_start = 1 + ((unit.unit_id - 1) * 5)
|
||||
local prv_start = 1 + ((unit.unit_id - 1) * 3)
|
||||
local v_fields = { "pu", "po", "pl", "am" }
|
||||
local v_names = {
|
||||
sprintf("PV%02d-PU", v_start),
|
||||
sprintf("PV%02d-PO", v_start + 1),
|
||||
sprintf("PV%02d-PL", v_start + 2),
|
||||
sprintf("PV%02d-AM", v_start + 3),
|
||||
sprintf("PRV%02d", prv_start),
|
||||
sprintf("PRV%02d", prv_start + 1),
|
||||
sprintf("PRV%02d", prv_start + 2)
|
||||
}
|
||||
|
||||
assert(parent.get_height() >= (y + height), "flow display not of sufficient vertical resolution (add an additional row of monitors) " .. y .. "," .. parent.get_height())
|
||||
|
||||
local function _wide(a, b) return util.trinary(wide, a, b) end
|
||||
|
||||
-- bounding box div
|
||||
local root = Div{parent=parent,x=x,y=y,width=_wide(136, 114),height=height}
|
||||
|
||||
------------------
|
||||
-- COOLING LOOP --
|
||||
------------------
|
||||
|
||||
local reactor = Rectangle{parent=root,x=1,y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
|
||||
TextBox{parent=reactor,y=1,text="FISSION REACTOR",alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=root,x=19,y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
|
||||
TextBox{parent=root,x=3,y=5,text="\x19",width=1,height=1,fg_bg=lg_gray}
|
||||
|
||||
local rc_pipes = {}
|
||||
|
||||
local emc_x = 42 -- emergency coolant connection x point
|
||||
|
||||
if unit.num_boilers > 0 then
|
||||
table.insert(rc_pipes, pipe(0, 1, _wide(28, 19), 1, colors.lightBlue, true))
|
||||
table.insert(rc_pipes, pipe(0, 3, _wide(28, 19), 3, colors.orange, true))
|
||||
table.insert(rc_pipes, pipe(_wide(46 ,39), 1, _wide(72,58), 1, colors.blue, true))
|
||||
table.insert(rc_pipes, pipe(_wide(46,39), 3, _wide(72,58), 3, colors.white, true))
|
||||
else
|
||||
emc_x = 3
|
||||
table.insert(rc_pipes, pipe(0, 1, _wide(72,58), 1, colors.blue, true))
|
||||
table.insert(rc_pipes, pipe(0, 3, _wide(72,58), 3, colors.white, true))
|
||||
end
|
||||
|
||||
if unit.has_tank then
|
||||
table.insert(rc_pipes, pipe(emc_x, 1, emc_x, 0, colors.blue, true, true))
|
||||
end
|
||||
|
||||
local prv_yo = math.max(3 - unit.num_turbines, 0)
|
||||
for i = 1, unit.num_turbines do
|
||||
local py = 2 * (i - 1) + prv_yo
|
||||
table.insert(rc_pipes, pipe(_wide(92, 78), py, _wide(104, 83), py, colors.white, true))
|
||||
end
|
||||
|
||||
PipeNetwork{parent=root,x=20,y=1,pipes=rc_pipes,bg=colors.lightGray}
|
||||
|
||||
if unit.num_boilers > 0 then
|
||||
local cc_rate = DataIndicator{parent=root,x=_wide(25,22),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
local hc_rate = DataIndicator{parent=root,x=_wide(25,22),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
|
||||
cc_rate.register(unit.unit_ps, "boiler_boil_sum", function (sum) cc_rate.update(sum * 10) end)
|
||||
hc_rate.register(unit.unit_ps, "heating_rate", hc_rate.update)
|
||||
|
||||
local boiler = Rectangle{parent=root,x=_wide(47,40),y=1,border=border(1, colors.gray, true),width=19,height=5,fg_bg=wh_gray}
|
||||
TextBox{parent=boiler,y=1,text="THERMO-ELECTRIC",alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=boiler,y=3,text=util.trinary(unit.num_boilers>1,"BOILERS","BOILER"),alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=root,x=_wide(47,40),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
|
||||
TextBox{parent=root,x=_wide(65,58),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
|
||||
|
||||
local wt_rate = DataIndicator{parent=root,x=_wide(71,61),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
local st_rate = DataIndicator{parent=root,x=_wide(71,61),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
|
||||
wt_rate.register(unit.unit_ps, "turbine_flow_sum", wt_rate.update)
|
||||
st_rate.register(unit.unit_ps, "boiler_boil_sum", st_rate.update)
|
||||
else
|
||||
local wt_rate = DataIndicator{parent=root,x=28,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
local st_rate = DataIndicator{parent=root,x=28,y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
|
||||
wt_rate.register(unit.unit_ps, "turbine_flow_sum", wt_rate.update)
|
||||
st_rate.register(unit.unit_ps, "heating_rate", st_rate.update)
|
||||
end
|
||||
|
||||
local turbine = Rectangle{parent=root,x=_wide(93,79),y=1,border=border(1, colors.gray, true),width=19,height=5,fg_bg=wh_gray}
|
||||
TextBox{parent=turbine,y=1,text="STEAM TURBINE",alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=root,x=_wide(93,79),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
|
||||
|
||||
for i = 1, unit.num_turbines do
|
||||
local ry = 1 + (2 * (i - 1)) + prv_yo
|
||||
TextBox{parent=root,x=_wide(125,103),y=ry,text="\x10\x11\x7f",fg_bg=text_c,width=3,height=1}
|
||||
local state = TriIndicatorLight{parent=root,x=_wide(129,107),y=ry,label=v_names[i+4],c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
||||
state.register(unit.turbine_ps_tbl[i], "SteamDumpOpen", state.update)
|
||||
end
|
||||
|
||||
----------------------
|
||||
-- WASTE PROCESSING --
|
||||
----------------------
|
||||
|
||||
local waste = Div{parent=root,x=3,y=6}
|
||||
|
||||
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, colors.black, true, true),
|
||||
pipe(_wide(108, 94), 4, _wide(111, 95), 1, colors.black, true, true),
|
||||
pipe(_wide(132, 110), 6, _wide(130, 108), 6, colors.black, true, true)
|
||||
}
|
||||
|
||||
PipeNetwork{parent=waste,x=1,y=1,pipes=waste_pipes,bg=colors.lightGray}
|
||||
|
||||
local function _valve(vx, vy, n)
|
||||
TextBox{parent=waste,x=vx,y=vy,text="\x10\x11",fg_bg=text_c,width=2,height=1}
|
||||
local conn = IndicatorLight{parent=waste,x=vx-3,y=vy+1,label=v_names[n],colors=ind_grn}
|
||||
local open = IndicatorLight{parent=waste,x=vx-3,y=vy+2,label="OPEN",colors=ind_wht}
|
||||
conn.register(unit.unit_ps, util.c("V_", v_fields[n], "_conn"), conn.update)
|
||||
open.register(unit.unit_ps, util.c("V_", v_fields[n], "_state"), open.update)
|
||||
end
|
||||
|
||||
local function _machine(mx, my, name)
|
||||
local l = string.len(name) + 2
|
||||
TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=ALIGN.CENTER,fg_bg=lg_gray,width=l,height=1}
|
||||
TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=ALIGN.CENTER,fg_bg=wh_gray,width=l,height=1}
|
||||
end
|
||||
|
||||
local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
local pu_rate = DataIndicator{parent=waste,x=_wide(82,70),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
local po_rate = DataIndicator{parent=waste,x=_wide(52,45),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
local popl_rate = DataIndicator{parent=waste,x=_wide(82,70),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
local poam_rate = DataIndicator{parent=waste,x=_wide(82,70),y=10,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
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=bw_fg_bg}
|
||||
|
||||
waste_rate.register(unit.unit_ps, "act_burn_rate", waste_rate.update)
|
||||
pu_rate.register(unit.unit_ps, "pu_rate", pu_rate.update)
|
||||
po_rate.register(unit.unit_ps, "po_rate", po_rate.update)
|
||||
popl_rate.register(unit.unit_ps, "po_pl_rate", popl_rate.update)
|
||||
poam_rate.register(unit.unit_ps, "po_am_rate", poam_rate.update)
|
||||
spent_rate.register(unit.unit_ps, "ws_rate", spent_rate.update)
|
||||
|
||||
_valve(_wide(21, 18), 2, 1)
|
||||
_valve(_wide(21, 18), 6, 2)
|
||||
_valve(_wide(73, 62), 5, 3)
|
||||
_valve(_wide(73, 62), 9, 4)
|
||||
|
||||
_machine(_wide(51, 45), 1, "CENTRIFUGE \x1a");
|
||||
_machine(_wide(97, 83), 1, "PRC [Pu] \x1a");
|
||||
_machine(_wide(97, 83), 4, "PRC [Po] \x1a");
|
||||
_machine(_wide(116, 94), 6, "SPENT WASTE \x1b")
|
||||
|
||||
TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
|
||||
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=bw_fg_bg}
|
||||
local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn}
|
||||
local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c,label="CNT",unit="",format="%2d",value=0,width=7}
|
||||
local sna_pk = DataIndicator{parent=sna_po,y=3,lu_colors=lu_c,label="PEAK",unit="mB/t",format="%7.2f",value=0,width=17}
|
||||
local sna_max = DataIndicator{parent=sna_po,lu_colors=lu_c,label="MAX",unit="mB/t",format="%8.2f",value=0,width=17}
|
||||
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c,label="IN",unit="mB/t",format="%9.2f",value=0,width=17}
|
||||
|
||||
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
|
||||
175
coordinator/ui/components/unit_overview.lua
Normal file
175
coordinator/ui/components/unit_overview.lua
Normal file
@@ -0,0 +1,175 @@
|
||||
--
|
||||
-- Basic Unit Overview
|
||||
--
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
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 ALIGN = core.ALIGN
|
||||
|
||||
local pipe = core.pipe
|
||||
|
||||
-- make a new unit overview window
|
||||
---@param parent graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param unit ioctl_unit unit database entry
|
||||
local function make(parent, x, y, unit)
|
||||
local num_boilers = #unit.boiler_data_tbl
|
||||
local num_turbines = #unit.turbine_data_tbl
|
||||
|
||||
assert(num_boilers >= 0 and num_boilers <= 2, "minimum 0 boilers, maximum 2 boilers")
|
||||
assert(num_turbines >= 1 and num_turbines <= 3, "minimum 1 turbine, maximum 3 turbines")
|
||||
|
||||
local height = 25
|
||||
|
||||
if num_boilers == 0 and num_turbines == 1 then
|
||||
height = 9
|
||||
elseif num_boilers <= 1 and num_turbines <= 2 then
|
||||
height = 17
|
||||
end
|
||||
|
||||
assert(parent.get_height() >= (y + height), "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||
|
||||
-- bounding box div
|
||||
local root = Div{parent=parent,x=x,y=y,width=80,height=height}
|
||||
|
||||
-- unit header message
|
||||
TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
|
||||
-------------
|
||||
-- REACTOR --
|
||||
-------------
|
||||
|
||||
reactor_view(root, 1, 3, unit.unit_ps)
|
||||
|
||||
if num_boilers > 0 then
|
||||
local coolant_pipes = {}
|
||||
|
||||
if num_boilers >= 2 then
|
||||
table.insert(coolant_pipes, pipe(0, 0, 11, 12, colors.lightBlue))
|
||||
end
|
||||
|
||||
table.insert(coolant_pipes, pipe(0, 0, 11, 3, colors.lightBlue))
|
||||
table.insert(coolant_pipes, pipe(2, 0, 11, 2, colors.orange))
|
||||
|
||||
if num_boilers >= 2 then
|
||||
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}
|
||||
end
|
||||
|
||||
-------------
|
||||
-- BOILERS --
|
||||
-------------
|
||||
|
||||
if num_boilers >= 1 then boiler_view(root, 16, 11, unit.boiler_ps_tbl[1]) end
|
||||
if num_boilers >= 2 then boiler_view(root, 16, 19, unit.boiler_ps_tbl[2]) end
|
||||
|
||||
--------------
|
||||
-- TURBINES --
|
||||
--------------
|
||||
|
||||
local t_idx = 1
|
||||
local no_boilers = num_boilers == 0
|
||||
|
||||
if (num_turbines >= 3) or no_boilers or (num_boilers == 1 and num_turbines >= 2) then
|
||||
turbine_view(root, 58, 3, unit.turbine_ps_tbl[t_idx])
|
||||
t_idx = t_idx + 1
|
||||
end
|
||||
|
||||
if (num_turbines >= 1 and not no_boilers) or num_turbines >= 2 then
|
||||
turbine_view(root, 58, 11, unit.turbine_ps_tbl[t_idx])
|
||||
t_idx = t_idx + 1
|
||||
end
|
||||
|
||||
if (num_turbines >= 2 and num_boilers >= 2) or num_turbines >= 3 then
|
||||
turbine_view(root, 58, 19, unit.turbine_ps_tbl[t_idx])
|
||||
end
|
||||
|
||||
local steam_pipes_b = {}
|
||||
|
||||
if no_boilers then
|
||||
table.insert(steam_pipes_b, pipe(0, 1, 3, 1, colors.white)) -- steam to turbine 1
|
||||
table.insert(steam_pipes_b, pipe(0, 2, 3, 2, colors.blue)) -- water to turbine 1
|
||||
|
||||
if num_turbines >= 2 then
|
||||
table.insert(steam_pipes_b, pipe(1, 2, 3, 9, colors.white)) -- steam to turbine 2
|
||||
table.insert(steam_pipes_b, pipe(2, 3, 3, 10, colors.blue)) -- water to turbine 2
|
||||
end
|
||||
|
||||
if num_turbines >= 3 then
|
||||
table.insert(steam_pipes_b, pipe(1, 9, 3, 17, colors.white)) -- steam boiler 1 to turbine 1 junction end
|
||||
table.insert(steam_pipes_b, pipe(2, 10, 3, 18, colors.blue)) -- water boiler 1 to turbine 1 junction start
|
||||
end
|
||||
else
|
||||
-- boiler side pipes
|
||||
local steam_pipes_a = {
|
||||
-- boiler 1 steam/water pipes
|
||||
pipe(0, 1, 6, 1, colors.white, false, true), -- steam boiler 1 to turbine junction
|
||||
pipe(0, 2, 6, 2, colors.blue, false, true) -- water boiler 1 to turbine junction
|
||||
}
|
||||
|
||||
if num_boilers >= 2 then
|
||||
-- boiler 2 steam/water pipes
|
||||
table.insert(steam_pipes_a, pipe(0, 9, 6, 9, colors.white, false, true)) -- steam boiler 2 to turbine junction
|
||||
table.insert(steam_pipes_a, pipe(0, 10, 6, 10, colors.blue, false, true)) -- water boiler 2 to turbine junction
|
||||
end
|
||||
|
||||
-- turbine side pipes
|
||||
|
||||
if num_turbines >= 3 or (num_boilers == 1 and num_turbines == 2) then
|
||||
table.insert(steam_pipes_b, pipe(0, 9, 1, 2, colors.white, false, true)) -- steam boiler 1 to turbine 1 junction start
|
||||
table.insert(steam_pipes_b, pipe(1, 1, 3, 1, colors.white, false, false)) -- steam boiler 1 to turbine 1 junction end
|
||||
end
|
||||
|
||||
table.insert(steam_pipes_b, pipe(0, 9, 3, 9, colors.white, false, true)) -- steam boiler 1 to turbine 2
|
||||
|
||||
if num_turbines >= 3 or (num_boilers == 1 and num_turbines == 2) then
|
||||
table.insert(steam_pipes_b, pipe(0, 10, 2, 3, colors.blue, false, true)) -- water boiler 1 to turbine 1 junction start
|
||||
table.insert(steam_pipes_b, pipe(2, 2, 3, 2, colors.blue, false, false)) -- water boiler 1 to turbine 1 junction end
|
||||
end
|
||||
|
||||
table.insert(steam_pipes_b, pipe(0, 10, 3, 10, colors.blue, false, true)) -- water boiler 1 to turbine 2
|
||||
|
||||
if num_turbines >= 3 or (num_turbines >= 2 and num_boilers >= 2) then
|
||||
if num_boilers >= 2 then
|
||||
table.insert(steam_pipes_b, pipe(0, 17, 1, 9, colors.white, false, true)) -- steam boiler 2 to turbine 2 junction
|
||||
table.insert(steam_pipes_b, pipe(0, 17, 3, 17, colors.white, false, true)) -- steam boiler 2 to turbine 3
|
||||
|
||||
table.insert(steam_pipes_b, pipe(0, 18, 2, 10, colors.blue, false, true)) -- water boiler 2 to turbine 3
|
||||
table.insert(steam_pipes_b, pipe(0, 18, 3, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
|
||||
else
|
||||
table.insert(steam_pipes_b, pipe(1, 17, 1, 9, colors.white, false, true)) -- steam boiler 2 to turbine 2 junction
|
||||
table.insert(steam_pipes_b, pipe(1, 17, 3, 17, colors.white, false, true)) -- steam boiler 2 to turbine 3
|
||||
|
||||
table.insert(steam_pipes_b, pipe(2, 18, 2, 10, colors.blue, false, true)) -- water boiler 2 to turbine 3
|
||||
table.insert(steam_pipes_b, pipe(2, 18, 3, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
|
||||
end
|
||||
elseif num_turbines == 1 and num_boilers >= 2 then
|
||||
table.insert(steam_pipes_b, pipe(0, 17, 1, 9, colors.white, false, true)) -- steam boiler 2 to turbine 2 junction
|
||||
table.insert(steam_pipes_b, pipe(0, 17, 1, 17, colors.white, false, true)) -- steam boiler 2 to turbine 3
|
||||
|
||||
table.insert(steam_pipes_b, pipe(0, 18, 2, 10, colors.blue, false, true)) -- water boiler 2 to turbine 3
|
||||
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}
|
||||
end
|
||||
|
||||
PipeNetwork{parent=root,x=54,y=3,pipes=steam_pipes_b,bg=colors.lightGray}
|
||||
|
||||
return root
|
||||
end
|
||||
|
||||
return make
|
||||
387
coordinator/ui/layout/flow_view.lua
Normal file
387
coordinator/ui/layout/flow_view.lua
Normal file
@@ -0,0 +1,387 @@
|
||||
--
|
||||
-- Flow Monitor GUI
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local unit_flow = require("coordinator.ui.components.unit_flow")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local PipeNetwork = require("graphics.elements.pipenet")
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local HorizontalBar = require("graphics.elements.indicators.hbar")
|
||||
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
|
||||
local CONTAINER_MODE = types.CONTAINER_MODE
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
local pipe = core.pipe
|
||||
|
||||
local wh_gray = style.wh_gray
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local text_col = style.text_colors
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
-- create new flow view
|
||||
---@param main graphics_element main displaybox
|
||||
local function init(main)
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
|
||||
local tank_defs = facility.tank_defs
|
||||
local tank_list = facility.tank_list
|
||||
|
||||
-- window header message
|
||||
local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
-- max length example: "01:23:45 AM - Wednesday, September 28 2022"
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
|
||||
|
||||
datetime.register(facility.ps, "date_time", datetime.set_value)
|
||||
|
||||
local po_pipes = {}
|
||||
local water_pipes = {}
|
||||
|
||||
-- get the y offset for this unit index
|
||||
---@param idx integer unit index
|
||||
local function y_ofs(idx) return ((idx - 1) * 20) end
|
||||
|
||||
-- determinte facility tank start/end from the definitions list
|
||||
---@param start_idx integer start index of table iteration
|
||||
---@param end_idx integer end index of table iteration
|
||||
local function find_fdef(start_idx, end_idx)
|
||||
local first, last = 4, 0
|
||||
for i = start_idx, end_idx do
|
||||
if tank_defs[i] == 2 then
|
||||
last = i
|
||||
if i < first then first = i end
|
||||
end
|
||||
end
|
||||
return first, last
|
||||
end
|
||||
|
||||
if facility.tank_mode == 0 or facility.tank_mode == 8 then
|
||||
-- (0) tanks belong to reactor units OR (8) 4 total facility tanks (A B C D)
|
||||
for i = 1, facility.num_units do
|
||||
if units[i].has_tank then
|
||||
local y = y_ofs(i)
|
||||
table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true))
|
||||
table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true))
|
||||
|
||||
local u = units[i] ---@type ioctl_unit
|
||||
local x = util.trinary(u.num_boilers == 0, 45, 84)
|
||||
table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true))
|
||||
end
|
||||
end
|
||||
else
|
||||
-- setup connections for units with emergency coolant, always the same
|
||||
for i = 1, #tank_defs do
|
||||
if tank_defs[i] > 0 then
|
||||
local y = y_ofs(i)
|
||||
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(1, y, 21, y, colors.blue, true))
|
||||
else
|
||||
table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true))
|
||||
table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true))
|
||||
end
|
||||
|
||||
local u = units[i] ---@type ioctl_unit
|
||||
local x = util.trinary(u.num_boilers == 0, 45, 84)
|
||||
table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true))
|
||||
end
|
||||
end
|
||||
|
||||
if facility.tank_mode == 1 then
|
||||
-- (1) 1 total facility tank (A A A A)
|
||||
local first_fdef, last_fdef = find_fdef(1, #tank_defs)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 2 then
|
||||
-- (2) 2 total facility tanks (A A A B)
|
||||
local first_fdef, last_fdef = find_fdef(1, math.min(3, #tank_defs))
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 4 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 3 then
|
||||
-- (3) 2 total facility tanks (A A B B)
|
||||
for _, a in pairs({ 1, 3 }) do
|
||||
local b = a + 1
|
||||
if tank_defs[a] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y_ofs(a), 1, y_ofs(a) + 6, colors.blue, true))
|
||||
if tank_defs[b] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y_ofs(b) - 13, 1, y_ofs(b), colors.blue, true))
|
||||
end
|
||||
elseif tank_defs[b] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y_ofs(b), 1, y_ofs(b) + 6, colors.blue, true))
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 4 then
|
||||
-- (4) 2 total facility tanks (A B B B)
|
||||
local first_fdef, last_fdef = find_fdef(2, #tank_defs)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 1 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 5 then
|
||||
-- (5) 3 total facility tanks (A A B C)
|
||||
local first_fdef, last_fdef = find_fdef(1, math.min(2, #tank_defs))
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 3 or i == 4 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 6 then
|
||||
-- (6) 3 total facility tanks (A B B C)
|
||||
local first_fdef, last_fdef = find_fdef(2, math.min(3, #tank_defs))
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 1 or i == 4 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 7 then
|
||||
-- (7) 3 total facility tanks (A B C C)
|
||||
local first_fdef, last_fdef = find_fdef(3, #tank_defs)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 1 or i == 2 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local flow_x = 3
|
||||
if #water_pipes > 0 then
|
||||
flow_x = 25
|
||||
PipeNetwork{parent=main,x=2,y=3,pipes=water_pipes,bg=colors.lightGray}
|
||||
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))
|
||||
end
|
||||
|
||||
PipeNetwork{parent=main,x=139,y=15,pipes=po_pipes,bg=colors.lightGray}
|
||||
|
||||
-----------------
|
||||
-- tank valves --
|
||||
-----------------
|
||||
|
||||
local next_f_id = 1
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
if tank_defs[i] > 0 then
|
||||
local vy = 3 + y_ofs(i)
|
||||
|
||||
TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=text_col,width=2,height=1}
|
||||
|
||||
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", i * 5),colors=style.ind_grn}
|
||||
local open = IndicatorLight{parent=main,x=9,y=vy+2,label="OPEN",colors=style.ind_wht}
|
||||
|
||||
conn.register(units[i].unit_ps, "V_emc_conn", conn.update)
|
||||
open.register(units[i].unit_ps, "V_emc_state", open.update)
|
||||
end
|
||||
end
|
||||
|
||||
-------------------
|
||||
-- dynamic tanks --
|
||||
-------------------
|
||||
|
||||
for i = 1, #tank_list do
|
||||
if tank_list[i] > 0 then
|
||||
local id = "U-" .. i
|
||||
local f_id = next_f_id
|
||||
if tank_list[i] == 2 then
|
||||
id = "F-" .. next_f_id
|
||||
next_f_id = next_f_id + 1
|
||||
end
|
||||
|
||||
local y_offset = y_ofs(i)
|
||||
|
||||
local tank = Div{parent=main,x=3,y=7+y_offset,width=20,height=14}
|
||||
|
||||
TextBox{parent=tank,text=" ",height=1,x=1,y=1,fg_bg=style.lg_gray}
|
||||
TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=ALIGN.CENTER,height=1,fg_bg=style.wh_gray}
|
||||
|
||||
local tank_box = Rectangle{parent=tank,border=border(1,colors.gray,true),width=20,height=12}
|
||||
|
||||
local status = StateIndicator{parent=tank_box,x=3,y=1,states=style.dtank.states,value=1,min_width=14}
|
||||
|
||||
TextBox{parent=tank_box,x=2,y=3,text="Fill",height=1,width=10,fg_bg=style.label}
|
||||
local tank_pcnt = DataIndicator{parent=tank_box,x=10,y=3,label="",format="%5.2f",value=100,unit="%",lu_colors=lu_col,width=8,fg_bg=text_col}
|
||||
local tank_amnt = DataIndicator{parent=tank_box,x=2,label="",format="%13d",value=0,commas=true,unit="mB",lu_colors=lu_col,width=16,fg_bg=bw_fg_bg}
|
||||
|
||||
TextBox{parent=tank_box,x=2,y=6,text="Water Level",height=1,width=11,fg_bg=style.label}
|
||||
local level = HorizontalBar{parent=tank_box,x=2,y=7,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=16}
|
||||
|
||||
TextBox{parent=tank_box,x=2,y=9,text="In/Out Mode",height=1,width=11,fg_bg=style.label}
|
||||
local can_fill = IndicatorLight{parent=tank_box,x=2,y=10,label="FILL",colors=style.ind_wht}
|
||||
local can_empty = IndicatorLight{parent=tank_box,x=10,y=10,label="EMPTY",colors=style.ind_wht}
|
||||
|
||||
local function _can_fill(mode)
|
||||
can_fill.update((mode == CONTAINER_MODE.BOTH) or (mode == CONTAINER_MODE.FILL))
|
||||
end
|
||||
|
||||
local function _can_empty(mode)
|
||||
can_empty.update((mode == CONTAINER_MODE.BOTH) or (mode == CONTAINER_MODE.EMPTY))
|
||||
end
|
||||
|
||||
if tank_list[i] == 1 then
|
||||
status.register(units[i].tank_ps_tbl[1], "computed_status", status.update)
|
||||
tank_pcnt.register(units[i].tank_ps_tbl[1], "fill", function (f) tank_pcnt.update(f * 100) end)
|
||||
tank_amnt.register(units[i].tank_ps_tbl[1], "stored", function (sto) tank_amnt.update(sto.amount) end)
|
||||
level.register(units[i].tank_ps_tbl[1], "fill", level.update)
|
||||
can_fill.register(units[i].tank_ps_tbl[1], "container_mode", _can_fill)
|
||||
can_empty.register(units[i].tank_ps_tbl[1], "container_mode", _can_empty)
|
||||
else
|
||||
status.register(facility.tank_ps_tbl[f_id], "computed_status", status.update)
|
||||
tank_pcnt.register(facility.tank_ps_tbl[f_id], "fill", function (f) tank_pcnt.update(f * 100) end)
|
||||
tank_amnt.register(facility.tank_ps_tbl[f_id], "stored", function (sto) tank_amnt.update(sto.amount) end)
|
||||
level.register(facility.tank_ps_tbl[f_id], "fill", level.update)
|
||||
can_fill.register(facility.tank_ps_tbl[f_id], "container_mode", _can_fill)
|
||||
can_empty.register(facility.tank_ps_tbl[f_id], "container_mode", _can_empty)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---------
|
||||
-- SPS --
|
||||
---------
|
||||
|
||||
local sps = Div{parent=main,x=140,y=3,height=12}
|
||||
|
||||
TextBox{parent=sps,text=" ",width=24,height=1,x=1,y=1,fg_bg=style.lg_gray}
|
||||
TextBox{parent=sps,text="SPS",alignment=ALIGN.CENTER,width=24,height=1,fg_bg=wh_gray}
|
||||
|
||||
local sps_box = Rectangle{parent=sps,border=border(1,colors.gray,true),width=24,height=10}
|
||||
|
||||
local status = StateIndicator{parent=sps_box,x=5,y=1,states=style.sps.states,value=1,min_width=14}
|
||||
|
||||
status.register(facility.sps_ps_tbl[1], "computed_status", status.update)
|
||||
|
||||
TextBox{parent=sps_box,x=2,y=3,text="Input Rate",height=1,width=10,fg_bg=style.label}
|
||||
local sps_in = DataIndicator{parent=sps_box,x=2,label="",format="%15.2f",value=0,unit="mB/t",lu_colors=lu_col,width=20,fg_bg=bw_fg_bg}
|
||||
|
||||
sps_in.register(facility.ps, "po_am_rate", sps_in.update)
|
||||
|
||||
TextBox{parent=sps_box,x=2,y=6,text="Production Rate",height=1,width=15,fg_bg=style.label}
|
||||
local sps_rate = DataIndicator{parent=sps_box,x=2,label="",format="%15d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=20,fg_bg=bw_fg_bg}
|
||||
|
||||
sps_rate.register(facility.sps_ps_tbl[1], "process_rate", function (r) sps_rate.update(r * 1000) end)
|
||||
|
||||
----------------
|
||||
-- statistics --
|
||||
----------------
|
||||
|
||||
TextBox{parent=main,x=145,y=16,text="RAW WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
|
||||
local raw_waste = Rectangle{parent=main,x=145,y=17,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg}
|
||||
local sum_raw_waste = DataIndicator{parent=raw_waste,lu_colors=lu_col,label="SUM",unit="mB/t",format="%8.2f",value=0,width=17}
|
||||
|
||||
sum_raw_waste.register(facility.ps, "burn_sum", sum_raw_waste.update)
|
||||
|
||||
TextBox{parent=main,x=145,y=21,text="PROC. WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
|
||||
local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=bw_fg_bg}
|
||||
local pu = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17}
|
||||
local po = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Po",unit="mB/t",format="%9.2f",value=0,width=17}
|
||||
local popl = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="PoPl",unit="mB/t",format="%7.2f",value=0,width=17}
|
||||
|
||||
pu.register(facility.ps, "pu_rate", pu.update)
|
||||
po.register(facility.ps, "po_rate", po.update)
|
||||
popl.register(facility.ps, "po_pl_rate", popl.update)
|
||||
|
||||
TextBox{parent=main,x=145,y=28,text="SPENT WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
|
||||
local sp_waste = Rectangle{parent=main,x=145,y=29,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg}
|
||||
local sum_sp_waste = DataIndicator{parent=sp_waste,lu_colors=lu_col,label="SUM",unit="mB/t",format="%8.3f",value=0,width=17}
|
||||
|
||||
sum_sp_waste.register(facility.ps, "spent_waste_rate", sum_sp_waste.update)
|
||||
end
|
||||
|
||||
return init
|
||||
126
coordinator/ui/layout/front_panel.lua
Normal file
126
coordinator/ui/layout/front_panel.lua
Normal file
@@ -0,0 +1,126 @@
|
||||
--
|
||||
-- Coordinator Front Panel GUI
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local pkt_entry = require("coordinator.ui.components.pkt_entry")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local ListBox = require("graphics.elements.listbox")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local TabBar = require("graphics.elements.controls.tabbar")
|
||||
|
||||
local LED = require("graphics.elements.indicators.led")
|
||||
local RGBLED = require("graphics.elements.indicators.ledrgb")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local led_grn = style.led_grn
|
||||
|
||||
-- create new front panel view
|
||||
---@param panel graphics_element main displaybox
|
||||
---@param num_units integer number of units (number of unit monitors)
|
||||
local function init(panel, num_units)
|
||||
local ps = iocontrol.get_db().fp.ps
|
||||
|
||||
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,height=1,fg_bg=style.fp.header}
|
||||
|
||||
local page_div = Div{parent=panel,x=1,y=3}
|
||||
|
||||
--
|
||||
-- system indicators
|
||||
--
|
||||
|
||||
local main_page = Div{parent=page_div,x=1,y=1}
|
||||
|
||||
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=led_grn}
|
||||
status.update(true)
|
||||
system.line_break()
|
||||
|
||||
heartbeat.register(ps, "heartbeat", heartbeat.update)
|
||||
|
||||
local modem = LED{parent=system,label="MODEM",colors=led_grn}
|
||||
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
|
||||
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
system.line_break()
|
||||
|
||||
modem.register(ps, "has_modem", modem.update)
|
||||
network.register(ps, "link_state", network.update)
|
||||
|
||||
local speaker = LED{parent=system,label="SPEAKER",colors=led_grn}
|
||||
speaker.register(ps, "has_speaker", speaker.update)
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local comp_id = util.sprintf("(%d)", os.getComputerID())
|
||||
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=style.fp_label}
|
||||
|
||||
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
|
||||
|
||||
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=led_grn}
|
||||
unit_monitor.register(ps, "unit_monitor_" .. i, unit_monitor.update)
|
||||
end
|
||||
|
||||
--
|
||||
-- about footer
|
||||
--
|
||||
|
||||
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp_label}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1}
|
||||
|
||||
fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
|
||||
comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
|
||||
|
||||
--
|
||||
-- page handling
|
||||
--
|
||||
|
||||
-- API page
|
||||
|
||||
local api_page = Div{parent=page_div,x=1,y=1,hidden=true}
|
||||
local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp_text,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
|
||||
local _ = Div{parent=api_list,height=1,hidden=true} -- padding
|
||||
|
||||
-- assemble page panes
|
||||
|
||||
local panes = { main_page, api_page }
|
||||
|
||||
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
|
||||
local tabs = {
|
||||
{ name = "CRD", color = style.fp_text },
|
||||
{ name = "API", color = style.fp_text },
|
||||
}
|
||||
|
||||
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=style.bw_fg_bg}
|
||||
|
||||
-- link pocket API list management to PGI
|
||||
pgi.link_elements(api_list, pkt_entry)
|
||||
end
|
||||
|
||||
return init
|
||||
85
coordinator/ui/layout/main_view.lua
Normal file
85
coordinator/ui/layout/main_view.lua
Normal file
@@ -0,0 +1,85 @@
|
||||
--
|
||||
-- Main SCADA Coordinator GUI
|
||||
--
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local imatrix = require("coordinator.ui.components.imatrix")
|
||||
local process_ctl = require("coordinator.ui.components.process_ctl")
|
||||
local unit_overview = require("coordinator.ui.components.unit_overview")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
-- create new main view
|
||||
---@param main graphics_element main displaybox
|
||||
local function init(main)
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
|
||||
-- window header message
|
||||
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=style.lg_white,width=12,fg_bg=style.header}
|
||||
-- max length example: "01:23:45 AM - Wednesday, September 28 2022"
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
|
||||
|
||||
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
|
||||
|
||||
local cnc_y_start = 3
|
||||
local row_1_height = 0
|
||||
|
||||
-- unit overviews
|
||||
if facility.num_units >= 1 then
|
||||
uo_1 = unit_overview(main, 2, 3, units[1])
|
||||
row_1_height = uo_1.get_height()
|
||||
end
|
||||
|
||||
if facility.num_units >= 2 then
|
||||
uo_2 = unit_overview(main, 84, 3, units[2])
|
||||
row_1_height = math.max(row_1_height, uo_2.get_height())
|
||||
end
|
||||
|
||||
cnc_y_start = cnc_y_start + row_1_height + 1
|
||||
|
||||
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
|
||||
|
||||
uo_3 = unit_overview(main, 2, row_2_offset, units[3])
|
||||
cnc_y_start = row_2_offset + uo_3.get_height() + 1
|
||||
|
||||
if facility.num_units == 4 then
|
||||
uo_4 = unit_overview(main, 84, row_2_offset, units[4])
|
||||
cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.get_height() + 1)
|
||||
end
|
||||
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=string.rep("\x8c", header.get_width()),alignment=ALIGN.CENTER,height=1,fg_bg=style.lg_gray}
|
||||
|
||||
cnc_bottom_align_start = cnc_bottom_align_start + 2
|
||||
|
||||
process_ctl(main, 2, cnc_bottom_align_start)
|
||||
|
||||
imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1])
|
||||
end
|
||||
|
||||
return init
|
||||
14
coordinator/ui/layout/unit_view.lua
Normal file
14
coordinator/ui/layout/unit_view.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
--
|
||||
-- Reactor Unit SCADA Coordinator GUI
|
||||
--
|
||||
|
||||
local unit_detail = require("coordinator.ui.components.unit_detail")
|
||||
|
||||
-- create a unit view
|
||||
---@param main graphics_element main displaybox
|
||||
---@param id integer
|
||||
local function init(main, id)
|
||||
unit_detail(main, id)
|
||||
end
|
||||
|
||||
return init
|
||||
60
coordinator/ui/pgi.lua
Normal file
60
coordinator/ui/pgi.lua
Normal file
@@ -0,0 +1,60 @@
|
||||
--
|
||||
-- Protected Graphics Interface
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local pgi = {}
|
||||
|
||||
local data = {
|
||||
pkt_list = nil, ---@type nil|graphics_element
|
||||
pkt_entry = nil, ---@type function
|
||||
-- session entries
|
||||
s_entries = { pkt = {} }
|
||||
}
|
||||
|
||||
-- link list boxes
|
||||
---@param pkt_list graphics_element pocket list element
|
||||
---@param pkt_entry function pocket entry constructor
|
||||
function pgi.link_elements(pkt_list, pkt_entry)
|
||||
data.pkt_list = pkt_list
|
||||
data.pkt_entry = pkt_entry
|
||||
end
|
||||
|
||||
-- unlink all fields, disabling the PGI
|
||||
function pgi.unlink()
|
||||
data.pkt_list = nil
|
||||
data.pkt_entry = nil
|
||||
end
|
||||
|
||||
-- add a PKT entry to the PKT list
|
||||
---@param session_id integer pocket session
|
||||
function pgi.create_pkt_entry(session_id)
|
||||
if data.pkt_list ~= nil and data.pkt_entry ~= nil then
|
||||
local success, result = pcall(data.pkt_entry, data.pkt_list, session_id)
|
||||
|
||||
if success then
|
||||
data.s_entries.pkt[session_id] = result
|
||||
else
|
||||
log.error(util.c("PGI: failed to create PKT entry (", result, ")"), true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- delete a PKT entry from the PKT list
|
||||
---@param session_id integer pocket session
|
||||
function pgi.delete_pkt_entry(session_id)
|
||||
if data.s_entries.pkt[session_id] ~= nil then
|
||||
local success, result = pcall(data.s_entries.pkt[session_id].delete)
|
||||
data.s_entries.pkt[session_id] = nil
|
||||
|
||||
if not success then
|
||||
log.error(util.c("PGI: failed to delete PKT entry (", result, ")"), true)
|
||||
end
|
||||
else
|
||||
log.debug(util.c("PGI: tried to delete unknown PKT entry ", session_id))
|
||||
end
|
||||
end
|
||||
|
||||
return pgi
|
||||
329
coordinator/ui/style.lua
Normal file
329
coordinator/ui/style.lua
Normal file
@@ -0,0 +1,329 @@
|
||||
--
|
||||
-- Graphics Style Options
|
||||
--
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
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.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
|
||||
}
|
||||
|
||||
-- main GUI styling
|
||||
|
||||
style.root = cpair(colors.black, colors.lightGray)
|
||||
style.header = cpair(colors.white, colors.gray)
|
||||
style.label = cpair(colors.gray, colors.lightGray)
|
||||
|
||||
style.colors = {
|
||||
{ c = colors.red, hex = 0xdf4949 },
|
||||
{ c = colors.orange, hex = 0xffb659 },
|
||||
{ c = colors.yellow, hex = 0xfffc79 },
|
||||
{ c = colors.lime, hex = 0x80ff80 },
|
||||
{ c = colors.green, hex = 0x4aee8a },
|
||||
{ c = colors.cyan, hex = 0x34bac8 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.purple, hex = 0xb156ee },
|
||||
{ c = colors.pink, hex = 0xf26ba2 },
|
||||
{ c = colors.magenta, hex = 0xf9488a },
|
||||
-- { c = colors.white, hex = 0xf0f0f0 },
|
||||
{ c = colors.lightGray, hex = 0xcacaca },
|
||||
{ c = colors.gray, hex = 0x575757 },
|
||||
-- { c = colors.black, hex = 0x191919 },
|
||||
-- { c = colors.brown, hex = 0x7f664c }
|
||||
}
|
||||
|
||||
-- COMMON COLOR PAIRS --
|
||||
|
||||
style.wh_gray = cpair(colors.white, colors.gray)
|
||||
|
||||
style.bw_fg_bg = cpair(colors.black, colors.white)
|
||||
style.text_colors = cpair(colors.black, colors.lightGray)
|
||||
style.lu_colors = cpair(colors.gray, colors.gray)
|
||||
style.hzd_fg_bg = style.wh_gray
|
||||
style.dis_colors = cpair(colors.white, colors.lightGray)
|
||||
|
||||
style.lg_gray = cpair(colors.lightGray, colors.gray)
|
||||
style.lg_white = cpair(colors.lightGray, colors.white)
|
||||
style.gray_white = cpair(colors.gray, colors.white)
|
||||
|
||||
style.ind_grn = cpair(colors.green, colors.gray)
|
||||
style.ind_yel = cpair(colors.yellow, colors.gray)
|
||||
style.ind_red = cpair(colors.red, colors.gray)
|
||||
style.ind_wht = style.wh_gray
|
||||
|
||||
style.fp_text = cpair(colors.black, colors.ivory)
|
||||
style.fp_label = cpair(colors.lightGray, colors.ivory)
|
||||
style.led_grn = cpair(colors.green, colors.green_off)
|
||||
|
||||
-- UI COMPONENTS --
|
||||
|
||||
style.reactor = {
|
||||
-- reactor states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "PLC OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "PLC FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "DISABLED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ACTIVE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.red),
|
||||
text = "SCRAMMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.red),
|
||||
text = "FORCE DISABLED"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style.boiler = {
|
||||
-- boiler states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "IDLE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ACTIVE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style.turbine = {
|
||||
-- turbine states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "IDLE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ACTIVE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.red),
|
||||
text = "TRIP"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style.imatrix = {
|
||||
-- induction matrix states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ONLINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "LOW CHARGE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "HIGH CHARGE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style.sps = {
|
||||
-- SPS states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "IDLE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ACTIVE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = {
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "PLUTONIUM"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.cyan),
|
||||
text = "POLONIUM"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.purple),
|
||||
text = "ANTI MATTER"
|
||||
}
|
||||
},
|
||||
states_abbrv = {
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "Pu"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.cyan),
|
||||
text = "Po"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.purple),
|
||||
text = "AM"
|
||||
}
|
||||
},
|
||||
-- process radio button options
|
||||
options = { "Plutonium", "Polonium", "Antimatter" },
|
||||
-- unit waste selection
|
||||
unit_opts = {
|
||||
{
|
||||
text = "Auto",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.white, colors.gray)
|
||||
},
|
||||
{
|
||||
text = "Pu",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.black, colors.green)
|
||||
},
|
||||
{
|
||||
text = "Po",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.black, colors.cyan)
|
||||
},
|
||||
{
|
||||
text = "AM",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.black, colors.purple)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return style
|
||||
325
graphics/core.lua
Normal file
325
graphics/core.lua
Normal file
@@ -0,0 +1,325 @@
|
||||
--
|
||||
-- Graphics Core Types, Checks, and Constructors
|
||||
--
|
||||
|
||||
local events = require("graphics.events")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
local core = {}
|
||||
|
||||
core.version = "2.1.1"
|
||||
|
||||
core.flasher = flasher
|
||||
core.events = events
|
||||
|
||||
-- Core Types
|
||||
|
||||
---@enum ALIGN
|
||||
core.ALIGN = { LEFT = 1, CENTER = 2, RIGHT = 3 }
|
||||
|
||||
---@class graphics_border
|
||||
---@field width integer
|
||||
---@field color color
|
||||
---@field even boolean
|
||||
|
||||
---@alias element_id string|integer
|
||||
|
||||
-- create a new border definition
|
||||
---@nodiscard
|
||||
---@param width integer border width
|
||||
---@param color color border color
|
||||
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
|
||||
---@return graphics_border
|
||||
function core.border(width, color, even)
|
||||
return { width = width, color = color, even = even or false }
|
||||
end
|
||||
|
||||
---@class graphics_frame
|
||||
---@field x integer
|
||||
---@field y integer
|
||||
---@field w integer
|
||||
---@field h integer
|
||||
|
||||
-- create a new graphics frame definition
|
||||
---@nodiscard
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@param w integer
|
||||
---@param h integer
|
||||
---@return graphics_frame
|
||||
function core.gframe(x, y, w, h)
|
||||
return { x = x, y = y, w = w, h = h }
|
||||
end
|
||||
|
||||
---@class cpair
|
||||
---@field color_a color
|
||||
---@field color_b color
|
||||
---@field blit_a string
|
||||
---@field blit_b string
|
||||
---@field fgd color
|
||||
---@field bkg color
|
||||
---@field blit_fgd string
|
||||
---@field blit_bkg string
|
||||
|
||||
-- create a new color pair definition
|
||||
---@nodiscard
|
||||
---@param a color
|
||||
---@param b color
|
||||
---@return cpair
|
||||
function core.cpair(a, b)
|
||||
return {
|
||||
-- color pairs
|
||||
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)
|
||||
}
|
||||
end
|
||||
|
||||
---@class pipe
|
||||
---@field x1 integer starting x, origin is 0
|
||||
---@field y1 integer starting y, origin is 0
|
||||
---@field x2 integer ending x, origin is 0
|
||||
---@field y2 integer ending y, origin is 0
|
||||
---@field w integer width
|
||||
---@field h integer height
|
||||
---@field color color pipe color
|
||||
---@field thin boolean true for 1 subpixel, false (default) for 2
|
||||
---@field align_tr boolean false to align bottom left (default), true to align top right
|
||||
|
||||
-- create a new pipe<br>
|
||||
-- note: pipe coordinate origin is (0, 0)
|
||||
---@nodiscard
|
||||
---@param x1 integer starting x, origin is 0
|
||||
---@param y1 integer starting y, origin is 0
|
||||
---@param x2 integer ending x, origin is 0
|
||||
---@param y2 integer ending y, origin is 0
|
||||
---@param color color pipe color
|
||||
---@param thin? boolean true for 1 subpixel, false (default) for 2
|
||||
---@param align_tr? boolean false to align bottom left (default), true to align top right
|
||||
---@return pipe
|
||||
function core.pipe(x1, y1, x2, y2, color, thin, align_tr)
|
||||
return {
|
||||
x1 = x1,
|
||||
y1 = y1,
|
||||
x2 = x2,
|
||||
y2 = y2,
|
||||
w = math.abs(x2 - x1) + 1,
|
||||
h = math.abs(y2 - y1) + 1,
|
||||
color = color,
|
||||
thin = thin or false,
|
||||
align_tr = align_tr or false
|
||||
}
|
||||
end
|
||||
|
||||
-- Assertion Handling
|
||||
|
||||
-- extract the custom element assert message, dropping the path to the element file
|
||||
function core.extract_assert_msg(msg)
|
||||
return string.sub(msg, (string.find(msg, "@") or 0) + 1)
|
||||
end
|
||||
|
||||
-- Interactive Field Manager
|
||||
|
||||
---@param e graphics_base
|
||||
---@param max_len any
|
||||
---@param fg_bg any
|
||||
---@param dis_fg_bg any
|
||||
function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
|
||||
local self = {
|
||||
frame_start = 1,
|
||||
visible_text = e.value,
|
||||
cursor_pos = string.len(e.value) + 1,
|
||||
selected_all = false
|
||||
}
|
||||
|
||||
-- update visible text
|
||||
local function _update_visible()
|
||||
self.visible_text = string.sub(e.value, self.frame_start, self.frame_start + math.min(string.len(e.value), e.frame.w) - 1)
|
||||
end
|
||||
|
||||
-- try shifting frame left
|
||||
local function _try_lshift()
|
||||
if self.frame_start > 1 then
|
||||
self.frame_start = self.frame_start - 1
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- try shifting frame right
|
||||
local function _try_rshift()
|
||||
if (self.frame_start + e.frame.w - 1) <= string.len(e.value) then
|
||||
self.frame_start = self.frame_start + 1
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
---@class ifield
|
||||
local public = {}
|
||||
|
||||
-- censor the display (for private info, for example) with the provided character<br>
|
||||
-- disable by passing no argument
|
||||
---@param censor string? character to hide data with
|
||||
function public.censor(censor)
|
||||
if type(censor) == "string" and string.len(censor) == 1 then
|
||||
self.censor = censor
|
||||
else self.censor = nil end
|
||||
public.show()
|
||||
end
|
||||
|
||||
-- show the field
|
||||
function public.show()
|
||||
_update_visible()
|
||||
|
||||
if e.enabled then
|
||||
e.w_set_bkg(fg_bg.bkg)
|
||||
e.w_set_fgd(fg_bg.fgd)
|
||||
elseif dis_fg_bg ~= nil then
|
||||
e.w_set_bkg(dis_fg_bg.bkg)
|
||||
e.w_set_fgd(dis_fg_bg.fgd)
|
||||
end
|
||||
|
||||
-- clear and print
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(string.rep(" ", e.frame.w))
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
local function _write()
|
||||
if self.censor then
|
||||
e.w_write(string.rep(self.censor, string.len(self.visible_text)))
|
||||
else
|
||||
e.w_write(self.visible_text)
|
||||
end
|
||||
end
|
||||
|
||||
if e.is_focused() and e.enabled then
|
||||
-- write text with cursor
|
||||
if self.selected_all then
|
||||
e.w_set_bkg(fg_bg.fgd)
|
||||
e.w_set_fgd(fg_bg.bkg)
|
||||
_write()
|
||||
elseif self.cursor_pos >= (string.len(self.visible_text) + 1) then
|
||||
-- write text with cursor at the end, no need to blit
|
||||
_write()
|
||||
e.w_set_fgd(colors.lightGray)
|
||||
e.w_write("_")
|
||||
else
|
||||
local a, b = "", ""
|
||||
|
||||
if self.cursor_pos <= string.len(self.visible_text) then
|
||||
a = fg_bg.blit_bkg
|
||||
b = fg_bg.blit_fgd
|
||||
end
|
||||
|
||||
local b_fgd = string.rep(fg_bg.blit_fgd, self.cursor_pos - 1) .. a .. string.rep(fg_bg.blit_fgd, string.len(self.visible_text) - self.cursor_pos)
|
||||
local b_bkg = string.rep(fg_bg.blit_bkg, self.cursor_pos - 1) .. b .. string.rep(fg_bg.blit_bkg, string.len(self.visible_text) - self.cursor_pos)
|
||||
|
||||
if self.censor then
|
||||
e.w_blit(string.rep(self.censor, string.len(self.visible_text)), b_fgd, b_bkg)
|
||||
else
|
||||
e.w_blit(self.visible_text, b_fgd, b_bkg)
|
||||
end
|
||||
end
|
||||
else
|
||||
self.selected_all = false
|
||||
|
||||
-- write text without cursor
|
||||
_write()
|
||||
end
|
||||
end
|
||||
|
||||
-- move cursor to x
|
||||
---@param x integer
|
||||
function public.move_cursor(x)
|
||||
self.selected_all = false
|
||||
self.cursor_pos = math.min(x, string.len(self.visible_text) + 1)
|
||||
public.show()
|
||||
end
|
||||
|
||||
-- select all text
|
||||
function public.select_all()
|
||||
self.selected_all = true
|
||||
public.show()
|
||||
end
|
||||
|
||||
-- set field value
|
||||
---@param val string
|
||||
function public.set_value(val)
|
||||
e.value = string.sub(val, 1, math.min(max_len, string.len(val)))
|
||||
public.nav_end()
|
||||
end
|
||||
|
||||
-- try to insert a character if there is space
|
||||
---@param char string
|
||||
function public.try_insert_char(char)
|
||||
-- limit length
|
||||
if string.len(e.value) >= max_len then return end
|
||||
|
||||
-- replace if selected all, insert otherwise
|
||||
if self.selected_all then
|
||||
self.selected_all = false
|
||||
self.cursor_pos = 2
|
||||
self.frame_start = 1
|
||||
|
||||
e.value = char
|
||||
public.show()
|
||||
else
|
||||
e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 2) .. char .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value))
|
||||
_update_visible()
|
||||
public.nav_right()
|
||||
end
|
||||
end
|
||||
|
||||
-- remove charcter before cursor if there is anything to remove, or delete all if selected all
|
||||
function public.backspace()
|
||||
if self.selected_all then
|
||||
self.selected_all = false
|
||||
e.value = ""
|
||||
self.cursor_pos = 1
|
||||
self.frame_start = 1
|
||||
public.show()
|
||||
else
|
||||
if self.frame_start + self.cursor_pos > 2 then
|
||||
e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 3) .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value))
|
||||
if self.cursor_pos > 1 then
|
||||
self.cursor_pos = self.cursor_pos - 1
|
||||
public.show()
|
||||
elseif _try_lshift() then public.show() end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- move cursor left by one
|
||||
function public.nav_left()
|
||||
if self.cursor_pos > 1 then
|
||||
self.cursor_pos = self.cursor_pos - 1
|
||||
public.show()
|
||||
elseif _try_lshift() then public.show() end
|
||||
end
|
||||
|
||||
-- move cursor right by one
|
||||
function public.nav_right()
|
||||
if self.cursor_pos < math.min(string.len(self.visible_text) + 1, e.frame.w) then
|
||||
self.cursor_pos = self.cursor_pos + 1
|
||||
public.show()
|
||||
elseif _try_rshift() then public.show() end
|
||||
end
|
||||
|
||||
-- move cursor to the start
|
||||
function public.nav_start()
|
||||
self.cursor_pos = 1
|
||||
self.frame_start = 1
|
||||
public.show()
|
||||
end
|
||||
|
||||
-- move cursor to the end
|
||||
function public.nav_end()
|
||||
self.frame_start = math.max(1, string.len(e.value) - e.frame.w + 2)
|
||||
_update_visible()
|
||||
self.cursor_pos = string.len(self.visible_text) + 1
|
||||
public.show()
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return core
|
||||
867
graphics/element.lua
Normal file
867
graphics/element.lua
Normal file
@@ -0,0 +1,867 @@
|
||||
--
|
||||
-- Generic Graphics Element
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local events = core.events
|
||||
|
||||
local element = {}
|
||||
|
||||
---@class graphics_args_generic
|
||||
---@field window? table
|
||||
---@field parent? graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer next line 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
|
||||
---@field can_focus? boolean true if this element can be focused, false by default
|
||||
|
||||
---@alias graphics_args graphics_args_generic
|
||||
---|waiting_args
|
||||
---|app_button_args
|
||||
---|checkbox_args
|
||||
---|hazard_button_args
|
||||
---|multi_button_args
|
||||
---|push_button_args
|
||||
---|radio_2d_args
|
||||
---|radio_button_args
|
||||
---|sidebar_args
|
||||
---|spinbox_args
|
||||
---|switch_button_args
|
||||
---|tabbar_args
|
||||
---|number_field_args
|
||||
---|text_field_args
|
||||
---|alarm_indicator_light
|
||||
---|core_map_args
|
||||
---|data_indicator_args
|
||||
---|hbar_args
|
||||
---|icon_indicator_args
|
||||
---|indicator_led_args
|
||||
---|indicator_led_pair_args
|
||||
---|indicator_led_rgb_args
|
||||
---|indicator_light_args
|
||||
---|power_indicator_args
|
||||
---|rad_indicator_args
|
||||
---|state_indicator_args
|
||||
---|tristate_indicator_light_args
|
||||
---|vbar_args
|
||||
---|colormap_args
|
||||
---|displaybox_args
|
||||
---|div_args
|
||||
---|listbox_args
|
||||
---|multipane_args
|
||||
---|pipenet_args
|
||||
---|rectangle_args
|
||||
---|textbox_args
|
||||
---|tiling_args
|
||||
|
||||
---@class element_subscription
|
||||
---@field ps psil ps used
|
||||
---@field key string data key
|
||||
---@field func function callback
|
||||
|
||||
-- more detailed assert message for element verification
|
||||
---@param condition any assert condition
|
||||
---@param msg string assert message
|
||||
---@param callstack_offset? integer shift value to change targets of debug.getinfo()
|
||||
function element.assert(condition, msg, callstack_offset)
|
||||
callstack_offset = callstack_offset or 0
|
||||
local caller = debug.getinfo(3 + callstack_offset)
|
||||
assert(condition, util.c(caller.source, ":", caller.currentline, "{", debug.getinfo(2 + callstack_offset).name, "}: ", msg))
|
||||
end
|
||||
|
||||
-- a base graphics element, should not be created on its own
|
||||
---@nodiscard
|
||||
---@param args graphics_args arguments
|
||||
---@param child_offset_x? integer mouse event offset x
|
||||
---@param child_offset_y? integer mouse event offset y
|
||||
function element.new(args, child_offset_x, child_offset_y)
|
||||
local self = {
|
||||
id = nil, ---@type element_id|nil
|
||||
is_root = args.parent == nil,
|
||||
elem_type = debug.getinfo(2).name,
|
||||
define_completed = false,
|
||||
p_window = nil, ---@type table
|
||||
position = events.new_coord_2d(1, 1),
|
||||
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
|
||||
offset_x = 0,
|
||||
offset_y = 0,
|
||||
next_y = 1, -- next child y coordinate
|
||||
next_id = 0, -- next child ID
|
||||
subscriptions = {},
|
||||
button_down = { events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1) },
|
||||
focused = false,
|
||||
mt = {}
|
||||
}
|
||||
|
||||
---@class graphics_base
|
||||
local protected = {
|
||||
enabled = true,
|
||||
value = nil, ---@type any
|
||||
window = nil, ---@type table
|
||||
content_window = nil, ---@type table|nil
|
||||
mouse_window_shift = { x = 0, y = 0 },
|
||||
fg_bg = core.cpair(colors.white, colors.black),
|
||||
frame = core.gframe(1, 1, 1, 1),
|
||||
children = {},
|
||||
child_id_map = {}
|
||||
}
|
||||
|
||||
-- element as string
|
||||
function self.mt.__tostring()
|
||||
return util.c("graphics.element{", self.elem_type, "} @ ", self)
|
||||
end
|
||||
|
||||
---@class graphics_element
|
||||
local public = {}
|
||||
|
||||
setmetatable(public, self.mt)
|
||||
|
||||
-----------------------
|
||||
-- PRIVATE FUNCTIONS --
|
||||
-----------------------
|
||||
|
||||
-- use tab to jump to the next focusable field
|
||||
---@param reverse boolean
|
||||
local function _tab_focusable(reverse)
|
||||
local first_f = nil ---@type graphics_element|nil
|
||||
local prev_f = nil ---@type graphics_element|nil
|
||||
local cur_f = nil ---@type graphics_element|nil
|
||||
local done = false
|
||||
|
||||
---@param elem graphics_element
|
||||
local function handle_element(elem)
|
||||
if elem.is_visible() and elem.is_focusable() and elem.is_enabled() then
|
||||
if first_f == nil then first_f = elem end
|
||||
|
||||
if cur_f == nil then
|
||||
if elem.is_focused() then
|
||||
cur_f = elem
|
||||
if (not done) and (reverse and prev_f ~= nil) then
|
||||
cur_f.unfocus()
|
||||
prev_f.focus()
|
||||
done = true
|
||||
end
|
||||
end
|
||||
else
|
||||
if elem.is_focused() then
|
||||
elem.unfocus()
|
||||
elseif not (reverse or done) then
|
||||
cur_f.unfocus()
|
||||
elem.focus()
|
||||
done = true
|
||||
end
|
||||
end
|
||||
|
||||
prev_f = elem
|
||||
end
|
||||
end
|
||||
|
||||
---@param children table
|
||||
local function traverse(children)
|
||||
for i = 1, #children do
|
||||
local child = children[i] ---@type graphics_base
|
||||
handle_element(child.get())
|
||||
if child.get().is_visible() then traverse(child.children) end
|
||||
end
|
||||
end
|
||||
|
||||
traverse(protected.children)
|
||||
|
||||
-- if no element was focused, wrap focus
|
||||
if first_f ~= nil and not done then
|
||||
if reverse then
|
||||
if cur_f ~= nil then cur_f.unfocus() end
|
||||
if prev_f ~= nil then prev_f.focus() end
|
||||
else
|
||||
if cur_f ~= nil then cur_f.unfocus() end
|
||||
first_f.focus()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------
|
||||
-- PROTECTED FUNCTIONS --
|
||||
-------------------------
|
||||
|
||||
-- prepare the template
|
||||
---@param offset_x integer x offset for mouse events
|
||||
---@param offset_y integer y offset for mouse events
|
||||
---@param next_y integer next line if no y was provided
|
||||
function protected.prepare_template(offset_x, offset_y, next_y)
|
||||
-- record offsets in case there is a reposition
|
||||
self.offset_x = offset_x
|
||||
self.offset_y = offset_y
|
||||
|
||||
-- get frame coordinates/size
|
||||
if args.gframe ~= nil then
|
||||
protected.frame.x = args.gframe.x
|
||||
protected.frame.y = args.gframe.y
|
||||
protected.frame.w = args.gframe.w
|
||||
protected.frame.h = args.gframe.h
|
||||
else
|
||||
local w, h = self.p_window.getSize()
|
||||
protected.frame.x = args.x or 1
|
||||
protected.frame.y = args.y or next_y
|
||||
protected.frame.w = args.width or w
|
||||
protected.frame.h = args.height or h
|
||||
end
|
||||
|
||||
-- adjust window frame if applicable
|
||||
local f = protected.frame
|
||||
if args.parent ~= nil then
|
||||
-- constrain to parent inner width/height
|
||||
local w, h = self.p_window.getSize()
|
||||
f.w = math.min(f.w, w - (f.x - 1))
|
||||
f.h = math.min(f.h, h - (f.y - 1))
|
||||
end
|
||||
|
||||
-- check frame
|
||||
element.assert(f.x >= 1, "frame x not >= 1", 3)
|
||||
element.assert(f.y >= 1, "frame y not >= 1", 3)
|
||||
element.assert(f.w >= 1, "frame width not >= 1", 3)
|
||||
element.assert(f.h >= 1, "frame height not >= 1", 3)
|
||||
|
||||
-- create window
|
||||
protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, args.hidden ~= true)
|
||||
|
||||
-- init colors
|
||||
if args.fg_bg ~= nil then
|
||||
protected.fg_bg = args.fg_bg
|
||||
elseif args.parent ~= nil then
|
||||
protected.fg_bg = args.parent.get_fg_bg()
|
||||
end
|
||||
|
||||
-- set colors
|
||||
protected.window.setBackgroundColor(protected.fg_bg.bkg)
|
||||
protected.window.setTextColor(protected.fg_bg.fgd)
|
||||
protected.window.clear()
|
||||
|
||||
-- record position
|
||||
self.position.x, self.position.y = protected.window.getPosition()
|
||||
|
||||
-- shift per parent child offset
|
||||
self.position.x = self.position.x + offset_x
|
||||
self.position.y = self.position.y + offset_y
|
||||
|
||||
-- calculate mouse event bounds
|
||||
self.bounds.x1 = self.position.x
|
||||
self.bounds.x2 = self.position.x + f.w - 1
|
||||
self.bounds.y1 = self.position.y
|
||||
self.bounds.y2 = self.position.y + f.h - 1
|
||||
|
||||
-- alias functions
|
||||
|
||||
-- window set cursor position
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
function protected.w_set_cur(x, y) protected.window.setCursorPos(x, y) end
|
||||
|
||||
-- set background color
|
||||
---@param c color
|
||||
function protected.w_set_bkg(c) protected.window.setBackgroundColor(c) end
|
||||
|
||||
-- set foreground (text) color
|
||||
---@param c color
|
||||
function protected.w_set_fgd(c) protected.window.setTextColor(c) end
|
||||
|
||||
-- write text
|
||||
---@param str string
|
||||
function protected.w_write(str) protected.window.write(str) end
|
||||
|
||||
-- blit text
|
||||
---@param str string
|
||||
---@param fg string
|
||||
---@param bg string
|
||||
function protected.w_blit(str, fg, bg) protected.window.blit(str, fg, bg) end
|
||||
end
|
||||
|
||||
-- check if a coordinate relative to the parent is within the bounds of this element
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
function protected.in_window_bounds(x, y)
|
||||
local in_x = x >= self.bounds.x1 and x <= self.bounds.x2
|
||||
local in_y = y >= self.bounds.y1 and y <= self.bounds.y2
|
||||
return in_x and in_y
|
||||
end
|
||||
|
||||
-- check if a coordinate relative to this window is within the bounds of this element
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
function protected.in_frame_bounds(x, y)
|
||||
local in_x = x >= 1 and x <= protected.frame.w
|
||||
local in_y = y >= 1 and y <= protected.frame.h
|
||||
return in_x and in_y
|
||||
end
|
||||
|
||||
-- get public interface
|
||||
---@nodiscard
|
||||
---@return graphics_element element, element_id id
|
||||
function protected.get() return public, self.id end
|
||||
|
||||
-- report completion of element instantiation and get the public interface
|
||||
---@nodiscard
|
||||
---@return graphics_element element, element_id id
|
||||
function protected.complete()
|
||||
if args.parent ~= nil then args.parent.__child_ready(self.id, public) end
|
||||
return public, self.id
|
||||
end
|
||||
|
||||
-- protected version of public is_focused()
|
||||
---@nodiscard
|
||||
---@return boolean is_focused
|
||||
function protected.is_focused() return self.focused end
|
||||
|
||||
-- defocus this element
|
||||
function protected.defocus() public.unfocus_all() end
|
||||
|
||||
-- focus this element and take away focus from all other elements
|
||||
function protected.take_focus() args.parent.__focus_child(public) end
|
||||
|
||||
-- action handlers --
|
||||
|
||||
-- luacheck: push ignore
|
||||
---@diagnostic disable: unused-local, unused-vararg
|
||||
|
||||
-- handle a child element having been added
|
||||
---@param id element_id element identifier
|
||||
---@param child graphics_element child element
|
||||
function protected.on_added(id, child) end
|
||||
|
||||
-- handle a child element having been removed
|
||||
---@param id element_id element identifier
|
||||
function protected.on_removed(id) end
|
||||
|
||||
-- handle enabled
|
||||
function protected.on_enabled() end
|
||||
|
||||
-- handle disabled
|
||||
function protected.on_disabled() end
|
||||
|
||||
-- handle this element having been focused
|
||||
function protected.on_focused() end
|
||||
|
||||
-- handle this element having been unfocused
|
||||
function protected.on_unfocused() end
|
||||
|
||||
-- handle this element having had a child focused
|
||||
---@param child graphics_element
|
||||
function protected.on_child_focused(child) end
|
||||
|
||||
-- handle this element having been shown
|
||||
function protected.on_shown() end
|
||||
|
||||
-- handle this element having been hidden
|
||||
function protected.on_hidden() end
|
||||
|
||||
-- handle a mouse event
|
||||
---@param event mouse_interaction mouse interaction event
|
||||
function protected.handle_mouse(event) end
|
||||
|
||||
-- handle a keyboard event
|
||||
---@param event key_interaction key interaction event
|
||||
function protected.handle_key(event) end
|
||||
|
||||
-- handle a paste event
|
||||
---@param text string pasted text
|
||||
function protected.handle_paste(text) end
|
||||
|
||||
-- handle data value changes
|
||||
---@vararg any value(s)
|
||||
function protected.on_update(...) end
|
||||
|
||||
-- callback on control press responses
|
||||
---@param result any
|
||||
function protected.response_callback(result) end
|
||||
|
||||
-- accessors and control --
|
||||
|
||||
-- get value
|
||||
---@nodiscard
|
||||
function protected.get_value() return protected.value end
|
||||
|
||||
-- set value
|
||||
---@param value any value to set
|
||||
function protected.set_value(value) end
|
||||
|
||||
-- set minimum input value
|
||||
---@param min integer minimum allowed value
|
||||
function protected.set_min(min) end
|
||||
|
||||
-- set maximum input value
|
||||
---@param max integer maximum allowed value
|
||||
function protected.set_max(max) end
|
||||
|
||||
-- custom recolor command, varies by element if implemented
|
||||
---@vararg cpair|color color(s)
|
||||
function protected.recolor(...) end
|
||||
|
||||
-- custom resize command, varies by element if implemented
|
||||
---@vararg integer sizing
|
||||
function protected.resize(...) end
|
||||
|
||||
-- luacheck: pop
|
||||
---@diagnostic enable: unused-local, unused-vararg
|
||||
|
||||
-- re-draw this element
|
||||
function protected.redraw() end
|
||||
|
||||
-- start animations
|
||||
function protected.start_anim() end
|
||||
|
||||
-- stop animations
|
||||
function protected.stop_anim() end
|
||||
|
||||
-----------
|
||||
-- SETUP --
|
||||
-----------
|
||||
|
||||
-- get the parent window
|
||||
self.p_window = args.window
|
||||
if self.p_window == nil and args.parent ~= nil then
|
||||
self.p_window = args.parent.window()
|
||||
end
|
||||
|
||||
-- check window
|
||||
element.assert(self.p_window, "no parent window provided", 1)
|
||||
|
||||
-- prepare the template
|
||||
if args.parent == nil then
|
||||
self.id = args.id or "__ROOT__"
|
||||
protected.prepare_template(0, 0, 1)
|
||||
else
|
||||
self.id = args.parent.__add_child(args.id, protected)
|
||||
end
|
||||
|
||||
----------------------
|
||||
-- PUBLIC FUNCTIONS --
|
||||
----------------------
|
||||
|
||||
-- get the window object
|
||||
---@nodiscard
|
||||
function public.window() return protected.content_window or protected.window end
|
||||
|
||||
-- delete this element (hide and unsubscribe from PSIL)
|
||||
function public.delete()
|
||||
local fg_bg = protected.fg_bg
|
||||
|
||||
if args.parent ~= nil then
|
||||
-- grab parent fg/bg so we can clear cleanly as a child element
|
||||
fg_bg = args.parent.get_fg_bg()
|
||||
end
|
||||
|
||||
-- clear, hide, and stop animations
|
||||
protected.window.setBackgroundColor(fg_bg.bkg)
|
||||
protected.window.setTextColor(fg_bg.fgd)
|
||||
protected.window.clear()
|
||||
public.hide()
|
||||
|
||||
-- unsubscribe from PSIL
|
||||
for i = 1, #self.subscriptions do
|
||||
local s = self.subscriptions[i] ---@type element_subscription
|
||||
s.ps.unsubscribe(s.key, s.func)
|
||||
end
|
||||
|
||||
-- delete all children
|
||||
for k, v in pairs(protected.children) do
|
||||
v.get().delete()
|
||||
protected.children[k] = nil
|
||||
end
|
||||
|
||||
if args.parent ~= nil then
|
||||
-- remove self from parent
|
||||
args.parent.__remove_child(self.id)
|
||||
end
|
||||
end
|
||||
|
||||
-- ELEMENT TREE --
|
||||
|
||||
-- add a child element
|
||||
---@nodiscard
|
||||
---@param key string|nil id
|
||||
---@param child graphics_base
|
||||
---@return integer|string key
|
||||
function public.__add_child(key, child)
|
||||
child.prepare_template(child_offset_x or 0, child_offset_y or 0, self.next_y)
|
||||
|
||||
self.next_y = child.frame.y + child.frame.h
|
||||
|
||||
local id = key ---@type string|integer|nil
|
||||
if id == nil then
|
||||
id = self.next_id
|
||||
self.next_id = self.next_id + 1
|
||||
end
|
||||
|
||||
table.insert(protected.children, child)
|
||||
|
||||
protected.child_id_map[id] = #protected.children
|
||||
|
||||
return id
|
||||
end
|
||||
|
||||
-- remove a child element
|
||||
---@param id element_id id
|
||||
function public.__remove_child(id)
|
||||
local index = protected.child_id_map[id]
|
||||
if protected.children[index] ~= nil then
|
||||
protected.on_removed(id)
|
||||
protected.children[index] = nil
|
||||
protected.child_id_map[id] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- actions to take upon a child element becoming ready (initial draw/construction completed)
|
||||
---@param key element_id id
|
||||
---@param child graphics_element
|
||||
function public.__child_ready(key, child) protected.on_added(key, child) end
|
||||
|
||||
-- focus solely on this child
|
||||
---@param child graphics_element
|
||||
function public.__focus_child(child)
|
||||
if self.is_root then
|
||||
public.unfocus_all()
|
||||
child.focus()
|
||||
else args.parent.__focus_child(child) end
|
||||
end
|
||||
|
||||
-- a child was focused, used to make sure it is actually visible to the user in the content frame
|
||||
---@param child graphics_element
|
||||
function public.__child_focused(child)
|
||||
protected.on_child_focused(child)
|
||||
if not self.is_root then args.parent.__child_focused(public) end
|
||||
end
|
||||
|
||||
-- get a child element
|
||||
---@nodiscard
|
||||
---@param id element_id
|
||||
---@return graphics_element
|
||||
function public.get_child(id) return protected.children[protected.child_id_map[id]].get() end
|
||||
|
||||
-- remove a child element
|
||||
---@param id element_id
|
||||
function public.remove(id)
|
||||
local index = protected.child_id_map[id]
|
||||
if protected.children[index] ~= nil then
|
||||
protected.children[index].get().delete()
|
||||
protected.on_removed(id)
|
||||
protected.children[index] = nil
|
||||
protected.child_id_map[id] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- remove all child elements and reset next y
|
||||
function public.remove_all()
|
||||
for i = 1, #protected.children do
|
||||
local child = protected.children[i].get() ---@type graphics_element
|
||||
child.delete()
|
||||
protected.on_removed(child.get_id())
|
||||
end
|
||||
|
||||
self.next_y = 1
|
||||
protected.children = {}
|
||||
protected.child_id_map = {}
|
||||
end
|
||||
|
||||
-- attempt to get a child element by ID (does not include this element itself)
|
||||
---@nodiscard
|
||||
---@param id element_id
|
||||
---@return graphics_element|nil element
|
||||
function public.get_element_by_id(id)
|
||||
local index = protected.child_id_map[id]
|
||||
if protected.children[index] == nil then
|
||||
for _, child in pairs(protected.children) do
|
||||
local elem = child.get().get_element_by_id(id)
|
||||
if elem ~= nil then return elem end
|
||||
end
|
||||
else return protected.children[index].get() end
|
||||
end
|
||||
|
||||
-- AUTO-PLACEMENT --
|
||||
|
||||
-- skip a line for automatically placed elements
|
||||
function public.line_break()
|
||||
self.next_y = self.next_y + 1
|
||||
end
|
||||
|
||||
-- PROPERTIES --
|
||||
|
||||
-- get element id
|
||||
---@nodiscard
|
||||
---@return element_id
|
||||
function public.get_id() return self.id end
|
||||
|
||||
-- get element x
|
||||
---@nodiscard
|
||||
---@return integer x
|
||||
function public.get_x() return protected.frame.x end
|
||||
|
||||
-- get element y
|
||||
---@nodiscard
|
||||
---@return integer y
|
||||
function public.get_y() return protected.frame.y end
|
||||
|
||||
-- get element width
|
||||
---@nodiscard
|
||||
---@return integer width
|
||||
function public.get_width() return protected.frame.w end
|
||||
|
||||
-- get element height
|
||||
---@nodiscard
|
||||
---@return integer height
|
||||
function public.get_height() return protected.frame.h end
|
||||
|
||||
-- get the foreground/background colors
|
||||
---@nodiscard
|
||||
---@return cpair fg_bg
|
||||
function public.get_fg_bg() return protected.fg_bg end
|
||||
|
||||
-- get the element value
|
||||
---@nodiscard
|
||||
---@return any value
|
||||
function public.get_value() return protected.get_value() end
|
||||
|
||||
-- set the element value
|
||||
---@param value any new value
|
||||
function public.set_value(value) protected.set_value(value) end
|
||||
|
||||
-- set minimum input value
|
||||
---@param min integer minimum allowed value
|
||||
function public.set_min(min) protected.set_min(min) end
|
||||
|
||||
-- set maximum input value
|
||||
---@param max integer maximum allowed value
|
||||
function public.set_max(max) protected.set_max(max) end
|
||||
|
||||
-- check if this element is enabled
|
||||
function public.is_enabled() return protected.enabled end
|
||||
|
||||
-- enable the element
|
||||
function public.enable()
|
||||
if not protected.enabled then
|
||||
protected.enabled = true
|
||||
protected.on_enabled()
|
||||
end
|
||||
end
|
||||
|
||||
-- disable the element
|
||||
function public.disable()
|
||||
if protected.enabled then
|
||||
protected.enabled = false
|
||||
protected.on_disabled()
|
||||
public.unfocus_all()
|
||||
end
|
||||
end
|
||||
|
||||
-- can this element be focused
|
||||
function public.is_focusable() return args.can_focus end
|
||||
|
||||
-- is this element focused
|
||||
function public.is_focused() return self.focused end
|
||||
|
||||
-- focus the element
|
||||
function public.focus()
|
||||
if args.can_focus and protected.enabled and not self.focused then
|
||||
self.focused = true
|
||||
protected.on_focused()
|
||||
if not self.is_root then args.parent.__child_focused(public) end
|
||||
end
|
||||
end
|
||||
|
||||
-- unfocus this element
|
||||
function public.unfocus()
|
||||
if args.can_focus and self.focused then
|
||||
self.focused = false
|
||||
protected.on_unfocused()
|
||||
end
|
||||
end
|
||||
|
||||
-- unfocus this element and all its children
|
||||
function public.unfocus_all()
|
||||
public.unfocus()
|
||||
for _, child in pairs(protected.children) do child.get().unfocus_all() end
|
||||
end
|
||||
|
||||
-- custom recolor command, varies by element if implemented
|
||||
---@vararg cpair|color color(s)
|
||||
function public.recolor(...) protected.recolor(...) end
|
||||
|
||||
-- resize attributes of the element value if supported
|
||||
---@vararg number dimensions (element specific)
|
||||
function public.resize(...) protected.resize(...) end
|
||||
|
||||
-- reposition the element window<br>
|
||||
-- offsets relative to parent frame are where (1, 1) would be on top of the parent's top left corner
|
||||
---@param x integer x position relative to parent frame
|
||||
---@param y integer y position relative to parent frame
|
||||
function public.reposition(x, y)
|
||||
protected.window.reposition(x, y)
|
||||
|
||||
-- record position
|
||||
self.position.x, self.position.y = protected.window.getPosition()
|
||||
|
||||
-- shift per parent child offset
|
||||
self.position.x = self.position.x + self.offset_x
|
||||
self.position.y = self.position.y + self.offset_y
|
||||
|
||||
-- calculate mouse event bounds
|
||||
self.bounds.x1 = self.position.x
|
||||
self.bounds.x2 = self.position.x + protected.frame.w - 1
|
||||
self.bounds.y1 = self.position.y
|
||||
self.bounds.y2 = self.position.y + protected.frame.h - 1
|
||||
end
|
||||
|
||||
-- FUNCTION CALLBACKS --
|
||||
|
||||
-- handle a monitor touch or mouse click if this element is visible
|
||||
---@param event mouse_interaction mouse interaction event
|
||||
function public.handle_mouse(event)
|
||||
if protected.window.isVisible() then
|
||||
local x_ini, y_ini = event.initial.x, event.initial.y
|
||||
|
||||
local ini_in = protected.in_window_bounds(x_ini, y_ini)
|
||||
|
||||
if ini_in then
|
||||
if event.type == events.MOUSE_CLICK.UP or event.type == events.MOUSE_CLICK.DRAG then
|
||||
-- make sure we don't handle mouse events that started before this element was made visible
|
||||
if (event.initial.x ~= self.button_down[event.button].x) or (event.initial.y ~= self.button_down[event.button].y) then
|
||||
return
|
||||
end
|
||||
elseif event.type == events.MOUSE_CLICK.DOWN then
|
||||
self.button_down[event.button] = event.initial
|
||||
end
|
||||
|
||||
local event_T = events.mouse_transposed(event, self.position.x, self.position.y)
|
||||
protected.handle_mouse(event_T)
|
||||
|
||||
-- shift child event if the content window has moved then pass to children
|
||||
local c_event_T = events.mouse_transposed(event_T, protected.mouse_window_shift.x + 1, protected.mouse_window_shift.y + 1)
|
||||
for _, child in pairs(protected.children) do child.get().handle_mouse(c_event_T) end
|
||||
elseif event.type == events.MOUSE_CLICK.DOWN or event.type == events.MOUSE_CLICK.TAP then
|
||||
-- clicked out, unfocus this element and children
|
||||
public.unfocus_all()
|
||||
end
|
||||
else
|
||||
-- don't track clicks while hidden
|
||||
self.button_down[event.button] = events.new_coord_2d(-1, -1)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle a keyboard click if this element is visible and focused
|
||||
---@param event key_interaction keyboard interaction event
|
||||
function public.handle_key(event)
|
||||
if protected.window.isVisible() then
|
||||
if self.is_root and (event.type == events.KEY_CLICK.DOWN) and (event.key == keys.tab) then
|
||||
-- try to jump to the next/previous focusable field
|
||||
_tab_focusable(event.shift)
|
||||
else
|
||||
-- handle the key event then pass to children
|
||||
if self.focused then protected.handle_key(event) end
|
||||
for _, child in pairs(protected.children) do child.get().handle_key(event) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle text paste
|
||||
---@param text string pasted text
|
||||
function public.handle_paste(text)
|
||||
if protected.window.isVisible() then
|
||||
-- handle the paste event then pass to children
|
||||
if self.focused then protected.handle_paste(text) end
|
||||
for _, child in pairs(protected.children) do child.get().handle_paste(text) end
|
||||
end
|
||||
end
|
||||
|
||||
-- draw the element given new data
|
||||
---@vararg any new data
|
||||
function public.update(...) protected.on_update(...) end
|
||||
|
||||
-- on a control request response
|
||||
---@param result any
|
||||
function public.on_response(result) protected.response_callback(result) end
|
||||
|
||||
-- register a callback with a PSIL, allowing for automatic unregister on delete<br>
|
||||
-- do not use graphics elements directly with PSIL subscribe()
|
||||
---@param ps psil PSIL to subscribe to
|
||||
---@param key string key to subscribe to
|
||||
---@param func function function to link
|
||||
function public.register(ps, key, func)
|
||||
table.insert(self.subscriptions, { ps = ps, key = key, func = func })
|
||||
ps.subscribe(key, func)
|
||||
end
|
||||
|
||||
-- VISIBILITY & ANIMATIONS --
|
||||
|
||||
-- check if this element is visible
|
||||
function public.is_visible() return protected.window.isVisible() end
|
||||
|
||||
-- show the element and enables animations by default
|
||||
---@param animate? boolean true (default) to automatically resume animations
|
||||
function public.show(animate)
|
||||
protected.window.setVisible(true)
|
||||
if animate ~= false then public.animate_all() end
|
||||
end
|
||||
|
||||
-- hide the element and disables animations<br>
|
||||
-- this alone does not cause an element to be fully hidden, it only prevents updates from being shown<br>
|
||||
---@see graphics_element.redraw
|
||||
---@see graphics_element.content_redraw
|
||||
---@param clear? boolean true to visibly hide this element (redraws the parent)
|
||||
function public.hide(clear)
|
||||
public.freeze_all() -- stop animations for efficiency/performance
|
||||
public.unfocus_all()
|
||||
protected.window.setVisible(false)
|
||||
if clear and args.parent then args.parent.redraw() end
|
||||
end
|
||||
|
||||
-- start/resume animation(s)
|
||||
function public.animate() protected.start_anim() end
|
||||
|
||||
-- start/resume animation(s) for this element and all its children<br>
|
||||
-- only animates if a window is visible
|
||||
function public.animate_all()
|
||||
if protected.window.isVisible() then
|
||||
public.animate()
|
||||
for _, child in pairs(protected.children) do child.get().animate_all() end
|
||||
end
|
||||
end
|
||||
|
||||
-- freeze animation(s)
|
||||
function public.freeze() protected.stop_anim() end
|
||||
|
||||
-- freeze animation(s) for this element and all its children
|
||||
function public.freeze_all()
|
||||
public.freeze()
|
||||
for _, child in pairs(protected.children) do child.get().freeze_all() end
|
||||
end
|
||||
|
||||
-- re-draw this element and all its children
|
||||
function public.redraw()
|
||||
local bg, fg = protected.window.getBackgroundColor(), protected.window.getTextColor()
|
||||
protected.window.setBackgroundColor(protected.fg_bg.bkg)
|
||||
protected.window.setTextColor(protected.fg_bg.fgd)
|
||||
protected.window.clear()
|
||||
protected.window.setBackgroundColor(bg)
|
||||
protected.window.setTextColor(fg)
|
||||
protected.redraw()
|
||||
for _, child in pairs(protected.children) do child.get().redraw() end
|
||||
end
|
||||
|
||||
-- if a content window is set, clears it then re-draws all children
|
||||
function public.content_redraw()
|
||||
if protected.content_window ~= nil then
|
||||
protected.content_window.clear()
|
||||
for _, child in pairs(protected.children) do child.get().redraw() end
|
||||
end
|
||||
end
|
||||
|
||||
return protected
|
||||
end
|
||||
|
||||
return element
|
||||
109
graphics/elements/animations/waiting.lua
Normal file
109
graphics/elements/animations/waiting.lua
Normal file
@@ -0,0 +1,109 @@
|
||||
-- Loading/Waiting Animation Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class waiting_args
|
||||
---@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 waiting animation element
|
||||
---@param args waiting_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function waiting(args)
|
||||
local state = 0
|
||||
local run_animation = false
|
||||
|
||||
args.width = 4
|
||||
args.height = 3
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local blit_fg = e.fg_bg.blit_fgd
|
||||
local blit_bg = e.fg_bg.blit_bkg
|
||||
local blit_fg_2x = e.fg_bg.blit_fgd .. e.fg_bg.blit_fgd
|
||||
local blit_bg_2x = e.fg_bg.blit_bkg .. e.fg_bg.blit_bkg
|
||||
|
||||
-- tick the animation
|
||||
local function animate()
|
||||
e.window.clear()
|
||||
|
||||
if state >= 0 and state < 7 then
|
||||
-- top
|
||||
e.w_set_cur(1 + math.floor(state / 2), 1)
|
||||
if state % 2 == 0 then
|
||||
e.w_blit("\x8f", blit_fg, blit_bg)
|
||||
else
|
||||
e.w_blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
|
||||
end
|
||||
|
||||
-- bottom
|
||||
e.w_set_cur(4 - math.ceil(state / 2), 3)
|
||||
if state % 2 == 0 then
|
||||
e.w_blit("\x8f", blit_fg, blit_bg)
|
||||
else
|
||||
e.w_blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
|
||||
end
|
||||
else
|
||||
local st = state - 7
|
||||
|
||||
-- right
|
||||
if st % 3 == 0 then
|
||||
e.w_set_cur(4, 1 + math.floor(st / 3))
|
||||
e.w_blit("\x83", blit_bg, blit_fg)
|
||||
elseif st % 3 == 1 then
|
||||
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.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.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.w_set_cur(1, 2 - math.floor(st / 3))
|
||||
e.w_blit("\x83", blit_bg, blit_fg)
|
||||
else
|
||||
e.w_set_cur(1, 2 - math.floor(st / 3))
|
||||
e.w_blit("\x8f", blit_fg, blit_bg)
|
||||
end
|
||||
end
|
||||
|
||||
state = state + 1
|
||||
if state >= 12 then state = 0 end
|
||||
|
||||
if run_animation then
|
||||
tcd.dispatch_unique(0.15, animate)
|
||||
end
|
||||
end
|
||||
|
||||
-- start the animation
|
||||
function e.start_anim()
|
||||
run_animation = true
|
||||
animate()
|
||||
end
|
||||
|
||||
-- stop the animation
|
||||
function e.stop_anim()
|
||||
run_animation = false
|
||||
end
|
||||
|
||||
e.start_anim()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return waiting
|
||||
37
graphics/elements/colormap.lua
Normal file
37
graphics/elements/colormap.lua
Normal file
@@ -0,0 +1,37 @@
|
||||
-- Color Map Graphics Element
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class colormap_args
|
||||
---@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 color map
|
||||
---@param args colormap_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function colormap(args)
|
||||
local bkg = "008877FFCCEE114455DD9933BBAA2266"
|
||||
local spaces = string.rep(" ", 32)
|
||||
|
||||
args.width = 32
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- draw color map
|
||||
function e.redraw()
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(spaces, bkg, bkg)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return colormap
|
||||
132
graphics/elements/controls/app.lua
Normal file
132
graphics/elements/controls/app.lua
Normal file
@@ -0,0 +1,132 @@
|
||||
-- App Button Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class app_button_args
|
||||
---@field text string app icon text
|
||||
---@field title string app title text
|
||||
---@field callback function function to call on touch
|
||||
---@field app_fg_bg cpair app icon foreground/background colors
|
||||
---@field active_fg_bg? cpair foreground/background colors when pressed
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new app button
|
||||
---@param args app_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function app_button(args)
|
||||
element.assert(type(args.text) == "string", "text is a required field")
|
||||
element.assert(type(args.title) == "string", "title is a required field")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
element.assert(type(args.app_fg_bg) == "table", "app_fg_bg is a required field")
|
||||
|
||||
args.height = 4
|
||||
args.width = 5
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- draw the app button
|
||||
local function draw()
|
||||
local fgd = args.app_fg_bg.fgd
|
||||
local bkg = args.app_fg_bg.bkg
|
||||
|
||||
if e.value then
|
||||
fgd = args.active_fg_bg.fgd
|
||||
bkg = args.active_fg_bg.bkg
|
||||
end
|
||||
|
||||
-- draw icon
|
||||
e.w_set_cur(1, 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(1, 2)
|
||||
e.w_write("\x95 ")
|
||||
e.w_set_fgd(bkg)
|
||||
e.w_set_bkg(fgd)
|
||||
e.w_write("\x95")
|
||||
e.w_set_cur(1, 3)
|
||||
e.w_write("\x82\x8f\x8f\x8f\x81")
|
||||
|
||||
-- write the icon text
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_set_fgd(fgd)
|
||||
e.w_set_bkg(bkg)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
-- draw the app button as pressed (if active_fg_bg set)
|
||||
local function show_pressed()
|
||||
if e.enabled and args.active_fg_bg ~= nil then
|
||||
e.value = true
|
||||
e.w_set_fgd(args.active_fg_bg.fgd)
|
||||
e.w_set_bkg(args.active_fg_bg.bkg)
|
||||
draw()
|
||||
end
|
||||
end
|
||||
|
||||
-- draw the app button as unpressed (if active_fg_bg set)
|
||||
local function show_unpressed()
|
||||
if e.enabled and args.active_fg_bg ~= nil then
|
||||
e.value = false
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
draw()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled then
|
||||
if event.type == MOUSE_CLICK.TAP then
|
||||
show_pressed()
|
||||
-- show as unpressed in 0.25 seconds
|
||||
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
|
||||
args.callback()
|
||||
elseif event.type == MOUSE_CLICK.DOWN then
|
||||
show_pressed()
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
show_unpressed()
|
||||
if e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
args.callback()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value (true simulates pressing the app button)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
|
||||
end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
e.w_set_cur(math.floor((e.frame.w - string.len(args.title)) / 2) + 1, 4)
|
||||
e.w_write(args.title)
|
||||
draw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return app_button
|
||||
121
graphics/elements/controls/checkbox.lua
Normal file
121
graphics/elements/controls/checkbox.lua
Normal file
@@ -0,0 +1,121 @@
|
||||
-- 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 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
|
||||
|
||||
-- new checkbox control
|
||||
---@param args checkbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function checkbox(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)
|
||||
|
||||
e.value = args.default == true
|
||||
|
||||
-- show the button state
|
||||
local function draw()
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if e.value then
|
||||
-- show as selected
|
||||
e.w_set_fgd(args.box_fg_bg.bkg)
|
||||
e.w_set_bkg(args.box_fg_bg.fgd)
|
||||
e.w_write("\x88")
|
||||
e.w_set_fgd(args.box_fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write("\x95")
|
||||
else
|
||||
-- show as unselected
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(args.box_fg_bg.bkg)
|
||||
e.w_write("\x88")
|
||||
e.w_set_fgd(args.box_fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write("\x95")
|
||||
end
|
||||
end
|
||||
|
||||
-- write label text
|
||||
local function draw_label()
|
||||
if e.enabled and e.is_focused() then
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.fgd)
|
||||
e.w_write(args.label)
|
||||
else
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) 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
|
||||
|
||||
-- handle focus
|
||||
e.on_focused = draw_label
|
||||
e.on_unfocused = draw_label
|
||||
|
||||
-- handle enable
|
||||
e.on_enabled = draw_label
|
||||
e.on_disabled = draw_label
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
draw()
|
||||
draw_label()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return checkbox
|
||||
206
graphics/elements/controls/hazard_button.lua
Normal file
206
graphics/elements/controls/hazard_button.lua
Normal file
@@ -0,0 +1,206 @@
|
||||
-- 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 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)
|
||||
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
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(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 then
|
||||
if core.events.was_clicked(event.type) then
|
||||
-- change text color to indicate clicked
|
||||
e.w_set_fgd(args.accent)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
|
||||
-- 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)
|
||||
|
||||
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.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
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return hazard_button
|
||||
135
graphics/elements/controls/multi_button.lua
Normal file
135
graphics/elements/controls/multi_button.lua
Normal file
@@ -0,0 +1,135 @@
|
||||
-- Multi Button Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class button_option
|
||||
---@field text string
|
||||
---@field fg_bg cpair
|
||||
---@field active_fg_bg cpair
|
||||
---@field _start_x integer starting touch x range (inclusive)
|
||||
---@field _end_x integer ending touch x range (inclusive)
|
||||
|
||||
---@class multi_button_args
|
||||
---@field options table button options
|
||||
---@field callback function function to call on touch
|
||||
---@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 height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new multi button (latch selection, exclusively one button at a time)
|
||||
---@param args multi_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function multi_button(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
|
||||
|
||||
-- determine widths
|
||||
local max_width = 1
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type button_option
|
||||
if string.len(opt.text) > max_width then
|
||||
max_width = string.len(opt.text)
|
||||
end
|
||||
end
|
||||
|
||||
local button_width = math.max(max_width, args.min_width or 0)
|
||||
|
||||
args.width = (button_width * #args.options) + #args.options + 1
|
||||
|
||||
-- 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
|
||||
|
||||
-- calculate required button information
|
||||
local next_x = 2
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type button_option
|
||||
|
||||
opt._start_x = next_x
|
||||
opt._end_x = next_x + button_width - 1
|
||||
|
||||
next_x = next_x + (button_width + 1)
|
||||
end
|
||||
|
||||
-- show the button state
|
||||
function e.redraw()
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type button_option
|
||||
|
||||
e.w_set_cur(opt._start_x, 1)
|
||||
|
||||
if e.value == i then
|
||||
-- show as pressed
|
||||
e.w_set_fgd(opt.active_fg_bg.fgd)
|
||||
e.w_set_bkg(opt.active_fg_bg.bkg)
|
||||
else
|
||||
-- show as unpressed
|
||||
e.w_set_fgd(opt.fg_bg.fgd)
|
||||
e.w_set_bkg(opt.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.w_write(util.pad(opt.text, button_width))
|
||||
end
|
||||
end
|
||||
|
||||
-- check which button a given x is within
|
||||
---@return integer|nil button index or nil if not within a button
|
||||
local function which_button(x)
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type button_option
|
||||
if x >= opt._start_x and x <= opt._end_x then return i end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- if enabled and the button row was pressed...
|
||||
if e.enabled and core.events.was_clicked(event.type) then
|
||||
-- a button may have been pressed, which one was it?
|
||||
local button_ini = which_button(event.initial.x)
|
||||
local button_cur = which_button(event.current.x)
|
||||
|
||||
-- mouse up must always have started with a mouse down on the same button to count as a click
|
||||
-- tap always has identical coordinates, so this always passes for taps
|
||||
if button_ini == button_cur and button_cur ~= nil then
|
||||
e.value = button_cur
|
||||
e.redraw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return multi_button
|
||||
153
graphics/elements/controls/push_button.lua
Normal file
153
graphics/elements/controls/push_button.lua
Normal file
@@ -0,0 +1,153 @@
|
||||
-- Button Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
|
||||
---@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 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)
|
||||
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.height = 1
|
||||
args.min_width = args.min_width or 0
|
||||
args.width = math.max(text_width, args.min_width)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local h_pad = 1
|
||||
local v_pad = math.floor(e.frame.h / 2) + 1
|
||||
|
||||
if alignment == ALIGN.CENTER then
|
||||
h_pad = math.floor((e.frame.w - text_width) / 2) + 1
|
||||
elseif alignment == ALIGN.RIGHT then
|
||||
h_pad = (e.frame.w - text_width) + 1
|
||||
end
|
||||
|
||||
-- draw the button
|
||||
function e.redraw()
|
||||
e.window.clear()
|
||||
|
||||
-- write the button text
|
||||
e.w_set_cur(h_pad, v_pad)
|
||||
e.w_write(args.text)
|
||||
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()
|
||||
e.defocus()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value (true simulates pressing the button)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.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
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return push_button
|
||||
203
graphics/elements/controls/radio_2d.lua
Normal file
203
graphics/elements/controls/radio_2d.lua
Normal file
@@ -0,0 +1,203 @@
|
||||
-- 2D Radio Button Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class radio_2d_args
|
||||
---@field rows integer
|
||||
---@field columns integer
|
||||
---@field options table
|
||||
---@field radio_colors cpair radio button colors (inner & outer)
|
||||
---@field select_color? color color for radio button when selected
|
||||
---@field color_map? table colors for each radio button when selected
|
||||
---@field disable_color? color color for radio button when disabled
|
||||
---@field disable_fg_bg? cpair text colors when disabled
|
||||
---@field default? integer default state, defaults to options[1]
|
||||
---@field callback? function function to call on touch
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new 2D radio button list (latch selection, exclusively one color at a time)
|
||||
---@param args radio_2d_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function radio_2d_button(args)
|
||||
element.assert(type(args.options) == "table" and #args.options > 0, "options should be a table with length >= 1")
|
||||
element.assert(util.is_int(args.rows) and util.is_int(args.columns), "rows/columns must be integers")
|
||||
element.assert((args.rows * args.columns) >= #args.options, "rows x columns size insufficient for provided number of options")
|
||||
element.assert(type(args.radio_colors) == "table", "radio_colors is a required field")
|
||||
element.assert(type(args.select_color) == "number" or type(args.color_map) == "table", "select_color or color_map is required")
|
||||
element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
|
||||
|
||||
local array = {}
|
||||
local col_widths = {}
|
||||
|
||||
local next_idx = 1
|
||||
local total_width = 0
|
||||
local max_rows = 1
|
||||
|
||||
local focused_opt = 1
|
||||
local focus_x, focus_y = 1, 1
|
||||
|
||||
-- build table to display
|
||||
for col = 1, args.columns do
|
||||
local max_width = 0
|
||||
array[col] = {}
|
||||
|
||||
for row = 1, args.rows do
|
||||
local len = string.len(args.options[next_idx])
|
||||
if len > max_width then max_width = len end
|
||||
if row > max_rows then max_rows = row end
|
||||
|
||||
table.insert(array[col], { text = args.options[next_idx], id = next_idx, x_1 = 1 + total_width, x_2 = 2 + total_width + len })
|
||||
|
||||
next_idx = next_idx + 1
|
||||
if next_idx > #args.options then break end
|
||||
end
|
||||
|
||||
table.insert(col_widths, max_width + 3)
|
||||
total_width = total_width + max_width + 3
|
||||
if next_idx > #args.options then break end
|
||||
end
|
||||
|
||||
args.can_focus = true
|
||||
args.width = total_width
|
||||
args.height = max_rows
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- selected option (convert nil to 1 if missing)
|
||||
e.value = args.default or 1
|
||||
|
||||
-- draw the element
|
||||
function e.redraw()
|
||||
local col_x = 1
|
||||
|
||||
local radio_color_b = util.trinary(type(args.disable_color) == "number" and not e.enabled, args.disable_color, args.radio_colors.color_b)
|
||||
|
||||
for col = 1, #array do
|
||||
for row = 1, #array[col] do
|
||||
local opt = array[col][row]
|
||||
local select_color = args.select_color
|
||||
|
||||
if type(args.color_map) == "table" and args.color_map[opt.id] then
|
||||
select_color = args.color_map[opt.id]
|
||||
end
|
||||
|
||||
local inner_color = util.trinary((e.value == opt.id) and e.enabled, radio_color_b, args.radio_colors.color_a)
|
||||
local outer_color = util.trinary((e.value == opt.id) and e.enabled, select_color, radio_color_b)
|
||||
|
||||
e.w_set_cur(col_x, row)
|
||||
|
||||
e.w_set_fgd(inner_color)
|
||||
e.w_set_bkg(outer_color)
|
||||
e.w_write("\x88")
|
||||
|
||||
e.w_set_fgd(outer_color)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write("\x95")
|
||||
|
||||
if opt.id == focused_opt then
|
||||
focus_x, focus_y = row, col
|
||||
end
|
||||
|
||||
-- write button text
|
||||
if opt.id == focused_opt and e.is_focused() and e.enabled then
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.fgd)
|
||||
elseif type(args.disable_fg_bg) == "table" and not e.enabled then
|
||||
e.w_set_fgd(args.disable_fg_bg.fgd)
|
||||
e.w_set_bkg(args.disable_fg_bg.bkg)
|
||||
else
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.w_write(opt.text)
|
||||
end
|
||||
|
||||
col_x = col_x + col_widths[col]
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) and (event.initial.y == event.current.y) then
|
||||
-- determine what was pressed
|
||||
for _, row in ipairs(array) do
|
||||
local elem = row[event.current.y]
|
||||
if elem ~= nil and event.initial.x >= elem.x_1 and event.initial.x <= elem.x_2 and event.current.x >= elem.x_1 and event.current.x <= elem.x_2 then
|
||||
e.value = elem.id
|
||||
focused_opt = elem.id
|
||||
e.redraw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == core.events.KEY_CLICK.DOWN or event.type == core.events.KEY_CLICK.HELD then
|
||||
if event.type == core.events.KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then
|
||||
e.value = focused_opt
|
||||
e.redraw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
elseif event.key == keys.down then
|
||||
if focused_opt < #args.options then
|
||||
focused_opt = focused_opt + 1
|
||||
e.redraw()
|
||||
end
|
||||
elseif event.key == keys.up then
|
||||
if focused_opt > 1 then
|
||||
focused_opt = focused_opt - 1
|
||||
e.redraw()
|
||||
end
|
||||
elseif event.key == keys.right then
|
||||
if array[focus_y + 1] and array[focus_y + 1][focus_x] then
|
||||
focused_opt = array[focus_y + 1][focus_x].id
|
||||
else focused_opt = array[1][focus_x].id end
|
||||
e.redraw()
|
||||
elseif event.key == keys.left then
|
||||
if array[focus_y - 1] and array[focus_y - 1][focus_x] then
|
||||
focused_opt = array[focus_y - 1][focus_x].id
|
||||
e.redraw()
|
||||
elseif array[#array][focus_x] then
|
||||
focused_opt = array[#array][focus_x].id
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
if type(val) == "number" and val > 0 and val <= #args.options then
|
||||
e.value = val
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle focus & enable
|
||||
e.on_focused = e.redraw
|
||||
e.on_unfocused = e.redraw
|
||||
e.on_enabled = e.redraw
|
||||
e.on_disabled = e.redraw
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return radio_2d_button
|
||||
147
graphics/elements/controls/radio_button.lua
Normal file
147
graphics/elements/controls/radio_button.lua
Normal file
@@ -0,0 +1,147 @@
|
||||
-- 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 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
|
||||
|
||||
-- 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)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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 i == focused_opt and e.is_focused() and e.enabled then
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.fgd)
|
||||
else
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.w_write(opt)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) and (event.initial.y == event.current.y) then
|
||||
-- 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
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return radio_button
|
||||
124
graphics/elements/controls/sidebar.lua
Normal file
124
graphics/elements/controls/sidebar.lua
Normal file
@@ -0,0 +1,124 @@
|
||||
-- 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_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)
|
||||
element.assert(type(args.tabs) == "table", "tabs is a required field")
|
||||
element.assert(#args.tabs > 0, "at least one tab is required")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
|
||||
args.width = 3
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
element.assert(e.frame.h >= (#args.tabs * 3), "height insufficent to display all tabs")
|
||||
|
||||
-- default to 1st tab
|
||||
e.value = 1
|
||||
|
||||
local was_pressed = false
|
||||
|
||||
-- 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
|
||||
|
||||
for i = 1, #args.tabs do
|
||||
local tab = args.tabs[i] ---@type sidebar_tab
|
||||
|
||||
local y = ((i - 1) * 3) + 1
|
||||
|
||||
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
|
||||
|
||||
e.w_write(" ")
|
||||
e.w_set_cur(1, y + 1)
|
||||
if e.value == i then
|
||||
e.w_write(" " .. tab.char .. "\x10")
|
||||
else e.w_write(" " .. tab.char .. " ") end
|
||||
e.w_set_cur(1, y + 2)
|
||||
e.w_write(" ")
|
||||
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 == MOUSE_CLICK.TAP then
|
||||
e.value = cur_idx
|
||||
draw(true)
|
||||
-- show as unpressed in 0.25 seconds
|
||||
tcd.dispatch(0.25, function () draw(false) end)
|
||||
args.callback(e.value)
|
||||
elseif event.type == MOUSE_CLICK.DOWN then
|
||||
draw(true, cur_idx)
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
e.value = cur_idx
|
||||
draw(false)
|
||||
args.callback(e.value)
|
||||
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
|
||||
|
||||
-- element redraw
|
||||
e.redraw = draw
|
||||
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return sidebar
|
||||
189
graphics/elements/controls/spinbox_numeric.lua
Normal file
189
graphics/elements/controls/spinbox_numeric.lua
Normal file
@@ -0,0 +1,189 @@
|
||||
-- Spinbox Numeric Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class spinbox_args
|
||||
---@field default? number default value, defaults to 0.0
|
||||
---@field min? number default 0, currently must be 0 or greater
|
||||
---@field max? number default max number that can be displayed with the digits configuration
|
||||
---@field whole_num_precision integer number of whole number digits
|
||||
---@field fractional_precision integer number of fractional digits
|
||||
---@field arrow_fg_bg cpair arrow foreground/background colors
|
||||
---@field arrow_disable? color color when disabled (default light gray)
|
||||
---@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 spinbox control (minimum value is 0)
|
||||
---@param args spinbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function spinbox(args)
|
||||
-- properties
|
||||
local digits = {}
|
||||
local wn_prec = args.whole_num_precision
|
||||
local fr_prec = args.fractional_precision
|
||||
|
||||
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
|
||||
|
||||
if fr_prec > 0 then
|
||||
fmt = "%" .. (wn_prec + fr_prec + 1) .. "." .. fr_prec .. "f"
|
||||
fmt_init = "%0" .. (wn_prec + fr_prec + 1) .. "." .. fr_prec .. "f"
|
||||
else
|
||||
fmt = "%" .. wn_prec .. "d"
|
||||
fmt_init = "%0" .. wn_prec .. "d"
|
||||
end
|
||||
|
||||
local dec_point_x = args.whole_num_precision + 1
|
||||
|
||||
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)
|
||||
|
||||
-- set initial value
|
||||
e.value = args.default or 0
|
||||
|
||||
-- draw the arrows
|
||||
local function draw_arrows(color)
|
||||
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.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
|
||||
|
||||
-- populate digits from current value
|
||||
local function set_digits()
|
||||
local initial_str = util.sprintf(fmt_init, e.value)
|
||||
|
||||
digits = {}
|
||||
---@diagnostic disable-next-line: discard-returns
|
||||
initial_str:gsub("%d", function (char) table.insert(digits, char) end)
|
||||
end
|
||||
|
||||
-- update the value per digits table
|
||||
local function update_value()
|
||||
e.value = 0
|
||||
for i = 1, #digits do
|
||||
local pow = math.abs(wn_prec - i)
|
||||
if i <= wn_prec then
|
||||
e.value = e.value + (digits[i] * (10 ^ pow))
|
||||
else
|
||||
e.value = e.value + (digits[i] * (10 ^ -pow))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- print out the current value
|
||||
local function show_num()
|
||||
-- enforce limits
|
||||
if (type(args.min) == "number") and (e.value < args.min) then
|
||||
e.value = args.min
|
||||
set_digits()
|
||||
elseif e.value < 0 then
|
||||
e.value = 0
|
||||
set_digits()
|
||||
else
|
||||
if string.len(util.sprintf(fmt, e.value)) > args.width then
|
||||
-- max printable exceeded, so max out to all 9s
|
||||
for i = 1, #digits do digits[i] = 9 end
|
||||
update_value()
|
||||
elseif (type(args.max) == "number") and (e.value > args.max) then
|
||||
e.value = args.max
|
||||
set_digits()
|
||||
else
|
||||
set_digits()
|
||||
end
|
||||
end
|
||||
|
||||
-- draw
|
||||
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
|
||||
|
||||
-- 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
|
||||
digits[idx] = digits[idx] + 1
|
||||
elseif event.current.y == 3 then
|
||||
digits[idx] = digits[idx] - 1
|
||||
end
|
||||
|
||||
update_value()
|
||||
show_num()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val number number to show
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
show_num()
|
||||
end
|
||||
|
||||
-- set minimum input value
|
||||
---@param min integer minimum allowed value
|
||||
function e.set_min(min)
|
||||
if min >= 0 then
|
||||
args.min = min
|
||||
show_num()
|
||||
end
|
||||
end
|
||||
|
||||
-- set maximum input value
|
||||
---@param max integer maximum allowed value
|
||||
function e.set_max(max)
|
||||
args.max = max
|
||||
show_num()
|
||||
end
|
||||
|
||||
-- enable this input
|
||||
function e.on_enabled() draw_arrows(args.arrow_fg_bg.fgd) end
|
||||
|
||||
-- disable this input
|
||||
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
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return spinbox
|
||||
81
graphics/elements/controls/switch_button.lua
Normal file
81
graphics/elements/controls/switch_button.lua
Normal file
@@ -0,0 +1,81 @@
|
||||
-- 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)
|
||||
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)
|
||||
|
||||
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) then
|
||||
e.value = not e.value
|
||||
e.redraw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return switch_button
|
||||
129
graphics/elements/controls/tabbar.lua
Normal file
129
graphics/elements/controls/tabbar.lua
Normal file
@@ -0,0 +1,129 @@
|
||||
-- Tab Bar Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class tabbar_tab
|
||||
---@field name string tab name
|
||||
---@field color cpair tab colors (fg/bg)
|
||||
---@field _start_x integer starting touch x range (inclusive)
|
||||
---@field _end_x integer ending touch x range (inclusive)
|
||||
|
||||
---@class tabbar_args
|
||||
---@field tabs table tab options
|
||||
---@field callback function function to call on tab change
|
||||
---@field min_width? integer text length + 2 if omitted
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new tab selector
|
||||
---@param args tabbar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function tabbar(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")
|
||||
|
||||
args.height = 1
|
||||
|
||||
-- determine widths
|
||||
local max_width = 1
|
||||
for i = 1, #args.tabs do
|
||||
local opt = args.tabs[i] ---@type tabbar_tab
|
||||
if string.len(opt.name) > max_width then
|
||||
max_width = string.len(opt.name)
|
||||
end
|
||||
end
|
||||
|
||||
local button_width = math.max(max_width, args.min_width or 0)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
element.assert(e.frame.w >= (button_width * #args.tabs), "width insufficent to display all tabs")
|
||||
|
||||
-- default to 1st tab
|
||||
e.value = 1
|
||||
|
||||
-- calculate required tab dimension information
|
||||
local next_x = 1
|
||||
for i = 1, #args.tabs do
|
||||
local tab = args.tabs[i] ---@type tabbar_tab
|
||||
|
||||
tab._start_x = next_x
|
||||
tab._end_x = next_x + button_width - 1
|
||||
|
||||
next_x = next_x + button_width
|
||||
end
|
||||
|
||||
-- show the tab state
|
||||
function e.redraw()
|
||||
for i = 1, #args.tabs do
|
||||
local tab = args.tabs[i] ---@type tabbar_tab
|
||||
|
||||
e.w_set_cur(tab._start_x, 1)
|
||||
|
||||
if e.value == i then
|
||||
e.w_set_fgd(tab.color.fgd)
|
||||
e.w_set_bkg(tab.color.bkg)
|
||||
else
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.w_write(util.pad(tab.name, button_width))
|
||||
end
|
||||
end
|
||||
|
||||
-- check which tab a given x is within
|
||||
---@return integer|nil button index or nil if not within a tab
|
||||
local function which_tab(x)
|
||||
for i = 1, #args.tabs do
|
||||
local tab = args.tabs[i] ---@type tabbar_tab
|
||||
if x >= tab._start_x and x <= tab._end_x then return i end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- determine what was pressed
|
||||
if e.enabled and core.events.was_clicked(event.type) then
|
||||
-- a button may have been pressed, which one was it?
|
||||
local tab_ini = which_tab(event.initial.x)
|
||||
local tab_cur = which_tab(event.current.x)
|
||||
|
||||
-- mouse up must always have started with a mouse down on the same tab to count as a click
|
||||
-- tap always has identical coordinates, so this always passes for taps
|
||||
if tab_ini == tab_cur and tab_cur ~= nil then
|
||||
e.value = tab_cur
|
||||
e.redraw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return tabbar
|
||||
25
graphics/elements/displaybox.lua
Normal file
25
graphics/elements/displaybox.lua
Normal file
@@ -0,0 +1,25 @@
|
||||
-- Root Display Box Graphics Element
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class displaybox_args
|
||||
---@field window table
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new root display box
|
||||
---@nodiscard
|
||||
---@param args displaybox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function displaybox(args)
|
||||
-- create new graphics element base object
|
||||
return element.new(args).complete()
|
||||
end
|
||||
|
||||
return displaybox
|
||||
25
graphics/elements/div.lua
Normal file
25
graphics/elements/div.lua
Normal file
@@ -0,0 +1,25 @@
|
||||
-- Div (Division, like in HTML) Graphics Element
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class div_args
|
||||
---@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 div element
|
||||
---@nodiscard
|
||||
---@param args div_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function div(args)
|
||||
-- create new graphics element base object
|
||||
return element.new(args).complete()
|
||||
end
|
||||
|
||||
return div
|
||||
197
graphics/elements/form/number_field.lua
Normal file
197
graphics/elements/form/number_field.lua
Normal file
@@ -0,0 +1,197 @@
|
||||
-- Numeric Value Entry Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class number_field_args
|
||||
---@field default? number default value, defaults to 0
|
||||
---@field min? number minimum, enforced on unfocus
|
||||
---@field max? number maximum, enforced on unfocus
|
||||
---@field max_chars? integer maximum number of characters, defaults to width
|
||||
---@field max_int_digits? integer maximum number of integer digits, enforced on unfocus
|
||||
---@field max_frac_digits? integer maximum number of fractional digits, enforced on unfocus
|
||||
---@field allow_decimal? boolean true to allow decimals
|
||||
---@field allow_negative? boolean true to allow negative numbers
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new numeric entry field
|
||||
---@param args number_field_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function number_field(args)
|
||||
element.assert(args.max_int_digits == nil or (util.is_int(args.max_int_digits) and args.max_int_digits > 0), "max_int_digits must be an integer greater than zero if supplied")
|
||||
element.assert(args.max_frac_digits == nil or (util.is_int(args.max_frac_digits) and args.max_frac_digits > 0), "max_frac_digits must be an integer greater than zero if supplied")
|
||||
|
||||
args.height = 1
|
||||
args.can_focus = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local has_decimal = false
|
||||
|
||||
args.max_chars = args.max_chars or e.frame.w
|
||||
|
||||
-- set initial value
|
||||
e.value = "" .. (args.default or 0)
|
||||
|
||||
-- make an interactive field manager
|
||||
local ifield = core.new_ifield(e, args.max_chars, args.fg_bg, args.dis_fg_bg)
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- only handle if on an increment or decrement arrow
|
||||
if e.enabled then
|
||||
if core.events.was_clicked(event.type) then
|
||||
e.take_focus()
|
||||
|
||||
if event.type == MOUSE_CLICK.UP and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
ifield.move_cursor(event.current.x)
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
|
||||
ifield.select_all()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == KEY_CLICK.CHAR and string.len(e.value) < args.max_chars then
|
||||
if tonumber(event.name) then
|
||||
if e.value == 0 then e.value = "" end
|
||||
ifield.try_insert_char(event.name)
|
||||
end
|
||||
elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
|
||||
if (event.key == keys.backspace or event.key == keys.delete) and (string.len(e.value) > 0) then
|
||||
ifield.backspace()
|
||||
has_decimal = string.find(e.value, "%.") ~= nil
|
||||
elseif (event.key == keys.period or event.key == keys.numPadDecimal) and (not has_decimal) and args.allow_decimal then
|
||||
has_decimal = true
|
||||
ifield.try_insert_char(".")
|
||||
elseif (event.key == keys.minus or event.key == keys.numPadSubtract) and (string.len(e.value) == 0) and args.allow_negative then
|
||||
ifield.set_value("-")
|
||||
elseif event.key == keys.left then
|
||||
ifield.nav_left()
|
||||
elseif event.key == keys.right then
|
||||
ifield.nav_right()
|
||||
elseif event.key == keys.a and event.ctrl then
|
||||
ifield.select_all()
|
||||
elseif event.key == keys.home or event.key == keys.up then
|
||||
ifield.nav_start()
|
||||
elseif event.key == keys["end"] or event.key == keys.down then
|
||||
ifield.nav_end()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value (must be a number)
|
||||
---@param val number number to show
|
||||
function e.set_value(val)
|
||||
if tonumber(val) then ifield.set_value("" .. tonumber(val)) end
|
||||
end
|
||||
|
||||
-- set minimum input value
|
||||
---@param min integer minimum allowed value
|
||||
function e.set_min(min)
|
||||
args.min = min
|
||||
e.on_unfocused()
|
||||
end
|
||||
|
||||
-- set maximum input value
|
||||
---@param max integer maximum allowed value
|
||||
function e.set_max(max)
|
||||
args.max = max
|
||||
e.on_unfocused()
|
||||
end
|
||||
|
||||
-- replace text with pasted text if its a number
|
||||
---@param text string string pasted
|
||||
function e.handle_paste(text)
|
||||
if tonumber(text) then
|
||||
ifield.set_value("" .. tonumber(text))
|
||||
else
|
||||
ifield.set_value("0")
|
||||
end
|
||||
end
|
||||
|
||||
-- handle unfocused
|
||||
function e.on_unfocused()
|
||||
local val = tonumber(e.value)
|
||||
local max = tonumber(args.max)
|
||||
local min = tonumber(args.min)
|
||||
|
||||
if type(val) == "number" then
|
||||
if args.max_int_digits or args.max_frac_digits then
|
||||
local str = e.value
|
||||
local ceil = false
|
||||
|
||||
if string.find(str, "-") then str = string.sub(e.value, 2) end
|
||||
local parts = util.strtok(str, ".")
|
||||
|
||||
if parts[1] and args.max_int_digits then
|
||||
if string.len(parts[1]) > args.max_int_digits then
|
||||
parts[1] = string.rep("9", args.max_int_digits)
|
||||
ceil = true
|
||||
end
|
||||
end
|
||||
|
||||
if args.allow_decimal and args.max_frac_digits then
|
||||
if ceil then
|
||||
parts[2] = string.rep("9", args.max_frac_digits)
|
||||
elseif parts[2] and (string.len(parts[2]) > args.max_frac_digits) then
|
||||
-- add a half of the highest precision fractional value in order to round using floor
|
||||
local scaled = math.fmod(val, 1) * (10 ^ (args.max_frac_digits))
|
||||
local value = math.floor(scaled + 0.5)
|
||||
local unscaled = value * (10 ^ (-args.max_frac_digits))
|
||||
parts[2] = string.sub(tostring(unscaled), 3) -- remove starting "0."
|
||||
end
|
||||
end
|
||||
|
||||
if parts[2] then parts[2] = "." .. parts[2] else parts[2] = "" end
|
||||
|
||||
val = tonumber((parts[1] or "") .. parts[2])
|
||||
end
|
||||
|
||||
if type(args.max) == "number" and val > max then
|
||||
e.value = "" .. max
|
||||
ifield.nav_start()
|
||||
elseif type(args.min) == "number" and val < min then
|
||||
e.value = "" .. min
|
||||
ifield.nav_start()
|
||||
else
|
||||
e.value = "" .. val
|
||||
ifield.nav_end()
|
||||
end
|
||||
else
|
||||
e.value = ""
|
||||
end
|
||||
|
||||
ifield.show()
|
||||
end
|
||||
|
||||
-- handle focus (not unfocus), enable, and redraw with show()
|
||||
e.on_focused = ifield.show
|
||||
e.on_enabled = ifield.show
|
||||
e.on_disabled = ifield.show
|
||||
e.redraw = ifield.show
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return number_field
|
||||
105
graphics/elements/form/text_field.lua
Normal file
105
graphics/elements/form/text_field.lua
Normal file
@@ -0,0 +1,105 @@
|
||||
-- Text Value Entry Graphics Element
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class text_field_args
|
||||
---@field value? string initial value
|
||||
---@field max_len? integer maximum string length
|
||||
---@field censor? string character to replace text with when printing to screen
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new text entry field
|
||||
---@param args text_field_args
|
||||
---@return graphics_element element, element_id id, function censor_ctl
|
||||
local function text_field(args)
|
||||
args.height = 1
|
||||
args.can_focus = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- set initial value
|
||||
e.value = args.value or ""
|
||||
|
||||
-- make an interactive field manager
|
||||
local ifield = core.new_ifield(e, args.max_len or e.frame.w, args.fg_bg, args.dis_fg_bg)
|
||||
|
||||
ifield.censor(args.censor)
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- only handle if on an increment or decrement arrow
|
||||
if e.enabled then
|
||||
if core.events.was_clicked(event.type) then
|
||||
e.take_focus()
|
||||
|
||||
if event.type == MOUSE_CLICK.UP and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
ifield.move_cursor(event.current.x)
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
|
||||
ifield.select_all()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == KEY_CLICK.CHAR then
|
||||
ifield.try_insert_char(event.name)
|
||||
elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
|
||||
if (event.key == keys.backspace or event.key == keys.delete) then
|
||||
ifield.backspace()
|
||||
elseif event.key == keys.left then
|
||||
ifield.nav_left()
|
||||
elseif event.key == keys.right then
|
||||
ifield.nav_right()
|
||||
elseif event.key == keys.a and event.ctrl then
|
||||
ifield.select_all()
|
||||
elseif event.key == keys.home or event.key == keys.up then
|
||||
ifield.nav_start()
|
||||
elseif event.key == keys["end"] or event.key == keys.down then
|
||||
ifield.nav_end()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val string string to set
|
||||
function e.set_value(val)
|
||||
ifield.set_value(val)
|
||||
end
|
||||
|
||||
-- replace text with pasted text
|
||||
---@param text string string to set
|
||||
function e.handle_paste(text)
|
||||
ifield.set_value(text)
|
||||
end
|
||||
|
||||
-- handle focus, enable, and redraw with show()
|
||||
e.on_focused = ifield.show
|
||||
e.on_unfocused = ifield.show
|
||||
e.on_enabled = ifield.show
|
||||
e.on_disabled = ifield.show
|
||||
e.redraw = ifield.show
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
local elem, id = e.complete()
|
||||
return elem, id, ifield.censor
|
||||
end
|
||||
|
||||
return text_field
|
||||
122
graphics/elements/indicators/alight.lua
Normal file
122
graphics/elements/indicators/alight.lua
Normal file
@@ -0,0 +1,122 @@
|
||||
-- Tri-State Alarm Indicator Light Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
---@class alarm_indicator_light
|
||||
---@field label string indicator label
|
||||
---@field c1 color color for off state
|
||||
---@field c2 color color for alarm state
|
||||
---@field c3 color color for ring-back state
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field flash? boolean whether to flash on alarm state rather than stay on
|
||||
---@field period? PERIOD flash period
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new alarm indicator light
|
||||
---@nodiscard
|
||||
---@param args alarm_indicator_light
|
||||
---@return graphics_element element, element_id id
|
||||
local function alarm_indicator_light(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
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
local flash_on = true
|
||||
|
||||
-- blit translations
|
||||
local c1 = colors.toBlit(args.c1)
|
||||
local c2 = colors.toBlit(args.c2)
|
||||
local c3 = colors.toBlit(args.c3)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 1
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
if e.value == 2 then
|
||||
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
else
|
||||
if e.value == 3 then
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
local was_off = e.value ~= 2
|
||||
|
||||
e.value = new_state
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if args.flash then
|
||||
if was_off and (new_state == 2) then
|
||||
flash_on = true
|
||||
flasher.start(flash_callback, args.period)
|
||||
elseif new_state ~= 2 then
|
||||
flash_on = false
|
||||
flasher.stop(flash_callback)
|
||||
|
||||
if new_state == 3 then
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
elseif new_state == 2 then
|
||||
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
elseif new_state == 3 then
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(e.value)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return alarm_indicator_light
|
||||
174
graphics/elements/indicators/coremap.lua
Normal file
174
graphics/elements/indicators/coremap.lua
Normal file
@@ -0,0 +1,174 @@
|
||||
-- Reactor Core View Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class core_map_args
|
||||
---@field reactor_l integer reactor length
|
||||
---@field reactor_w integer reactor width
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
|
||||
-- new core map box
|
||||
---@nodiscard
|
||||
---@param args core_map_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function core_map(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
|
||||
args.height = 18
|
||||
|
||||
-- inherit only foreground color
|
||||
args.fg_bg = core.cpair(args.parent.get_fg_bg().fgd, colors.gray)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 0
|
||||
|
||||
local alternator = true
|
||||
|
||||
local core_l = args.reactor_l - 2
|
||||
local core_w = args.reactor_w - 2
|
||||
|
||||
local shift_x = 8 - math.floor(core_l / 2)
|
||||
local shift_y = 8 - math.floor(core_w / 2)
|
||||
|
||||
local start_x = 2 + shift_x
|
||||
local start_y = 2 + shift_y
|
||||
|
||||
local inner_width = core_l
|
||||
local inner_height = core_w
|
||||
|
||||
-- create coordinate grid and frame
|
||||
local function draw_frame()
|
||||
e.w_set_fgd(colors.white)
|
||||
|
||||
for x = 0, (inner_width - 1) do
|
||||
e.w_set_cur(x + start_x, 1)
|
||||
e.w_write(util.sprintf("%X", x))
|
||||
end
|
||||
|
||||
for y = 0, (inner_height - 1) do
|
||||
e.w_set_cur(1, y + start_y)
|
||||
e.w_write(util.sprintf("%X", y))
|
||||
end
|
||||
|
||||
-- even out bottom edge
|
||||
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
|
||||
---@param t number temperature in K
|
||||
local function draw_core(t)
|
||||
local i = 1
|
||||
local back_c = "F"
|
||||
local text_c ---@type string
|
||||
|
||||
-- determine fuel assembly coloring
|
||||
if t <= 300 then
|
||||
-- gray
|
||||
text_c = "8"
|
||||
elseif t <= 350 then
|
||||
-- blue
|
||||
text_c = "3"
|
||||
elseif t < 600 then
|
||||
-- green
|
||||
text_c = "D"
|
||||
elseif t < 1000 then
|
||||
-- yellow
|
||||
text_c = "4"
|
||||
-- back_c = "8"
|
||||
elseif t < 1200 then
|
||||
-- orange
|
||||
text_c = "1"
|
||||
elseif t < 1300 then
|
||||
-- red
|
||||
text_c = "E"
|
||||
else
|
||||
-- pink
|
||||
text_c = "2"
|
||||
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
|
||||
i = i + 1
|
||||
e.w_blit("\x07", text_c, back_c)
|
||||
else
|
||||
e.w_blit("\x07", "7", "8")
|
||||
end
|
||||
|
||||
alternator = not alternator
|
||||
end
|
||||
|
||||
if inner_width % 2 == 0 then alternator = not alternator end
|
||||
end
|
||||
|
||||
-- reset alternator
|
||||
alternator = true
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param temperature number temperature in Kelvin
|
||||
function e.on_update(temperature)
|
||||
e.value = temperature
|
||||
draw_core(e.value)
|
||||
end
|
||||
|
||||
-- set temperature to display
|
||||
---@param val number degrees K
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- resize reactor dimensions
|
||||
---@param reactor_l integer reactor length (rendered in 2D top-down as width)
|
||||
---@param reactor_w integer reactor width (rendered in 2D top-down as height)
|
||||
function e.resize(reactor_l, reactor_w)
|
||||
-- enforce possible dimensions
|
||||
if reactor_l > 18 then reactor_l = 18 elseif reactor_l < 3 then reactor_l = 3 end
|
||||
if reactor_w > 18 then reactor_w = 18 elseif reactor_w < 3 then reactor_w = 3 end
|
||||
|
||||
-- update dimensions
|
||||
core_l = reactor_l - 2
|
||||
core_w = reactor_w - 2
|
||||
shift_x = 8 - math.floor(core_l / 2)
|
||||
shift_y = 8 - math.floor(core_w / 2)
|
||||
start_x = 2 + shift_x
|
||||
start_y = 2 + shift_y
|
||||
inner_width = core_l
|
||||
inner_height = core_w
|
||||
|
||||
e.window.clear()
|
||||
|
||||
-- re-draw
|
||||
draw_frame()
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- redraw both frame and core
|
||||
function e.redraw()
|
||||
draw_frame()
|
||||
draw_core(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return core_map
|
||||
103
graphics/elements/indicators/data.lua
Normal file
103
graphics/elements/indicators/data.lua
Normal file
@@ -0,0 +1,103 @@
|
||||
-- Data Indicator Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class data_indicator_args
|
||||
---@field label string indicator label
|
||||
---@field unit? string indicator unit
|
||||
---@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 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 data indicator
|
||||
---@nodiscard
|
||||
---@param args data_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function data(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")
|
||||
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = args.value
|
||||
|
||||
local value_color = e.fg_bg.fgd
|
||||
local label_len = string.len(args.label)
|
||||
local data_start = 1
|
||||
local clear_width = args.width
|
||||
|
||||
if label_len > 0 then
|
||||
data_start = data_start + (label_len + 1)
|
||||
clear_width = args.width - (label_len + 1)
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param value any new value
|
||||
function e.on_update(value)
|
||||
e.value = value
|
||||
|
||||
-- clear old data and label
|
||||
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.w_set_cur(data_start, 1)
|
||||
e.w_set_fgd(value_color)
|
||||
if args.commas then
|
||||
e.w_write(util.comma_format(data_str))
|
||||
else
|
||||
e.w_write(data_str)
|
||||
end
|
||||
|
||||
-- write label
|
||||
if args.unit ~= nil then
|
||||
if args.lu_colors ~= nil then
|
||||
e.w_set_fgd(args.lu_colors.color_b)
|
||||
end
|
||||
e.w_write(" " .. args.unit)
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val any new value
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- change the foreground color of the value, or all text if no label/unit colors provided
|
||||
---@param c color
|
||||
function e.recolor(c)
|
||||
value_color = c
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(args.label)
|
||||
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return data
|
||||
128
graphics/elements/indicators/hbar.lua
Normal file
128
graphics/elements/indicators/hbar.lua
Normal file
@@ -0,0 +1,128 @@
|
||||
-- Horizontal Bar Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class hbar_args
|
||||
---@field show_percent? boolean whether or not to show the percent
|
||||
---@field bar_fg_bg? cpair bar foreground/background colors if showing percent
|
||||
---@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 horizontal bar
|
||||
---@nodiscard
|
||||
---@param args hbar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function hbar(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 0.0
|
||||
|
||||
-- bar width is width - 5 characters for " 100%" if showing percent
|
||||
local bar_width = util.trinary(args.show_percent, e.frame.w - 5, e.frame.w)
|
||||
|
||||
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
|
||||
local bar_fgd = e.fg_bg.blit_fgd
|
||||
if args.bar_fg_bg ~= nil then
|
||||
bar_bkg = args.bar_fg_bg.blit_bkg
|
||||
bar_fgd = args.bar_fg_bg.blit_fgd
|
||||
end
|
||||
|
||||
-- handle data changes
|
||||
---@param fraction number 0.0 to 1.0
|
||||
function e.on_update(fraction)
|
||||
e.value = fraction
|
||||
|
||||
-- enforce minimum and maximum
|
||||
if fraction < 0 then
|
||||
fraction = 0.0
|
||||
elseif fraction > 1 then
|
||||
fraction = 1.0
|
||||
end
|
||||
|
||||
-- compute number of bars
|
||||
local num_bars = util.round(fraction * (bar_width * 2))
|
||||
|
||||
-- redraw bar if changed
|
||||
if num_bars ~= last_num_bars then
|
||||
last_num_bars = num_bars
|
||||
|
||||
local fgd = ""
|
||||
local bkg = ""
|
||||
local spaces = ""
|
||||
|
||||
-- fill percentage
|
||||
for _ = 1, num_bars / 2 do
|
||||
spaces = spaces .. " "
|
||||
fgd = fgd .. bar_fgd
|
||||
bkg = bkg .. bar_bkg
|
||||
end
|
||||
|
||||
-- add fractional bar if needed
|
||||
if num_bars % 2 == 1 then
|
||||
spaces = spaces .. "\x95"
|
||||
fgd = fgd .. bar_bkg
|
||||
bkg = bkg .. bar_fgd
|
||||
end
|
||||
|
||||
-- pad background
|
||||
for _ = 1, ((bar_width * 2) - num_bars) / 2 do
|
||||
spaces = spaces .. " "
|
||||
fgd = fgd .. bar_bkg
|
||||
bkg = bkg .. bar_bkg
|
||||
end
|
||||
|
||||
-- draw bar
|
||||
for y = 1, e.frame.h do
|
||||
e.w_set_cur(1, y)
|
||||
-- intentionally swapped fgd/bkg since we use spaces as fill, but they are the opposite
|
||||
e.w_blit(spaces, bkg, fgd)
|
||||
end
|
||||
end
|
||||
|
||||
-- update percentage
|
||||
if args.show_percent then
|
||||
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
|
||||
|
||||
-- change bar color
|
||||
---@param bar_fg_bg cpair new bar colors
|
||||
function e.recolor(bar_fg_bg)
|
||||
bar_bkg = bar_fg_bg.blit_bkg
|
||||
bar_fgd = bar_fg_bg.blit_fgd
|
||||
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
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
last_num_bars = -1
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return hbar
|
||||
76
graphics/elements/indicators/icon.lua
Normal file
76
graphics/elements/indicators/icon.lua
Normal file
@@ -0,0 +1,76 @@
|
||||
-- Icon Indicator Graphics Element
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class icon_sym_color
|
||||
---@field color cpair
|
||||
---@field symbol string
|
||||
|
||||
---@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 min_label_width? integer label length if omitted
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new icon indicator
|
||||
---@nodiscard
|
||||
---@param args icon_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function icon(args)
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.states) == "table", "states is a required field")
|
||||
|
||||
args.height = 1
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = args.value or 1
|
||||
|
||||
-- state blit strings
|
||||
local state_blit_cmds = {}
|
||||
for i = 1, #args.states do
|
||||
local sym_color = args.states[i] ---@type icon_sym_color
|
||||
|
||||
table.insert(state_blit_cmds, {
|
||||
text = " " .. sym_color.symbol .. " ",
|
||||
fgd = string.rep(sym_color.color.blit_fgd, 3),
|
||||
bkg = string.rep(sym_color.color.blit_bkg, 3)
|
||||
})
|
||||
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.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
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
e.w_set_cur(5, 1)
|
||||
e.w_write(args.label)
|
||||
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return icon
|
||||
104
graphics/elements/indicators/led.lua
Normal file
104
graphics/elements/indicators/led.lua
Normal file
@@ -0,0 +1,104 @@
|
||||
-- Indicator "LED" Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
---@class indicator_led_args
|
||||
---@field label string indicator label
|
||||
---@field colors cpair on/off colors (a/b respectively)
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field flash? boolean whether to flash on true rather than stay on
|
||||
---@field period? PERIOD flash period
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new indicator LED
|
||||
---@nodiscard
|
||||
---@param args indicator_led_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led(args)
|
||||
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
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
args.height = 1
|
||||
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||
|
||||
local flash_on = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = false
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
e.w_blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.w_blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
end
|
||||
|
||||
-- enable light or start flashing
|
||||
local function enable()
|
||||
if args.flash then
|
||||
flash_on = true
|
||||
flasher.start(flash_callback, args.period)
|
||||
else
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
-- disable light or stop flashing
|
||||
local function disable()
|
||||
if args.flash then
|
||||
flash_on = false
|
||||
flasher.stop(flash_callback)
|
||||
end
|
||||
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param new_state boolean indicator state
|
||||
function e.on_update(new_state)
|
||||
e.value = new_state
|
||||
if new_state then enable() else disable() end
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val boolean indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(e.value)
|
||||
if string.len(args.label) > 0 then
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return indicator_led
|
||||
113
graphics/elements/indicators/ledpair.lua
Normal file
113
graphics/elements/indicators/ledpair.lua
Normal file
@@ -0,0 +1,113 @@
|
||||
-- Indicator LED Pair Graphics Element (two LEDs provide: off, color_a, color_b)
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
---@class indicator_led_pair_args
|
||||
---@field label string indicator label
|
||||
---@field off color color for off
|
||||
---@field c1 color color for #1 on
|
||||
---@field c2 color color for #2 on
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field flash? boolean whether to flash when on rather than stay on
|
||||
---@field period? PERIOD flash period
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new dual LED indicator light
|
||||
---@nodiscard
|
||||
---@param args indicator_led_pair_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led_pair(args)
|
||||
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
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
args.height = 1
|
||||
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||
|
||||
local flash_on = true
|
||||
|
||||
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)
|
||||
|
||||
e.value = 1
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
if e.value == 2 then
|
||||
e.w_blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
elseif e.value == 3 then
|
||||
e.w_blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
local was_off = e.value <= 1
|
||||
|
||||
e.value = new_state
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if args.flash then
|
||||
if was_off and (new_state > 1) then
|
||||
flash_on = true
|
||||
flasher.start(flash_callback, args.period)
|
||||
elseif new_state <= 1 then
|
||||
flash_on = false
|
||||
flasher.stop(flash_callback)
|
||||
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
elseif new_state == 2 then
|
||||
e.w_blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
elseif new_state == 3 then
|
||||
e.w_blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(e.value)
|
||||
if string.len(args.label) > 0 then
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return indicator_led_pair
|
||||
61
graphics/elements/indicators/ledrgb.lua
Normal file
61
graphics/elements/indicators/ledrgb.lua
Normal file
@@ -0,0 +1,61 @@
|
||||
-- Indicator RGB LED Graphics Element
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class indicator_led_rgb_args
|
||||
---@field label string indicator label
|
||||
---@field colors table colors to use
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new RGB LED indicator light
|
||||
---@nodiscard
|
||||
---@param args indicator_led_rgb_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led_rgb(args)
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.colors) == "table", "colors is a required field")
|
||||
|
||||
args.height = 1
|
||||
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)
|
||||
|
||||
e.value = 1
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
e.value = new_state
|
||||
e.w_set_cur(1, 1)
|
||||
if type(args.colors[new_state]) == "number" then
|
||||
e.w_blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(e.value)
|
||||
if string.len(args.label) > 0 then
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return indicator_led_rgb
|
||||
102
graphics/elements/indicators/light.lua
Normal file
102
graphics/elements/indicators/light.lua
Normal file
@@ -0,0 +1,102 @@
|
||||
-- Indicator Light Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
---@class indicator_light_args
|
||||
---@field label string indicator label
|
||||
---@field colors cpair on/off colors (a/b respectively)
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field flash? boolean whether to flash on true rather than stay on
|
||||
---@field period? PERIOD flash period
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new indicator light
|
||||
---@nodiscard
|
||||
---@param args indicator_light_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_light(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
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
args.height = 1
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
||||
|
||||
local flash_on = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = false
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
e.w_blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.w_blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
end
|
||||
|
||||
-- enable light or start flashing
|
||||
local function enable()
|
||||
if args.flash then
|
||||
flash_on = true
|
||||
flasher.start(flash_callback, args.period)
|
||||
else
|
||||
e.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
|
||||
|
||||
-- disable light or stop flashing
|
||||
local function disable()
|
||||
if args.flash then
|
||||
flash_on = false
|
||||
flasher.stop(flash_callback)
|
||||
end
|
||||
|
||||
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
|
||||
---@param new_state boolean indicator state
|
||||
function e.on_update(new_state)
|
||||
e.value = new_state
|
||||
if new_state then enable() else disable() end
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val boolean indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(false)
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return indicator_light
|
||||
89
graphics/elements/indicators/power.lua
Normal file
89
graphics/elements/indicators/power.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 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
|
||||
|
||||
-- new power indicator
|
||||
---@nodiscard
|
||||
---@param args power_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function power(args)
|
||||
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)
|
||||
|
||||
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, 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
|
||||
-- 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.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
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return power
|
||||
92
graphics/elements/indicators/rad.lua
Normal file
92
graphics/elements/indicators/rad.lua
Normal file
@@ -0,0 +1,92 @@
|
||||
-- Radiation Indicator Graphics Element
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class rad_indicator_args
|
||||
---@field label string indicator label
|
||||
---@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? radiation_reading 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 radiation indicator
|
||||
---@nodiscard
|
||||
---@param args rad_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function rad(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")
|
||||
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = args.value or types.new_zero_radiation_reading()
|
||||
|
||||
local label_len = string.len(args.label)
|
||||
local data_start = 1
|
||||
local clear_width = args.width
|
||||
|
||||
if label_len > 0 then
|
||||
data_start = data_start + (label_len + 1)
|
||||
clear_width = args.width - (label_len + 1)
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param value any new value
|
||||
function e.on_update(value)
|
||||
e.value = value.radiation
|
||||
|
||||
-- clear old data and label
|
||||
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.w_set_cur(data_start, 1)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
if args.commas then
|
||||
e.w_write(util.comma_format(data_str))
|
||||
else
|
||||
e.w_write(data_str)
|
||||
end
|
||||
|
||||
-- write unit
|
||||
if args.lu_colors ~= nil then
|
||||
e.w_set_fgd(args.lu_colors.color_b)
|
||||
end
|
||||
e.w_write(" " .. value.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)
|
||||
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return rad
|
||||
83
graphics/elements/indicators/state.lua
Normal file
83
graphics/elements/indicators/state.lua
Normal file
@@ -0,0 +1,83 @@
|
||||
-- State (Text) Indicator Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class state_text_color
|
||||
---@field color cpair
|
||||
---@field text string
|
||||
|
||||
---@class state_indicator_args
|
||||
---@field states table state color and text table
|
||||
---@field value? integer default state, defaults to 1
|
||||
---@field min_width? integer max state text length 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 height? integer 1 if omitted, must be an odd number
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new state indicator
|
||||
---@nodiscard
|
||||
---@param args state_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function state_indicator(args)
|
||||
element.assert(type(args.states) == "table", "states is a required field")
|
||||
|
||||
if util.is_int(args.height) then
|
||||
element.assert(args.height % 2 == 1, "height should be an odd number")
|
||||
else args.height = 1 end
|
||||
|
||||
args.width = args.min_width or 1
|
||||
|
||||
-- state blit strings
|
||||
local state_blit_cmds = {}
|
||||
for i = 1, #args.states do
|
||||
local state_def = args.states[i] ---@type state_text_color
|
||||
|
||||
if string.len(state_def.text) > args.width then
|
||||
args.width = string.len(state_def.text)
|
||||
end
|
||||
|
||||
local text = util.pad(state_def.text, args.width)
|
||||
|
||||
table.insert(state_blit_cmds, {
|
||||
text = text,
|
||||
fgd = string.rep(state_def.color.blit_fgd, string.len(text)),
|
||||
bkg = string.rep(state_def.color.blit_bkg, string.len(text))
|
||||
})
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = args.value or 1
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
local blit_cmd = state_blit_cmds[e.value]
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
e.value = new_state
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return state_indicator
|
||||
111
graphics/elements/indicators/trilight.lua
Normal file
111
graphics/elements/indicators/trilight.lua
Normal file
@@ -0,0 +1,111 @@
|
||||
-- Tri-State Indicator Light Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
---@class tristate_indicator_light_args
|
||||
---@field label string indicator label
|
||||
---@field c1 color color for state 1
|
||||
---@field c2 color color for state 2
|
||||
---@field c3 color color for state 3
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field flash? boolean whether to flash on state 2 or 3 rather than stay on
|
||||
---@field period? PERIOD flash period
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new tri-state indicator light
|
||||
---@nodiscard
|
||||
---@param args tristate_indicator_light_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function tristate_indicator_light(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
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
args.height = 1
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 1
|
||||
|
||||
local flash_on = true
|
||||
|
||||
local c1 = colors.toBlit(args.c1)
|
||||
local c2 = colors.toBlit(args.c2)
|
||||
local c3 = colors.toBlit(args.c3)
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
if e.value == 2 then
|
||||
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
elseif e.value == 3 then
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
local was_off = e.value <= 1
|
||||
|
||||
e.value = new_state
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if args.flash then
|
||||
if was_off and (new_state > 1) then
|
||||
flash_on = true
|
||||
flasher.start(flash_callback, args.period)
|
||||
elseif new_state <= 1 then
|
||||
flash_on = false
|
||||
flasher.stop(flash_callback)
|
||||
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
elseif new_state == 2 then
|
||||
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
elseif new_state == 3 then
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- draw light and label
|
||||
function e.redraw()
|
||||
e.on_update(1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return tristate_indicator_light
|
||||
107
graphics/elements/indicators/vbar.lua
Normal file
107
graphics/elements/indicators/vbar.lua
Normal file
@@ -0,0 +1,107 @@
|
||||
-- Vertical Bar Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class vbar_args
|
||||
---@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 vertical bar
|
||||
---@nodiscard
|
||||
---@param args vbar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function vbar(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 0.0
|
||||
|
||||
local last_num_bars = -1
|
||||
|
||||
local fgd = string.rep(e.fg_bg.blit_fgd, e.frame.w)
|
||||
local bkg = string.rep(e.fg_bg.blit_bkg, e.frame.w)
|
||||
local spaces = util.spaces(e.frame.w)
|
||||
local one_third = 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
|
||||
function e.on_update(fraction)
|
||||
e.value = fraction
|
||||
|
||||
-- enforce minimum and maximum
|
||||
if fraction < 0 then
|
||||
fraction = 0.0
|
||||
elseif fraction > 1 then
|
||||
fraction = 1.0
|
||||
end
|
||||
|
||||
-- compute number of bars
|
||||
local num_bars = util.round(fraction * (e.frame.h * 3))
|
||||
|
||||
-- redraw only if number of bars has changed
|
||||
if num_bars ~= last_num_bars then
|
||||
last_num_bars = num_bars
|
||||
|
||||
local y = e.frame.h
|
||||
e.w_set_cur(1, y)
|
||||
|
||||
-- fill percentage
|
||||
for _ = 1, num_bars / 3 do
|
||||
e.w_blit(spaces, bkg, fgd)
|
||||
y = y - 1
|
||||
e.w_set_cur(1, y)
|
||||
end
|
||||
|
||||
-- add fractional bar if needed
|
||||
if num_bars % 3 == 1 then
|
||||
e.w_blit(one_third, bkg, fgd)
|
||||
y = y - 1
|
||||
elseif num_bars % 3 == 2 then
|
||||
e.w_blit(two_thirds, bkg, fgd)
|
||||
y = y - 1
|
||||
end
|
||||
|
||||
-- fill the rest blank
|
||||
while y > 0 do
|
||||
e.w_set_cur(1, y)
|
||||
e.w_blit(spaces, fgd, bkg)
|
||||
y = y - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the percentage value
|
||||
---@param val number 0.0 to 1.0
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
last_num_bars = -1
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- change bar color
|
||||
---@param fg_bg cpair new bar colors
|
||||
function e.recolor(fg_bg)
|
||||
fgd = string.rep(fg_bg.blit_fgd, e.frame.w)
|
||||
bkg = string.rep(fg_bg.blit_bkg, e.frame.w)
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return vbar
|
||||
343
graphics/elements/listbox.lua
Normal file
343
graphics/elements/listbox.lua
Normal file
@@ -0,0 +1,343 @@
|
||||
-- Scroll-able List Box Display Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class listbox_args
|
||||
---@field scroll_height integer height of internal scrolling container (must fit all elements vertically tiled)
|
||||
---@field item_pad? integer spacing (lines) between items in the list (default 0)
|
||||
---@field nav_fg_bg? cpair foreground/background colors for scroll arrows and bar area
|
||||
---@field nav_active? cpair active colors for bar held down or arrow held down
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 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
|
||||
|
||||
---@class listbox_item
|
||||
---@field id string|integer element ID
|
||||
---@field e graphics_element element
|
||||
---@field y integer y position
|
||||
---@field h integer element height
|
||||
|
||||
-- new listbox element
|
||||
---@nodiscard
|
||||
---@param args listbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function listbox(args)
|
||||
args.can_focus = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- create content window for child elements
|
||||
local scroll_frame = window.create(e.window, 1, 1, e.frame.w - 1, args.scroll_height, false)
|
||||
e.content_window = scroll_frame
|
||||
|
||||
-- item list and scroll management
|
||||
local list = {}
|
||||
local item_pad = args.item_pad or 0
|
||||
local scroll_offset = 0
|
||||
local content_height = 0
|
||||
local max_down_scroll = 0
|
||||
-- bar control/tracking variables
|
||||
local max_bar_height = e.frame.h - 2
|
||||
local bar_height = 0 -- full height of bar
|
||||
local bar_bounds = { 0, 0 } -- top and bottom of bar
|
||||
local bar_is_scaled = false -- if the scrollbar doesn't have a 1:1 ratio with lines
|
||||
local holding_bar = false -- bar is being held by mouse
|
||||
local bar_grip_pos = 0 -- where the bar was gripped by mouse down
|
||||
local mouse_last_y = 0 -- last reported y coordinate of drag
|
||||
|
||||
-- draw scroll bar arrows, optionally showing one of them as pressed
|
||||
---@param pressed_arrow? 1|0|-1 arrow to show as pressed (1 = scroll up, 0 = neither, -1 = scroll down)
|
||||
local function draw_arrows(pressed_arrow)
|
||||
local nav_fg_bg = args.nav_fg_bg or e.fg_bg
|
||||
local active_fg_bg = args.nav_active or nav_fg_bg
|
||||
|
||||
-- draw up/down arrows
|
||||
if pressed_arrow == 1 then
|
||||
e.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.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.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.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
|
||||
local function draw_bar()
|
||||
local offset = 2 + math.abs(scroll_offset)
|
||||
|
||||
bar_height = math.min(max_bar_height + max_down_scroll, max_bar_height)
|
||||
|
||||
if bar_height < 1 then
|
||||
bar_is_scaled = true
|
||||
-- can't do a 1:1 ratio
|
||||
-- use minimum size bar with scaled offset
|
||||
local scroll_progress = scroll_offset / max_down_scroll
|
||||
offset = 2 + math.floor(scroll_progress * (max_bar_height - 1))
|
||||
bar_height = 1
|
||||
else
|
||||
bar_is_scaled = false
|
||||
end
|
||||
|
||||
bar_bounds = { offset, (bar_height + offset) - 1 }
|
||||
|
||||
for i = 2, e.frame.h - 1 do
|
||||
if (i >= offset and i < (bar_height + offset)) and (bar_height ~= max_bar_height) then
|
||||
if args.nav_fg_bg ~= nil then
|
||||
e.w_set_bkg(args.nav_fg_bg.fgd)
|
||||
else
|
||||
e.w_set_bkg(e.fg_bg.fgd)
|
||||
end
|
||||
else
|
||||
if args.nav_fg_bg ~= nil then
|
||||
e.w_set_bkg(args.nav_fg_bg.bkg)
|
||||
else
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
end
|
||||
|
||||
e.w_set_cur(e.frame.w, i)
|
||||
if e.is_focused() then e.w_write("\x7f") else e.w_write(" ") end
|
||||
end
|
||||
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- update item y positions and move elements
|
||||
local function update_positions()
|
||||
local next_y = 1
|
||||
|
||||
scroll_frame.setVisible(false)
|
||||
scroll_frame.setBackgroundColor(e.fg_bg.bkg)
|
||||
scroll_frame.setTextColor(e.fg_bg.fgd)
|
||||
scroll_frame.clear()
|
||||
|
||||
for i = 1, #list do
|
||||
local item = list[i] ---@type listbox_item
|
||||
item.y = next_y
|
||||
next_y = next_y + item.h + item_pad
|
||||
item.e.reposition(1, item.y)
|
||||
item.e.show()
|
||||
end
|
||||
|
||||
content_height = next_y
|
||||
max_down_scroll = math.min(-1 * (content_height - (e.frame.h + 1 + item_pad)), 0)
|
||||
if scroll_offset < max_down_scroll then scroll_offset = max_down_scroll end
|
||||
|
||||
scroll_frame.reposition(1, 1 + scroll_offset)
|
||||
scroll_frame.setVisible(true)
|
||||
|
||||
-- shift mouse events
|
||||
e.mouse_window_shift.y = scroll_offset
|
||||
|
||||
draw_bar()
|
||||
end
|
||||
|
||||
-- determine where to scroll to based on a scrollbar being dragged without a 1:1 relationship
|
||||
---@param direction -1|1 negative 1 to scroll up by one, positive 1 to scroll down by one
|
||||
local function scaled_bar_scroll(direction)
|
||||
local scroll_progress = scroll_offset / max_down_scroll
|
||||
local bar_position = math.floor(scroll_progress * (max_bar_height - 1))
|
||||
|
||||
-- check what moving the scroll bar up or down would mean for the scroll progress
|
||||
scroll_progress = (bar_position + direction) / (max_bar_height - 1)
|
||||
|
||||
return math.max(math.floor(scroll_progress * max_down_scroll), max_down_scroll)
|
||||
end
|
||||
|
||||
-- scroll down the list
|
||||
local function scroll_down(scaled)
|
||||
if scroll_offset > max_down_scroll then
|
||||
if scaled then
|
||||
scroll_offset = scaled_bar_scroll(1)
|
||||
else
|
||||
scroll_offset = scroll_offset - 1
|
||||
end
|
||||
|
||||
update_positions()
|
||||
end
|
||||
end
|
||||
|
||||
-- scroll up the list
|
||||
local function scroll_up(scaled)
|
||||
if scroll_offset < 0 then
|
||||
if scaled then
|
||||
scroll_offset = scaled_bar_scroll(-1)
|
||||
else
|
||||
scroll_offset = scroll_offset + 1
|
||||
end
|
||||
|
||||
update_positions()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle a child element having been added to the list
|
||||
---@param id element_id element identifier
|
||||
---@param child graphics_element child element
|
||||
function e.on_added(id, child)
|
||||
table.insert(list, { id = id, e = child, y = 0, h = child.get_height() })
|
||||
update_positions()
|
||||
end
|
||||
|
||||
-- handle a child element having been removed from the list
|
||||
---@param id element_id element identifier
|
||||
function e.on_removed(id)
|
||||
for idx, elem in ipairs(list) do
|
||||
if elem.id == id then
|
||||
table.remove(list, idx)
|
||||
update_positions()
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle 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 == 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
|
||||
scroll_up()
|
||||
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
|
||||
scroll_down()
|
||||
if event.current.y == e.frame.h then
|
||||
draw_arrows(-1)
|
||||
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.DOWN then
|
||||
if event.current.x == e.frame.w then
|
||||
if event.current.y == 1 or event.current.y < bar_bounds[1] then
|
||||
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
|
||||
scroll_down()
|
||||
if event.current.y == e.frame.h then draw_arrows(-1) end
|
||||
else
|
||||
-- clicked on bar
|
||||
holding_bar = true
|
||||
bar_grip_pos = event.current.y - bar_bounds[1]
|
||||
mouse_last_y = event.current.y
|
||||
end
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
holding_bar = false
|
||||
draw_arrows(0)
|
||||
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
|
||||
if event.current.y < mouse_last_y then
|
||||
scroll_up(bar_is_scaled)
|
||||
elseif event.current.y > mouse_last_y then
|
||||
scroll_down(bar_is_scaled)
|
||||
end
|
||||
|
||||
mouse_last_y = event.current.y
|
||||
end
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.SCROLL_DOWN then
|
||||
scroll_down()
|
||||
elseif event.type == MOUSE_CLICK.SCROLL_UP then
|
||||
scroll_up()
|
||||
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.key == keys.up then
|
||||
scroll_up()
|
||||
elseif event.key == keys.down then
|
||||
scroll_down()
|
||||
elseif event.key == keys.home then
|
||||
scroll_offset = 0
|
||||
update_positions()
|
||||
elseif event.key == keys["end"] then
|
||||
scroll_offset = max_down_scroll
|
||||
update_positions()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
draw_arrows(0)
|
||||
draw_bar()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return listbox
|
||||
50
graphics/elements/multipane.lua
Normal file
50
graphics/elements/multipane.lua
Normal file
@@ -0,0 +1,50 @@
|
||||
-- Multi-Pane Display Graphics Element
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class multipane_args
|
||||
---@field panes table panes to swap between
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 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 multipane element
|
||||
---@nodiscard
|
||||
---@param args multipane_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function multipane(args)
|
||||
element.assert(type(args.panes) == "table", "panes is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 1
|
||||
|
||||
-- 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
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return multipane
|
||||
331
graphics/elements/pipenet.lua
Normal file
331
graphics/elements/pipenet.lua
Normal file
@@ -0,0 +1,331 @@
|
||||
-- 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
|
||||
|
||||
-- new pipe network
|
||||
---@param args pipenet_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function pipenet(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)
|
||||
|
||||
-- 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
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return pipenet
|
||||
197
graphics/elements/rectangle.lua
Normal file
197
graphics/elements/rectangle.lua
Normal file
@@ -0,0 +1,197 @@
|
||||
-- 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)
|
||||
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, 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
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return rectangle
|
||||
73
graphics/elements/textbox.lua
Normal file
73
graphics/elements/textbox.lua
Normal file
@@ -0,0 +1,73 @@
|
||||
-- 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 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)
|
||||
element.assert(type(args.text) == "string", "text is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return textbox
|
||||
95
graphics/elements/tiling.lua
Normal file
95
graphics/elements/tiling.lua
Normal file
@@ -0,0 +1,95 @@
|
||||
-- "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)
|
||||
element.assert(type(args.fill_c) == "table", "fill_c is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local 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 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
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return tiling
|
||||
254
graphics/events.lua
Normal file
254
graphics/events.lua
Normal file
@@ -0,0 +1,254 @@
|
||||
--
|
||||
-- Graphics Events and Event Handlers
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local DOUBLE_CLICK_MS = 500
|
||||
|
||||
local events = {}
|
||||
|
||||
---@enum CLICK_BUTTON
|
||||
local CLICK_BUTTON = {
|
||||
GENERIC = 0,
|
||||
LEFT_BUTTON = 1,
|
||||
RIGHT_BUTTON = 2,
|
||||
MID_BUTTON = 3
|
||||
}
|
||||
|
||||
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
|
||||
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 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) },
|
||||
-- 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
|
||||
---@nodiscard
|
||||
---@param monitor string
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return mouse_interaction
|
||||
local function _monitor_touch(monitor, x, y)
|
||||
return {
|
||||
monitor = monitor,
|
||||
button = CLICK_BUTTON.GENERIC,
|
||||
type = MOUSE_CLICK.TAP,
|
||||
initial = _coord2d(x, y),
|
||||
current = _coord2d(x, y)
|
||||
}
|
||||
end
|
||||
|
||||
-- create a new mouse button mouse interaction event
|
||||
---@nodiscard
|
||||
---@param button CLICK_BUTTON mouse button
|
||||
---@param type MOUSE_CLICK click type
|
||||
---@param x1 integer initial x
|
||||
---@param y1 integer initial y
|
||||
---@param x2 integer current x
|
||||
---@param y2 integer current y
|
||||
---@return mouse_interaction
|
||||
local function _mouse_event(button, type, x1, y1, x2, y2)
|
||||
return {
|
||||
monitor = "terminal",
|
||||
button = button,
|
||||
type = type,
|
||||
initial = _coord2d(x1, y1),
|
||||
current = _coord2d(x2, y2)
|
||||
}
|
||||
end
|
||||
|
||||
-- create a new generic mouse interaction event
|
||||
---@nodiscard
|
||||
---@param type MOUSE_CLICK
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return mouse_interaction
|
||||
function events.mouse_generic(type, x, y)
|
||||
return {
|
||||
monitor = "",
|
||||
button = CLICK_BUTTON.GENERIC,
|
||||
type = type,
|
||||
initial = _coord2d(x, y),
|
||||
current = _coord2d(x, y)
|
||||
}
|
||||
end
|
||||
|
||||
-- create a new transposed mouse interaction event using the event's monitor/button fields
|
||||
---@nodiscard
|
||||
---@param event mouse_interaction
|
||||
---@param elem_pos_x integer element's x position: new x = (event x - element x) + 1
|
||||
---@param elem_pos_y integer element's y position: new y = (event y - element y) + 1
|
||||
---@return mouse_interaction
|
||||
function events.mouse_transposed(event, elem_pos_x, elem_pos_y)
|
||||
return {
|
||||
monitor = event.monitor,
|
||||
button = event.button,
|
||||
type = event.type,
|
||||
initial = _coord2d((event.initial.x - elem_pos_x) + 1, (event.initial.y - elem_pos_y) + 1),
|
||||
current = _coord2d((event.current.x - elem_pos_x) + 1, (event.current.y - elem_pos_y) + 1)
|
||||
}
|
||||
end
|
||||
|
||||
-- check if an event qualifies as a click (tap or up)
|
||||
---@nodiscard
|
||||
---@param t 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
|
||||
---@param event_type os_event OS event to handle
|
||||
---@param opt integer|string button, scroll direction, or monitor for monitor touch
|
||||
---@param x integer x coordinate
|
||||
---@param y integer y coordinate
|
||||
---@return mouse_interaction|nil
|
||||
function events.new_mouse_event(event_type, opt, x, y)
|
||||
local h = handler
|
||||
|
||||
if event_type == "mouse_click" then
|
||||
---@cast opt 1|2|3
|
||||
|
||||
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
|
||||
|
||||
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 = 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, 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 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 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
|
||||
|
||||
return events
|
||||
82
graphics/flasher.lua
Normal file
82
graphics/flasher.lua
Normal file
@@ -0,0 +1,82 @@
|
||||
--
|
||||
-- Indicator Light Flasher
|
||||
--
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local flasher = {}
|
||||
|
||||
-- note: no additional call needs to be made in a main loop as this class automatically uses the TCD to operate
|
||||
|
||||
---@alias PERIOD integer
|
||||
local PERIOD = {
|
||||
BLINK_250_MS = 1,
|
||||
BLINK_500_MS = 2,
|
||||
BLINK_1000_MS = 3
|
||||
}
|
||||
|
||||
flasher.PERIOD = PERIOD
|
||||
|
||||
local active = false
|
||||
local registry = { {}, {}, {} } -- one registry table per period
|
||||
local callback_counter = 0
|
||||
|
||||
-- blink registered indicators<br>
|
||||
-- this assumes it is called every 250ms, it does no checking of time on its own
|
||||
local function callback_250ms()
|
||||
if active then
|
||||
for _, f in ipairs(registry[PERIOD.BLINK_250_MS]) do f() end
|
||||
|
||||
if callback_counter % 2 == 0 then
|
||||
for _, f in ipairs(registry[PERIOD.BLINK_500_MS]) do f() end
|
||||
end
|
||||
|
||||
if callback_counter % 4 == 0 then
|
||||
for _, f in ipairs(registry[PERIOD.BLINK_1000_MS]) do f() end
|
||||
end
|
||||
|
||||
callback_counter = callback_counter + 1
|
||||
|
||||
tcd.dispatch_unique(0.25, callback_250ms)
|
||||
end
|
||||
end
|
||||
|
||||
-- start/resume the flasher periodic
|
||||
function flasher.run()
|
||||
if not active then
|
||||
active = true
|
||||
callback_250ms()
|
||||
end
|
||||
end
|
||||
|
||||
-- clear all blinking indicators and stop the flasher periodic
|
||||
function flasher.clear()
|
||||
active = false
|
||||
callback_counter = 0
|
||||
registry = { {}, {}, {} }
|
||||
end
|
||||
|
||||
-- register a function to be called on the selected blink period<br>
|
||||
-- times are not strictly enforced, but all with a given period will be set at the same time
|
||||
---@param f function function to call each period
|
||||
---@param period PERIOD time period option (1, 2, or 3)
|
||||
function flasher.start(f, period)
|
||||
if type(registry[period]) == "table" then
|
||||
table.insert(registry[period], f)
|
||||
end
|
||||
end
|
||||
|
||||
-- stop a function from being called at the blink period
|
||||
---@param f function function callback registered
|
||||
function flasher.stop(f)
|
||||
for i = 1, #registry do
|
||||
for key, val in ipairs(registry[i]) do
|
||||
if val == f then
|
||||
table.remove(registry[i], key)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return flasher
|
||||
133
imgen.py
Normal file
133
imgen.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 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
|
||||
|
||||
# get size of all files in a directory
|
||||
def dir_size(path):
|
||||
total = 0
|
||||
|
||||
for (root, dirs, files) in os.walk(path):
|
||||
for f in files:
|
||||
total += os.path.getsize(root + "/" + f)
|
||||
|
||||
return total
|
||||
|
||||
# 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
|
||||
|
||||
# generate installation manifest object
|
||||
def make_manifest(size):
|
||||
manifest = {
|
||||
"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),
|
||||
"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" : [ "initenv.lua", "startup.lua", "configure.lua", "LICENSE" ],
|
||||
"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" : {
|
||||
"reactor-plc" : [ "system", "common", "graphics", "lockbox" ],
|
||||
"rtu" : [ "system", "common", "graphics", "lockbox" ],
|
||||
"supervisor" : [ "system", "common", "graphics", "lockbox" ],
|
||||
"coordinator" : [ "system", "common", "graphics", "lockbox" ],
|
||||
"pocket" : [ "system", "common", "graphics", "lockbox" ]
|
||||
},
|
||||
"sizes" : {
|
||||
# manifest file estimate
|
||||
"manifest" : size,
|
||||
# common files
|
||||
"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"),
|
||||
# platform files
|
||||
"reactor-plc" : dir_size("./reactor-plc"),
|
||||
"rtu" : dir_size("./rtu"),
|
||||
"supervisor" : dir_size("./supervisor"),
|
||||
"coordinator" : dir_size("./coordinator"),
|
||||
"pocket" : dir_size("./pocket"),
|
||||
}
|
||||
}
|
||||
|
||||
return manifest
|
||||
|
||||
# write initial manifest with placeholder size
|
||||
f = open("install_manifest.json", "w")
|
||||
json.dump(make_manifest("-----"), f)
|
||||
f.close()
|
||||
|
||||
manifest_size = os.path.getsize("install_manifest.json")
|
||||
|
||||
final_manifest = make_manifest(manifest_size)
|
||||
|
||||
# calculate file size then regenerate with embedded size
|
||||
f = open("install_manifest.json", "w")
|
||||
json.dump(final_manifest, f)
|
||||
f.close()
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "shields":
|
||||
# write all the JSON files for shields.io
|
||||
for key, version in final_manifest["versions"].items():
|
||||
f = open("./deploy/" + key + ".json", "w")
|
||||
|
||||
if version.find("alpha") >= 0:
|
||||
color = "yellow"
|
||||
elif version.find("beta") >= 0:
|
||||
color = "orange"
|
||||
else:
|
||||
color = "blue"
|
||||
|
||||
json.dump({
|
||||
"schemaVersion": 1,
|
||||
"label": key,
|
||||
"message": "" + version,
|
||||
"color": color
|
||||
}, f)
|
||||
|
||||
f.close()
|
||||
8
initenv.lua
Normal file
8
initenv.lua
Normal file
@@ -0,0 +1,8 @@
|
||||
return {
|
||||
-- initialize booted environment
|
||||
init_env = function ()
|
||||
local _require, _env = require("cc.require"), setmetatable({}, { __index = _ENV })
|
||||
require, package = _require.make(_env, "/")
|
||||
term.clear(); term.setCursorPos(1, 1)
|
||||
end
|
||||
}
|
||||
22
lockbox/LICENSE
Normal file
22
lockbox/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 James L.
|
||||
|
||||
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, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
199
lockbox/digest/md5.lua
Normal file
199
lockbox/digest/md5.lua
Normal file
@@ -0,0 +1,199 @@
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local String = require("string");
|
||||
local Math = require("math");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local SHIFT = {
|
||||
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
|
||||
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
|
||||
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
|
||||
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21};
|
||||
|
||||
local CONSTANTS = {
|
||||
0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
|
||||
0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
|
||||
0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
|
||||
0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
|
||||
0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
|
||||
0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
|
||||
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
|
||||
0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
|
||||
0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
|
||||
0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
|
||||
0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
|
||||
0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
|
||||
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
|
||||
0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
|
||||
0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
|
||||
0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391};
|
||||
|
||||
local AND = Bit.band;
|
||||
local OR = Bit.bor;
|
||||
local NOT = Bit.bnot;
|
||||
local XOR = Bit.bxor;
|
||||
local LROT = Bit.lrotate;
|
||||
local LSHIFT = Bit.lshift;
|
||||
local RSHIFT = Bit.rshift;
|
||||
|
||||
--MD5 is little-endian
|
||||
local bytes2word = function(b0, b1, b2, b3)
|
||||
local i = b3; i = LSHIFT(i, 8);
|
||||
i = OR(i, b2); i = LSHIFT(i, 8);
|
||||
i = OR(i, b1); i = LSHIFT(i, 8);
|
||||
i = OR(i, b0);
|
||||
return i;
|
||||
end
|
||||
|
||||
local word2bytes = function(word)
|
||||
local b0, b1, b2, b3;
|
||||
b0 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b3 = AND(word, 0xFF);
|
||||
return b0, b1, b2, b3;
|
||||
end
|
||||
|
||||
local dword2bytes = function(i)
|
||||
local b4, b5, b6, b7 = word2bytes(Math.floor(i / 0x100000000));
|
||||
local b0, b1, b2, b3 = word2bytes(i);
|
||||
return b0, b1, b2, b3, b4, b5, b6, b7;
|
||||
end
|
||||
|
||||
local F = function(x, y, z) return OR(AND(x, y), AND(NOT(x), z)); end
|
||||
local G = function(x, y, z) return OR(AND(x, z), AND(y, NOT(z))); end
|
||||
local H = function(x, y, z) return XOR(x, XOR(y, z)); end
|
||||
local I = function(x, y, z) return XOR(y, OR(x, NOT(z))); end
|
||||
|
||||
local MD5 = function()
|
||||
|
||||
local queue = Queue();
|
||||
|
||||
local A = 0x67452301;
|
||||
local B = 0xefcdab89;
|
||||
local C = 0x98badcfe;
|
||||
local D = 0x10325476;
|
||||
local public = {};
|
||||
|
||||
local processBlock = function()
|
||||
local a = A;
|
||||
local b = B;
|
||||
local c = C;
|
||||
local d = D;
|
||||
|
||||
local X = {};
|
||||
|
||||
for i = 1, 16 do
|
||||
X[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
|
||||
end
|
||||
|
||||
for i = 0, 63 do
|
||||
local f, g, temp;
|
||||
|
||||
if (0 <= i) and (i <= 15) then
|
||||
f = F(b, c, d);
|
||||
g = i;
|
||||
elseif (16 <= i) and (i <= 31) then
|
||||
f = G(b, c, d);
|
||||
g = (5 * i + 1) % 16;
|
||||
elseif (32 <= i) and (i <= 47) then
|
||||
f = H(b, c, d);
|
||||
g = (3 * i + 5) % 16;
|
||||
elseif (48 <= i) and (i <= 63) then
|
||||
f = I(b, c, d);
|
||||
g = (7 * i) % 16;
|
||||
end
|
||||
temp = d;
|
||||
d = c;
|
||||
c = b;
|
||||
b = b + LROT((a + f + CONSTANTS[i + 1] + X[g + 1]), SHIFT[i + 1]);
|
||||
a = temp;
|
||||
end
|
||||
|
||||
A = AND(A + a, 0xFFFFFFFF);
|
||||
B = AND(B + b, 0xFFFFFFFF);
|
||||
C = AND(C + c, 0xFFFFFFFF);
|
||||
D = AND(D + d, 0xFFFFFFFF);
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
queue.reset();
|
||||
|
||||
A = 0x67452301;
|
||||
B = 0xefcdab89;
|
||||
C = 0x98badcfe;
|
||||
D = 0x10325476;
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(bytes)
|
||||
for b in bytes do
|
||||
queue.push(b);
|
||||
if(queue.size() >= 64) then processBlock(); end
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local bits = queue.getHead() * 8;
|
||||
|
||||
queue.push(0x80);
|
||||
while ((queue.size() + 7) % 64) < 63 do
|
||||
queue.push(0x00);
|
||||
end
|
||||
|
||||
local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits);
|
||||
|
||||
queue.push(b0);
|
||||
queue.push(b1);
|
||||
queue.push(b2);
|
||||
queue.push(b3);
|
||||
queue.push(b4);
|
||||
queue.push(b5);
|
||||
queue.push(b6);
|
||||
queue.push(b7);
|
||||
|
||||
while queue.size() > 0 do
|
||||
processBlock();
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
local b0, b1, b2, b3 = word2bytes(A);
|
||||
local b4, b5, b6, b7 = word2bytes(B);
|
||||
local b8, b9, b10, b11 = word2bytes(C);
|
||||
local b12, b13, b14, b15 = word2bytes(D);
|
||||
|
||||
return {b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15};
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
local b0, b1, b2, b3 = word2bytes(A);
|
||||
local b4, b5, b6, b7 = word2bytes(B);
|
||||
local b8, b9, b10, b11 = word2bytes(C);
|
||||
local b12, b13, b14, b15 = word2bytes(D);
|
||||
|
||||
return String.format("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
|
||||
b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15);
|
||||
end
|
||||
|
||||
public.asString = function()
|
||||
local b0, b1, b2, b3 = word2bytes(A);
|
||||
local b4, b5, b6, b7 = word2bytes(B);
|
||||
local b8, b9, b10, b11 = word2bytes(C);
|
||||
local b12, b13, b14, b15 = word2bytes(D);
|
||||
|
||||
return string.pack(string.rep('B', 16),
|
||||
b0, b1, b2, b3, b4, b5, b6, b7, b8,
|
||||
b9, b10, b11, b12, b13, b14, b15
|
||||
)
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
return MD5;
|
||||
171
lockbox/digest/sha1.lua
Normal file
171
lockbox/digest/sha1.lua
Normal file
@@ -0,0 +1,171 @@
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local String = require("string");
|
||||
local Math = require("math");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local AND = Bit.band;
|
||||
local OR = Bit.bor;
|
||||
local XOR = Bit.bxor;
|
||||
local LROT = Bit.lrotate;
|
||||
local LSHIFT = Bit.lshift;
|
||||
local RSHIFT = Bit.rshift;
|
||||
|
||||
--SHA1 is big-endian
|
||||
local bytes2word = function(b0, b1, b2, b3)
|
||||
local i = b0; i = LSHIFT(i, 8);
|
||||
i = OR(i, b1); i = LSHIFT(i, 8);
|
||||
i = OR(i, b2); i = LSHIFT(i, 8);
|
||||
i = OR(i, b3);
|
||||
return i;
|
||||
end
|
||||
|
||||
local word2bytes = function(word)
|
||||
local b0, b1, b2, b3;
|
||||
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b0 = AND(word, 0xFF);
|
||||
return b0, b1, b2, b3;
|
||||
end
|
||||
|
||||
local dword2bytes = function(i)
|
||||
local b4, b5, b6, b7 = word2bytes(i);
|
||||
local b0, b1, b2, b3 = word2bytes(Math.floor(i / 0x100000000));
|
||||
return b0, b1, b2, b3, b4, b5, b6, b7;
|
||||
end
|
||||
|
||||
local F = function(x, y, z) return XOR(z, AND(x, XOR(y, z))); end
|
||||
local G = function(x, y, z) return XOR(x, XOR(y, z)); end
|
||||
local H = function(x, y, z) return OR(AND(x, OR(y, z)), AND(y, z)); end
|
||||
local I = function(x, y, z) return XOR(x, XOR(y, z)); end
|
||||
|
||||
local SHA1 = function()
|
||||
|
||||
local queue = Queue();
|
||||
|
||||
local h0 = 0x67452301;
|
||||
local h1 = 0xEFCDAB89;
|
||||
local h2 = 0x98BADCFE;
|
||||
local h3 = 0x10325476;
|
||||
local h4 = 0xC3D2E1F0;
|
||||
|
||||
local public = {};
|
||||
|
||||
local processBlock = function()
|
||||
local a = h0;
|
||||
local b = h1;
|
||||
local c = h2;
|
||||
local d = h3;
|
||||
local e = h4;
|
||||
local temp;
|
||||
local k;
|
||||
|
||||
local w = {};
|
||||
for i = 0, 15 do
|
||||
w[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
|
||||
end
|
||||
|
||||
for i = 16, 79 do
|
||||
w[i] = LROT((XOR(XOR(w[i - 3], w[i - 8]), XOR(w[i - 14], w[i - 16]))), 1);
|
||||
end
|
||||
|
||||
for i = 0, 79 do
|
||||
if (i <= 19) then
|
||||
temp = F(b, c, d);
|
||||
k = 0x5A827999;
|
||||
elseif (i <= 39) then
|
||||
temp = G(b, c, d);
|
||||
k = 0x6ED9EBA1;
|
||||
elseif (i <= 59) then
|
||||
temp = H(b, c, d);
|
||||
k = 0x8F1BBCDC;
|
||||
else
|
||||
temp = I(b, c, d);
|
||||
k = 0xCA62C1D6;
|
||||
end
|
||||
temp = LROT(a, 5) + temp + e + k + w[i];
|
||||
e = d;
|
||||
d = c;
|
||||
c = LROT(b, 30);
|
||||
b = a;
|
||||
a = temp;
|
||||
end
|
||||
|
||||
h0 = AND(h0 + a, 0xFFFFFFFF);
|
||||
h1 = AND(h1 + b, 0xFFFFFFFF);
|
||||
h2 = AND(h2 + c, 0xFFFFFFFF);
|
||||
h3 = AND(h3 + d, 0xFFFFFFFF);
|
||||
h4 = AND(h4 + e, 0xFFFFFFFF);
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
queue.reset();
|
||||
h0 = 0x67452301;
|
||||
h1 = 0xEFCDAB89;
|
||||
h2 = 0x98BADCFE;
|
||||
h3 = 0x10325476;
|
||||
h4 = 0xC3D2E1F0;
|
||||
return public;
|
||||
end
|
||||
|
||||
|
||||
public.update = function(bytes)
|
||||
for b in bytes do
|
||||
queue.push(b);
|
||||
if queue.size() >= 64 then processBlock(); end
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local bits = queue.getHead() * 8;
|
||||
|
||||
queue.push(0x80);
|
||||
while ((queue.size() + 7) % 64) < 63 do
|
||||
queue.push(0x00);
|
||||
end
|
||||
|
||||
local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits);
|
||||
|
||||
queue.push(b0);
|
||||
queue.push(b1);
|
||||
queue.push(b2);
|
||||
queue.push(b3);
|
||||
queue.push(b4);
|
||||
queue.push(b5);
|
||||
queue.push(b6);
|
||||
queue.push(b7);
|
||||
|
||||
while queue.size() > 0 do
|
||||
processBlock();
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
local b0, b1, b2, b3 = word2bytes(h0);
|
||||
local b4, b5, b6, b7 = word2bytes(h1);
|
||||
local b8, b9, b10, b11 = word2bytes(h2);
|
||||
local b12, b13, b14, b15 = word2bytes(h3);
|
||||
local b16, b17, b18, b19 = word2bytes(h4);
|
||||
|
||||
return {b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19};
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
local b0, b1, b2, b3 = word2bytes(h0);
|
||||
local b4, b5, b6, b7 = word2bytes(h1);
|
||||
local b8, b9, b10, b11 = word2bytes(h2);
|
||||
local b12, b13, b14, b15 = word2bytes(h3);
|
||||
local b16, b17, b18, b19 = word2bytes(h4);
|
||||
|
||||
return String.format("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
|
||||
b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19);
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
return SHA1;
|
||||
200
lockbox/digest/sha2_224.lua
Normal file
200
lockbox/digest/sha2_224.lua
Normal file
@@ -0,0 +1,200 @@
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local String = require("string");
|
||||
local Math = require("math");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local CONSTANTS = {
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 };
|
||||
|
||||
local fmt = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" ..
|
||||
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
|
||||
|
||||
local AND = Bit.band;
|
||||
local OR = Bit.bor;
|
||||
local NOT = Bit.bnot;
|
||||
local XOR = Bit.bxor;
|
||||
local RROT = Bit.rrotate;
|
||||
local LSHIFT = Bit.lshift;
|
||||
local RSHIFT = Bit.rshift;
|
||||
|
||||
--SHA2 is big-endian
|
||||
local bytes2word = function(b0, b1, b2, b3)
|
||||
local i = b0; i = LSHIFT(i, 8);
|
||||
i = OR(i, b1); i = LSHIFT(i, 8);
|
||||
i = OR(i, b2); i = LSHIFT(i, 8);
|
||||
i = OR(i, b3);
|
||||
return i;
|
||||
end
|
||||
|
||||
local word2bytes = function(word)
|
||||
local b0, b1, b2, b3;
|
||||
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b0 = AND(word, 0xFF);
|
||||
return b0, b1, b2, b3;
|
||||
end
|
||||
|
||||
local dword2bytes = function(i)
|
||||
local b4, b5, b6, b7 = word2bytes(i);
|
||||
local b0, b1, b2, b3 = word2bytes(Math.floor(i / 0x100000000));
|
||||
return b0, b1, b2, b3, b4, b5, b6, b7;
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
local SHA2_224 = function()
|
||||
|
||||
local queue = Queue();
|
||||
|
||||
local h0 = 0xc1059ed8;
|
||||
local h1 = 0x367cd507;
|
||||
local h2 = 0x3070dd17;
|
||||
local h3 = 0xf70e5939;
|
||||
local h4 = 0xffc00b31;
|
||||
local h5 = 0x68581511;
|
||||
local h6 = 0x64f98fa7;
|
||||
local h7 = 0xbefa4fa4;
|
||||
|
||||
local public = {};
|
||||
|
||||
local processBlock = function()
|
||||
local a = h0;
|
||||
local b = h1;
|
||||
local c = h2;
|
||||
local d = h3;
|
||||
local e = h4;
|
||||
local f = h5;
|
||||
local g = h6;
|
||||
local h = h7;
|
||||
|
||||
local w = {};
|
||||
|
||||
for i = 0, 15 do
|
||||
w[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
|
||||
end
|
||||
|
||||
for i = 16, 63 do
|
||||
local s0 = XOR(RROT(w[i - 15], 7), XOR(RROT(w[i - 15], 18), RSHIFT(w[i - 15], 3)));
|
||||
local s1 = XOR(RROT(w[i - 2], 17), XOR(RROT(w[i - 2], 19), RSHIFT(w[i - 2], 10)));
|
||||
w[i] = AND(w[i - 16] + s0 + w[i - 7] + s1, 0xFFFFFFFF);
|
||||
end
|
||||
|
||||
for i = 0, 63 do
|
||||
local s1 = XOR(RROT(e, 6), XOR(RROT(e, 11), RROT(e, 25)));
|
||||
local ch = XOR(AND(e, f), AND(NOT(e), g));
|
||||
local temp1 = h + s1 + ch + CONSTANTS[i + 1] + w[i];
|
||||
local s0 = XOR(RROT(a, 2), XOR(RROT(a, 13), RROT(a, 22)));
|
||||
local maj = XOR(AND(a, b), XOR(AND(a, c), AND(b, c)));
|
||||
local temp2 = s0 + maj;
|
||||
|
||||
h = g;
|
||||
g = f;
|
||||
f = e;
|
||||
e = d + temp1;
|
||||
d = c;
|
||||
c = b;
|
||||
b = a;
|
||||
a = temp1 + temp2;
|
||||
end
|
||||
|
||||
h0 = AND(h0 + a, 0xFFFFFFFF);
|
||||
h1 = AND(h1 + b, 0xFFFFFFFF);
|
||||
h2 = AND(h2 + c, 0xFFFFFFFF);
|
||||
h3 = AND(h3 + d, 0xFFFFFFFF);
|
||||
h4 = AND(h4 + e, 0xFFFFFFFF);
|
||||
h5 = AND(h5 + f, 0xFFFFFFFF);
|
||||
h6 = AND(h6 + g, 0xFFFFFFFF);
|
||||
h7 = AND(h7 + h, 0xFFFFFFFF);
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
queue.reset();
|
||||
|
||||
h0 = 0xc1059ed8;
|
||||
h1 = 0x367cd507;
|
||||
h2 = 0x3070dd17;
|
||||
h3 = 0xf70e5939;
|
||||
h4 = 0xffc00b31;
|
||||
h5 = 0x68581511;
|
||||
h6 = 0x64f98fa7;
|
||||
h7 = 0xbefa4fa4;
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(bytes)
|
||||
for b in bytes do
|
||||
queue.push(b);
|
||||
if queue.size() >= 64 then processBlock(); end
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local bits = queue.getHead() * 8;
|
||||
|
||||
queue.push(0x80);
|
||||
while ((queue.size() + 7) % 64) < 63 do
|
||||
queue.push(0x00);
|
||||
end
|
||||
|
||||
local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits);
|
||||
|
||||
queue.push(b0);
|
||||
queue.push(b1);
|
||||
queue.push(b2);
|
||||
queue.push(b3);
|
||||
queue.push(b4);
|
||||
queue.push(b5);
|
||||
queue.push(b6);
|
||||
queue.push(b7);
|
||||
|
||||
while queue.size() > 0 do
|
||||
processBlock();
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
local b0, b1, b2, b3 = word2bytes(h0);
|
||||
local b4, b5, b6, b7 = word2bytes(h1);
|
||||
local b8, b9, b10, b11 = word2bytes(h2);
|
||||
local b12, b13, b14, b15 = word2bytes(h3);
|
||||
local b16, b17, b18, b19 = word2bytes(h4);
|
||||
local b20, b21, b22, b23 = word2bytes(h5);
|
||||
local b24, b25, b26, b27 = word2bytes(h6);
|
||||
|
||||
return { b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
|
||||
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27};
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
local b0, b1, b2, b3 = word2bytes(h0);
|
||||
local b4, b5, b6, b7 = word2bytes(h1);
|
||||
local b8, b9, b10, b11 = word2bytes(h2);
|
||||
local b12, b13, b14, b15 = word2bytes(h3);
|
||||
local b16, b17, b18, b19 = word2bytes(h4);
|
||||
local b20, b21, b22, b23 = word2bytes(h5);
|
||||
local b24, b25, b26, b27 = word2bytes(h6);
|
||||
|
||||
return String.format(fmt, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
|
||||
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
return SHA2_224;
|
||||
|
||||
203
lockbox/digest/sha2_256.lua
Normal file
203
lockbox/digest/sha2_256.lua
Normal file
@@ -0,0 +1,203 @@
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local String = require("string");
|
||||
local Math = require("math");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local CONSTANTS = {
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 };
|
||||
|
||||
local fmt = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" ..
|
||||
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
|
||||
|
||||
local AND = Bit.band;
|
||||
local OR = Bit.bor;
|
||||
local NOT = Bit.bnot;
|
||||
local XOR = Bit.bxor;
|
||||
local RROT = Bit.rrotate;
|
||||
local LSHIFT = Bit.lshift;
|
||||
local RSHIFT = Bit.rshift;
|
||||
|
||||
--SHA2 is big-endian
|
||||
local bytes2word = function(b0, b1, b2, b3)
|
||||
local i = b0; i = LSHIFT(i, 8);
|
||||
i = OR(i, b1); i = LSHIFT(i, 8);
|
||||
i = OR(i, b2); i = LSHIFT(i, 8);
|
||||
i = OR(i, b3);
|
||||
return i;
|
||||
end
|
||||
|
||||
local word2bytes = function(word)
|
||||
local b0, b1, b2, b3;
|
||||
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b0 = AND(word, 0xFF);
|
||||
return b0, b1, b2, b3;
|
||||
end
|
||||
|
||||
local dword2bytes = function(i)
|
||||
local b4, b5, b6, b7 = word2bytes(i);
|
||||
local b0, b1, b2, b3 = word2bytes(Math.floor(i / 0x100000000));
|
||||
return b0, b1, b2, b3, b4, b5, b6, b7;
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
local SHA2_256 = function()
|
||||
|
||||
local queue = Queue();
|
||||
|
||||
local h0 = 0x6a09e667;
|
||||
local h1 = 0xbb67ae85;
|
||||
local h2 = 0x3c6ef372;
|
||||
local h3 = 0xa54ff53a;
|
||||
local h4 = 0x510e527f;
|
||||
local h5 = 0x9b05688c;
|
||||
local h6 = 0x1f83d9ab;
|
||||
local h7 = 0x5be0cd19;
|
||||
|
||||
local public = {};
|
||||
|
||||
local processBlock = function()
|
||||
local a = h0;
|
||||
local b = h1;
|
||||
local c = h2;
|
||||
local d = h3;
|
||||
local e = h4;
|
||||
local f = h5;
|
||||
local g = h6;
|
||||
local h = h7;
|
||||
|
||||
local w = {};
|
||||
|
||||
for i = 0, 15 do
|
||||
w[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
|
||||
end
|
||||
|
||||
for i = 16, 63 do
|
||||
local s0 = XOR(RROT(w[i - 15], 7), XOR(RROT(w[i - 15], 18), RSHIFT(w[i - 15], 3)));
|
||||
local s1 = XOR(RROT(w[i - 2], 17), XOR(RROT(w[i - 2], 19), RSHIFT(w[i - 2], 10)));
|
||||
w[i] = AND(w[i - 16] + s0 + w[i - 7] + s1, 0xFFFFFFFF);
|
||||
end
|
||||
|
||||
for i = 0, 63 do
|
||||
local s1 = XOR(RROT(e, 6), XOR(RROT(e, 11), RROT(e, 25)));
|
||||
local ch = XOR(AND(e, f), AND(NOT(e), g));
|
||||
local temp1 = h + s1 + ch + CONSTANTS[i + 1] + w[i];
|
||||
local s0 = XOR(RROT(a, 2), XOR(RROT(a, 13), RROT(a, 22)));
|
||||
local maj = XOR(AND(a, b), XOR(AND(a, c), AND(b, c)));
|
||||
local temp2 = s0 + maj;
|
||||
|
||||
h = g;
|
||||
g = f;
|
||||
f = e;
|
||||
e = d + temp1;
|
||||
d = c;
|
||||
c = b;
|
||||
b = a;
|
||||
a = temp1 + temp2;
|
||||
end
|
||||
|
||||
h0 = AND(h0 + a, 0xFFFFFFFF);
|
||||
h1 = AND(h1 + b, 0xFFFFFFFF);
|
||||
h2 = AND(h2 + c, 0xFFFFFFFF);
|
||||
h3 = AND(h3 + d, 0xFFFFFFFF);
|
||||
h4 = AND(h4 + e, 0xFFFFFFFF);
|
||||
h5 = AND(h5 + f, 0xFFFFFFFF);
|
||||
h6 = AND(h6 + g, 0xFFFFFFFF);
|
||||
h7 = AND(h7 + h, 0xFFFFFFFF);
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
queue.reset();
|
||||
|
||||
h0 = 0x6a09e667;
|
||||
h1 = 0xbb67ae85;
|
||||
h2 = 0x3c6ef372;
|
||||
h3 = 0xa54ff53a;
|
||||
h4 = 0x510e527f;
|
||||
h5 = 0x9b05688c;
|
||||
h6 = 0x1f83d9ab;
|
||||
h7 = 0x5be0cd19;
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(bytes)
|
||||
for b in bytes do
|
||||
queue.push(b);
|
||||
if queue.size() >= 64 then processBlock(); end
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local bits = queue.getHead() * 8;
|
||||
|
||||
queue.push(0x80);
|
||||
while ((queue.size() + 7) % 64) < 63 do
|
||||
queue.push(0x00);
|
||||
end
|
||||
|
||||
local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits);
|
||||
|
||||
queue.push(b0);
|
||||
queue.push(b1);
|
||||
queue.push(b2);
|
||||
queue.push(b3);
|
||||
queue.push(b4);
|
||||
queue.push(b5);
|
||||
queue.push(b6);
|
||||
queue.push(b7);
|
||||
|
||||
while queue.size() > 0 do
|
||||
processBlock();
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
local b0, b1, b2, b3 = word2bytes(h0);
|
||||
local b4, b5, b6, b7 = word2bytes(h1);
|
||||
local b8, b9, b10, b11 = word2bytes(h2);
|
||||
local b12, b13, b14, b15 = word2bytes(h3);
|
||||
local b16, b17, b18, b19 = word2bytes(h4);
|
||||
local b20, b21, b22, b23 = word2bytes(h5);
|
||||
local b24, b25, b26, b27 = word2bytes(h6);
|
||||
local b28, b29, b30, b31 = word2bytes(h7);
|
||||
|
||||
|
||||
return { b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
|
||||
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29, b30, b31};
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
local b0, b1, b2, b3 = word2bytes(h0);
|
||||
local b4, b5, b6, b7 = word2bytes(h1);
|
||||
local b8, b9, b10, b11 = word2bytes(h2);
|
||||
local b12, b13, b14, b15 = word2bytes(h3);
|
||||
local b16, b17, b18, b19 = word2bytes(h4);
|
||||
local b20, b21, b22, b23 = word2bytes(h5);
|
||||
local b24, b25, b26, b27 = word2bytes(h6);
|
||||
local b28, b29, b30, b31 = word2bytes(h7);
|
||||
|
||||
return String.format(fmt, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
|
||||
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29, b30, b31);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
return SHA2_256;
|
||||
|
||||
6
lockbox/init.lua
Normal file
6
lockbox/init.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
local Lockbox = {}
|
||||
|
||||
-- cc-mek-scada lockbox version
|
||||
Lockbox.version = "1.1"
|
||||
|
||||
return Lockbox
|
||||
114
lockbox/kdf/pbkdf2.lua
Normal file
114
lockbox/kdf/pbkdf2.lua
Normal file
@@ -0,0 +1,114 @@
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local Array = require("lockbox.util.array");
|
||||
local Stream = require("lockbox.util.stream");
|
||||
local Math = require("math");
|
||||
|
||||
local AND = Bit.band;
|
||||
local RSHIFT = Bit.rshift;
|
||||
|
||||
local word2bytes = function(word)
|
||||
local b0, b1, b2, b3;
|
||||
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b0 = AND(word, 0xFF);
|
||||
return b0, b1, b2, b3;
|
||||
end
|
||||
|
||||
local PBKDF2 = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local blockLen = 16;
|
||||
local dKeyLen = 256;
|
||||
local iterations = 4096;
|
||||
|
||||
local salt;
|
||||
local password;
|
||||
|
||||
|
||||
local PRF;
|
||||
|
||||
local dKey;
|
||||
|
||||
|
||||
public.setBlockLen = function(len)
|
||||
blockLen = len;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setDKeyLen = function(len)
|
||||
dKeyLen = len
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setIterations = function(iter)
|
||||
iterations = iter;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setSalt = function(saltBytes)
|
||||
salt = saltBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPassword = function(passwordBytes)
|
||||
password = passwordBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPRF = function(prf)
|
||||
PRF = prf;
|
||||
return public;
|
||||
end
|
||||
|
||||
local buildBlock = function(i)
|
||||
local b0, b1, b2, b3 = word2bytes(i);
|
||||
local ii = {b0, b1, b2, b3};
|
||||
local s = Array.concat(salt, ii);
|
||||
|
||||
local out = {};
|
||||
|
||||
PRF.setKey(password);
|
||||
for c = 1, iterations do
|
||||
PRF.init()
|
||||
.update(Stream.fromArray(s));
|
||||
|
||||
s = PRF.finish().asBytes();
|
||||
if(c > 1) then
|
||||
out = Array.XOR(out, s);
|
||||
else
|
||||
out = s;
|
||||
end
|
||||
end
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local blocks = Math.ceil(dKeyLen / blockLen);
|
||||
|
||||
dKey = {};
|
||||
|
||||
for b = 1, blocks do
|
||||
local block = buildBlock(b);
|
||||
dKey = Array.concat(dKey, block);
|
||||
end
|
||||
|
||||
if(Array.size(dKey) > dKeyLen) then dKey = Array.truncate(dKey, dKeyLen); end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return dKey;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Array.toHex(dKey);
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
return PBKDF2;
|
||||
85
lockbox/mac/hmac.lua
Normal file
85
lockbox/mac/hmac.lua
Normal file
@@ -0,0 +1,85 @@
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local Stream = require("lockbox.util.stream");
|
||||
local Array = require("lockbox.util.array");
|
||||
|
||||
local XOR = Bit.bxor;
|
||||
|
||||
local HMAC = function()
|
||||
|
||||
local public = {};
|
||||
local blockSize = 64;
|
||||
local Digest;
|
||||
local outerPadding = {};
|
||||
local innerPadding = {}
|
||||
local digest;
|
||||
|
||||
public.setBlockSize = function(bytes)
|
||||
blockSize = bytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setDigest = function(digestModule)
|
||||
Digest = digestModule;
|
||||
digest = Digest();
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setKey = function(key)
|
||||
local keyStream;
|
||||
|
||||
if(Array.size(key) > blockSize) then
|
||||
keyStream = Stream.fromArray(Digest()
|
||||
.update(Stream.fromArray(key))
|
||||
.finish()
|
||||
.asBytes());
|
||||
else
|
||||
keyStream = Stream.fromArray(key);
|
||||
end
|
||||
|
||||
outerPadding = {};
|
||||
innerPadding = {};
|
||||
|
||||
for i = 1, blockSize do
|
||||
local byte = keyStream();
|
||||
if byte == nil then byte = 0x00; end
|
||||
outerPadding[i] = XOR(0x5C, byte);
|
||||
innerPadding[i] = XOR(0x36, byte);
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
digest.init()
|
||||
.update(Stream.fromArray(innerPadding));
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
digest.update(messageStream);
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local inner = digest.finish().asBytes();
|
||||
digest.init()
|
||||
.update(Stream.fromArray(outerPadding))
|
||||
.update(Stream.fromArray(inner))
|
||||
.finish();
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return digest.asBytes();
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return digest.asHex();
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
return HMAC;
|
||||
210
lockbox/util/array.lua
Normal file
210
lockbox/util/array.lua
Normal file
@@ -0,0 +1,210 @@
|
||||
local String = require("string");
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local XOR = Bit.bxor;
|
||||
|
||||
local Array = {};
|
||||
|
||||
Array.size = function(array)
|
||||
return #array;
|
||||
end
|
||||
|
||||
Array.fromString = function(string)
|
||||
local bytes = {};
|
||||
|
||||
local i = 1;
|
||||
local byte = String.byte(string, i);
|
||||
while byte ~= nil do
|
||||
bytes[i] = byte;
|
||||
i = i + 1;
|
||||
byte = String.byte(string, i);
|
||||
end
|
||||
|
||||
return bytes;
|
||||
|
||||
end
|
||||
|
||||
Array.toString = function(bytes)
|
||||
local chars = {};
|
||||
local i = 1;
|
||||
|
||||
local byte = bytes[i];
|
||||
while byte ~= nil do
|
||||
chars[i] = String.char(byte);
|
||||
i = i + 1;
|
||||
byte = bytes[i];
|
||||
end
|
||||
|
||||
return table.concat(chars, "");
|
||||
end
|
||||
|
||||
Array.fromStream = function(stream)
|
||||
local array = {};
|
||||
local i = 1;
|
||||
|
||||
local byte = stream();
|
||||
while byte ~= nil do
|
||||
array[i] = byte;
|
||||
i = i + 1;
|
||||
byte = stream();
|
||||
end
|
||||
|
||||
return array;
|
||||
end
|
||||
|
||||
Array.readFromQueue = function(queue, size)
|
||||
local array = {};
|
||||
|
||||
for i = 1, size do
|
||||
array[i] = queue.pop();
|
||||
end
|
||||
|
||||
return array;
|
||||
end
|
||||
|
||||
Array.writeToQueue = function(queue, array)
|
||||
local size = Array.size(array);
|
||||
|
||||
for i = 1, size do
|
||||
queue.push(array[i]);
|
||||
end
|
||||
end
|
||||
|
||||
Array.toStream = function(array)
|
||||
local queue = Queue();
|
||||
local i = 1;
|
||||
|
||||
local byte = array[i];
|
||||
while byte ~= nil do
|
||||
queue.push(byte);
|
||||
i = i + 1;
|
||||
byte = array[i];
|
||||
end
|
||||
|
||||
return queue.pop;
|
||||
end
|
||||
|
||||
|
||||
local fromHexTable = {};
|
||||
for i = 0, 255 do
|
||||
fromHexTable[String.format("%02X", i)] = i;
|
||||
fromHexTable[String.format("%02x", i)] = i;
|
||||
end
|
||||
|
||||
Array.fromHex = function(hex)
|
||||
local array = {};
|
||||
|
||||
for i = 1, String.len(hex) / 2 do
|
||||
local h = String.sub(hex, i * 2 - 1, i * 2);
|
||||
array[i] = fromHexTable[h];
|
||||
end
|
||||
|
||||
return array;
|
||||
end
|
||||
|
||||
|
||||
local toHexTable = {};
|
||||
for i = 0, 255 do
|
||||
toHexTable[i] = String.format("%02X", i);
|
||||
end
|
||||
|
||||
Array.toHex = function(array)
|
||||
local hex = {};
|
||||
local i = 1;
|
||||
|
||||
local byte = array[i];
|
||||
while byte ~= nil do
|
||||
hex[i] = toHexTable[byte];
|
||||
i = i + 1;
|
||||
byte = array[i];
|
||||
end
|
||||
|
||||
return table.concat(hex, "");
|
||||
|
||||
end
|
||||
|
||||
Array.concat = function(a, b)
|
||||
local concat = {};
|
||||
local out = 1;
|
||||
|
||||
local i = 1;
|
||||
local byte = a[i];
|
||||
while byte ~= nil do
|
||||
concat[out] = byte;
|
||||
i = i + 1;
|
||||
out = out + 1;
|
||||
byte = a[i];
|
||||
end
|
||||
|
||||
i = 1;
|
||||
byte = b[i];
|
||||
while byte ~= nil do
|
||||
concat[out] = byte;
|
||||
i = i + 1;
|
||||
out = out + 1;
|
||||
byte = b[i];
|
||||
end
|
||||
|
||||
return concat;
|
||||
end
|
||||
|
||||
Array.truncate = function(a, newSize)
|
||||
local x = {};
|
||||
|
||||
for i = 1, newSize do
|
||||
x[i] = a[i];
|
||||
end
|
||||
|
||||
return x;
|
||||
end
|
||||
|
||||
Array.XOR = function(a, b)
|
||||
local x = {};
|
||||
|
||||
for k, v in pairs(a) do
|
||||
x[k] = XOR(v, b[k]);
|
||||
end
|
||||
|
||||
return x;
|
||||
end
|
||||
|
||||
Array.substitute = function(input, sbox)
|
||||
local out = {};
|
||||
|
||||
for k, v in pairs(input) do
|
||||
out[k] = sbox[v];
|
||||
end
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
Array.permute = function(input, pbox)
|
||||
local out = {};
|
||||
|
||||
for k, v in pairs(pbox) do
|
||||
out[k] = input[v];
|
||||
end
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
Array.copy = function(input)
|
||||
local out = {};
|
||||
|
||||
for k, v in pairs(input) do
|
||||
out[k] = v;
|
||||
end
|
||||
return out;
|
||||
end
|
||||
|
||||
Array.slice = function(input, start, stop)
|
||||
local out = {};
|
||||
|
||||
for i = start, stop do
|
||||
out[i - start + 1] = input[i];
|
||||
end
|
||||
return out;
|
||||
end
|
||||
|
||||
return Array;
|
||||
19
lockbox/util/bit.lua
Normal file
19
lockbox/util/bit.lua
Normal file
@@ -0,0 +1,19 @@
|
||||
-- modified (simplified) for ComputerCraft
|
||||
|
||||
local ok, e = nil, nil
|
||||
|
||||
if not ok then
|
||||
ok, e = pcall(require, "bit32") -- Lua 5.2
|
||||
end
|
||||
|
||||
if not ok then
|
||||
ok, e = pcall(require, "bit")
|
||||
end
|
||||
|
||||
if not ok then
|
||||
error("no bitwise support found", 2)
|
||||
end
|
||||
|
||||
assert(type(e) == "table", "invalid bit module")
|
||||
|
||||
return e
|
||||
47
lockbox/util/queue.lua
Normal file
47
lockbox/util/queue.lua
Normal file
@@ -0,0 +1,47 @@
|
||||
local Queue = function()
|
||||
local queue = {};
|
||||
local tail = 0;
|
||||
local head = 0;
|
||||
|
||||
local public = {};
|
||||
|
||||
public.push = function(obj)
|
||||
queue[head] = obj;
|
||||
head = head + 1;
|
||||
return;
|
||||
end
|
||||
|
||||
public.pop = function()
|
||||
if tail < head
|
||||
then
|
||||
local obj = queue[tail];
|
||||
queue[tail] = nil;
|
||||
tail = tail + 1;
|
||||
return obj;
|
||||
else
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
public.size = function()
|
||||
return head - tail;
|
||||
end
|
||||
|
||||
public.getHead = function()
|
||||
return head;
|
||||
end
|
||||
|
||||
public.getTail = function()
|
||||
return tail;
|
||||
end
|
||||
|
||||
public.reset = function()
|
||||
queue = {};
|
||||
head = 0;
|
||||
tail = 0;
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
return Queue;
|
||||
99
lockbox/util/stream.lua
Normal file
99
lockbox/util/stream.lua
Normal file
@@ -0,0 +1,99 @@
|
||||
local Queue = require("lockbox.util.queue");
|
||||
local String = require("string");
|
||||
|
||||
local Stream = {};
|
||||
|
||||
|
||||
Stream.fromString = function(string)
|
||||
local i = 0;
|
||||
return function()
|
||||
i = i + 1;
|
||||
return String.byte(string, i);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Stream.toString = function(stream)
|
||||
local array = {};
|
||||
local i = 1;
|
||||
|
||||
local byte = stream();
|
||||
while byte ~= nil do
|
||||
array[i] = String.char(byte);
|
||||
i = i + 1;
|
||||
byte = stream();
|
||||
end
|
||||
|
||||
return table.concat(array);
|
||||
end
|
||||
|
||||
|
||||
Stream.fromArray = function(array)
|
||||
local queue = Queue();
|
||||
local i = 1;
|
||||
|
||||
local byte = array[i];
|
||||
while byte ~= nil do
|
||||
queue.push(byte);
|
||||
i = i + 1;
|
||||
byte = array[i];
|
||||
end
|
||||
|
||||
return queue.pop;
|
||||
end
|
||||
|
||||
|
||||
Stream.toArray = function(stream)
|
||||
local array = {};
|
||||
local i = 1;
|
||||
|
||||
local byte = stream();
|
||||
while byte ~= nil do
|
||||
array[i] = byte;
|
||||
i = i + 1;
|
||||
byte = stream();
|
||||
end
|
||||
|
||||
return array;
|
||||
end
|
||||
|
||||
|
||||
local fromHexTable = {};
|
||||
for i = 0, 255 do
|
||||
fromHexTable[String.format("%02X", i)] = i;
|
||||
fromHexTable[String.format("%02x", i)] = i;
|
||||
end
|
||||
|
||||
Stream.fromHex = function(hex)
|
||||
local queue = Queue();
|
||||
|
||||
for i = 1, String.len(hex) / 2 do
|
||||
local h = String.sub(hex, i * 2 - 1, i * 2);
|
||||
queue.push(fromHexTable[h]);
|
||||
end
|
||||
|
||||
return queue.pop;
|
||||
end
|
||||
|
||||
|
||||
|
||||
local toHexTable = {};
|
||||
for i = 0, 255 do
|
||||
toHexTable[i] = String.format("%02X", i);
|
||||
end
|
||||
|
||||
Stream.toHex = function(stream)
|
||||
local hex = {};
|
||||
local i = 1;
|
||||
|
||||
local byte = stream();
|
||||
while byte ~= nil do
|
||||
hex[i] = toHexTable[byte];
|
||||
i = i + 1;
|
||||
byte = stream();
|
||||
end
|
||||
|
||||
return table.concat(hex);
|
||||
end
|
||||
|
||||
return Stream;
|
||||
@@ -1,135 +0,0 @@
|
||||
-- mekanism reactor controller
|
||||
-- monitors and regulates mekanism reactors
|
||||
|
||||
os.loadAPI("reactor.lua")
|
||||
os.loadAPI("defs.lua")
|
||||
os.loadAPI("log.lua")
|
||||
os.loadAPI("render.lua")
|
||||
os.loadAPI("server.lua")
|
||||
os.loadAPI("regulator.lua")
|
||||
|
||||
-- constants, aliases, properties
|
||||
local header = "MEKANISM REACTOR CONTROLLER - v" .. defs.CTRL_VERSION
|
||||
local monitor_0 = peripheral.wrap(defs.MONITOR_0)
|
||||
local monitor_1 = peripheral.wrap(defs.MONITOR_1)
|
||||
local monitor_2 = peripheral.wrap(defs.MONITOR_2)
|
||||
local monitor_3 = peripheral.wrap(defs.MONITOR_3)
|
||||
|
||||
monitor_0.setBackgroundColor(colors.black)
|
||||
monitor_0.setTextColor(colors.white)
|
||||
monitor_0.clear()
|
||||
|
||||
monitor_1.setBackgroundColor(colors.black)
|
||||
monitor_1.setTextColor(colors.white)
|
||||
monitor_1.clear()
|
||||
|
||||
monitor_2.setBackgroundColor(colors.black)
|
||||
monitor_2.setTextColor(colors.white)
|
||||
monitor_2.clear()
|
||||
|
||||
log.init(monitor_3)
|
||||
|
||||
local main_w, main_h = monitor_0.getSize()
|
||||
local view = window.create(monitor_0, 1, 1, main_w, main_h)
|
||||
view.setBackgroundColor(colors.black)
|
||||
view.clear()
|
||||
|
||||
local stat_w, stat_h = monitor_1.getSize()
|
||||
local stat_view = window.create(monitor_1, 1, 1, stat_w, stat_h)
|
||||
stat_view.setBackgroundColor(colors.black)
|
||||
stat_view.clear()
|
||||
|
||||
local reactors = {
|
||||
reactor.create(1, view, stat_view, 62, 3, 63, 2),
|
||||
reactor.create(2, view, stat_view, 42, 3, 43, 2),
|
||||
reactor.create(3, view, stat_view, 22, 3, 23, 2),
|
||||
reactor.create(4, view, stat_view, 2, 3, 3, 2)
|
||||
}
|
||||
print("[debug] reactor tables created")
|
||||
|
||||
server.init(reactors)
|
||||
print("[debug] modem server started")
|
||||
|
||||
regulator.init(reactors)
|
||||
print("[debug] regulator started")
|
||||
|
||||
-- header
|
||||
view.setBackgroundColor(colors.white)
|
||||
view.setTextColor(colors.black)
|
||||
view.setCursorPos(1, 1)
|
||||
local header_pad_x = (main_w - string.len(header)) / 2
|
||||
view.write(string.rep(" ", header_pad_x) .. header .. string.rep(" ", header_pad_x))
|
||||
|
||||
-- inital draw of each reactor
|
||||
for key, rctr in pairs(reactors) do
|
||||
render.draw_reactor_system(rctr)
|
||||
render.draw_reactor_status(rctr)
|
||||
end
|
||||
|
||||
-- inital draw of clock
|
||||
monitor_2.setTextScale(2)
|
||||
monitor_2.setCursorPos(1, 1)
|
||||
monitor_2.write(os.date("%Y/%m/%d %H:%M:%S"))
|
||||
|
||||
local clock_update_timer = os.startTimer(1)
|
||||
|
||||
while true do
|
||||
event, param1, param2, param3, param4, param5 = os.pullEvent()
|
||||
|
||||
if event == "redstone" then
|
||||
-- redstone state change
|
||||
regulator.handle_redstone()
|
||||
elseif event == "modem_message" then
|
||||
-- received signal router packet
|
||||
packet = {
|
||||
side = param1,
|
||||
sender = param2,
|
||||
reply = param3,
|
||||
message = param4,
|
||||
distance = param5
|
||||
}
|
||||
|
||||
server.handle_message(packet, reactors)
|
||||
elseif event == "monitor_touch" then
|
||||
if param1 == "monitor_5" then
|
||||
local tap_x = param2
|
||||
local tap_y = param3
|
||||
|
||||
for key, rctr in pairs(reactors) do
|
||||
if tap_x >= rctr.render.stat_x and tap_x <= (rctr.render.stat_x + 15) then
|
||||
local old_val = rctr.waste_production
|
||||
-- width in range
|
||||
if tap_y == (rctr.render.stat_y + 12) then
|
||||
rctr.waste_production = "plutonium"
|
||||
elseif tap_y == (rctr.render.stat_y + 14) then
|
||||
rctr.waste_production = "polonium"
|
||||
elseif tap_y == (rctr.render.stat_y + 16) then
|
||||
rctr.waste_production = "antimatter"
|
||||
end
|
||||
|
||||
-- notify reactor of changes
|
||||
if old_val ~= rctr.waste_production then
|
||||
server.send(rctr.id, rctr.waste_production)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif event == "timer" then
|
||||
-- update the clock about every second
|
||||
monitor_2.setCursorPos(1, 1)
|
||||
monitor_2.write(os.date("%Y/%m/%d %H:%M:%S"))
|
||||
clock_update_timer = os.startTimer(1)
|
||||
|
||||
-- send keep-alive
|
||||
server.broadcast(1)
|
||||
end
|
||||
|
||||
-- update reactor display
|
||||
for key, rctr in pairs(reactors) do
|
||||
render.draw_reactor_system(rctr)
|
||||
render.draw_reactor_status(rctr)
|
||||
end
|
||||
|
||||
-- update system status monitor
|
||||
render.update_system_monitor(monitor_2, regulator.is_scrammed(), reactors)
|
||||
end
|
||||
@@ -1,23 +0,0 @@
|
||||
-- configuration definitions
|
||||
|
||||
CTRL_VERSION = "0.7"
|
||||
|
||||
-- monitors
|
||||
MONITOR_0 = "monitor_6"
|
||||
MONITOR_1 = "monitor_5"
|
||||
MONITOR_2 = "monitor_7"
|
||||
MONITOR_3 = "monitor_8"
|
||||
|
||||
-- modem server
|
||||
LISTEN_PORT = 1000
|
||||
|
||||
-- regulator (should match the number of reactors present)
|
||||
BUNDLE_DEF = { colors.red, colors.orange, colors.yellow, colors.lime }
|
||||
|
||||
-- stats calculation
|
||||
REACTOR_MB_T = 39
|
||||
TURBINE_MRF_T = 3.114
|
||||
PLUTONIUM_PER_WASTE = 0.1
|
||||
POLONIUM_PER_WASTE = 0.1
|
||||
SPENT_PER_BYPRODUCT = 1
|
||||
ANTIMATTER_PER_POLONIUM = 0.001
|
||||
52
main/log.lua
52
main/log.lua
@@ -1,52 +0,0 @@
|
||||
os.loadAPI("defs.lua")
|
||||
|
||||
local out, out_w, out_h
|
||||
local output_full = false
|
||||
|
||||
-- initialize the logger to the given monitor
|
||||
-- monitor: monitor to write to (in addition to calling print())
|
||||
function init(monitor)
|
||||
out = monitor
|
||||
out_w, out_h = out.getSize()
|
||||
|
||||
out.clear()
|
||||
out.setTextColor(colors.white)
|
||||
out.setBackgroundColor(colors.black)
|
||||
|
||||
out.setCursorPos(1, 1)
|
||||
out.write("version " .. defs.CTRL_VERSION)
|
||||
out.setCursorPos(1, 2)
|
||||
out.write("system startup at " .. os.date("%Y/%m/%d %H:%M:%S"))
|
||||
|
||||
print("server v" .. defs.CTRL_VERSION .. " started at " .. os.date("%Y/%m/%d %H:%M:%S"))
|
||||
end
|
||||
|
||||
-- write a log message to the log screen and console
|
||||
-- msg: message to write
|
||||
-- color: (optional) color to print in, defaults to white
|
||||
function write(msg, color)
|
||||
color = color or colors.white
|
||||
local _x, _y = out.getCursorPos()
|
||||
|
||||
if output_full then
|
||||
out.scroll(1)
|
||||
out.setCursorPos(1, _y)
|
||||
else
|
||||
if _y == out_h then
|
||||
output_full = true
|
||||
out.scroll(1)
|
||||
out.setCursorPos(1, _y)
|
||||
else
|
||||
out.setCursorPos(1, _y + 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- output to screen
|
||||
out.setTextColor(colors.lightGray)
|
||||
out.write(os.date("[%H:%M:%S] "))
|
||||
out.setTextColor(color)
|
||||
out.write(msg)
|
||||
|
||||
-- output to console
|
||||
print(os.date("[%H:%M:%S] ") .. msg)
|
||||
end
|
||||
@@ -1,28 +0,0 @@
|
||||
-- create a new reactor 'object'
|
||||
-- reactor_id: the ID for this reactor
|
||||
-- main_view: the parent window/monitor for the main display (components)
|
||||
-- status_view: the parent window/monitor for the status display
|
||||
-- main_x: where to create the main window, x coordinate
|
||||
-- main_y: where to create the main window, y coordinate
|
||||
-- status_x: where to create the status window, x coordinate
|
||||
-- status_y: where to create the status window, y coordinate
|
||||
function create(reactor_id, main_view, status_view, main_x, main_y, status_x, status_y)
|
||||
return {
|
||||
id = reactor_id,
|
||||
render = {
|
||||
win_main = window.create(main_view, main_x, main_y, 20, 60, true),
|
||||
win_stat = window.create(status_view, status_x, status_y, 20, 20, true),
|
||||
stat_x = status_x,
|
||||
stat_y = status_y
|
||||
},
|
||||
control_state = false,
|
||||
waste_production = "antimatter", -- "plutonium", "polonium", "antimatter"
|
||||
state = {
|
||||
run = false,
|
||||
no_fuel = false,
|
||||
full_waste = false,
|
||||
high_temp = false,
|
||||
damage_crit = false
|
||||
}
|
||||
}
|
||||
end
|
||||
@@ -1,128 +0,0 @@
|
||||
os.loadAPI("defs.lua")
|
||||
os.loadAPI("log.lua")
|
||||
os.loadAPI("server.lua")
|
||||
|
||||
local reactors
|
||||
local scrammed
|
||||
local auto_scram
|
||||
|
||||
-- initialize the system regulator which provides safety measures, SCRAM functionality, and handles redstone
|
||||
-- _reactors: reactor table
|
||||
function init(_reactors)
|
||||
reactors = _reactors
|
||||
scrammed = false
|
||||
auto_scram = false
|
||||
|
||||
-- scram all reactors
|
||||
server.broadcast(false, reactors)
|
||||
|
||||
-- check initial states
|
||||
regulator.handle_redstone()
|
||||
end
|
||||
|
||||
-- check if the system is scrammed
|
||||
function is_scrammed()
|
||||
return scrammed
|
||||
end
|
||||
|
||||
-- handle redstone state changes
|
||||
function handle_redstone()
|
||||
-- check scram button
|
||||
if not rs.getInput("right") then
|
||||
if not scrammed then
|
||||
log.write("user SCRAM", colors.red)
|
||||
scram()
|
||||
end
|
||||
|
||||
-- toggling scram will release auto scram state
|
||||
auto_scram = false
|
||||
else
|
||||
scrammed = false
|
||||
end
|
||||
|
||||
-- check individual control buttons
|
||||
local input = rs.getBundledInput("left")
|
||||
for key, rctr in pairs(reactors) do
|
||||
if colors.test(input, defs.BUNDLE_DEF[key]) ~= rctr.control_state then
|
||||
-- state changed
|
||||
rctr.control_state = colors.test(input, defs.BUNDLE_DEF[key])
|
||||
if not scrammed then
|
||||
local safe = true
|
||||
|
||||
if rctr.control_state then
|
||||
safe = check_enable_safety(reactors[key])
|
||||
if safe then
|
||||
log.write("reactor " .. reactors[key].id .. " enabled", colors.lime)
|
||||
end
|
||||
else
|
||||
log.write("reactor " .. reactors[key].id .. " disabled", colors.cyan)
|
||||
end
|
||||
|
||||
-- start/stop reactor
|
||||
if safe then
|
||||
server.send(rctr.id, rctr.control_state)
|
||||
end
|
||||
elseif colors.test(input, defs.BUNDLE_DEF[key]) then
|
||||
log.write("scrammed: state locked off", colors.yellow)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- make sure enabling the provided reactor is safe
|
||||
-- reactor: reactor to check
|
||||
function check_enable_safety(reactor)
|
||||
if reactor.state.no_fuel or reactor.state.full_waste or reactor.state.high_temp or reactor.state.damage_crit then
|
||||
log.write("RCT-" .. reactor.id .. ": unsafe enable denied", colors.yellow)
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- make sure no running reactors are in a bad state
|
||||
function enforce_safeties()
|
||||
for key, reactor in pairs(reactors) do
|
||||
local overridden = false
|
||||
local state = reactor.state
|
||||
|
||||
-- check for problems
|
||||
if state.damage_crit and state.run then
|
||||
reactor.control_state = false
|
||||
log.write("RCT-" .. reactor.id .. ": shut down (damage)", colors.yellow)
|
||||
|
||||
-- scram all, so ignore setting overridden
|
||||
log.write("auto SCRAM all reactors", colors.red)
|
||||
auto_scram = true
|
||||
scram()
|
||||
elseif state.high_temp and state.run then
|
||||
reactor.control_state = false
|
||||
overridden = true
|
||||
log.write("RCT-" .. reactor.id .. ": shut down (temp)", colors.yellow)
|
||||
elseif state.full_waste and state.run then
|
||||
reactor.control_state = false
|
||||
overridden = true
|
||||
log.write("RCT-" .. reactor.id .. ": shut down (waste)", colors.yellow)
|
||||
elseif state.no_fuel and state.run then
|
||||
reactor.control_state = false
|
||||
overridden = true
|
||||
log.write("RCT-" .. reactor.id .. ": shut down (fuel)", colors.yellow)
|
||||
end
|
||||
|
||||
if overridden then
|
||||
server.send(reactor.id, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- shut down all reactors and prevent enabling them until the scram button is toggled/released
|
||||
function scram()
|
||||
scrammed = true
|
||||
server.broadcast(false, reactors)
|
||||
|
||||
for key, rctr in pairs(reactors) do
|
||||
if rctr.control_state then
|
||||
log.write("reactor " .. reactors[key].id .. " disabled", colors.cyan)
|
||||
end
|
||||
end
|
||||
end
|
||||
370
main/render.lua
370
main/render.lua
@@ -1,370 +0,0 @@
|
||||
os.loadAPI("defs.lua")
|
||||
|
||||
-- draw pipes between machines
|
||||
-- win: window to render in
|
||||
-- x: starting x coord
|
||||
-- y: starting y coord
|
||||
-- spacing: spacing between the pipes
|
||||
-- color_out: output pipe contents color
|
||||
-- color_ret: return pipe contents color
|
||||
-- tick: tick the pipes for an animation
|
||||
function draw_pipe(win, x, y, spacing, color_out, color_ret, tick)
|
||||
local _color
|
||||
local _off
|
||||
tick = tick or 0
|
||||
|
||||
for i = 0, 4, 1
|
||||
do
|
||||
_off = (i + tick) % 2 == 0 or (tick == 1 and i == 0) or (tick == 3 and i == 4)
|
||||
|
||||
if _off then
|
||||
_color = colors.lightGray
|
||||
else
|
||||
_color = color_out
|
||||
end
|
||||
|
||||
win.setBackgroundColor(_color)
|
||||
win.setCursorPos(x, y + i)
|
||||
win.write(" ")
|
||||
|
||||
if not _off then
|
||||
_color = color_ret
|
||||
end
|
||||
|
||||
win.setBackgroundColor(_color)
|
||||
win.setCursorPos(x + spacing, y + i)
|
||||
win.write(" ")
|
||||
end
|
||||
end
|
||||
|
||||
-- draw a reactor view consisting of the reactor, boiler, turbine, and pipes
|
||||
-- data: reactor table
|
||||
function draw_reactor_system(data)
|
||||
local win = data.render.win_main
|
||||
local win_w, win_h = win.getSize()
|
||||
|
||||
win.setBackgroundColor(colors.black)
|
||||
win.setTextColor(colors.black)
|
||||
win.clear()
|
||||
win.setCursorPos(1, 1)
|
||||
|
||||
-- draw header --
|
||||
|
||||
local header = "REACTOR " .. data.id
|
||||
local header_pad_x = (win_w - string.len(header) - 2) / 2
|
||||
local header_color
|
||||
if data.state.no_fuel then
|
||||
if data.state.run then
|
||||
header_color = colors.purple
|
||||
else
|
||||
header_color = colors.brown
|
||||
end
|
||||
elseif data.state.full_waste then
|
||||
header_color = colors.yellow
|
||||
elseif data.state.high_temp then
|
||||
header_color = colors.orange
|
||||
elseif data.state.damage_crit then
|
||||
header_color = colors.red
|
||||
elseif data.state.run then
|
||||
header_color = colors.green
|
||||
else
|
||||
header_color = colors.lightGray
|
||||
end
|
||||
|
||||
local running = data.state.run and not data.state.no_fuel
|
||||
|
||||
win.write(" ")
|
||||
win.setBackgroundColor(header_color)
|
||||
win.write(string.rep(" ", win_w - 2))
|
||||
win.setBackgroundColor(colors.black)
|
||||
win.write(" ")
|
||||
win.setCursorPos(1, 2)
|
||||
win.write(" ")
|
||||
win.setBackgroundColor(header_color)
|
||||
win.write(string.rep(" ", header_pad_x) .. header .. string.rep(" ", header_pad_x))
|
||||
win.setBackgroundColor(colors.black)
|
||||
win.write(" ")
|
||||
|
||||
-- create strings for use in blit
|
||||
local line_text = string.rep(" ", 14)
|
||||
local line_text_color = string.rep("0", 14)
|
||||
|
||||
-- draw components --
|
||||
|
||||
-- draw reactor
|
||||
local rod = "88"
|
||||
if data.state.high_temp then
|
||||
rod = "11"
|
||||
elseif running then
|
||||
rod = "99"
|
||||
end
|
||||
|
||||
win.setCursorPos(4, 4)
|
||||
win.setBackgroundColor(colors.gray)
|
||||
win.write(line_text)
|
||||
win.setCursorPos(4, 5)
|
||||
win.blit(line_text, line_text_color, "77" .. rod .. "77" .. rod .. "77" .. rod .. "77")
|
||||
win.setCursorPos(4, 6)
|
||||
win.blit(line_text, line_text_color, "7777" .. rod .. "77" .. rod .. "7777")
|
||||
win.setCursorPos(4, 7)
|
||||
win.blit(line_text, line_text_color, "77" .. rod .. "77" .. rod .. "77" .. rod .. "77")
|
||||
win.setCursorPos(4, 8)
|
||||
win.blit(line_text, line_text_color, "7777" .. rod .. "77" .. rod .. "7777")
|
||||
win.setCursorPos(4, 9)
|
||||
win.blit(line_text, line_text_color, "77" .. rod .. "77" .. rod .. "77" .. rod .. "77")
|
||||
win.setCursorPos(4, 10)
|
||||
win.write(line_text)
|
||||
|
||||
-- boiler
|
||||
local steam = "ffffffffff"
|
||||
if running then
|
||||
steam = "0000000000"
|
||||
end
|
||||
|
||||
win.setCursorPos(4, 16)
|
||||
win.setBackgroundColor(colors.gray)
|
||||
win.write(line_text)
|
||||
win.setCursorPos(4, 17)
|
||||
win.blit(line_text, line_text_color, "77" .. steam .. "77")
|
||||
win.setCursorPos(4, 18)
|
||||
win.blit(line_text, line_text_color, "77" .. steam .. "77")
|
||||
win.setCursorPos(4, 19)
|
||||
win.blit(line_text, line_text_color, "77888888888877")
|
||||
win.setCursorPos(4, 20)
|
||||
win.blit(line_text, line_text_color, "77bbbbbbbbbb77")
|
||||
win.setCursorPos(4, 21)
|
||||
win.blit(line_text, line_text_color, "77bbbbbbbbbb77")
|
||||
win.setCursorPos(4, 22)
|
||||
win.blit(line_text, line_text_color, "77bbbbbbbbbb77")
|
||||
win.setCursorPos(4, 23)
|
||||
win.setBackgroundColor(colors.gray)
|
||||
win.write(line_text)
|
||||
|
||||
-- turbine
|
||||
win.setCursorPos(4, 29)
|
||||
win.setBackgroundColor(colors.gray)
|
||||
win.write(line_text)
|
||||
win.setCursorPos(4, 30)
|
||||
if running then
|
||||
win.blit(line_text, line_text_color, "77000000000077")
|
||||
else
|
||||
win.blit(line_text, line_text_color, "77ffffffffff77")
|
||||
end
|
||||
win.setCursorPos(4, 31)
|
||||
if running then
|
||||
win.blit(line_text, line_text_color, "77008000080077")
|
||||
else
|
||||
win.blit(line_text, line_text_color, "77ff8ffff8ff77")
|
||||
end
|
||||
win.setCursorPos(4, 32)
|
||||
if running then
|
||||
win.blit(line_text, line_text_color, "77000800800077")
|
||||
else
|
||||
win.blit(line_text, line_text_color, "77fff8ff8fff77")
|
||||
end
|
||||
win.setCursorPos(4, 33)
|
||||
if running then
|
||||
win.blit(line_text, line_text_color, "77000088000077")
|
||||
else
|
||||
win.blit(line_text, line_text_color, "77ffff88ffff77")
|
||||
end
|
||||
win.setCursorPos(4, 34)
|
||||
if running then
|
||||
win.blit(line_text, line_text_color, "77000800800077")
|
||||
else
|
||||
win.blit(line_text, line_text_color, "77fff8ff8fff77")
|
||||
end
|
||||
win.setCursorPos(4, 35)
|
||||
if running then
|
||||
win.blit(line_text, line_text_color, "77008000080077")
|
||||
else
|
||||
win.blit(line_text, line_text_color, "77ff8ffff8ff77")
|
||||
end
|
||||
win.setCursorPos(4, 36)
|
||||
if running then
|
||||
win.blit(line_text, line_text_color, "77000000000077")
|
||||
else
|
||||
win.blit(line_text, line_text_color, "77ffffffffff77")
|
||||
end
|
||||
win.setCursorPos(4, 37)
|
||||
win.setBackgroundColor(colors.gray)
|
||||
win.write(line_text)
|
||||
|
||||
-- draw reactor coolant pipes
|
||||
draw_pipe(win, 7, 11, 6, colors.orange, colors.lightBlue)
|
||||
|
||||
-- draw turbine pipes
|
||||
draw_pipe(win, 7, 24, 6, colors.white, colors.blue)
|
||||
end
|
||||
|
||||
-- draw the reactor statuses on the status screen
|
||||
-- data: reactor table
|
||||
function draw_reactor_status(data)
|
||||
local win = data.render.win_stat
|
||||
|
||||
win.setBackgroundColor(colors.black)
|
||||
win.setTextColor(colors.white)
|
||||
win.clear()
|
||||
|
||||
-- show control state
|
||||
win.setCursorPos(1, 1)
|
||||
if data.control_state then
|
||||
win.blit(" + ENABLED", "00000000000", "dddffffffff")
|
||||
else
|
||||
win.blit(" - DISABLED", "000000000000", "eeefffffffff")
|
||||
end
|
||||
|
||||
-- show run state
|
||||
win.setCursorPos(1, 2)
|
||||
if data.state.run then
|
||||
win.blit(" + RUNNING", "00000000000", "dddffffffff")
|
||||
else
|
||||
win.blit(" - STOPPED", "00000000000", "888ffffffff")
|
||||
end
|
||||
|
||||
-- show fuel state
|
||||
win.setCursorPos(1, 4)
|
||||
if data.state.no_fuel then
|
||||
win.blit(" - NO FUEL", "00000000000", "eeeffffffff")
|
||||
else
|
||||
win.blit(" + FUEL OK", "00000000000", "999ffffffff")
|
||||
end
|
||||
|
||||
-- show waste state
|
||||
win.setCursorPos(1, 5)
|
||||
if data.state.full_waste then
|
||||
win.blit(" - WASTE FULL", "00000000000000", "eeefffffffffff")
|
||||
else
|
||||
win.blit(" + WASTE OK", "000000000000", "999fffffffff")
|
||||
end
|
||||
|
||||
-- show high temp state
|
||||
win.setCursorPos(1, 6)
|
||||
if data.state.high_temp then
|
||||
win.blit(" - HIGH TEMP", "0000000000000", "eeeffffffffff")
|
||||
else
|
||||
win.blit(" + TEMP OK", "00000000000", "999ffffffff")
|
||||
end
|
||||
|
||||
-- show damage state
|
||||
win.setCursorPos(1, 7)
|
||||
if data.state.damage_crit then
|
||||
win.blit(" - CRITICAL DAMAGE", "0000000000000000000", "eeeffffffffffffffff")
|
||||
else
|
||||
win.blit(" + CASING INTACT", "00000000000000000", "999ffffffffffffff")
|
||||
end
|
||||
|
||||
-- waste processing options --
|
||||
win.setTextColor(colors.black)
|
||||
win.setBackgroundColor(colors.white)
|
||||
|
||||
win.setCursorPos(1, 10)
|
||||
win.write(" ")
|
||||
win.setCursorPos(1, 11)
|
||||
win.write(" WASTE OUTPUT ")
|
||||
|
||||
win.setCursorPos(1, 13)
|
||||
win.setBackgroundColor(colors.cyan)
|
||||
if data.waste_production == "plutonium" then
|
||||
win.write(" > plutonium ")
|
||||
else
|
||||
win.write(" plutonium ")
|
||||
end
|
||||
|
||||
win.setCursorPos(1, 15)
|
||||
win.setBackgroundColor(colors.green)
|
||||
if data.waste_production == "polonium" then
|
||||
win.write(" > polonium ")
|
||||
else
|
||||
win.write(" polonium ")
|
||||
end
|
||||
|
||||
win.setCursorPos(1, 17)
|
||||
win.setBackgroundColor(colors.purple)
|
||||
if data.waste_production == "antimatter" then
|
||||
win.write(" > antimatter ")
|
||||
else
|
||||
win.write(" antimatter ")
|
||||
end
|
||||
end
|
||||
|
||||
-- update the system monitor screen
|
||||
-- mon: monitor to update
|
||||
-- is_scrammed:
|
||||
function update_system_monitor(mon, is_scrammed, reactors)
|
||||
if is_scrammed then
|
||||
-- display scram banner
|
||||
mon.setTextColor(colors.white)
|
||||
mon.setBackgroundColor(colors.black)
|
||||
mon.setCursorPos(1, 2)
|
||||
mon.clearLine()
|
||||
mon.setBackgroundColor(colors.red)
|
||||
mon.setCursorPos(1, 3)
|
||||
mon.write(" ")
|
||||
mon.setCursorPos(1, 4)
|
||||
mon.write(" SCRAM ")
|
||||
mon.setCursorPos(1, 5)
|
||||
mon.write(" ")
|
||||
mon.setBackgroundColor(colors.black)
|
||||
mon.setCursorPos(1, 6)
|
||||
mon.clearLine()
|
||||
mon.setTextColor(colors.white)
|
||||
else
|
||||
-- clear where scram banner would be
|
||||
mon.setCursorPos(1, 3)
|
||||
mon.clearLine()
|
||||
mon.setCursorPos(1, 4)
|
||||
mon.clearLine()
|
||||
mon.setCursorPos(1, 5)
|
||||
mon.clearLine()
|
||||
|
||||
-- show production statistics--
|
||||
|
||||
local mrf_t = 0
|
||||
local mb_t = 0
|
||||
local plutonium = 0
|
||||
local polonium = 0
|
||||
local spent_waste = 0
|
||||
local antimatter = 0
|
||||
|
||||
-- determine production values
|
||||
for key, rctr in pairs(reactors) do
|
||||
if rctr.state.run then
|
||||
mrf_t = mrf_t + defs.TURBINE_MRF_T
|
||||
mb_t = mb_t + defs.REACTOR_MB_T
|
||||
|
||||
if rctr.waste_production == "plutonium" then
|
||||
plutonium = plutonium + (defs.REACTOR_MB_T * defs.PLUTONIUM_PER_WASTE)
|
||||
spent_waste = spent_waste + (defs.REACTOR_MB_T * defs.PLUTONIUM_PER_WASTE * defs.SPENT_PER_BYPRODUCT)
|
||||
elseif rctr.waste_production == "polonium" then
|
||||
polonium = polonium + (defs.REACTOR_MB_T * defs.POLONIUM_PER_WASTE)
|
||||
spent_waste = spent_waste + (defs.REACTOR_MB_T * defs.POLONIUM_PER_WASTE * defs.SPENT_PER_BYPRODUCT)
|
||||
elseif rctr.waste_production == "antimatter" then
|
||||
antimatter = antimatter + (defs.REACTOR_MB_T * defs.POLONIUM_PER_WASTE * defs.ANTIMATTER_PER_POLONIUM)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- draw stats
|
||||
mon.setTextColor(colors.lightGray)
|
||||
mon.setCursorPos(1, 2)
|
||||
mon.clearLine()
|
||||
mon.write("ENERGY: " .. string.format("%0.2f", mrf_t) .. " MRF/t")
|
||||
-- mon.setCursorPos(1, 3)
|
||||
-- mon.clearLine()
|
||||
-- mon.write("FUEL: " .. mb_t .. " mB/t")
|
||||
mon.setCursorPos(1, 3)
|
||||
mon.clearLine()
|
||||
mon.write("Pu: " .. string.format("%0.2f", plutonium) .. " mB/t")
|
||||
mon.setCursorPos(1, 4)
|
||||
mon.clearLine()
|
||||
mon.write("Po: " .. string.format("%0.2f", polonium) .. " mB/t")
|
||||
mon.setCursorPos(1, 5)
|
||||
mon.clearLine()
|
||||
mon.write("SPENT: " .. string.format("%0.2f", spent_waste) .. " mB/t")
|
||||
mon.setCursorPos(1, 6)
|
||||
mon.clearLine()
|
||||
mon.write("ANTI-M: " .. string.format("%0.2f", antimatter * 1000) .. " uB/t")
|
||||
mon.setTextColor(colors.white)
|
||||
end
|
||||
end
|
||||
109
main/server.lua
109
main/server.lua
@@ -1,109 +0,0 @@
|
||||
os.loadAPI("defs.lua")
|
||||
os.loadAPI("log.lua")
|
||||
os.loadAPI("regulator.lua")
|
||||
|
||||
local modem
|
||||
local reactors
|
||||
|
||||
-- initalize the listener running on the wireless modem
|
||||
-- _reactors: reactor table
|
||||
function init(_reactors)
|
||||
modem = peripheral.wrap("top")
|
||||
reactors = _reactors
|
||||
|
||||
-- open listening port
|
||||
if not modem.isOpen(defs.LISTEN_PORT) then
|
||||
modem.open(defs.LISTEN_PORT)
|
||||
end
|
||||
|
||||
-- send out a greeting to solicit responses for clients that are already running
|
||||
broadcast(0, reactors)
|
||||
end
|
||||
|
||||
-- handle an incoming message from the modem
|
||||
-- packet: table containing message fields
|
||||
function handle_message(packet)
|
||||
if type(packet.message) == "number" then
|
||||
-- this is a greeting
|
||||
log.write("reactor " .. packet.message .. " connected", colors.green)
|
||||
|
||||
-- send current control command
|
||||
for key, rctr in pairs(reactors) do
|
||||
if rctr.id == packet.message then
|
||||
send(rctr.id, rctr.control_state)
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
-- got reactor status
|
||||
local eval_safety = false
|
||||
|
||||
for key, value in pairs(reactors) do
|
||||
if value.id == packet.message.id then
|
||||
local tag = "RCT-" .. value.id .. ": "
|
||||
|
||||
if value.state.run ~= packet.message.run then
|
||||
value.state.run = packet.message.run
|
||||
if value.state.run then
|
||||
eval_safety = true
|
||||
log.write(tag .. "running", colors.green)
|
||||
end
|
||||
end
|
||||
|
||||
if value.state.no_fuel ~= packet.message.no_fuel then
|
||||
value.state.no_fuel = packet.message.no_fuel
|
||||
if value.state.no_fuel then
|
||||
eval_safety = true
|
||||
log.write(tag .. "insufficient fuel", colors.gray)
|
||||
end
|
||||
end
|
||||
|
||||
if value.state.full_waste ~= packet.message.full_waste then
|
||||
value.state.full_waste = packet.message.full_waste
|
||||
if value.state.full_waste then
|
||||
eval_safety = true
|
||||
log.write(tag .. "waste tank full", colors.brown)
|
||||
end
|
||||
end
|
||||
|
||||
if value.state.high_temp ~= packet.message.high_temp then
|
||||
value.state.high_temp = packet.message.high_temp
|
||||
if value.state.high_temp then
|
||||
eval_safety = true
|
||||
log.write(tag .. "high temperature", colors.orange)
|
||||
end
|
||||
end
|
||||
|
||||
if value.state.damage_crit ~= packet.message.damage_crit then
|
||||
value.state.damage_crit = packet.message.damage_crit
|
||||
if value.state.damage_crit then
|
||||
eval_safety = true
|
||||
log.write(tag .. "critical damage", colors.red)
|
||||
end
|
||||
end
|
||||
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- check to ensure safe operation
|
||||
if eval_safety then
|
||||
regulator.enforce_safeties()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- send a message to a given reactor
|
||||
-- dest: reactor ID
|
||||
-- message: true or false for enable control or another value for other functionality, like 0 for greeting
|
||||
function send(dest, message)
|
||||
modem.transmit(dest + defs.LISTEN_PORT, defs.LISTEN_PORT, message)
|
||||
end
|
||||
|
||||
-- broadcast a message to all reactors
|
||||
-- message: true or false for enable control or another value for other functionality, like 0 for greeting
|
||||
function broadcast(message)
|
||||
for key, value in pairs(reactors) do
|
||||
modem.transmit(value.id + defs.LISTEN_PORT, defs.LISTEN_PORT, message)
|
||||
end
|
||||
end
|
||||
578
pocket/configure.lua
Normal file
578
pocket/configure.lua
Normal file
@@ -0,0 +1,578 @@
|
||||
--
|
||||
-- Configuration GUI
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
local Div = require("graphics.elements.div")
|
||||
local ListBox = require("graphics.elements.listbox")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local CheckBox = require("graphics.elements.controls.checkbox")
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
local RadioButton = require("graphics.elements.controls.radio_button")
|
||||
|
||||
local NumberField = require("graphics.elements.form.number_field")
|
||||
local TextField = require("graphics.elements.form.text_field")
|
||||
|
||||
local println = util.println
|
||||
local tri = util.trinary
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local LEFT = core.ALIGN.LEFT
|
||||
local CENTER = core.ALIGN.CENTER
|
||||
local RIGHT = core.ALIGN.RIGHT
|
||||
|
||||
-- changes to the config data/format to let the user know
|
||||
local changes = {}
|
||||
|
||||
---@class pkt_configurator
|
||||
local configurator = {}
|
||||
|
||||
local style = {}
|
||||
|
||||
style.root = cpair(colors.black, colors.lightGray)
|
||||
style.header = cpair(colors.white, colors.gray)
|
||||
|
||||
style.colors = {
|
||||
{ c = colors.red, hex = 0xdf4949 },
|
||||
{ c = colors.orange, hex = 0xffb659 },
|
||||
{ c = colors.yellow, hex = 0xfffc79 },
|
||||
{ c = colors.lime, hex = 0x80ff80 },
|
||||
{ c = colors.green, hex = 0x4aee8a },
|
||||
{ c = colors.cyan, hex = 0x34bac8 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.purple, hex = 0xb156ee },
|
||||
{ c = colors.pink, hex = 0xf26ba2 },
|
||||
{ c = colors.magenta, hex = 0xf9488a },
|
||||
{ c = colors.lightGray, hex = 0xcacaca },
|
||||
{ c = colors.gray, hex = 0x575757 }
|
||||
}
|
||||
|
||||
local bw_fg_bg = cpair(colors.black, colors.white)
|
||||
local g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
|
||||
local nav_fg_bg = bw_fg_bg
|
||||
local btn_act_fg_bg = cpair(colors.white, colors.gray)
|
||||
local dis_fg_bg = cpair(colors.lightGray,colors.white)
|
||||
|
||||
local tool_ctl = {
|
||||
ask_config = false,
|
||||
has_config = false,
|
||||
viewing_config = false,
|
||||
importing_legacy = false,
|
||||
|
||||
view_cfg = nil, ---@type graphics_element
|
||||
settings_apply = nil, ---@type graphics_element
|
||||
|
||||
set_networked = nil, ---@type function
|
||||
bundled_emcool = nil, ---@type function
|
||||
gen_summary = nil, ---@type function
|
||||
show_current_cfg = nil, ---@type function
|
||||
load_legacy = nil, ---@type function
|
||||
|
||||
show_auth_key = nil, ---@type function
|
||||
show_key_btn = nil, ---@type graphics_element
|
||||
auth_key_textbox = nil, ---@type graphics_element
|
||||
auth_key_value = ""
|
||||
}
|
||||
|
||||
---@class pkt_config
|
||||
local tmp_cfg = {
|
||||
SVR_Channel = nil, ---@type integer
|
||||
CRD_Channel = nil, ---@type integer
|
||||
PKT_Channel = nil, ---@type integer
|
||||
ConnTimeout = nil, ---@type number
|
||||
TrustedRange = nil, ---@type number
|
||||
AuthKey = nil, ---@type string|nil
|
||||
LogMode = 0,
|
||||
LogPath = "",
|
||||
LogDebug = false,
|
||||
}
|
||||
|
||||
---@class pkt_config
|
||||
local ini_cfg = {}
|
||||
---@class pkt_config
|
||||
local settings_cfg = {}
|
||||
|
||||
-- all settings fields, their nice names, and their default values
|
||||
local fields = {
|
||||
{ "SVR_Channel", "SVR Channel", 16240 },
|
||||
{ "CRD_Channel", "CRD Channel", 16243 },
|
||||
{ "PKT_Channel", "PKT Channel", 16244 },
|
||||
{ "ConnTimeout", "Connection Timeout", 5 },
|
||||
{ "TrustedRange", "Trusted Range", 0 },
|
||||
{ "AuthKey", "Facility Auth Key" , ""},
|
||||
{ "LogMode", "Log Mode", log.MODE.APPEND },
|
||||
{ "LogPath", "Log Path", "/log.txt" },
|
||||
{ "LogDebug","Log Debug Messages", false }
|
||||
}
|
||||
|
||||
-- load data from the settings file
|
||||
---@param target pkt_config
|
||||
---@param raw boolean? true to not use default values
|
||||
local function load_settings(target, raw)
|
||||
for _, v in pairs(fields) do settings.unset(v[1]) end
|
||||
|
||||
local loaded = settings.load("/pocket.settings")
|
||||
|
||||
for _, v in pairs(fields) do target[v[1]] = settings.get(v[1], tri(raw, nil, v[3])) end
|
||||
|
||||
return loaded
|
||||
end
|
||||
|
||||
-- create the config view
|
||||
---@param display graphics_element
|
||||
local function config_view(display)
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local function exit() os.queueEvent("terminate") end
|
||||
|
||||
TextBox{parent=display,y=1,text="Pocket Configurator",alignment=CENTER,height=1,fg_bg=style.header}
|
||||
|
||||
local root_pane_div = Div{parent=display,x=1,y=2}
|
||||
|
||||
local main_page = Div{parent=root_pane_div,x=1,y=1}
|
||||
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local summary = Div{parent=root_pane_div,x=1,y=1}
|
||||
local changelog = Div{parent=root_pane_div,x=1,y=1}
|
||||
|
||||
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,net_cfg,log_cfg,summary,changelog}}
|
||||
|
||||
-- Main Page
|
||||
|
||||
local y_start = 7
|
||||
|
||||
TextBox{parent=main_page,x=2,y=2,height=4,text="Welcome to the Pocket configurator! Please select one of the following options."}
|
||||
|
||||
if tool_ctl.ask_config then
|
||||
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Please configure before starting up.",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
y_start = y_start + 3
|
||||
end
|
||||
|
||||
local function view_config()
|
||||
tool_ctl.viewing_config = true
|
||||
tool_ctl.gen_summary(settings_cfg)
|
||||
tool_ctl.settings_apply.hide(true)
|
||||
main_pane.set_value(4)
|
||||
end
|
||||
|
||||
if fs.exists("/pocket/config.lua") then
|
||||
PushButton{parent=main_page,x=2,y=y_start,min_width=22,text="Import Legacy Config",callback=function()tool_ctl.load_legacy()end,fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=btn_act_fg_bg}
|
||||
y_start = y_start + 2
|
||||
end
|
||||
|
||||
PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure Device",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg}
|
||||
|
||||
if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end
|
||||
|
||||
PushButton{parent=main_page,x=2,y=18,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=main_page,x=14,y=18,min_width=12,text="Change Log",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#region Network
|
||||
|
||||
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||
|
||||
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
|
||||
|
||||
TextBox{parent=net_cfg,x=1,y=2,height=1,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=1,height=1,text="Set network channels."}
|
||||
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the named channels must be the same within a particular SCADA network.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=8,height=1,width=18,text="Supervisor Channel"}
|
||||
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=10,height=1,width=19,text="Coordinator Channel"}
|
||||
local crd_chan = NumberField{parent=net_c_1,x=1,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=12,height=1,width=14,text="Pocket Channel"}
|
||||
local pkt_chan = NumberField{parent=net_c_1,x=1,y=13,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=13,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local chan_err = TextBox{parent=net_c_1,x=1,y=14,height=1,width=24,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_channels()
|
||||
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
|
||||
if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then
|
||||
tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c
|
||||
net_pane.set_value(2)
|
||||
chan_err.hide(true)
|
||||
else chan_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,x=19,y=15,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=1,height=1,text="Set connection timeout."}
|
||||
TextBox{parent=net_c_2,x=1,y=3,height=7,text="You generally should not need to modify this. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=11,height=1,width=19,text="Connection Timeout"}
|
||||
local timeout = NumberField{parent=net_c_2,x=1,y=12,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=9,y=12,height=2,text="seconds\n(default 5)",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local ct_err = TextBox{parent=net_c_2,x=1,y=14,height=1,width=24,text="Please set timeout.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_timeouts()
|
||||
local timeout_val = tonumber(timeout.get_value())
|
||||
if timeout_val ~= nil then
|
||||
tmp_cfg.ConnTimeout = timeout_val
|
||||
net_pane.set_value(3)
|
||||
ct_err.hide(true)
|
||||
else ct_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_2,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,x=19,y=15,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,x=1,y=1,height=1,text="Set the trusted range."}
|
||||
TextBox{parent=net_c_3,x=1,y=3,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many blocks away.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_3,x=1,y=8,height=4,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local range = NumberField{parent=net_c_3,x=1,y=13,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
|
||||
local tr_err = TextBox{parent=net_c_3,x=1,y=14,height=1,width=24,text="Set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_tr()
|
||||
local range_val = tonumber(range.get_value())
|
||||
if range_val ~= nil then
|
||||
tmp_cfg.TrustedRange = range_val
|
||||
net_pane.set_value(4)
|
||||
tr_err.hide(true)
|
||||
else tr_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_3,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,x=19,y=15,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_4,x=1,y=1,height=4,text="Optionally, set the facility authentication key. Do NOT use one of your passwords."}
|
||||
TextBox{parent=net_c_4,x=1,y=6,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_4,x=1,y=12,height=1,text="Facility Auth Key"}
|
||||
local key, _, censor = TextField{parent=net_c_4,x=1,y=13,max_len=64,value=ini_cfg.AuthKey,width=24,height=1,fg_bg=bw_fg_bg}
|
||||
|
||||
local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
|
||||
|
||||
-- declare back first so tabbing makes sense visually
|
||||
PushButton{parent=net_c_4,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
local hide_key = CheckBox{parent=net_c_4,x=8,y=15,label="Hide Key",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
|
||||
|
||||
hide_key.set_value(true)
|
||||
censor_key(true)
|
||||
|
||||
local key_err = TextBox{parent=net_c_4,x=1,y=14,height=1,width=24,text="Length must be > 7.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_auth()
|
||||
local v = key.get_value()
|
||||
if string.len(v) == 0 or string.len(v) >= 8 then
|
||||
tmp_cfg.AuthKey = key.get_value()
|
||||
main_pane.set_value(3)
|
||||
key_err.hide(true)
|
||||
else key_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_4,x=19,y=15,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Logging
|
||||
|
||||
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=24}
|
||||
|
||||
TextBox{parent=log_cfg,x=1,y=2,height=1,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=1,height=1,text="Configure logging below."}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=3,height=1,text="Log File Mode"}
|
||||
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=7,height=1,text="Log File Path"}
|
||||
local path = TextField{parent=log_c_1,x=1,y=8,width=24,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||
|
||||
local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||
TextBox{parent=log_c_1,x=3,y=11,height=4,text="This results in much larger log files. Use only as needed.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local path_err = TextBox{parent=log_c_1,x=1,y=14,height=1,width=24,text="Provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_log()
|
||||
if path.get_value() ~= "" then
|
||||
path_err.hide(true)
|
||||
tmp_cfg.LogMode = mode.get_value() - 1
|
||||
tmp_cfg.LogPath = path.get_value()
|
||||
tmp_cfg.LogDebug = en_dbg.get_value()
|
||||
tool_ctl.gen_summary(tmp_cfg)
|
||||
tool_ctl.viewing_config = false
|
||||
tool_ctl.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
main_pane.set_value(4)
|
||||
else path_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=log_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,x=19,y=15,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Summary and Saving
|
||||
|
||||
local sum_c_1 = Div{parent=summary,x=2,y=4,width=24}
|
||||
local sum_c_2 = Div{parent=summary,x=2,y=4,width=24}
|
||||
local sum_c_3 = Div{parent=summary,x=2,y=4,width=24}
|
||||
local sum_c_4 = Div{parent=summary,x=2,y=4,width=24}
|
||||
|
||||
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
|
||||
|
||||
TextBox{parent=summary,x=1,y=2,height=1,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||
|
||||
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=11,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local function back_from_summary()
|
||||
if tool_ctl.viewing_config or tool_ctl.importing_legacy then
|
||||
main_pane.set_value(1)
|
||||
tool_ctl.viewing_config = false
|
||||
tool_ctl.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
else
|
||||
main_pane.set_value(3)
|
||||
end
|
||||
end
|
||||
|
||||
---@param element graphics_element
|
||||
---@param data any
|
||||
local function try_set(element, data)
|
||||
if data ~= nil then element.set_value(data) end
|
||||
end
|
||||
|
||||
local function save_and_continue()
|
||||
for k, v in pairs(tmp_cfg) do settings.set(k, v) end
|
||||
|
||||
if settings.save("/pocket.settings") then
|
||||
load_settings(settings_cfg, true)
|
||||
load_settings(ini_cfg)
|
||||
|
||||
try_set(svr_chan, ini_cfg.SVR_Channel)
|
||||
try_set(crd_chan, ini_cfg.CRD_Channel)
|
||||
try_set(pkt_chan, ini_cfg.PKT_Channel)
|
||||
try_set(timeout, ini_cfg.ConnTimeout)
|
||||
try_set(range, ini_cfg.TrustedRange)
|
||||
try_set(key, ini_cfg.AuthKey)
|
||||
try_set(mode, ini_cfg.LogMode)
|
||||
try_set(path, ini_cfg.LogPath)
|
||||
try_set(en_dbg, ini_cfg.LogDebug)
|
||||
|
||||
tool_ctl.view_cfg.enable()
|
||||
|
||||
if tool_ctl.importing_legacy then
|
||||
tool_ctl.importing_legacy = false
|
||||
sum_pane.set_value(3)
|
||||
else
|
||||
sum_pane.set_value(2)
|
||||
end
|
||||
else
|
||||
sum_pane.set_value(4)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_1,x=1,y=15,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=1,y=13,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg}
|
||||
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=18,y=15,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=sum_c_2,x=1,y=1,height=1,text="Settings saved!"}
|
||||
|
||||
local function go_home()
|
||||
main_pane.set_value(1)
|
||||
net_pane.set_value(1)
|
||||
sum_pane.set_value(1)
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_2,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_2,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_3,x=1,y=1,height=4,text="The old config.lua file will now be deleted, then the configurator will exit."}
|
||||
|
||||
local function delete_legacy()
|
||||
fs.delete("/pocket/config.lua")
|
||||
exit()
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_3,x=1,y=15,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_3,x=19,y=15,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_4,x=1,y=1,height=8,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=sum_c_4,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_4,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
--#endregion
|
||||
|
||||
-- Config Change Log
|
||||
|
||||
local cl = Div{parent=changelog,x=2,y=4,width=24}
|
||||
|
||||
TextBox{parent=changelog,x=1,y=2,height=1,text=" Config Change Log",fg_bg=bw_fg_bg}
|
||||
|
||||
local c_log = ListBox{parent=cl,x=1,y=1,height=13,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
for _, change in ipairs(changes) do
|
||||
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg}
|
||||
for _, v in ipairs(change[2]) do
|
||||
local e = Div{parent=c_log,height=#util.strwrap(v,21)}
|
||||
TextBox{parent=e,y=1,x=1,text="- ",height=1,fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=cl,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
-- set tool functions now that we have the elements
|
||||
|
||||
-- load a legacy config file
|
||||
function tool_ctl.load_legacy()
|
||||
local config = require("pocket.config")
|
||||
|
||||
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
|
||||
tmp_cfg.CRD_Channel = config.CRD_CHANNEL
|
||||
tmp_cfg.PKT_Channel = config.PKT_CHANNEL
|
||||
tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT
|
||||
tmp_cfg.TrustedRange = config.TRUSTED_RANGE
|
||||
tmp_cfg.AuthKey = config.AUTH_KEY or ""
|
||||
|
||||
tmp_cfg.LogMode = config.LOG_MODE
|
||||
tmp_cfg.LogPath = config.LOG_PATH
|
||||
tmp_cfg.LogDebug = config.LOG_DEBUG or false
|
||||
|
||||
tool_ctl.gen_summary(tmp_cfg)
|
||||
sum_pane.set_value(1)
|
||||
main_pane.set_value(4)
|
||||
tool_ctl.importing_legacy = true
|
||||
end
|
||||
|
||||
-- expose the auth key on the summary page
|
||||
function tool_ctl.show_auth_key()
|
||||
tool_ctl.show_key_btn.disable()
|
||||
tool_ctl.auth_key_textbox.set_value(tool_ctl.auth_key_value)
|
||||
end
|
||||
|
||||
-- generate the summary list
|
||||
---@param cfg pkt_config
|
||||
function tool_ctl.gen_summary(cfg)
|
||||
setting_list.remove_all()
|
||||
|
||||
local alternate = false
|
||||
local inner_width = setting_list.get_width() - 1
|
||||
|
||||
tool_ctl.show_key_btn.enable()
|
||||
tool_ctl.auth_key_value = cfg.AuthKey or "" -- to show auth key
|
||||
|
||||
for i = 1, #fields do
|
||||
local f = fields[i]
|
||||
local height = 1
|
||||
local label_w = string.len(f[2])
|
||||
local val_max_w = (inner_width - label_w) - 1
|
||||
local raw = cfg[f[1]]
|
||||
local val = util.strval(raw)
|
||||
|
||||
if f[1] == "AuthKey" then val = string.rep("*", string.len(val))
|
||||
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end
|
||||
|
||||
if val == "nil" then val = "<not set>" end
|
||||
|
||||
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
|
||||
alternate = not alternate
|
||||
|
||||
if string.len(val) > val_max_w then
|
||||
local lines = util.strwrap(val, inner_width)
|
||||
height = #lines + 1
|
||||
end
|
||||
|
||||
local line = Div{parent=setting_list,height=height,fg_bg=c}
|
||||
TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)}
|
||||
|
||||
local textbox
|
||||
if height > 1 then
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
|
||||
else
|
||||
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||
end
|
||||
|
||||
if f[1] == "AuthKey" then tool_ctl.auth_key_textbox = textbox end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- reset terminal screen
|
||||
local function reset_term()
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
end
|
||||
|
||||
-- run the pcoket configurator
|
||||
---@param ask_config? boolean indicate if this is being called by the startup app due to an invalid configuration
|
||||
function configurator.configure(ask_config)
|
||||
tool_ctl.ask_config = ask_config == true
|
||||
|
||||
load_settings(settings_cfg, true)
|
||||
tool_ctl.has_config = load_settings(ini_cfg)
|
||||
|
||||
reset_term()
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.colors do
|
||||
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||
end
|
||||
|
||||
local status, error = pcall(function ()
|
||||
local display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||
config_view(display)
|
||||
|
||||
while true do
|
||||
local event, param1, param2, param3 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
if event == "timer" then
|
||||
tcd.handle(param1)
|
||||
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
|
||||
local m_e = core.events.new_mouse_event(event, param1, param2, param3)
|
||||
if m_e then display.handle_mouse(m_e) end
|
||||
elseif event == "char" or event == "key" or event == "key_up" then
|
||||
local k_e = core.events.new_key_event(event, param1, param2)
|
||||
if k_e then display.handle_key(k_e) end
|
||||
elseif event == "paste" then
|
||||
display.handle_paste(param1)
|
||||
end
|
||||
|
||||
if event == "terminate" then return end
|
||||
end
|
||||
end)
|
||||
|
||||
-- restore colors
|
||||
for i = 1, #style.colors do
|
||||
local r, g, b = term.nativePaletteColor(style.colors[i].c)
|
||||
term.setPaletteColor(style.colors[i].c, r, g, b)
|
||||
end
|
||||
|
||||
reset_term()
|
||||
if not status then
|
||||
println("configurator error: " .. error)
|
||||
end
|
||||
|
||||
return status, error
|
||||
end
|
||||
|
||||
return configurator
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user