Compare commits

...

555 Commits

Author SHA1 Message Date
c9e9e514ca 更新 ccmsi.lua
Some checks failed
Lua Checks / check (push) Has been cancelled
Deploy Installation Data / deploy (push) Has been cancelled
2025-11-20 00:14:28 +08:00
e247135d31 更新 ccmsi.lua
Some checks failed
Lua Checks / check (push) Has been cancelled
Deploy Installation Data / deploy (push) Has been cancelled
2025-11-20 00:12:56 +08:00
Mikayla
c6d526163f Create CONTRIBUTING.md 2025-10-06 11:50:05 -04:00
Mikayla
6e7c843258 Merge pull request #631 from MikaylaFischler/devel
2025.09.14 Release
2025-09-13 16:10:00 -04:00
Mikayla Fischler
f7fe9754fe #629 luacheck fix for remaining unused variables 2025-09-13 16:07:12 -04:00
Mikayla Fischler
ff68eeae1a #629 removed unused render commands 2025-09-13 16:04:45 -04:00
Mikayla Fischler
20f949a9dd fixed pocket main thread crash on nil current page, added info page to alarm test app 2025-09-13 15:50:25 -04:00
Mikayla
99c9fec195 Merge pull request #628 from MikaylaFischler/pocket-beta-dev
Pocket Beta
2025-09-12 21:56:12 -04:00
Mikayla Fischler
e7a859438e added additional terms to glossary 2025-09-12 21:54:58 -04:00
Mikayla
c81c83f432 Merge pull request #630 from MikaylaFischler/629-pocket-release-cleanup
629 pocket release cleanup
2025-09-12 21:49:26 -04:00
Mikayla Fischler
6ad63aedeb include reordering 2025-09-12 21:49:08 -04:00
Mikayla Fischler
a1bb5ce50b #629 rework of alarm testing app and other cleanup 2025-09-12 21:46:02 -04:00
Mikayla
afc89ac727 #629 code cleanup 2025-09-13 00:55:15 +00:00
Mikayla Fischler
55685fb6a6 #629 home page cleanup 2025-09-12 20:24:42 -04:00
Mikayla Fischler
61f1af7f4e #403 resolved todos in documentation 2025-09-12 20:20:11 -04:00
Mikayla Fischler
16f62bc32a pocket version update 2025-09-12 19:54:51 -04:00
Mikayla Fischler
a2ec418d53 luacheck fix 2025-09-12 19:52:58 -04:00
Mikayla Fischler
384ebb461f #622 comment change and version increment 2025-09-12 19:50:43 -04:00
Mikayla Fischler
0392385037 #622 reinforced induction matrix support 2025-09-12 19:45:15 -04:00
Mikayla Fischler
9e020b2852 #403 remaining documentation 2025-09-12 19:24:29 -04:00
Mikayla Fischler
4f7285573f #403 work on configuration guide 2025-09-12 00:00:01 -04:00
Mikayla Fischler
59f99f70a4 #403 basic connections guide 2025-09-11 23:39:06 -04:00
Mikayla Fischler
df9f1195e3 #403 fixed scroll bar sticking around sometimes 2025-09-11 22:30:39 -04:00
Mikayla Fischler
aba79e88cf #403 auto control usage guide 2025-09-11 22:28:58 -04:00
Mikayla
d0276e149b #400 revert changes 2025-09-12 01:22:27 +00:00
Mikayla Fischler
eb95f2331d #403 waste control doc updates 2025-09-02 14:33:10 -04:00
Mikayla
28150042cc #403 waste control usage documentation 2025-09-02 15:38:43 +00:00
Mikayla
4bc5af46ab #403 spelling fixes 2025-09-02 14:17:43 +00:00
Mikayla Fischler
2cee1ea895 #403 start of auto and waste control docs 2025-09-01 23:06:13 -04:00
Mikayla Fischler
7d0bbafd6c #403 manual control guide 2025-09-01 19:09:49 -04:00
Mikayla Fischler
dc19127836 #403 guide loading detailed info 2025-09-01 18:18:51 -04:00
Mikayla Fischler
6db6a7d7b7 #403 flow display documentation 2025-09-01 17:54:21 -04:00
Mikayla Fischler
017deec06e #403 unit display documentation 2025-09-01 17:05:31 -04:00
Mikayla Fischler
d0401fe51f #403 main display documentation 2025-09-01 16:37:21 -04:00
Mikayla Fischler
92113671ff #403 front panel documentation complete 2025-08-24 19:33:31 -04:00
Mikayla Fischler
e3d0692dcc #400 start of peripherals list app 2025-08-24 18:46:07 -04:00
Mikayla Fischler
83e29abea7 #401 bug fixes 2025-08-19 22:14:19 -04:00
Mikayla Fischler
4a1730ec47 #401 cleanup and global RTT limit constants 2025-08-19 18:43:43 -04:00
Mikayla Fischler
691b781c52 #401 slowed polling rate for computer app 2025-08-19 18:42:48 -04:00
Mikayla Fischler
3b856655c3 #401 comment updates 2025-08-13 11:46:26 -03:00
Mikayla Fischler
415cb71294 #401 working RTU and PKT computer lists 2025-08-13 11:32:49 -03:00
Mikayla Fischler
a678b8dbe0 #401 fix for supervisor coordinator RTT reporting 2025-08-13 11:17:30 -03:00
Mikayla Fischler
9bb3f59496 reworked pocket app loader 2025-08-13 11:15:33 -03:00
Mikayla Fischler
170cba702c PSIL updates stored value before notifying subscribers 2025-08-13 10:46:09 -03:00
Mikayla Fischler
fe78360948 #401 working main page of computer list app 2025-08-13 10:45:32 -03:00
Mikayla
e29a88eeea #401 sidebar updates 2025-08-06 16:00:28 +00:00
Mikayla
f3eb6d0464 #401 PLC, RTU, PKT computer lists 2025-08-06 15:56:06 +00:00
Mikayla
f4d4de659c #401 supervisor and coordinator computer display 2025-08-05 13:35:29 +00:00
Mikayla
1ce0bbfc65 #401 work on pocket computer list app 2025-08-01 21:43:19 +00:00
Mikayla
0bfe767710 #401 pocket handling of computer data 2025-08-01 21:41:40 +00:00
Mikayla
4fb39213f2 #401 supervisor support of pocket computer list app 2025-07-01 14:41:09 +00:00
Mikayla
6eb9ac5845 Merge pull request #626 from MikaylaFischler/devel
2025.06.29 Release
2025-06-29 16:48:45 -04:00
Mikayla Fischler
acaa9369f4 added new! tags to reactor PLC configurator 2025-06-29 16:32:40 -04:00
Mikayla Fischler
2998371b89 #623 PLC option to invert emergency coolant control 2025-06-27 11:46:50 -04:00
Mikayla
12664c6190 Merge pull request #624 from MikaylaFischler/pocket-alpha-dev
Radiation App
2025-06-21 14:59:46 -04:00
Mikayla Fischler
abe0c45534 cleanup, #593 fixes, version increment 2025-06-21 14:58:45 -04:00
Mikayla Fischler
a104d8ba83 #593 radiation app fixes/cleanup 2025-06-21 10:45:43 -04:00
Mikayla
a629c04d11 #593 radiation monitor lists 2025-06-16 19:21:16 +00:00
Mikayla
6d3b35a41d #621 support environment_detector peripheral type 2025-06-16 15:07:56 +00:00
Mikayla Fischler
e1ac42f5f8 #593 structure and graphics elements for the radiation monitor app 2025-06-15 18:24:36 -04:00
Mikayla
9e59883a84 #593 radiation monitor data comms 2025-06-03 14:10:30 +00:00
Mikayla Fischler
79d63fce78 #593 start of radiation monitor app 2025-05-24 17:17:57 -04:00
Mikayla Fischler
ce92fd15ef Merge branch 'devel' into pocket-alpha-dev 2025-05-10 17:51:17 -04:00
Mikayla
919ca6f0af Merge pull request #620 from MikaylaFischler/devel
2025.05.10 Release
2025-05-10 17:45:19 -04:00
Mikayla Fischler
264edc0030 #592 fixed bug with pocket help page linking navigation 2025-05-10 17:30:54 -04:00
Mikayla Fischler
fcb17ae5e7 show "new!" next to new config fields 2025-05-10 11:26:19 -04:00
Mikayla Fischler
35f82af2e2 fixed CONFIG button coloring 2025-05-10 11:25:52 -04:00
Mikayla
1a2ecd0599 Merge pull request #619 from MikaylaFischler/rtu-redstone-enhancements
604 redstone relay integration
2025-05-09 11:41:53 -04:00
Mikayla
5f8c947105 cleanup and fixes 2025-05-09 15:41:14 +00:00
Mikayla Fischler
41e6d89a4b incremented comms version for RTU advertisement changes 2025-05-07 20:06:04 -04:00
Mikayla Fischler
f01fb62863 #604 updated emergency coolant annunciator logic 2025-05-07 20:05:03 -04:00
Mikayla Fischler
8f6425b814 #604 fixed supervisor bugs with new redstone 2025-05-07 20:04:39 -04:00
Mikayla Fischler
069a7ce0ad #604 front panel updates and hw state tracking fixes 2025-05-07 20:03:48 -04:00
Mikayla Fischler
8eff1c0d76 #604 refresh connections count on saving an interface 2025-05-07 20:03:20 -04:00
Mikayla Fischler
7404e6da31 #604 updated self check for relays and added duplicate input detection 2025-05-07 11:48:32 -04:00
Mikayla Fischler
12ead136a3 #604 configuration of redstone RTUs 2025-05-07 11:27:53 -04:00
Mikayla Fischler
e3dbda3c54 fixed logic for duplicate input detection 2025-05-07 10:42:52 -04:00
Mikayla
41b6a558d5 init RTU gateway UI after checking for modem to prevent that failure making a UI mess 2025-05-05 16:35:44 +00:00
Mikayla Fischler
07bb0f13e3 Merge branch 'devel' into rtu-redstone-enhancements 2025-05-03 09:54:29 -04:00
Mikayla
f1014ce941 Merge pull request #618 from MikaylaFischler/572-audible-alarms-on-facility-radiation
572 audible alarms on facility radiation
2025-04-30 10:27:08 -04:00
Mikayla Fischler
0debbdc167 fixed facility not scram'ing on radiation 2025-04-30 10:25:30 -04:00
Mikayla Fischler
0a26629e20 fixed no_ring_back going to RING_BACK if leaving TRIPPED not ACKED 2025-04-30 10:17:28 -04:00
Mikayla Fischler
9393b1830d restored use of self for unit logic function calls 2025-04-30 10:17:09 -04:00
Mikayla
0df1e48780 reorganized unit logic inclusion to work like facility update 2025-04-29 20:11:12 +00:00
Mikayla
b8c30ba8a4 cleanup 2025-04-29 19:47:29 +00:00
Mikayla
eafd39fa35 #572 added facility radiation alarm 2025-04-29 19:41:52 +00:00
Mikayla
86dc92f09a Merge branch 'devel' into pocket-alpha-dev 2025-04-29 14:33:05 +00:00
Mikayla
e6f5ab8ef4 #604 reworked supervisor redstone RTU interface 2025-04-29 02:38:42 +00:00
Mikayla
be462db50b #604 new redstone initialization logic 2025-04-29 01:44:52 +00:00
Mikayla Fischler
1dc3d82e59 #604 work on redstone RTU rework 2025-04-27 22:40:42 -04:00
Mikayla Fischler
fa2a6d7786 Merge branch 'devel' into rtu-redstone-enhancements 2025-04-26 15:30:31 -04:00
Mikayla Fischler
04c53c7074 #616 pocket pellet color options 2025-04-26 15:24:50 -04:00
Mikayla Fischler
1af2cdba8d #616 fixes to coordinator pellet color option 2025-04-26 15:21:09 -04:00
Mikayla Fischler
0d7302dc8e #604 start of total rework of redstone RTUs for relay functionalitiy 2025-04-21 22:18:09 -04:00
Mikayla Fischler
48ec973695 #616 added pellet color option to the coordinator 2025-04-21 22:13:58 -04:00
Mikayla Fischler
ee868eb607 #616 updated flow view to match fluid colors not pellet colors 2025-04-20 21:36:40 -04:00
Mikayla
e4da9a62d9 Merge pull request #617 from MikaylaFischler/rtu-redstone-enhancements
redstone invert
2025-04-20 17:41:26 -04:00
Mikayla Fischler
c8910bfc40 clear inverted on analog creation 2025-04-20 17:40:40 -04:00
Mikayla Fischler
d6e3a67562 #484 redstone inversion support 2025-04-20 17:36:16 -04:00
Mikayla Fischler
f7c0a1d97d #484 work on redstone inversion 2025-04-20 11:28:02 -04:00
Mikayla
13509136b8 cleanup 2025-04-19 22:20:57 +00:00
Mikayla Fischler
bfab2d6af2 #583 fixes for pocket crash display 2025-04-19 18:09:43 -04:00
Mikayla Fischler
ae055a7d99 luacheck fixes and version increment 2025-04-19 00:21:15 -04:00
Mikayla Fischler
592f1110ed define pocket global in craftos-pc environment pocket application 2025-04-19 00:16:43 -04:00
Mikayla Fischler
97875f4e52 #583 graphical crash screens 2025-04-19 00:15:41 -04:00
Mikayla
657261642c Merge pull request #615 from MikaylaFischler/364-rtu-configurator-self-check
364 rtu configurator self check
2025-04-05 21:18:15 -04:00
Mikayla Fischler
0da944c3ea #364 updated scroll height max and reduced duplicate text 2025-04-05 21:17:19 -04:00
Mikayla Fischler
1b692b5b9a #364 fail self check if using a side for both bundled and unbundled redstone 2025-04-05 21:14:33 -04:00
Mikayla Fischler
b4a9366f73 #364 fixes to redstone and peripheral checks 2025-04-05 21:00:16 -04:00
Mikayla Fischler
2b3099ac59 #364 check validity of redstone and peripheral entries and check redstone side/color combos are not repeated 2025-04-04 00:17:40 -04:00
Mikayla Fischler
cd654fb9b8 shorter variable for self.settings in PLC self check 2025-04-03 23:21:31 -04:00
Mikayla Fischler
ad834218c2 #364 check all configured RTU peripherals in self check 2025-04-03 23:21:06 -04:00
Mikayla Fischler
c6a7de2669 #364 base RTU gateway self checks 2025-04-03 23:06:45 -04:00
Mikayla Fischler
d374967cb7 #614 fixed reactor PLC self check when configured as not networked 2025-04-03 22:56:54 -04:00
Mikayla
b1ad2084f2 Merge pull request #610 from MikaylaFischler/devel
2025.02.26 Release
2025-02-26 18:52:56 -05:00
Mikayla Fischler
1971153dae configurator summary enhancements 2025-02-26 18:38:21 -05:00
Mikayla Fischler
5fc8912590 #480 fixed aux coolant connection to boilers with emergency coolant 2025-02-26 13:08:58 -05:00
Mikayla
122fa1a7a7 Merge pull request #609 from MikaylaFischler/480-auxiliarybackup-water-control
480 auxiliary backup water control
2025-02-25 16:44:23 -05:00
Mikayla Fischler
2b73196130 #480 updated aux coolant logic 2025-02-25 16:43:03 -05:00
Mikayla Fischler
d45f19c8a6 refactor 2025-02-25 15:32:07 -05:00
Mikayla Fischler
a9f68ce3ea Merge branch 'devel' into 480-auxiliarybackup-water-control 2025-02-25 14:53:44 -05:00
Mikayla Fischler
7ab5ea710f additional supervisor config validations 2025-02-25 14:52:05 -05:00
Mikayla Fischler
de41ee56aa #480 auxiliary water coolant 2025-02-25 14:33:25 -05:00
Mikayla Fischler
99ea59a86b #526 coordinator front panel scale to term size 2025-02-16 13:32:08 -05:00
Mikayla Fischler
234652b886 #526 cleanup 2025-02-16 13:21:00 -05:00
Mikayla Fischler
e37e3ba696 #526 supervisor front panel scale to term size 2025-02-16 13:20:23 -05:00
Mikayla Fischler
20b71bead1 #526 RTU gateway front panel scale to term size 2025-02-16 12:51:10 -05:00
Mikayla Fischler
18d093e72d #526 reactor PLC front panel scale to term size 2025-02-16 12:34:06 -05:00
Mikayla Fischler
21eae4932f #607 updated deny message 2025-02-16 11:54:45 -05:00
Mikayla Fischler
9163fb14c4 RTU gateway version increment 2025-02-16 11:45:26 -05:00
Mikayla Fischler
02db01524c Merge branch 'devel' of github.com:MikaylaFischler/cc-mek-scada into devel 2025-02-16 11:44:44 -05:00
Mikayla Fischler
e0d1eb3445 #608 fixed front panel network lights 2025-02-16 11:44:30 -05:00
Mikayla Fischler
7c22c172d5 #607 deny reactor PLC with index out of range 2025-02-16 11:43:32 -05:00
Mikayla
7b29702000 #480 auxiliary coolant control logic 2025-02-11 22:42:52 +00:00
Mikayla
425a6c8775 #480 added auxiliary coolant redstone output 2025-02-11 22:42:07 +00:00
Mikayla
eafcd89aba updated SNA RTU note after #564's changes 2025-02-11 22:40:17 +00:00
Mikayla Fischler
016cd988e1 #564 improved SNA statistic clarity 2025-02-09 16:17:37 -05:00
Mikayla
06a8e3d9ca Merge pull request #603 from MikaylaFischler/589-reboot-recovery
589 reboot recovery
2025-02-09 15:25:48 -05:00
Mikayla Fischler
5f22069ce1 #589 cleanup and fixes 2025-02-09 14:19:06 -05:00
Mikayla Fischler
ecdaf78ed0 #589 moved boot recovery to facility update file 2025-02-09 13:48:20 -05:00
Mikayla Fischler
3b2fb00285 cleanup 2025-02-09 13:37:22 -05:00
Mikayla Fischler
54167e2113 #589 only scram reactor on plc boot if networked 2025-02-09 13:13:18 -05:00
Mikayla Fischler
22cdbc8638 #589 supervisor control reboot recovery 2025-02-09 13:07:36 -05:00
Mikayla Fischler
556331f75b better unit ready check 2025-02-09 13:07:01 -05:00
Mikayla Fischler
40cb9f599a #602 only auto reset units that should be 2025-02-09 13:06:44 -05:00
Mikayla Fischler
cab3427c70 #601 only reset on timeout once per unit per supervisor boot 2025-02-09 12:10:13 -05:00
Mikayla Fischler
4e31b33b09 #601 reset RPS if the triggering condition is a timeout on PLC session establish 2025-02-09 11:59:03 -05:00
Mikayla Fischler
f32855084e #589 WIP reboot recovery 2025-02-08 22:20:00 -05:00
Mikayla
b3cf40a01a #589 initial attempt at reboot recovery 2025-02-08 20:35:04 +00:00
Mikayla
cf9e26ac8f Merge pull request #599 from MikaylaFischler/devel
Pocket Beta Release
2025-01-27 12:52:32 -05:00
Mikayla
cbc84c5998 Merge pull request #598 from MikaylaFischler/559-modbus-device-busy-unrecoverable
559 modbus device busy unrecoverable
2025-01-27 11:49:50 -05:00
Mikayla Fischler
869e67710f #559 supervisor bugfix 2025-01-26 14:49:44 -05:00
Mikayla Fischler
1b9d3d3f23 Merge branch 'devel' into 559-modbus-device-busy-unrecoverable 2025-01-26 12:05:42 -05:00
Mikayla
0a060b656c Merge pull request #595 from MikaylaFischler/pocket-alpha-dev
Pocket Alpha
2025-01-20 17:18:01 -05:00
Mikayla Fischler
c859c22964 cleanup 2025-01-20 17:01:49 -05:00
Mikayla Fischler
3767c0f8d9 luacheck fixes and coordinator version bump 2025-01-20 16:26:41 -05:00
Mikayla Fischler
fbebc2a021 prep for beta 2025-01-20 16:24:18 -05:00
Mikayla Fischler
afd6800be6 updated pocket version 2025-01-20 15:40:00 -05:00
Mikayla Fischler
baba2e1411 #557 facility app data and fixes 2025-01-20 15:38:53 -05:00
Mikayla Fischler
127c878794 #557 facility app ui design complete 2025-01-20 12:21:51 -05:00
Mikayla
767b54c3e6 #557 facility tank overview page 2025-01-15 22:49:55 +00:00
Mikayla Fischler
1c57fc1fe3 #557 work on facility app 2025-01-11 11:57:28 -05:00
Mikayla Fischler
2d83de8b88 moved ETA string generation to icontrol 2025-01-11 11:57:06 -05:00
Mikayla Fischler
4a4234c8c8 #557 ui improvements 2025-01-10 22:52:27 -05:00
Mikayla
eb197e7fdd updated dynamic tank page to indicate which tank it is 2025-01-09 23:51:02 +00:00
Mikayla
78b0e1bf24 #557 facility app and induction matrix updates 2025-01-09 23:50:47 +00:00
Mikayla Fischler
cbc004a6c7 #557 induction matrix page updates 2025-01-08 22:49:05 -05:00
Mikayla Fischler
d05abf6e00 #557 facility app and sps page fixes 2025-01-08 21:54:45 -05:00
Mikayla
813e30bcde Merge branch 'devel' into pocket-alpha-dev 2025-01-09 00:17:22 +00:00
Mikayla
fb139949f8 fix to induction matrix transfer bars not rescaling with capacity changes 2025-01-09 00:17:00 +00:00
Mikayla
2fdc9feea7 #557 work on induction matrix page 2025-01-09 00:15:12 +00:00
Mikayla
eabb065d17 #557 ui cleanup on sps page 2025-01-09 00:14:49 +00:00
Mikayla
fb221a566c #557 facility app bug fix 2025-01-09 00:14:28 +00:00
Mikayla Fischler
cd4caf0163 #559 supervisor updates to handle busy errors 2025-01-08 19:07:53 -05:00
Mikayla Fischler
1190fe2dd5 #559 discard modbus messages if busy 2025-01-08 19:04:38 -05:00
Mikayla
4cb6f9ca0f #557 work on message data 2025-01-07 23:21:48 +00:00
Mikayla
872082b970 #557 sps page 2025-01-07 23:21:29 +00:00
Mikayla Fischler
071df9e431 #557 include matrix page 2025-01-05 15:08:41 -05:00
Mikayla Fischler
ae85cfc579 #557 start of induction matrix and sps pages 2025-01-05 15:05:01 -05:00
Mikayla Fischler
1dece587b2 cleanup 2025-01-05 14:40:36 -05:00
Mikayla Fischler
01c5d62f38 #557 skeleton of facility app with some pages 2025-01-05 14:39:16 -05:00
Mikayla
c6a5d487e0 comment updates and refactors 2025-01-04 15:33:57 +00:00
Mikayla
ba4a5aa85e #557 work on facility app 2025-01-04 15:33:37 +00:00
Mikayla
451232ce91 Merge pull request #586 from MikaylaFischler/devel
2024.12.21 Release
2024-12-21 12:30:33 -05:00
Mikayla Fischler
11fa9f625d #587 bumped up ccmsi version for release after testing 2024-12-21 12:14:17 -05:00
Mikayla Fischler
b61fd2c620 #479 updated comms protocol versions for sodium emergency coolant changes 2024-12-21 12:13:51 -05:00
Mikayla Fischler
cb2ebd409d #588 close main UI via queue 2024-12-21 11:54:25 -05:00
Mikayla Fischler
57c75be997 bump up versions 2024-12-21 11:48:35 -05:00
Mikayla Fischler
22a7fdae88 #587 ccmsi autodetect app 2024-12-21 11:46:52 -05:00
Mikayla Fischler
5a9768f005 configurator warning updates 2024-12-21 11:07:12 -05:00
Mikayla
fd414a814c Merge pull request #585 from MikaylaFischler/479-sodium-emergency-coolant
479 Sodium Emergency Coolant
2024-12-20 20:47:36 -05:00
Mikayla Fischler
909bd78912 Merge branch 'devel' into 479-sodium-emergency-coolant 2024-12-20 20:46:48 -05:00
Mikayla Fischler
c487b22fe1 cleanup 2024-12-20 20:45:57 -05:00
Mikayla Fischler
9892fbc602 luacheck fixes 2024-12-20 17:30:26 -05:00
Mikayla Fischler
1695b58329 #584 removed test code 2024-12-20 17:27:50 -05:00
Mikayla Fischler
178681941f #584 logging improvements 2024-12-20 17:26:49 -05:00
Mikayla Fischler
de3fa163c5 #479 fixed dynamic tank fill color in pocket 2024-12-20 17:20:56 -05:00
Mikayla Fischler
68977bcdea bump versions for previous commit 2024-12-20 12:46:01 -05:00
Mikayla Fischler
c4c45ae329 #574 additional fixes and log cleanup 2024-12-20 12:44:39 -05:00
Mikayla Fischler
e8b8dfde5b shorter log message timestamps 2024-12-20 12:43:19 -05:00
Mikayla Fischler
3f42adea5b #479 fixes and emphasis on needing to keep supervisor and coordinator unit counts in sync 2024-12-20 12:42:45 -05:00
Mikayla Fischler
feabed6a1e #479 fixed configurator tank summary 2024-12-19 20:10:57 -05:00
Mikayla
ffd4bae2d5 #479 work on sodium emergency coolant config and ui 2024-12-20 00:57:25 +00:00
Mikayla Fischler
bc4228d4eb #479 WIP sodium emergency coolant fixes 2024-12-18 21:47:16 -05:00
Mikayla
4501cb783f #479 sodium emergency coolant 2024-12-19 00:58:53 +00:00
Mikayla Fischler
78225a8cf4 #574 ignore failure to check formed on disconnected devices 2024-12-13 17:36:32 -05:00
Mikayla Fischler
9b443709f4 #539 fixed child ID map not being correct under specific circumstances 2024-12-13 17:16:25 -05:00
Mikayla
33803a1ace Merge pull request #582 from MikaylaFischler/pocket-alpha-dev
Unit Dynamic Tank View
2024-12-12 20:07:54 -05:00
Mikayla Fischler
fe8ac349d6 bump coordinator version 2024-12-12 20:07:08 -05:00
Mikayla Fischler
1538fb3d26 comment fix 2024-12-12 20:01:32 -05:00
Mikayla Fischler
a546b946ee #556 reworded fill mode text 2024-12-12 19:21:00 -05:00
Mikayla
019284de7b #574 possible fix for RTU formed checking 2024-12-12 03:18:21 +00:00
Mikayla
849caa2521 #575 ensure max burn is a number for multiplying 2024-12-10 23:22:15 +00:00
Mikayla
6838d21bd7 #581 fixed peripheral/redstone saving behavior in RTU configurator 2024-12-10 15:07:43 +00:00
Mikayla
6bd43af5c0 missing fields fixes 2024-12-10 14:57:16 +00:00
Mikayla
7eebf0524f cleaned up state style definitions 2024-12-10 04:34:49 +00:00
Mikayla
20bffec79f reworked computed status logic and handle dynamic tank data 2024-12-10 04:31:53 +00:00
Mikayla
49b545ba2c diagnostic disables 2024-12-10 04:17:30 +00:00
Mikayla
e54ecf43ed type and psil updates 2024-12-10 03:43:23 +00:00
Mikayla Fischler
0544587d84 #556 ui for dynamic tank view in unit apps 2024-11-29 15:36:13 -05:00
Mikayla
72fcc01acd #556 WIP dynamic tank views in unit app 2024-11-29 19:33:19 +00:00
Mikayla
c6343e5956 Merge pull request #579 from MikaylaFischler/devel
2024.11.21 Release
2024-11-21 18:40:52 -05:00
Mikayla Fischler
7372908637 updated ccmsi version 2024-11-21 11:35:49 -05:00
Mikayla Fischler
50b2f62c66 #578 don't allow bundled analog I/O 2024-11-19 22:28:08 -05:00
Mikayla Fischler
68851a6b30 visually disable disabled checkboxes 2024-11-19 22:24:37 -05:00
Mikayla
56e4f93db8 Merge pull request #577 from MikaylaFischler/pocket-alpha-dev
Waste App
2024-11-19 21:24:08 -05:00
Mikayla Fischler
bc7a38b9d4 luacheck fix 2024-11-19 21:22:07 -05:00
Mikayla Fischler
8bdb6b9ed6 cleanup 2024-11-19 21:21:05 -05:00
Mikayla Fischler
8469bb78a3 luacheck fixes 2024-11-18 23:55:17 -05:00
Mikayla Fischler
fc603677ef #399 finished waste app indicators 2024-11-18 23:50:34 -05:00
Mikayla Fischler
532c15e258 #399 auto waste control 2024-11-17 23:07:58 -05:00
Mikayla Fischler
7b6b1de539 #399 working unit data updating and unit waste control 2024-11-17 19:46:04 -05:00
Mikayla Fischler
edde416889 #576 fixed incorrect SNA output rate 2024-11-17 19:35:34 -05:00
Mikayla Fischler
8fad94c4c6 #399 unit waste data updating 2024-11-17 18:22:40 -05:00
Mikayla Fischler
3e1f567c0f #399 added a page for SNA info, added unit waste stats 2024-11-17 17:01:01 -05:00
Mikayla Fischler
bafd20ec22 #399 most of pocket waste UI 2024-11-13 22:54:53 -05:00
Mikayla Fischler
21591f4d7d #399 work on pocket waste control 2024-11-10 22:43:20 -05:00
Mikayla Fischler
b15835ab87 Merge branch 'devel' into pocket-alpha-dev 2024-11-09 12:56:36 -05:00
Mikayla Fischler
d36f7adab1 #573 fix to install 2024-11-09 12:10:50 -05:00
Mikayla Fischler
8439e02586 #535 added startup button to configurators 2024-11-09 11:56:56 -05:00
Mikayla
764638c212 #535 updates to configurator launcher 2024-11-09 06:01:37 +00:00
Mikayla
459ddbaef8 #573 don't require and install to update the installer, cleanup 2024-11-09 04:01:17 +00:00
Mikayla Fischler
627dd99dd7 #566 fixes for matrix fault logic 2024-11-07 22:13:03 -05:00
Mikayla
129bf8809a Merge branch 'devel' of https://github.com/MikaylaFischler/cc-mek-scada into devel 2024-11-08 02:52:22 +00:00
Mikayla
55f6e4756e #566 interrupt auto control on unformed/faulted induction matrix 2024-11-08 02:52:17 +00:00
Mikayla Fischler
e27d5eeb85 #571 fix matrix dc 2024-11-07 21:45:15 -05:00
Mikayla Fischler
661bef063c safemin update for @as 2024-11-07 21:44:34 -05:00
Mikayla
801fd99448 #571 still check for critical unit alarms and facility radiation when induction matrix is disconnected 2024-11-07 16:46:38 +00:00
Mikayla
c1c3723b67 #567 bump supervisor version 2024-11-07 16:45:53 +00:00
Mikayla
7fb88becb8 #567 detect and report ramping in max burn and burn rate modes 2024-11-07 15:01:19 +00:00
Mikayla
051d119b99 #562 delay opening guide page until loaded 2024-11-07 14:40:26 +00:00
Mikayla Fischler
21a3a18764 Merge branch 'devel' into pocket-alpha-dev 2024-10-19 14:02:08 -04:00
Mikayla Fischler
91cb51bad9 minifier fix to allow @as type hints 2024-10-19 13:58:56 -04:00
Mikayla
7130176781 Merge pull request #563 from MikaylaFischler/devel
2024.10.18 Release
2024-10-18 20:34:14 -04:00
Mikayla
8ddc233da0 #399 pocket waste control comms commands 2024-10-18 02:35:48 +00:00
Mikayla Fischler
e847505ac2 bump installer version from letter 2024-10-17 22:23:21 -04:00
Mikayla Fischler
0b14a01784 #569 mitigate Windows being a case insensitive os 2024-10-17 22:01:50 -04:00
Mikayla Fischler
b7969d2cd7 encourage updating installer 2024-10-16 22:59:44 -04:00
Mikayla Fischler
9ecff2fa2b #568 significantly improved out of space handling in ccmsi 2024-10-16 22:43:01 -04:00
Mikayla Fischler
dbe5ee1f54 comms version increment 2024-10-14 13:40:27 -04:00
Mikayla Fischler
7bb49c51c8 version increments 2024-10-14 13:39:37 -04:00
Mikayla
620fa362f6 Merge pull request #560 from MikaylaFischler/pocket-alpha-dev
Pocket Process Control
2024-10-14 13:24:45 -04:00
Mikayla Fischler
0639870410 comments and cleanup 2024-10-14 13:23:42 -04:00
Mikayla Fischler
440989aed6 comments and removed unused variables 2024-10-14 12:12:35 -04:00
Mikayla
48e5c50f0a Merge pull request #561 from Toby222/devel
fix typo
2024-10-14 12:08:11 -04:00
Tobias Berger
c780cd2664 fix typo 2024-10-14 09:39:08 +02:00
Mikayla Fischler
60ff22f57d better process start check + logging 2024-10-14 01:20:52 -04:00
Mikayla Fischler
c0b7d7e13c #398 process UI fixes 2024-10-14 01:20:15 -04:00
Mikayla Fischler
30f37c0ef9 added numeric value accessor to number field 2024-10-14 01:19:56 -04:00
Mikayla Fischler
4bd64e71bf number field enforce limits on set 2024-10-14 00:52:28 -04:00
Mikayla Fischler
da87745996 #398 #355 pocket app data updating 2024-10-14 00:10:25 -04:00
Mikayla Fischler
8b9f83754b number fields now display numbers cleanly without using scientific notation 2024-10-14 00:02:52 -04:00
Mikayla Fischler
40e749d363 added some style type aliases 2024-10-13 19:46:36 -04:00
Mikayla Fischler
38a1a4282c #398 #355 pocket process control UI 2024-10-12 15:30:14 -04:00
Mikayla
41843a2478 Merge branch 'devel' into pocket-alpha-dev 2024-10-12 04:40:27 +00:00
Mikayla
75a3b82f31 #398 #355 start of pocket process control app 2024-10-12 04:39:45 +00:00
Mikayla
eae2dfef60 pocket process cleanup 2024-10-12 04:37:02 +00:00
Mikayla
26906d10d6 added go_home alias function for pocket navigation 2024-10-12 04:36:20 +00:00
Mikayla Fischler
89ab742f8e #528 configurator fixes, restoring textbox whitespace handling and adding specific trim whitespace option 2024-10-12 00:30:58 -04:00
Mikayla
10b675d84d #398 coordinator pocket process command support 2024-10-12 04:14:05 +00:00
Mikayla
0497ec44e9 #528 coordinator configurator cleanup 2024-10-08 18:57:05 +00:00
Mikayla
eea3a8f7d0 #528 refactored facility svr page prefixes to fac in supervisor configurator 2024-10-07 16:51:27 +00:00
Mikayla
46d19c180b #528 supervisor configurator cleanup 2024-10-07 16:47:34 +00:00
Mikayla
8428b68f77 #528 indentation fix 2024-10-07 16:35:50 +00:00
Mikayla Fischler
f61791427d #554 fixed app loading multiple times 2024-10-06 21:16:25 -04:00
Mikayla Fischler
acb5a1cbf9 #555 fixed sidebar bug caused by #552 2024-10-06 21:14:39 -04:00
Mikayla
393be2acec Merge pull request #553 from MikaylaFischler/pocket-alpha-dev
Pocket Configurator and Control Updates
2024-10-06 18:55:39 -04:00
Mikayla Fischler
4e5858bd2d optimizations on data handling in pocket and computer for internally loading reactor data structures 2024-10-06 18:21:04 -04:00
Mikayla Fischler
f1a13f1125 updated color accessibility note in coordinator config 2024-10-04 21:16:30 -04:00
Mikayla Fischler
65609ddaa2 cleanup 2024-10-04 21:13:46 -04:00
Mikayla Fischler
8238b26eec comments and version increments 2024-10-04 21:12:37 -04:00
Mikayla Fischler
9521acd8af Merge branch 'pocket-alpha-dev' of github.com:MikaylaFischler/cc-mek-scada into pocket-alpha-dev 2024-10-04 21:09:29 -04:00
Mikayla
749c84490b #528 moved pocket config folder 2024-10-04 16:38:34 +00:00
Mikayla
519fae3a27 #528 pocket configurator cleanup 2024-10-04 16:33:28 +00:00
Mikayla
2ccba197c7 updated cfg type annotations 2024-10-04 16:31:49 +00:00
Mikayla Fischler
6a04354964 fixes for control app data updating 2024-10-03 22:56:38 -04:00
Mikayla
60c4cc2f80 left align search result buttons 2024-10-04 02:33:39 +00:00
Mikayla
966ca94775 added control app update message for better performance 2024-10-04 02:33:26 +00:00
Mikayla
35bbd14cbc #552 only modify sidebar if app is open 2024-10-01 16:35:04 +00:00
Mikayla
316dc5819f removed unused python import 2024-09-30 20:53:31 +00:00
Mikayla
4b188bef8f #528 RTU gateway configurator cleanup 2024-09-30 20:53:19 +00:00
Mikayla Fischler
00157cc45e #549 always display tmp_cfg peripherals and redstone and toggle enable of revert/apply based on detected changes 2024-09-29 19:24:06 -04:00
Mikayla Fischler
499ec7c5b0 fixed configurator section titles not being offset 2024-09-29 11:55:53 -04:00
Mikayla Fischler
cc3b04a184 increment graphics version 2024-09-28 18:38:27 -04:00
Mikayla
85df0d61d5 #539 possible fixes for rare listbox issues 2024-09-28 22:35:22 +00:00
Mikayla
4e3330d4b3 Merge pull request #548 from MikaylaFischler/updated-annotations
Updated Annotations
2024-09-28 18:24:08 -04:00
Mikayla Fischler
a873c921c0 fixed sidebar require capitalization 2024-09-28 18:20:16 -04:00
Mikayla Fischler
7b3147008e renamed PipeNet to PipeNetwork 2024-09-28 18:05:35 -04:00
Mikayla
2579feaf32 corrected some types 2024-09-26 21:26:47 +00:00
Mikayla
17e53fdba2 updated type hints for remaining graphics_element generic types 2024-09-26 21:23:50 +00:00
Mikayla
ec1fc13ae7 logger optimizations 2024-09-26 21:01:34 +00:00
Mikayla
69855af861 updated type hints and comments 2024-09-26 21:00:57 +00:00
Mikayla
e4cb1f6c70 luacheck fix 2024-09-25 21:26:07 +00:00
Mikayla
bc2ae291a7 consistent checkbox capitalization 2024-09-25 21:23:57 +00:00
Mikayla
a4a59d4a3d specific graphics element types rather than graphics_element 2024-09-25 21:21:12 +00:00
Mikayla
741dd2467f bump up graphics version 2024-09-25 01:25:20 +00:00
Mikayla
2b2ca237cb move on_response/callback_response element functions to HazardButton elements only 2024-09-25 01:23:58 +00:00
Mikayla Fischler
6acd6b161c renamed SpinboxNumeric to NumericSpinbox 2024-09-24 21:09:20 -04:00
Mikayla Fischler
a1efca3fc8 fixed incorrect require for switchbutton 2024-09-24 21:07:07 -04:00
Mikayla
b766488dea added see reference to Window.redraw 2024-09-25 01:02:07 +00:00
Mikayla
851d481b76 #544 #545 updates to graphics indicator elements 2024-09-25 01:01:38 +00:00
Mikayla Fischler
2becaeccd7 element description updates 2024-09-21 23:02:46 -04:00
Mikayla Fischler
d41eb3aaeb updated require paths for renamed element files 2024-09-21 22:59:23 -04:00
Mikayla Fischler
0daf314918 #544 #545 updates to graphics animations, controls, and form elements 2024-09-21 22:49:36 -04:00
Mikayla Fischler
b15c60fdab animations renamining 2024-09-21 19:04:05 -04:00
Mikayla Fischler
7df060e1fb control renaming 2024-09-21 19:03:07 -04:00
Mikayla Fischler
a80c2a4cc5 form field renaming 2024-09-21 18:52:00 -04:00
Mikayla Fischler
525330ab59 indicator renaming 2024-09-21 18:51:33 -04:00
Mikayla Fischler
5d1379d60d graphics element renaming 2024-09-21 18:40:00 -04:00
Mikayla Fischler
1d53241b82 #544 #545 work on graphics 2024-09-21 18:38:25 -04:00
Mikayla Fischler
2047794173 more type hints and resolved diagnostic disables with 'as' 2024-09-21 17:58:53 -04:00
Mikayla Fischler
ec2921e393 #545 #544 added cc graphics and peripheral classes 2024-09-14 22:16:12 -04:00
Mikayla Fischler
85fc8d2920 #545 coordinator type annotation updates 2024-09-14 17:07:53 -04:00
Mikayla Fischler
63a9e23b3a #545 pocket type annotation updates 2024-09-14 15:45:36 -04:00
Mikayla Fischler
f1b7bac6f9 #545 plc type annotation updates and refactoring 2024-09-13 22:48:07 -04:00
Mikayla
c3ccd051dc refactored RTU_UNIT_HW_STATE to RTU_HW_STATE 2024-09-13 21:33:41 +00:00
Mikayla
0bf7b8204d #545 rtu gateway type annotation updates 2024-09-13 21:30:46 +00:00
Mikayla
033bcdb9e3 #545 ppm type annotation updates 2024-09-13 21:28:28 +00:00
Mikayla
b2e5ced54d #544 #545 supervisor class and type annotation updates 2024-09-13 21:25:23 +00:00
Mikayla
a1dbc15d16 #545 supervisor type annotation updates 2024-09-13 02:23:16 +00:00
Mikayla
3003e57531 scada-common annotation updates 2024-09-12 17:47:12 +00:00
Mikayla
8e19418701 Merge pull request #547 from MikaylaFischler/devel
2024.09.08 Release
2024-09-11 21:29:36 -04:00
Mikayla Fischler
fb56634fc4 Merge branch 'devel' of github.com:MikaylaFischler/cc-mek-scada into devel 2024-09-11 21:08:28 -04:00
Mikayla Fischler
48fa715aaa incremented util version 2024-09-11 21:08:15 -04:00
Mikayla
753f062bfc #403 doc spelling fix 2024-09-10 19:44:40 +00:00
Mikayla Fischler
356657c9c0 incremented API version 2024-09-08 17:17:30 -04:00
Mikayla
a4452ebbd2 Merge pull request #546 from MikaylaFischler/pocket-alpha-dev
Start of Pocket Controls
2024-09-08 16:54:22 -04:00
Mikayla Fischler
35134822a9 coordinator handle SPS low power ack 2024-09-08 16:49:23 -04:00
Mikayla Fischler
f56d68d972 removed unused iocontrol functions 2024-09-08 16:35:02 -04:00
Mikayla Fischler
06933b2fb7 removed more unused pocket code 2024-09-08 16:27:50 -04:00
Mikayla Fischler
a1494b4afd luacheck fix 2024-09-08 16:11:46 -04:00
Mikayla Fischler
2933b24318 cleanup 2024-09-08 16:05:20 -04:00
Mikayla Fischler
402d8607b6 added AUTO_GROUP enum 2024-09-08 13:26:43 -04:00
Mikayla Fischler
2e978db859 cleanup and version increments 2024-09-08 13:23:37 -04:00
Mikayla Fischler
a7b3a2a0b8 removed unused variables 2024-09-07 23:08:49 -04:00
Mikayla Fischler
6ff096fd31 #498 auto control mode based UI disabling and increased timeouts 2024-09-07 21:39:16 -04:00
Mikayla Fischler
13bb6cb026 #498 fixed wrong facility SCRAM ack 2024-09-07 01:11:13 -04:00
Mikayla Fischler
5b311fcfbc #498 pocket facility scram and ack all alarms 2024-09-06 23:31:01 -04:00
Mikayla Fischler
d6a9f9c5f3 #498 clear requestors on ack 2024-09-06 21:26:41 -04:00
Mikayla Fischler
8ffbbb5ac9 #498 supervisor block disallowed commands based on state, removed unused acks 2024-09-06 21:11:56 -04:00
Mikayla Fischler
bf10b3241e #403 RPS FP indicator doc updates 2024-09-05 22:19:24 -04:00
Mikayla Fischler
ab11ff03b5 #498 functioning pocket manual unit controls 2024-09-05 22:18:59 -04:00
Mikayla Fischler
66fae0695c #498 handle pocket manual unit commands 2024-09-05 22:01:58 -04:00
Mikayla Fischler
dbd79cbc4f #498 coordinator process handle system for manual controls 2024-09-05 21:49:47 -04:00
Mikayla
b5b67b425a #498 work on command acknowledgement handling 2024-09-04 21:12:43 +00:00
Mikayla Fischler
f8bd79a234 #498 work on command handling 2024-09-02 22:25:33 -04:00
Mikayla Fischler
07c3b3ec63 #403 improved guide UI and added supervisor front panel docs 2024-08-31 00:17:39 -04:00
Mikayla Fischler
d7ea68ed3a #403 reactor PLC docs 2024-08-29 22:49:20 -04:00
Mikayla Fischler
db94ac7ff5 #403 section headers and details on RTU front panel 2024-08-29 20:56:20 -04:00
Mikayla Fischler
ee922a3aed #403 fixed section focusing 2024-08-29 19:56:36 -04:00
Mikayla Fischler
75c77cc5b5 #403 weight exact matches over start of key matches 2024-08-28 23:02:08 -04:00
Mikayla Fischler
7683293c5e #403 additional guide section doc types and some more documentation 2024-08-28 22:52:55 -04:00
Mikayla Fischler
672a9c8dd1 Merge branch 'devel' into pocket-alpha-dev 2024-08-28 21:19:39 -04:00
Mikayla
3c10e28d03 #403 guide lists 2024-08-29 01:19:26 +00:00
Mikayla Fischler
035a26cc07 #543 reset remote sequence numbers when linking 2024-08-28 21:01:04 -04:00
Mikayla Fischler
097edc5bf9 adjusted guide section heights and moved process init to have facility access 2024-08-27 23:21:49 -04:00
Mikayla
8a0d05c94b #403 guide additions for front panel docs 2024-08-28 03:12:38 +00:00
Mikayla Fischler
fbbd7e1ccd WIP rearchitecting process command orchestration 2024-08-27 23:05:46 -04:00
Mikayla Fischler
0f40c1d7f2 removed unused set burn ack 2024-08-27 23:03:42 -04:00
Mikayla Fischler
c299dce8ef #498 work on pocket control app and support process code 2024-08-27 23:02:31 -04:00
Mikayla Fischler
11e9c11cf7 GitHub and Discord links in pocket guide 2024-08-27 23:00:29 -04:00
Mikayla Fischler
61ff055d60 allow right alignment for numeric inputs 2024-08-26 20:31:36 -04:00
Mikayla Fischler
f4be6519e8 refactoring and removed unused set_waste_ack 2024-08-26 20:30:30 -04:00
Mikayla
705494bb7e specify python version 2024-08-26 13:55:13 +00:00
Mikayla
610fb12bb3 actions dependency version updates 2024-08-26 13:52:47 +00:00
Mikayla
07406ca5fc Merge pull request #542 from MikaylaFischler/devel
2024.08.25 Release
2024-08-25 22:50:18 -04:00
Mikayla Fischler
6b20445446 added INF tab to supervisor to provide helpful info and removed some redundant alignment specifiers 2024-08-25 22:45:41 -04:00
Mikayla Fischler
f93db02793 incremented common version 2024-08-25 21:29:20 -04:00
Mikayla
fe1b916b1f Merge pull request #541 from MikaylaFischler/pocket-alpha-dev
display pocket connecting failure reasons
2024-08-25 20:41:12 -04:00
Mikayla Fischler
ebeeecc5ab luacheck fix 2024-08-25 20:40:30 -04:00
Mikayla Fischler
dbabcd13b0 luacheck fix 2024-08-25 20:39:21 -04:00
Mikayla Fischler
acc8e1c058 incremented graphics version and disabled listbox debug messages for now 2024-08-25 20:38:01 -04:00
Mikayla Fischler
5a38acf2a7 #540 display pocket connecting failure reasons 2024-08-25 20:29:52 -04:00
Mikayla Fischler
b3be2d4bfc #537 close sessions on receiving an ESTABLISH packet to allow for retries 2024-08-24 14:46:58 -04:00
Mikayla
0ab2d57b66 Merge pull request #538 from MikaylaFischler/367-list-duplicate-and-missing-device-ids
Supervisor Listing of Missing and Bad Device IDs
2024-08-24 14:11:46 -04:00
Mikayla
183af8a5ca #539 logging for investigations 2024-08-22 18:18:13 +00:00
Mikayla
6f63092d4b #367 check facility dynamic tank linking 2024-08-22 16:45:36 +00:00
Mikayla
a087eda0ee #367 RTU fail enum and logging messages 2024-08-22 16:42:57 +00:00
Mikayla Fischler
a1b6ff4bcc luacheck fixes 2024-08-21 19:18:55 -04:00
Mikayla Fischler
8c6b264f6b #367 simplified chk_entry 2024-08-21 19:15:12 -04:00
Mikayla Fischler
8a5c468606 #367 fixes and removed computer ID display 2024-08-21 18:53:52 -04:00
Mikayla
12f187f596 #367 logic for missing device detection and user-friendly messages 2024-08-21 21:23:16 +00:00
Mikayla
01a1c374ab Merge branch 'devel' into 367-list-duplicate-and-missing-device-ids 2024-08-21 13:56:50 +00:00
Mikayla Fischler
465875b287 coordinator receives tank list from supervisor 2024-08-20 22:28:41 -04:00
Mikayla Fischler
45d4b4e653 fixed PLC status retry packet type 2024-08-20 21:35:05 -04:00
Mikayla Fischler
fc7441b2f6 #367 reworked ownership of tank data and facility instance to make more sense 2024-08-20 21:32:54 -04:00
Mikayla Fischler
6917697290 #536 proper clearing of cleared config values 2024-08-20 20:56:41 -04:00
Mikayla
c323967b6a #536 fix for clearing settings 2024-08-20 20:52:38 +00:00
Mikayla Fischler
4775639245 #367 WIP listing ID check failures and missing devices 2024-08-18 23:04:44 -04:00
Mikayla Fischler
072613959c facility tank list generation on supervisor 2024-08-18 23:04:07 -04:00
Mikayla Fischler
f259f85a99 fixed wrong function name 2024-08-18 19:12:13 -04:00
Mikayla Fischler
e076e327d8 split up facility logic into two files 2024-08-18 19:10:43 -04:00
Mikayla
f34747372f #367 work on device ID check failure list 2024-08-16 21:19:25 +00:00
Mikayla
affe2d6c6d listbox debugging 2024-08-16 21:17:36 +00:00
Mikayla
5597ea2097 comment updates for clarity around RTU gateway vs RTU 2024-08-16 19:53:43 +00:00
Mikayla
0f4a8b6dfc refactoring and RTU gateway terminology cleanup 2024-08-16 18:17:03 +00:00
Mikayla
ab97f8935d #367 reject and record bad or duplicate RTU IDs 2024-08-16 18:08:53 +00:00
Mikayla
b0342654e7 added off-line installation to installation options 2024-08-12 09:55:19 -04:00
Mikayla Fischler
bee96ed12e #517 ccmsi print wrapping and other adjustments for pocket environment 2024-08-11 22:11:57 -04:00
Mikayla Fischler
50bd59781e #534 fixed PLC self-check UI problem 2024-08-11 20:21:26 -04:00
Mikayla Fischler
196e0b1daf #519 fixed issue with turbine stability evaluation 2024-08-11 19:58:29 -04:00
Mikayla
f725eb0eef Merge pull request #533 from MikaylaFischler/devel
2024.07.28 Release
2024-07-28 17:21:26 -04:00
Mikayla Fischler
bcc55628cf don't disable self-check even if there is no config 2024-07-28 16:41:39 -04:00
Mikayla Fischler
9bffd6feee incremented coordinator version 2024-07-27 21:28:37 -04:00
Mikayla
1500004481 Merge pull request #532 from MikaylaFischler/configurator-updates
Configurator Updates
2024-07-27 20:55:25 -04:00
Mikayla Fischler
2904621e81 fixed wrong disable format on self-check button 2024-07-27 20:55:00 -04:00
Mikayla Fischler
08eee198c8 cleanup and rewording notices 2024-07-27 20:35:09 -04:00
Mikayla Fischler
e750ffe69d updated element asserts for power indicator and incremented graphics version 2024-07-27 16:23:37 -04:00
Mikayla Fischler
de6d8a89ca avoid redundant calls to report_link_state 2024-07-27 16:23:19 -04:00
Mikayla Fischler
f00751edeb still display supervisor/coordinator address info if not linked to both 2024-07-27 13:17:56 -04:00
Mikayla Fischler
d58a6a3369 #531 pocket energy scale options 2024-07-27 12:51:46 -04:00
Mikayla Fischler
340c6689a9 #523 coordinator configurator updates 2024-07-27 12:35:26 -04:00
Mikayla Fischler
7cc088ca95 #523 coordinator energy scale options 2024-07-27 12:34:01 -04:00
Mikayla Fischler
01f6b1e190 #363 added tip about self-check 2024-07-27 11:15:23 -04:00
Mikayla Fischler
3ffc79b181 #530 fix RTU reconnection issue 2024-07-27 11:15:05 -04:00
Mikayla Fischler
8e4bb583a8 #528 reactor PLC configurator fixes 2024-07-26 23:06:42 -04:00
Mikayla
ec107929bc #528 reactor PLC configurator cleanup 2024-07-27 00:27:38 +00:00
Mikayla Fischler
3406d12681 #363 check config 2024-07-24 22:42:14 -04:00
Mikayla Fischler
03bbf8a891 updated coordinator configurator connection sequence number logic to match new system 2024-07-22 23:45:25 -04:00
Mikayla Fischler
b61867be3c updated RTU configurator change log 2024-07-22 23:44:56 -04:00
Mikayla Fischler
1358d95269 cc strings infinite loop mitigation 2024-07-22 23:44:34 -04:00
Mikayla Fischler
fd06730e46 #363 PLC configurator self check WIP 2024-07-22 23:44:12 -04:00
Mikayla
fb5f3b9474 #363 work on PLC self-check 2024-07-20 18:17:36 +00:00
Mikayla
3afc1e6cfa #512 rtu help text updates 2024-07-20 18:14:59 +00:00
Mikayla Fischler
715765d442 #512 increased clarity of peripheral assignments 2024-07-16 18:07:37 -04:00
Mikayla
3762e9dced #524 fix tank layout render reset 2024-07-16 21:03:52 +00:00
Mikayla Fischler
022d1f9f49 updated main's manifest workflow 2024-07-06 00:55:03 -04:00
Mikayla
b1da76c2f6 Merge pull request #516 from MikaylaFischler/devel
2024.07.05 Release
2024-07-06 00:51:28 -04:00
Mikayla Fischler
0364b4df7b fixed pocket crash due to guide section height too small 2024-07-06 00:07:21 -04:00
Mikayla Fischler
e04bd032fe incremented common version 2024-07-06 00:07:00 -04:00
Mikayla
34fe5dc382 Merge pull request #518 from MikaylaFischler/pocket-alpha-dev
Pocket alpha dev
2024-07-05 23:29:06 -04:00
Mikayla
1f8ea56095 Merge pull request #515 from MikaylaFischler/514-retry-file-downloads-on-failure
514 retry file downloads on failure
2024-07-05 13:39:20 -04:00
Mikayla Fischler
f2cd98c57a #194 fixes to log file handling, improved failure behavior, skip extra dialogs if nothing can be updated 2024-07-03 21:14:39 -04:00
Mikayla
1e341af8a5 #514 optimizations and fixes 2024-07-03 15:01:43 +00:00
Mikayla Fischler
2fb3d9b515 #514 cleaned up download logic and added retries 2024-07-02 22:07:12 -04:00
Mikayla
220f9d152f Merge branch 'devel' into pocket-alpha-dev 2024-07-01 16:36:36 +00:00
Mikayla
604b4a1927 Merge pull request #511 from MikaylaFischler/500-remove-height=1-from-textbox-elements
#500 removed now redundant height=1 from TextBox elements
2024-06-30 14:14:20 -04:00
Mikayla Fischler
0ecaa42a7f restored incorrectly modified height 2024-06-30 14:05:34 -04:00
Mikayla Fischler
9614407c37 #500 removed now redundant height=1 from TextBox elements 2024-06-30 13:55:13 -04:00
Mikayla Fischler
f1c4f8c00a keep main on old file path for now 2024-06-30 12:40:23 -04:00
Mikayla Fischler
8a409f0313 manifest build fix 2024-06-30 12:36:22 -04:00
Mikayla
da68398fa4 Merge pull request #510 from MikaylaFischler/506-single-file-off-line-installer
506 single file off line installer
2024-06-30 12:34:27 -04:00
Mikayla Fischler
375e969161 cleanup 2024-06-30 12:33:52 -04:00
Mikayla Fischler
c93cd4d0bd remove UTF-8 copyright symbol 2024-06-30 00:01:00 -04:00
Mikayla Fischler
e69cbc8633 luacheck fix 3 who could see this coming 2024-06-29 23:53:37 -04:00
Mikayla Fischler
ea1bcbf81c luacheck fix 2 2024-06-29 23:52:34 -04:00
Mikayla Fischler
f13f03fddc luacheck fix 2024-06-29 23:51:32 -04:00
Mikayla Fischler
72a480e475 luacheck the offline script but with an ignore 2024-06-29 23:49:33 -04:00
Mikayla Fischler
ec1b56b853 disable luacheck on offline script 2024-06-29 23:42:22 -04:00
Mikayla Fischler
c5fb299f55 message rewording, fixed colors on deletions 2024-06-29 23:38:51 -04:00
Mikayla Fischler
63c990a3cf #506 two-file bundled offline installer generation 2024-06-29 22:44:12 -04:00
Mikayla Fischler
89e84f9711 #194 ccmsi updates around log handling 2024-06-29 22:39:58 -04:00
Mikayla Fischler
270aeb13ca removed config.lua from luacheck 2024-06-29 22:38:11 -04:00
Mikayla Fischler
df8c71f12e #506 use minified files for off-line installer 2024-06-29 16:02:25 -04:00
Mikayla Fischler
347f67c8ee Merge branch '465-safe-lua-minifier' into 506-single-file-off-line-installer 2024-06-29 15:57:07 -04:00
Mikayla
8de2d7071e Merge pull request #509 from MikaylaFischler/465-safe-lua-minifier
465 safe lua minifier
2024-06-29 15:55:11 -04:00
Mikayla Fischler
d424cf74d3 Merge branch 'devel' into 506-single-file-off-line-installer 2024-06-29 15:53:27 -04:00
Mikayla Fischler
a1b571d7c0 copy over LICENSE to minified output directory 2024-06-29 15:52:47 -04:00
Mikayla Fischler
8e0e4df3eb rename package zip script 2024-06-29 15:52:34 -04:00
Mikayla Fischler
b025958173 comments in minifier 2024-06-29 15:41:04 -04:00
Mikayla Fischler
f83eecf2e2 script to package zips for installation without internet but with filesystem upload access 2024-06-29 15:28:16 -04:00
Mikayla Fischler
0b2f7b13a1 moved build scripts to new build directory 2024-06-29 15:14:43 -04:00
Mikayla Fischler
3ad3cbb4eb Merge branch 'devel' into 465-safe-lua-minifier 2024-06-29 15:11:55 -04:00
Mikayla Fischler
e868fd3397 cleanup of bootloader 2024-06-29 15:11:16 -04:00
Mikayla Fischler
a4add9370c RTU modem init consistency and cleanup 2024-06-29 15:08:11 -04:00
Mikayla
4f48ba8abc #403 work on guide docs 2024-06-29 18:59:39 +00:00
Mikayla
3cc6781844 Merge pull request #507 from MikaylaFischler/488-accelerate-hmac-computation
488 accelerate hmac computation
2024-06-29 14:52:15 -04:00
Mikayla Fischler
c05a45c29a more cleanup 2024-06-29 14:51:15 -04:00
Mikayla Fischler
f2937b47e9 cleanup 2024-06-29 14:49:26 -04:00
Mikayla
8e14fa1591 disable a diagnostic message in ccmsi 2024-06-29 18:30:32 +00:00
Mikayla
aebb9f42be #506 work on off-line installer generation script 2024-06-29 18:29:49 +00:00
Mikayla Fischler
2de30ef064 #488 fixes to sequence number changes and auth packet data 2024-06-29 14:10:58 -04:00
Mikayla Fischler
8dedb092e7 Merge branch 'devel' into 488-accelerate-hmac-computation 2024-06-29 12:51:52 -04:00
Mikayla
807e575580 Merge pull request #505 from MikaylaFischler/502-supervisor-crash-arithmetic-operation-on-nil-value
502 supervisor crash arithmetic operation on nil value
2024-06-29 12:48:28 -04:00
Mikayla Fischler
55dc203cdd increment reactor plc version to 1.8.0 2024-06-29 12:47:56 -04:00
Mikayla Fischler
a15cbadd32 #497 initial loading screen 2024-06-29 12:29:28 -04:00
Mikayla Fischler
bc76c01aa5 #504 fixed reactor idle status on pocket display 2024-06-29 12:18:55 -04:00
Mikayla
d2bc4f6bc0 #488 HMAC acceleration and seq_num changes 2024-06-29 02:27:55 +00:00
Mikayla Fischler
4cdbe3b07f some more cleanup 2024-06-27 21:05:53 -04:00
Mikayla Fischler
897a3ed22d #502 much needed refresh and cleanup of PLC struct and status packet handling 2024-06-27 21:03:53 -04:00
Mikayla Fischler
2bc20ec312 cleanup 2024-06-27 20:01:53 -04:00
Mikayla Fischler
fc42049aa0 removed deprecated high temp constant 2024-06-27 19:57:55 -04:00
Mikayla Fischler
4a7028f401 #497 instantly launch pocket program, block network dependent apps until connected 2024-06-27 19:57:43 -04:00
Mikayla
006c5e6adf Merge pull request #501 from MikaylaFischler/devel
2024.06.15 Release
2024-06-15 18:27:17 -04:00
Mikayla Fischler
f64db66448 comms version updates 2024-06-14 17:58:39 -04:00
Mikayla
a8e6bc0e35 Merge pull request #499 from MikaylaFischler/pocket-alpha-dev
Pocket June Update
2024-06-14 17:50:47 -04:00
Mikayla Fischler
4a39ed9d38 removed stray newline 2024-06-14 17:50:16 -04:00
Mikayla Fischler
9fe0669fda updated guide section heights and added a debug message to track height usage 2024-06-14 17:49:43 -04:00
Mikayla Fischler
219f02b188 print render crash cause to user 2024-06-14 17:42:03 -04:00
Mikayla Fischler
ea8f62dea6 #497 exit app if it is unloaded 2024-06-14 17:38:45 -04:00
Mikayla
1c719ad67b cleanup for pull request 2024-06-14 21:10:42 +00:00
Mikayla
87a91e309d #403 guide updates 2024-06-14 21:09:14 +00:00
Mikayla
c66ad44adb pocket cleanup 2024-06-14 16:32:25 +00:00
Mikayla
14736e414f luacheck ignore 2024-06-14 16:20:53 +00:00
Mikayla
fb1f85a626 possible luacheck suppression 2024-06-14 16:16:41 +00:00
Mikayla
697a3d6f6b luacheck fixes 2024-06-14 16:14:24 +00:00
Mikayla
00cacd6d0a #497 unload apps when required connections are lost 2024-06-14 16:10:04 +00:00
Mikayla Fischler
0b97d4d4b0 updated header message 2024-06-13 21:52:13 -04:00
Mikayla Fischler
def5b49327 #496 threaded app loading 2024-06-13 21:43:56 -04:00
Mikayla Fischler
38457cfbbc enforce pocket computer requirement 2024-06-13 20:34:39 -04:00
Mikayla
e851a5275f #496 pocket threading 2024-06-13 16:45:44 +00:00
Mikayla Fischler
5848c2ac1a test code for debugging 2024-06-12 20:22:41 -04:00
Mikayla Fischler
f8a5dd9c32 #200 additional fields for info display 2024-06-12 20:20:30 -04:00
Mikayla Fischler
ff8ae5e609 #200 updated status info display fields 2024-06-12 20:17:31 -04:00
Mikayla Fischler
95b93eb795 #403 nav up button sends back to prior app if open_help was used 2024-06-12 20:10:16 -04:00
Mikayla Fischler
356caf7b4d #403 guide searching 2024-06-12 20:01:31 -04:00
Mikayla Fischler
09c44a6969 multi-line push button and keep focus on keyboard select 2024-06-12 19:59:19 -04:00
Mikayla Fischler
83ba6e3961 #403 updated search navigation 2024-06-06 23:10:19 -04:00
Mikayla Fischler
e88e1afcc4 #403 started work on guide searching 2024-06-06 23:05:22 -04:00
Mikayla Fischler
b457edbc71 #403 variable sizing on listbox heights for sections 2024-06-06 22:54:26 -04:00
Mikayla
5e70e4131e #403 more work on guide and help linking 2024-06-07 02:30:55 +00:00
Mikayla Fischler
375b7f680e fixes to graphics constraint logic 2024-06-05 19:38:22 -04:00
Mikayla Fischler
e37b8758cd #403 guide fixes and focusing improvements 2024-06-05 19:38:02 -04:00
Mikayla
9404b50a8c #403 additional work on guide app 2024-06-05 22:07:38 +00:00
Mikayla
58fb35e85b keyboard and paste support for pocket 2024-06-05 00:31:47 +00:00
Mikayla
b9030d6bed #403 work on guide app 2024-06-05 00:31:29 +00:00
Mikayla
25ebf2c8c7 graphics automatic constraints 2024-06-05 00:31:06 +00:00
Mikayla Fischler
4d87887709 #403 pocket guide fixes 2024-06-03 20:52:59 -04:00
Mikayla
b63a17bda0 Merge branch 'pocket-alpha-dev' of https://github.com/MikaylaFischler/cc-mek-scada into pocket-alpha-dev 2024-06-04 00:24:41 +00:00
Mikayla
39233dae8a Merge branch 'pocket-alpha-dev' of https://github.com/MikaylaFischler/cc-mek-scada into pocket-alpha-dev 2024-06-04 00:22:06 +00:00
Mikayla
a2af0d3829 #403 work on pocket guide app 2024-06-04 00:21:54 +00:00
Mikayla Fischler
db901129f9 #200 status display updates 2024-06-02 22:00:42 -04:00
Mikayla Fischler
c1c49ea3fb #200 unit app updates 2024-06-02 16:06:32 -04:00
Mikayla Fischler
be6c3755a4 #207 pocket turbine view 2024-06-01 00:50:19 -04:00
Mikayla Fischler
ac2d189c1a reactor and boiler view fixes 2024-05-31 19:25:36 -04:00
Mikayla Fischler
0fa0324940 #206 pocket boiler view 2024-05-31 18:16:04 -04:00
Mikayla Fischler
3181ab96f1 #206 work on boiler view and reorganized app code 2024-05-29 22:08:36 -04:00
Mikayla Fischler
30c9215658 #202 pocket reactor view 2024-05-27 23:53:35 -04:00
Mikayla Fischler
946c28c929 record additional reactor unit data 2024-05-27 23:49:53 -04:00
Mikayla Fischler
e6d6353d05 added temperature units to pocket and to common types 2024-05-27 19:31:24 -04:00
Mikayla Fischler
0e81391144 #200 fixes to alarm/info display 2024-05-22 21:45:52 -04:00
Mikayla Fischler
b18cadb53e Merge branch 'devel' into pocket-alpha-dev 2024-05-22 18:46:23 -04:00
Mikayla Fischler
9a500d8f96 Merge branch 'pocket-alpha-dev' of github.com:MikaylaFischler/cc-mek-scada into pocket-alpha-dev 2024-05-22 18:15:18 -04:00
Mikayla
a268a770f2 #200 pocket alarm/status informational display, ECAM style 2024-05-22 21:55:59 +00:00
Mikayla
43b44cc425 #465 first pass at minifier 2024-04-24 21:11:41 +00:00
188 changed files with 20902 additions and 10659 deletions

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v3.5.1 uses: actions/checkout@v4
- name: Luacheck - name: Luacheck
uses: lunarmodules/luacheck@v1.1.0 uses: lunarmodules/luacheck@v1.1.0
with: with:
@@ -26,4 +26,4 @@ jobs:
# --no-max-line-length = Disable warnings for long line lengths # --no-max-line-length = Disable warnings for long line lengths
# --exclude-files ... = Exclude lockbox library (external) and config files # --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' # --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os'
args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http keys parallel periphemu peripheral read rs settings shell term textutils window args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* --globals os _HOST bit colors fs http keys parallel periphemu peripheral read rs settings shell term textutils window

View File

@@ -29,13 +29,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Setup Pages - name: Setup Pages
uses: actions/configure-pages@v3 uses: actions/configure-pages@v5
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v3.1.3 uses: actions/setup-python@v5
with:
python-version: '3.10'
# Generate manifest + shields files for main branch # Generate manifest + shields files for main branch
- name: Checkout main - name: Checkout main
id: checkout-main id: checkout-main
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
ref: 'main' ref: 'main'
clean: false clean: false
@@ -46,7 +48,7 @@ jobs:
- name: Generate manifest and shields for main branch - name: Generate manifest and shields for main branch
id: manifest-main id: manifest-main
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }} if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
run: python imgen.py shields run: python build/imgen.py shields
- name: Save main's manifest - name: Save main's manifest
if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }} if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/main run: mv install_manifest.json deploy/manifests/main
@@ -54,14 +56,14 @@ jobs:
- name: Checkout devel - name: Checkout devel
id: checkout-devel id: checkout-devel
if: success() || failure() if: success() || failure()
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
ref: 'devel' ref: 'devel'
clean: false clean: false
- name: Generate manifest for devel - name: Generate manifest for devel
id: manifest-devel id: manifest-devel
if: ${{ (success() || failure()) && steps.checkout-devel.outcome == 'success' }} if: ${{ (success() || failure()) && steps.checkout-devel.outcome == 'success' }}
run: python imgen.py run: python build/imgen.py
- name: Save devel's manifest - name: Save devel's manifest
if: ${{ (success() || failure()) && steps.manifest-devel.outcome == 'success' }} if: ${{ (success() || failure()) && steps.manifest-devel.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/devel run: mv install_manifest.json deploy/manifests/devel
@@ -69,11 +71,11 @@ jobs:
- name: Upload artifacts - name: Upload artifacts
id: upload-artifacts id: upload-artifacts
if: ${{ (success() || failure()) && (steps.manifest-main.outcome == 'success' || steps.manifest-latest.outcome == 'success' || steps.manifest-devel.outcome == 'success') }} if: ${{ (success() || failure()) && (steps.manifest-main.outcome == 'success' || steps.manifest-latest.outcome == 'success' || steps.manifest-devel.outcome == 'success') }}
uses: actions/upload-pages-artifact@v1 uses: actions/upload-pages-artifact@v3
with: with:
# Upload manifest JSON # Upload manifest JSON
path: 'deploy/' path: 'deploy/'
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
if: ${{ (success() || failure()) && steps.upload-artifacts.outcome == 'success' }} if: ${{ (success() || failure()) && steps.upload-artifacts.outcome == 'success' }}
id: deployment id: deployment
uses: actions/deploy-pages@v2 uses: actions/deploy-pages@v4

2
.gitignore vendored
View File

@@ -1,2 +1,2 @@
_notes/ _*/
/*program.sh /*program.sh

57
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,57 @@
# Contribution Guide
>[!NOTE]
Until the system is out of beta, contributions will be limited as I wrap up the specific release feature set.
This project is highly complex for a ComputerCraft Lua application. Contributions need to follow style guides and meet the code quality I've kept this project up to for years. Contributions must be tested appropriately with test results included.
I have extensively tested software components for stability required for safety, with tiers of software robustness.
1. **Critical: High-Impact** -
The Reactor-PLC is "uncrashable" and must remain so. I've extensively reviewed every line and behavior, so any code contributions must be at this high standard. Simple is stable, so the less code the better. Always check for parameter validity and extensively test any changes to critical thread functions.
2. **Important: Moderate-Impact** -
The Supervisor and RTU Gateway should rarely, if ever, crash. Certain places may not be held to as strict of a level as above, but should be written understanding all the possible inputs to and impacts of a section of code.
3. **Useful: Low-Impact** -
The Coordinator and Pocket are nice UI apps, and things can break. There's a lot of data going to and from them, so checking every single incoming value would have negative performance impacts and increase program size. If they break, the user can restart them. Don't introduce careless bugs, but making assumptions about the integrity of incoming data is acceptable.
## Valuable Contributions
Pull requests should not consist of purely whitespace changes, comment changes, or other trivial changes. They should target specific features, bug fixes, or functional improvements. I reserve the right to decline PRs that don't follow this in good faith.
## Project Management Guidelines
Any contributions should be linked to an open GitHub issue. These are used to track progress, discuss changes, etc. Surprise changes to this project might conflict with existing plans, so I prefer we coordinate changes ahead of time.
## Software Guidelines
These guidelines are subject to change. The general rule is make the code look like the rest of the code around it and elsewhere in the project.
### Style Guide
PRs will only be accepted if they match the style of this project and pass manual and automated code analysis. Listing out the whole style guide would take a while, so as stated above, please review code adjacent to your modifications.
1. **No Block Comments.**
These interfere with the minification used for the bundled installation files due to the complexity of parsing Lua block comments. The minification code is meant to be simple to have 0 risk of breaking anything, so I'm staying far away from those.
2. **Comment Your Code.**
This includes type hints as used elsewhere throughout the project. Your comments should be associated with parts of code that are more complex or unclear, or otherwise to split up sections of tasks. You'll see `--#region` used in various places.
- Type hints are intended to be utilized by the `sumneko.lua` vscode extension. You should use this while developing, as it provides extremely valuable functionality.
3. **Whitespace Usage.**
Whitespace should be used to separate function parameters and operators. The one exception is the unique styling of graphics elements, which you should compare against if modifying them.
- 4 spaces are used for all indentation.
- Try to align assignment operator lines as is done elsewhere (adding space before `=`).
- Use empty new lines to separate steps or distinct groups of operations.
- Generally add new lines for each step in loops and for statements. For some single-line ones, they may be compressed into a single line. This saves on space utilization, especially on deeply indented lines.
4. **Variables and Classes.**
- Variables, functions, and class-like tables follow the snake_case convention.
- Graphics objects and configuration settings follow PascalCase.
- Constants follow all-caps SNAKE_CASE and local ones should be declared at the top of files after `require` statements and external ones (like `local ALARM = types.ALARM`).
5. **No `goto`.**
These are generally frowned upon due to reducing code readability.
6. **Multiple `return`s.**
These are allowed to minimize code size, but if it is simple to avoid multiple, do so.
7. **Classes and Objects.**
Review the existing code for examples on how objects are implemented in this project. They do not use Lua's `:` operator and `self` functionality. A manual object-like table definition is used. Some global single-instance classes don't use a `new()` function, such as the [PPM](https://github.com/MikaylaFischler/cc-mek-scada/blob/main/scada-common/ppm.lua). Multi-instance ones do, such as the Supervisor's [unit](https://github.com/MikaylaFischler/cc-mek-scada/blob/main/supervisor/unit.lua) class.
### No AI
Your code should follow the style guide, be succinct, make sense, and you should be able to explain what it does. Random changes done in multiple places will be deemed suspicious along with poor comments or nonsensical code.
Use your contributions as programming practice or to hone your skills; don't automate away thinking.

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright © 2022 - 2024 Mikayla Fischler Copyright 2022 - 2024 Mikayla Fischler
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -45,6 +45,7 @@ v10.1+ is required due to the complete support of CC:Tweaked added in Mekanism v
You can install this on a ComputerCraft computer using either: You can install this on a ComputerCraft computer using either:
* `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua` * `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua`
* `pastebin get sqUN6VUb ccmsi.lua` * `pastebin get sqUN6VUb ccmsi.lua`
* Off-line (when HTTP is disabled) installation via [release bundles](https://github.com/MikaylaFischler/cc-mek-scada/wiki/Alternative-Installation-Strategies#release-bundles)
## Contributing ## Contributing

118
build/_offline.lua Normal file
View File

@@ -0,0 +1,118 @@
---@diagnostic disable: undefined-global
-- luacheck: push ignore install_manifest ccmsi_offline app_files dep_files lgray green white
local b64_lookup = {
['A'] = 0, ['B'] = 1, ['C'] = 2, ['D'] = 3, ['E'] = 4, ['F'] = 5, ['G'] = 6, ['H'] = 7, ['I'] = 8, ['J'] = 9, ['K'] = 10, ['L'] = 11, ['M'] = 12, ['N'] = 13, ['O'] = 14, ['P'] = 15, ['Q'] = 16, ['R'] = 17, ['S'] = 18, ['T'] = 19, ['U'] = 20, ['V'] = 21, ['W'] = 22, ['X'] = 23, ['Y'] = 24, ['Z'] = 25,
['a'] = 26, ['b'] = 27, ['c'] = 28, ['d'] = 29, ['e'] = 30, ['f'] = 31, ['g'] = 32, ['h'] = 33, ['i'] = 34, ['j'] = 35, ['k'] = 36, ['l'] = 37, ['m'] = 38, ['n'] = 39, ['o'] = 40, ['p'] = 41, ['q'] = 42, ['r'] = 43, ['s'] = 44, ['t'] = 45, ['u'] = 46, ['v'] = 47, ['w'] = 48, ['x'] = 49, ['y'] = 50, ['z'] = 51,
['0'] = 52, ['1'] = 53, ['2'] = 54, ['3'] = 55, ['4'] = 56, ['5'] = 57, ['6'] = 58, ['7'] = 59, ['8'] = 60, ['9'] = 61, ['+'] = 62, ['/'] = 63
}
local BYTE = 0xFF
local CHAR = string.char
local BOR = bit.bor ---@type function
local BAND = bit.band ---@type function
local LSHFT = bit.blshift ---@type function
local RSHFT = bit.blogic_rshift ---@type function
-- decode a base64 string
---@param input string
local function b64_decode(input)
---@diagnostic disable-next-line: undefined-field
local t_start = os.epoch("local")
local decoded = {}
local c_idx, idx = 1, 1
for _ = 1, input:len() / 4 do
local block = input:sub(idx, idx + 4)
local word = 0x0
-- build the 24-bit sequence from the 4 characters
for i = 1, 4 do
local num = b64_lookup[block:sub(i, i)]
if num then
word = BOR(word, LSHFT(b64_lookup[block:sub(i, i)], (4 - i) * 6))
end
end
-- decode the 24-bit sequence as 8 bytes
for i = 1, 3 do
local char = BAND(BYTE, RSHFT(word, (3 - i) * 8))
if char ~= 0 then
decoded[c_idx] = CHAR(char)
c_idx = c_idx + 1
end
end
idx = idx + 4
end
---@diagnostic disable-next-line: undefined-field
local elapsed = (os.epoch("local") - t_start)
local decoded_str = table.concat(decoded)
return decoded_str, elapsed
end
-- write files recursively from base64 encodings in a table
---@param files table
---@param path string
local function write_files(files, path)
fs.makeDir(path)
for k, v in pairs(files) do
if type(v) == "table" then
if k == "system" then
-- write system files to root
write_files(v, "/")
else
-- descend into directories
write_files(v, path .. "/" .. k .. "/")
end
---@diagnostic disable-next-line: undefined-field
os.sleep(0.05)
else
local handle = fs.open(path .. k, "w")
local text, time = b64_decode(v)
print("decoded '" .. k .. "' in " .. time .. "ms")
handle.write(text)
handle.close()
end
end
end
-- write installation manifiest and offline install manager
local function write_install()
local handle = fs.open("install_manifest.json", "w")
handle.write(b64_decode(install_manifest))
handle.close()
handle = fs.open("ccmsim.lua", "w")
handle.write(b64_decode(ccmsi_offline))
handle.close()
end
lgray()
-- write both app and dependency files
write_files(app_files, "/")
write_files(dep_files, "/")
-- write an install manifest and the offline installer
write_install()
green()
print("Done!")
white()
print("All files have been installed. The app can be started with 'startup' and configured with 'configure'.")
lgray()
print("Hint: You can use 'ccmsim' to manage your off-line installation.")
white()
--luacheck: pop

213
build/bundle.py Normal file
View File

@@ -0,0 +1,213 @@
import base64
import json
import os
import subprocess
path_prefix = "./_minified/"
# get git build info
build = subprocess.check_output(["git", "describe", "--tags"]).strip().decode('UTF-8')
# list files in a directory
def list_files(path):
list = []
for (root, dirs, files) in os.walk(path):
for f in files:
list.append((root[2:] + "/" + f).replace('\\','/'))
return list
# recursively encode files with base64
def encode_recursive(path):
list = {}
for item in os.listdir(path):
item_path = path + '/' + item
if os.path.isfile(item_path):
handle = open(item_path, 'r')
list[item] = base64.b64encode(bytes(handle.read(), 'UTF-8')).decode('ASCII')
handle.close()
else:
list[item] = encode_recursive(item_path)
return list
# encode listed files with base64
def encode_files(files):
list = {}
for item in files:
item_path = path_prefix + './' + item
handle = open(item_path, 'r')
list[item] = base64.b64encode(bytes(handle.read(), 'UTF-8')).decode('ASCII')
handle.close()
return list
# get the version of an application at the provided path
def get_version(path, is_lib = False):
ver = ""
string = ".version = \""
if not is_lib:
string = "_VERSION = \""
f = open(path, "r")
for line in f:
pos = line.find(string)
if pos >= 0:
ver = line[(pos + len(string)):(len(line) - 2)]
break
f.close()
return ver
# file manifest (reflects imgen.py)
manifest = {
"common_versions" : {
"bootloader" : get_version("./startup.lua"),
"common" : get_version("./scada-common/util.lua", True),
"comms" : get_version("./scada-common/comms.lua", True),
"graphics" : get_version("./graphics/core.lua", True),
"lockbox" : get_version("./lockbox/init.lua", True),
},
"app_versions" : {
"reactor-plc" : get_version("./reactor-plc/startup.lua"),
"rtu" : get_version("./rtu/startup.lua"),
"supervisor" : get_version("./supervisor/startup.lua"),
"coordinator" : get_version("./coordinator/startup.lua"),
"pocket" : get_version("./pocket/startup.lua")
},
"files" : {
# common files
"system" : encode_files([ "initenv.lua", "startup.lua", "configure.lua", "LICENSE" ]),
"scada-common" : encode_recursive(path_prefix + "./scada-common"),
"graphics" : encode_recursive(path_prefix + "./graphics"),
"lockbox" : encode_recursive(path_prefix + "./lockbox"),
# platform files
"reactor-plc" : encode_recursive(path_prefix + "./reactor-plc"),
"rtu" : encode_recursive(path_prefix + "./rtu"),
"supervisor" : encode_recursive(path_prefix + "./supervisor"),
"coordinator" : encode_recursive(path_prefix + "./coordinator"),
"pocket" : encode_recursive(path_prefix + "./pocket"),
},
"install_files" : {
# common files
"system" : [ "initenv.lua", "startup.lua", "configure.lua", "LICENSE" ],
"scada-common" : list_files("./scada-common"),
"graphics" : list_files("./graphics"),
"lockbox" : list_files("./lockbox"),
# platform files
"reactor-plc" : list_files("./reactor-plc"),
"rtu" : list_files("./rtu"),
"supervisor" : list_files("./supervisor"),
"coordinator" : list_files("./coordinator"),
"pocket" : list_files("./pocket"),
},
"depends" : [ "system", "scada-common", "graphics", "lockbox" ]
}
# write the application installation items as Lua tables
def write_items(body, items, indent):
indent_str = " " * indent
for key, value in items.items():
if isinstance(value, str):
body = body + f"{indent_str}['{key}'] = \"{value}\",\n"
else:
body = body + f"{indent_str}['{key}'] = {{\n"
body = write_items(body, value, indent + 4)
body = body + f"{indent_str}}},\n"
return body
# create output directory
if not os.path.exists("./BUNDLE"):
os.makedirs("./BUNDLE")
# get offline installer
ccmsim_file = open("./build/ccmsim.lua", "r")
ccmsim_script = ccmsim_file.read()
ccmsim_file.close()
# create dependency bundled file
dep_file = "common_" + build + ".lua"
f_d = open("./BUNDLE/" + dep_file, "w")
body_b = "local dep_files = {\n"
for depend in manifest["depends"]:
body_b = body_b + write_items("", { f"{depend}": manifest["files"][depend] }, 4)
body_b = body_b + "}\n"
body_b = body_b + f"""
if select("#", ...) == 0 then
term.setTextColor(colors.red)
print("You must run the other file you should have uploaded (it has the app in its name).")
term.setTextColor(colors.white)
end
return dep_files
"""
f_d.write(body_b)
f_d.close()
# application bundled files
for app in [ "reactor-plc", "rtu", "supervisor", "coordinator", "pocket" ]:
app_file = app + "_" + build + ".lua"
f_script = open("./build/_offline.lua", "r")
script = f_script.read()
f_script.close()
f_a = open("./BUNDLE/" + app_file, "w")
body_a = "local app_files = {\n"
body_a = body_a + write_items("", { f"{app}": manifest["files"][app] }, 4) + "}\n"
versions = manifest["common_versions"].copy()
versions[app] = manifest["app_versions"][app]
depends = manifest["depends"].copy()
depends.append(app)
install_manifest = json.dumps({ "versions" : versions, "files" : manifest["install_files"], "depends" : depends })
body_a = body_a + f"""
-- install manifest JSON and offline installer
local install_manifest = "{base64.b64encode(bytes(install_manifest, 'UTF-8')).decode('ASCII')}"
local ccmsi_offline = "{base64.b64encode(bytes(ccmsim_script, 'UTF-8')).decode('ASCII')}"
local function red() term.setTextColor(colors.red) end
local function green() term.setTextColor(colors.green) end
local function white() term.setTextColor(colors.white) end
local function lgray() term.setTextColor(colors.lightGray) end
if not fs.exists("{dep_file}") then
red()
print("Missing '{dep_file}'! Please upload it, then run this file again.")
white()
return
end
-- rename the dependency file
fs.move("{dep_file}", "install_depends.lua")
-- load the other file
local dep_files = require("install_depends")
-- delete the uploaded files to free up space to actually install
fs.delete("{app_file}")
fs.delete("install_depends.lua")
-- get started installing
{script}"""
f_a.write(body_a)
f_a.close()

237
build/ccmsim.lua Normal file
View File

@@ -0,0 +1,237 @@
local function println(message) print(tostring(message)) end
local function print(message) term.write(tostring(message)) end
local opts = { ... }
local mode, app
local function red() term.setTextColor(colors.red) end
local function orange() term.setTextColor(colors.orange) end
local function yellow() term.setTextColor(colors.yellow) end
local function green() term.setTextColor(colors.green) end
local function blue() term.setTextColor(colors.blue) end
local function white() term.setTextColor(colors.white) end
local function lgray() term.setTextColor(colors.lightGray) end
-- get command line option in list
local function get_opt(opt, options)
for _, v in pairs(options) do if opt == v then return v end end
return nil
end
-- wait for any key to be pressed
---@diagnostic disable-next-line: undefined-field
local function any_key() os.pullEvent("key_up") end
-- ask the user yes or no
local function ask_y_n(question, default)
print(question)
if default == true then print(" (Y/n)? ") else print(" (y/N)? ") end
local response = read();any_key()
if response == "" then return default
elseif response == "Y" or response == "y" then return true
elseif response == "N" or response == "n" then return false
else return nil end
end
-- read the local manifest file
local function read_local_manifest()
local local_ok = false
local local_manifest = {}
local imfile = fs.open("install_manifest.json", "r")
if imfile ~= nil then
local_ok, local_manifest = pcall(function () return textutils.unserializeJSON(imfile.readAll()) end)
imfile.close()
end
return local_ok, local_manifest
end
-- recursively build a tree out of the file manifest
local function gen_tree(manifest, log)
local function _tree_add(tree, split)
if #split > 1 then
local name = table.remove(split, 1)
if tree[name] == nil then tree[name] = {} end
table.insert(tree[name], _tree_add(tree[name], split))
else return split[1] end
return nil
end
local list, tree = { log }, {}
-- make a list of each and every file
for _, files in pairs(manifest.files) do for i = 1, #files do table.insert(list, files[i]) end end
for i = 1, #list do
local split = {}
---@diagnostic disable-next-line: discard-returns
string.gsub(list[i], "([^/]+)", function(c) split[#split + 1] = c end)
if #split == 1 then table.insert(tree, list[i])
else table.insert(tree, _tree_add(tree, split)) end
end
return tree
end
local function _in_array(val, array)
for _, v in pairs(array) do if v == val then return true end end
return false
end
local function _clean_dir(dir, tree)
if tree == nil then tree = {} end
local ls = fs.list(dir)
for _, val in pairs(ls) do
local path = dir.."/"..val
if fs.isDir(path) then
_clean_dir(path, tree[val])
if #fs.list(path) == 0 then fs.delete(path);println("deleted "..path) end
elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then
fs.delete(path)
println("deleted "..path)
end
end
end
-- go through app/common directories to delete unused files
local function clean(manifest)
local log = nil
if fs.exists(app..".settings") and settings.load(app..".settings") then
log = settings.get("LogPath")
if log:sub(1, 1) == "/" then log = log:sub(2) end
end
local tree = gen_tree(manifest, log)
table.insert(tree, "install_manifest.json")
table.insert(tree, "ccmsim.lua")
local ls = fs.list("/")
for _, val in pairs(ls) do
if fs.isDriveRoot(val) then
yellow();println("skipped mount '"..val.."'")
elseif fs.isDir(val) then
if tree[val] ~= nil then lgray();_clean_dir("/"..val, tree[val])
else white(); if ask_y_n("delete the unused directory '"..val.."'") then lgray();_clean_dir("/"..val) end end
if #fs.list(val) == 0 then fs.delete(val);lgray();println("deleted empty directory '"..val.."'") end
elseif not _in_array(val, tree) and (string.find(val, ".settings") == nil) then
white();if ask_y_n("delete the unused file '"..val.."'") then fs.delete(val);lgray();println("deleted "..val) end
end
end
white()
end
-- get and validate command line options
println("-- CC Mekanism SCADA Install Manager (Off-Line) --")
if #opts == 0 or opts[1] == "help" then
println("usage: ccmsim <mode>")
println("<mode>")
lgray()
println(" check - check your installed versions")
println(" update-rm - delete everything except the config,")
println(" so that you can upload files for a")
println(" new two-file off-line update")
println(" uninstall - delete all app files and config")
return
else
mode = get_opt(opts[1], { "check", "update-rm", "uninstall" })
if mode == nil then
red();println("Unrecognized mode.");white()
return
end
end
-- run selected mode
if mode == "check" then
local local_ok, manifest = read_local_manifest()
if not local_ok then
yellow();println("failed to load local installation information");white()
end
-- list all versions
for key, value in pairs(manifest.versions) do
term.setTextColor(colors.purple)
print(string.format("%-14s", "["..key.."]"))
blue();println(value);white()
end
elseif mode == "update-rm" or mode == "uninstall" then
local ok, manifest = read_local_manifest()
if not ok then
red();println("Error parsing local installation manifest.");white()
return
end
app = manifest.depends[#manifest.depends]
if mode == "uninstall" then
orange();println("Uninstalling all app files...")
else
orange();println("Deleting all app files except for configuration...")
end
-- ask for confirmation
if not ask_y_n("Continue", false) then return end
-- delete unused files first
clean(manifest)
local file_list = manifest.files
local dependencies = manifest.depends
-- delete all installed files
lgray()
for _, dependency in pairs(dependencies) do
local files = file_list[dependency]
for _, file in pairs(files) do
if fs.exists(file) then fs.delete(file);println("deleted "..file) end
end
local folder = files[1]
while true do
local dir = fs.getDir(folder)
if dir == "" or dir == ".." then break else folder = dir end
end
if fs.isDir(folder) then
fs.delete(folder)
println("deleted directory "..folder)
end
end
-- delete log file
local log_deleted = false
local settings_file = app..".settings"
if fs.exists(settings_file) and settings.load(settings_file) then
local log = settings.get("LogPath")
if log ~= nil then
log_deleted = true
if fs.exists(log) then
fs.delete(log)
println("deleted log file "..log)
end
end
end
if not log_deleted then
red();println("Failed to delete log file (it may not exist).");lgray()
end
if mode == "uninstall" then
if fs.exists(settings_file) then
fs.delete(settings_file);println("deleted "..settings_file)
end
fs.delete("install_manifest.json")
println("deleted install_manifest.json")
fs.delete("ccmsim.lua")
println("deleted ccmsim.lua")
end
green();println("Done!")
end
white()

14
build/package.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
# Create zips to attach to GitHub releases.
# These can be extracted onto a computer and will include all files CCMSI would otherwise install.
tag=$(git describe --tags)
apps=(coordinator pocket reactor-plc rtu supervisor)
for app in "${apps[@]}" do
mkdir ${tag}_${app}
cp -R $app scada-common graphics lockbox configure.lua initenv.lua startup.lua LICENSE ${tag}_${app}
zip -r ${tag}_${app}.zip ${tag}_${app}
rm -R ${tag}_${app}
done

83
build/safemin.py Normal file
View File

@@ -0,0 +1,83 @@
import os
import re
# minify files in a directory
def min_files(path):
start_sum, end_sum = 0, 0
for (root, _, files) in os.walk(path):
os.makedirs('_minified/' + root, exist_ok=True)
for f in files:
start, end = minify(root + "/" + f)
start_sum = start_sum + start
end_sum = end_sum + end
delta = start_sum - end_sum
print(f"> done with '{path}': shrunk from {start_sum} bytes to {end_sum} bytes (saved {delta} bytes, or {(100*delta/start_sum):.2f}%)")
return list
# minify a file
def minify(path: str):
size_start = os.stat(path).st_size
f = open(path, "r")
contents = f.read()
f.close()
# remove --[[@as type]] hints before anything, since it would detect as multiline comments
contents = re.sub(r' --+\[.+]]', '', contents)
if re.search(r'--+\[+', contents) != None:
# absolutely not dealing with lua multiline comments
# - there are more important things to do
# - this minification is intended to be 100% safe, so working with multiline comments is asking for trouble
# - the project doesn't use them as of writing this (except in test/), and it might as well stay that way
raise Exception(f"no multiline comments allowed! (offending file: {path})")
if re.search(r'\\$', contents, flags=re.MULTILINE) != None:
# '\' allows for multiline strings, which would require reverting to processing syntax line by line to support them
raise Exception(f"no escaping newlines! (offending file: {path})")
# drop the comments, unless the line has quotes, because quotes are scary
# (quotes are scary since we could actually be inside a string: "-- ..." shouldn't get deleted)
# -> whitespace before '--' and anything after that, which includes '---' comments
minified = re.sub(r'\s*--+(?!.*[\'"]).*', '', contents)
# drop leading whitespace on each line
minified = re.sub(r'^ +', '', minified, flags=re.MULTILINE)
# drop blank lines
while minified != re.sub(r'\n\n', '\n', minified):
minified = re.sub(r'\n\n', '\n', minified)
# write the minified file
f_min = open(f"_minified/{path}", "w")
f_min.write(minified)
f_min.close()
size_end = os.stat(f"_minified/{path}").st_size
print(f">> shrunk '{path}' from {size_start} bytes to {size_end} bytes (saved {size_start-size_end} bytes)")
return size_start, size_end
# minify applications and libraries
dirs = [ 'scada-common', 'graphics', 'lockbox', 'reactor-plc', 'rtu', 'supervisor', 'coordinator', 'pocket' ]
for _, d in enumerate(dirs):
min_files(d)
# minify root files
minify("startup.lua")
minify("initenv.lua")
minify("configure.lua")
# copy in license for build usage
lic1 = open("LICENSE", "r")
lic2 = open("_minified/LICENSE", "w")
lic2.write(lic1.read())
lic1.close()
lic2.close()

568
ccmsi.lua
View File

@@ -15,14 +15,62 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]-- ]]--
local function println(message) print(tostring(message)) end local CCMSI_VERSION = "v1.21"
local function print(message) term.write(tostring(message)) end
local CCMSI_VERSION = "v1.14"
local install_dir = "/.install-cache" local install_dir = "/.install-cache"
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/" local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/" local repo_path = "https://git.liulikeji.cn/GitHub/cc-mek-scada/raw/branch/"
---@diagnostic disable-next-line: undefined-global
local _is_pkt_env = pocket -- luacheck: ignore pocket
local function println(msg) print(tostring(msg)) end
-- stripped down & modified copy of log.dmesg
local function print(msg)
msg = tostring(msg)
local cur_x, cur_y = term.getCursorPos()
local out_w, out_h = term.getSize()
-- jump to next line if needed
if cur_x == out_w then
cur_x = 1
if cur_y == out_h then
term.scroll(1)
term.setCursorPos(1, cur_y)
else
term.setCursorPos(1, cur_y + 1)
end
end
-- wrap
local lines, remaining, s_start, s_end, ln = {}, true, 1, out_w + 1 - cur_x, 1
while remaining do
local line = string.sub(msg, s_start, s_end)
if line == "" then
remaining = false
else
lines[ln] = line
s_start = s_end + 1
s_end = s_end + out_w
ln = ln + 1
end
end
-- print
for i = 1, #lines do
cur_x, cur_y = term.getCursorPos()
if i > 1 and cur_x > 1 then
if cur_y == out_h then
term.scroll(1)
term.setCursorPos(1, cur_y)
else term.setCursorPos(1, cur_y + 1) end
end
term.write(lines[i])
end
end
local opts = { ... } local opts = { ... }
local mode, app, target local mode, app, target
@@ -101,16 +149,16 @@ local function get_remote_manifest()
end end
-- record the local installation manifest -- record the local installation manifest
local function write_install_manifest(manifest, dependencies) local function write_install_manifest(manifest, deps)
local versions = {} local versions = {}
for key, value in pairs(manifest.versions) do for key, value in pairs(manifest.versions) do
local is_dependency = false local is_dep = false
for _, dependency in pairs(dependencies) do for _, dep in pairs(deps) do
if (key == "bootloader" and dependency == "system") or key == dependency then if (key == "bootloader" and dep == "system") or key == dep then
is_dependency = true;break is_dep = true;break
end end
end end
if key == app or key == "comms" or is_dependency then versions[key] = value end if key == app or key == "comms" or is_dep then versions[key] = value end
end end
manifest.versions = versions manifest.versions = versions
@@ -120,8 +168,41 @@ local function write_install_manifest(manifest, dependencies)
imfile.close() imfile.close()
end end
-- try at most 3 times to download a file from the repository and write into w_path base directory
---@return 0|1|2|3 success 0: ok, 1: download fail, 2: file open fail, 3: out of space
local function http_get_file(file, w_path)
local dl, err
for i = 1, 3 do
dl, err = http.get(repo_path..file)
if dl then
if i > 1 then green();println("success!");lgray() end
local f = fs.open(w_path..file, "w")
if not f then return 2 end
local ok, msg = pcall(function() f.write(dl.readAll()) end)
f.close()
if not ok then
if string.find(msg or "", "Out of space") ~= nil then
red();println("[out of space]");lgray()
return 3
else return 2 end
end
break
else
red();println("HTTP Error: "..err)
if i < 3 then
lgray();print("> retrying...")
---@diagnostic disable-next-line: undefined-field
os.sleep(i/3.0)
else
return 1
end
end
end
return 0
end
-- recursively build a tree out of the file manifest -- recursively build a tree out of the file manifest
local function gen_tree(manifest) local function gen_tree(manifest, log)
local function _tree_add(tree, split) local function _tree_add(tree, split)
if #split > 1 then if #split > 1 then
local name = table.remove(split, 1) local name = table.remove(split, 1)
@@ -131,13 +212,14 @@ local function gen_tree(manifest)
return nil return nil
end end
local list, tree = {}, {} local list, tree = { log }, {}
-- make a list of each and every file -- 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 _, files in pairs(manifest.files) do for i = 1, #files do table.insert(list, files[i]) end end
for i = 1, #list do for i = 1, #list do
local split = {} local split = {}
---@diagnostic disable-next-line: discard-returns
string.gsub(list[i], "([^/]+)", function(c) split[#split + 1] = c end) string.gsub(list[i], "([^/]+)", function(c) split[#split + 1] = c end)
if #split == 1 then table.insert(tree, list[i]) if #split == 1 then table.insert(tree, list[i])
else table.insert(tree, _tree_add(tree, split)) end else table.insert(tree, _tree_add(tree, split)) end
@@ -159,7 +241,7 @@ local function _clean_dir(dir, tree)
if fs.isDir(path) then if fs.isDir(path) then
_clean_dir(path, tree[val]) _clean_dir(path, tree[val])
if #fs.list(path) == 0 then fs.delete(path);println("deleted "..path) end if #fs.list(path) == 0 then fs.delete(path);println("deleted "..path) end
elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then ---@todo remove config.lua on full release
fs.delete(path) fs.delete(path)
println("deleted "..path) println("deleted "..path)
end end
@@ -168,11 +250,16 @@ end
-- go through app/common directories to delete unused files -- go through app/common directories to delete unused files
local function clean(manifest) local function clean(manifest)
local tree = gen_tree(manifest) local log = nil
if fs.exists(app..".settings") and settings.load(app..".settings") then
log = settings.get("LogPath")
if log:sub(1, 1) == "/" then log = log:sub(2) end
end
local tree = gen_tree(manifest, log)
table.insert(tree, "install_manifest.json") table.insert(tree, "install_manifest.json")
table.insert(tree, "ccmsi.lua") table.insert(tree, "ccmsi.lua")
table.insert(tree, "log.txt") ---@fixme fix after migration to settings files?
local ls = fs.list("/") local ls = fs.list("/")
for _, val in pairs(ls) do for _, val in pairs(ls) do
@@ -192,10 +279,27 @@ end
-- get and validate command line options -- get and validate command line options
println("-- CC Mekanism SCADA Installer "..CCMSI_VERSION.." --") if _is_pkt_env then println("- SCADA Installer "..CCMSI_VERSION.." -")
else println("-- CC Mekanism SCADA Installer "..CCMSI_VERSION.." --") end
if #opts == 0 or opts[1] == "help" then if #opts == 0 or opts[1] == "help" then
println("usage: ccmsi <mode> <app> <branch>") println("usage: ccmsi <mode> <app> <branch>")
if _is_pkt_env then
yellow();println("<mode>");lgray()
println(" check - check latest")
println(" install - fresh install")
println(" update - update app")
println(" uninstall - remove app")
yellow();println("<app>");lgray()
println(" reactor-plc")
println(" rtu")
println(" supervisor")
println(" coordinator")
println(" pocket")
println(" installer (update only)")
yellow();println("<branch>");lgray();
println(" main (default) | devel");white()
else
println("<mode>") println("<mode>")
lgray() lgray()
println(" check - check latest versions available") println(" check - check latest versions available")
@@ -214,25 +318,41 @@ if #opts == 0 or opts[1] == "help" then
println(" installer - ccmsi installer (update only)") println(" installer - ccmsi installer (update only)")
white();println("<branch>") white();println("<branch>")
lgray();println(" main (default) | devel");white() lgray();println(" main (default) | devel");white()
end
return return
else else
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" }) mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
if mode == nil then if mode == nil then
red();println("Unrecognized mode.");white() red();println("Unrecognized mode.");white()
return return
end end
app = get_opt(opts[2], { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket", "installer" }) local next_opt = 3
local apps = { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket", "installer" }
app = get_opt(opts[2], apps)
if app == nil then
for _, a in pairs(apps) do
if fs.exists(a) and fs.isDir(a) then
app = a
next_opt = 2
break
end
end
end
if app == nil and mode ~= "check" then if app == nil and mode ~= "check" then
red();println("Unrecognized application.");white() red();println("Unrecognized application.");white()
return return
elseif mode == "check" then
next_opt = 2
elseif app == "installer" and mode ~= "update" then elseif app == "installer" and mode ~= "update" then
red();println("Installer app only supports 'update' option.");white() red();println("Installer app only supports 'update' option.");white()
return return
end end
-- determine target -- determine target
if mode == "check" then target = opts[2] else target = opts[3] end target = opts[next_opt]
if (target ~= "main") and (target ~= "devel") then if (target ~= "main") and (target ~= "devel") then
if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end
target = "main" target = "main"
@@ -244,7 +364,6 @@ else
end end
-- run selected mode -- run selected mode
if mode == "check" then if mode == "check" then
local ok, manifest = get_remote_manifest() local ok, manifest = get_remote_manifest()
if not ok then return end if not ok then return end
@@ -260,25 +379,29 @@ if mode == "check" then
-- list all versions -- list all versions
for key, value in pairs(manifest.versions) do for key, value in pairs(manifest.versions) do
term.setTextColor(colors.purple) term.setTextColor(colors.purple)
print(string.format("%-14s", "["..key.."]")) local tag = string.format("%-14s", "["..key.."]")
if not _is_pkt_env then print(tag) end
if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then
if _is_pkt_env then println(tag) end
blue();print(local_manifest.versions[key]) blue();print(local_manifest.versions[key])
if value ~= local_manifest.versions[key] then if value ~= local_manifest.versions[key] then
white();print(" (") white();print(" (")
cyan();print(value);white();println(" available)") cyan();print(value);white();println(" available)")
else green();println(" (up to date)") end else green();println(" (up to date)") end
else elseif not _is_pkt_env then
lgray();print("not installed");white();print(" (latest ") lgray();print("not installed");white();print(" (latest ")
cyan();print(value);white();println(")") cyan();print(value);white();println(")")
end end
end end
if manifest.versions.installer ~= local_manifest.versions.installer then if manifest.versions.installer ~= local_manifest.versions.installer and not _is_pkt_env then
yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white() yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white()
end end
elseif mode == "install" or mode == "update" then elseif mode == "install" or mode == "update" then
local ok, r_manifest, l_manifest
local update_installer = app == "installer" local update_installer = app == "installer"
local ok, manifest = get_remote_manifest() ok, r_manifest = get_remote_manifest()
if not ok then return end if not ok then return end
local ver = { local ver = {
@@ -291,34 +414,34 @@ elseif mode == "install" or mode == "update" then
} }
-- try to find local versions -- try to find local versions
local local_ok, lmnf = read_local_manifest() ok, l_manifest = read_local_manifest()
if not local_ok then if mode == "update" and not update_installer then
if mode == "update" then if not ok then
red();println("Failed to load local installation information, cannot update.");white() red();println("Failed to load local installation information, cannot update.");white()
return return
end else
elseif not update_installer then ver.boot.v_local = l_manifest.versions.bootloader
ver.boot.v_local = lmnf.versions.bootloader ver.app.v_local = l_manifest.versions[app]
ver.app.v_local = lmnf.versions[app] ver.comms.v_local = l_manifest.versions.comms
ver.comms.v_local = lmnf.versions.comms ver.common.v_local = l_manifest.versions.common
ver.common.v_local = lmnf.versions.common ver.graphics.v_local = l_manifest.versions.graphics
ver.graphics.v_local = lmnf.versions.graphics ver.lockbox.v_local = l_manifest.versions.lockbox
ver.lockbox.v_local = lmnf.versions.lockbox
if lmnf.versions[app] == nil then if l_manifest.versions[app] == nil then
red();println("Another application is already installed, please uninstall it before installing a new application.");white() red();println("Another application is already installed, please uninstall it before installing a new application.");white()
return return
end
end end
end end
if manifest.versions.installer ~= CCMSI_VERSION then if r_manifest.versions.installer ~= CCMSI_VERSION then
if not update_installer then yellow();println("A different version of the installer is available, it is recommended to update to it.");white() end if not update_installer then yellow();println("A different version of the installer is available, it is recommended to update to it.");white() end
if update_installer or ask_y_n("Would you like to update now") then if update_installer or ask_y_n("Would you like to update now", true) then
lgray();println("GET ccmsi.lua") lgray();println("GET ccmsi.lua")
local dl, err = http.get(repo_path.."ccmsi.lua") local dl, err = http.get(repo_path.."ccmsi.lua")
if dl == nil then if dl == nil then
red();println("HTTP Error "..err) red();println("HTTP Error: "..err)
println("Installer download failed.");white() println("Installer download failed.");white()
else else
local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location
@@ -334,12 +457,12 @@ elseif mode == "install" or mode == "update" then
return return
end end
ver.boot.v_remote = manifest.versions.bootloader ver.boot.v_remote = r_manifest.versions.bootloader
ver.app.v_remote = manifest.versions[app] ver.app.v_remote = r_manifest.versions[app]
ver.comms.v_remote = manifest.versions.comms ver.comms.v_remote = r_manifest.versions.comms
ver.common.v_remote = manifest.versions.common ver.common.v_remote = r_manifest.versions.common
ver.graphics.v_remote = manifest.versions.graphics ver.graphics.v_remote = r_manifest.versions.graphics
ver.lockbox.v_remote = manifest.versions.lockbox ver.lockbox.v_remote = r_manifest.versions.lockbox
green() green()
if mode == "install" then print("Installing ") else print("Updating ") end if mode == "install" then print("Installing ") else print("Updating ") end
@@ -355,92 +478,197 @@ elseif mode == "install" or mode == "update" then
ver.graphics.changed = show_pkg_change("graphics", ver.graphics) ver.graphics.changed = show_pkg_change("graphics", ver.graphics)
ver.lockbox.changed = show_pkg_change("lockbox", ver.lockbox) ver.lockbox.changed = show_pkg_change("lockbox", ver.lockbox)
-- ask for confirmation -- start install/update
if not ask_y_n("Continue", false) then return end
-------------------------- local space_req = r_manifest.sizes.manifest
-- START INSTALL/UPDATE -- local space_avail = fs.getFreeSpace("/")
--------------------------
local space_required = manifest.sizes.manifest local file_list = r_manifest.files
local space_available = fs.getFreeSpace("/") local size_list = r_manifest.sizes
local deps = r_manifest.depends[app]
local single_file_mode = false table.insert(deps, app)
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 -- helper function to check if a dependency is unchanged
local function unchanged(dependency) local function unchanged(dep)
if dependency == "system" then return not ver.boot.changed if dep == "system" then return not ver.boot.changed
elseif dependency == "graphics" then return not ver.graphics.changed elseif dep == "graphics" then return not ver.graphics.changed
elseif dependency == "lockbox" then return not ver.lockbox.changed elseif dep == "lockbox" then return not ver.lockbox.changed
elseif dependency == "common" then return not (ver.common.changed or ver.comms.changed) elseif dep == "common" then return not (ver.common.changed or ver.comms.changed)
elseif dependency == app then return not ver.app.changed elseif dep == app then return not ver.app.changed
else return true end else return true end
end end
if not single_file_mode then local any_change = false
if fs.exists(install_dir) then fs.delete(install_dir);fs.makeDir(install_dir) end
-- download all dependencies for _, dep in pairs(deps) do
for _, dependency in pairs(dependencies) do local size = size_list[dep]
if mode == "update" and unchanged(dependency) then space_req = space_req + size
pkg_message("skipping download of unchanged package", dependency) any_change = any_change or not unchanged(dep)
end
if mode == "update" and not any_change then
yellow();println("Nothing to do, everything is already up-to-date!");white()
return
end
-- ask for confirmation
if not ask_y_n("Continue", false) then return end
local single_file_mode = space_avail < space_req
local success = true
-- delete a file if the capitalization changes so that things work on Windows
---@param path string
local function mitigate_case(path)
local dir, file = fs.getDir(path), fs.getName(path)
if not fs.isDir(dir) then return end
for _, p in ipairs(fs.list(dir)) do
if string.lower(p) == string.lower(file) then
if p ~= file then fs.delete(path) end
return
end
end
end
---@param dl_stat 1|2|3 download status
---@param file string file name
---@param attempt integer recursive attempt #
---@param sf_install function installer function for recursion
local function handle_dl_fail(dl_stat, file, attempt, sf_install)
red()
if dl_stat == 1 then
println("failed to download "..file)
elseif dl_stat > 1 then
if dl_stat == 2 then println("filesystem error with "..file) else println("no space for "..file) end
if attempt == 1 then
orange();println("re-attempting operation...");white()
sf_install(2)
elseif attempt == 2 then
yellow()
if dl_stat == 2 then println("There was an error writing to a file.") else println("Insufficient space available.") end
lgray()
if dl_stat == 2 then
println("This may be due to insufficent space available or file permission issues. The installer can now attempt to delete files not used by the SCADA system.")
else
println("The installer can now attempt to delete files not used by the SCADA system.")
end
white()
if not ask_y_n("Continue", false) then
success = false
return
end
clean(r_manifest)
sf_install(3)
elseif attempt == 3 then
yellow()
if dl_stat == 2 then println("There again was an error writing to a file.") else println("Insufficient space available.") end
lgray()
if dl_stat == 2 then
println("This may be due to insufficent space available or file permission issues. Please delete any unused files you have on this computer then try again. Do not delete the "..app..".settings file unless you want to re-configure.")
else
println("Please delete any unused files you have on this computer then try again. Do not delete the "..app..".settings file unless you want to re-configure.")
end
white()
success = false
end
end
end
-- single file update routine: go through all files and replace one by one
---@param attempt integer recursive attempt #
local function sf_install(attempt)
---@diagnostic disable-next-line: undefined-field
if attempt > 1 then os.sleep(2.0) end
local abort_attempt = false
success = true
for _, dep in pairs(deps) do
if mode == "update" and unchanged(dep) then
pkg_message("skipping install of unchanged package", dep)
else else
pkg_message("downloading package", dependency) pkg_message("installing package", dep)
lgray() lgray()
local files = file_list[dependency] -- beginning on the second try, delete the directory before starting
if attempt >= 2 then
if dep == "system" then
elseif dep == "common" then
if fs.exists("/scada-common") then
fs.delete("/scada-common")
println("deleted /scada-common")
end
else
if fs.exists("/"..dep) then
fs.delete("/"..dep)
println("deleted /"..dep)
end
end
end
local files = file_list[dep]
for _, file in pairs(files) do for _, file in pairs(files) do
println("GET "..file) println("GET "..file)
local dl, err = http.get(repo_path..file) mitigate_case(file)
local dl_stat = http_get_file(file, "/")
if dl == nil then if dl_stat ~= 0 then
red();println("HTTP Error "..err) abort_attempt = true
success = false ---@diagnostic disable-next-line: param-type-mismatch
handle_dl_fail(dl_stat, file, attempt, sf_install)
break break
else
local handle = fs.open(install_dir.."/"..file, "w")
handle.write(dl.readAll())
handle.close()
end end
end end
end end
if abort_attempt or not success then break end
end
end
-- handle update/install
if single_file_mode then sf_install(1)
else
if fs.exists(install_dir) then fs.delete(install_dir);fs.makeDir(install_dir) end
-- download all dependencies
for _, dep in pairs(deps) do
if mode == "update" and unchanged(dep) then
pkg_message("skipping download of unchanged package", dep)
else
pkg_message("downloading package", dep)
lgray()
local files = file_list[dep]
for _, file in pairs(files) do
println("GET "..file)
local dl_stat = http_get_file(file, install_dir.."/")
success = dl_stat == 0
if dl_stat == 1 then
red();println("failed to download "..file)
break
elseif dl_stat == 2 then
red();println("filesystem error with "..file)
break
elseif dl_stat == 3 then
-- this shouldn't occur in this mode
red();println("no space for "..file)
break
end
end
end
if not success then break end
end end
-- copy in downloaded files (installation) -- copy in downloaded files (installation)
if success then if success then
for _, dependency in pairs(dependencies) do for _, dep in pairs(deps) do
if mode == "update" and unchanged(dependency) then if mode == "update" and unchanged(dep) then
pkg_message("skipping install of unchanged package", dependency) pkg_message("skipping install of unchanged package", dep)
else else
pkg_message("installing package", dependency) pkg_message("installing package", dep)
lgray() lgray()
local files = file_list[dependency] local files = file_list[dep]
for _, file in pairs(files) do for _, file in pairs(files) do
local temp_file = install_dir.."/"..file local temp_file = install_dir.."/"..file
if fs.exists(file) then fs.delete(file) end if fs.exists(file) then fs.delete(file) end
@@ -451,62 +679,27 @@ elseif mode == "install" or mode == "update" then
end end
fs.delete(install_dir) fs.delete(install_dir)
end
if success then if success then
write_install_manifest(manifest, dependencies) write_install_manifest(r_manifest, deps)
green() green()
if mode == "install" then if mode == "install" then
println("Installation completed successfully.") println("Installation completed successfully.")
else println("Update completed successfully.") end else println("Update completed successfully.") end
white();println("Ready to clean up unused files, press any key to continue...") white();println("Ready to clean up unused files, press any key to continue...")
any_key();clean(manifest) any_key();clean(r_manifest)
white();println("Done.") white();println("Done.")
else
if mode == "install" then
red();println("Installation failed.")
else orange();println("Update failed, existing files unmodified.") end
end
else else
-- go through all files and replace one by one red()
for _, dependency in pairs(dependencies) do if single_file_mode then
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 if mode == "install" then
println("Installation failed, files may have been skipped.") println("Installation failed, files may have been skipped.")
else println("Update failed, files may have been skipped.") end else println("Update failed, files may have been skipped.") end
else
if mode == "install" then
println("Installation failed.")
else orange();println("Update failed, existing files unmodified.") end
end end
end end
elseif mode == "uninstall" then elseif mode == "uninstall" then
@@ -530,42 +723,14 @@ elseif mode == "uninstall" then
clean(manifest) clean(manifest)
local file_list = manifest.files local file_list = manifest.files
local dependencies = manifest.depends[app] local deps = manifest.depends[app]
table.insert(dependencies, app) table.insert(deps, 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 -- delete all installed files
for _, dependency in pairs(dependencies) do lgray()
local files = file_list[dependency] for _, dep in pairs(deps) do
local files = file_list[dep]
for _, file in pairs(files) do for _, file in pairs(files) do
if fs.exists(file) then fs.delete(file);println("deleted "..file) end if fs.exists(file) then fs.delete(file);println("deleted "..file) end
end end
@@ -582,8 +747,23 @@ elseif mode == "uninstall" then
end end
end end
if fs.exists(legacy_config_file) then -- delete log file
fs.delete(legacy_config_file);println("deleted "..legacy_config_file) local log_deleted = false
local settings_file = app..".settings"
if fs.exists(settings_file) and settings.load(settings_file) then
local log = settings.get("LogPath")
if log ~= nil then
log_deleted = true
if fs.exists(log) then
fs.delete(log)
println("deleted log file "..log)
end
end
end
if not log_deleted then
red();println("Failed to delete log file (it may not exist).");lgray()
end end
if fs.exists(settings_file) then if fs.exists(settings_file) then

View File

@@ -1,11 +1,12 @@
print("CONFIGURE> SCANNING FOR CONFIGURATOR...") print("CONFIGURE> SCANNING FOR CONFIGURATOR...")
if fs.exists("reactor-plc/configure.lua") then require("reactor-plc.configure").configure() for _, app in ipairs({ "reactor-plc", "rtu", "supervisor", "coordinator", "pocket" }) do
elseif fs.exists("rtu/configure.lua") then require("rtu.configure").configure() if fs.exists(app .. "/configure.lua") then
elseif fs.exists("supervisor/configure.lua") then require("supervisor.configure").configure() local _, _, launch = require(app .. ".configure").configure()
elseif fs.exists("coordinator/configure.lua") then require("coordinator.configure").configure() if launch then shell.execute("/startup") end
elseif fs.exists("pocket/configure.lua") then require("pocket.configure").configure() return
else end
print("CONFIGURE> NO CONFIGURATOR FOUND")
print("CONFIGURE> EXIT")
end end
print("CONFIGURE> NO CONFIGURATOR FOUND")
print("CONFIGURE> EXIT")

View File

@@ -0,0 +1,318 @@
local comms = require("scada-common.comms")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton")
local NumberField = require("graphics.elements.form.NumberField")
local tri = util.trinary
local cpair = core.cpair
local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE
local self = {
nic = nil, ---@type nic
net_listen = false,
sv_addr = comms.BROADCAST,
sv_seq_num = util.time_ms() * 10,
show_sv_cfg = nil, ---@type function
sv_conn_button = nil, ---@type PushButton
sv_conn_status = nil, ---@type TextBox
sv_conn_detail = nil, ---@type TextBox
sv_next = nil, ---@type PushButton
sv_skip = nil, ---@type PushButton
tool_ctl = nil, ---@type _crd_cfg_tool_ctl
tmp_cfg = nil ---@type crd_config
}
-- check if a value is an integer within a range (inclusive)
---@param x any
---@param min integer
---@param max integer
local function is_int_min_max(x, min, max) return util.is_int(x) and x >= min and x <= max end
-- send a management packet to the supervisor
---@param msg_type MGMT_TYPE
---@param msg table
local function send_sv(msg_type, msg)
local s_pkt = comms.scada_packet()
local pkt = comms.mgmt_packet()
pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.sv_seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
self.nic.transmit(self.tmp_cfg.SVR_Channel, self.tmp_cfg.CRD_Channel, s_pkt)
self.sv_seq_num = self.sv_seq_num + 1
end
-- handle an establish message from the supervisor
---@param packet mgmt_frame
local function handle_packet(packet)
local error_msg = nil
if packet.scada_frame.local_channel() ~= self.tmp_cfg.CRD_Channel then
error_msg = "Error: unknown receive channel."
elseif packet.scada_frame.remote_channel() == self.tmp_cfg.SVR_Channel and packet.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
if packet.type == MGMT_TYPE.ESTABLISH then
if packet.length == 2 then
local est_ack = packet.data[1]
local config = packet.data[2]
if est_ack == ESTABLISH_ACK.ALLOW then
if type(config) == "table" and #config == 2 then
local count_ok = is_int_min_max(config[1], 1, 4)
local cool_ok = type(config[2]) == "table" and type(config[2].r_cool) == "table" and #config[2].r_cool == config[1]
if count_ok and cool_ok then
self.tmp_cfg.UnitCount = config[1]
self.tool_ctl.sv_cool_conf = {}
for i = 1, self.tmp_cfg.UnitCount do
local num_b = config[2].r_cool[i].BoilerCount
local num_t = config[2].r_cool[i].TurbineCount
self.tool_ctl.sv_cool_conf[i] = { num_b, num_t }
cool_ok = cool_ok and is_int_min_max(num_b, 0, 2) and is_int_min_max(num_t, 1, 3)
end
end
if not count_ok then
error_msg = "Error: supervisor unit count out of range."
elseif not cool_ok then
error_msg = "Error: supervisor cooling configuration malformed."
self.tool_ctl.sv_cool_conf = nil
end
self.sv_addr = packet.scada_frame.src_addr()
send_sv(MGMT_TYPE.CLOSE, {})
else
error_msg = "Error: invalid cooling configuration supervisor."
end
else
error_msg = "Error: invalid allow reply length from supervisor."
end
elseif packet.length == 1 then
local est_ack = packet.data[1]
if est_ack == ESTABLISH_ACK.DENY then
error_msg = "Error: supervisor connection denied."
elseif est_ack == ESTABLISH_ACK.COLLISION then
error_msg = "Error: a coordinator is already/still connected. Please try again."
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
error_msg = "Error: coordinator comms version does not match supervisor comms version."
else
error_msg = "Error: invalid reply from supervisor."
end
else
error_msg = "Error: invalid reply length from supervisor."
end
else
error_msg = "Error: didn't get an establish reply from supervisor."
end
end
self.net_listen = false
if error_msg then
self.sv_conn_status.set_value("")
self.sv_conn_detail.set_value(error_msg)
self.sv_conn_button.enable()
else
self.sv_conn_status.set_value("Connected!")
self.sv_conn_detail.set_value("Data received successfully, press 'Next' to continue.")
self.sv_skip.hide()
self.sv_next.show()
end
end
-- handle supervisor connection failure
local function handle_timeout()
self.net_listen = false
self.sv_conn_button.enable()
self.sv_conn_status.set_value("Timed out.")
self.sv_conn_detail.set_value("Supervisor did not reply. Ensure startup app is running on the supervisor.")
end
-- attempt a connection to the supervisor to get cooling info
local function sv_connect()
self.sv_conn_button.disable()
self.sv_conn_detail.set_value("")
local modem = ppm.get_wireless_modem()
if modem == nil then
self.sv_conn_status.set_value("Please connect an ender/wireless modem.")
else
self.sv_conn_status.set_value("Modem found, connecting...")
if self.nic == nil then self.nic = network.nic(modem) end
self.nic.closeAll()
self.nic.open(self.tmp_cfg.CRD_Channel)
self.sv_addr = comms.BROADCAST
self.net_listen = true
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.CRD })
tcd.dispatch_unique(8, handle_timeout)
end
end
local facility = {}
-- create the facility configuration view
---@param tool_ctl _crd_cfg_tool_ctl
---@param main_pane MultiPane
---@param cfg_sys [ crd_config, crd_config, crd_config, { [1]: string, [2]: string, [3]: any }[], function ]
---@param fac_cfg Div
---@param style { [string]: cpair }
---@return MultiPane fac_pane
function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
local _, ini_cfg, tmp_cfg, _, _ = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5]
self.tmp_cfg = tmp_cfg
self.tool_ctl = tool_ctl
local bw_fg_bg = style.bw_fg_bg
local g_lg_fg_bg = style.g_lg_fg_bg
local nav_fg_bg = style.nav_fg_bg
local btn_act_fg_bg = style.btn_act_fg_bg
local btn_dis_fg_bg = style.btn_dis_fg_bg
--#region Facility
local fac_c_1 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_c_2 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_c_3 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_pane = MultiPane{parent=fac_cfg,x=1,y=4,panes={fac_c_1,fac_c_2,fac_c_3}}
TextBox{parent=fac_cfg,x=1,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)}
TextBox{parent=fac_c_1,x=1,y=1,height=4,text="This tool can attempt to connect to your supervisor computer. This would load facility information in order to get the unit count and aid monitor setup."}
TextBox{parent=fac_c_1,x=1,y=6,height=2,text="The supervisor startup app must be running and fully configured on your supervisor computer."}
self.sv_conn_status = TextBox{parent=fac_c_1,x=11,y=9,text=""}
self.sv_conn_detail = TextBox{parent=fac_c_1,x=1,y=11,height=2,text=""}
self.sv_conn_button = PushButton{parent=fac_c_1,x=1,y=9,text="Connect",min_width=9,callback=function()sv_connect()end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
local function sv_skip()
tcd.abort(handle_timeout)
tool_ctl.sv_cool_conf = nil
self.net_listen = false
fac_pane.set_value(2)
end
local function sv_next()
self.show_sv_cfg()
tool_ctl.update_mon_reqs()
fac_pane.set_value(3)
end
PushButton{parent=fac_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.sv_skip = PushButton{parent=fac_c_1,x=44,y=14,text="Skip \x1a",callback=sv_skip,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
self.sv_next = PushButton{parent=fac_c_1,x=44,y=14,text="Next \x1a",callback=sv_next,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,hidden=true}
TextBox{parent=fac_c_2,x=1,y=1,height=3,text="Please enter the number of reactors you have, also referred to as reactor units or 'units' for short. A maximum of 4 is currently supported."}
tool_ctl.num_units = NumberField{parent=fac_c_2,x=1,y=5,width=5,max_chars=2,default=ini_cfg.UnitCount,min=1,max=4,fg_bg=bw_fg_bg}
TextBox{parent=fac_c_2,x=7,y=5,text="reactors"}
TextBox{parent=fac_c_2,x=1,y=7,height=3,text="This will decide how many monitors you need. If this does not match the supervisor's number of reactor units, the coordinator will not connect.",fg_bg=cpair(colors.yellow,colors._INHERIT)}
TextBox{parent=fac_c_2,x=1,y=10,height=3,text="Since you skipped supervisor sync, the main monitor minimum height can't be determined precisely. It is marked with * on the next page.",fg_bg=g_lg_fg_bg}
local nu_error = TextBox{parent=fac_c_2,x=8,y=14,width=35,text="Please set the number of reactors.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_num_units()
local count = tonumber(tool_ctl.num_units.get_value())
if count ~= nil and count > 0 and count < 5 then
nu_error.hide(true)
tmp_cfg.UnitCount = count
tool_ctl.update_mon_reqs()
main_pane.set_value(4)
else nu_error.show() end
end
PushButton{parent=fac_c_2,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_2,x=44,y=14,text="Next \x1a",callback=submit_num_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=fac_c_3,x=1,y=1,height=2,text="The following facility configuration was fetched from your supervisor computer."}
local fac_config_list = ListBox{parent=fac_c_3,x=1,y=4,height=9,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
PushButton{parent=fac_c_3,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_3,x=44,y=14,text="Next \x1a",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Tool and Helper Functions
tool_ctl.is_int_min_max = is_int_min_max
-- reset the connection display for a new attempt
function tool_ctl.init_sv_connect_ui()
self.sv_next.hide()
self.sv_skip.disable()
self.sv_skip.show()
self.sv_conn_button.enable()
self.sv_conn_status.set_value("")
self.sv_conn_detail.set_value("")
-- the user needs to wait a few seconds, encouraging the to connect
tcd.dispatch_unique(2, function () self.sv_skip.enable() end)
end
-- show the facility's unit count and cooling configuration data
function self.show_sv_cfg()
local conf = tool_ctl.sv_cool_conf
fac_config_list.remove_all()
local str = util.sprintf("Facility has %d reactor unit%s:", #conf, tri(#conf==1,"","s"))
TextBox{parent=fac_config_list,text=str,fg_bg=cpair(colors.gray,colors.white)}
for i = 1, #conf do
local num_b, num_t = conf[i][1], conf[i][2]
str = util.sprintf("\x07 Unit %d has %d boiler%s and %d turbine%s", i, num_b, tri(num_b == 1, "", "s"), num_t, tri(num_t == 1, "", "s"))
TextBox{parent=fac_config_list,text=str,fg_bg=cpair(colors.gray,colors.white)}
end
end
--#endregion
return fac_pane
end
-- handle incoming modem messages
---@param side string
---@param sender integer
---@param reply_to integer
---@param message any
---@param distance integer
function facility.receive_sv(side, sender, reply_to, message, distance)
if self.nic ~= nil and self.net_listen then
local s_pkt = self.nic.receive(side, sender, reply_to, message, distance)
if s_pkt and s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then
tcd.abort(handle_timeout)
handle_packet(mgmt_pkt.get())
end
end
end
end
return facility

455
coordinator/config/hmi.lua Normal file
View File

@@ -0,0 +1,455 @@
local ppm = require("scada-common.ppm")
local types = require("scada-common.types")
local util = require("scada-common.util")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local Checkbox = require("graphics.elements.controls.Checkbox")
local PushButton = require("graphics.elements.controls.PushButton")
local RadioButton = require("graphics.elements.controls.RadioButton")
local NumberField = require("graphics.elements.form.NumberField")
local cpair = core.cpair
local self = {
apply_mon = nil, ---@type PushButton
edit_monitor = nil, ---@type function
mon_iface = "",
mon_expect = {} ---@type integer[]
}
local hmi = {}
-- create the HMI (human machine interface) configuration view
---@param tool_ctl _crd_cfg_tool_ctl
---@param main_pane MultiPane
---@param cfg_sys [ crd_config, crd_config, crd_config, { [1]: string, [2]: string, [3]: any }[], function ]
---@param divs Div[]
---@param style { [string]: cpair }
---@return MultiPane mon_pane
function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
local _, ini_cfg, tmp_cfg, _, _ = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5]
local mon_cfg, spkr_cfg, crd_cfg = divs[1], divs[2], divs[3]
local bw_fg_bg = style.bw_fg_bg
local g_lg_fg_bg = style.g_lg_fg_bg
local nav_fg_bg = style.nav_fg_bg
local btn_act_fg_bg = style.btn_act_fg_bg
local btn_dis_fg_bg = style.btn_dis_fg_bg
--#region Monitors
local mon_c_1 = Div{parent=mon_cfg,x=2,y=4,width=49}
local mon_c_2 = Div{parent=mon_cfg,x=2,y=4,width=49}
local mon_c_3 = Div{parent=mon_cfg,x=2,y=4,width=49}
local mon_c_4 = Div{parent=mon_cfg,x=2,y=4,width=49}
local mon_pane = MultiPane{parent=mon_cfg,x=1,y=4,panes={mon_c_1,mon_c_2,mon_c_3,mon_c_4}}
TextBox{parent=mon_cfg,x=1,y=2,text=" Monitor Configuration",fg_bg=cpair(colors.black,colors.blue)}
TextBox{parent=mon_c_1,x=1,y=1,height=5,text="Your configuration requires the following monitors. The main and flow monitors' heights are dependent on your unit count and cooling setup. If you manually entered the unit count, a * will be shown on potentially inaccurate calculations."}
local mon_reqs = ListBox{parent=mon_c_1,x=1,y=7,height=6,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function next_from_reqs()
-- unassign unit monitors above the unit count
for i = tmp_cfg.UnitCount + 1, 4 do tmp_cfg.UnitDisplays[i] = nil end
tool_ctl.gen_mon_list()
mon_pane.set_value(2)
end
PushButton{parent=mon_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=mon_c_1,x=8,y=14,text="Legacy Options",min_width=16,callback=function()mon_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=mon_c_1,x=44,y=14,text="Next \x1a",callback=next_from_reqs,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=mon_c_2,x=1,y=1,height=5,text="Please configure your monitors below. You can go back to the prior page without losing progress to double check what you need. All of those monitors must be assigned before you can proceed."}
local mon_list = ListBox{parent=mon_c_2,x=1,y=6,height=7,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local assign_err = TextBox{parent=mon_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_monitors()
if tmp_cfg.MainDisplay == nil then
assign_err.set_value("Please assign the main monitor.")
elseif tmp_cfg.FlowDisplay == nil and not tmp_cfg.DisableFlowView then
assign_err.set_value("Please assign the flow monitor.")
elseif util.table_len(tmp_cfg.UnitDisplays) ~= tmp_cfg.UnitCount then
for i = 1, tmp_cfg.UnitCount do
if tmp_cfg.UnitDisplays[i] == nil then
assign_err.set_value("Please assign the unit " .. i .. " monitor.")
break
end
end
else
assign_err.hide(true)
main_pane.set_value(5)
return
end
assign_err.show()
end
PushButton{parent=mon_c_2,x=1,y=14,text="\x1b Back",callback=function()mon_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=mon_c_2,x=44,y=14,text="Next \x1a",callback=submit_monitors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local mon_desc = TextBox{parent=mon_c_3,x=1,y=1,height=4,text=""}
local mon_unit_l, mon_unit = nil, nil ---@type TextBox, NumberField
local mon_warn = TextBox{parent=mon_c_3,x=1,y=11,height=2,text="",fg_bg=cpair(colors.red,colors.lightGray)}
---@param val integer assignment type
local function on_assign_mon(val)
if val == 2 and tmp_cfg.DisableFlowView then
self.apply_mon.disable()
mon_warn.set_value("You disabled having a flow view monitor. It can't be set unless you go back and enable it.")
mon_warn.show()
elseif not util.table_contains(self.mon_expect, val) then
self.apply_mon.disable()
mon_warn.set_value("That assignment doesn't fit monitor dimensions. You'll need to resize the monitor for it to work.")
mon_warn.show()
else
self.apply_mon.enable()
mon_warn.hide(true)
end
if val == 3 then
mon_unit_l.show()
mon_unit.show()
else
mon_unit_l.hide(true)
mon_unit.hide(true)
end
local value = mon_unit.get_value()
mon_unit.set_max(tmp_cfg.UnitCount)
if value == "0" or value == nil then mon_unit.set_value(0) end
end
TextBox{parent=mon_c_3,x=1,y=6,width=10,text="Assignment"}
local mon_assign = RadioButton{parent=mon_c_3,x=1,y=7,default=1,options={"Main Monitor","Flow Monitor","Unit Monitor"},callback=on_assign_mon,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.blue}
mon_unit_l = TextBox{parent=mon_c_3,x=18,y=6,width=7,text="Unit ID"}
mon_unit = NumberField{parent=mon_c_3,x=18,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
local mon_u_err = TextBox{parent=mon_c_3,x=8,y=14,width=35,text="Please provide a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
-- purge all assignments for a given monitor
---@param iface string
local function purge_assignments(iface)
if tmp_cfg.MainDisplay == iface then
tmp_cfg.MainDisplay = nil
elseif tmp_cfg.FlowDisplay == iface then
tmp_cfg.FlowDisplay = nil
else
for i = 1, tmp_cfg.UnitCount do
if tmp_cfg.UnitDisplays[i] == iface then tmp_cfg.UnitDisplays[i] = nil end
end
end
end
local function apply_monitor()
local iface = self.mon_iface
local type = mon_assign.get_value()
local u_id = tonumber(mon_unit.get_value())
if type == 1 then
purge_assignments(iface)
tmp_cfg.MainDisplay = iface
elseif type == 2 then
purge_assignments(iface)
tmp_cfg.FlowDisplay = iface
elseif u_id and u_id > 0 then
purge_assignments(iface)
tmp_cfg.UnitDisplays[u_id] = iface
else
mon_u_err.show()
return
end
tool_ctl.gen_mon_list()
mon_u_err.hide(true)
mon_pane.set_value(2)
end
PushButton{parent=mon_c_3,x=1,y=14,text="\x1b Back",callback=function()mon_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.apply_mon = PushButton{parent=mon_c_3,x=43,y=14,min_width=7,text="Apply",callback=apply_monitor,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
TextBox{parent=mon_c_4,x=1,y=1,height=3,text="For legacy compatibility with facilities built without space for a flow monitor, you can disable the flow monitor requirement here."}
TextBox{parent=mon_c_4,x=1,y=5,height=3,text="Please be aware that THIS OPTION WILL BE REMOVED ON RELEASE. Disabling it will only be available for the remainder of the beta."}
tool_ctl.dis_flow_view = Checkbox{parent=mon_c_4,x=1,y=9,default=ini_cfg.DisableFlowView,label="Disable Flow View Monitor",box_fg_bg=cpair(colors.blue,colors.black)}
local function back_from_legacy()
tmp_cfg.DisableFlowView = tool_ctl.dis_flow_view.get_value()
tool_ctl.update_mon_reqs()
mon_pane.set_value(1)
end
PushButton{parent=mon_c_4,x=44,y=14,min_width=6,text="Done",callback=back_from_legacy,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Speaker
local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49}
TextBox{parent=spkr_cfg,x=1,y=2,text=" Speaker Configuration",fg_bg=cpair(colors.black,colors.cyan)}
TextBox{parent=spkr_c,x=1,y=1,height=2,text="The coordinator uses a speaker to play alarm sounds."}
TextBox{parent=spkr_c,x=1,y=4,height=3,text="You can change the speaker audio volume from the default. The range is 0.0 to 3.0, where 1.0 is standard volume."}
tool_ctl.s_vol = NumberField{parent=spkr_c,x=1,y=8,width=9,max_chars=7,allow_decimal=true,default=ini_cfg.SpeakerVolume,min=0,max=3,fg_bg=bw_fg_bg}
TextBox{parent=spkr_c,x=1,y=10,height=3,text="Note: alarm sine waves are at half scale so that multiple will be required to reach full scale.",fg_bg=g_lg_fg_bg}
local s_vol_err = TextBox{parent=spkr_c,x=8,y=14,width=35,text="Please set a volume.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_vol()
local vol = tonumber(tool_ctl.s_vol.get_value())
if vol ~= nil then
s_vol_err.hide(true)
tmp_cfg.SpeakerVolume = vol
main_pane.set_value(6)
else s_vol_err.show() end
end
PushButton{parent=spkr_c,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=spkr_c,x=44,y=14,text="Next \x1a",callback=submit_vol,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Coordinator UI
local crd_c_1 = Div{parent=crd_cfg,x=2,y=4,width=49}
TextBox{parent=crd_cfg,x=1,y=2,text=" Coordinator UI Configuration",fg_bg=cpair(colors.black,colors.lime)}
TextBox{parent=crd_c_1,x=1,y=1,height=2,text="You can customize the UI with the interface options below."}
TextBox{parent=crd_c_1,x=1,y=4,text="Clock Time Format"}
tool_ctl.clock_fmt = RadioButton{parent=crd_c_1,x=1,y=5,default=util.trinary(ini_cfg.Time24Hour,1,2),options={"24-Hour","12-Hour"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=crd_c_1,x=20,y=4,text="Po/Pu Pellet Color"}
TextBox{parent=crd_c_1,x=39,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
tool_ctl.pellet_color = RadioButton{parent=crd_c_1,x=20,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po (Mek 10.4+)"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=crd_c_1,x=1,y=8,text="Temperature Scale"}
tool_ctl.temp_scale = RadioButton{parent=crd_c_1,x=1,y=9,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=crd_c_1,x=20,y=8,text="Energy Scale"}
tool_ctl.energy_scale = RadioButton{parent=crd_c_1,x=20,y=9,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
local function submit_ui_opts()
tmp_cfg.Time24Hour = tool_ctl.clock_fmt.get_value() == 1
tmp_cfg.GreenPuPellet = tool_ctl.pellet_color.get_value() == 1
tmp_cfg.TempScale = tool_ctl.temp_scale.get_value()
tmp_cfg.EnergyScale = tool_ctl.energy_scale.get_value()
main_pane.set_value(7)
end
PushButton{parent=crd_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=crd_c_1,x=44,y=14,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Tool and Helper Functions
-- update list of monitor requirements
function tool_ctl.update_mon_reqs()
local plural = tmp_cfg.UnitCount > 1
if tool_ctl.sv_cool_conf ~= nil then
local cnf = tool_ctl.sv_cool_conf
local row1_tall = cnf[1][1] > 1 or cnf[1][2] > 2 or (cnf[2] and (cnf[2][1] > 1 or cnf[2][2] > 2))
local row1_short = (cnf[1][1] == 0 and cnf[1][2] == 1) and (cnf[2] == nil or (cnf[2][1] == 0 and cnf[2][2] == 1))
local row2_tall = (cnf[3] and (cnf[3][1] > 1 or cnf[3][2] > 2)) or (cnf[4] and (cnf[4][1] > 1 or cnf[4][2] > 2))
local row2_short = (cnf[3] == nil or (cnf[3][1] == 0 and cnf[3][2] == 1)) and (cnf[4] == nil or (cnf[4][1] == 0 and cnf[4][2] == 1))
if tmp_cfg.UnitCount <= 2 then
tool_ctl.main_mon_h = util.trinary(row1_tall, 5, 4)
else
-- is only one tall and the other short, or are both tall? -> 5 or 6; are neither tall? -> 5
if row1_tall or row2_tall then
tool_ctl.main_mon_h = util.trinary((row1_short and row2_tall) or (row1_tall and row2_short), 5, 6)
else tool_ctl.main_mon_h = 5 end
end
else
tool_ctl.main_mon_h = util.trinary(tmp_cfg.UnitCount <= 2, 4, 5)
end
tool_ctl.flow_mon_h = 2 + tmp_cfg.UnitCount
local asterisk = util.trinary(tool_ctl.sv_cool_conf == nil, "*", "")
local m_at_least = util.trinary(tool_ctl.main_mon_h < 6, "at least ", "")
local f_at_least = util.trinary(tool_ctl.flow_mon_h < 6, "at least ", "")
mon_reqs.remove_all()
TextBox{parent=mon_reqs,x=1,y=1,text="\x1a "..tmp_cfg.UnitCount.." Unit View Monitor"..util.trinary(plural,"s","")}
TextBox{parent=mon_reqs,x=1,y=1,text=" "..util.trinary(plural,"each ","").."must be 4 blocks wide by 4 tall",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=mon_reqs,x=1,y=1,text="\x1a 1 Main View Monitor"}
TextBox{parent=mon_reqs,x=1,y=1,text=" must be 8 blocks wide by "..m_at_least..tool_ctl.main_mon_h..asterisk.." tall",fg_bg=cpair(colors.gray,colors.white)}
if not tmp_cfg.DisableFlowView then
TextBox{parent=mon_reqs,x=1,y=1,text="\x1a 1 Flow View Monitor"}
TextBox{parent=mon_reqs,x=1,y=1,text=" must be 8 blocks wide by "..f_at_least..tool_ctl.flow_mon_h.." tall",fg_bg=cpair(colors.gray,colors.white)}
end
end
-- set/edit a monitor's assignment
---@param iface string
---@param device ppm_entry
function self.edit_monitor(iface, device)
self.mon_iface = iface
local dev = device.dev
local w, h = ppm.monitor_block_size(dev.getSize())
local msg = "This size doesn't match a required screen. Please go back and resize it, or configure below at the risk of it not working."
self.mon_expect = {}
mon_assign.set_value(1)
mon_unit.set_value(0)
if w == 4 and h == 4 then
msg = "This could work as a unit display. Please configure below."
self.mon_expect = { 3 }
mon_assign.set_value(3)
elseif w == 8 then
if h >= tool_ctl.main_mon_h and h >= tool_ctl.flow_mon_h then
msg = "This could work as either your main monitor or flow monitor. Please configure below."
self.mon_expect = { 1, 2 }
if tmp_cfg.MainDisplay then mon_assign.set_value(2) end
elseif h >= tool_ctl.main_mon_h then
msg = "This could work as your main monitor. Please configure below."
self.mon_expect = { 1 }
elseif h >= tool_ctl.flow_mon_h then
msg = "This could work as your flow monitor. Please configure below."
self.mon_expect = { 2 }
mon_assign.set_value(2)
end
end
-- override if a config exists
if tmp_cfg.MainDisplay == iface then
mon_assign.set_value(1)
elseif tmp_cfg.FlowDisplay == iface then
mon_assign.set_value(2)
else
for i = 1, tmp_cfg.UnitCount do
if tmp_cfg.UnitDisplays[i] == iface then
mon_assign.set_value(3)
mon_unit.set_value(i)
break
end
end
end
on_assign_mon(mon_assign.get_value())
mon_desc.set_value(util.c("You have selected '", iface, "', which has a block size of ", w, " wide by ", h, " tall. ", msg))
mon_pane.set_value(3)
end
-- generate the list of available monitors
function tool_ctl.gen_mon_list()
mon_list.remove_all()
local missing = { main = tmp_cfg.MainDisplay ~= nil, flow = tmp_cfg.FlowDisplay ~= nil, unit = {} }
for i = 1, tmp_cfg.UnitCount do missing.unit[i] = tmp_cfg.UnitDisplays[i] ~= nil end
-- list connected monitors
local monitors = ppm.get_monitor_list()
for iface, device in pairs(monitors) do
local dev = device.dev ---@type Monitor
dev.setTextScale(0.5)
dev.setTextColor(colors.white)
dev.setBackgroundColor(colors.black)
dev.clear()
dev.setCursorPos(1, 1)
dev.setTextColor(colors.magenta)
dev.write("This is monitor")
dev.setCursorPos(1, 2)
dev.setTextColor(colors.white)
dev.write(iface)
local assignment = "Unused"
if tmp_cfg.MainDisplay == iface then
assignment = "Main"
missing.main = false
elseif tmp_cfg.FlowDisplay == iface then
assignment = "Flow"
missing.flow = false
else
for i = 1, tmp_cfg.UnitCount do
if tmp_cfg.UnitDisplays[i] == iface then
missing.unit[i] = false
assignment = "Unit " .. i
break
end
end
end
local line = Div{parent=mon_list,x=1,y=1,height=1}
TextBox{parent=line,x=1,y=1,width=6,text=assignment,fg_bg=cpair(util.trinary(assignment=="Unused",colors.red,colors.blue),colors.white)}
TextBox{parent=line,x=8,y=1,text=iface}
local w, h = ppm.monitor_block_size(dev.getSize())
local function unset_mon()
purge_assignments(iface)
tool_ctl.gen_mon_list()
end
TextBox{parent=line,x=33,y=1,width=4,text=w.."x"..h,fg_bg=cpair(colors.black,colors.white)}
PushButton{parent=line,x=37,y=1,min_width=5,height=1,text="SET",callback=function()self.edit_monitor(iface,device)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
local unset = PushButton{parent=line,x=42,y=1,min_width=7,height=1,text="UNSET",callback=unset_mon,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.black,colors.gray)}
if assignment == "Unused" then unset.disable() end
end
local dc_list = {} -- disconnected monitor list
if missing.main then table.insert(dc_list, { "Main", tmp_cfg.MainDisplay }) end
if missing.flow then table.insert(dc_list, { "Flow", tmp_cfg.FlowDisplay }) end
for i = 1, tmp_cfg.UnitCount do
if missing.unit[i] then table.insert(dc_list, { "Unit " .. i, tmp_cfg.UnitDisplays[i] }) end
end
-- add monitors that are assigned but not connected
for i = 1, #dc_list do
local line = Div{parent=mon_list,x=1,y=1,height=1}
TextBox{parent=line,x=1,y=1,width=6,text=dc_list[i][1],fg_bg=cpair(colors.blue,colors.white)}
TextBox{parent=line,x=8,y=1,text="disconnected",fg_bg=cpair(colors.red,colors.white)}
local function unset_mon()
purge_assignments(dc_list[i][2])
tool_ctl.gen_mon_list()
end
TextBox{parent=line,x=33,y=1,width=4,text="?x?",fg_bg=cpair(colors.black,colors.white)}
PushButton{parent=line,x=37,y=1,min_width=5,height=1,text="SET",callback=function()end,dis_fg_bg=cpair(colors.black,colors.gray)}.disable()
PushButton{parent=line,x=42,y=1,min_width=7,height=1,text="UNSET",callback=unset_mon,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.black,colors.gray)}
end
end
--#endregion
return mon_pane
end
return hmi

View File

@@ -0,0 +1,580 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local network = require("scada-common.network")
local types = require("scada-common.types")
local util = require("scada-common.util")
local core = require("graphics.core")
local themes = require("graphics.themes")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local Checkbox = require("graphics.elements.controls.Checkbox")
local PushButton = require("graphics.elements.controls.PushButton")
local RadioButton = require("graphics.elements.controls.RadioButton")
local NumberField = require("graphics.elements.form.NumberField")
local TextField = require("graphics.elements.form.TextField")
local IndLight = require("graphics.elements.indicators.IndicatorLight")
local tri = util.trinary
local cpair = core.cpair
local RIGHT = core.ALIGN.RIGHT
local self = {
importing_legacy = false,
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type PushButton
auth_key_textbox = nil, ---@type TextBox
auth_key_value = ""
}
local system = {}
-- create the system configuration view
---@param tool_ctl _crd_cfg_tool_ctl
---@param main_pane MultiPane
---@param cfg_sys [ crd_config, crd_config, crd_config, { [1]: string, [2]: string, [3]: any }[], function ]
---@param divs Div[]
---@param ext [ MultiPane, MultiPane, function, function ]
---@param style { [string]: cpair }
function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
local settings_cfg, ini_cfg, tmp_cfg, fields, load_settings = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5]
local net_cfg, log_cfg, clr_cfg, summary = divs[1], divs[2], divs[3], divs[4]
local fac_pane, mon_pane, preset_monitor_fields, exit = ext[1], ext[2], ext[3], ext[4]
local bw_fg_bg = style.bw_fg_bg
local g_lg_fg_bg = style.g_lg_fg_bg
local nav_fg_bg = style.nav_fg_bg
local btn_act_fg_bg = style.btn_act_fg_bg
local btn_dis_fg_bg = style.btn_dis_fg_bg
--#region Network
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."}
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 3 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_1,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=10,width=19,text="Coordinator Channel"}
local crd_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=10,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"}
local pkt_chan = NumberField{parent=net_c_1,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_channels()
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then
tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c
net_pane.set_value(2)
chan_err.hide(true)
else chan_err.show() end
end
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,text="Please set the connection timeouts below."}
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=8,width=19,text="Supervisor Timeout"}
local svr_timeout = NumberField{parent=net_c_2,x=20,y=8,width=7,default=ini_cfg.SVR_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=1,y=10,width=14,text="Pocket Timeout"}
local api_timeout = NumberField{parent=net_c_2,x=20,y=10,width=7,default=ini_cfg.API_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=28,y=8,height=4,width=7,text="seconds\n\nseconds",fg_bg=g_lg_fg_bg}
local ct_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_timeouts()
local svr_cto, api_cto = tonumber(svr_timeout.get_value()), tonumber(api_timeout.get_value())
if svr_cto ~= nil and api_cto ~= nil then
tmp_cfg.SVR_Timeout, tmp_cfg.API_Timeout = svr_cto, api_cto
net_pane.set_value(3)
ct_err.hide(true)
else ct_err.show() end
end
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,text="Please set the trusted range below."}
TextBox{parent=net_c_3,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local tr_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_tr()
local range_val = tonumber(range.get_value())
if range_val ~= nil then
tmp_cfg.TrustedRange = range_val
comms.set_trusted_range(range_val)
net_pane.set_value(4)
tr_err.hide(true)
else tr_err.show() end
end
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
TextBox{parent=net_c_4,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=1,y=11,text="Facility Auth Key"}
local key, _ = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
local function censor_key(enable) key.censor(tri(enable, "*", nil)) end
local hide_key = Checkbox{parent=net_c_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
hide_key.set_value(true)
censor_key(true)
local key_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_auth()
local v = key.get_value()
if string.len(v) == 0 or string.len(v) >= 8 then
tmp_cfg.AuthKey = key.get_value()
key_err.hide(true)
-- init mac for supervisor connection
if string.len(v) >= 8 then network.init_mac(tmp_cfg.AuthKey) else network.deinit_mac() end
-- prep supervisor connection screen
tool_ctl.init_sv_connect_ui()
main_pane.set_value(3)
else key_err.show() end
end
PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Logging
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49}
TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
local en_dbg = Checkbox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg}
local path_err = TextBox{parent=log_c_1,x=8,y=14,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_log()
if path.get_value() ~= "" then
path_err.hide(true)
tmp_cfg.LogMode = mode.get_value() - 1
tmp_cfg.LogPath = path.get_value()
tmp_cfg.LogDebug = en_dbg.get_value()
tool_ctl.color_apply.hide(true)
tool_ctl.color_next.show()
main_pane.set_value(8)
else path_err.show() end
end
PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Color Options
local clr_c_1 = Div{parent=clr_cfg,x=2,y=4,width=49}
local clr_c_2 = Div{parent=clr_cfg,x=2,y=4,width=49}
local clr_c_3 = Div{parent=clr_cfg,x=2,y=4,width=49}
local clr_c_4 = Div{parent=clr_cfg,x=2,y=4,width=49}
local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}}
TextBox{parent=clr_cfg,x=1,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)}
TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color themes for the different UI displays."}
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
TextBox{parent=clr_c_1,x=1,y=7,text="Main UI Theme"}
local main_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.MainTheme,options=themes.UI_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_1,x=18,y=7,text="Front Panel Theme"}
local fp_theme = RadioButton{parent=clr_c_1,x=18,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will usually be split up."}
TextBox{parent=clr_c_2,x=21,y=7,text="Preview"}
local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)}
_ = IndLight{parent=clr_c_2,x=21,y=9,label="Warning",colors=cpair(colors.black,colors.yellow)}
_ = IndLight{parent=clr_c_2,x=21,y=10,label="Bad",colors=cpair(colors.black,colors.red)}
local b_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.black,colors.black),hidden=true}
local g_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.gray,colors.gray),hidden=true}
local function recolor(value)
local c = themes.smooth_stone.color_modes[value]
if value == themes.COLOR_MODE.STANDARD or value == themes.COLOR_MODE.BLUE_IND then
b_off.hide()
g_off.show()
else
g_off.hide()
b_off.show()
end
if #c == 0 then
for i = 1, #style.colors do term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end
else
term.setPaletteColor(colors.green, c[1].hex)
term.setPaletteColor(colors.yellow, c[2].hex)
term.setPaletteColor(colors.red, c[3].hex)
end
end
TextBox{parent=clr_c_2,x=1,y=7,width=10,text="Color Mode"}
local c_mode = RadioButton{parent=clr_c_2,x=1,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_2,x=21,y=13,height=2,width=18,text="Note: exact color varies by theme.",fg_bg=g_lg_fg_bg}
PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local function back_from_colors()
main_pane.set_value(tri(tool_ctl.jumped_to_color, 1, 7))
tool_ctl.jumped_to_color = false
recolor(1)
end
local function show_access()
clr_pane.set_value(2)
recolor(c_mode.get_value())
end
local function submit_colors()
tmp_cfg.MainTheme = main_theme.get_value()
tmp_cfg.FrontPanelTheme = fp_theme.get_value()
tmp_cfg.ColorMode = c_mode.get_value()
if tool_ctl.jumped_to_color then
settings.set("MainTheme", tmp_cfg.MainTheme)
settings.set("FrontPanelTheme", tmp_cfg.FrontPanelTheme)
settings.set("ColorMode", tmp_cfg.ColorMode)
if settings.save("/coordinator.settings") then
load_settings(settings_cfg, true)
load_settings(ini_cfg)
clr_pane.set_value(3)
else
clr_pane.set_value(4)
end
else
tool_ctl.gen_summary(tmp_cfg)
tool_ctl.viewing_config = false
self.importing_legacy = false
tool_ctl.settings_apply.show()
main_pane.set_value(9)
end
end
PushButton{parent=clr_c_1,x=1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=clr_c_1,x=8,y=14,min_width=15,text="Accessibility",callback=show_access,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
tool_ctl.color_next = PushButton{parent=clr_c_1,x=44,y=14,text="Next \x1a",callback=submit_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
tool_ctl.color_apply = PushButton{parent=clr_c_1,x=43,y=14,min_width=7,text="Apply",callback=submit_colors,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
tool_ctl.color_apply.hide(true)
local function c_go_home()
main_pane.set_value(1)
clr_pane.set_value(1)
end
TextBox{parent=clr_c_3,x=1,y=1,text="Settings saved!"}
PushButton{parent=clr_c_3,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
PushButton{parent=clr_c_3,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=clr_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
PushButton{parent=clr_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
PushButton{parent=clr_c_4,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Summary and Saving
local sum_c_1 = Div{parent=summary,x=2,y=4,width=49}
local sum_c_2 = Div{parent=summary,x=2,y=4,width=49}
local sum_c_3 = Div{parent=summary,x=2,y=4,width=49}
local sum_c_4 = Div{parent=summary,x=2,y=4,width=49}
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function back_from_summary()
if tool_ctl.viewing_config or self.importing_legacy then
main_pane.set_value(1)
tool_ctl.viewing_config = false
self.importing_legacy = false
tool_ctl.settings_apply.show()
else
main_pane.set_value(8)
end
end
---@param element graphics_element
---@param data any
local function try_set(element, data)
if data ~= nil then element.set_value(data) end
end
local function save_and_continue()
for _, field in ipairs(fields) do
local k, v = field[1], tmp_cfg[field[1]]
if v == nil then settings.unset(k) else settings.set(k, v) end
end
if settings.save("/coordinator.settings") then
load_settings(settings_cfg, true)
load_settings(ini_cfg)
try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(crd_chan, ini_cfg.CRD_Channel)
try_set(pkt_chan, ini_cfg.PKT_Channel)
try_set(svr_timeout, ini_cfg.SVR_Timeout)
try_set(api_timeout, ini_cfg.API_Timeout)
try_set(range, ini_cfg.TrustedRange)
try_set(key, ini_cfg.AuthKey)
try_set(tool_ctl.num_units, ini_cfg.UnitCount)
try_set(tool_ctl.dis_flow_view, ini_cfg.DisableFlowView)
try_set(tool_ctl.s_vol, ini_cfg.SpeakerVolume)
try_set(tool_ctl.pellet_color, ini_cfg.GreenPuPellet)
try_set(tool_ctl.clock_fmt, tri(ini_cfg.Time24Hour, 1, 2))
try_set(tool_ctl.temp_scale, ini_cfg.TempScale)
try_set(tool_ctl.energy_scale, ini_cfg.EnergyScale)
try_set(mode, ini_cfg.LogMode)
try_set(path, ini_cfg.LogPath)
try_set(en_dbg, ini_cfg.LogDebug)
try_set(main_theme, ini_cfg.MainTheme)
try_set(fp_theme, ini_cfg.FrontPanelTheme)
try_set(c_mode, ini_cfg.ColorMode)
preset_monitor_fields()
tool_ctl.gen_mon_list()
tool_ctl.view_cfg.enable()
tool_ctl.color_cfg.enable()
if self.importing_legacy then
self.importing_legacy = false
sum_pane.set_value(3)
else
sum_pane.set_value(2)
end
else
sum_pane.set_value(4)
end
end
PushButton{parent=sum_c_1,x=1,y=14,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"}
local function go_home()
main_pane.set_value(1)
net_pane.set_value(1)
fac_pane.set_value(1)
mon_pane.set_value(1)
clr_pane.set_value(1)
sum_pane.set_value(1)
end
PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=sum_c_3,x=1,y=1,height=2,text="The old config.lua and coord.settings files will now be deleted, then the configurator will exit."}
local function delete_legacy()
fs.delete("/coordinator/config.lua")
fs.delete("/coord.settings")
exit()
end
PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=sum_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
--#endregion
--#region Tool Functions
-- load a legacy config file
function tool_ctl.load_legacy()
local config = require("coordinator.config")
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
tmp_cfg.CRD_Channel = config.CRD_CHANNEL
tmp_cfg.PKT_Channel = config.PKT_CHANNEL
tmp_cfg.SVR_Timeout = config.SV_TIMEOUT
tmp_cfg.API_Timeout = config.API_TIMEOUT
tmp_cfg.TrustedRange = config.TRUSTED_RANGE
tmp_cfg.AuthKey = config.AUTH_KEY or ""
tmp_cfg.UnitCount = config.NUM_UNITS
tmp_cfg.DisableFlowView = config.DISABLE_FLOW_VIEW
tmp_cfg.SpeakerVolume = config.SOUNDER_VOLUME
tmp_cfg.Time24Hour = config.TIME_24_HOUR
tmp_cfg.LogMode = config.LOG_MODE
tmp_cfg.LogPath = config.LOG_PATH
tmp_cfg.LogDebug = config.LOG_DEBUG or false
settings.load("/coord.settings")
tmp_cfg.MainDisplay = settings.get("PRIMARY_DISPLAY")
tmp_cfg.FlowDisplay = settings.get("FLOW_DISPLAY")
tmp_cfg.UnitDisplays = settings.get("UNIT_DISPLAYS", {})
-- if there are extra monitor entries, delete them now
-- not doing so will cause the app to fail to start
if tool_ctl.is_int_min_max(tmp_cfg.UnitCount, 1, 4) then
for i = tmp_cfg.UnitCount + 1, 4 do tmp_cfg.UnitDisplays[i] = nil end
end
if settings.get("ControlStates") == nil then
local ctrl_states = {
process = settings.get("PROCESS"),
waste_modes = settings.get("WASTE_MODES"),
priority_groups = settings.get("PRIORITY_GROUPS"),
}
settings.set("ControlStates", ctrl_states)
end
settings.unset("PRIMARY_DISPLAY")
settings.unset("FLOW_DISPLAY")
settings.unset("UNIT_DISPLAYS")
settings.unset("PROCESS")
settings.unset("WASTE_MODES")
settings.unset("PRIORITY_GROUPS")
tool_ctl.gen_summary(tmp_cfg)
sum_pane.set_value(1)
main_pane.set_value(9)
self.importing_legacy = true
end
-- expose the auth key on the summary page
function self.show_auth_key()
self.show_key_btn.disable()
self.auth_key_textbox.set_value(self.auth_key_value)
end
-- generate the summary list
---@param cfg crd_config
function tool_ctl.gen_summary(cfg)
setting_list.remove_all()
local alternate = false
local inner_width = setting_list.get_width() - 1
self.show_key_btn.enable()
self.auth_key_value = cfg.AuthKey or "" -- to show auth key
for i = 1, #fields do
local f = fields[i]
local height = 1
local label_w = string.len(f[2])
local val_max_w = (inner_width - label_w) + 1
local raw = cfg[f[1]]
local val = util.strval(raw)
if f[1] == "AuthKey" then val = string.rep("*", string.len(val))
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "GreenPuPellet" then
val = tri(raw, "Green Pu/Cyan Po", "Cyan Pu/Green Po")
elseif f[1] == "TempScale" then
val = util.strval(types.TEMP_SCALE_NAMES[raw])
elseif f[1] == "EnergyScale" then
val = util.strval(types.ENERGY_SCALE_NAMES[raw])
elseif f[1] == "MainTheme" then
val = util.strval(themes.ui_theme_name(raw))
elseif f[1] == "FrontPanelTheme" then
val = util.strval(themes.fp_theme_name(raw))
elseif f[1] == "ColorMode" then
val = util.strval(themes.color_mode_name(raw))
elseif f[1] == "UnitDisplays" and type(cfg.UnitDisplays) == "table" then
val = ""
for idx = 1, #cfg.UnitDisplays do
val = val .. util.trinary(idx == 1, "", "\n") .. util.sprintf(" \x07 Unit %d - %s", idx, cfg.UnitDisplays[idx])
end
end
if val == "nil" then val = "<not set>" end
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end
if (f[1] == "UnitDisplays") and (height == 1) and (val ~= "<not set>") then height = 2 end
local line = Div{parent=setting_list,height=height,fg_bg=c}
TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)}
local textbox
if height > 1 then
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end
if f[1] == "AuthKey" then self.auth_key_textbox = textbox end
end
end
--#endregion
end
return system

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,7 @@ local LINK_TIMEOUT = 60.0
local coordinator = {} local coordinator = {}
---@type crd_config ---@type crd_config
---@diagnostic disable-next-line: missing-fields
local config = {} local config = {}
coordinator.config = config coordinator.config = config
@@ -37,7 +38,9 @@ function coordinator.load_config()
config.UnitCount = settings.get("UnitCount") config.UnitCount = settings.get("UnitCount")
config.SpeakerVolume = settings.get("SpeakerVolume") config.SpeakerVolume = settings.get("SpeakerVolume")
config.Time24Hour = settings.get("Time24Hour") config.Time24Hour = settings.get("Time24Hour")
config.GreenPuPellet = settings.get("GreenPuPellet")
config.TempScale = settings.get("TempScale") config.TempScale = settings.get("TempScale")
config.EnergyScale = settings.get("EnergyScale")
config.DisableFlowView = settings.get("DisableFlowView") config.DisableFlowView = settings.get("DisableFlowView")
config.MainDisplay = settings.get("MainDisplay") config.MainDisplay = settings.get("MainDisplay")
@@ -65,8 +68,11 @@ function coordinator.load_config()
cfv.assert_type_int(config.UnitCount) cfv.assert_type_int(config.UnitCount)
cfv.assert_range(config.UnitCount, 1, 4) cfv.assert_range(config.UnitCount, 1, 4)
cfv.assert_type_bool(config.Time24Hour) cfv.assert_type_bool(config.Time24Hour)
cfv.assert_type_bool(config.GreenPuPellet)
cfv.assert_type_int(config.TempScale) cfv.assert_type_int(config.TempScale)
cfv.assert_range(config.TempScale, 1, 4) cfv.assert_range(config.TempScale, 1, 4)
cfv.assert_type_int(config.EnergyScale)
cfv.assert_range(config.EnergyScale, 1, 3)
cfv.assert_type_bool(config.DisableFlowView) cfv.assert_type_bool(config.DisableFlowView)
cfv.assert_type_table(config.UnitDisplays) cfv.assert_type_table(config.UnitDisplays)
@@ -108,12 +114,12 @@ function coordinator.load_config()
---@class monitors_struct ---@class monitors_struct
local monitors = { local monitors = {
main = nil, ---@type table|nil main = nil, ---@type Monitor|nil
main_name = "", main_name = "",
flow = nil, ---@type table|nil flow = nil, ---@type Monitor|nil
flow_name = "", flow_name = "",
unit_displays = {}, unit_displays = {}, ---@type Monitor[]
unit_name_map = {} unit_name_map = {} ---@type string[]
} }
local mon_cfv = util.new_validator() local mon_cfv = util.new_validator()
@@ -232,8 +238,8 @@ function coordinator.comms(version, nic, sv_watchdog)
local self = { local self = {
sv_linked = false, sv_linked = false,
sv_addr = comms.BROADCAST, sv_addr = comms.BROADCAST,
sv_seq_num = 0, sv_seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
sv_r_seq_num = nil, sv_r_seq_num = nil, ---@type nil|integer
sv_config_err = false, sv_config_err = false,
last_est_ack = ESTABLISH_ACK.ALLOW, last_est_ack = ESTABLISH_ACK.ALLOW,
last_api_est_acks = {}, last_api_est_acks = {},
@@ -293,6 +299,7 @@ function coordinator.comms(version, nic, sv_watchdog)
-- attempt connection establishment -- attempt connection establishment
local function _send_establish() local function _send_establish()
self.sv_r_seq_num = nil
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRD }) _send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRD })
end end
@@ -375,6 +382,18 @@ function coordinator.comms(version, nic, sv_watchdog)
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {}) _send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {})
end end
-- send the resume ready state to the supervisor
---@param mode PROCESS process control mode
---@param burn_target number burn rate target
---@param charge_target number charge level target
---@param gen_target number generation rate target
---@param limits number[] unit burn rate limits
function public.send_ready(mode, burn_target, charge_target, gen_target, limits)
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.PROCESS_READY, {
mode, burn_target, charge_target, gen_target, limits
})
end
-- send a facility command -- send a facility command
---@param cmd FAC_COMMAND command ---@param cmd FAC_COMMAND command
---@param option any? optional option options for the optional options (like waste mode) ---@param option any? optional option options for the optional options (like waste mode)
@@ -383,10 +402,14 @@ function coordinator.comms(version, nic, sv_watchdog)
end end
-- send the auto process control configuration with a start command -- send the auto process control configuration with a start command
---@param auto_cfg coord_auto_config configuration ---@param mode PROCESS process control mode
function public.send_auto_start(auto_cfg) ---@param burn_target number burn rate target
---@param charge_target number charge level target
---@param gen_target number generation rate target
---@param limits number[] unit burn rate limits
function public.send_auto_start(mode, burn_target, charge_target, gen_target, limits)
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, { _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
FAC_COMMAND.START, auto_cfg.mode, auto_cfg.burn_target, auto_cfg.charge_target, auto_cfg.gen_target, auto_cfg.limits FAC_COMMAND.START, mode, burn_target, charge_target, gen_target, limits
}) })
end end
@@ -492,7 +515,7 @@ function coordinator.comms(version, nic, sv_watchdog)
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_API_VERSION) _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_API_VERSION)
elseif dev_type == DEVICE_TYPE.PKT then elseif dev_type == DEVICE_TYPE.PKT then
-- pocket linking request -- pocket linking request
local id = apisessions.establish_session(src_addr, firmware_v) local id = apisessions.establish_session(src_addr, packet.scada_frame.seq_num(), firmware_v)
coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id)) coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id))
local conf = iocontrol.get_db().facility.conf local conf = iocontrol.get_db().facility.conf
@@ -515,15 +538,15 @@ function coordinator.comms(version, nic, sv_watchdog)
elseif r_chan == config.SVR_Channel then elseif r_chan == config.SVR_Channel then
-- check sequence number -- check sequence number
if self.sv_r_seq_num == nil then if self.sv_r_seq_num == nil then
self.sv_r_seq_num = packet.scada_frame.seq_num() self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.sv_linked and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then elseif self.sv_r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) log.warning("sequence out-of-order: next = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return false return false
elseif self.sv_linked and src_addr ~= self.sv_addr then 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?") log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?")
return false return false
else else
self.sv_r_seq_num = packet.scada_frame.seq_num() self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
end end
-- feed watchdog on valid sequence number -- feed watchdog on valid sequence number
@@ -572,9 +595,9 @@ function coordinator.comms(version, nic, sv_watchdog)
local ack = packet.data[2] == true local ack = packet.data[2] == true
if cmd == FAC_COMMAND.SCRAM_ALL then if cmd == FAC_COMMAND.SCRAM_ALL then
iocontrol.get_db().facility.scram_ack(ack) process.fac_ack(cmd, ack)
elseif cmd == FAC_COMMAND.STOP then elseif cmd == FAC_COMMAND.STOP then
iocontrol.get_db().facility.stop_ack(ack) process.fac_ack(cmd, ack)
elseif cmd == FAC_COMMAND.START then elseif cmd == FAC_COMMAND.START then
if packet.length == 7 then if packet.length == 7 then
process.start_ack_handle({ table.unpack(packet.data, 2) }) process.start_ack_handle({ table.unpack(packet.data, 2) })
@@ -582,11 +605,13 @@ function coordinator.comms(version, nic, sv_watchdog)
log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch") log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch")
end end
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
iocontrol.get_db().facility.ack_alarms_ack(ack) process.fac_ack(cmd, ack)
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
process.waste_ack_handle(packet.data[2]) process.waste_ack_handle(packet.data[2])
elseif cmd == FAC_COMMAND.SET_PU_FB then elseif cmd == FAC_COMMAND.SET_PU_FB then
process.pu_fb_ack_handle(packet.data[2]) process.pu_fb_ack_handle(packet.data[2])
elseif cmd == FAC_COMMAND.SET_SPS_LP then
process.sps_lp_ack_handle(packet.data[2])
else else
log.debug(util.c("received facility command ack with unknown command ", cmd)) log.debug(util.c("received facility command ack with unknown command ", cmd))
end end
@@ -617,25 +642,19 @@ function coordinator.comms(version, nic, sv_watchdog)
local unit_id = packet.data[2] local unit_id = packet.data[2]
local ack = packet.data[3] == true local ack = packet.data[3] == true
local unit = iocontrol.get_db().units[unit_id] ---@type ioctl_unit local unit = iocontrol.get_db().units[unit_id]
if unit ~= nil then if unit ~= nil then
if cmd == UNIT_COMMAND.SCRAM then if cmd == UNIT_COMMAND.SCRAM then
unit.scram_ack(ack) process.unit_ack(unit_id, cmd, ack)
elseif cmd == UNIT_COMMAND.START then elseif cmd == UNIT_COMMAND.START then
unit.start_ack(ack) process.unit_ack(unit_id, cmd, ack)
elseif cmd == UNIT_COMMAND.RESET_RPS then elseif cmd == UNIT_COMMAND.RESET_RPS then
unit.reset_rps_ack(ack) process.unit_ack(unit_id, cmd, 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 elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
unit.ack_alarms_ack(ack) process.unit_ack(unit_id, cmd, ack)
elseif cmd == UNIT_COMMAND.SET_GROUP then
-- UI will be updated to display current group if changed successfully
else else
log.debug(util.c("received unit command ack with unknown command ", cmd)) log.debug(util.c("received unsupported unit command ack for command ", cmd))
end end
else else
log.debug(util.c("received unit command ack with unknown unit ", unit_id)) log.debug(util.c("received unit command ack with unknown unit ", unit_id))
@@ -702,11 +721,10 @@ function coordinator.comms(version, nic, sv_watchdog)
if conf.num_units == config.UnitCount then if conf.num_units == config.UnitCount then
-- init io controller -- init io controller
iocontrol.init(conf, public, config.TempScale) iocontrol.init(conf, public, config.TempScale, config.EnergyScale)
self.sv_addr = src_addr self.sv_addr = src_addr
self.sv_linked = true self.sv_linked = true
self.sv_r_seq_num = nil
self.sv_config_err = false self.sv_config_err = false
iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED) iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED)

File diff suppressed because it is too large Load Diff

View File

@@ -7,55 +7,84 @@ local log = require("scada-common.log")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local FAC_COMMAND = comms.FAC_COMMAND local F_CMD = comms.FAC_COMMAND
local UNIT_COMMAND = comms.UNIT_COMMAND local U_CMD = comms.UNIT_COMMAND
local PROCESS = types.PROCESS local PROCESS = types.PROCESS
local PRODUCT = types.WASTE_PRODUCT local PRODUCT = types.WASTE_PRODUCT
local REQUEST_TIMEOUT_MS = 10000
---@class process_controller ---@class process_controller
local process = {} local process = {}
local self = { local pctl = {
io = nil, ---@type ioctl io = nil, ---@type ioctl
comms = nil, ---@type coord_comms comms = nil, ---@type coord_comms
---@class coord_control_states ---@class sys_control_states
control_states = { control_states = {
---@class coord_auto_config ---@class sys_auto_config
process = { process = {
mode = PROCESS.INACTIVE, mode = PROCESS.INACTIVE, ---@type PROCESS
burn_target = 0.0, burn_target = 0.0,
charge_target = 0.0, charge_target = 0.0,
gen_target = 0.0, gen_target = 0.0,
limits = {}, limits = {}, ---@type number[]
waste_product = PRODUCT.PLUTONIUM, waste_product = PRODUCT.PLUTONIUM, ---@type WASTE_PRODUCT
pu_fallback = false, pu_fallback = false,
sps_low_power = false sps_low_power = false
}, },
waste_modes = {}, waste_modes = {}, ---@type WASTE_MODE[]
priority_groups = {} priority_groups = {} ---@type AUTO_GROUP[]
},
commands = {
unit = {}, ---@type process_command_state[][]
fac = {} ---@type process_command_state[]
} }
} }
-------------------------- ---@class process_command_state
-- UNIT COMMAND CONTROL -- ---@field active boolean if this command is live
-------------------------- ---@field timeout integer expiration time of this command request
---@field requestors function[] list of callbacks from the requestors
-- write auto process control to config file
---@return boolean saved
local function _write_auto_config()
-- save config
settings.set("ControlStates", pctl.control_states)
local saved = settings.save("/coordinator.settings")
if not saved then
log.warning("process._write_auto_config(): failed to save coordinator settings file")
end
return saved
end
--#region Core
-- initialize the process controller -- initialize the process controller
---@param iocontrol ioctl iocontrl system ---@param iocontrol ioctl iocontrl system
---@param coord_comms coord_comms coordinator communications ---@param coord_comms coord_comms coordinator communications
function process.init(iocontrol, coord_comms) function process.init(iocontrol, coord_comms)
self.io = iocontrol pctl.io = iocontrol
self.comms = coord_comms pctl.comms = coord_comms
local ctl_proc = self.control_states.process -- create command handling objects
for _, v in pairs(F_CMD) do pctl.commands.fac[v] = { active = false, timeout = 0, requestors = {} } end
for i = 1, pctl.io.facility.num_units do
pctl.commands.unit[i] = {}
for _, v in pairs(U_CMD) do pctl.commands.unit[i][v] = { active = false, timeout = 0, requestors = {} } end
end
for i = 1, self.io.facility.num_units do local ctl_proc = pctl.control_states.process
for i = 1, pctl.io.facility.num_units do
ctl_proc.limits[i] = 0.1 ctl_proc.limits[i] = 0.1
end end
local ctrl_states = settings.get("ControlStates", {}) local ctrl_states = settings.get("ControlStates", {}) ---@type sys_control_states
local config = ctrl_states.process ---@type coord_auto_config local config = ctrl_states.process
-- facility auto control configuration -- facility auto control configuration
if type(config) == "table" then if type(config) == "table" then
@@ -68,123 +97,339 @@ function process.init(iocontrol, coord_comms)
ctl_proc.pu_fallback = config.pu_fallback ctl_proc.pu_fallback = config.pu_fallback
ctl_proc.sps_low_power = config.sps_low_power ctl_proc.sps_low_power = config.sps_low_power
self.io.facility.ps.publish("process_mode", ctl_proc.mode) pctl.io.facility.ps.publish("process_mode", ctl_proc.mode)
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target) pctl.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target) pctl.io.facility.ps.publish("process_charge_target", pctl.io.energy_convert_from_fe(ctl_proc.charge_target))
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target) pctl.io.facility.ps.publish("process_gen_target", pctl.io.energy_convert_from_fe(ctl_proc.gen_target))
self.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product) pctl.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
self.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback) pctl.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback)
self.io.facility.ps.publish("process_sps_low_power", ctl_proc.sps_low_power) pctl.io.facility.ps.publish("process_sps_low_power", ctl_proc.sps_low_power)
for id = 1, math.min(#ctl_proc.limits, self.io.facility.num_units) do for id = 1, math.min(#ctl_proc.limits, pctl.io.facility.num_units) do
local unit = self.io.units[id] ---@type ioctl_unit local unit = pctl.io.units[id]
unit.unit_ps.publish("burn_limit", ctl_proc.limits[id]) unit.unit_ps.publish("burn_limit", ctl_proc.limits[id])
end end
log.info("PROCESS: loaded auto control settings") log.info("PROCESS: loaded auto control settings")
-- notify supervisor of auto waste config -- notify supervisor of auto waste config
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, ctl_proc.waste_product) pctl.comms.send_fac_command(F_CMD.SET_WASTE_MODE, ctl_proc.waste_product)
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, ctl_proc.pu_fallback) pctl.comms.send_fac_command(F_CMD.SET_PU_FB, ctl_proc.pu_fallback)
self.comms.send_fac_command(FAC_COMMAND.SET_SPS_LP, ctl_proc.sps_low_power) pctl.comms.send_fac_command(F_CMD.SET_SPS_LP, ctl_proc.sps_low_power)
end end
-- unit waste states -- unit waste states
local waste_modes = ctrl_states.waste_modes ---@type table|nil local waste_modes = ctrl_states.waste_modes
if type(waste_modes) == "table" then if type(waste_modes) == "table" then
for id, mode in pairs(waste_modes) do for id, mode in pairs(waste_modes) do
self.control_states.waste_modes[id] = mode pctl.control_states.waste_modes[id] = mode
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode) pctl.comms.send_unit_command(U_CMD.SET_WASTE, id, mode)
end end
log.info("PROCESS: loaded unit waste mode settings") log.info("PROCESS: loaded unit waste mode settings")
end end
-- unit priority groups -- unit priority groups
local prio_groups = ctrl_states.priority_groups ---@type table|nil local prio_groups = ctrl_states.priority_groups
if type(prio_groups) == "table" then if type(prio_groups) == "table" then
for id, group in pairs(prio_groups) do for id, group in pairs(prio_groups) do
self.control_states.priority_groups[id] = group pctl.control_states.priority_groups[id] = group
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group) pctl.comms.send_unit_command(U_CMD.SET_GROUP, id, group)
end end
log.info("PROCESS: loaded priority groups settings") log.info("PROCESS: loaded priority groups settings")
end end
-- report to the supervisor all initial configuration data has been sent
-- startup resume can occur if needed
local p = ctl_proc
pctl.comms.send_ready(p.mode, p.burn_target, p.charge_target, p.gen_target, p.limits)
end end
-- facility SCRAM command -- create a handle to process control for usage of commands that get acknowledgements
function process.fac_scram() function process.create_handle()
self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL) ---@class process_handle
log.debug("PROCESS: FAC SCRAM ALL") local handle = {}
-- add this handle to the requestors and activate the command if inactive
---@param cmd process_command_state
---@param ack function
local function request(cmd, ack)
local new = not cmd.active
if new then
cmd.active = true
cmd.timeout = util.time_ms() + REQUEST_TIMEOUT_MS
end
table.insert(cmd.requestors, ack)
return new
end
local function u_request(u_id, cmd_id, ack) return request(pctl.commands.unit[u_id][cmd_id], ack) end
local function f_request(cmd_id, ack) return request(pctl.commands.fac[cmd_id], ack) end
--#region Facility Commands
-- facility SCRAM command
function handle.fac_scram()
if f_request(F_CMD.SCRAM_ALL, handle.fac_ack.on_scram) then
pctl.comms.send_fac_command(F_CMD.SCRAM_ALL)
log.debug("PROCESS: FAC SCRAM ALL")
end
end
-- facility alarm acknowledge command
function handle.fac_ack_alarms()
if f_request(F_CMD.ACK_ALL_ALARMS, handle.fac_ack.on_ack_alarms) then
pctl.comms.send_fac_command(F_CMD.ACK_ALL_ALARMS)
log.debug("PROCESS: FAC ACK ALL ALARMS")
end
end
-- start automatic process control with current settings
function handle.process_start()
if f_request(F_CMD.START, handle.fac_ack.on_start) then
local p = pctl.control_states.process
pctl.comms.send_auto_start(p.mode, p.burn_target, p.charge_target, p.gen_target, p.limits)
log.debug("PROCESS: START AUTO CTRL")
end
end
-- start automatic process control with remote settings that haven't been set on the coordinator
---@param mode PROCESS process control mode
---@param burn_target number burn rate target
---@param charge_target number charge level target
---@param gen_target number generation rate target
---@param limits number[] unit burn rate limits
function handle.process_start_remote(mode, burn_target, charge_target, gen_target, limits)
if f_request(F_CMD.START, handle.fac_ack.on_start) then
pctl.comms.send_auto_start(mode, burn_target, charge_target, gen_target, limits)
log.debug("PROCESS: START AUTO CTRL")
end
end
-- stop process control
function handle.process_stop()
if f_request(F_CMD.STOP, handle.fac_ack.on_stop) then
pctl.comms.send_fac_command(F_CMD.STOP)
log.debug("PROCESS: STOP AUTO CTRL")
end
end
handle.fac_ack = {}
-- luacheck: no unused args
-- facility SCRAM ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function handle.fac_ack.on_scram(success) end
-- facility acknowledge all alarms ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function handle.fac_ack.on_ack_alarms(success) end
-- facility auto control start ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function handle.fac_ack.on_start(success) end
-- facility auto control stop ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function handle.fac_ack.on_stop(success) end
-- luacheck: unused args
--#endregion
--#region Unit Commands
-- start a reactor
---@param id integer unit ID
function handle.start(id)
if u_request(id, U_CMD.START, handle.unit_ack[id].on_start) then
pctl.io.units[id].control_state = true
pctl.comms.send_unit_command(U_CMD.START, id)
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
end
end
-- SCRAM reactor
---@param id integer unit ID
function handle.scram(id)
if u_request(id, U_CMD.SCRAM, handle.unit_ack[id].on_scram) then
pctl.io.units[id].control_state = false
pctl.comms.send_unit_command(U_CMD.SCRAM, id)
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
end
end
-- reset reactor protection system
---@param id integer unit ID
function handle.reset_rps(id)
if u_request(id, U_CMD.RESET_RPS, handle.unit_ack[id].on_rps_reset) then
pctl.comms.send_unit_command(U_CMD.RESET_RPS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
end
end
-- acknowledge all alarms
---@param id integer unit ID
function handle.ack_all_alarms(id)
if u_request(id, U_CMD.ACK_ALL_ALARMS, handle.unit_ack[id].on_ack_alarms) then
pctl.comms.send_unit_command(U_CMD.ACK_ALL_ALARMS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
end
end
-- unit command acknowledgement callbacks, indexed by unit ID
---@type process_unit_ack[]
handle.unit_ack = {}
for u = 1, pctl.io.facility.num_units do
---@diagnostic disable-next-line: missing-fields
handle.unit_ack[u] = {}
---@class process_unit_ack
local u_ack = handle.unit_ack[u]
-- luacheck: no unused args
-- unit start ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function u_ack.on_start(success) end
-- unit SCRAM ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function u_ack.on_scram(success) end
-- unit RPS reset ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function u_ack.on_rps_reset(success) end
-- unit acknowledge all alarms ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function u_ack.on_ack_alarms(success) end
-- luacheck: unused args
end
--#endregion
return handle
end end
-- facility alarm acknowledge command -- clear outstanding process commands that have timed out
function process.fac_ack_alarms() function process.clear_timed_out()
self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_ALARMS) local now = util.time_ms()
log.debug("PROCESS: FAC ACK ALL ALARMS") local objs = { pctl.commands.fac, table.unpack(pctl.commands.unit) }
for _, obj in pairs(objs) do
-- cancel expired requests
for _, cmd in pairs(obj) do
if cmd.active and now > cmd.timeout then
cmd.active = false
cmd.requestors = {}
end
end
end
end end
-- start reactor -- get the control states table
---@param id integer unit ID ---@nodiscard
function process.start(id) function process.get_control_states() return pctl.control_states end
self.io.units[id].control_state = true
self.comms.send_unit_command(UNIT_COMMAND.START, id) --#endregion
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
--#region Command Handling
-- handle a command acknowledgement
---@param cmd_state process_command_state
---@param success boolean if the command was successful
local function cmd_ack(cmd_state, success)
if cmd_state.active then
cmd_state.active = false
-- call all acknowledge callback functions
for i = 1, #cmd_state.requestors do
cmd_state.requestors[i](success)
end
cmd_state.requestors = {}
end
end end
-- SCRAM reactor -- handle a facility command acknowledgement
---@param id integer unit ID ---@param command FAC_COMMAND command
function process.scram(id) ---@param success boolean if the command was successful
self.io.units[id].control_state = false function process.fac_ack(command, success)
self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id) cmd_ack(pctl.commands.fac[command], success)
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
end end
-- reset reactor protection system -- handle a unit command acknowledgement
---@param id integer unit ID ---@param unit integer unit ID
function process.reset_rps(id) ---@param command UNIT_COMMAND command
self.comms.send_unit_command(UNIT_COMMAND.RESET_RPS, id) ---@param success boolean if the command was successful
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS")) function process.unit_ack(unit, command, success)
cmd_ack(pctl.commands.unit[unit][command], success)
end end
--#region One-Way Commands (no acknowledgements)
-- set burn rate -- set burn rate
---@param id integer unit ID ---@param id integer unit ID
---@param rate number burn rate ---@param rate number burn rate
function process.set_rate(id, rate) function process.set_rate(id, rate)
self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate) pctl.comms.send_unit_command(U_CMD.SET_BURN, id, rate)
log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate)) log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
end end
-- assign a unit to a group
---@param unit_id integer unit ID
---@param group_id integer|0 group ID or 0 for independent
function process.set_group(unit_id, group_id)
pctl.comms.send_unit_command(U_CMD.SET_GROUP, unit_id, group_id)
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
pctl.control_states.priority_groups[unit_id] = group_id
settings.set("ControlStates", pctl.control_states)
if not settings.save("/coordinator.settings") then
log.error("process.set_group(): failed to save coordinator settings file")
end
end
-- set waste mode -- set waste mode
---@param id integer unit ID ---@param id integer unit ID
---@param mode integer waste mode ---@param mode integer waste mode
function process.set_unit_waste(id, mode) function process.set_unit_waste(id, mode)
-- publish so that if it fails then it gets reset -- publish so that if it fails then it gets reset
self.io.units[id].unit_ps.publish("U_WasteMode", mode) pctl.io.units[id].unit_ps.publish("U_WasteMode", mode)
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode) pctl.comms.send_unit_command(U_CMD.SET_WASTE, id, mode)
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode)) log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
self.control_states.waste_modes[id] = mode pctl.control_states.waste_modes[id] = mode
settings.set("ControlStates", self.control_states) settings.set("ControlStates", pctl.control_states)
if not settings.save("/coordinator.settings") then if not settings.save("/coordinator.settings") then
log.error("process.set_unit_waste(): failed to save coordinator settings file") log.error("process.set_unit_waste(): failed to save coordinator settings file")
end end
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 -- acknowledge an alarm
---@param id integer unit ID ---@param id integer unit ID
---@param alarm integer alarm ID ---@param alarm integer alarm ID
function process.ack_alarm(id, alarm) function process.ack_alarm(id, alarm)
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALARM, id, alarm) pctl.comms.send_unit_command(U_CMD.ACK_ALARM, id, alarm)
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm)) log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
end end
@@ -192,100 +437,48 @@ end
---@param id integer unit ID ---@param id integer unit ID
---@param alarm integer alarm ID ---@param alarm integer alarm ID
function process.reset_alarm(id, alarm) function process.reset_alarm(id, alarm)
self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm) pctl.comms.send_unit_command(U_CMD.RESET_ALARM, id, alarm)
log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm)) log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
end end
-- assign a unit to a group --#endregion
---@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 -- -- 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 -- set automatic process control waste mode
---@param product WASTE_PRODUCT waste product for auto control ---@param product WASTE_PRODUCT waste product for auto control
function process.set_process_waste(product) function process.set_process_waste(product)
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, product) pctl.comms.send_fac_command(F_CMD.SET_WASTE_MODE, product)
log.debug(util.c("PROCESS: SET WASTE ", product)) log.debug(util.c("PROCESS: SET WASTE ", product))
-- update config table and save
self.control_states.process.waste_product = product
_write_auto_config()
end end
-- set automatic process control plutonium fallback -- set automatic process control plutonium fallback
---@param enabled boolean whether to enable plutonium fallback ---@param enabled boolean whether to enable plutonium fallback
function process.set_pu_fallback(enabled) function process.set_pu_fallback(enabled)
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, enabled) pctl.comms.send_fac_command(F_CMD.SET_PU_FB, enabled)
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled)) 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 end
-- set automatic process control SPS usage at low power -- set automatic process control SPS usage at low power
---@param enabled boolean whether to enable SPS usage at low power ---@param enabled boolean whether to enable SPS usage at low power
function process.set_sps_low_power(enabled) function process.set_sps_low_power(enabled)
self.comms.send_fac_command(FAC_COMMAND.SET_SPS_LP, enabled) pctl.comms.send_fac_command(F_CMD.SET_SPS_LP, enabled)
log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled)) log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled))
-- update config table and save
self.control_states.process.sps_low_power = enabled
_write_auto_config()
end end
-- save process control settings -- save process control settings
---@param mode PROCESS control mode ---@param mode PROCESS process control mode
---@param burn_target number burn rate target ---@param burn_target number burn rate target
---@param charge_target number charge target ---@param charge_target number charge level target
---@param gen_target number generation rate target ---@param gen_target number generation rate target
---@param limits table unit burn rate limits ---@param limits number[] unit burn rate limits
function process.save(mode, burn_target, charge_target, gen_target, limits) function process.save(mode, burn_target, charge_target, gen_target, limits)
log.debug("PROCESS: SAVE") log.debug("PROCESS: SAVE")
-- update config table -- update config table
local ctl_proc = self.control_states.process local ctl_proc = pctl.control_states.process
ctl_proc.mode = mode ctl_proc.mode = mode
ctl_proc.burn_target = burn_target ctl_proc.burn_target = burn_target
ctl_proc.charge_target = charge_target ctl_proc.charge_target = charge_target
@@ -293,7 +486,7 @@ function process.save(mode, burn_target, charge_target, gen_target, limits)
ctl_proc.limits = limits ctl_proc.limits = limits
-- save config -- save config
self.io.facility.save_cfg_ack(_write_auto_config()) pctl.io.facility.save_cfg_ack(_write_auto_config())
end end
-- handle a start command acknowledgement -- handle a start command acknowledgement
@@ -301,39 +494,57 @@ end
function process.start_ack_handle(response) function process.start_ack_handle(response)
local ack = response[1] local ack = response[1]
local ctl_proc = self.control_states.process local ctl_proc = pctl.control_states.process
ctl_proc.mode = response[2] ctl_proc.mode = response[2]
ctl_proc.burn_target = response[3] ctl_proc.burn_target = response[3]
ctl_proc.charge_target = response[4] ctl_proc.charge_target = response[4]
ctl_proc.gen_target = response[5] ctl_proc.gen_target = response[5]
for i = 1, math.min(#response[6], self.io.facility.num_units) do for i = 1, math.min(#response[6], pctl.io.facility.num_units) do
ctl_proc.limits[i] = response[6][i] ctl_proc.limits[i] = response[6][i]
pctl.io.units[i].unit_ps.publish("burn_limit", ctl_proc.limits[i])
local unit = self.io.units[i] ---@type ioctl_unit
unit.unit_ps.publish("burn_limit", ctl_proc.limits[i])
end end
self.io.facility.ps.publish("process_mode", ctl_proc.mode) pctl.io.facility.ps.publish("process_mode", ctl_proc.mode)
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target) pctl.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target) pctl.io.facility.ps.publish("process_charge_target", pctl.io.energy_convert_from_fe(ctl_proc.charge_target))
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target) pctl.io.facility.ps.publish("process_gen_target", pctl.io.energy_convert_from_fe(ctl_proc.gen_target))
self.io.facility.start_ack(ack) _write_auto_config()
process.fac_ack(F_CMD.START, ack)
end end
-- record waste product state after attempting to change it -- record waste product settting after attempting to change it
---@param response WASTE_PRODUCT supervisor waste product state ---@param response WASTE_PRODUCT supervisor waste product settting
function process.waste_ack_handle(response) function process.waste_ack_handle(response)
self.control_states.process.waste_product = response -- update config table and save
self.io.facility.ps.publish("process_waste_product", response) pctl.control_states.process.waste_product = response
_write_auto_config()
pctl.io.facility.ps.publish("process_waste_product", response)
end end
-- record plutonium fallback state after attempting to change it -- record plutonium fallback settting after attempting to change it
---@param response boolean supervisor plutonium fallback state ---@param response boolean supervisor plutonium fallback settting
function process.pu_fb_ack_handle(response) function process.pu_fb_ack_handle(response)
self.control_states.process.pu_fallback = response -- update config table and save
self.io.facility.ps.publish("process_pu_fallback", response) pctl.control_states.process.pu_fallback = response
_write_auto_config()
pctl.io.facility.ps.publish("process_pu_fallback", response)
end end
-- record SPS low power settting after attempting to change it
---@param response boolean supervisor SPS low power settting
function process.sps_lp_ack_handle(response)
-- update config table and save
pctl.control_states.process.sps_low_power = response
_write_auto_config()
pctl.io.facility.ps.publish("process_sps_low_power", response)
end
--#endregion
return process return process

View File

@@ -19,7 +19,7 @@ local unit_view = require("coordinator.ui.layout.unit_view")
local core = require("graphics.core") local core = require("graphics.core")
local flasher = require("graphics.flasher") local flasher = require("graphics.flasher")
local DisplayBox = require("graphics.elements.displaybox") local DisplayBox = require("graphics.elements.DisplayBox")
local log_render = coordinator.log_render local log_render = coordinator.log_render
@@ -30,20 +30,20 @@ local renderer = {}
local engine = { local engine = {
color_mode = 1, ---@type COLOR_MODE color_mode = 1, ---@type COLOR_MODE
monitors = nil, ---@type monitors_struct|nil monitors = nil, ---@type monitors_struct|nil
dmesg_window = nil, ---@type table|nil dmesg_window = nil, ---@type Window|nil
ui_ready = false, ui_ready = false,
fp_ready = false, fp_ready = false,
ui = { ui = {
front_panel = nil, ---@type graphics_element|nil front_panel = nil, ---@type DisplayBox|nil
main_display = nil, ---@type graphics_element|nil main_display = nil, ---@type DisplayBox|nil
flow_display = nil, ---@type graphics_element|nil flow_display = nil, ---@type DisplayBox|nil
unit_displays = {} unit_displays = {} ---@type (DisplayBox|nil)[]
}, },
disable_flow_view = false disable_flow_view = false
} }
-- init a display to the "default", but set text scale to 0.5 -- init a display to the "default", but set text scale to 0.5
---@param monitor table monitor ---@param monitor Monitor monitor
local function _init_display(monitor) local function _init_display(monitor)
monitor.setTextScale(0.5) monitor.setTextScale(0.5)
monitor.setTextColor(colors.white) monitor.setTextColor(colors.white)
@@ -64,7 +64,7 @@ local function _init_display(monitor)
end end
-- print out that the monitor is too small -- print out that the monitor is too small
---@param monitor table monitor ---@param monitor Monitor monitor
local function _print_too_small(monitor) local function _print_too_small(monitor)
monitor.setCursorPos(1, 1) monitor.setCursorPos(1, 1)
monitor.setBackgroundColor(colors.black) monitor.setBackgroundColor(colors.black)
@@ -137,7 +137,7 @@ function renderer.try_start_fp()
if not engine.fp_ready then if not engine.fp_ready then
-- show front panel view on terminal -- show front panel view on terminal
status, msg = pcall(function () status, msg = pcall(function ()
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root} engine.ui.front_panel = DisplayBox{window=term.current(),fg_bg=style.fp.root}
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays) panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
end) end)
@@ -275,7 +275,7 @@ function renderer.fp_ready() return engine.fp_ready end
function renderer.ui_ready() return engine.ui_ready end function renderer.ui_ready() return engine.ui_ready end
-- handle a monitor peripheral being disconnected -- handle a monitor peripheral being disconnected
---@param device table monitor ---@param device Monitor monitor
---@return boolean is_used if the monitor is one of the configured monitors ---@return boolean is_used if the monitor is one of the configured monitors
function renderer.handle_disconnect(device) function renderer.handle_disconnect(device)
local is_used = false local is_used = false
@@ -326,7 +326,7 @@ end
-- handle a monitor peripheral being reconnected -- handle a monitor peripheral being reconnected
---@param name string monitor name ---@param name string monitor name
---@param device table monitor ---@param device Monitor monitor
---@return boolean is_used if the monitor is one of the configured monitors ---@return boolean is_used if the monitor is one of the configured monitors
function renderer.handle_reconnect(name, device) function renderer.handle_reconnect(name, device)
local is_used = false local is_used = false
@@ -373,7 +373,7 @@ function renderer.handle_resize(name)
if not engine.monitors then return false, false end if not engine.monitors then return false, false end
if engine.monitors.main_name == name and engine.monitors.main then if engine.monitors.main_name == name and engine.monitors.main then
local device = engine.monitors.main ---@type table local device = engine.monitors.main ---@type Monitor
-- this is necessary if the bottom left block was broken and on reconnect -- this is necessary if the bottom left block was broken and on reconnect
_init_display(device) _init_display(device)
@@ -416,7 +416,7 @@ function renderer.handle_resize(name)
end end
else engine.dmesg_window.redraw() end else engine.dmesg_window.redraw() end
elseif engine.monitors.flow_name == name and engine.monitors.flow then elseif engine.monitors.flow_name == name and engine.monitors.flow then
local device = engine.monitors.flow ---@type table local device = engine.monitors.flow ---@type Monitor
-- this is necessary if the bottom left block was broken and on reconnect -- this is necessary if the bottom left block was broken and on reconnect
_init_display(device) _init_display(device)

View File

@@ -13,7 +13,7 @@ local self = {
nic = nil, ---@type nic nic = nil, ---@type nic
config = nil, ---@type crd_config config = nil, ---@type crd_config
next_id = 0, next_id = 0,
sessions = {} sessions = {} ---@type pkt_session_struct[]
} }
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
@@ -89,10 +89,11 @@ end
-- establish a new API session -- establish a new API session
---@nodiscard ---@nodiscard
---@param source_addr integer ---@param source_addr integer pocket computer ID
---@param version string ---@param i_seq_num integer initial (most recent) sequence number
---@param version string pocket version
---@return integer session_id ---@return integer session_id
function apisessions.establish_session(source_addr, version) function apisessions.establish_session(source_addr, i_seq_num, version)
---@class pkt_session_struct ---@class pkt_session_struct
local pkt_s = { local pkt_s = {
open = true, open = true,
@@ -105,7 +106,7 @@ function apisessions.establish_session(source_addr, version)
local id = self.next_id local id = self.next_id
pkt_s.instance = pocket.new_session(id, source_addr, pkt_s.in_queue, pkt_s.out_queue, self.config.API_Timeout) pkt_s.instance = pocket.new_session(id, source_addr, i_seq_num, pkt_s.in_queue, pkt_s.out_queue, self.config.API_Timeout)
table.insert(self.sessions, pkt_s) table.insert(self.sessions, pkt_s)
local mt = { local mt = {
@@ -128,7 +129,7 @@ end
---@param timer_event number ---@param timer_event number
function apisessions.check_all_watchdogs(timer_event) function apisessions.check_all_watchdogs(timer_event)
for i = 1, #self.sessions do for i = 1, #self.sessions do
local session = self.sessions[i] ---@type pkt_session_struct local session = self.sessions[i]
if session.open then if session.open then
local triggered = session.instance.check_wd(timer_event) local triggered = session.instance.check_wd(timer_event)
if triggered then if triggered then
@@ -142,7 +143,7 @@ end
-- iterate all the API sessions -- iterate all the API sessions
function apisessions.iterate_all() function apisessions.iterate_all()
for i = 1, #self.sessions do for i = 1, #self.sessions do
local session = self.sessions[i] ---@type pkt_session_struct local session = self.sessions[i]
if session.open and session.instance.iterate() then if session.open and session.instance.iterate() then
_api_handle_outq(session) _api_handle_outq(session)
@@ -167,7 +168,7 @@ end
-- close all open connections -- close all open connections
function apisessions.close_all() function apisessions.close_all()
for i = 1, #self.sessions do for i = 1, #self.sessions do
local session = self.sessions[i] ---@type pkt_session_struct local session = self.sessions[i]
if session.open then _shutdown(session) end if session.open then _shutdown(session) end
end end

View File

@@ -1,15 +1,22 @@
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local pocket = {} local pocket = {}
local PROTOCOL = comms.PROTOCOL local PROTOCOL = comms.PROTOCOL
local CRDN_TYPE = comms.CRDN_TYPE local CRDN_TYPE = comms.CRDN_TYPE
local MGMT_TYPE = comms.MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
local FAC_COMMAND = comms.FAC_COMMAND
local UNIT_COMMAND = comms.UNIT_COMMAND
local AUTO_GROUP = types.AUTO_GROUP
local WASTE_MODE = types.WASTE_MODE
-- retry time constants in ms -- retry time constants in ms
-- local INITIAL_WAIT = 1500 -- local INITIAL_WAIT = 1500
@@ -32,19 +39,22 @@ local PERIODICS = {
---@nodiscard ---@nodiscard
---@param id integer session ID ---@param id integer session ID
---@param s_addr integer device source address ---@param s_addr integer device source address
---@param i_seq_num integer initial sequence number
---@param in_queue mqueue in message queue ---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue ---@param out_queue mqueue out message queue
---@param timeout number communications timeout ---@param timeout number communications timeout
function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
local log_header = "pkt_session(" .. id .. "): " local log_tag = "pkt_session(" .. id .. "): "
local self = { local self = {
-- connection properties -- connection properties
seq_num = 0, seq_num = i_seq_num + 2, -- next after the establish approval was sent
r_seq_num = nil, r_seq_num = i_seq_num + 1,
connected = true, connected = true,
conn_watchdog = util.new_watchdog(timeout), conn_watchdog = util.new_watchdog(timeout),
last_rtt = 0, last_rtt = 0,
-- process accessor handle
proc_handle = process.create_handle(),
-- periodic messages -- periodic messages
periodics = { periodics = {
last_update = 0, last_update = 0,
@@ -100,17 +110,33 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
end end
-- link callback transmissions
local f_ack = self.proc_handle.fac_ack
f_ack.on_scram = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.SCRAM_ALL, success }) end
f_ack.on_ack_alarms = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.ACK_ALL_ALARMS, success }) end
f_ack.on_start = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.START, success }) end
f_ack.on_stop = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.STOP, success }) end
for u = 1, iocontrol.get_db().facility.num_units do
local u_ack = self.proc_handle.unit_ack[u]
u_ack.on_start = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.START, u, success }) end
u_ack.on_scram = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.SCRAM, u, success }) end
u_ack.on_rps_reset = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.RESET_RPS, u, success }) end
u_ack.on_ack_alarms = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.ACK_ALL_ALARMS, u, success }) end
end
-- handle a packet -- handle a packet
---@param pkt mgmt_frame|crdn_frame ---@param pkt mgmt_frame|crdn_frame
local function _handle_packet(pkt) local function _handle_packet(pkt)
-- check sequence number -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num ~= pkt.scada_frame.seq_num() then
self.r_seq_num = pkt.scada_frame.seq_num() log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. 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 return
else else
self.r_seq_num = pkt.scada_frame.seq_num() self.r_seq_num = pkt.scada_frame.seq_num() + 1
end end
-- feed watchdog -- feed watchdog
@@ -123,7 +149,108 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
local db = iocontrol.get_db() local db = iocontrol.get_db()
-- handle packet by type -- handle packet by type
if pkt.type == CRDN_TYPE.API_GET_FAC then if pkt.type == CRDN_TYPE.FAC_CMD then
if pkt.length >= 1 then
local cmd = pkt.data[1]
if cmd == FAC_COMMAND.SCRAM_ALL then
log.info(log_tag .. "FAC SCRAM ALL")
self.proc_handle.fac_scram()
elseif cmd == FAC_COMMAND.STOP then
log.info(log_tag .. "STOP PROCESS CTRL")
self.proc_handle.process_stop()
elseif cmd == FAC_COMMAND.START then
if pkt.length == 6 then
log.info(log_tag .. "START PROCESS CTRL")
self.proc_handle.process_start_remote(pkt.data[2], pkt.data[3], pkt.data[4], pkt.data[5], pkt.data[6])
else
log.debug(log_tag .. "CRDN auto start (with configuration) packet length mismatch")
end
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
log.info(log_tag .. "FAC ACK ALL ALARMS")
self.proc_handle.fac_ack_alarms()
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
if pkt.length == 2 then
log.info(util.c(log_tag, " SET WASTE ", pkt.data[2]))
process.set_process_waste(pkt.data[2])
else
log.debug(log_tag .. "CRDN set waste mode packet length mismatch")
end
elseif cmd == FAC_COMMAND.SET_PU_FB then
if pkt.length == 2 then
log.info(util.c(log_tag, " SET PU FALLBACK ", pkt.data[2]))
process.set_pu_fallback(pkt.data[2] == true)
else
log.debug(log_tag .. "CRDN set pu fallback packet length mismatch")
end
elseif cmd == FAC_COMMAND.SET_SPS_LP then
if pkt.length == 2 then
log.info(util.c(log_tag, " SET SPS LOW POWER ", pkt.data[2]))
process.set_sps_low_power(pkt.data[2] == true)
else
log.debug(log_tag .. "CRDN set sps low power packet length mismatch")
end
else
log.debug(log_tag .. "CRDN facility command unknown")
end
else
log.debug(log_tag .. "CRDN facility command packet length mismatch")
end
elseif pkt.type == CRDN_TYPE.UNIT_CMD then
if pkt.length >= 2 then
-- get command and unit id
local cmd = pkt.data[1]
local uid = pkt.data[2]
-- continue if valid unit id
if util.is_int(uid) and uid > 0 and uid <= #db.units then
if cmd == UNIT_COMMAND.SCRAM then
log.info(util.c(log_tag, "UNIT[", uid, "] SCRAM"))
self.proc_handle.scram(uid)
elseif cmd == UNIT_COMMAND.START then
log.info(util.c(log_tag, "UNIT[", uid, "] START"))
self.proc_handle.start(uid)
elseif cmd == UNIT_COMMAND.RESET_RPS then
log.info(util.c(log_tag, "UNIT[", uid, "] RESET RPS"))
self.proc_handle.reset_rps(uid)
elseif cmd == UNIT_COMMAND.SET_BURN then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") then
log.info(util.c(log_tag, "UNIT[", uid, "] SET BURN ", pkt.data[3]))
process.set_rate(uid, pkt.data[3])
else
log.debug(log_tag .. "CRDN unit command burn rate missing option")
end
elseif cmd == UNIT_COMMAND.SET_WASTE then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and
(pkt.data[3] >= WASTE_MODE.AUTO) and (pkt.data[3] <= WASTE_MODE.MANUAL_ANTI_MATTER) then
log.info(util.c(log_tag, "UNIT[", id, "] SET WASTE ", pkt.data[3]))
process.set_unit_waste(uid, pkt.data[3])
else
log.debug(log_tag .. "CRDN unit command set waste missing/invalid option")
end
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
log.info(util.c(log_tag, "UNIT[", uid, "] ACK ALL ALARMS"))
self.proc_handle.ack_all_alarms(uid)
elseif cmd == UNIT_COMMAND.ACK_ALARM then
elseif cmd == UNIT_COMMAND.RESET_ALARM then
elseif cmd == UNIT_COMMAND.SET_GROUP then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and
(pkt.data[3] >= AUTO_GROUP.MANUAL) and (pkt.data[3] <= AUTO_GROUP.BACKUP) then
log.info(util.c(log_tag, "UNIT[", uid, "] SET GROUP ", pkt.data[3]))
process.set_group(uid, pkt.data[3])
else
log.debug(log_tag .. "CRDN unit set group missing option")
end
else
log.debug(log_tag .. "CRDN unit command unknown")
end
else
log.debug(log_tag .. "CRDN unit command invalid")
end
else
log.debug(log_tag .. "CRDN unit command packet length mismatch")
end
elseif pkt.type == CRDN_TYPE.API_GET_FAC then
local fac = db.facility local fac = db.facility
local data = { local data = {
@@ -133,33 +260,182 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
{ fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated }, { fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated },
{ fac.auto_current_waste_product, fac.auto_pu_fallback_active }, { fac.auto_current_waste_product, fac.auto_pu_fallback_active },
util.table_len(fac.tank_data_tbl), util.table_len(fac.tank_data_tbl),
fac.induction_data_tbl[1] ~= nil, fac.induction_data_tbl[1] ~= nil, ---@fixme this means nothing
fac.sps_data_tbl[1] ~= nil, fac.sps_data_tbl[1] ~= nil ---@fixme this means nothing
} }
_send(CRDN_TYPE.API_GET_FAC, data) _send(CRDN_TYPE.API_GET_FAC, data)
elseif pkt.type == CRDN_TYPE.API_GET_FAC_DTL then
local fac = db.facility
local mtx_sps = fac.induction_ps_tbl[1]
local units = {}
local tank_statuses = {}
for i = 1, #db.units do
local u = db.units[i]
units[i] = { u.connected, u.annunciator, u.reactor_data, u.tank_data_tbl }
for t = 1, #u.tank_ps_tbl do table.insert(tank_statuses, u.tank_ps_tbl[t].get("computed_status")) end
end
for i = 1, #fac.tank_ps_tbl do table.insert(tank_statuses, fac.tank_ps_tbl[i].get("computed_status")) end
local matrix_data = {
mtx_sps.get("eta_string"),
mtx_sps.get("avg_charge"),
mtx_sps.get("avg_inflow"),
mtx_sps.get("avg_outflow"),
mtx_sps.get("is_charging"),
mtx_sps.get("is_discharging"),
mtx_sps.get("at_max_io")
}
local data = {
fac.all_sys_ok,
fac.rtu_count,
fac.auto_scram,
fac.ascram_status,
tank_statuses,
fac.tank_data_tbl,
fac.induction_ps_tbl[1].get("computed_status") or types.IMATRIX_STATE.OFFLINE,
fac.induction_data_tbl[1],
matrix_data,
fac.sps_ps_tbl[1].get("computed_status") or types.SPS_STATE.OFFLINE,
fac.sps_data_tbl[1],
units
}
_send(CRDN_TYPE.API_GET_FAC_DTL, data)
elseif pkt.type == CRDN_TYPE.API_GET_UNIT then elseif pkt.type == CRDN_TYPE.API_GET_UNIT then
if pkt.length == 1 and type(pkt.data[1]) == "number" then if pkt.length == 1 and type(pkt.data[1]) == "number" then
local u = db.units[pkt.data[1]] ---@type ioctl_unit local u = db.units[pkt.data[1]]
local statuses = { u.unit_ps.get("computed_status") }
for i = 1, #u.boiler_ps_tbl do table.insert(statuses, u.boiler_ps_tbl[i].get("computed_status")) end
for i = 1, #u.turbine_ps_tbl do table.insert(statuses, u.turbine_ps_tbl[i].get("computed_status")) end
for i = 1, #u.tank_ps_tbl do table.insert(statuses, u.tank_ps_tbl[i].get("computed_status")) end
if u then if u then
local data = { local data = {
u.unit_id, u.unit_id,
u.connected, u.connected,
u.rtu_hw, statuses,
u.a_group,
u.alarms, u.alarms,
u.annunciator, u.annunciator,
u.reactor_data, u.reactor_data,
u.boiler_data_tbl, u.boiler_data_tbl,
u.turbine_data_tbl, u.turbine_data_tbl,
u.tank_data_tbl u.tank_data_tbl,
u.last_rate_change_ms,
u.turbine_flow_stable
} }
_send(CRDN_TYPE.API_GET_UNIT, data) _send(CRDN_TYPE.API_GET_UNIT, data)
end end
end end
elseif pkt.type == CRDN_TYPE.API_GET_CTRL then
local data = {}
for i = 1, #db.units do
local u = db.units[i]
data[i] = {
u.connected,
u.reactor_data.rps_tripped,
u.reactor_data.mek_status.status,
u.reactor_data.mek_status.temp,
u.reactor_data.mek_status.burn_rate,
u.reactor_data.mek_status.act_burn_rate,
u.reactor_data.mek_struct.max_burn,
u.annunciator.AutoControl,
u.a_group
}
end
_send(CRDN_TYPE.API_GET_CTRL, data)
elseif pkt.type == CRDN_TYPE.API_GET_PROC then
local data = {}
local fac = db.facility
local proc = process.get_control_states().process
-- unit data
for i = 1, #db.units do
local u = db.units[i]
data[i] = {
u.reactor_data.mek_status.status,
u.reactor_data.mek_struct.max_burn,
proc.limits[i],
u.auto_ready,
u.auto_degraded,
u.annunciator.AutoControl,
u.a_group
}
end
-- facility data
data[#db.units + 1] = {
fac.status_lines,
{ fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated },
fac.auto_scram,
fac.ascram_status,
{ proc.mode, proc.burn_target, proc.charge_target, proc.gen_target }
}
_send(CRDN_TYPE.API_GET_PROC, data)
elseif pkt.type == CRDN_TYPE.API_GET_WASTE then
local data = {}
local fac = db.facility
local proc = process.get_control_states().process
-- unit data
for i = 1, #db.units do
local u = db.units[i]
data[i] = {
u.waste_mode,
u.waste_product,
u.num_snas,
u.sna_peak_rate,
u.sna_max_rate,
u.sna_out_rate,
u.waste_stats
}
end
local process_rate = 0
if fac.sps_data_tbl[1].state then
process_rate = fac.sps_data_tbl[1].state.process_rate
end
-- facility data
data[#db.units + 1] = {
fac.auto_current_waste_product,
fac.auto_pu_fallback_active,
fac.auto_sps_disabled,
proc.waste_product,
proc.pu_fallback,
proc.sps_low_power,
fac.waste_stats,
fac.sps_ps_tbl[1].get("computed_status") or types.SPS_STATE.OFFLINE,
process_rate
}
_send(CRDN_TYPE.API_GET_WASTE, data)
elseif pkt.type == CRDN_TYPE.API_GET_RAD then
local data = {}
for i = 1, #db.units do data[i] = db.units[i].rad_monitors end
data[#db.units + 1] = db.facility.rad_monitors
_send(CRDN_TYPE.API_GET_RAD, data)
else else
log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type) log.debug(log_tag .. "handler received unsupported CRDN packet type " .. pkt.type)
end end
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
---@cast pkt mgmt_frame ---@cast pkt mgmt_frame
@@ -172,7 +448,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
self.last_rtt = srv_now - srv_start self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then if self.last_rtt > 750 then
log.warning(log_header .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)") log.warning(log_tag .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
end end
-- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms") -- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms")
@@ -180,13 +456,17 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
iocontrol.fp_pkt_rtt(id, self.last_rtt) iocontrol.fp_pkt_rtt(id, self.last_rtt)
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_tag .. "SCADA keep alive packet length mismatch")
end end
elseif pkt.type == MGMT_TYPE.CLOSE then elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session -- close the session
_close() _close()
elseif pkt.type == MGMT_TYPE.ESTABLISH then
-- something is wrong, kill the session
_close()
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
else else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end end
end end
end end
@@ -211,7 +491,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
function public.close() function public.close()
_close() _close()
_send_mgmt(MGMT_TYPE.CLOSE, {}) _send_mgmt(MGMT_TYPE.CLOSE, {})
log.info(log_header .. "session closed by server") log.info(log_tag .. "session closed by server")
end end
-- iterate the session -- iterate the session
@@ -242,14 +522,14 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
-- max 100ms spent processing queue -- max 100ms spent processing queue
if util.time() - handle_start > 100 then if util.time() - handle_start > 100 then
log.warning(log_header .. "exceeded 100ms queue process limit") log.warning(log_tag .. "exceeded 100ms queue process limit")
break break
end end
end end
-- exit if connection was closed -- exit if connection was closed
if not self.connected then if not self.connected then
log.info(log_header .. "session closed by remote host") log.info(log_tag .. "session closed by remote host")
return self.connected return self.connected
end end

View File

@@ -9,7 +9,7 @@ local log = require("scada-common.log")
local sounder = {} local sounder = {}
local alarm_ctl = { local alarm_ctl = {
speaker = nil, speaker = nil, ---@type Speaker
volume = 0.5, volume = 0.5,
stream = audio.new_stream() stream = audio.new_stream()
} }
@@ -24,7 +24,7 @@ local function play()
end end
-- initialize the annunciator alarm system -- initialize the annunciator alarm system
---@param speaker table speaker peripheral ---@param speaker Speaker speaker peripheral
---@param volume number speaker volume ---@param volume number speaker volume
function sounder.init(speaker, volume) function sounder.init(speaker, volume)
alarm_ctl.speaker = speaker alarm_ctl.speaker = speaker
@@ -36,7 +36,7 @@ function sounder.init(speaker, volume)
end end
-- reconnect the speaker peripheral -- reconnect the speaker peripheral
---@param speaker table speaker peripheral ---@param speaker Speaker speaker peripheral
function sounder.reconnect(speaker) function sounder.reconnect(speaker)
alarm_ctl.speaker = speaker alarm_ctl.speaker = speaker
alarm_ctl.playing = false alarm_ctl.playing = false
@@ -44,7 +44,7 @@ function sounder.reconnect(speaker)
end end
-- set alarm tones -- set alarm tones
---@param states table alarm tone commands from supervisor ---@param states { [TONE]: boolean } alarm tone commands from supervisor
function sounder.set(states) function sounder.set(states)
-- set tone states -- set tone states
for id = 1, #states do alarm_ctl.stream.set_active(id, states[id]) end for id = 1, #states do alarm_ctl.stream.set_active(id, states[id]) end

View File

@@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder") local sounder = require("coordinator.sounder")
local threads = require("coordinator.threads") local threads = require("coordinator.threads")
local COORDINATOR_VERSION = "v1.4.6" local COORDINATOR_VERSION = "v1.6.16"
local CHUNK_LOAD_DELAY_S = 30.0 local CHUNK_LOAD_DELAY_S = 30.0
@@ -151,8 +151,8 @@ local function main()
-- core coordinator devices -- core coordinator devices
crd_dev = { crd_dev = {
speaker = ppm.get_device("speaker"), modem = ppm.get_wireless_modem(),
modem = ppm.get_wireless_modem() speaker = ppm.get_device("speaker") ---@type Speaker|nil
}, },
-- system objects -- system objects

View File

@@ -6,6 +6,7 @@ local util = require("scada-common.util")
local coordinator = require("coordinator.coordinator") local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local renderer = require("coordinator.renderer") local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder") local sounder = require("coordinator.sounder")
@@ -23,7 +24,8 @@ local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local RENDER_SLEEP = 100 -- (100ms, 2 ticks) local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
local MQ__RENDER_CMD = { local MQ__RENDER_CMD = {
START_MAIN_UI = 1 START_MAIN_UI = 1,
CLOSE_MAIN_UI = 2
} }
local MQ__RENDER_DATA = { local MQ__RENDER_DATA = {
@@ -67,6 +69,7 @@ function threads.thread__main(smem)
if type ~= nil and device ~= nil then if type ~= nil and device ~= nil then
if type == "modem" then if type == "modem" then
---@cast device Modem
-- we only really care if this is our wireless modem -- we only really care if this is our wireless modem
-- if it is another modem, handle other peripheral losses separately -- if it is another modem, handle other peripheral losses separately
if nic.is_modem(device) then if nic.is_modem(device) then
@@ -79,7 +82,7 @@ function threads.thread__main(smem)
nic.connect(other_modem) nic.connect(other_modem)
else else
-- close out main UI -- close out main UI
renderer.close_ui() smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
-- alert user to status -- alert user to status
log_sys("awaiting comms modem reconnect...") log_sys("awaiting comms modem reconnect...")
@@ -90,8 +93,10 @@ function threads.thread__main(smem)
log_sys("non-comms modem disconnected") log_sys("non-comms modem disconnected")
end end
elseif type == "monitor" then elseif type == "monitor" then
---@cast device Monitor
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, device) smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, device)
elseif type == "speaker" then elseif type == "speaker" then
---@cast device Speaker
log_sys("lost alarm sounder speaker") log_sys("lost alarm sounder speaker")
iocontrol.fp_has_speaker(false) iocontrol.fp_has_speaker(false)
end end
@@ -101,6 +106,7 @@ function threads.thread__main(smem)
if type ~= nil and device ~= nil then if type ~= nil and device ~= nil then
if type == "modem" then if type == "modem" then
---@cast device Modem
if device.isWireless() and not nic.is_connected() then if device.isWireless() and not nic.is_connected() then
-- reconnected modem -- reconnected modem
log_sys("comms modem reconnected") log_sys("comms modem reconnected")
@@ -112,8 +118,10 @@ function threads.thread__main(smem)
log_sys("wired modem reconnected") log_sys("wired modem reconnected")
end end
elseif type == "monitor" then elseif type == "monitor" then
---@cast device Monitor
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, { name = param1, device = device }) smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, { name = param1, device = device })
elseif type == "speaker" then elseif type == "speaker" then
---@cast device Speaker
log_sys("alarm sounder speaker reconnected") log_sys("alarm sounder speaker reconnected")
sounder.reconnect(device) sounder.reconnect(device)
iocontrol.fp_has_speaker(true) iocontrol.fp_has_speaker(true)
@@ -147,6 +155,9 @@ function threads.thread__main(smem)
apisessions.iterate_all() apisessions.iterate_all()
apisessions.free_all_closed() apisessions.free_all_closed()
-- clear timed out process commands
process.clear_timed_out()
if renderer.ui_ready() then if renderer.ui_ready() then
-- update clock used on main and flow monitors -- update clock used on main and flow monitors
iocontrol.get_db().facility.ps.publish("date_time", os.date(smem.date_format)) iocontrol.get_db().facility.ps.publish("date_time", os.date(smem.date_format))
@@ -157,9 +168,9 @@ function threads.thread__main(smem)
-- supervisor watchdog timeout -- supervisor watchdog timeout
log_comms("supervisor server timeout") log_comms("supervisor server timeout")
-- close connection, main UI, and stop sounder -- close main UI, connection, and stop sounder
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
coord_comms.close() coord_comms.close()
renderer.close_ui()
sounder.stop() sounder.stop()
else else
-- a non-clock/main watchdog timer event -- a non-clock/main watchdog timer event
@@ -178,9 +189,9 @@ function threads.thread__main(smem)
if coord_comms.handle_packet(packet) then if coord_comms.handle_packet(packet) then
log_comms("supervisor closed connection") log_comms("supervisor closed connection")
-- close connection, main UI, and stop sounder -- close main UI, connection, and stop sounder
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
coord_comms.close() coord_comms.close()
renderer.close_ui()
sounder.stop() sounder.stop()
end end
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
@@ -244,7 +255,7 @@ function threads.thread__main(smem)
return public return public
end end
-- coordinator renderer thread, tasked with long duration re-draws -- coordinator renderer thread, tasked with long duration draws
---@nodiscard ---@nodiscard
---@param smem crd_shared_memory ---@param smem crd_shared_memory
function threads.thread__render(smem) function threads.thread__render(smem)
@@ -292,6 +303,13 @@ function threads.thread__render(smem)
else else
log_render("main UI draw took " .. (util.time_ms() - draw_start) .. "ms") log_render("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
end end
elseif msg.message == MQ__RENDER_CMD.CLOSE_MAIN_UI then
-- close the main UI if it has been drawn
if renderer.ui_ready() then
log_render("closing main UI...")
renderer.close_ui()
log_render("main UI closed")
end
end end
elseif msg.qtype == mqueue.TYPE.DATA then elseif msg.qtype == mqueue.TYPE.DATA then
-- received data -- received data

View File

@@ -1,21 +1,21 @@
local style = require("coordinator.ui.style")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local core = require("graphics.core") local core = require("graphics.core")
local Rectangle = require("graphics.elements.rectangle") local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.TextBox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local StateIndicator = require("graphics.elements.indicators.state") local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local VerticalBar = require("graphics.elements.indicators.vbar") local VerticalBar = require("graphics.elements.indicators.VerticalBar")
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
-- new boiler view -- new boiler view
---@param root graphics_element parent ---@param root Container parent
---@param x integer top left x ---@param x integer top left x
---@param y integer top left y ---@param y integer top left y
---@param ps psil ps interface ---@param ps psil ps interface
@@ -35,10 +35,10 @@ local function new_view(root, x, y, ps)
temp.register(ps, "temperature", function (t) temp.update(db.temp_convert(t)) end) temp.register(ps, "temperature", function (t) temp.update(db.temp_convert(t)) end)
boil_r.register(ps, "boil_rate", boil_r.update) boil_r.register(ps, "boil_rate", boil_r.update)
TextBox{parent=boiler,text="H",x=2,y=5,height=1,width=1,fg_bg=text_fg} TextBox{parent=boiler,text="H",x=2,y=5,width=1,fg_bg=text_fg}
TextBox{parent=boiler,text="W",x=3,y=5,height=1,width=1,fg_bg=text_fg} TextBox{parent=boiler,text="W",x=3,y=5,width=1,fg_bg=text_fg}
TextBox{parent=boiler,text="S",x=27,y=5,height=1,width=1,fg_bg=text_fg} TextBox{parent=boiler,text="S",x=27,y=5,width=1,fg_bg=text_fg}
TextBox{parent=boiler,text="C",x=28,y=5,height=1,width=1,fg_bg=text_fg} TextBox{parent=boiler,text="C",x=28,y=5,width=1,fg_bg=text_fg}
local hcool = VerticalBar{parent=boiler,x=2,y=1,fg_bg=cpair(colors.orange,colors.gray),height=4,width=1} local 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 water = VerticalBar{parent=boiler,x=3,y=1,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1}

View File

@@ -1,18 +1,20 @@
local util = require("scada-common.util") local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
local core = require("graphics.core") local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.Div")
local Rectangle = require("graphics.elements.rectangle") local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.TextBox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IndicatorLight = require("graphics.elements.indicators.light") local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local PowerIndicator = require("graphics.elements.indicators.power") local PowerIndicator = require("graphics.elements.indicators.PowerIndicator")
local StateIndicator = require("graphics.elements.indicators.state") local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local VerticalBar = require("graphics.elements.indicators.vbar") local VerticalBar = require("graphics.elements.indicators.VerticalBar")
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
@@ -20,13 +22,12 @@ local border = core.border
local ALIGN = core.ALIGN local ALIGN = core.ALIGN
-- new induction matrix view -- new induction matrix view
---@param root graphics_element parent ---@param root Container parent
---@param x integer top left x ---@param x integer top left x
---@param y integer top left y ---@param y integer top left y
---@param data imatrix_session_db matrix data
---@param ps psil ps interface ---@param ps psil ps interface
---@param id number? matrix ID ---@param id number? matrix ID
local function new_view(root, x, y, data, ps, id) local function new_view(root, x, y, ps, id)
local label_fg = style.theme.label_fg local label_fg = style.theme.label_fg
local text_fg = style.theme.text_fg local text_fg = style.theme.text_fg
local lu_col = style.lu_colors local lu_col = style.lu_colors
@@ -34,6 +35,8 @@ local function new_view(root, x, y, data, ps, id)
local ind_yel = style.ind_yel local ind_yel = style.ind_yel
local ind_wht = style.ind_wht local ind_wht = style.ind_wht
local db = iocontrol.get_db()
local title = "INDUCTION MATRIX" local title = "INDUCTION MATRIX"
if type(id) == "number" then title = title .. id end if type(id) == "number" then title = title .. id end
@@ -42,30 +45,30 @@ local function new_view(root, x, y, data, ps, id)
-- black has low contrast with dark gray, so if background is black use white instead -- black has low contrast with dark gray, so if background is black use white instead
local cutout_fg_bg = cpair(util.trinary(style.theme.bg == colors.black, colors.white, style.theme.bg), colors.gray) local cutout_fg_bg = cpair(util.trinary(style.theme.bg == colors.black, colors.white, style.theme.bg), colors.gray)
TextBox{parent=matrix,text=" ",width=33,height=1,x=1,y=1,fg_bg=cutout_fg_bg} TextBox{parent=matrix,text=" ",width=33,x=1,y=1,fg_bg=cutout_fg_bg}
TextBox{parent=matrix,text=title,alignment=ALIGN.CENTER,width=33,height=1,x=1,y=2,fg_bg=cutout_fg_bg} TextBox{parent=matrix,text=title,alignment=ALIGN.CENTER,width=33,x=1,y=2,fg_bg=cutout_fg_bg}
local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3} local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14} local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
local capacity = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Capacity:",format="%8.2f",value=0,width=26,fg_bg=text_fg} local capacity = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Capacity:",unit=db.energy_label,format="%8.2f",value=0,width=26,fg_bg=text_fg}
local energy = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg} local energy = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Energy: ",unit=db.energy_label,format="%8.2f",value=0,width=26,fg_bg=text_fg}
local avg_chg = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",value=0,width=26,fg_bg=text_fg} local avg_chg = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="\xb7Average:",unit=db.energy_label,format="%8.2f",value=0,width=26,fg_bg=text_fg}
local input = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Input: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} local input = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Input: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local avg_in = PowerIndicator{parent=rect,x=7,y=7,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} local avg_in = PowerIndicator{parent=rect,x=7,y=7,lu_colors=lu_col,label="\xb7Average:",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local output = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Output: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} local output = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Output: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local avg_out = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} local avg_out = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="\xb7Average:",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local trans_cap = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Max I/O: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} local trans_cap = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Max I/O: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
status.register(ps, "computed_status", status.update) status.register(ps, "computed_status", status.update)
capacity.register(ps, "max_energy", function (val) capacity.update(util.joules_to_fe(val)) end) capacity.register(ps, "max_energy", function (val) capacity.update(db.energy_convert(val)) end)
energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end) energy.register(ps, "energy", function (val) energy.update(db.energy_convert(val)) end)
avg_chg.register(ps, "avg_charge", avg_chg.update) avg_chg.register(ps, "avg_charge", avg_chg.update)
input.register(ps, "last_input", function (val) input.update(util.joules_to_fe(val)) end) input.register(ps, "last_input", function (val) input.update(db.energy_convert(val)) end)
avg_in.register(ps, "avg_inflow", avg_in.update) avg_in.register(ps, "avg_inflow", avg_in.update)
output.register(ps, "last_output", function (val) output.update(util.joules_to_fe(val)) end) output.register(ps, "last_output", function (val) output.update(db.energy_convert(val)) end)
avg_out.register(ps, "avg_outflow", avg_out.update) avg_out.register(ps, "avg_outflow", avg_out.update)
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end) trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(db.energy_convert(val)) end)
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill: ",format="%7.2f",unit="%",value=0,width=20,fg_bg=text_fg} local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill: ",format="%7.2f",unit="%",value=0,width=20,fg_bg=text_fg}
local cells = DataIndicator{parent=rect,x=11,y=13,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg} local cells = DataIndicator{parent=rect,x=11,y=13,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg}
@@ -87,9 +90,10 @@ local function new_view(root, x, y, data, ps, id)
local in_cap = VerticalBar{parent=rect,x=7,y=12,fg_bg=cpair(colors.red,colors.gray),height=7,width=1} local 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} local out_cap = VerticalBar{parent=rect,x=9,y=12,fg_bg=cpair(colors.blue,colors.gray),height=7,width=1}
TextBox{parent=rect,text="FILL I/O",x=2,y=20,height=1,width=8,fg_bg=label_fg} TextBox{parent=rect,text="FILL I/O",x=2,y=20,width=8,fg_bg=label_fg}
local function calc_saturation(val) local function calc_saturation(val)
local data = db.facility.induction_data_tbl[id or 1]
if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then 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 return val / data.build.transfer_cap
else return 0 end else return 0 end
@@ -99,48 +103,9 @@ local function new_view(root, x, y, data, ps, id)
in_cap.register(ps, "last_input", function (val) in_cap.update(calc_saturation(val)) end) 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) out_cap.register(ps, "last_output", function (val) out_cap.update(calc_saturation(val)) end)
local eta = TextBox{parent=rect,x=11,y=20,width=20,height=1,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=style.theme.field_box} local eta = TextBox{parent=rect,x=11,y=20,width=20,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=style.theme.field_box}
eta.register(ps, "eta_ms", function (eta_ms) eta.register(ps, "eta_string", eta.set_value)
local str, pre = "", util.trinary(eta_ms >= 0, "Full in ", "Empty in ")
local seconds = math.abs(eta_ms) / 1000
local minutes = seconds / 60
local hours = minutes / 60
local days = hours / 24
if math.abs(eta_ms) < 1000 or (eta_ms ~= eta_ms) then
-- really small or NaN
str = "No ETA"
elseif days < 1000 then
days = math.floor(days)
hours = math.floor(hours % 24)
minutes = math.floor(minutes % 60)
seconds = math.floor(seconds % 60)
if days > 0 then
str = days .. "d"
elseif hours > 0 then
str = hours .. "h " .. minutes .. "m"
elseif minutes > 0 then
str = minutes .. "m " .. seconds .. "s"
elseif seconds > 0 then
str = seconds .. "s"
end
str = pre .. str
else
local years = math.floor(days / 365.25)
if years <= 99999999 then
str = pre .. years .. "y"
else
str = pre .. "eras"
end
end
eta.set_value(str)
end)
end end
return new_view return new_view

View File

@@ -8,17 +8,17 @@ local style = require("coordinator.ui.style")
local core = require("graphics.core") local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.Div")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.TextBox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local ALIGN = core.ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
-- create a pocket list entry -- create a pocket list entry
---@param parent graphics_element parent ---@param parent ListBox parent
---@param id integer PKT session ID ---@param id integer PKT session ID
local function init(parent, id) local function init(parent, id)
local s_hi_box = style.fp_theme.highlight_box local s_hi_box = style.fp_theme.highlight_box
@@ -28,24 +28,26 @@ local function init(parent, id)
local ps = iocontrol.get_db().fp.ps local ps = iocontrol.get_db().fp.ps
local term_w, _ = term.getSize()
-- root div -- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true} local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=s_hi_bright} local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=s_hi_bright}
local ps_prefix = "pkt_" .. id .. "_" local ps_prefix = "pkt_" .. id .. "_"
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=s_hi_box} TextBox{parent=entry,x=1,y=1,text="",width=8,fg_bg=s_hi_box}
local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,height=1,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)} local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)}
TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=s_hi_box} TextBox{parent=entry,x=1,y=3,text="",width=8,fg_bg=s_hi_box}
pkt_addr.register(ps, ps_prefix .. "addr", pkt_addr.set_value) pkt_addr.register(ps, ps_prefix .. "addr", pkt_addr.set_value)
TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1} TextBox{parent=entry,x=10,y=2,text="FW:",width=3}
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=label_fg} local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,fg_bg=label_fg}
pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value) 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} TextBox{parent=entry,x=term_w-16,y=2,text="RTT:",width=4}
local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg} local pkt_rtt = DataIndicator{parent=entry,x=term_w-11,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=label_fg} TextBox{parent=entry,x=term_w-5,y=2,text="ms",width=4,fg_bg=label_fg}
pkt_rtt.register(ps, ps_prefix .. "rtt", pkt_rtt.update) pkt_rtt.register(ps, ps_prefix .. "rtt", pkt_rtt.update)
pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor) pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor)

View File

@@ -8,20 +8,20 @@ local style = require("coordinator.ui.style")
local core = require("graphics.core") local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.Div")
local Rectangle = require("graphics.elements.rectangle") local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.TextBox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IndicatorLight = require("graphics.elements.indicators.light") local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local RadIndicator = require("graphics.elements.indicators.rad") local RadIndicator = require("graphics.elements.indicators.RadIndicator")
local StateIndicator = require("graphics.elements.indicators.state") local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local TriIndicatorLight = require("graphics.elements.indicators.trilight") local TriIndicatorLight = require("graphics.elements.indicators.TriIndicatorLight")
local Checkbox = require("graphics.elements.controls.checkbox") local Checkbox = require("graphics.elements.controls.Checkbox")
local HazardButton = require("graphics.elements.controls.hazard_button") local HazardButton = require("graphics.elements.controls.HazardButton")
local RadioButton = require("graphics.elements.controls.radio_button") local NumericSpinbox = require("graphics.elements.controls.NumericSpinbox")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric") local RadioButton = require("graphics.elements.controls.RadioButton")
local ALIGN = core.ALIGN local ALIGN = core.ALIGN
@@ -33,7 +33,7 @@ local bw_fg_bg = style.bw_fg_bg
local period = core.flasher.PERIOD local period = core.flasher.PERIOD
-- new process control view -- new process control view
---@param root graphics_element parent ---@param root Container parent
---@param x integer top left x ---@param x integer top left x
---@param y integer top left y ---@param y integer top left y
local function new_view(root, x, y) local function new_view(root, x, y)
@@ -56,16 +56,18 @@ local function new_view(root, x, y)
local blk_brn = cpair(colors.black, colors.brown) local blk_brn = cpair(colors.black, colors.brown)
local blk_pur = cpair(colors.black, colors.purple) local blk_pur = cpair(colors.black, colors.purple)
local facility = iocontrol.get_db().facility local db = iocontrol.get_db()
local units = iocontrol.get_db().units
local facility = db.facility
local units = db.units
local main = Div{parent=root,width=128,height=24,x=x,y=y} 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 scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=db.process.fac_scram,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,fg_bg=hzd_fg_bg} local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=db.process.fac_ack_alarms,fg_bg=hzd_fg_bg}
facility.scram_ack = scram.on_response db.process.fac_ack.on_scram = scram.on_response
facility.ack_alarms_ack = ack_a.on_response db.process.fac_ack.on_ack_alarms = ack_a.on_response
local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=ind_grn} local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=ind_grn}
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=style.ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd} local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=style.ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd}
@@ -92,24 +94,24 @@ local function new_view(root, x, y)
main.line_break() main.line_break()
local auto_scram = IndicatorLight{parent=main,label="Automatic SCRAM",colors=ind_red,flash=true,period=period.BLINK_250_MS} local auto_scram = IndicatorLight{parent=main,label="Automatic SCRAM",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local matrix_dc = IndicatorLight{parent=main,label="Matrix Disconnected",colors=ind_yel,flash=true,period=period.BLINK_500_MS} local matrix_flt = IndicatorLight{parent=main,label="Induction Matrix Fault",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
local matrix_fill = IndicatorLight{parent=main,label="Matrix Charge High",colors=ind_red,flash=true,period=period.BLINK_500_MS} local 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 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 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} 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) auto_scram.register(facility.ps, "auto_scram", auto_scram.update)
matrix_dc.register(facility.ps, "as_matrix_dc", matrix_dc.update) matrix_flt.register(facility.ps, "as_matrix_fault", matrix_flt.update)
matrix_fill.register(facility.ps, "as_matrix_fill", matrix_fill.update) matrix_fill.register(facility.ps, "as_matrix_fill", matrix_fill.update)
unit_crit.register(facility.ps, "as_crit_alarm", unit_crit.update) unit_crit.register(facility.ps, "as_crit_alarm", unit_crit.update)
fac_rad_h.register(facility.ps, "as_radiation", fac_rad_h.update) fac_rad_h.register(facility.ps, "as_radiation", fac_rad_h.update)
gen_fault.register(facility.ps, "as_gen_fault", gen_fault.update) gen_fault.register(facility.ps, "as_gen_fault", gen_fault.update)
TextBox{parent=main,y=23,text="Radiation",height=1,width=13,fg_bg=style.label} TextBox{parent=main,y=23,text="Radiation",width=13,fg_bg=style.label}
local radiation = RadIndicator{parent=main,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=s_field} local radiation = RadIndicator{parent=main,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=s_field}
radiation.register(facility.ps, "radiation", radiation.update) radiation.register(facility.ps, "radiation", radiation.update)
TextBox{parent=main,x=15,y=23,text="Linked RTUs",height=1,width=11,fg_bg=style.label} TextBox{parent=main,x=15,y=23,text="Linked RTUs",width=11,fg_bg=style.label}
local rtu_count = DataIndicator{parent=main,x=15,y=24,label="",format="%11d",value=0,lu_colors=lu_cpair,width=11,fg_bg=s_field} local rtu_count = DataIndicator{parent=main,x=15,y=24,label="",format="%11d",value=0,lu_colors=lu_cpair,width=11,fg_bg=s_field}
rtu_count.register(facility.ps, "rtu_count", rtu_count.update) rtu_count.register(facility.ps, "rtu_count", rtu_count.update)
@@ -129,7 +131,7 @@ local function new_view(root, x, y)
TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2} TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2}
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=s_hi_box} local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=s_hi_box}
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled} local b_target = NumericSpinbox{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=burn_target,x=18,y=2,text="mB/t",fg_bg=style.theme.label_fg} TextBox{parent=burn_target,x=18,y=2,text="mB/t",fg_bg=style.theme.label_fg}
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn} local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
@@ -140,23 +142,23 @@ local function new_view(root, x, y)
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2} TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=s_hi_box} local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=s_hi_box}
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled} local c_target = NumericSpinbox{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=chg_target,x=18,y=2,text="MFE",fg_bg=style.theme.label_fg} TextBox{parent=chg_target,x=18,y=2,text="M"..db.energy_label,fg_bg=style.theme.label_fg}
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=black,width=23,fg_bg=blk_brn} local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="M"..db.energy_label,commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
c_target.register(facility.ps, "process_charge_target", c_target.set_value) c_target.register(facility.ps, "process_charge_target", c_target.set_value)
cur_charge.register(facility.induction_ps_tbl[1], "avg_charge", function (fe) cur_charge.update(fe / 1000000) end) cur_charge.register(facility.induction_ps_tbl[1], "avg_charge", function (fe) cur_charge.update(db.energy_convert_from_fe(fe) / 1000000) end)
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur} 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} TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=s_hi_box} local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=s_hi_box}
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled} local g_target = NumericSpinbox{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=gen_target,x=18,y=2,text="kFE/t",fg_bg=style.theme.label_fg} TextBox{parent=gen_target,x=18,y=2,text="k"..db.energy_label.."/t",fg_bg=style.theme.label_fg}
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn} local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="k"..db.energy_label.."/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
g_target.register(facility.ps, "process_gen_target", g_target.set_value) g_target.register(facility.ps, "process_gen_target", g_target.set_value)
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end) cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(db.energy_convert(j) / 1000)) end)
----------------- -----------------
-- unit limits -- -- unit limits --
@@ -175,7 +177,7 @@ local function new_view(root, x, y)
local cur_lu = style.theme.disabled local cur_lu = style.theme.disabled
if i <= facility.num_units then if i <= facility.num_units then
unit = units[i] ---@type ioctl_unit unit = units[i]
tag_fg_bg = cpair(colors.black, colors.lightBlue) tag_fg_bg = cpair(colors.black, colors.lightBlue)
lim_fg_bg = s_hi_box lim_fg_bg = s_hi_box
label_fg = style.theme.label_fg label_fg = style.theme.label_fg
@@ -189,8 +191,8 @@ local function new_view(root, x, y)
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2} TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2}
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=s_hi_box} local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=s_hi_box}
local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled,fg_bg=lim_fg_bg} local lim = NumericSpinbox{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled,fg_bg=lim_fg_bg}
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1,fg_bg=label_fg} TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,fg_bg=label_fg}
local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(cur_lu,cur_lu),width=14,fg_bg=cur_fg_bg} local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(cur_lu,cur_lu),width=14,fg_bg=cur_fg_bg}
@@ -232,7 +234,7 @@ local function new_view(root, x, y)
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(ind_red.fgd,ind_off),flash=true,period=period.BLINK_250_MS} local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(ind_red.fgd,ind_off),flash=true,period=period.BLINK_250_MS}
if i <= facility.num_units then if i <= facility.num_units then
local unit = units[i] ---@type ioctl_unit local unit = units[i]
ready.register(unit.unit_ps, "U_AutoReady", ready.update) ready.register(unit.unit_ps, "U_AutoReady", ready.update)
degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update) degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update)
@@ -249,8 +251,8 @@ local function new_view(root, x, y)
mode.register(facility.ps, "process_mode", mode.set_value) 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 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_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.white)} local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.white)}
stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value) stat_line_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) stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value)
@@ -262,21 +264,22 @@ local function new_view(root, x, y)
local limits = {} local limits = {}
for i = 1, #rate_limits do limits[i] = rate_limits[i].get_value() end for i = 1, #rate_limits do limits[i] = rate_limits[i].get_value() end
process.save(mode.get_value(), b_target.get_value(), c_target.get_value(), g_target.get_value(), limits) process.save(mode.get_value(), b_target.get_value(), db.energy_convert_to_fe(c_target.get_value()),
db.energy_convert_to_fe(g_target.get_value()), limits)
end end
-- start automatic control after saving process control settings -- start automatic control after saving process control settings
local function _start_auto() local function _start_auto()
_save_cfg() _save_cfg()
process.start_auto() db.process.process_start()
end 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 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 start = HazardButton{parent=auto_controls,x=13,y=2,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=_start_auto,fg_bg=hzd_fg_bg}
local stop = HazardButton{parent=auto_controls,x=23,y=2,text="STOP",accent=colors.red,dis_colors=dis_colors,callback=process.stop_auto,fg_bg=hzd_fg_bg} local stop = HazardButton{parent=auto_controls,x=23,y=2,text="STOP",accent=colors.red,dis_colors=dis_colors,callback=db.process.process_stop,fg_bg=hzd_fg_bg}
facility.start_ack = start.on_response db.process.fac_ack.on_start = start.on_response
facility.stop_ack = stop.on_response db.process.fac_ack.on_stop = stop.on_response
function facility.save_cfg_ack(ack) function facility.save_cfg_ack(ack)
tcd.dispatch(0.2, function () save.on_response(ack) end) tcd.dispatch(0.2, function () save.on_response(ack) end)
@@ -318,11 +321,11 @@ local function new_view(root, x, y)
local waste_status = Div{parent=proc,width=24,height=4,x=57,y=1,} local waste_status = Div{parent=proc,width=24,height=4,x=57,y=1,}
for i = 1, facility.num_units do for i = 1, facility.num_units do
local unit = units[i] ---@type ioctl_unit local unit = units[i]
TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8,height=1} TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8}
local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=ind_wht} local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=ind_wht}
local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.waste.states_abbrv,value=1,min_width=6} local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.get_waste().states_abbrv,value=1,min_width=6}
a_waste.register(unit.unit_ps, "U_AutoWaste", a_waste.update) a_waste.register(unit.unit_ps, "U_AutoWaste", a_waste.update)
waste_m.register(unit.unit_ps, "U_WasteProduct", waste_m.update) waste_m.register(unit.unit_ps, "U_WasteProduct", waste_m.update)
@@ -332,15 +335,15 @@ local function new_view(root, x, y)
local cutout_fg_bg = cpair(style.theme.bg, colors.brown) local cutout_fg_bg = cpair(style.theme.bg, colors.brown)
TextBox{parent=waste_sel,text=" ",width=21,height=1,x=1,y=1,fg_bg=cutout_fg_bg} TextBox{parent=waste_sel,text=" ",width=21,x=1,y=1,fg_bg=cutout_fg_bg}
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,height=1,x=1,y=2,fg_bg=cutout_fg_bg} TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,x=1,y=2,fg_bg=cutout_fg_bg}
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3} local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17} local status = StateIndicator{parent=rect,x=2,y=1,states=style.get_waste().states,value=1,min_width=17}
status.register(facility.ps, "current_waste_product", status.update) status.register(facility.ps, "current_waste_product", status.update)
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.brown} local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.get_waste().options,callback=process.set_process_waste,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.brown}
waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value) waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value)

View File

@@ -6,18 +6,18 @@ local style = require("coordinator.ui.style")
local core = require("graphics.core") local core = require("graphics.core")
local Rectangle = require("graphics.elements.rectangle") local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.TextBox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local HorizontalBar = require("graphics.elements.indicators.hbar") local HorizontalBar = require("graphics.elements.indicators.HorizontalBar")
local StateIndicator = require("graphics.elements.indicators.state") local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
-- create new reactor view -- create new reactor view
---@param root graphics_element parent ---@param root Container parent
---@param x integer top left x ---@param x integer top left x
---@param y integer top left y ---@param y integer top left y
---@param ps psil ps interface ---@param ps psil ps interface
@@ -41,10 +41,10 @@ local function new_view(root, x, y, ps)
local reactor_fills = Rectangle{parent=root,border=border(1, colors.gray, true),width=24,height=7,x=(x + 29),y=y} local reactor_fills = Rectangle{parent=root,border=border(1, colors.gray, true),width=24,height=7,x=(x + 29),y=y}
TextBox{parent=reactor_fills,text="FUEL",x=2,y=1,height=1,fg_bg=text_fg} TextBox{parent=reactor_fills,text="FUEL",x=2,y=1,fg_bg=text_fg}
TextBox{parent=reactor_fills,text="COOL",x=2,y=2,height=1,fg_bg=text_fg} TextBox{parent=reactor_fills,text="COOL",x=2,y=2,fg_bg=text_fg}
TextBox{parent=reactor_fills,text="HCOOL",x=2,y=4,height=1,fg_bg=text_fg} TextBox{parent=reactor_fills,text="HCOOL",x=2,y=4,fg_bg=text_fg}
TextBox{parent=reactor_fills,text="WASTE",x=2,y=5,height=1,fg_bg=text_fg} TextBox{parent=reactor_fills,text="WASTE",x=2,y=5,fg_bg=text_fg}
local fuel = HorizontalBar{parent=reactor_fills,x=8,y=1,show_percent=true,bar_fg_bg=cpair(style.theme.fuel_color,colors.gray),height=1,width=14} local fuel = HorizontalBar{parent=reactor_fills,x=8,y=1,show_percent=true,bar_fg_bg=cpair(style.theme.fuel_color,colors.gray),height=1,width=14}
local ccool = HorizontalBar{parent=reactor_fills,x=8,y=2,show_percent=true,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=14} local ccool = HorizontalBar{parent=reactor_fills,x=8,y=2,show_percent=true,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=14}

View File

@@ -1,22 +1,22 @@
local util = require("scada-common.util") local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
local core = require("graphics.core") local core = require("graphics.core")
local Rectangle = require("graphics.elements.rectangle") local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.TextBox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local PowerIndicator = require("graphics.elements.indicators.power") local PowerIndicator = require("graphics.elements.indicators.PowerIndicator")
local StateIndicator = require("graphics.elements.indicators.state") local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local VerticalBar = require("graphics.elements.indicators.vbar") local VerticalBar = require("graphics.elements.indicators.VerticalBar")
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
-- new turbine view -- new turbine view
---@param root graphics_element parent ---@param root Container parent
---@param x integer top left x ---@param x integer top left x
---@param y integer top left y ---@param y integer top left y
---@param ps psil ps interface ---@param ps psil ps interface
@@ -24,21 +24,23 @@ local function new_view(root, x, y, ps)
local text_fg = style.theme.text_fg local text_fg = style.theme.text_fg
local lu_col = style.lu_colors local lu_col = style.lu_colors
local db = iocontrol.get_db()
local turbine = Rectangle{parent=root,border=border(1,colors.gray,true),width=23,height=7,x=x,y=y} local 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 status = StateIndicator{parent=turbine,x=7,y=1,states=style.turbine.states,value=1,min_width=12}
local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg} local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",unit=db.energy_label,format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg}
local flow_rate = DataIndicator{parent=turbine,x=5,y=4,lu_colors=lu_col,label="",unit="mB/t",format="%10.0f",value=0,commas=true,width=16,fg_bg=text_fg} local flow_rate = DataIndicator{parent=turbine,x=5,y=4,lu_colors=lu_col,label="",unit="mB/t",format="%10.0f",value=0,commas=true,width=16,fg_bg=text_fg}
status.register(ps, "computed_status", status.update) status.register(ps, "computed_status", status.update)
prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end) prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(db.energy_convert(val)) end)
flow_rate.register(ps, "steam_input_rate", flow_rate.update) 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 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} local energy = VerticalBar{parent=turbine,x=3,y=1,fg_bg=cpair(colors.green,colors.gray),height=4,width=1}
TextBox{parent=turbine,text="S",x=2,y=5,height=1,width=1,fg_bg=text_fg} TextBox{parent=turbine,text="S",x=2,y=5,width=1,fg_bg=text_fg}
TextBox{parent=turbine,text="E",x=3,y=5,height=1,width=1,fg_bg=text_fg} TextBox{parent=turbine,text="E",x=3,y=5,width=1,fg_bg=text_fg}
steam.register(ps, "steam_fill", steam.update) steam.register(ps, "steam_fill", steam.update)
energy.register(ps, "energy_fill", energy.update) energy.register(ps, "energy_fill", energy.update)

View File

@@ -11,23 +11,25 @@ local style = require("coordinator.ui.style")
local core = require("graphics.core") local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.Div")
local Rectangle = require("graphics.elements.rectangle") local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.TextBox")
local AlarmLight = require("graphics.elements.indicators.alight") local AlarmLight = require("graphics.elements.indicators.AlarmLight")
local CoreMap = require("graphics.elements.indicators.coremap") local CoreMap = require("graphics.elements.indicators.CoreMap")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IndicatorLight = require("graphics.elements.indicators.light") local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local RadIndicator = require("graphics.elements.indicators.rad") local RadIndicator = require("graphics.elements.indicators.RadIndicator")
local TriIndicatorLight = require("graphics.elements.indicators.trilight") local TriIndicatorLight = require("graphics.elements.indicators.TriIndicatorLight")
local VerticalBar = require("graphics.elements.indicators.vbar") local VerticalBar = require("graphics.elements.indicators.VerticalBar")
local HazardButton = require("graphics.elements.controls.hazard_button") local HazardButton = require("graphics.elements.controls.HazardButton")
local MultiButton = require("graphics.elements.controls.multi_button") local MultiButton = require("graphics.elements.controls.MultiButton")
local PushButton = require("graphics.elements.controls.push_button") local NumericSpinbox = require("graphics.elements.controls.NumericSpinbox")
local RadioButton = require("graphics.elements.controls.radio_button") local PushButton = require("graphics.elements.controls.PushButton")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric") local RadioButton = require("graphics.elements.controls.RadioButton")
local AUTO_GROUP = types.AUTO_GROUP
local ALIGN = core.ALIGN local ALIGN = core.ALIGN
@@ -40,7 +42,7 @@ local gry_wht = style.gray_white
local period = core.flasher.PERIOD local period = core.flasher.PERIOD
-- create a unit view -- create a unit view
---@param parent graphics_element parent ---@param parent Container parent
---@param id integer ---@param id integer
local function init(parent, id) local function init(parent, id)
local s_hi_box = style.theme.highlight_box local s_hi_box = style.theme.highlight_box
@@ -60,7 +62,7 @@ local function init(parent, id)
local ind_wht = style.ind_wht local ind_wht = style.ind_wht
local db = iocontrol.get_db() local db = iocontrol.get_db()
local unit = db.units[id] ---@type ioctl_unit local unit = db.units[id]
local f_ps = db.facility.ps local f_ps = db.facility.ps
local main = Div{parent=parent,x=1,y=1} local main = Div{parent=parent,x=1,y=1}
@@ -71,7 +73,7 @@ local function init(parent, id)
local b_ps = unit.boiler_ps_tbl local b_ps = unit.boiler_ps_tbl
local t_ps = unit.turbine_ps_tbl local t_ps = unit.turbine_ps_tbl
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,height=1,fg_bg=style.theme.header} TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,fg_bg=style.theme.header}
----------------------------- -----------------------------
-- main stats and core map -- -- main stats and core map --
@@ -81,20 +83,20 @@ local function init(parent, id)
core_map.register(u_ps, "temp", core_map.update) core_map.register(u_ps, "temp", core_map.update)
core_map.register(u_ps, "size", function (s) core_map.resize(s[1], s[2]) end) core_map.register(u_ps, "size", function (s) core_map.resize(s[1], s[2]) end)
TextBox{parent=main,x=12,y=22,text="Heating Rate",height=1,width=12,fg_bg=style.label} TextBox{parent=main,x=12,y=22,text="Heating Rate",width=12,fg_bg=style.label}
local heating_r = DataIndicator{parent=main,x=12,label="",format="%14.0f",value=0,unit="mB/t",commas=true,lu_colors=lu_cpair,width=19,fg_bg=s_field} local heating_r = DataIndicator{parent=main,x=12,label="",format="%14.0f",value=0,unit="mB/t",commas=true,lu_colors=lu_cpair,width=19,fg_bg=s_field}
heating_r.register(u_ps, "heating_rate", heating_r.update) heating_r.register(u_ps, "heating_rate", heating_r.update)
TextBox{parent=main,x=12,y=25,text="Commanded Burn Rate",height=1,width=19,fg_bg=style.label} TextBox{parent=main,x=12,y=25,text="Commanded Burn Rate",width=19,fg_bg=style.label}
local burn_r = DataIndicator{parent=main,x=12,label="",format="%14.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=19,fg_bg=s_field} local burn_r = DataIndicator{parent=main,x=12,label="",format="%14.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=19,fg_bg=s_field}
burn_r.register(u_ps, "burn_rate", burn_r.update) 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="F",x=2,y=22,width=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="C",x=4,y=22,width=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=24,width=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="\x1a",x=6,y=25,width=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="H",x=8,y=22,width=1,fg_bg=style.label}
TextBox{parent=main,text="W",x=10,y=22,width=1,height=1,fg_bg=style.label} TextBox{parent=main,text="W",x=10,y=22,width=1,fg_bg=style.label}
local fuel = VerticalBar{parent=main,x=2,y=23,fg_bg=cpair(style.theme.fuel_color,colors.gray),height=4,width=1} local fuel = VerticalBar{parent=main,x=2,y=23,fg_bg=cpair(style.theme.fuel_color,colors.gray),height=4,width=1}
local ccool = VerticalBar{parent=main,x=4,y=23,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1} local ccool = VerticalBar{parent=main,x=4,y=23,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1}
@@ -122,20 +124,20 @@ local function init(parent, id)
end end
end) end)
TextBox{parent=main,x=32,y=22,text="Core Temp",height=1,width=9,fg_bg=style.label} TextBox{parent=main,x=32,y=22,text="Core Temp",width=9,fg_bg=style.label}
local fmt = util.trinary(string.len(db.temp_label) == 2, "%10.2f", "%11.2f") local fmt = util.trinary(string.len(db.temp_label) == 2, "%10.2f", "%11.2f")
local core_temp = DataIndicator{parent=main,x=32,label="",format=fmt,value=0,commas=true,unit=db.temp_label,lu_colors=lu_cpair,width=13,fg_bg=s_field} local core_temp = DataIndicator{parent=main,x=32,label="",format=fmt,value=0,commas=true,unit=db.temp_label,lu_colors=lu_cpair,width=13,fg_bg=s_field}
core_temp.register(u_ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end) core_temp.register(u_ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end)
TextBox{parent=main,x=32,y=25,text="Burn Rate",height=1,width=9,fg_bg=style.label} TextBox{parent=main,x=32,y=25,text="Burn Rate",width=9,fg_bg=style.label}
local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=s_field} local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=s_field}
act_burn_r.register(u_ps, "act_burn_rate", act_burn_r.update) act_burn_r.register(u_ps, "act_burn_rate", act_burn_r.update)
TextBox{parent=main,x=32,y=28,text="Damage",height=1,width=6,fg_bg=style.label} TextBox{parent=main,x=32,y=28,text="Damage",width=6,fg_bg=style.label}
local damage_p = DataIndicator{parent=main,x=32,label="",format="%11.0f",value=0,unit="%",lu_colors=lu_cpair,width=13,fg_bg=s_field} local damage_p = DataIndicator{parent=main,x=32,label="",format="%11.0f",value=0,unit="%",lu_colors=lu_cpair,width=13,fg_bg=s_field}
damage_p.register(u_ps, "damage", damage_p.update) damage_p.register(u_ps, "damage", damage_p.update)
TextBox{parent=main,x=32,y=31,text="Radiation",height=1,width=21,fg_bg=style.label} TextBox{parent=main,x=32,y=31,text="Radiation",width=21,fg_bg=style.label}
local radiation = RadIndicator{parent=main,x=32,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=s_field} local radiation = RadIndicator{parent=main,x=32,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=s_field}
radiation.register(u_ps, "radiation", radiation.update) radiation.register(u_ps, "radiation", radiation.update)
@@ -144,8 +146,8 @@ local function init(parent, id)
------------------- -------------------
local u_stat = Rectangle{parent=main,border=border(1,colors.gray,true),thin=true,width=33,height=4,x=46,y=3,fg_bg=bw_fg_bg} local 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_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=ALIGN.CENTER,fg_bg=gry_wht} local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,alignment=ALIGN.CENTER,fg_bg=gry_wht}
stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value) 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) stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value)
@@ -205,7 +207,7 @@ local function init(parent, id)
-- RPS annunciator panel -- RPS annunciator panel
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=8} TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,x=46,y=8}
local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9} local rps = 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_annunc = Div{parent=rps,width=31,height=10,x=2,y=1}
@@ -233,7 +235,7 @@ local function init(parent, id)
-- cooling annunciator panel -- cooling annunciator panel
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=22} TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,x=46,y=22}
local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23} local rcs = 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_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 rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7}
@@ -265,11 +267,11 @@ local function init(parent, id)
if unit.num_boilers > 0 then if unit.num_boilers > 0 then
if available_space > 0 then _add_space() end if available_space > 0 then _add_space() end
TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,x=1,text="B1",width=2,fg_bg=hc_text}
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red} local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
b1_wll.register(b_ps[1], "WaterLevelLow", b1_wll.update) b1_wll.register(b_ps[1], "WaterLevelLow", b1_wll.update)
TextBox{parent=rcs_tags,text="B1",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="B1",width=2,fg_bg=hc_text}
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel} local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
b1_hr.register(b_ps[1], "HeatingRateLow", b1_hr.update) b1_hr.register(b_ps[1], "HeatingRateLow", b1_hr.update)
end end
@@ -281,11 +283,11 @@ local function init(parent, id)
_add_space() _add_space()
end end
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="B2",width=2,fg_bg=hc_text}
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red} local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
b2_wll.register(b_ps[2], "WaterLevelLow", b2_wll.update) b2_wll.register(b_ps[2], "WaterLevelLow", b2_wll.update)
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="B2",width=2,fg_bg=hc_text}
local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel} local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
b2_hr.register(b_ps[2], "HeatingRateLow", b2_hr.update) b2_hr.register(b_ps[2], "HeatingRateLow", b2_hr.update)
end end
@@ -294,19 +296,19 @@ local function init(parent, id)
if available_space > 1 then _add_space() end if available_space > 1 then _add_space() end
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd} local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
t1_sdo.register(t_ps[1], "SteamDumpOpen", t1_sdo.update) t1_sdo.register(t_ps[1], "SteamDumpOpen", t1_sdo.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red} local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
t1_tos.register(t_ps[1], "TurbineOverSpeed", t1_tos.update) t1_tos.register(t_ps[1], "TurbineOverSpeed", t1_tos.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS} 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) t1_gtrp.register(t_ps[1], "GeneratorTrip", t1_gtrp.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS} 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) t1_trp.register(t_ps[1], "TurbineTrip", t1_trp.update)
@@ -315,19 +317,19 @@ local function init(parent, id)
_add_space() _add_space()
end end
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd} local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
t2_sdo.register(t_ps[2], "SteamDumpOpen", t2_sdo.update) t2_sdo.register(t_ps[2], "SteamDumpOpen", t2_sdo.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red} local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
t2_tos.register(t_ps[2], "TurbineOverSpeed", t2_tos.update) t2_tos.register(t_ps[2], "TurbineOverSpeed", t2_tos.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS} 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) t2_gtrp.register(t_ps[2], "GeneratorTrip", t2_gtrp.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS} 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) t2_trp.register(t_ps[2], "TurbineTrip", t2_trp.update)
end end
@@ -335,19 +337,19 @@ local function init(parent, id)
if unit.num_turbines > 2 then if unit.num_turbines > 2 then
if available_space > 3 then _add_space() end if available_space > 3 then _add_space() end
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd} local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
t3_sdo.register(t_ps[3], "SteamDumpOpen", t3_sdo.update) t3_sdo.register(t_ps[3], "SteamDumpOpen", t3_sdo.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red} local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
t3_tos.register(t_ps[3], "TurbineOverSpeed", t3_tos.update) t3_tos.register(t_ps[3], "TurbineOverSpeed", t3_tos.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS} 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) t3_gtrp.register(t_ps[3], "GeneratorTrip", t3_gtrp.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS} 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) t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update)
end end
@@ -359,7 +361,7 @@ local function init(parent, id)
---------------------- ----------------------
local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=s_hi_box} local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=s_hi_box}
local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled} local burn_rate = NumericSpinbox{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=burn_control,x=9,y=2,text="mB/t",fg_bg=style.theme.label_fg} TextBox{parent=burn_control,x=9,y=2,text="mB/t",fg_bg=style.theme.label_fg}
local set_burn = function () unit.set_burn(burn_rate.get_value()) end local set_burn = function () unit.set_burn(burn_rate.get_value()) end
@@ -373,18 +375,16 @@ local function init(parent, id)
local scram = HazardButton{parent=main,x=2,y=32,text="SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=unit.scram,fg_bg=hzd_fg_bg} local 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} 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 db.process.unit_ack[id].on_start = start.on_response
unit.scram_ack = scram.on_response db.process.unit_ack[id].on_scram = scram.on_response
unit.reset_rps_ack = reset.on_response db.process.unit_ack[id].on_rps_reset = reset.on_response
unit.ack_alarms_ack = ack_a.on_response db.process.unit_ack[id].on_ack_alarms = ack_a.on_response
local function start_button_en_check() 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
local can_start = (not unit.reactor_data.mek_status.status) and (not unit.reactor_data.rps_tripped) and
(not unit.reactor_data.rps_tripped) and (unit.a_group == AUTO_GROUP.MANUAL)
(unit.a_group == 0) if can_start then start.enable() else start.disable() end
if can_start then start.enable() else start.disable() end
end
end end
start.register(u_ps, "status", start_button_en_check) start.register(u_ps, "status", start_button_en_check)
@@ -394,11 +394,11 @@ local function init(parent, id)
reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end) reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=48} TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=ALIGN.CENTER,width=33,x=46,y=48}
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49} local waste_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_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1}
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.waste.unit_opts,callback=unit.set_waste,min_width=6} local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.get_waste().unit_opts,callback=unit.set_waste,min_width=6}
waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value) waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)
@@ -474,21 +474,19 @@ local function init(parent, id)
-- color tags -- color tags
TextBox{parent=alarm_panel,x=5,y=13,text="\x95",width=1,height=1,fg_bg=cpair(s_hi_bright.bkg,colors.cyan)} TextBox{parent=alarm_panel,x=5,y=13,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.cyan)}
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,height=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)} TextBox{parent=alarm_panel,x=5,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)}
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,height=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)} TextBox{parent=alarm_panel,x=5,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)}
-------------------------------- --------------------------------
-- automatic control settings -- -- automatic control settings --
-------------------------------- --------------------------------
TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,height=1,x=32,y=36} TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,x=32,y=36}
local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37} local auto_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 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=types.AUTO_GROUP_NAMES,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end) group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end)
@@ -499,8 +497,8 @@ local function init(parent, id)
auto_div.line_break() auto_div.line_break()
TextBox{parent=auto_div,text="Prio. Group",height=1,width=11,fg_bg=style.label} TextBox{parent=auto_div,text="Prio. Group",width=11,fg_bg=style.label}
local auto_grp = TextBox{parent=auto_div,text="Manual",height=1,width=11,fg_bg=s_field} local auto_grp = TextBox{parent=auto_div,text="Manual",width=11,fg_bg=s_field}
auto_grp.register(u_ps, "auto_group", auto_grp.set_value) auto_grp.register(u_ps, "auto_group", auto_grp.set_value)
@@ -523,10 +521,10 @@ local function init(parent, id)
-- enable/disable controls based on group assignment (start button is separate) -- enable/disable controls based on group assignment (start button is separate)
burn_rate.register(u_ps, "auto_group_id", function (gid) burn_rate.register(u_ps, "auto_group_id", function (gid)
if gid == 0 then burn_rate.enable() else burn_rate.disable() end if gid == AUTO_GROUP.MANUAL then burn_rate.enable() else burn_rate.disable() end
end) end)
set_burn_btn.register(u_ps, "auto_group_id", function (gid) set_burn_btn.register(u_ps, "auto_group_id", function (gid)
if gid == 0 then set_burn_btn.enable() else set_burn_btn.disable() end if gid == AUTO_GROUP.MANUAL then set_burn_btn.enable() else set_burn_btn.disable() end
end) end)
-- can't change group if auto is engaged regardless of if this unit is part of auto control -- can't change group if auto is engaged regardless of if this unit is part of auto control

View File

@@ -2,22 +2,27 @@
-- Basic Unit Flow Overview -- Basic Unit Flow Overview
-- --
local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
local core = require("graphics.core") local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.Div")
local PipeNetwork = require("graphics.elements.pipenet") local PipeNetwork = require("graphics.elements.PipeNetwork")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.TextBox")
local Rectangle = require("graphics.elements.rectangle") local Rectangle = require("graphics.elements.Rectangle")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IndicatorLight = require("graphics.elements.indicators.light") local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local TriIndicatorLight = require("graphics.elements.indicators.trilight") local TriIndicatorLight = require("graphics.elements.indicators.TriIndicatorLight")
local COOLANT_TYPE = types.COOLANT_TYPE
local ALIGN = core.ALIGN local ALIGN = core.ALIGN
@@ -31,12 +36,12 @@ local wh_gray = style.wh_gray
local lg_gray = style.lg_gray local lg_gray = style.lg_gray
-- make a new unit flow window -- make a new unit flow window
---@param parent graphics_element parent ---@param parent Container parent
---@param x integer top left x ---@param x integer top left x
---@param y integer top left y ---@param y integer top left y
---@param wide boolean whether to render wide version ---@param wide boolean whether to render wide version
---@param unit ioctl_unit unit database entry ---@param unit_id integer unit index
local function make(parent, x, y, wide, unit) local function make(parent, x, y, wide, unit_id)
local s_field = style.theme.field_box local s_field = style.theme.field_box
local text_c = style.text_colors local text_c = style.text_colors
@@ -48,7 +53,13 @@ local function make(parent, x, y, wide, unit)
local height = 16 local height = 16
local v_start = 1 + ((unit.unit_id - 1) * 5) local facility = iocontrol.get_db().facility
local unit = iocontrol.get_db().units[unit_id]
local tank_conns = facility.tank_conns
local tank_types = facility.tank_fluid_types
local v_start = 1 + ((unit.unit_id - 1) * 6)
local prv_start = 1 + ((unit.unit_id - 1) * 3) local prv_start = 1 + ((unit.unit_id - 1) * 3)
local v_fields = { "pu", "po", "pl", "am" } local v_fields = { "pu", "po", "pl", "am" }
local v_names = { local v_names = {
@@ -73,28 +84,39 @@ local function make(parent, x, y, wide, unit)
------------------ ------------------
local reactor = Rectangle{parent=root,x=1,y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray} 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=1,text="FISSION REACTOR",alignment=ALIGN.CENTER}
TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=ALIGN.CENTER,height=1} TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=ALIGN.CENTER}
TextBox{parent=root,x=19,y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray} TextBox{parent=root,x=19,y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
TextBox{parent=root,x=3,y=5,text="\x19",width=1,height=1,fg_bg=lg_gray} TextBox{parent=root,x=3,y=5,text="\x19",width=1,fg_bg=lg_gray}
local rc_pipes = {} local rc_pipes = {}
local emc_x = 42 -- emergency coolant connection x point
if unit.num_boilers > 0 then 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, 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(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), 1, _wide(72, 58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(_wide(46,39), 3, _wide(72,58), 3, colors.white, true)) table.insert(rc_pipes, pipe(_wide(46, 39), 3, _wide(72, 58), 3, colors.white, true))
if unit.aux_coolant then
local em_water = facility.tank_fluid_types[facility.tank_conns[unit_id]] == COOLANT_TYPE.WATER
local offset = util.trinary(unit.has_tank and em_water, 3, 0)
table.insert(rc_pipes, pipe(_wide(51, 41) + offset, 0, _wide(51, 41) + offset, 0, colors.blue, true))
end
else else
emc_x = 3 table.insert(rc_pipes, pipe(0, 1, _wide(72, 58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(0, 1, _wide(72,58), 1, colors.blue, true)) table.insert(rc_pipes, pipe(0, 3, _wide(72, 58), 3, colors.white, true))
table.insert(rc_pipes, pipe(0, 3, _wide(72,58), 3, colors.white, true))
if unit.aux_coolant then
table.insert(rc_pipes, pipe(8, 0, 8, 0, colors.blue, true))
end
end end
if unit.has_tank then if unit.has_tank then
table.insert(rc_pipes, pipe(emc_x, 1, emc_x, 0, colors.blue, true, true)) local is_water = tank_types[tank_conns[unit_id]] == COOLANT_TYPE.WATER
-- emergency coolant connection x point
local emc_x = util.trinary(is_water and (unit.num_boilers > 0), 42, 3)
table.insert(rc_pipes, pipe(emc_x, 1, emc_x, 0, util.trinary(is_water, colors.blue, colors.lightBlue), true, true))
end end
local prv_yo = math.max(3 - unit.num_turbines, 0) local prv_yo = math.max(3 - unit.num_turbines, 0)
@@ -113,8 +135,8 @@ local function make(parent, x, y, wide, unit)
hc_rate.register(unit.unit_ps, "heating_rate", hc_rate.update) hc_rate.register(unit.unit_ps, "heating_rate", hc_rate.update)
local boiler = Rectangle{parent=root,x=_wide(47,40),y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray} local boiler = Rectangle{parent=root,x=_wide(47,40),y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=boiler,y=1,text="THERMO-ELECTRIC",alignment=ALIGN.CENTER,height=1} TextBox{parent=boiler,y=1,text="THERMO-ELECTRIC",alignment=ALIGN.CENTER}
TextBox{parent=boiler,y=3,text=util.trinary(unit.num_boilers>1,"BOILERS","BOILER"),alignment=ALIGN.CENTER,height=1} TextBox{parent=boiler,y=3,text=util.trinary(unit.num_boilers>1,"BOILERS","BOILER"),alignment=ALIGN.CENTER}
TextBox{parent=root,x=_wide(47,40),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray} TextBox{parent=root,x=_wide(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} TextBox{parent=root,x=_wide(65,58),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
@@ -132,13 +154,13 @@ local function make(parent, x, y, wide, unit)
end end
local turbine = Rectangle{parent=root,x=_wide(93,79),y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray} local turbine = Rectangle{parent=root,x=_wide(93,79),y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=turbine,y=1,text="STEAM TURBINE",alignment=ALIGN.CENTER,height=1} TextBox{parent=turbine,y=1,text="STEAM TURBINE",alignment=ALIGN.CENTER}
TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=ALIGN.CENTER,height=1} TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=ALIGN.CENTER}
TextBox{parent=root,x=_wide(93,79),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray} 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 for i = 1, unit.num_turbines do
local ry = 1 + (2 * (i - 1)) + prv_yo local ry = 1 + (2 * (i - 1)) + prv_yo
TextBox{parent=root,x=_wide(125,103),y=ry,text="\x10\x11\x7f",fg_bg=text_c,width=3,height=1} TextBox{parent=root,x=_wide(125,103),y=ry,text="\x10\x11\x7f",fg_bg=text_c,width=3}
local state = TriIndicatorLight{parent=root,x=_wide(129,107),y=ry,label=v_names[i+4],c1=style.ind_bkg,c2=style.ind_yel.fgd,c3=style.ind_red.fgd} local state = TriIndicatorLight{parent=root,x=_wide(129,107),y=ry,label=v_names[i+4],c1=style.ind_bkg,c2=style.ind_yel.fgd,c3=style.ind_red.fgd}
state.register(unit.turbine_ps_tbl[i], "SteamDumpOpen", state.update) state.register(unit.turbine_ps_tbl[i], "SteamDumpOpen", state.update)
end end
@@ -157,12 +179,12 @@ local function make(parent, x, y, wide, unit)
pipe(_wide(22, 19), 1, _wide(49, 45), 1, colors.brown, true), pipe(_wide(22, 19), 1, _wide(49, 45), 1, colors.brown, true),
pipe(_wide(22, 19), 5, _wide(28, 24), 5, colors.brown, true), pipe(_wide(22, 19), 5, _wide(28, 24), 5, colors.brown, true),
pipe(_wide(64, 53), 1, _wide(95, 81), 1, colors.green, true), pipe(_wide(64, 53), 1, _wide(95, 81), 1, colors.cyan, true),
pipe(_wide(48, 43), 4, _wide(71, 61), 4, colors.cyan, true), pipe(_wide(48, 43), 4, _wide(71, 61), 4, colors.green, true),
pipe(_wide(66, 57), 4, _wide(71, 61), 8, colors.cyan, true), pipe(_wide(66, 57), 4, _wide(71, 61), 8, colors.green, true),
pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.cyan, true), pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.green, true),
pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.cyan, true), pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.green, true),
pipe(_wide(108, 94), 1, _wide(132, 110), 6, waste_c, true, true), pipe(_wide(108, 94), 1, _wide(132, 110), 6, waste_c, true, true),
pipe(_wide(108, 94), 4, _wide(111, 95), 1, waste_c, true, true), pipe(_wide(108, 94), 4, _wide(111, 95), 1, waste_c, true, true),
@@ -172,7 +194,7 @@ local function make(parent, x, y, wide, unit)
PipeNetwork{parent=waste,x=1,y=1,pipes=waste_pipes,bg=style.theme.bg} PipeNetwork{parent=waste,x=1,y=1,pipes=waste_pipes,bg=style.theme.bg}
local function _valve(vx, vy, n) local function _valve(vx, vy, n)
TextBox{parent=waste,x=vx,y=vy,text="\x10\x11",fg_bg=text_c,width=2,height=1} TextBox{parent=waste,x=vx,y=vy,text="\x10\x11",fg_bg=text_c,width=2}
local conn = IndicatorLight{parent=waste,x=vx-3,y=vy+1,label=v_names[n],colors=ind_grn} local 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} 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) conn.register(unit.unit_ps, util.c("V_", v_fields[n], "_conn"), conn.update)
@@ -181,8 +203,8 @@ local function make(parent, x, y, wide, unit)
local function _machine(mx, my, name) local function _machine(mx, my, name)
local l = string.len(name) + 2 local l = string.len(name) + 2
TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=ALIGN.CENTER,fg_bg=cpair(style.theme.bg,style.theme.header.bkg),width=l,height=1} TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=ALIGN.CENTER,fg_bg=cpair(style.theme.bg,style.theme.header.bkg),width=l}
TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=ALIGN.CENTER,fg_bg=style.theme.header,width=l,height=1} TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=ALIGN.CENTER,fg_bg=style.theme.header,width=l}
end end
local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field} local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
@@ -209,18 +231,22 @@ local function make(parent, x, y, wide, unit)
_machine(_wide(97, 83), 4, "PRC [Po] \x1a"); _machine(_wide(97, 83), 4, "PRC [Po] \x1a");
_machine(_wide(116, 94), 6, "SPENT WASTE \x1b") _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} TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=style.theme.highlight_box_bright} local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=8,thin=true,fg_bg=style.theme.highlight_box_bright}
local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn} local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn}
local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c_d,label="CNT",unit="",format="%2d",value=0,width=7} local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c_d,label="CNT",unit="",format="%2d",value=0,width=7}
local sna_pk = DataIndicator{parent=sna_po,y=3,lu_colors=lu_c_d,label="PEAK",unit="mB/t",format="%7.2f",value=0,width=17} TextBox{parent=sna_po,y=3,text="PEAK\x1a",width=5,fg_bg=cpair(style.theme.label_dark,colors._INHERIT)}
local sna_max = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="MAX",unit="mB/t",format="%8.2f",value=0,width=17} TextBox{parent=sna_po,text="MAX \x1a",width=5,fg_bg=cpair(style.theme.label_dark,colors._INHERIT)}
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="IN",unit="mB/t",format="%9.2f",value=0,width=17} local sna_pk = DataIndicator{parent=sna_po,x=6,y=3,lu_colors=lu_c_d,label="",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max_o = DataIndicator{parent=sna_po,x=6,lu_colors=lu_c_d,label="",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max_i = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="\x1aMAX",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="\x1aIN",unit="mB/t",format="%8.2f",value=0,width=17}
sna_act.register(unit.unit_ps, "po_rate", function (r) sna_act.update(r > 0) end) sna_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_cnt.register(unit.unit_ps, "sna_count", sna_cnt.update)
sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.update) sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.update)
sna_max.register(unit.unit_ps, "sna_max_rate", sna_max.update) sna_max_o.register(unit.unit_ps, "sna_max_rate", sna_max_o.update)
sna_max_i.register(unit.unit_ps, "sna_max_rate", function (r) sna_max_i.update(r * 10) end)
sna_in.register(unit.unit_ps, "sna_in", sna_in.update) sna_in.register(unit.unit_ps, "sna_in", sna_in.update)
return root return root

View File

@@ -10,16 +10,16 @@ local reactor_view = require("coordinator.ui.components.reactor")
local boiler_view = require("coordinator.ui.components.boiler") local boiler_view = require("coordinator.ui.components.boiler")
local turbine_view = require("coordinator.ui.components.turbine") local turbine_view = require("coordinator.ui.components.turbine")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.Div")
local PipeNetwork = require("graphics.elements.pipenet") local PipeNetwork = require("graphics.elements.PipeNetwork")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.TextBox")
local ALIGN = core.ALIGN local ALIGN = core.ALIGN
local pipe = core.pipe local pipe = core.pipe
-- make a new unit overview window -- make a new unit overview window
---@param parent graphics_element parent ---@param parent Container parent
---@param x integer top left x ---@param x integer top left x
---@param y integer top left y ---@param y integer top left y
---@param unit ioctl_unit unit database entry ---@param unit ioctl_unit unit database entry
@@ -44,7 +44,7 @@ local function make(parent, x, y, unit)
local root = Div{parent=parent,x=x,y=y,width=80,height=height} local root = Div{parent=parent,x=x,y=y,width=80,height=height}
-- unit header message -- unit header message
TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,height=1,fg_bg=style.theme.header} TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,fg_bg=style.theme.header}
------------- -------------
-- REACTOR -- -- REACTOR --

View File

@@ -13,17 +13,18 @@ local unit_flow = require("coordinator.ui.components.unit_flow")
local core = require("graphics.core") local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.Div")
local PipeNetwork = require("graphics.elements.pipenet") local PipeNetwork = require("graphics.elements.PipeNetwork")
local Rectangle = require("graphics.elements.rectangle") local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.TextBox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local HorizontalBar = require("graphics.elements.indicators.hbar") local HorizontalBar = require("graphics.elements.indicators.HorizontalBar")
local IndicatorLight = require("graphics.elements.indicators.light") local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local StateIndicator = require("graphics.elements.indicators.state") local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local CONTAINER_MODE = types.CONTAINER_MODE local CONTAINER_MODE = types.CONTAINER_MODE
local COOLANT_TYPE = types.COOLANT_TYPE
local ALIGN = core.ALIGN local ALIGN = core.ALIGN
@@ -34,7 +35,7 @@ local pipe = core.pipe
local wh_gray = style.wh_gray local wh_gray = style.wh_gray
-- create new flow view -- create new flow view
---@param main graphics_element main displaybox ---@param main DisplayBox main displaybox
local function init(main) local function init(main)
local s_hi_bright = style.theme.highlight_box_bright local s_hi_bright = style.theme.highlight_box_bright
local s_field = style.theme.field_box local s_field = style.theme.field_box
@@ -45,23 +46,29 @@ local function init(main)
local facility = iocontrol.get_db().facility local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units local units = iocontrol.get_db().units
local tank_defs = facility.tank_defs local tank_defs = facility.tank_defs
local tank_list = facility.tank_list local tank_conns = facility.tank_conns
local tank_list = facility.tank_list
local tank_types = facility.tank_fluid_types
-- window header message -- window header message
local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=ALIGN.CENTER,height=1,fg_bg=style.theme.header} local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
-- max length example: "01:23:45 AM - Wednesday, September 28 2022" -- max length example: "01:23:45 AM - Wednesday, September 28 2022"
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=style.theme.header} local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,fg_bg=style.theme.header}
datetime.register(facility.ps, "date_time", datetime.set_value) datetime.register(facility.ps, "date_time", datetime.set_value)
local po_pipes = {} local po_pipes = {}
local water_pipes = {} local emcool_pipes = {}
-- get the y offset for this unit index -- get the y offset for this unit index
---@param idx integer unit index ---@param idx integer unit index
local function y_ofs(idx) return ((idx - 1) * 20) end local function y_ofs(idx) return ((idx - 1) * 20) end
-- get the coolant color
---@param idx integer tank index
local function c_clr(idx) return util.trinary(tank_types[tank_conns[idx]] == COOLANT_TYPE.WATER, colors.blue, colors.lightBlue) end
-- determinte facility tank start/end from the definitions list -- determinte facility tank start/end from the definitions list
---@param start_idx integer start index of table iteration ---@param start_idx integer start index of table iteration
---@param end_idx integer end index of table iteration ---@param end_idx integer end index of table iteration
@@ -81,12 +88,13 @@ local function init(main)
for i = 1, facility.num_units do for i = 1, facility.num_units do
if units[i].has_tank then if units[i].has_tank then
local y = y_ofs(i) local y = y_ofs(i)
table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true)) local color = c_clr(i)
table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true))
local u = units[i] ---@type ioctl_unit table.insert(emcool_pipes, pipe(2, y, 2, y + 3, color, true))
local x = util.trinary(u.num_boilers == 0, 45, 84) table.insert(emcool_pipes, pipe(2, y, 21, y, color, true))
table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true))
local x = util.trinary((tank_types[tank_conns[i]] == COOLANT_TYPE.SODIUM) or (units[i].num_boilers == 0), 45, 84)
table.insert(emcool_pipes, pipe(21, y, x, y + 2, color, true, true))
end end
end end
else else
@@ -94,17 +102,17 @@ local function init(main)
for i = 1, #tank_defs do for i = 1, #tank_defs do
if tank_defs[i] > 0 then if tank_defs[i] > 0 then
local y = y_ofs(i) local y = y_ofs(i)
local color = c_clr(i)
if tank_defs[i] == 2 then if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(1, y, 21, y, colors.blue, true)) table.insert(emcool_pipes, pipe(1, y, 21, y, color, true))
else else
table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true)) table.insert(emcool_pipes, pipe(2, y, 2, y + 3, color, true))
table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true)) table.insert(emcool_pipes, pipe(2, y, 21, y, color, true))
end end
local u = units[i] ---@type ioctl_unit local x = util.trinary((tank_types[tank_conns[i]] == COOLANT_TYPE.SODIUM) or (units[i].num_boilers == 0), 45, 84)
local x = util.trinary(u.num_boilers == 0, 45, 84) table.insert(emcool_pipes, pipe(21, y, x, y + 2, color, true, true))
table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true))
end end
end end
@@ -114,13 +122,14 @@ local function init(main)
for i = 1, #tank_defs do for i = 1, #tank_defs do
local y = y_ofs(i) local y = y_ofs(i)
if i == first_fdef then if i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
elseif i > first_fdef then elseif i > first_fdef then
if i == last_fdef then if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y - 14, 0, y, c_clr(first_fdef), true))
elseif i < last_fdef then elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, c_clr(first_fdef), true))
end end
end end
end end
@@ -130,17 +139,19 @@ local function init(main)
for i = 1, #tank_defs do for i = 1, #tank_defs do
local y = y_ofs(i) local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 4 then if i == 4 then
if tank_defs[i] == 2 then if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
end end
elseif i == first_fdef then elseif i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
elseif i > first_fdef then elseif i > first_fdef then
if i == last_fdef then if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
elseif i < last_fdef then elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
end end
end end
end end
@@ -149,12 +160,12 @@ local function init(main)
for _, a in pairs({ 1, 3 }) do for _, a in pairs({ 1, 3 }) do
local b = a + 1 local b = a + 1
if tank_defs[a] == 2 then if tank_defs[a] == 2 then
table.insert(water_pipes, pipe(0, y_ofs(a), 1, y_ofs(a) + 6, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y_ofs(a), 1, y_ofs(a) + 6, c_clr(a), true))
if tank_defs[b] == 2 then if tank_defs[b] == 2 then
table.insert(water_pipes, pipe(0, y_ofs(b) - 13, 1, y_ofs(b), colors.blue, true)) table.insert(emcool_pipes, pipe(0, y_ofs(b) - 13, 1, y_ofs(b), c_clr(a), true))
end end
elseif tank_defs[b] == 2 then elseif tank_defs[b] == 2 then
table.insert(water_pipes, pipe(0, y_ofs(b), 1, y_ofs(b) + 6, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y_ofs(b), 1, y_ofs(b) + 6, c_clr(b), true))
end end
end end
elseif facility.tank_mode == 4 then elseif facility.tank_mode == 4 then
@@ -163,17 +174,19 @@ local function init(main)
for i = 1, #tank_defs do for i = 1, #tank_defs do
local y = y_ofs(i) local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 1 then if i == 1 then
if tank_defs[i] == 2 then if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
end end
elseif i == first_fdef then elseif i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
elseif i > first_fdef then elseif i > first_fdef then
if i == last_fdef then if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
elseif i < last_fdef then elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
end end
end end
end end
@@ -183,17 +196,19 @@ local function init(main)
for i = 1, #tank_defs do for i = 1, #tank_defs do
local y = y_ofs(i) local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 3 or i == 4 then if i == 3 or i == 4 then
if tank_defs[i] == 2 then if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
end end
elseif i == first_fdef then elseif i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
elseif i > first_fdef then elseif i > first_fdef then
if i == last_fdef then if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
elseif i < last_fdef then elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
end end
end end
end end
@@ -203,17 +218,19 @@ local function init(main)
for i = 1, #tank_defs do for i = 1, #tank_defs do
local y = y_ofs(i) local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 1 or i == 4 then if i == 1 or i == 4 then
if tank_defs[i] == 2 then if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
end end
elseif i == first_fdef then elseif i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
elseif i > first_fdef then elseif i > first_fdef then
if i == last_fdef then if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
elseif i < last_fdef then elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
end end
end end
end end
@@ -223,17 +240,19 @@ local function init(main)
for i = 1, #tank_defs do for i = 1, #tank_defs do
local y = y_ofs(i) local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 1 or i == 2 then if i == 1 or i == 2 then
if tank_defs[i] == 2 then if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
end end
elseif i == first_fdef then elseif i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
elseif i > first_fdef then elseif i > first_fdef then
if i == last_fdef then if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
elseif i < last_fdef then elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true)) table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
end end
end end
end end
@@ -241,15 +260,15 @@ local function init(main)
end end
local flow_x = 3 local flow_x = 3
if #water_pipes > 0 then if #emcool_pipes > 0 then
flow_x = 25 flow_x = 25
PipeNetwork{parent=main,x=2,y=3,pipes=water_pipes,bg=style.theme.bg} PipeNetwork{parent=main,x=2,y=3,pipes=emcool_pipes,bg=style.theme.bg}
end end
for i = 1, facility.num_units do for i = 1, facility.num_units do
local y_offset = y_ofs(i) local y_offset = y_ofs(i)
unit_flow(main, flow_x, 5 + y_offset, #water_pipes == 0, units[i]) unit_flow(main, flow_x, 5 + y_offset, #emcool_pipes == 0, i)
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.cyan, true, true)) table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.green, true, true))
util.nop() util.nop()
end end
@@ -265,9 +284,9 @@ local function init(main)
if tank_defs[i] > 0 then if tank_defs[i] > 0 then
local vy = 3 + y_ofs(i) local vy = 3 + y_ofs(i)
TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=text_col,width=2,height=1} TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=text_col,width=2}
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", i * 5),colors=style.ind_grn} local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", (i * 6) - 1),colors=style.ind_grn}
local open = IndicatorLight{parent=main,x=9,y=vy+2,label="OPEN",colors=style.ind_wht} 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) conn.register(units[i].unit_ps, "V_emc_conn", conn.update)
@@ -275,6 +294,35 @@ local function init(main)
end end
end end
------------------------------
-- auxiliary coolant valves --
------------------------------
for i = 1, facility.num_units do
if units[i].aux_coolant then
local vx
local vy = 3 + y_ofs(i)
if #emcool_pipes == 0 then
vx = util.trinary(units[i].num_boilers == 0, 36, 79)
else
local em_water = tank_types[tank_conns[i]] == COOLANT_TYPE.WATER
vx = util.trinary(units[i].num_boilers == 0, 58, util.trinary(units[i].has_tank and em_water, 94, 91))
end
PipeNetwork{parent=main,x=vx-6,y=vy,pipes={pipe(0,1,9,0,colors.blue,true)},bg=style.theme.bg}
TextBox{parent=main,x=vx,y=vy,text="\x10\x11",fg_bg=text_col,width=2}
TextBox{parent=main,x=vx+5,y=vy,text="\x1b",fg_bg=cpair(colors.blue,text_col.bkg),width=1}
local conn = IndicatorLight{parent=main,x=vx-3,y=vy+1,label=util.sprintf("PV%02d-AUX", i * 6),colors=style.ind_grn}
local open = IndicatorLight{parent=main,x=vx-3,y=vy+2,label="OPEN",colors=style.ind_wht}
conn.register(units[i].unit_ps, "V_aux_conn", conn.update)
open.register(units[i].unit_ps, "V_aux_state", open.update)
end
end
------------------- -------------------
-- dynamic tanks -- -- dynamic tanks --
------------------- -------------------
@@ -292,21 +340,23 @@ local function init(main)
local tank = Div{parent=main,x=3,y=7+y_offset,width=20,height=14} 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=" ",x=1,y=1,fg_bg=style.lg_gray}
TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=ALIGN.CENTER,height=1,fg_bg=style.wh_gray} TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=ALIGN.CENTER,fg_bg=style.wh_gray}
local tank_box = Rectangle{parent=tank,border=border(1,colors.gray,true),width=20,height=12} local 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} local status = StateIndicator{parent=tank_box,x=3,y=1,states=style.dtank.states,value=1,min_width=14}
TextBox{parent=tank_box,x=2,y=3,text="Fill",height=1,width=10,fg_bg=style.label} TextBox{parent=tank_box,x=2,y=3,text="Fill",width=10,fg_bg=style.label}
local tank_pcnt = DataIndicator{parent=tank_box,x=10,y=3,label="",format="%5.2f",value=100,unit="%",lu_colors=lu_col,width=8,fg_bg=text_col} local tank_pcnt = DataIndicator{parent=tank_box,x=10,y=3,label="",format="%5.2f",value=100,unit="%",lu_colors=lu_col,width=8,fg_bg=text_col}
local tank_amnt = DataIndicator{parent=tank_box,x=2,label="",format="%13d",value=0,commas=true,unit="mB",lu_colors=lu_col,width=16,fg_bg=s_field} local tank_amnt = DataIndicator{parent=tank_box,x=2,label="",format="%13d",value=0,commas=true,unit="mB",lu_colors=lu_col,width=16,fg_bg=s_field}
TextBox{parent=tank_box,x=2,y=6,text="Water Level",height=1,width=11,fg_bg=style.label} local is_water = tank_types[i] == COOLANT_TYPE.WATER
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} TextBox{parent=tank_box,x=2,y=6,text=util.trinary(is_water,"Water","Sodium").." Level",width=12,fg_bg=style.label}
local level = HorizontalBar{parent=tank_box,x=2,y=7,bar_fg_bg=cpair(util.trinary(is_water,colors.blue,colors.lightBlue),colors.gray),height=1,width=16}
TextBox{parent=tank_box,x=2,y=9,text="In/Out Mode",width=11,fg_bg=style.label}
local can_fill = IndicatorLight{parent=tank_box,x=2,y=10,label="FILL",colors=style.ind_wht} local can_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 can_empty = IndicatorLight{parent=tank_box,x=10,y=10,label="EMPTY",colors=style.ind_wht}
@@ -344,8 +394,8 @@ local function init(main)
local sps = Div{parent=main,x=140,y=3,height=12} 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=" ",width=24,x=1,y=1,fg_bg=style.lg_gray}
TextBox{parent=sps,text="SPS",alignment=ALIGN.CENTER,width=24,height=1,fg_bg=wh_gray} TextBox{parent=sps,text="SPS",alignment=ALIGN.CENTER,width=24,fg_bg=wh_gray}
local sps_box = Rectangle{parent=sps,border=border(1,colors.gray,true),width=24,height=10} local sps_box = Rectangle{parent=sps,border=border(1,colors.gray,true),width=24,height=10}
@@ -353,12 +403,12 @@ local function init(main)
status.register(facility.sps_ps_tbl[1], "computed_status", status.update) status.register(facility.sps_ps_tbl[1], "computed_status", status.update)
TextBox{parent=sps_box,x=2,y=3,text="Input Rate",height=1,width=10,fg_bg=style.label} TextBox{parent=sps_box,x=2,y=3,text="Input Rate",width=10,fg_bg=style.label}
local sps_in = DataIndicator{parent=sps_box,x=2,label="",format="%15.2f",value=0,unit="mB/t",lu_colors=lu_col,width=20,fg_bg=s_field} local sps_in = DataIndicator{parent=sps_box,x=2,label="",format="%15.2f",value=0,unit="mB/t",lu_colors=lu_col,width=20,fg_bg=s_field}
sps_in.register(facility.ps, "po_am_rate", sps_in.update) sps_in.register(facility.ps, "po_am_rate", sps_in.update)
TextBox{parent=sps_box,x=2,y=6,text="Production Rate",height=1,width=15,fg_bg=style.label} TextBox{parent=sps_box,x=2,y=6,text="Production Rate",width=15,fg_bg=style.label}
local sps_rate = DataIndicator{parent=sps_box,x=2,label="",format="%15d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=20,fg_bg=s_field} local sps_rate = DataIndicator{parent=sps_box,x=2,label="",format="%15d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=20,fg_bg=s_field}
sps_rate.register(facility.sps_ps_tbl[1], "process_rate", function (r) sps_rate.update(r * 1000) end) sps_rate.register(facility.sps_ps_tbl[1], "process_rate", function (r) sps_rate.update(r * 1000) end)
@@ -367,13 +417,13 @@ local function init(main)
-- statistics -- -- statistics --
---------------- ----------------
TextBox{parent=main,x=145,y=16,text="RAW WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} TextBox{parent=main,x=145,y=16,text="RAW WASTE",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
local raw_waste = Rectangle{parent=main,x=145,y=17,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=s_hi_bright} local raw_waste = Rectangle{parent=main,x=145,y=17,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=s_hi_bright}
local sum_raw_waste = DataIndicator{parent=raw_waste,lu_colors=lu_c_d,label="SUM",unit="mB/t",format="%8.2f",value=0,width=17} local sum_raw_waste = DataIndicator{parent=raw_waste,lu_colors=lu_c_d,label="SUM",unit="mB/t",format="%8.2f",value=0,width=17}
sum_raw_waste.register(facility.ps, "burn_sum", sum_raw_waste.update) sum_raw_waste.register(facility.ps, "burn_sum", sum_raw_waste.update)
TextBox{parent=main,x=145,y=21,text="PROC. WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} TextBox{parent=main,x=145,y=21,text="PROC. WASTE",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=s_hi_bright} local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=s_hi_bright}
local pu = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17} local pu = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17}
local po = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="Po",unit="mB/t",format="%9.2f",value=0,width=17} local po = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="Po",unit="mB/t",format="%9.2f",value=0,width=17}
@@ -383,7 +433,7 @@ local function init(main)
po.register(facility.ps, "po_rate", po.update) po.register(facility.ps, "po_rate", po.update)
popl.register(facility.ps, "po_pl_rate", popl.update) popl.register(facility.ps, "po_pl_rate", popl.update)
TextBox{parent=main,x=145,y=28,text="SPENT WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} TextBox{parent=main,x=145,y=28,text="SPENT WASTE",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
local sp_waste = Rectangle{parent=main,x=145,y=29,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=s_hi_bright} local sp_waste = Rectangle{parent=main,x=145,y=29,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=s_hi_bright}
local sum_sp_waste = DataIndicator{parent=sp_waste,lu_colors=lu_c_d,label="SUM",unit="mB/t",format="%8.3f",value=0,width=17} local sum_sp_waste = DataIndicator{parent=sp_waste,lu_colors=lu_c_d,label="SUM",unit="mB/t",format="%8.3f",value=0,width=17}

View File

@@ -14,16 +14,16 @@ local pkt_entry = require("coordinator.ui.components.pkt_entry")
local core = require("graphics.core") local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.listbox") local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.multipane") local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.TextBox")
local TabBar = require("graphics.elements.controls.tabbar") local TabBar = require("graphics.elements.controls.TabBar")
local LED = require("graphics.elements.indicators.led") local LED = require("graphics.elements.indicators.LED")
local LEDPair = require("graphics.elements.indicators.ledpair") local LEDPair = require("graphics.elements.indicators.LEDPair")
local RGBLED = require("graphics.elements.indicators.ledrgb") local RGBLED = require("graphics.elements.indicators.RGBLED")
local LINK_STATE = types.PANEL_LINK_STATE local LINK_STATE = types.PANEL_LINK_STATE
@@ -34,12 +34,14 @@ local cpair = core.cpair
local led_grn = style.led_grn local led_grn = style.led_grn
-- create new front panel view -- create new front panel view
---@param panel graphics_element main displaybox ---@param panel DisplayBox main displaybox
---@param num_units integer number of units (number of unit monitors) ---@param num_units integer number of units (number of unit monitors)
local function init(panel, num_units) local function init(panel, num_units)
local ps = iocontrol.get_db().fp.ps local ps = iocontrol.get_db().fp.ps
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,height=1,fg_bg=style.fp_theme.header} local term_w, term_h = term.getSize()
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,fg_bg=style.fp_theme.header}
local page_div = Div{parent=panel,x=1,y=3} local page_div = Div{parent=panel,x=1,y=3}
@@ -61,7 +63,7 @@ local function init(panel, num_units)
local modem = LED{parent=system,label="MODEM",colors=led_grn} local modem = LED{parent=system,label="MODEM",colors=led_grn}
if not style.colorblind then if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,style.fp_ind_bkg}} local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.fp_ind_bkg}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED) network.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(ps, "link_state", network.update) network.register(ps, "link_state", network.update)
else else
@@ -110,7 +112,7 @@ local function init(panel, num_units)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID()) local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=style.fp.disabled_fg} TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=style.fp.disabled_fg}
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2} local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
@@ -131,9 +133,9 @@ local function init(panel, num_units)
-- about footer -- about footer
-- --
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg} local about = Div{parent=main_page,width=15,height=2,y=term_h-3,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1} local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1} local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) 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) comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@@ -145,8 +147,8 @@ local function init(panel, num_units)
-- API page -- API page
local api_page = Div{parent=page_div,x=1,y=1,hidden=true} local api_page = Div{parent=page_div,x=1,y=1,hidden=true}
local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} local api_list = ListBox{parent=api_page,y=1,height=term_h-2,width=term_w,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=api_list,height=1,hidden=true} -- padding local _ = Div{parent=api_list,height=1} -- padding
-- assemble page panes -- assemble page panes

View File

@@ -14,14 +14,14 @@ local unit_overview = require("coordinator.ui.components.unit_overview")
local core = require("graphics.core") local core = require("graphics.core")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.TextBox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local ALIGN = core.ALIGN local ALIGN = core.ALIGN
-- create new main view -- create new main view
---@param main graphics_element main displaybox ---@param main DisplayBox main displaybox
local function init(main) local function init(main)
local s_header = style.theme.header local s_header = style.theme.header
@@ -29,15 +29,16 @@ local function init(main)
local units = iocontrol.get_db().units local units = iocontrol.get_db().units
-- window header message -- window header message
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=ALIGN.CENTER,height=1,fg_bg=s_header} local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=ALIGN.CENTER,fg_bg=s_header}
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=style.lg_white,width=12,fg_bg=s_header} local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=style.lg_white,width=12,fg_bg=s_header}
-- max length example: "01:23:45 AM - Wednesday, September 28 2022" -- max length example: "01:23:45 AM - Wednesday, September 28 2022"
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=s_header} local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,fg_bg=s_header}
ping.register(facility.ps, "sv_ping", ping.update) ping.register(facility.ps, "sv_ping", ping.update)
datetime.register(facility.ps, "date_time", datetime.set_value) datetime.register(facility.ps, "date_time", datetime.set_value)
local uo_1, uo_2, uo_3, uo_4 ---@type graphics_element ---@type Div, Div, Div, Div
local uo_1, uo_2, uo_3, uo_4
local cnc_y_start = 3 local cnc_y_start = 3
local row_1_height = 0 local row_1_height = 0
@@ -79,7 +80,7 @@ local function init(main)
assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)") assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)")
TextBox{parent=main,y=cnc_bottom_align_start,text=string.rep("\x8c", header.get_width()),alignment=ALIGN.CENTER,height=1,fg_bg=style.lg_gray} TextBox{parent=main,y=cnc_bottom_align_start,text=string.rep("\x8c", header.get_width()),alignment=ALIGN.CENTER,fg_bg=style.lg_gray}
cnc_bottom_align_start = cnc_bottom_align_start + 2 cnc_bottom_align_start = cnc_bottom_align_start + 2
@@ -87,7 +88,7 @@ local function init(main)
util.nop() util.nop()
imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1]) imatrix(main, 131, cnc_bottom_align_start, facility.induction_ps_tbl[1])
end end
return init return init

View File

@@ -5,7 +5,7 @@
local unit_detail = require("coordinator.ui.components.unit_detail") local unit_detail = require("coordinator.ui.components.unit_detail")
-- create a unit view -- create a unit view
---@param main graphics_element main displaybox ---@param main DisplayBox main displaybox
---@param id integer ---@param id integer
local function init(main, id) local function init(main, id)
unit_detail(main, id) unit_detail(main, id)

View File

@@ -8,15 +8,17 @@ local util = require("scada-common.util")
local pgi = {} local pgi = {}
local data = { local data = {
pkt_list = nil, ---@type nil|graphics_element pkt_list = nil, ---@type ListBox|nil
pkt_entry = nil, ---@type function pkt_entry = nil, ---@type function
-- session entries -- session entries
s_entries = { pkt = {} } s_entries = {
pkt = {} ---@type Div[]
}
} }
-- link list boxes -- link list boxes
---@param pkt_list graphics_element pocket list element ---@param pkt_list ListBox pocket list element
---@param pkt_entry function pocket entry constructor ---@param pkt_entry fun(parent: ListBox, id: integer) : Div pocket entry constructor
function pgi.link_elements(pkt_list, pkt_entry) function pgi.link_elements(pkt_list, pkt_entry)
data.pkt_list = pkt_list data.pkt_list = pkt_list
data.pkt_entry = pkt_entry data.pkt_entry = pkt_entry

View File

@@ -2,16 +2,20 @@
-- Graphics Style Options -- Graphics Style Options
-- --
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local themes = require("graphics.themes") local themes = require("graphics.themes")
local coordinator = require("coordinator.coordinator")
---@class crd_style ---@class crd_style
local style = {} local style = {}
local cpair = core.cpair local cpair = core.cpair
local config = coordinator.config
-- front panel styling -- front panel styling
style.fp_theme = themes.sandstone style.fp_theme = themes.sandstone
@@ -147,236 +151,110 @@ style.gray_white = cpair(colors.gray, colors.white)
-- UI COMPONENTS -- -- UI COMPONENTS --
style.reactor = { style.reactor = {
-- reactor states -- reactor states<br>
---@see REACTOR_STATE
states = { states = {
{ { color = cpair(colors.black, colors.yellow), text = "PLC OFF-LINE" },
color = cpair(colors.black, colors.yellow), { color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
text = "PLC OFF-LINE" { 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.orange), { color = cpair(colors.black, colors.red), text = "SCRAMMED" },
text = "NOT FORMED" { color = cpair(colors.black, colors.red), text = "FORCE DISABLED" }
},
{
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 = { style.boiler = {
-- boiler states -- boiler states<br>
---@see BOILER_STATE
states = { states = {
{ { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
color = cpair(colors.black, colors.yellow), { color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
text = "OFF-LINE" { 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.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 = { style.turbine = {
-- turbine states -- turbine states<br>
---@see TURBINE_STATE
states = { states = {
{ { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
color = cpair(colors.black, colors.yellow), { color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
text = "OFF-LINE" { 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.orange), { color = cpair(colors.black, colors.red), text = "TRIP" }
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 = { style.dtank = {
-- dynamic tank states -- dynamic tank states<br>
---@see TANK_STATE
states = { states = {
{ { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
color = cpair(colors.black, colors.yellow), { color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
text = "OFF-LINE" { 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.orange), { color = cpair(colors.black, colors.green), text = "FILLED" }
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 = { style.imatrix = {
-- auto waste processing states -- induction matrix states<br>
---@see IMATRIX_STATE
states = { states = {
{ { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
color = cpair(colors.black, colors.green), { color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
text = "PLUTONIUM" { 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.cyan), { color = cpair(colors.black, colors.yellow), text = "HIGH CHARGE" }
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)
}
} }
} }
style.sps = {
-- SPS states<br>
---@see SPS_STATE
states = {
{ color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
{ color = cpair(colors.black, colors.orange), text = "RTU FAULT" },
{ color = cpair(colors.white, colors.gray), text = "IDLE" },
{ color = cpair(colors.black, colors.green), text = "ACTIVE" }
}
}
-- get waste styling, which depends on the configuration
---@return { states: { color: color, text: string }, states_abbrv: { color: color, text: string }, options: string[], unit_opts: { text: string, fg_bg: cpair, active_fg_bg:cpair } }
function style.get_waste()
local pu_color = util.trinary(config.GreenPuPellet, colors.green, colors.cyan)
local po_color = util.trinary(config.GreenPuPellet, colors.cyan, colors.green)
return {
-- auto waste processing states
states = {
{ color = cpair(colors.black, pu_color), text = "PLUTONIUM" },
{ color = cpair(colors.black, po_color), text = "POLONIUM" },
{ color = cpair(colors.black, colors.purple), text = "ANTI MATTER" }
},
states_abbrv = {
{ color = cpair(colors.black, pu_color), text = "Pu" },
{ color = cpair(colors.black, po_color), text = "Po" },
{ color = cpair(colors.black, colors.purple), text = "AM" }
},
-- process radio button options
options = { "Plutonium", "Polonium", "Antimatter" },
-- unit waste selection
unit_opts = {
{ text = "Auto", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.white, colors.gray) },
{ text = "Pu", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, pu_color) },
{ text = "Po", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, po_color) },
{ text = "AM", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.purple) }
}
}
end
return style return style

View File

@@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
local core = {} local core = {}
core.version = "2.2.4" core.version = "2.4.8"
core.flasher = flasher core.flasher = flasher
core.events = events core.events = events
@@ -17,6 +17,8 @@ core.events = events
---@enum ALIGN ---@enum ALIGN
core.ALIGN = { LEFT = 1, CENTER = 2, RIGHT = 3 } core.ALIGN = { LEFT = 1, CENTER = 2, RIGHT = 3 }
---@alias Container DisplayBox|Div|ListBox|MultiPane|AppMultiPane|Rectangle
---@class graphics_border ---@class graphics_border
---@field width integer ---@field width integer
---@field color color ---@field color color
@@ -123,15 +125,17 @@ end
-- Interactive Field Manager -- Interactive Field Manager
---@param e graphics_base ---@param e graphics_base element
---@param max_len any ---@param max_len integer max value length
---@param fg_bg any ---@param fg_bg cpair enabled fg/bg
---@param dis_fg_bg any ---@param dis_fg_bg? cpair disabled fg/bg
function core.new_ifield(e, max_len, fg_bg, dis_fg_bg) ---@param align_right? boolean true to align content right while unfocused
function core.new_ifield(e, max_len, fg_bg, dis_fg_bg, align_right)
local self = { local self = {
frame_start = 1, frame_start = 1,
visible_text = e.value, visible_text = e.value,
cursor_pos = string.len(e.value) + 1, cursor_pos = string.len(e.value) + 1,
align_offset = 0,
selected_all = false selected_all = false
} }
@@ -186,7 +190,12 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
e.w_write(string.rep(" ", e.frame.w)) e.w_write(string.rep(" ", e.frame.w))
e.w_set_cur(1, 1) e.w_set_cur(1, 1)
local function _write() local function _write(align_r)
if align_r and string.len(self.visible_text) <=e.frame.w then
self.align_offset = (e.frame.w - string.len(self.visible_text))
e.w_set_cur((e.frame.w - string.len(self.visible_text)) + 1, 1)
end
if self.censor then if self.censor then
e.w_write(string.rep(self.censor, string.len(self.visible_text))) e.w_write(string.rep(self.censor, string.len(self.visible_text)))
else else
@@ -226,15 +235,27 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
self.selected_all = false self.selected_all = false
-- write text without cursor -- write text without cursor
_write() _write(align_right)
end end
end end
-- move cursor to x -- get an x value to pass to move_cursor taking into account right alignment offset present when unfocused
---@param x integer ---@param x integer
function public.get_cursor_align_shift(x)
return math.max(0, x - self.align_offset)
end
-- move cursor to x
---@param x integer x position or 0 to jump to the end
function public.move_cursor(x) function public.move_cursor(x)
self.selected_all = false self.selected_all = false
self.cursor_pos = math.min(x, string.len(self.visible_text) + 1)
if x <= 0 then
self.cursor_pos = string.len(self.visible_text) + 1
else
self.cursor_pos = math.min(x, string.len(self.visible_text) + 1)
end
public.show() public.show()
end end

View File

@@ -10,9 +10,9 @@ local events = core.events
local element = {} local element = {}
---@class graphics_args_generic ---@class graphics_args
---@field window? table ---@field window? Window base window to use, only root elements should use this
---@field parent? graphics_element ---@field parent? graphics_element parent element, if not a root element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer next line if omitted ---@field y? integer next line if omitted
@@ -23,47 +23,6 @@ local element = {}
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
---@field can_focus? boolean true if this element can be focused, false by default ---@field can_focus? boolean true if this element can be focused, false by default
---@alias graphics_args graphics_args_generic
---|waiting_args
---|app_button_args
---|checkbox_args
---|hazard_button_args
---|multi_button_args
---|push_button_args
---|radio_2d_args
---|radio_button_args
---|sidebar_args
---|spinbox_args
---|switch_button_args
---|tabbar_args
---|number_field_args
---|text_field_args
---|alarm_indicator_light
---|core_map_args
---|data_indicator_args
---|hbar_args
---|icon_indicator_args
---|indicator_led_args
---|indicator_led_pair_args
---|indicator_led_rgb_args
---|indicator_light_args
---|power_indicator_args
---|rad_indicator_args
---|signal_bar_args
---|state_indicator_args
---|tristate_indicator_light_args
---|vbar_args
---|app_multipane_args
---|colormap_args
---|displaybox_args
---|div_args
---|listbox_args
---|multipane_args
---|pipenet_args
---|rectangle_args
---|textbox_args
---|tiling_args
---@class element_subscription ---@class element_subscription
---@field ps psil ps used ---@field ps psil ps used
---@field key string data key ---@field key string data key
@@ -82,22 +41,23 @@ end
-- a base graphics element, should not be created on its own -- a base graphics element, should not be created on its own
---@nodiscard ---@nodiscard
---@param args graphics_args arguments ---@param args graphics_args arguments
---@param constraint? function apply a dimensional constraint based on proposed dimensions function(frame) -> width, height
---@param child_offset_x? integer mouse event offset x ---@param child_offset_x? integer mouse event offset x
---@param child_offset_y? integer mouse event offset y ---@param child_offset_y? integer mouse event offset y
function element.new(args, child_offset_x, child_offset_y) function element.new(args, constraint, child_offset_x, child_offset_y)
local self = { local self = {
id = nil, ---@type element_id|nil id = nil, ---@type element_id|nil
is_root = args.parent == nil, is_root = args.parent == nil,
elem_type = debug.getinfo(2).name, elem_type = debug.getinfo(2).name,
define_completed = false, define_completed = false,
p_window = nil, ---@type table p_window = nil, ---@type Window
position = events.new_coord_2d(1, 1), position = events.new_coord_2d(1, 1),
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
offset_x = 0, offset_x = 0,
offset_y = 0, offset_y = 0,
next_y = 1, -- next child y coordinate next_y = 1, -- next child y coordinate
next_id = 0, -- next child ID next_id = 1, -- next child ID
subscriptions = {}, subscriptions = {}, ---@type { ps: psil, key: string, func: function }[]
button_down = { events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1) }, button_down = { events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1) },
focused = false, focused = false,
mt = {} mt = {}
@@ -107,13 +67,13 @@ function element.new(args, child_offset_x, child_offset_y)
local protected = { local protected = {
enabled = true, enabled = true,
value = nil, ---@type any value = nil, ---@type any
window = nil, ---@type table window = nil, ---@type Window
content_window = nil, ---@type table|nil content_window = nil, ---@type Window|nil
mouse_window_shift = { x = 0, y = 0 }, mouse_window_shift = { x = 0, y = 0 },
fg_bg = core.cpair(colors.white, colors.black), fg_bg = core.cpair(colors.white, colors.black),
frame = core.gframe(1, 1, 1, 1), frame = core.gframe(1, 1, 1, 1),
children = {}, children = {}, ---@type graphics_base[]
child_id_map = {} child_id_map = {} ---@type { [element_id]: integer }
} }
-- element as string -- element as string
@@ -126,9 +86,9 @@ function element.new(args, child_offset_x, child_offset_y)
setmetatable(public, self.mt) setmetatable(public, self.mt)
----------------------- ------------------------------
-- PRIVATE FUNCTIONS -- --#region PRIVATE FUNCTIONS --
----------------------- ------------------------------
-- use tab to jump to the next focusable field -- use tab to jump to the next focusable field
---@param reverse boolean ---@param reverse boolean
@@ -166,10 +126,10 @@ function element.new(args, child_offset_x, child_offset_y)
end end
end end
---@param children table ---@param children graphics_base[]
local function traverse(children) local function traverse(children)
for i = 1, #children do for i = 1, #children do
local child = children[i] ---@type graphics_base local child = children[i]
handle_element(child.get()) handle_element(child.get())
if child.get().is_visible() then traverse(child.children) end if child.get().is_visible() then traverse(child.children) end
end end
@@ -189,9 +149,11 @@ function element.new(args, child_offset_x, child_offset_y)
end end
end end
------------------------- --#endregion
-- PROTECTED FUNCTIONS --
------------------------- --------------------------------
--#region PROTECTED FUNCTIONS --
--------------------------------
-- prepare the template -- prepare the template
---@param offset_x integer x offset for mouse events ---@param offset_x integer x offset for mouse events
@@ -199,7 +161,7 @@ function element.new(args, child_offset_x, child_offset_y)
---@param next_y integer next line if no y was provided ---@param next_y integer next line if no y was provided
function protected.prepare_template(offset_x, offset_y, next_y) function protected.prepare_template(offset_x, offset_y, next_y)
-- don't auto incrememnt y if inheriting height, that would cause an assertion -- don't auto incrememnt y if inheriting height, that would cause an assertion
next_y = util.trinary(args.height == nil, 1, next_y) next_y = util.trinary(args.height == nil and constraint == nil, 1, next_y)
-- record offsets in case there is a reposition -- record offsets in case there is a reposition
self.offset_x = offset_x self.offset_x = offset_x
@@ -226,6 +188,13 @@ function element.new(args, child_offset_x, child_offset_y)
local w, h = self.p_window.getSize() local w, h = self.p_window.getSize()
f.w = math.min(f.w, w - (f.x - 1)) f.w = math.min(f.w, w - (f.x - 1))
f.h = math.min(f.h, h - (f.y - 1)) f.h = math.min(f.h, h - (f.y - 1))
if type(constraint) == "function" then
-- constrain per provided constraint function (can only get smaller than available space)
w, h = constraint(f)
f.w = math.min(f.w, w)
f.h = math.min(f.h, h)
end
end end
-- check frame -- check frame
@@ -277,24 +246,29 @@ function element.new(args, child_offset_x, child_offset_y)
-- alias functions -- alias functions
-- window set cursor position -- window set cursor position<br>
---@see Window.setCursorPos
---@param x integer ---@param x integer
---@param y integer ---@param y integer
function protected.w_set_cur(x, y) protected.window.setCursorPos(x, y) end function protected.w_set_cur(x, y) protected.window.setCursorPos(x, y) end
-- set background color -- set background color<br>
---@see Window.setBackgroundColor
---@param c color ---@param c color
function protected.w_set_bkg(c) protected.window.setBackgroundColor(c) end function protected.w_set_bkg(c) protected.window.setBackgroundColor(c) end
-- set foreground (text) color -- set foreground (text) color<br>
---@see Window.setTextColor
---@param c color ---@param c color
function protected.w_set_fgd(c) protected.window.setTextColor(c) end function protected.w_set_fgd(c) protected.window.setTextColor(c) end
-- write text -- write text<br>
---@see Window.write
---@param str string ---@param str string
function protected.w_write(str) protected.window.write(str) end function protected.w_write(str) protected.window.write(str) end
-- blit text -- blit text<br>
---@see Window.blit
---@param str string ---@param str string
---@param fg string ---@param fg string
---@param bg string ---@param bg string
@@ -326,8 +300,10 @@ function element.new(args, child_offset_x, child_offset_y)
-- report completion of element instantiation and get the public interface -- report completion of element instantiation and get the public interface
---@nodiscard ---@nodiscard
---@param redraw? boolean true to call redraw as part of completing this element
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
function protected.complete() function protected.complete(redraw)
if redraw then protected.redraw() end
if args.parent ~= nil then args.parent.__child_ready(self.id, public) end if args.parent ~= nil then args.parent.__child_ready(self.id, public) end
return public, self.id return public, self.id
end end
@@ -343,7 +319,7 @@ function element.new(args, child_offset_x, child_offset_y)
-- focus this element and take away focus from all other elements -- focus this element and take away focus from all other elements
function protected.take_focus() args.parent.__focus_child(public) end function protected.take_focus() args.parent.__focus_child(public) end
-- action handlers -- --#region Action Handlers
-- luacheck: push ignore -- luacheck: push ignore
---@diagnostic disable: unused-local, unused-vararg ---@diagnostic disable: unused-local, unused-vararg
@@ -392,14 +368,12 @@ function element.new(args, child_offset_x, child_offset_y)
function protected.handle_paste(text) end function protected.handle_paste(text) end
-- handle data value changes -- handle data value changes
---@vararg any value(s) ---@param ... any value(s)
function protected.on_update(...) end function protected.on_update(...) end
-- callback on control press responses --#endregion
---@param result any
function protected.response_callback(result) end
-- accessors and control -- --#region Accessors and Control
-- get value -- get value
---@nodiscard ---@nodiscard
@@ -418,11 +392,11 @@ function element.new(args, child_offset_x, child_offset_y)
function protected.set_max(max) end function protected.set_max(max) end
-- custom recolor command, varies by element if implemented -- custom recolor command, varies by element if implemented
---@vararg cpair|color color(s) ---@param ... cpair|color color(s)
function protected.recolor(...) end function protected.recolor(...) end
-- custom resize command, varies by element if implemented -- custom resize command, varies by element if implemented
---@vararg integer sizing ---@param ... integer sizing
function protected.resize(...) end function protected.resize(...) end
-- luacheck: pop -- luacheck: pop
@@ -437,9 +411,13 @@ function element.new(args, child_offset_x, child_offset_y)
-- stop animations -- stop animations
function protected.stop_anim() end function protected.stop_anim() end
----------- --#endregion
-- SETUP --
----------- --#endregion
------------------
--#region SETUP --
------------------
-- get the parent window -- get the parent window
self.p_window = args.window self.p_window = args.window
@@ -458,9 +436,11 @@ function element.new(args, child_offset_x, child_offset_y)
self.id = args.parent.__add_child(args.id, protected) self.id = args.parent.__add_child(args.id, protected)
end end
---------------------- --#endregion
-- PUBLIC FUNCTIONS --
---------------------- -----------------------------
--#region PUBLIC FUNCTIONS --
-----------------------------
-- get the window object -- get the window object
---@nodiscard ---@nodiscard
@@ -499,9 +479,10 @@ function element.new(args, child_offset_x, child_offset_y)
end end
end end
-- ELEMENT TREE -- --#region ELEMENT TREE
-- add a child element -- add a child element
---@package
---@nodiscard ---@nodiscard
---@param key string|nil id ---@param key string|nil id
---@param child graphics_base ---@param child graphics_base
@@ -511,20 +492,24 @@ function element.new(args, child_offset_x, child_offset_y)
self.next_y = child.frame.y + child.frame.h self.next_y = child.frame.y + child.frame.h
local id = key ---@type string|integer|nil local id = key ---@type element_id|nil
if id == nil then if id == nil then
id = self.next_id id = self.next_id
self.next_id = self.next_id + 1 self.next_id = self.next_id + 1
end end
table.insert(protected.children, child) -- see #539 on GitHub
-- using #protected.children after inserting may give the wrong index, since if it inserts in a hole that completes the list then
-- the length will jump up to the full length of the list, possibly making two map entries point to the same child
protected.child_id_map[id] = #protected.children + 1
protected.child_id_map[id] = #protected.children table.insert(protected.children, child)
return id return id
end end
-- remove a child element -- remove a child element
---@package
---@param id element_id id ---@param id element_id id
function public.__remove_child(id) function public.__remove_child(id)
local index = protected.child_id_map[id] local index = protected.child_id_map[id]
@@ -536,11 +521,13 @@ function element.new(args, child_offset_x, child_offset_y)
end end
-- actions to take upon a child element becoming ready (initial draw/construction completed) -- actions to take upon a child element becoming ready (initial draw/construction completed)
---@package
---@param key element_id id ---@param key element_id id
---@param child graphics_element ---@param child graphics_element
function public.__child_ready(key, child) protected.on_added(key, child) end function public.__child_ready(key, child) protected.on_added(key, child) end
-- focus solely on this child -- focus solely on this child
---@package
---@param child graphics_element ---@param child graphics_element
function public.__focus_child(child) function public.__focus_child(child)
if self.is_root then if self.is_root then
@@ -550,6 +537,7 @@ function element.new(args, child_offset_x, child_offset_y)
end end
-- a child was focused, used to make sure it is actually visible to the user in the content frame -- a child was focused, used to make sure it is actually visible to the user in the content frame
---@package
---@param child graphics_element ---@param child graphics_element
function public.__child_focused(child) function public.__child_focused(child)
protected.on_child_focused(child) protected.on_child_focused(child)
@@ -559,8 +547,17 @@ function element.new(args, child_offset_x, child_offset_y)
-- get a child element -- get a child element
---@nodiscard ---@nodiscard
---@param id element_id ---@param id element_id
---@return graphics_element ---@return graphics_element element
function public.get_child(id) return protected.children[protected.child_id_map[id]].get() end function public.get_child(id) return ({ protected.children[protected.child_id_map[id]].get() })[1] end
-- get all children
---@nodiscard
---@return table children table of graphics_element objects
function public.get_children()
local list = {}
for k, v in pairs(protected.children) do list[k] = v.get() end
return list
end
-- remove a child element -- remove a child element
---@param id element_id ---@param id element_id
@@ -577,7 +574,7 @@ function element.new(args, child_offset_x, child_offset_y)
-- remove all child elements and reset next y -- remove all child elements and reset next y
function public.remove_all() function public.remove_all()
for i = 1, #protected.children do for i = 1, #protected.children do
local child = protected.children[i].get() ---@type graphics_element local child = protected.children[i].get() ---@type graphics_element
child.delete() child.delete()
protected.on_removed(child.get_id()) protected.on_removed(child.get_id())
end end
@@ -598,29 +595,33 @@ function element.new(args, child_offset_x, child_offset_y)
local elem = child.get().get_element_by_id(id) local elem = child.get().get_element_by_id(id)
if elem ~= nil then return elem end if elem ~= nil then return elem end
end end
else return protected.children[index].get() end else return ({ protected.children[index].get() })[1] end
end end
-- AUTO-PLACEMENT -- --#endregion
--#region AUTO-PLACEMENT
-- skip a line for automatically placed elements -- skip a line for automatically placed elements
function public.line_break() function public.line_break()
self.next_y = self.next_y + 1 self.next_y = self.next_y + 1
end end
-- PROPERTIES -- --#endregion
-- get element id --#region PROPERTIES
-- get element ID
---@nodiscard ---@nodiscard
---@return element_id ---@return element_id
function public.get_id() return self.id end function public.get_id() return self.id end
-- get element x -- get element relative x position
---@nodiscard ---@nodiscard
---@return integer x ---@return integer x
function public.get_x() return protected.frame.x end function public.get_x() return protected.frame.x end
-- get element y -- get element relative y position
---@nodiscard ---@nodiscard
---@return integer y ---@return integer y
function public.get_y() return protected.frame.y end function public.get_y() return protected.frame.y end
@@ -640,12 +641,12 @@ function element.new(args, child_offset_x, child_offset_y)
---@return cpair fg_bg ---@return cpair fg_bg
function public.get_fg_bg() return protected.fg_bg end function public.get_fg_bg() return protected.fg_bg end
-- get the element value -- get the element's value
---@nodiscard ---@nodiscard
---@return any value ---@return any value
function public.get_value() return protected.get_value() end function public.get_value() return protected.get_value() end
-- set the element value -- set the element's value
---@param value any new value ---@param value any new value
function public.set_value(value) protected.set_value(value) end function public.set_value(value) protected.set_value(value) end
@@ -707,11 +708,11 @@ function element.new(args, child_offset_x, child_offset_y)
end end
-- custom recolor command, varies by element if implemented -- custom recolor command, varies by element if implemented
---@vararg cpair|color color(s) ---@param ... cpair|color color(s)
function public.recolor(...) protected.recolor(...) end function public.recolor(...) protected.recolor(...) end
-- resize attributes of the element value if supported -- resize attributes of the element value if supported
---@vararg number dimensions (element specific) ---@param ... number dimensions (element specific)
function public.resize(...) protected.resize(...) end function public.resize(...) protected.resize(...) end
-- reposition the element window<br> -- reposition the element window<br>
@@ -735,7 +736,9 @@ function element.new(args, child_offset_x, child_offset_y)
self.bounds.y2 = self.position.y + protected.frame.h - 1 self.bounds.y2 = self.position.y + protected.frame.h - 1
end end
-- FUNCTION CALLBACKS -- --#endregion
--#region FUNCTION CALLBACKS
-- handle a monitor touch or mouse click if this element is visible -- handle a monitor touch or mouse click if this element is visible
---@param event mouse_interaction mouse interaction event ---@param event mouse_interaction mouse interaction event
@@ -797,13 +800,9 @@ function element.new(args, child_offset_x, child_offset_y)
end end
-- draw the element given new data -- draw the element given new data
---@vararg any new data ---@param ... any new data
function public.update(...) protected.on_update(...) end function public.update(...) protected.on_update(...) end
-- on a control request response
---@param result any
function public.on_response(result) protected.response_callback(result) end
-- register a callback with a PSIL, allowing for automatic unregister on delete<br> -- register a callback with a PSIL, allowing for automatic unregister on delete<br>
-- do not use graphics elements directly with PSIL subscribe() -- do not use graphics elements directly with PSIL subscribe()
---@param ps psil PSIL to subscribe to ---@param ps psil PSIL to subscribe to
@@ -814,7 +813,9 @@ function element.new(args, child_offset_x, child_offset_y)
ps.subscribe(key, func) ps.subscribe(key, func)
end end
-- VISIBILITY & ANIMATIONS -- --#endregion
--#region VISIBILITY & ANIMATIONS
-- check if this element is visible -- check if this element is visible
function public.is_visible() return protected.window.isVisible() end function public.is_visible() return protected.window.isVisible() end
@@ -828,6 +829,7 @@ function element.new(args, child_offset_x, child_offset_y)
-- hide the element and disables animations<br> -- 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> -- this alone does not cause an element to be fully hidden, it only prevents updates from being shown<br>
---@see Window.redraw
---@see graphics_element.redraw ---@see graphics_element.redraw
---@see graphics_element.content_redraw ---@see graphics_element.content_redraw
---@param clear? boolean true to visibly hide this element (redraws the parent) ---@param clear? boolean true to visibly hide this element (redraws the parent)
@@ -879,6 +881,10 @@ function element.new(args, child_offset_x, child_offset_y)
end end
end end
--#endregion
--#endregion
return protected return protected
end end

View File

@@ -24,15 +24,15 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new app multipane element -- Create a new app multipane container element.
---@nodiscard ---@nodiscard
---@param args app_multipane_args ---@param args app_multipane_args
---@return graphics_element element, element_id id ---@return AppMultiPane element, element_id id
local function multipane(args) return function (args)
element.assert(type(args.panes) == "table", "panes is a required field") element.assert(type(args.panes) == "table", "panes is a required field")
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = 1 e.value = 1
@@ -100,10 +100,8 @@ local function multipane(args)
end end
end end
-- initial draw ---@class AppMultiPane:graphics_element
e.redraw() local AppMultiPane, id = e.complete(true)
return e.complete() return AppMultiPane, id
end end
return multipane

View File

@@ -9,10 +9,10 @@ local element = require("graphics.element")
---@field y? integer auto incremented if omitted ---@field y? integer auto incremented if omitted
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new color map -- Create a horizontal reference color map. Primarily used for tuning custom colors.
---@param args colormap_args ---@param args colormap_args
---@return graphics_element element, element_id id ---@return ColorMap element, element_id id
local function colormap(args) return function (args)
local bkg = "008877FFCCEE114455DD9933BBAA2266" local bkg = "008877FFCCEE114455DD9933BBAA2266"
local spaces = string.rep(" ", 32) local spaces = string.rep(" ", 32)
@@ -20,7 +20,7 @@ local function colormap(args)
args.height = 1 args.height = 1
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
-- draw color map -- draw color map
function e.redraw() function e.redraw()
@@ -28,10 +28,8 @@ local function colormap(args)
e.w_blit(spaces, bkg, bkg) e.w_blit(spaces, bkg, bkg)
end end
-- initial draw ---@class ColorMap:graphics_element
e.redraw() local ColorMap, id = e.complete(true)
return e.complete() return ColorMap, id
end end
return colormap

View File

@@ -13,13 +13,16 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new root display box -- Create a root display box.
---@nodiscard ---@nodiscard
---@param args displaybox_args ---@param args displaybox_args
---@return graphics_element element, element_id id ---@return DisplayBox element, element_id id
local function displaybox(args) return function (args)
-- create new graphics element base object -- create new graphics element base object
return element.new(args).complete() local e = element.new(args --[[@as graphics_args]])
end
return displaybox ---@class DisplayBox:graphics_element
local DisplayBox, id = e.complete()
return DisplayBox, id
end

View File

@@ -13,13 +13,16 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new div element -- Create a new div container element.
---@nodiscard ---@nodiscard
---@param args div_args ---@param args div_args
---@return graphics_element element, element_id id ---@return Div element, element_id id
local function div(args) return function (args)
-- create new graphics element base object -- create new graphics element base object
return element.new(args).complete() local e = element.new(args --[[@as graphics_args]])
end
return div ---@class Div:graphics_element
local Div, id = e.complete()
return Div, id
end

View File

@@ -29,15 +29,15 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@field y integer y position ---@field y integer y position
---@field h integer element height ---@field h integer element height
-- new listbox element -- Create a new scrollable listbox container element.
---@nodiscard ---@nodiscard
---@param args listbox_args ---@param args listbox_args
---@return graphics_element element, element_id id ---@return ListBox element, element_id id
local function listbox(args) return function (args)
args.can_focus = true args.can_focus = true
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
-- create content window for child elements -- create content window for child elements
local scroll_frame = window.create(e.window, 1, 1, e.frame.w - 1, args.scroll_height, false) local scroll_frame = window.create(e.window, 1, 1, e.frame.w - 1, args.scroll_height, false)
@@ -334,10 +334,8 @@ local function listbox(args)
draw_bar() draw_bar()
end end
-- initial draw ---@class ListBox:graphics_element
e.redraw() local ListBox, id = e.complete(true)
return e.complete() return ListBox, id
end end
return listbox

View File

@@ -14,15 +14,15 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new multipane element -- Create a new multipane container element.
---@nodiscard ---@nodiscard
---@param args multipane_args ---@param args multipane_args
---@return graphics_element element, element_id id ---@return MultiPane element, element_id id
local function multipane(args) return function (args)
element.assert(type(args.panes) == "table", "panes is a required field") element.assert(type(args.panes) == "table", "panes is a required field")
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = 1 e.value = 1
@@ -41,10 +41,8 @@ local function multipane(args)
end end
end end
-- initial draw ---@class MultiPane:graphics_element
e.redraw() local MultiPane, id = e.complete(true)
return e.complete() return MultiPane, id
end end
return multipane

View File

@@ -20,10 +20,10 @@ local element = require("graphics.element")
---@field fg string foreground blit ---@field fg string foreground blit
---@field bg string background blit ---@field bg string background blit
-- new pipe network -- Create a pipe network diagram.
---@param args pipenet_args ---@param args pipenet_args
---@return graphics_element element, element_id id ---@return PipeNetwork element, element_id id
local function pipenet(args) return function (args)
element.assert(type(args.pipes) == "table", "pipes is a required field") element.assert(type(args.pipes) == "table", "pipes is a required field")
args.width = 0 args.width = 0
@@ -47,7 +47,7 @@ local function pipenet(args)
end end
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
-- determine if there are any thin pipes involved -- determine if there are any thin pipes involved
local any_thin = false local any_thin = false
@@ -322,10 +322,8 @@ local function pipenet(args)
if any_thin then map_draw() else vector_draw() end if any_thin then map_draw() else vector_draw() end
end end
-- initial draw ---@class PipeNetwork:graphics_element
e.redraw() local PipeNetwork, id = e.complete(true)
return e.complete() return PipeNetwork, id
end end
return pipenet

View File

@@ -18,10 +18,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new rectangle -- Create a new rectangle container element.
---@param args rectangle_args ---@param args rectangle_args
---@return graphics_element element, element_id id ---@return Rectangle element, element_id id
local function rectangle(args) return function (args)
element.assert(args.border ~= nil or args.thin ~= true, "thin requires border to be provided") element.assert(args.border ~= nil or args.thin ~= true, "thin requires border to be provided")
-- if thin, then width will always need to be 1 -- if thin, then width will always need to be 1
@@ -45,7 +45,7 @@ local function rectangle(args)
end end
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args, offset_x, offset_y) local e = element.new(args --[[@as graphics_args]], nil, offset_x, offset_y)
-- create content window for child elements -- 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 = window.create(e.window, 1 + offset_x, 1 + offset_y, e.frame.w - (2 * offset_x), e.frame.h - (2 * offset_y))
@@ -191,7 +191,8 @@ local function rectangle(args)
e.redraw() e.redraw()
end end
return e.complete() ---@class Rectangle:graphics_element
end local Rectangle, id = e.complete()
return rectangle return Rectangle, id
end

View File

@@ -10,24 +10,40 @@ local ALIGN = core.ALIGN
---@class textbox_args ---@class textbox_args
---@field text string text to show ---@field text string text to show
---@field alignment? ALIGN text alignment, left by default ---@field alignment? ALIGN text alignment, left by default
---@field trim_whitespace? boolean true to trim whitespace before/after lines of text
---@field anchor? boolean true to use this as an anchor, making it focusable
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer minimum necessary height for wrapped text if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new text box -- Create a new text box element.
---@param args textbox_args ---@param args textbox_args
---@return graphics_element element, element_id id ---@return TextBox element, element_id id
local function textbox(args) return function (args)
element.assert(type(args.text) == "string", "text is a required field") element.assert(type(args.text) == "string", "text is a required field")
if args.anchor == true then args.can_focus = true end
-- provide a constraint condition to element creation to prevent an pointlessly tall text box
---@param frame graphics_frame
local function constrain(frame)
local new_height = math.max(1, #util.strwrap(args.text, frame.w))
if args.height then
new_height = math.max(frame.h, new_height)
end
return frame.w, new_height
end
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]], constrain)
e.value = args.text e.value = args.text
@@ -42,6 +58,12 @@ local function textbox(args)
for i = 1, #lines do for i = 1, #lines do
if i > e.frame.h then break end if i > e.frame.h then break end
-- trim leading/trailing whitespace, except on the first line
-- leading whitespace on the first line is usually intentional
if args.trim_whitespace == true then
lines[i] = util.trim(lines[i])
end
local len = string.len(lines[i]) local len = string.len(lines[i])
-- use cursor position to align this line -- use cursor position to align this line
@@ -64,10 +86,15 @@ local function textbox(args)
e.redraw() e.redraw()
end end
-- initial draw -- change the foreground color of the text
e.redraw() ---@param c color
function e.recolor(c)
e.w_set_fgd(c)
e.redraw()
end
return e.complete() ---@class TextBox:graphics_element
local TextBox, id = e.complete(true)
return TextBox, id
end end
return textbox

View File

@@ -18,14 +18,14 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new tiling box -- Create a new tiling box element.
---@param args tiling_args ---@param args tiling_args
---@return graphics_element element, element_id id ---@return Tiling element, element_id id
local function tiling(args) return function (args)
element.assert(type(args.fill_c) == "table", "fill_c is a required field") element.assert(type(args.fill_c) == "table", "fill_c is a required field")
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
local fill_a = args.fill_c.blit_a local fill_a = args.fill_c.blit_a
local fill_b = args.fill_c.blit_b local fill_b = args.fill_c.blit_b
@@ -52,7 +52,7 @@ local function tiling(args)
element.assert(start_x <= inner_width, "start_x > inner_width") element.assert(start_x <= inner_width, "start_x > inner_width")
element.assert(start_y <= inner_height, "start_y > inner_height") element.assert(start_y <= inner_height, "start_y > inner_height")
-- draw tiling box -- draw the tiling box
function e.redraw() function e.redraw()
local alternator = true local alternator = true
@@ -86,10 +86,8 @@ local function tiling(args)
end end
end end
-- initial draw ---@class Tiling:graphics_element
e.redraw() local Tiling, id = e.complete(true)
return e.complete() return Tiling, id
end end
return tiling

View File

@@ -12,10 +12,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new waiting animation element -- Create a new waiting animation element.
---@param args waiting_args ---@param args waiting_args
---@return graphics_element element, element_id id ---@return Waiting element, element_id id
local function waiting(args) return function (args)
local state = 0 local state = 0
local run_animation = false local run_animation = false
@@ -23,7 +23,7 @@ local function waiting(args)
args.height = 3 args.height = 3
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
local blit_fg = e.fg_bg.blit_fgd local blit_fg = e.fg_bg.blit_fgd
local blit_bg = e.fg_bg.blit_bkg local blit_bg = e.fg_bg.blit_bkg
@@ -103,7 +103,8 @@ local function waiting(args)
e.start_anim() e.start_anim()
return e.complete() ---@class Waiting:graphics_element
end local Waiting, id = e.complete()
return waiting return Waiting, id
end

View File

@@ -20,10 +20,10 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new app button -- Create a new app icon style button control element, like on a mobile device.
---@param args app_button_args ---@param args app_button_args
---@return graphics_element element, element_id id ---@return App element, element_id id
local function app_button(args) return function (args)
element.assert(type(args.text) == "string", "text is a required field") 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.title) == "string", "title is a required field")
element.assert(type(args.callback) == "function", "callback is a required field") element.assert(type(args.callback) == "function", "callback is a required field")
@@ -33,7 +33,7 @@ local function app_button(args)
args.width = 7 args.width = 7
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
-- draw the app button -- draw the app button
local function draw() local function draw()
@@ -123,10 +123,8 @@ local function app_button(args)
draw() draw()
end end
-- initial draw ---@class App:graphics_element
e.redraw() local App, id = e.complete(true)
return e.complete() return App, id
end end
return app_button

View File

@@ -6,6 +6,7 @@ local element = require("graphics.element")
---@class checkbox_args ---@class checkbox_args
---@field label string checkbox text ---@field label string checkbox text
---@field box_fg_bg cpair colors for checkbox ---@field box_fg_bg cpair colors for checkbox
---@field disable_fg_bg? cpair text colors when disabled
---@field default? boolean default value ---@field default? boolean default value
---@field callback? function function to call on press ---@field callback? function function to call on press
---@field parent graphics_element ---@field parent graphics_element
@@ -15,10 +16,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new checkbox control -- Create a new checkbox control element.
---@param args checkbox_args ---@param args checkbox_args
---@return graphics_element element, element_id id ---@return Checkbox element, element_id id
local function checkbox(args) return function (args)
element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.box_fg_bg) == "table", "box_fg_bg is a required field") element.assert(type(args.box_fg_bg) == "table", "box_fg_bg is a required field")
@@ -27,7 +28,7 @@ local function checkbox(args)
args.width = 2 + string.len(args.label) args.width = 2 + string.len(args.label)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = args.default == true e.value = args.default == true
@@ -35,20 +36,27 @@ local function checkbox(args)
local function draw() local function draw()
e.w_set_cur(1, 1) e.w_set_cur(1, 1)
local fgd, bkg = args.box_fg_bg.fgd, args.box_fg_bg.bkg
if (not e.enabled) and type(args.disable_fg_bg) == "table" then
fgd = args.disable_fg_bg.bkg
bkg = args.disable_fg_bg.fgd
end
if e.value then if e.value then
-- show as selected -- show as selected
e.w_set_fgd(args.box_fg_bg.bkg) e.w_set_fgd(bkg)
e.w_set_bkg(args.box_fg_bg.fgd) e.w_set_bkg(fgd)
e.w_write("\x88") e.w_write("\x88")
e.w_set_fgd(args.box_fg_bg.fgd) e.w_set_fgd(fgd)
e.w_set_bkg(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
e.w_write("\x95") e.w_write("\x95")
else else
-- show as unselected -- show as unselected
e.w_set_fgd(e.fg_bg.bkg) e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(args.box_fg_bg.bkg) e.w_set_bkg(bkg)
e.w_write("\x88") e.w_write("\x88")
e.w_set_fgd(args.box_fg_bg.bkg) e.w_set_fgd(bkg)
e.w_set_bkg(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
e.w_write("\x95") e.w_write("\x95")
end end
@@ -57,16 +65,18 @@ local function checkbox(args)
-- write label text -- write label text
local function draw_label() local function draw_label()
if e.enabled and e.is_focused() then 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_fgd(e.fg_bg.bkg)
e.w_set_bkg(e.fg_bg.fgd) e.w_set_bkg(e.fg_bg.fgd)
e.w_write(args.label) elseif (not e.enabled) and type(args.disable_fg_bg) == "table" then
e.w_set_fgd(args.disable_fg_bg.fgd)
e.w_set_bkg(args.disable_fg_bg.bkg)
else else
e.w_set_cur(3, 1)
e.w_set_fgd(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
e.w_write(args.label)
end end
e.w_set_cur(3, 1)
e.w_write(args.label)
end end
-- handle mouse interaction -- handle mouse interaction
@@ -98,24 +108,22 @@ local function checkbox(args)
draw() draw()
end 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 -- element redraw
function e.redraw() function e.redraw()
draw() draw()
draw_label() draw_label()
end end
-- initial draw -- handle focus
e.redraw() e.on_focused = draw_label
e.on_unfocused = draw_label
return e.complete() -- handle enable
e.on_enabled = e.redraw
e.on_disabled = e.redraw
---@class Checkbox:graphics_element
local Checkbox, id = e.complete(true)
return Checkbox, id
end end
return checkbox

View File

@@ -10,6 +10,7 @@ local element = require("graphics.element")
---@field accent color accent color for hazard border ---@field accent color accent color for hazard border
---@field dis_colors? cpair text color and border color when disabled ---@field dis_colors? cpair text color and border color when disabled
---@field callback function function to call on touch ---@field callback function function to call on touch
---@field timeout? integer override for the default 1.5 second timeout, in seconds
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
@@ -17,10 +18,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new hazard button -- Create a new hazard button control element.
---@param args hazard_button_args ---@param args hazard_button_args
---@return graphics_element element, element_id id ---@return HazardButton element, element_id id
local function hazard_button(args) return function (args)
element.assert(type(args.text) == "string", "text is a required field") element.assert(type(args.text) == "string", "text is a required field")
element.assert(type(args.accent) == "number", "accent is a required field") element.assert(type(args.accent) == "number", "accent is a required field")
element.assert(type(args.callback) == "function", "callback is a required field") element.assert(type(args.callback) == "function", "callback is a required field")
@@ -28,8 +29,10 @@ local function hazard_button(args)
args.height = 3 args.height = 3
args.width = string.len(args.text) + 4 args.width = string.len(args.text) + 4
local timeout = args.timeout or 1.5
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
-- draw border -- draw border
---@param accent color accent color ---@param accent color accent color
@@ -149,20 +152,13 @@ local function hazard_button(args)
tcd.abort(on_success) tcd.abort(on_success)
tcd.abort(on_failure) tcd.abort(on_failure)
-- 1.5 second timeout -- operation timeout animation
tcd.dispatch(1.5, on_timeout) tcd.dispatch(timeout, on_timeout)
args.callback() args.callback()
end 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) -- set the value (true simulates pressing the button)
---@param val boolean new value ---@param val boolean new value
function e.set_value(val) function e.set_value(val)
@@ -195,10 +191,15 @@ local function hazard_button(args)
draw_border(args.accent) draw_border(args.accent)
end end
-- initial draw ---@class HazardButton:graphics_element
e.redraw() local HazardButton, id = e.complete(true)
return e.complete() -- callback for request response
---@param success boolean
function HazardButton.on_response(success)
tcd.abort(on_timeout)
if success then on_success() else on_failure(0) end
end
return HazardButton, id
end end
return hazard_button

View File

@@ -25,10 +25,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new multi button (latch selection, exclusively one button at a time) -- Create a new multi button control element (latch selection, exclusively one button at a time).
---@param args multi_button_args ---@param args multi_button_args
---@return graphics_element element, element_id id ---@return MultiButton element, element_id id
local function multi_button(args) return function (args)
element.assert(type(args.options) == "table", "options is a required field") element.assert(type(args.options) == "table", "options is a required field")
element.assert(#args.options > 0, "at least one option is required") 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.callback) == "function", "callback is a required field")
@@ -52,7 +52,7 @@ local function multi_button(args)
args.width = (button_width * #args.options) + #args.options + 1 args.width = (button_width * #args.options) + #args.options + 1
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
-- button state (convert nil to 1 if missing) -- button state (convert nil to 1 if missing)
e.value = args.default or 1 e.value = args.default or 1
@@ -126,10 +126,8 @@ local function multi_button(args)
e.redraw() e.redraw()
end end
-- initial draw ---@class MultiButton:graphics_element
e.redraw() local MultiButton, id = e.complete(true)
return e.complete() return MultiButton, id
end end
return multi_button

View File

@@ -20,10 +20,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new spinbox control (minimum value is 0) -- Create a new spinbox control element (minimum value is 0).
---@param args spinbox_args ---@param args spinbox_args
---@return graphics_element element, element_id id ---@return NumericSpinbox element, element_id id
local function spinbox(args) return function (args)
-- properties -- properties
local digits = {} local digits = {}
local wn_prec = args.whole_num_precision local wn_prec = args.whole_num_precision
@@ -51,7 +51,7 @@ local function spinbox(args)
args.height = 3 args.height = 3
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
-- set initial value -- set initial value
e.value = args.default or 0 e.value = args.default or 0
@@ -179,10 +179,8 @@ local function spinbox(args)
draw_arrows(util.trinary(e.enabled, args.arrow_fg_bg.fgd, args.arrow_disable or colors.lightGray)) draw_arrows(util.trinary(e.enabled, args.arrow_fg_bg.fgd, args.arrow_disable or colors.lightGray))
end end
-- initial draw ---@class NumericSpinbox:graphics_element
e.redraw() local NumericSpinbox, id = e.complete(true)
return e.complete() return NumericSpinbox, id
end end
return spinbox

View File

@@ -1,6 +1,7 @@
-- Button Graphics Element -- Button Graphics Element
local tcd = require("scada-common.tcd") local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
@@ -21,14 +22,13 @@ local KEY_CLICK = core.events.KEY_CLICK
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted ---@field y? integer auto incremented if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new push button -- Create a new push button control element.
---@param args push_button_args ---@param args push_button_args
---@return graphics_element element, element_id id ---@return PushButton element, element_id id
local function push_button(args) return function (args)
element.assert(type(args.text) == "string", "text is a required field") 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.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") element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
@@ -38,29 +38,40 @@ local function push_button(args)
-- set automatic settings -- set automatic settings
args.can_focus = true args.can_focus = true
args.height = 1
args.min_width = args.min_width or 0 args.min_width = args.min_width or 0
args.width = math.max(text_width, args.min_width) args.width = math.max(text_width, args.min_width)
-- create new graphics element base object -- provide a constraint condition to element creation to prefer a single line button
local e = element.new(args) ---@param frame graphics_frame
local function constrain(frame)
local h_pad = 1 return frame.w, math.max(1, #util.strwrap(args.text, frame.w))
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 end
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]], constrain)
local text_lines = util.strwrap(args.text, e.frame.w)
-- draw the button -- draw the button
function e.redraw() function e.redraw()
e.window.clear() e.window.clear()
-- write the button text for i = 1, #text_lines do
e.w_set_cur(h_pad, v_pad) if i > e.frame.h then break end
e.w_write(args.text)
local len = string.len(text_lines[i])
-- use cursor position to align this line
if alignment == ALIGN.CENTER then
e.w_set_cur(math.floor((e.frame.w - len) / 2) + 1, i)
elseif alignment == ALIGN.RIGHT then
e.w_set_cur((e.frame.w - len) + 1, i)
else
e.w_set_cur(1, i)
end
e.w_write(text_lines[i])
end
end end
-- draw the button as pressed (if active_fg_bg set) -- draw the button as pressed (if active_fg_bg set)
@@ -109,7 +120,9 @@ local function push_button(args)
if event.type == KEY_CLICK.DOWN then if event.type == KEY_CLICK.DOWN then
if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then
args.callback() args.callback()
e.defocus() -- visualize click without unfocusing
show_unpressed()
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_pressed) end
end end
end end
end end
@@ -144,10 +157,8 @@ local function push_button(args)
e.on_focused = show_pressed e.on_focused = show_pressed
e.on_unfocused = show_unpressed e.on_unfocused = show_unpressed
-- initial draw ---@class PushButton:graphics_element
e.redraw() local PushButton, id = e.complete(true)
return e.complete() return PushButton, id
end end
return push_button

View File

@@ -23,10 +23,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new 2D radio button list (latch selection, exclusively one color at a time) -- Create a new 2-dimensional (rows and columns of options) radio button list control element (latch selection, exclusively one color at a time).
---@param args radio_2d_args ---@param args radio_2d_args
---@return graphics_element element, element_id id ---@return Radio2D element, element_id id
local function radio_2d_button(args) return function (args)
element.assert(type(args.options) == "table" and #args.options > 0, "options should be a table with length >= 1") 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(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((args.rows * args.columns) >= #args.options, "rows x columns size insufficient for provided number of options")
@@ -70,7 +70,7 @@ local function radio_2d_button(args)
args.height = max_rows args.height = max_rows
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
-- selected option (convert nil to 1 if missing) -- selected option (convert nil to 1 if missing)
e.value = args.default or 1 e.value = args.default or 1
@@ -194,10 +194,8 @@ local function radio_2d_button(args)
e.on_enabled = e.redraw e.on_enabled = e.redraw
e.on_disabled = e.redraw e.on_disabled = e.redraw
-- initial draw ---@class Radio2D:graphics_element
e.redraw() local Radio2D, id = e.complete(true)
return e.complete() return Radio2D, id
end end
return radio_2d_button

View File

@@ -11,6 +11,7 @@ local KEY_CLICK = core.events.KEY_CLICK
---@field options table button options ---@field options table button options
---@field radio_colors cpair radio button colors (inner & outer) ---@field radio_colors cpair radio button colors (inner & outer)
---@field select_color color color for radio button border when selected ---@field select_color color color for radio button border when selected
---@field dis_fg_bg? cpair foreground/background colors when disabled
---@field default? integer default state, defaults to options[1] ---@field default? integer default state, defaults to options[1]
---@field min_width? integer text length + 2 if omitted ---@field min_width? integer text length + 2 if omitted
---@field callback? function function to call on touch ---@field callback? function function to call on touch
@@ -21,10 +22,10 @@ local KEY_CLICK = core.events.KEY_CLICK
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new radio button list (latch selection, exclusively one button at a time) -- Create a new radio button list control element (latch selection, exclusively one button at a time).
---@param args radio_button_args ---@param args radio_button_args
---@return graphics_element element, element_id id ---@return RadioButton element, element_id id
local function radio_button(args) return function (args)
element.assert(type(args.options) == "table", "options is a required field") element.assert(type(args.options) == "table", "options is a required field")
element.assert(#args.options > 0, "at least one option is required") 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.radio_colors) == "table", "radio_colors is a required field")
@@ -49,7 +50,7 @@ local function radio_button(args)
args.height = #args.options -- one line per option args.height = #args.options -- one line per option
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
local focused_opt = 1 local focused_opt = 1
@@ -64,6 +65,10 @@ local function radio_button(args)
local inner_color = util.trinary(e.value == i, args.radio_colors.color_b, args.radio_colors.color_a) local 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) local outer_color = util.trinary(e.value == i, args.select_color, args.radio_colors.color_b)
if e.value == i and args.dis_fg_bg and not e.enabled then
outer_color = args.radio_colors.color_a
end
e.w_set_cur(1, i) e.w_set_cur(1, i)
e.w_set_fgd(inner_color) e.w_set_fgd(inner_color)
@@ -75,9 +80,14 @@ local function radio_button(args)
e.w_write("\x95") e.w_write("\x95")
-- write button text -- write button text
if i == focused_opt and e.is_focused() and e.enabled then if args.dis_fg_bg and not e.enabled then
e.w_set_fgd(e.fg_bg.bkg) e.w_set_fgd(args.dis_fg_bg.fgd)
e.w_set_bkg(e.fg_bg.fgd) e.w_set_bkg(args.dis_fg_bg.bkg)
elseif i == focused_opt and e.is_focused() then
if e.enabled then
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(e.fg_bg.fgd)
end
else else
e.w_set_fgd(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
@@ -139,10 +149,8 @@ local function radio_button(args)
e.on_enabled = e.redraw e.on_enabled = e.redraw
e.on_disabled = e.redraw e.on_disabled = e.redraw
-- initial draw ---@class RadioButton:graphics_element
e.redraw() local RadioButton, id = e.complete(true)
return e.complete() return RadioButton, id
end end
return radio_button

View File

@@ -17,14 +17,14 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new sidebar tab selector -- Create a new sidebar tab selector control element.
---@param args sidebar_args ---@param args sidebar_args
---@return graphics_element element, element_id id ---@return Sidebar element, element_id id
local function sidebar(args) return function (args)
args.width = 3 args.width = 3
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
-- default to 1st tab -- default to 1st tab
e.value = 1 e.value = 1
@@ -129,8 +129,14 @@ local function sidebar(args)
end end
-- update the sidebar navigation options -- update the sidebar navigation options
---@param items table sidebar entries ---@param items sidebar_entry[] sidebar entries
function e.on_update(items) function e.on_update(items)
---@class sidebar_entry
---@field label string
---@field tall boolean
---@field color cpair
---@field callback function|nil
local next_y = 1 local next_y = 1
tabs = {} tabs = {}
@@ -160,9 +166,8 @@ local function sidebar(args)
-- element redraw -- element redraw
e.redraw = draw e.redraw = draw
e.redraw() ---@class Sidebar:graphics_element
local Sidebar, id = e.complete(true)
return e.complete() return Sidebar, id
end end
return sidebar

View File

@@ -17,10 +17,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new switch button (latch high/low) -- Create a new latching switch button control element.
---@param args switch_button_args ---@param args switch_button_args
---@return graphics_element element, element_id id ---@return SwitchButton element, element_id id
local function switch_button(args) return function (args)
element.assert(type(args.text) == "string", "text is a required field") 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.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.active_fg_bg) == "table", "active_fg_bg is a required field")
@@ -33,7 +33,7 @@ local function switch_button(args)
args.width = math.max(text_width, args.min_width) args.width = math.max(text_width, args.min_width)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = args.default or false e.value = args.default or false
@@ -72,10 +72,8 @@ local function switch_button(args)
e.redraw() e.redraw()
end end
-- initial draw ---@class SwitchButton:graphics_element
e.redraw() local SwitchButton, id = e.complete(true)
return e.complete() return SwitchButton, id
end end
return switch_button

View File

@@ -23,10 +23,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new tab selector -- Create a new tab selector control element.
---@param args tabbar_args ---@param args tabbar_args
---@return graphics_element element, element_id id ---@return TabBar element, element_id id
local function tabbar(args) return function (args)
element.assert(type(args.tabs) == "table", "tabs is a required field") element.assert(type(args.tabs) == "table", "tabs is a required field")
element.assert(#args.tabs > 0, "at least one tab is required") 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.callback) == "function", "callback is a required field")
@@ -46,7 +46,7 @@ local function tabbar(args)
local button_width = math.max(max_width, args.min_width or 0) local button_width = math.max(max_width, args.min_width or 0)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
element.assert(e.frame.w >= (button_width * #args.tabs), "width insufficent to display all tabs") element.assert(e.frame.w >= (button_width * #args.tabs), "width insufficent to display all tabs")
@@ -120,10 +120,8 @@ local function tabbar(args)
e.redraw() e.redraw()
end end
-- initial draw ---@class TabBar:graphics_element
e.redraw() local TabBar, id = e.complete(true)
return e.complete() return TabBar, id
end end
return tabbar

View File

@@ -17,6 +17,7 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@field max_frac_digits? integer maximum number of fractional digits, enforced on unfocus ---@field max_frac_digits? integer maximum number of fractional digits, enforced on unfocus
---@field allow_decimal? boolean true to allow decimals ---@field allow_decimal? boolean true to allow decimals
---@field allow_negative? boolean true to allow negative numbers ---@field allow_negative? boolean true to allow negative numbers
---@field align_right? boolean true to align right while unfocused
---@field dis_fg_bg? cpair foreground/background colors when disabled ---@field dis_fg_bg? cpair foreground/background colors when disabled
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
@@ -26,10 +27,10 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new numeric entry field -- Create a new numeric entry field.
---@param args number_field_args ---@param args number_field_args
---@return graphics_element element, element_id id ---@return NumberField element, element_id id
local function number_field(args) return function (args)
element.assert(args.max_int_digits == nil or (util.is_int(args.max_int_digits) and args.max_int_digits > 0), "max_int_digits must be an integer greater than zero if supplied") element.assert(args.max_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") element.assert(args.max_frac_digits == nil or (util.is_int(args.max_frac_digits) and args.max_frac_digits > 0), "max_frac_digits must be an integer greater than zero if supplied")
@@ -37,17 +38,56 @@ local function number_field(args)
args.can_focus = true args.can_focus = true
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
local has_decimal = false local has_decimal = false
args.max_chars = args.max_chars or e.frame.w args.max_chars = args.max_chars or e.frame.w
-- determine the format to convert the number to a string
local format = "%d"
if args.allow_decimal then
if args.max_frac_digits then
format = "%."..args.max_frac_digits.."f"
else format = "%f" end
end
-- set the value to a formatted numeric string<br>
-- trims trailing zeros from floating point numbers
---@param num number
local function _set_value(num)
local str = util.sprintf(format, num)
if args.allow_decimal then
local found_nonzero = false
local str_table = {}
for i = #str, 1, -1 do
local c = string.sub(str, i, i)
if found_nonzero then
str_table[i] = c
else
if c == "." then
found_nonzero = true
elseif c ~= "0" then
str_table[i] = c
found_nonzero = true
end
end
end
e.value = table.concat(str_table)
else
e.value = str
end
end
-- set initial value -- set initial value
e.value = "" .. (args.default or 0) _set_value(args.default or 0)
-- make an interactive field manager -- make an interactive field manager
local ifield = core.new_ifield(e, args.max_chars, args.fg_bg, args.dis_fg_bg) local ifield = core.new_ifield(e, args.max_chars, args.fg_bg, args.dis_fg_bg, args.align_right)
-- handle mouse interaction -- handle mouse interaction
---@param event mouse_interaction mouse event ---@param event mouse_interaction mouse event
@@ -55,10 +95,16 @@ local function number_field(args)
-- only handle if on an increment or decrement arrow -- only handle if on an increment or decrement arrow
if e.enabled and e.in_frame_bounds(event.current.x, event.current.y) then if e.enabled and e.in_frame_bounds(event.current.x, event.current.y) then
if core.events.was_clicked(event.type) then if core.events.was_clicked(event.type) then
local x = event.current.x
if not e.is_focused() then
x = ifield.get_cursor_align_shift(x)
end
e.take_focus() e.take_focus()
if event.type == MOUSE_CLICK.UP then if event.type == MOUSE_CLICK.UP then
ifield.move_cursor(event.current.x) ifield.move_cursor(x)
end end
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
ifield.select_all() ifield.select_all()
@@ -100,7 +146,17 @@ local function number_field(args)
-- set the value (must be a number) -- set the value (must be a number)
---@param val number number to show ---@param val number number to show
function e.set_value(val) function e.set_value(val)
if tonumber(val) then ifield.set_value("" .. tonumber(val)) end local num, max, min = tonumber(val), tonumber(args.max), tonumber(args.min)
if max and num > max then
_set_value(max)
elseif min and num < min then
_set_value(min)
elseif num then
_set_value(num)
end
ifield.set_value(e.value)
end end
-- set minimum input value -- set minimum input value
@@ -129,11 +185,9 @@ local function number_field(args)
-- handle unfocused -- handle unfocused
function e.on_unfocused() function e.on_unfocused()
local val = tonumber(e.value) local val, max, min = tonumber(e.value), tonumber(args.max), tonumber(args.min)
local max = tonumber(args.max)
local min = tonumber(args.min)
if type(val) == "number" then if val then
if args.max_int_digits or args.max_frac_digits then if args.max_int_digits or args.max_frac_digits then
local str = e.value local str = e.value
local ceil = false local ceil = false
@@ -162,17 +216,17 @@ local function number_field(args)
if parts[2] then parts[2] = "." .. parts[2] else parts[2] = "" end if parts[2] then parts[2] = "." .. parts[2] else parts[2] = "" end
val = tonumber((parts[1] or "") .. parts[2]) val = tonumber((parts[1] or "") .. parts[2]) or 0
end end
if type(args.max) == "number" and val > max then if max and val > max then
e.value = "" .. max _set_value(max)
ifield.nav_start() ifield.nav_start()
elseif type(args.min) == "number" and val < min then elseif min and val < min then
e.value = "" .. min _set_value(min)
ifield.nav_start() ifield.nav_start()
else else
e.value = "" .. val _set_value(val)
ifield.nav_end() ifield.nav_end()
end end
else else
@@ -188,10 +242,14 @@ local function number_field(args)
e.on_disabled = ifield.show e.on_disabled = ifield.show
e.redraw = ifield.show e.redraw = ifield.show
-- initial draw ---@class NumberField:graphics_element
e.redraw() local NumberField, id = e.complete(true)
return e.complete() -- get the numeric value of this field
---@return number value the value, or 0 if not a valid number
function NumberField.get_numeric()
return tonumber(e.value) or 0
end
return NumberField, id
end end
return number_field

View File

@@ -19,15 +19,15 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new text entry field -- Create a new text entry field.
---@param args text_field_args ---@param args text_field_args
---@return graphics_element element, element_id id, function censor_ctl ---@return TextField element, element_id id
local function text_field(args) return function (args)
args.height = 1 args.height = 1
args.can_focus = true args.can_focus = true
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
-- set initial value -- set initial value
e.value = args.value or "" e.value = args.value or ""
@@ -95,11 +95,10 @@ local function text_field(args)
e.on_disabled = ifield.show e.on_disabled = ifield.show
e.redraw = ifield.show e.redraw = ifield.show
-- initial draw ---@class TextField:graphics_element
e.redraw() local TextField, id = e.complete(true)
local elem, id = e.complete() TextField.censor = ifield.censor
return elem, id, ifield.censor
return TextField, id
end end
return text_field

View File

@@ -20,11 +20,11 @@ local flasher = require("graphics.flasher")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new alarm indicator light -- Create a new alarm indicator light element.
---@nodiscard ---@nodiscard
---@param args alarm_indicator_light ---@param args alarm_indicator_light
---@return graphics_element element, element_id id ---@return AlarmLight element, element_id id
local function alarm_indicator_light(args) return function (args)
element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.c1) == "number", "c1 is a required field") element.assert(type(args.c1) == "number", "c1 is a required field")
element.assert(type(args.c2) == "number", "c2 is a required field") element.assert(type(args.c2) == "number", "c2 is a required field")
@@ -49,7 +49,7 @@ local function alarm_indicator_light(args)
local c3 = colors.toBlit(args.c3) local c3 = colors.toBlit(args.c3)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = 1 e.value = 1
@@ -113,10 +113,8 @@ local function alarm_indicator_light(args)
e.w_write(args.label) e.w_write(args.label)
end end
-- initial draw ---@class AlarmLight:graphics_element
e.redraw() local AlarmLight, id = e.complete(true)
return e.complete() return AlarmLight, id
end end
return alarm_indicator_light

View File

@@ -13,11 +13,11 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted ---@field y? integer auto incremented if omitted
-- new core map box -- Create a new core map diagram indicator element.
---@nodiscard ---@nodiscard
---@param args core_map_args ---@param args core_map_args
---@return graphics_element element, element_id id ---@return CoreMap element, element_id id
local function core_map(args) return function (args)
element.assert(util.is_int(args.reactor_l), "reactor_l is a required field") element.assert(util.is_int(args.reactor_l), "reactor_l is a required field")
element.assert(util.is_int(args.reactor_w), "reactor_w is a required field") element.assert(util.is_int(args.reactor_w), "reactor_w is a required field")
@@ -29,7 +29,7 @@ local function core_map(args)
args.fg_bg = core.cpair(args.parent.get_fg_bg().fgd, colors.gray) args.fg_bg = core.cpair(args.parent.get_fg_bg().fgd, colors.gray)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = 0 e.value = 0
@@ -165,10 +165,8 @@ local function core_map(args)
draw_core(e.value) draw_core(e.value)
end end
-- initial draw ---@class CoreMap:graphics_element
e.redraw() local CoreMap, id = e.complete(true)
return e.complete() return CoreMap, id
end end
return core_map

View File

@@ -19,11 +19,11 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new data indicator -- Create new data indicator element.
---@nodiscard ---@nodiscard
---@param args data_indicator_args ---@param args data_indicator_args
---@return graphics_element element, element_id id ---@return DataIndicator element, element_id id
local function data(args) return function (args)
element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.format) == "string", "format is a required field") element.assert(type(args.format) == "string", "format is a required field")
element.assert(args.value ~= nil, "value is a required field") element.assert(args.value ~= nil, "value is a required field")
@@ -32,7 +32,7 @@ local function data(args)
args.height = 1 args.height = 1
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = args.value e.value = args.value
@@ -94,10 +94,8 @@ local function data(args)
e.on_update(e.value) e.on_update(e.value)
end end
-- initial draw ---@class DataIndicator:graphics_element
e.redraw() local DataIndicator, id = e.complete(true)
return e.complete() return DataIndicator, id
end end
return data

View File

@@ -17,13 +17,13 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new horizontal bar -- Create a new horizontal fill bar indicator element.
---@nodiscard ---@nodiscard
---@param args hbar_args ---@param args hbar_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function hbar(args) return function (args)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = 0.0 e.value = 0.0
@@ -119,10 +119,8 @@ local function hbar(args)
e.on_update(e.value) e.on_update(e.value)
end end
-- initial draw ---@class HorizontalBar:graphics_element
e.redraw() local HorizontalBar, id = e.complete(true)
return e.complete() return HorizontalBar, id
end end
return hbar

View File

@@ -18,11 +18,11 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new icon indicator -- Create a new icon indicator element.
---@nodiscard ---@nodiscard
---@param args icon_indicator_args ---@param args icon_indicator_args
---@return graphics_element element, element_id id ---@return IconIndicator element, element_id id
local function icon(args) return function (args)
element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.states) == "table", "states is a required field") element.assert(type(args.states) == "table", "states is a required field")
@@ -30,7 +30,7 @@ local function icon(args)
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4 args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = args.value or 1 e.value = args.value or 1
if e.value == true then e.value = 2 end if e.value == true then e.value = 2 end
@@ -71,10 +71,8 @@ local function icon(args)
e.on_update(e.value) e.on_update(e.value)
end end
-- initial draw ---@class IconIndicator:graphics_element
e.redraw() local IconIndicator, id = e.complete(true)
return e.complete() return IconIndicator, id
end end
return icon

View File

@@ -18,11 +18,11 @@ local flasher = require("graphics.flasher")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new indicator light -- Create a new indicator light element.
---@nodiscard ---@nodiscard
---@param args indicator_light_args ---@param args indicator_light_args
---@return graphics_element element, element_id id ---@return IndicatorLight element, element_id id
local function indicator_light(args) return function (args)
element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.colors) == "table", "colors is a required field") element.assert(type(args.colors) == "table", "colors is a required field")
@@ -36,7 +36,7 @@ local function indicator_light(args)
local flash_on = true local flash_on = true
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = false e.value = false
@@ -93,10 +93,8 @@ local function indicator_light(args)
e.w_write(args.label) e.w_write(args.label)
end end
-- initial draw ---@class IndicatorLight:graphics_element
e.redraw() local IndicatorLight, id = e.complete(true)
return e.complete() return IndicatorLight, id
end end
return indicator_light

View File

@@ -18,11 +18,11 @@ local flasher = require("graphics.flasher")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new indicator LED -- Create a new indicator LED element.
---@nodiscard ---@nodiscard
---@param args indicator_led_args ---@param args indicator_led_args
---@return graphics_element element, element_id id ---@return LED element, element_id id
local function indicator_led(args) return function (args)
element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.colors) == "table", "colors is a required field") element.assert(type(args.colors) == "table", "colors is a required field")
@@ -36,7 +36,7 @@ local function indicator_led(args)
local flash_on = true local flash_on = true
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = false e.value = false
@@ -95,10 +95,8 @@ local function indicator_led(args)
end end
end end
-- initial draw ---@class LED:graphics_element
e.redraw() local LED, id = e.complete(true)
return e.complete() return LED, id
end end
return indicator_led

View File

@@ -20,11 +20,12 @@ local flasher = require("graphics.flasher")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new dual LED indicator light -- Create a new three-state LED indicator light. Two "active" states (colors c1 and c2) and an inactive state (off).<br>
-- Values: 1 = off, 2 = c1, 3 = c2
---@nodiscard ---@nodiscard
---@param args indicator_led_pair_args ---@param args indicator_led_pair_args
---@return graphics_element element, element_id id ---@return LEDPair element, element_id id
local function indicator_led_pair(args) return function (args)
element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.off) == "number", "off is a required field") element.assert(type(args.off) == "number", "off is a required field")
element.assert(type(args.c1) == "number", "c1 is a required field") element.assert(type(args.c1) == "number", "c1 is a required field")
@@ -44,7 +45,7 @@ local function indicator_led_pair(args)
local c2 = colors.toBlit(args.c2) local c2 = colors.toBlit(args.c2)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = 1 e.value = 1
@@ -104,10 +105,8 @@ local function indicator_led_pair(args)
end end
end end
-- initial draw ---@class LEDPair:graphics_element
e.redraw() local LEDPair, id = e.complete(true)
return e.complete() return LEDPair, id
end end
return indicator_led_pair

View File

@@ -6,6 +6,7 @@ local element = require("graphics.element")
---@class power_indicator_args ---@class power_indicator_args
---@field label string indicator label ---@field label string indicator label
---@field unit string energy unit
---@field format string power format override (lua string format) ---@field format string power format override (lua string format)
---@field rate boolean? whether to append /t to the end (power per tick) ---@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 lu_colors? cpair label foreground color (a), unit foreground color (b)
@@ -18,18 +19,20 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new power indicator -- Create a new power indicator. Variant of a data indicator with dynamic energy units.
---@nodiscard ---@nodiscard
---@param args power_indicator_args ---@param args power_indicator_args
---@return graphics_element element, element_id id ---@return PowerIndicator element, element_id id
local function power(args) return function (args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.unit) == "string", "unit is a required field")
element.assert(type(args.value) == "number", "value is a required field") element.assert(type(args.value) == "number", "value is a required field")
element.assert(util.is_int(args.width), "width is a required field") element.assert(util.is_int(args.width), "width is a required field")
args.height = 1 args.height = 1
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = args.value e.value = args.value
@@ -40,7 +43,7 @@ local function power(args)
function e.on_update(value) function e.on_update(value)
e.value = value e.value = value
local data_str, unit = util.power_format(value, false, args.format) local data_str, unit = util.power_format(value, args.unit, false, args.format)
-- write data -- write data
e.w_set_cur(data_start, 1) e.w_set_cur(data_start, 1)
@@ -53,14 +56,13 @@ local function power(args)
end end
-- append per tick if rate is set -- 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 if args.rate == true then
unit = unit .. "/t" unit = unit .. "/t"
if unit == "FE/t" then unit = "FE/t " end
else
if unit == "FE" then unit = "FE " end
end end
-- add space to unit so we don't end up with something like FEE after having kFE
unit = util.strminw(unit, 5)
e.w_write(" " .. unit) e.w_write(" " .. unit)
end end
@@ -80,10 +82,8 @@ local function power(args)
e.on_update(e.value) e.on_update(e.value)
end end
-- initial draw ---@class PowerIndicator:graphics_element
e.redraw() local PowerIndicator, id = e.complete(true)
return e.complete() return PowerIndicator, id
end end
return power

View File

@@ -13,11 +13,11 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new RGB LED indicator light -- Create a new RGB LED indicator light element.
---@nodiscard ---@nodiscard
---@param args indicator_led_rgb_args ---@param args indicator_led_rgb_args
---@return graphics_element element, element_id id ---@return RGBLED element, element_id id
local function indicator_led_rgb(args) return function (args)
element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.colors) == "table", "colors is a required field") element.assert(type(args.colors) == "table", "colors is a required field")
@@ -25,7 +25,7 @@ local function indicator_led_rgb(args)
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2 args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = 1 e.value = 1
@@ -52,10 +52,8 @@ local function indicator_led_rgb(args)
end end
end end
-- initial draw ---@class RGBLED:graphics_element
e.redraw() local RGBLED, id = e.complete(true)
return e.complete() return RGBLED, id
end end
return indicator_led_rgb

View File

@@ -19,11 +19,11 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new radiation indicator -- Create a new radiation indicator element. Variant of a data indicator using dynamic Sievert unit precision.
---@nodiscard ---@nodiscard
---@param args rad_indicator_args ---@param args rad_indicator_args
---@return graphics_element element, element_id id ---@return RadIndicator element, element_id id
local function rad(args) return function (args)
element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.format) == "string", "format is a required field") element.assert(type(args.format) == "string", "format is a required field")
element.assert(util.is_int(args.width), "width is a required field") element.assert(util.is_int(args.width), "width is a required field")
@@ -31,7 +31,7 @@ local function rad(args)
args.height = 1 args.height = 1
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = args.value or types.new_zero_radiation_reading() e.value = args.value or types.new_zero_radiation_reading()
@@ -83,10 +83,8 @@ local function rad(args)
e.on_update(e.value) e.on_update(e.value)
end end
-- initial draw ---@class RadIndicator:graphics_element
e.redraw() local RadIndicator, id = e.complete(true)
return e.complete() return RadIndicator, id
end end
return rad

View File

@@ -15,16 +15,16 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors (foreground is used for high signal quality) ---@field fg_bg? cpair foreground/background colors (foreground is used for high signal quality)
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new signal bar -- Create a new signal bar indicator element.
---@nodiscard ---@nodiscard
---@param args signal_bar_args ---@param args signal_bar_args
---@return graphics_element element, element_id id ---@return SignalBar element, element_id id
local function signal_bar(args) return function (args)
args.height = 1 args.height = 1
args.width = util.trinary(args.compact, 1, 2) args.width = util.trinary(args.compact, 1, 2)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = 0 e.value = 0
@@ -76,10 +76,8 @@ local function signal_bar(args)
end end
end end
-- initial draw ---@class SignalBar:graphics_element
e.redraw() local SignalBar, id = e.complete(true)
return e.complete() return SignalBar, id
end end
return signal_bar

View File

@@ -20,11 +20,11 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new state indicator -- Create a new state indicator element.
---@nodiscard ---@nodiscard
---@param args state_indicator_args ---@param args state_indicator_args
---@return graphics_element element, element_id id ---@return StateIndicator element, element_id id
local function state_indicator(args) return function (args)
element.assert(type(args.states) == "table", "states is a required field") element.assert(type(args.states) == "table", "states is a required field")
if util.is_int(args.height) then if util.is_int(args.height) then
@@ -52,7 +52,7 @@ local function state_indicator(args)
end end
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = args.value or 1 e.value = args.value or 1
@@ -74,10 +74,8 @@ local function state_indicator(args)
---@param val integer indicator state ---@param val integer indicator state
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- initial draw ---@class StateIndicator:graphics_element
e.redraw() local StateIndicator, id = e.complete(true)
return e.complete() return StateIndicator, id
end end
return state_indicator

View File

@@ -20,11 +20,11 @@ local flasher = require("graphics.flasher")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new tri-state indicator light -- Create a new tri-state indicator light element.
---@nodiscard ---@nodiscard
---@param args tristate_indicator_light_args ---@param args tristate_indicator_light_args
---@return graphics_element element, element_id id ---@return TriIndicatorLight element, element_id id
local function tristate_indicator_light(args) return function (args)
element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.c1) == "number", "c1 is a required field") element.assert(type(args.c1) == "number", "c1 is a required field")
element.assert(type(args.c2) == "number", "c2 is a required field") element.assert(type(args.c2) == "number", "c2 is a required field")
@@ -38,7 +38,7 @@ local function tristate_indicator_light(args)
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = 1 e.value = 1
@@ -102,10 +102,8 @@ local function tristate_indicator_light(args)
e.w_write(args.label) e.w_write(args.label)
end end
-- initial draw ---@class TriIndicatorLight:graphics_element
e.redraw() local TriIndicatorLight, id = e.complete(true)
return e.complete() return TriIndicatorLight, id
end end
return tristate_indicator_light

View File

@@ -15,13 +15,13 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new vertical bar -- Create a new vertical fill bar indicator element.
---@nodiscard ---@nodiscard
---@param args vbar_args ---@param args vbar_args
---@return graphics_element element, element_id id ---@return VerticalBar element, element_id id
local function vbar(args) return function (args)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args --[[@as graphics_args]])
e.value = 0.0 e.value = 0.0
@@ -98,10 +98,8 @@ local function vbar(args)
e.redraw() e.redraw()
end end
-- initial draw ---@class VerticalBar:graphics_element
e.redraw() local VerticalBar, id = e.complete(true)
return e.complete() return VerticalBar, id
end end
return vbar

View File

@@ -18,7 +18,7 @@ local PERIOD = {
flasher.PERIOD = PERIOD flasher.PERIOD = PERIOD
local active = false local active = false
local registry = { {}, {}, {} } -- one registry table per period local registry = { {}, {}, {} } ---@type [ function[], function[], function [] ] one registry table per period
local callback_counter = 0 local callback_counter = 0
-- blink registered indicators<br> -- blink registered indicators<br>

432
pocket/config/system.lua Normal file
View File

@@ -0,0 +1,432 @@
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local Checkbox = require("graphics.elements.controls.Checkbox")
local PushButton = require("graphics.elements.controls.PushButton")
local RadioButton = require("graphics.elements.controls.RadioButton")
local NumberField = require("graphics.elements.form.NumberField")
local TextField = require("graphics.elements.form.TextField")
local tri = util.trinary
local cpair = core.cpair
local RIGHT = core.ALIGN.RIGHT
local self = {
importing_legacy = false,
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type PushButton
auth_key_textbox = nil, ---@type TextBox
auth_key_value = ""
}
local system = {}
-- create the system configuration view
---@param tool_ctl _pkt_cfg_tool_ctl
---@param main_pane MultiPane
---@param cfg_sys [ pkt_config, pkt_config, pkt_config, { [1]: string, [2]: string, [3]: any }[], function ]
---@param divs Div[]
---@param style { [string]: cpair }
---@param exit function
function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
local settings_cfg, ini_cfg, tmp_cfg, fields, load_settings = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5]
local ui_cfg, net_cfg, log_cfg, summary = divs[1], divs[2], divs[3], divs[4]
local bw_fg_bg = style.bw_fg_bg
local g_lg_fg_bg = style.g_lg_fg_bg
local nav_fg_bg = style.nav_fg_bg
local btn_act_fg_bg = style.btn_act_fg_bg
local btn_dis_fg_bg = style.btn_dis_fg_bg
--#region Pocket UI
local ui_c_1 = Div{parent=ui_cfg,x=2,y=4,width=24}
local ui_c_2 = Div{parent=ui_cfg,x=2,y=4,width=24}
local ui_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={ui_c_1,ui_c_2}}
TextBox{parent=ui_cfg,x=1,y=2,text=" Pocket UI",fg_bg=cpair(colors.black,colors.lime)}
TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize UI options below."}
TextBox{parent=ui_c_1,y=4,text="Po/Pu Pellet Color"}
TextBox{parent=ui_c_1,x=20,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
local pellet_color = RadioButton{parent=ui_c_1,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=ui_c_1,y=8,height=4,text="In Mekanism 10.4 and later, pellet colors now match gas colors (Cyan Pu/Green Po).",fg_bg=g_lg_fg_bg}
local function submit_ui_opts()
tmp_cfg.GreenPuPellet = pellet_color.get_value() == 1
ui_pane.set_value(2)
end
PushButton{parent=ui_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=ui_c_1,x=19,y=15,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=ui_c_2,x=1,y=1,height=3,text="You may customize units below."}
TextBox{parent=ui_c_2,x=1,y=4,text="Temperature Scale"}
local temp_scale = RadioButton{parent=ui_c_2,x=1,y=5,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=ui_c_2,x=1,y=10,text="Energy Scale"}
local energy_scale = RadioButton{parent=ui_c_2,x=1,y=11,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
local function submit_ui_units()
tmp_cfg.TempScale = temp_scale.get_value()
tmp_cfg.EnergyScale = energy_scale.get_value()
main_pane.set_value(3)
end
PushButton{parent=ui_c_2,x=1,y=15,text="\x1b Back",callback=function()ui_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=ui_c_2,x=19,y=15,text="Next \x1a",callback=submit_ui_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Network
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=24}
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=24}
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=24}
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=24}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=net_c_1,x=1,y=1,text="Set network channels."}
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the named channels must be the same within a particular SCADA network.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=10,width=19,text="Coordinator Channel"}
local crd_chan = NumberField{parent=net_c_1,x=1,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=9,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"}
local pkt_chan = NumberField{parent=net_c_1,x=1,y=13,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=9,y=13,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
local chan_err = TextBox{parent=net_c_1,x=1,y=14,width=24,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_channels()
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then
tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c
net_pane.set_value(2)
chan_err.hide(true)
else chan_err.show() end
end
PushButton{parent=net_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=19,y=15,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,text="Set connection timeout."}
TextBox{parent=net_c_2,x=1,y=3,height=7,text="You generally should not need to modify this. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=11,width=19,text="Connection Timeout"}
local timeout = NumberField{parent=net_c_2,x=1,y=12,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=9,y=12,height=2,text="seconds\n(default 5)",fg_bg=g_lg_fg_bg}
local ct_err = TextBox{parent=net_c_2,x=1,y=14,width=24,text="Please set timeout.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_timeouts()
local timeout_val = tonumber(timeout.get_value())
if timeout_val ~= nil then
tmp_cfg.ConnTimeout = timeout_val
net_pane.set_value(3)
ct_err.hide(true)
else ct_err.show() end
end
PushButton{parent=net_c_2,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=19,y=15,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,text="Set the trusted range."}
TextBox{parent=net_c_3,x=1,y=3,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many blocks away.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=8,height=4,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
local range = NumberField{parent=net_c_3,x=1,y=13,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local tr_err = TextBox{parent=net_c_3,x=1,y=14,width=24,text="Set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_tr()
local range_val = tonumber(range.get_value())
if range_val ~= nil then
tmp_cfg.TrustedRange = range_val
net_pane.set_value(4)
tr_err.hide(true)
else tr_err.show() end
end
PushButton{parent=net_c_3,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=19,y=15,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_4,x=1,y=1,height=4,text="Optionally, set the facility authentication key. Do NOT use one of your passwords."}
TextBox{parent=net_c_4,x=1,y=6,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=1,y=12,text="Facility Auth Key"}
local key, _ = TextField{parent=net_c_4,x=1,y=13,max_len=64,value=ini_cfg.AuthKey,width=24,height=1,fg_bg=bw_fg_bg}
local function censor_key(enable) key.censor(tri(enable, "*", nil)) end
-- declare back first so tabbing makes sense visually
PushButton{parent=net_c_4,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local hide_key = Checkbox{parent=net_c_4,x=8,y=15,label="Hide Key",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
hide_key.set_value(true)
censor_key(true)
local key_err = TextBox{parent=net_c_4,x=1,y=14,width=24,text="Length must be > 7.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_auth()
local v = key.get_value()
if string.len(v) == 0 or string.len(v) >= 8 then
tmp_cfg.AuthKey = key.get_value()
main_pane.set_value(4)
key_err.hide(true)
else key_err.show() end
end
PushButton{parent=net_c_4,x=19,y=15,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Logging
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=24}
TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
TextBox{parent=log_c_1,x=1,y=1,text="Configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=24,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
local en_dbg = Checkbox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
TextBox{parent=log_c_1,x=3,y=11,height=4,text="This results in much larger log files. Use only as needed.",fg_bg=g_lg_fg_bg}
local path_err = TextBox{parent=log_c_1,x=1,y=14,width=24,text="Provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_log()
if path.get_value() ~= "" then
path_err.hide(true)
tmp_cfg.LogMode = mode.get_value() - 1
tmp_cfg.LogPath = path.get_value()
tmp_cfg.LogDebug = en_dbg.get_value()
tool_ctl.gen_summary(tmp_cfg)
tool_ctl.viewing_config = false
self.importing_legacy = false
tool_ctl.settings_apply.show()
main_pane.set_value(5)
else path_err.show() end
end
PushButton{parent=log_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=log_c_1,x=19,y=15,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Summary and Saving
local sum_c_1 = Div{parent=summary,x=2,y=4,width=24}
local sum_c_2 = Div{parent=summary,x=2,y=4,width=24}
local sum_c_3 = Div{parent=summary,x=2,y=4,width=24}
local sum_c_4 = Div{parent=summary,x=2,y=4,width=24}
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=11,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function back_from_summary()
if tool_ctl.viewing_config or self.importing_legacy then
main_pane.set_value(1)
tool_ctl.viewing_config = false
self.importing_legacy = false
tool_ctl.settings_apply.show()
else
main_pane.set_value(4)
end
end
---@param element graphics_element
---@param data any
local function try_set(element, data)
if data ~= nil then element.set_value(data) end
end
local function save_and_continue()
for _, field in ipairs(fields) do
local k, v = field[1], tmp_cfg[field[1]]
if v == nil then settings.unset(k) else settings.set(k, v) end
end
if settings.save("/pocket.settings") then
load_settings(settings_cfg, true)
load_settings(ini_cfg)
try_set(pellet_color, ini_cfg.GreenPuPellet)
try_set(temp_scale, ini_cfg.TempScale)
try_set(energy_scale, ini_cfg.EnergyScale)
try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(crd_chan, ini_cfg.CRD_Channel)
try_set(pkt_chan, ini_cfg.PKT_Channel)
try_set(timeout, ini_cfg.ConnTimeout)
try_set(range, ini_cfg.TrustedRange)
try_set(key, ini_cfg.AuthKey)
try_set(mode, ini_cfg.LogMode)
try_set(path, ini_cfg.LogPath)
try_set(en_dbg, ini_cfg.LogDebug)
tool_ctl.view_cfg.enable()
if self.importing_legacy then
self.importing_legacy = false
sum_pane.set_value(3)
else
sum_pane.set_value(2)
end
else
sum_pane.set_value(4)
end
end
PushButton{parent=sum_c_1,x=1,y=15,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.show_key_btn = PushButton{parent=sum_c_1,x=1,y=13,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=18,y=15,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"}
local function go_home()
main_pane.set_value(1)
net_pane.set_value(1)
sum_pane.set_value(1)
end
PushButton{parent=sum_c_2,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_2,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=sum_c_3,x=1,y=1,height=4,text="The old config.lua file will now be deleted, then the configurator will exit."}
local function delete_legacy()
fs.delete("/pocket/config.lua")
exit()
end
PushButton{parent=sum_c_3,x=1,y=15,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_3,x=19,y=15,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=sum_c_4,x=1,y=1,height=8,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
PushButton{parent=sum_c_4,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_4,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
--#endregion
--#region Tool Functions
-- load a legacy config file
function tool_ctl.load_legacy()
local config = require("pocket.config")
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
tmp_cfg.CRD_Channel = config.CRD_CHANNEL
tmp_cfg.PKT_Channel = config.PKT_CHANNEL
tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT
tmp_cfg.TrustedRange = config.TRUSTED_RANGE
tmp_cfg.AuthKey = config.AUTH_KEY or ""
tmp_cfg.LogMode = config.LOG_MODE
tmp_cfg.LogPath = config.LOG_PATH
tmp_cfg.LogDebug = config.LOG_DEBUG or false
tool_ctl.gen_summary(tmp_cfg)
sum_pane.set_value(1)
main_pane.set_value(5)
self.importing_legacy = true
end
-- expose the auth key on the summary page
function self.show_auth_key()
self.show_key_btn.disable()
self.auth_key_textbox.set_value(self.auth_key_value)
end
-- generate the summary list
---@param cfg pkt_config
function tool_ctl.gen_summary(cfg)
setting_list.remove_all()
local alternate = false
local inner_width = setting_list.get_width() - 1
self.show_key_btn.enable()
self.auth_key_value = cfg.AuthKey or "" -- to show auth key
for i = 1, #fields do
local f = fields[i]
local height = 1
local label_w = string.len(f[2])
local val_max_w = (inner_width - label_w) - 1
local raw = cfg[f[1]]
local val = util.strval(raw)
if f[1] == "AuthKey" then
val = string.rep("*", string.len(val))
elseif f[1] == "LogMode" then
val = tri(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "GreenPuPellet" then
val = tri(raw, "Green Pu/Cyan Po", "Cyan Pu/Green Po")
elseif f[1] == "TempScale" then
val = util.strval(types.TEMP_SCALE_NAMES[raw])
elseif f[1] == "EnergyScale" then
val = util.strval(types.ENERGY_SCALE_NAMES[raw])
end
if val == "nil" then val = "<not set>" end
local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end
local line = Div{parent=setting_list,height=height,fg_bg=c}
TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)}
local textbox
if height > 1 then
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end
if f[1] == "AuthKey" then self.auth_key_textbox = textbox end
end
end
--#endregion
end
return system

View File

@@ -3,83 +3,78 @@
-- --
local log = require("scada-common.log") local log = require("scada-common.log")
local tcd = require("scada-common.tcd") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local system = require("pocket.config.system")
local core = require("graphics.core") local core = require("graphics.core")
local themes = require("graphics.themes") local themes = require("graphics.themes")
local DisplayBox = require("graphics.elements.displaybox") local DisplayBox = require("graphics.elements.DisplayBox")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.listbox") local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.multipane") local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.TextBox")
local CheckBox = require("graphics.elements.controls.checkbox") local PushButton = require("graphics.elements.controls.PushButton")
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 println = util.println
local tri = util.trinary local tri = util.trinary
local cpair = core.cpair local cpair = core.cpair
local LEFT = core.ALIGN.LEFT
local CENTER = core.ALIGN.CENTER local CENTER = core.ALIGN.CENTER
local RIGHT = core.ALIGN.RIGHT
-- changes to the config data/format to let the user know -- changes to the config data/format to let the user know
local changes = {} local changes = {
{ "v0.9.2", { "Added temperature scale options" } },
{ "v0.11.3", { "Added energy scale options" } },
{ "v0.13.2", { "Added option for Po/Pu pellet green/cyan pairing" } }
}
---@class pkt_configurator ---@class pkt_configurator
local configurator = {} local configurator = {}
local style = {} local style = {}
style.root = cpair(colors.black, colors.lightGray) style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray) style.header = cpair(colors.white, colors.gray)
style.colors = themes.smooth_stone.colors style.colors = themes.smooth_stone.colors
local bw_fg_bg = cpair(colors.black, colors.white) style.bw_fg_bg = cpair(colors.black, colors.white)
local g_lg_fg_bg = cpair(colors.gray, colors.lightGray) style.g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
local nav_fg_bg = bw_fg_bg style.nav_fg_bg = style.bw_fg_bg
local btn_act_fg_bg = cpair(colors.white, colors.gray) style.btn_act_fg_bg = cpair(colors.white, colors.gray)
local dis_fg_bg = cpair(colors.lightGray,colors.white) style.btn_dis_fg_bg = cpair(colors.lightGray, colors.white)
---@class _pkt_cfg_tool_ctl
local tool_ctl = { local tool_ctl = {
launch_startup = false,
ask_config = false, ask_config = false,
has_config = false, has_config = false,
viewing_config = false, viewing_config = false,
importing_legacy = false,
view_cfg = nil, ---@type graphics_element view_cfg = nil, ---@type PushButton
settings_apply = nil, ---@type graphics_element settings_apply = nil, ---@type PushButton
set_networked = nil, ---@type function
bundled_emcool = nil, ---@type function
gen_summary = nil, ---@type function gen_summary = nil, ---@type function
show_current_cfg = nil, ---@type function load_legacy = 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 ---@class pkt_config
local tmp_cfg = { local tmp_cfg = {
GreenPuPellet = false,
TempScale = 1, ---@type TEMP_SCALE
EnergyScale = 1, ---@type ENERGY_SCALE
SVR_Channel = nil, ---@type integer SVR_Channel = nil, ---@type integer
CRD_Channel = nil, ---@type integer CRD_Channel = nil, ---@type integer
PKT_Channel = nil, ---@type integer PKT_Channel = nil, ---@type integer
ConnTimeout = nil, ---@type number ConnTimeout = nil, ---@type number
TrustedRange = nil, ---@type number TrustedRange = nil, ---@type number
AuthKey = nil, ---@type string|nil AuthKey = nil, ---@type string|nil
LogMode = 0, LogMode = 0, ---@type LOG_MODE
LogPath = "", LogPath = "",
LogDebug = false, LogDebug = false,
} }
@@ -91,6 +86,9 @@ local settings_cfg = {}
-- all settings fields, their nice names, and their default values -- all settings fields, their nice names, and their default values
local fields = { local fields = {
{ "GreenPuPellet", "Pellet Colors", false },
{ "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN },
{ "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE },
{ "SVR_Channel", "SVR Channel", 16240 }, { "SVR_Channel", "SVR Channel", 16240 },
{ "CRD_Channel", "CRD Channel", 16243 }, { "CRD_Channel", "CRD Channel", 16243 },
{ "PKT_Channel", "PKT Channel", 16244 }, { "PKT_Channel", "PKT Channel", 16244 },
@@ -116,24 +114,31 @@ local function load_settings(target, raw)
end end
-- create the config view -- create the config view
---@param display graphics_element ---@param display DisplayBox
local function config_view(display) local function config_view(display)
local bw_fg_bg = style.bw_fg_bg
local g_lg_fg_bg = style.g_lg_fg_bg
local nav_fg_bg = style.nav_fg_bg
local btn_act_fg_bg = style.btn_act_fg_bg
local btn_dis_fg_bg = style.btn_dis_fg_bg
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local function exit() os.queueEvent("terminate") end local function exit() os.queueEvent("terminate") end
TextBox{parent=display,y=1,text="Pocket Configurator",alignment=CENTER,height=1,fg_bg=style.header} TextBox{parent=display,y=1,text="Pocket Configurator",alignment=CENTER,fg_bg=style.header}
local root_pane_div = Div{parent=display,x=1,y=2} local root_pane_div = Div{parent=display,x=1,y=2}
local main_page = Div{parent=root_pane_div,x=1,y=1} local main_page = Div{parent=root_pane_div,x=1,y=1}
local ui_cfg = Div{parent=root_pane_div,x=1,y=1}
local net_cfg = Div{parent=root_pane_div,x=1,y=1} local net_cfg = Div{parent=root_pane_div,x=1,y=1}
local log_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 summary = Div{parent=root_pane_div,x=1,y=1}
local changelog = Div{parent=root_pane_div,x=1,y=1} local changelog = Div{parent=root_pane_div,x=1,y=1}
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,net_cfg,log_cfg,summary,changelog}} local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,ui_cfg,net_cfg,log_cfg,summary,changelog}}
-- Main Page --#region Main Page
local y_start = 7 local y_start = 7
@@ -148,7 +153,7 @@ local function config_view(display)
tool_ctl.viewing_config = true tool_ctl.viewing_config = true
tool_ctl.gen_summary(settings_cfg) tool_ctl.gen_summary(settings_cfg)
tool_ctl.settings_apply.hide(true) tool_ctl.settings_apply.hide(true)
main_pane.set_value(4) main_pane.set_value(5)
end end
if fs.exists("/pocket/config.lua") then if fs.exists("/pocket/config.lua") then
@@ -157,348 +162,52 @@ local function config_view(display)
end 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} PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure Device",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg} tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end 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} local function startup()
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} tool_ctl.launch_startup = true
--#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() exit()
end 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=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=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)} local start_btn = PushButton{parent=main_page,x=17,y=18,min_width=9,text="Startup",callback=startup,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=main_page,x=2,y=y_start+4,min_width=12,text="Change Log",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=sum_c_4,x=1,y=1,height=8,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} if tool_ctl.ask_config then start_btn.disable() end
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 --#endregion
-- Config Change Log --#region System Configuration
local settings = { settings_cfg, ini_cfg, tmp_cfg, fields, load_settings }
local divs = { ui_cfg, net_cfg, log_cfg, summary }
system.create(tool_ctl, main_pane, settings, divs, style, exit)
--#endregion
--#region Config Change Log
local cl = Div{parent=changelog,x=2,y=4,width=24} local cl = Div{parent=changelog,x=2,y=4,width=24}
TextBox{parent=changelog,x=1,y=2,height=1,text=" Config Change Log",fg_bg=bw_fg_bg} TextBox{parent=changelog,x=1,y=2,text=" Config Change Log",fg_bg=bw_fg_bg}
local c_log = ListBox{parent=cl,x=1,y=1,height=13,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} 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 for _, change in ipairs(changes) do
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg} TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
for _, v in ipairs(change[2]) do for _, v in ipairs(change[2]) do
local e = Div{parent=c_log,height=#util.strwrap(v,21)} local e = Div{parent=c_log,height=#util.strwrap(v,21)}
TextBox{parent=e,y=1,x=1,text="- ",height=1,fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=e,y=1,x=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
end end
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} 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 --#endregion
-- 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 end
-- reset terminal screen -- reset terminal screen
@@ -532,9 +241,7 @@ function configurator.configure(ask_config)
local event, param1, param2, param3 = util.pull_event() local event, param1, param2, param3 = util.pull_event()
-- handle event -- handle event
if event == "timer" then if event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" 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) local m_e = core.events.new_mouse_event(event, param1, param2, param3)
if m_e then display.handle_mouse(m_e) end if m_e then display.handle_mouse(m_e) end
elseif event == "char" or event == "key" or event == "key_up" then elseif event == "char" or event == "key" or event == "key_up" then
@@ -559,7 +266,7 @@ function configurator.configure(ask_config)
println("configurator error: " .. error) println("configurator error: " .. error)
end end
return status, error return status, error, tool_ctl.launch_startup
end end
return configurator return configurator

View File

@@ -2,15 +2,21 @@
-- I/O Control for Pocket Integration with Supervisor & Coordinator -- I/O Control for Pocket Integration with Supervisor & Coordinator
-- --
local log = require("scada-common.log") local psil = require("scada-common.psil")
local psil = require("scada-common.psil") local types = require("scada-common.types")
local types = require("scada-common.types") local util = require("scada-common.util")
local util = require("scada-common.util")
local iorx = require("pocket.iorx")
local process = require("pocket.process")
local ALARM = types.ALARM local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE local ALARM_STATE = types.ALARM_STATE
---@todo nominal trip time is ping (0ms to 10ms usually) local ENERGY_SCALE = types.ENERGY_SCALE
local ENERGY_UNITS = types.ENERGY_SCALE_UNITS
local TEMP_SCALE = types.TEMP_SCALE
local TEMP_UNITS = types.TEMP_SCALE_UNITS
local WARN_TT = 40 local WARN_TT = 40
local HIGH_TT = 80 local HIGH_TT = 80
@@ -26,200 +32,27 @@ local LINK_STATE = {
iocontrol.LINK_STATE = LINK_STATE iocontrol.LINK_STATE = LINK_STATE
---@enum POCKET_APP_ID
local APP_ID = {
ROOT = 1,
-- main app page
UNITS = 2,
ABOUT = 3,
-- diag app page
ALARMS = 4,
-- other
DUMMY = 5,
NUM_APPS = 5
}
iocontrol.APP_ID = APP_ID
---@class pocket_ioctl ---@class pocket_ioctl
local io = { local io = {
version = "unknown", version = "unknown", -- pocket version
ps = psil.create() ps = psil.create(), -- pocket PSIL
loader_require = { sv = false, api = false }
} }
---@class nav_tree_page local config = nil ---@type pkt_config
---@field _p nav_tree_page|nil page's parent local comms = nil ---@type pocket_comms
---@field _c table page's children
---@field nav_to function function to navigate to this page
---@field switcher function|nil function to switch between children
---@field tasks table tasks to run while viewing this page
-- allocate the page navigation system
function iocontrol.alloc_nav()
local self = {
pane = nil, ---@type graphics_element
apps = {},
containers = {},
cur_app = APP_ID.ROOT
}
self.cur_page = self.root
---@class pocket_nav
io.nav = {}
-- set the root pane element to switch between apps with
---@param root_pane graphics_element
function io.nav.set_pane(root_pane)
self.pane = root_pane
end
function io.nav.set_sidebar(sidebar)
self.sidebar = sidebar
end
-- register an app
---@param app_id POCKET_APP_ID app ID
---@param container graphics_element element that contains this app (usually a Div)
---@param pane graphics_element? multipane if this is a simple paned app, then nav_to must be a number
function io.nav.register_app(app_id, container, pane)
---@class pocket_app
local app = {
loaded = false,
load = nil,
cur_page = nil, ---@type nav_tree_page
pane = pane,
paned_pages = {},
sidebar_items = {}
}
app.load = function () app.loaded = true end
-- delayed set of the pane if it wasn't ready at the start
---@param root_pane graphics_element multipane
function app.set_root_pane(root_pane)
app.pane = root_pane
end
function app.set_sidebar(items)
app.sidebar_items = items
if self.sidebar then self.sidebar.update(items) end
end
-- function to run on initial load into memory
---@param on_load function callback
function app.set_on_load(on_load)
app.load = function ()
on_load()
app.loaded = true
end
end
-- if a pane was provided, this will switch between numbered pages
---@param idx integer page index
function app.switcher(idx)
if app.paned_pages[idx] then
app.paned_pages[idx].nav_to()
end
end
-- create a new page entry in the app's page navigation tree
---@param parent nav_tree_page? a parent page or nil to set this as the root
---@param nav_to function|integer function to navigate to this page or pane index
---@return nav_tree_page new_page this new page
function app.new_page(parent, nav_to)
---@type nav_tree_page
local page = { _p = parent, _c = {}, nav_to = function () end, switcher = function () end, tasks = {} }
if parent == nil and app.cur_page == nil then
app.cur_page = page
end
if type(nav_to) == "number" then
app.paned_pages[nav_to] = page
function page.nav_to()
app.cur_page = page
if app.pane then app.pane.set_value(nav_to) end
end
else
function page.nav_to()
app.cur_page = page
nav_to()
end
end
-- switch between children
---@param id integer child ID
function page.switcher(id) if page._c[id] then page._c[id].nav_to() end end
if parent ~= nil then
table.insert(page._p._c, page)
end
return page
end
-- get the currently active page
function app.get_current_page() return app.cur_page end
-- attempt to navigate up the tree
---@return boolean success true if successfully navigated up
function app.nav_up()
local parent = app.cur_page._p
if parent then parent.nav_to() end
return parent ~= nil
end
self.apps[app_id] = app
self.containers[app_id] = container
return app
end
-- get a list of the app containers (usually Div elements)
function io.nav.get_containers() return self.containers end
-- open a given app
---@param app_id POCKET_APP_ID
function io.nav.open_app(app_id)
local app = self.apps[app_id] ---@type pocket_app
if app then
if not app.loaded then app.load() end
self.cur_app = app_id
self.pane.set_value(app_id)
if #app.sidebar_items > 0 then
self.sidebar.update(app.sidebar_items)
end
else
log.debug("tried to open unknown app")
end
end
-- get the currently active page
---@return nav_tree_page
function io.nav.get_current_page()
return self.apps[self.cur_app].get_current_page()
end
-- attempt to navigate up
function io.nav.nav_up()
local app = self.apps[self.cur_app] ---@type pocket_app
log.debug("attempting app nav up for app " .. self.cur_app)
if not app.nav_up() then
log.debug("internal app nav up failed, going to home screen")
io.nav.open_app(APP_ID.ROOT)
end
end
end
-- initialize facility-independent components of pocket iocontrol -- initialize facility-independent components of pocket iocontrol
---@param comms pocket_comms ---@param pkt_comms pocket_comms
function iocontrol.init_core(comms) ---@param nav pocket_nav
iocontrol.alloc_nav() ---@param cfg pkt_config
function iocontrol.init_core(pkt_comms, nav, cfg)
comms = pkt_comms
config = cfg
iocontrol.rx = iorx(io)
io.nav = nav
---@class pocket_ioctl_diag ---@class pocket_ioctl_diag
io.diag = {} io.diag = {}
@@ -252,47 +85,70 @@ function iocontrol.init_core(comms)
get_tone_states = function () comms.diag__get_alarm_tones() end, get_tone_states = function () comms.diag__get_alarm_tones() end,
ready_warn = nil, ---@type graphics_element tone_buttons = {}, ---@type SwitchButton[]
tone_buttons = {}, alarm_buttons = {} ---@type Checkbox[]
alarm_buttons = {},
tone_indicators = {} -- indicators to update from supervisor tone states
} }
-- computer list
io.diag.get_comps = function () comms.diag__get_computers() end
-- API access -- API access
---@class pocket_ioctl_api ---@class pocket_ioctl_api
io.api = { io.api = {
get_unit = function (unit) comms.api__get_unit(unit) end get_fac = function () comms.api__get_facility() end,
get_unit = function (unit) comms.api__get_unit(unit) end,
get_ctrl = function () comms.api__get_control() end,
get_proc = function () comms.api__get_process() end,
get_waste = function () comms.api__get_waste() end,
get_rad = function () comms.api__get_rad() end
} }
end end
-- initialize facility-dependent components of pocket iocontrol -- initialize facility-dependent components of pocket iocontrol
---@param conf facility_conf configuration ---@param conf facility_conf facility configuration
---@param temp_scale 1|2|3|4 temperature unit (1 = K, 2 = C, 3 = F, 4 = R) function iocontrol.init_fac(conf)
function iocontrol.init_fac(conf, temp_scale) local temp_scale, energy_scale = config.TempScale, config.EnergyScale
io.temp_label = TEMP_UNITS[temp_scale]
io.energy_label = ENERGY_UNITS[energy_scale]
-- temperature unit label and conversion function (from Kelvin) -- temperature unit label and conversion function (from Kelvin)
if temp_scale == 2 then if temp_scale == TEMP_SCALE.CELSIUS then
io.temp_label = "\xb0C"
io.temp_convert = function (t) return t - 273.15 end io.temp_convert = function (t) return t - 273.15 end
elseif temp_scale == 3 then elseif temp_scale == TEMP_SCALE.FAHRENHEIT then
io.temp_label = "\xb0F"
io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end
elseif temp_scale == 4 then elseif temp_scale == TEMP_SCALE.RANKINE then
io.temp_label = "\xb0R"
io.temp_convert = function (t) return 1.8 * t end io.temp_convert = function (t) return 1.8 * t end
else else
io.temp_label = "K" io.temp_label = "K"
io.temp_convert = function (t) return t end io.temp_convert = function (t) return t end
end end
-- energy unit label and conversion function (from Joules unless otherwise specified)
if energy_scale == ENERGY_SCALE.FE or energy_scale == ENERGY_SCALE.RF then
io.energy_convert = util.joules_to_fe_rf
io.energy_convert_from_fe = function (t) return t end
io.energy_convert_to_fe = function (t) return t end
else
io.energy_label = "J"
io.energy_convert = function (t) return t end
io.energy_convert_from_fe = util.fe_rf_to_joules
io.energy_convert_to_fe = util.joules_to_fe_rf
end
-- facility data structure -- facility data structure
---@class pioctl_facility ---@class pioctl_facility
io.facility = { io.facility = {
num_units = conf.num_units, num_units = conf.num_units,
tank_mode = conf.cooling.fac_tank_mode, tank_mode = conf.cooling.fac_tank_mode,
tank_defs = conf.cooling.fac_tank_defs, tank_defs = conf.cooling.fac_tank_defs,
tank_list = conf.cooling.fac_tank_list,
tank_conns = conf.cooling.fac_tank_conns,
tank_fluid_types = conf.cooling.tank_fluid_types,
all_sys_ok = false, all_sys_ok = false,
rtu_count = 0, rtu_count = 0,
status_lines = { "", "" },
auto_ready = false, auto_ready = false,
auto_active = false, auto_active = false,
auto_ramping = false, auto_ramping = false,
@@ -301,7 +157,7 @@ function iocontrol.init_fac(conf, temp_scale)
auto_scram = false, auto_scram = false,
---@type ascram_status ---@type ascram_status
ascram_status = { ascram_status = {
matrix_dc = false, matrix_fault = false,
matrix_fill = false, matrix_fill = false,
crit_alarm = false, crit_alarm = false,
radiation = false, radiation = false,
@@ -311,22 +167,28 @@ function iocontrol.init_fac(conf, temp_scale)
---@type WASTE_PRODUCT ---@type WASTE_PRODUCT
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM, auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
auto_pu_fallback_active = false, auto_pu_fallback_active = false,
auto_sps_disabled = false,
waste_stats = { 0, 0, 0, 0, 0, 0 }, -- waste in, pu, po, po pellets, am, spent waste
radiation = types.new_zero_radiation_reading(), radiation = types.new_zero_radiation_reading(),
start_ack = nil, ---@type fun(success: boolean)
stop_ack = nil, ---@type fun(success: boolean)
scram_ack = nil, ---@type fun(success: boolean)
ack_alarms_ack = nil, ---@type fun(success: boolean)
ps = psil.create(), ps = psil.create(),
induction_ps_tbl = {}, induction_ps_tbl = {}, ---@type psil[]
induction_data_tbl = {}, induction_data_tbl = {}, ---@type imatrix_session_db[]
sps_ps_tbl = {}, sps_ps_tbl = {}, ---@type psil[]
sps_data_tbl = {}, sps_data_tbl = {}, ---@type sps_session_db[]
tank_ps_tbl = {}, tank_ps_tbl = {}, ---@type psil[]
tank_data_tbl = {}, tank_data_tbl = {}, ---@type dynamicv_session_db[]
env_d_ps = psil.create(), rad_monitors = {} ---@type { radiation: radiation_reading, raw: number }[]
env_d_data = {}
} }
-- create induction and SPS tables (currently only 1 of each is supported) -- create induction and SPS tables (currently only 1 of each is supported)
@@ -335,92 +197,6 @@ function iocontrol.init_fac(conf, temp_scale)
table.insert(io.facility.sps_ps_tbl, psil.create()) table.insert(io.facility.sps_ps_tbl, psil.create())
table.insert(io.facility.sps_data_tbl, {}) table.insert(io.facility.sps_data_tbl, {})
-- determine tank information
if io.facility.tank_mode == 0 then
io.facility.tank_defs = {}
-- on facility tank mode 0, setup tank defs to match unit tank option
for i = 1, conf.num_units do
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TankConnection, 1, 0)
end
io.facility.tank_list = { table.unpack(io.facility.tank_defs) }
else
-- decode the layout of tanks from the connections definitions
local tank_mode = io.facility.tank_mode
local tank_defs = io.facility.tank_defs
local tank_list = { table.unpack(tank_defs) }
local function calc_fdef(start_idx, end_idx)
local first = 4
for i = start_idx, end_idx do
if io.facility.tank_defs[i] == 2 then
if i < first then first = i end
end
end
return first
end
if tank_mode == 1 then
-- (1) 1 total facility tank (A A A A)
local first_fdef = calc_fdef(1, #tank_defs)
for i = 1, #tank_defs do
if i > first_fdef and tank_defs[i] == 2 then
tank_list[i] = 0
end
end
elseif tank_mode == 2 then
-- (2) 2 total facility tanks (A A A B)
local first_fdef = calc_fdef(1, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 3 then
-- (3) 2 total facility tanks (A A B B)
for _, a in pairs({ 1, 3 }) do
local b = a + 1
if (tank_defs[a] == 2) and (tank_defs[b] == 2) then
tank_list[b] = 0
end
end
elseif tank_mode == 4 then
-- (4) 2 total facility tanks (A B B B)
local first_fdef = calc_fdef(2, #tank_defs)
for i = 1, #tank_defs do
if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 5 then
-- (5) 3 total facility tanks (A A B C)
local first_fdef = calc_fdef(1, math.min(2, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 6 then
-- (6) 3 total facility tanks (A B B C)
local first_fdef = calc_fdef(2, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 7 then
-- (7) 3 total facility tanks (A B C C)
local first_fdef = calc_fdef(3, #tank_defs)
for i = 1, #tank_defs do
if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
end
io.facility.tank_list = tank_list
end
-- create facility tank tables -- create facility tank tables
for i = 1, #io.facility.tank_list do for i = 1, #io.facility.tank_list do
if io.facility.tank_list[i] == 2 then if io.facility.tank_list[i] == 2 then
@@ -430,19 +206,23 @@ function iocontrol.init_fac(conf, temp_scale)
end end
-- create unit data structures -- create unit data structures
io.units = {} io.units = {} ---@type pioctl_unit[]
for i = 1, conf.num_units do for i = 1, conf.num_units do
---@class pioctl_unit ---@class pioctl_unit
local entry = { local entry = {
unit_id = i, unit_id = i,
connected = false, connected = false,
rtu_hw = {},
num_boilers = 0, num_boilers = 0,
num_turbines = 0, num_turbines = 0,
num_snas = 0, num_snas = 0,
has_tank = conf.cooling.r_cool[i].TankConnection, has_tank = conf.cooling.r_cool[i].TankConnection,
status_lines = { "", "" },
auto_ready = false,
auto_degraded = false,
control_state = false, control_state = false,
burn_rate_cmd = 0.0, burn_rate_cmd = 0.0,
radiation = types.new_zero_radiation_reading(), radiation = types.new_zero_radiation_reading(),
@@ -454,25 +234,43 @@ function iocontrol.init_fac(conf, temp_scale)
waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM, waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
waste_product = types.WASTE_PRODUCT.PLUTONIUM, waste_product = types.WASTE_PRODUCT.PLUTONIUM,
-- auto control group last_rate_change_ms = 0,
a_group = 0, turbine_flow_stable = false,
waste_stats = { 0, 0, 0 }, -- plutonium, polonium, po pellets
---@type alarms -- auto control group
a_group = types.AUTO_GROUP.MANUAL,
start = function () process.start(i) end,
scram = function () process.scram(i) end,
reset_rps = function () process.reset_rps(i) end,
ack_alarms = function () process.ack_all_alarms(i) end,
set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate
start_ack = nil, ---@type fun(success: boolean)
scram_ack = nil, ---@type fun(success: boolean)
reset_rps_ack = nil, ---@type fun(success: boolean)
ack_alarms_ack = nil, ---@type fun(success: boolean)
---@type { [ALARM]: ALARM_STATE }
alarms = { ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE }, alarms = { ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE },
annunciator = {}, ---@type annunciator ---@diagnostic disable-next-line: missing-fields
annunciator = {}, ---@type annunciator
unit_ps = psil.create(), unit_ps = psil.create(),
reactor_data = {}, ---@type reactor_db reactor_data = types.new_reactor_db(),
boiler_ps_tbl = {}, boiler_ps_tbl = {}, ---@type psil[]
boiler_data_tbl = {}, boiler_data_tbl = {}, ---@type boilerv_session_db[]
turbine_ps_tbl = {}, turbine_ps_tbl = {}, ---@type psil[]
turbine_data_tbl = {}, turbine_data_tbl = {}, ---@type turbinev_session_db[]
tank_ps_tbl = {}, tank_ps_tbl = {}, ---@type psil[]
tank_data_tbl = {} tank_data_tbl = {}, ---@type dynamicv_session_db[]
rad_monitors = {} ---@type { radiation: radiation_reading, raw: number }[]
} }
-- on other facility modes, overwrite unit TANK option with facility tank defs -- on other facility modes, overwrite unit TANK option with facility tank defs
@@ -503,12 +301,15 @@ function iocontrol.init_fac(conf, temp_scale)
table.insert(io.units, entry) table.insert(io.units, entry)
end end
-- pass IO control here since it can't be require'd due to a require loop
process.init(io, comms)
end end
-- set network link state -- set network link state
---@param state POCKET_LINK_STATE ---@param state POCKET_LINK_STATE
---@param sv_addr integer? supervisor address if linked ---@param sv_addr integer|false|nil supervisor address if linked, nil if unchanged, false if unlinked
---@param api_addr integer? coordinator address if linked ---@param api_addr integer|false|nil coordinator address if linked, nil if unchanged, false if unlinked
function iocontrol.report_link_state(state, sv_addr, api_addr) function iocontrol.report_link_state(state, sv_addr, api_addr)
io.ps.publish("link_state", state) io.ps.publish("link_state", state)
@@ -520,12 +321,25 @@ function iocontrol.report_link_state(state, sv_addr, api_addr)
io.ps.publish("crd_conn_quality", 0) io.ps.publish("crd_conn_quality", 0)
end end
if state == LINK_STATE.LINKED then if sv_addr then
io.ps.publish("sv_addr", sv_addr) io.ps.publish("sv_addr", util.c(sv_addr, ":", config.SVR_Channel))
io.ps.publish("api_addr", api_addr) elseif sv_addr == false then
io.ps.publish("sv_addr", "unknown (not linked)")
end
if api_addr then
io.ps.publish("api_addr", util.c(api_addr, ":", config.CRD_Channel))
elseif api_addr == false then
io.ps.publish("api_addr", "unknown (not linked)")
end end
end end
-- show the reason the supervisor connection isn't linking
function iocontrol.report_svr_link_error(msg) io.ps.publish("svr_link_msg", msg) end
-- show the reason the coordinator api connection isn't linking
function iocontrol.report_crd_link_error(msg) io.ps.publish("api_link_msg", msg) end
-- determine supervisor connection quality (trip time) -- determine supervisor connection quality (trip time)
---@param trip_time integer ---@param trip_time integer
function iocontrol.report_svr_tt(trip_time) function iocontrol.report_svr_tt(trip_time)
@@ -552,227 +366,6 @@ function iocontrol.report_crd_tt(trip_time)
io.ps.publish("crd_conn_quality", state) io.ps.publish("crd_conn_quality", state)
end end
-- populate facility data from API_GET_FAC
---@param data table
---@return boolean valid
function iocontrol.record_facility_data(data)
local valid = true
local fac = io.facility
fac.all_sys_ok = data[1]
fac.rtu_count = data[2]
fac.radiation = data[3]
-- auto control
if type(data[4]) == "table" and #data[4] == 4 then
fac.auto_ready = data[4][1]
fac.auto_active = data[4][2]
fac.auto_ramping = data[4][3]
fac.auto_saturated = data[4][4]
end
-- waste
if type(data[5]) == "table" and #data[5] == 2 then
fac.auto_current_waste_product = data[5][1]
fac.auto_pu_fallback_active = data[5][2]
end
fac.num_tanks = data[6]
fac.has_imatrix = data[7]
fac.has_sps = data[8]
return valid
end
-- update unit status data from API_GET_UNIT
---@param data table
function iocontrol.record_unit_data(data)
if type(data[1]) == "number" and io.units[data[1]] then
local unit = io.units[data[1]] ---@type pioctl_unit
unit.connected = data[2]
unit.rtu_hw = data[3]
unit.alarms = data[4]
--#region Annunciator
unit.annunciator = data[5]
local rcs_disconn, rcs_warn, rcs_hazard = false, false, false
for key, val in pairs(unit.annunciator) do
if key == "BoilerOnline" or key == "TurbineOnline" then
local every = true
-- split up online arrays
for id = 1, #val do
every = every and val[id]
if key == "BoilerOnline" then
unit.boiler_ps_tbl[id].publish(key, val[id])
else
unit.turbine_ps_tbl[id].publish(key, val[id])
end
end
if not every then rcs_disconn = true end
unit.unit_ps.publish("U_" .. key, every)
elseif key == "HeatingRateLow" or key == "WaterLevelLow" then
-- split up array for all boilers
local any = false
for id = 1, #val do
any = any or val[id]
unit.boiler_ps_tbl[id].publish(key, val[id])
end
if key == "HeatingRateLow" and any then
rcs_warn = true
elseif key == "WaterLevelLow" and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
elseif key == "SteamDumpOpen" or key == "TurbineOverSpeed" or key == "GeneratorTrip" or key == "TurbineTrip" then
-- split up array for all turbines
local any = false
for id = 1, #val do
any = any or val[id]
unit.turbine_ps_tbl[id].publish(key, val[id])
end
if key == "GeneratorTrip" and any then
rcs_warn = true
elseif (key == "TurbineOverSpeed" or key == "TurbineTrip") and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
else
-- non-table fields
unit.unit_ps.publish(key, val)
end
end
local anc = unit.annunciator
rcs_hazard = rcs_hazard or anc.RCPTrip
rcs_warn = rcs_warn or anc.RCSFlowLow or anc.CoolantLevelLow or anc.RCSFault or anc.MaxWaterReturnFeed or
anc.CoolantFeedMismatch or anc.BoilRateMismatch or anc.SteamFeedMismatch or anc.MaxWaterReturnFeed
local rcs_status = 4
if rcs_hazard then
rcs_status = 2
elseif rcs_warn then
rcs_status = 3
elseif rcs_disconn then
rcs_status = 1
end
unit.unit_ps.publish("U_RCS", rcs_status)
--#endregion
--#region Reactor Data
unit.reactor_data = data[6]
local control_status = 1
local reactor_status = 1
local rps_status = 1
if unit.connected then
-- update RPS status
if unit.reactor_data.rps_tripped then
control_status = 2
rps_status = util.trinary(unit.reactor_data.rps_trip_cause == "manual", 3, 2)
else rps_status = 4 end
-- update reactor/control status
if unit.reactor_data.mek_status.status then
reactor_status = 4
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
else
if unit.reactor_data.no_reactor then
reactor_status = 2
elseif not unit.reactor_data.formed or unit.reactor_data.rps_status.force_dis then
reactor_status = 3
else
reactor_status = 4
end
end
for key, val in pairs(unit.reactor_data) do
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
unit.unit_ps.publish(key, val)
end
end
if type(unit.reactor_data.rps_status) == "table" then
for key, val in pairs(unit.reactor_data.rps_status) do
unit.unit_ps.publish(key, val)
end
end
if type(unit.reactor_data.mek_status) == "table" then
for key, val in pairs(unit.reactor_data.mek_status) do
unit.unit_ps.publish(key, val)
end
end
end
unit.unit_ps.publish("U_ControlStatus", control_status)
unit.unit_ps.publish("U_ReactorStatus", reactor_status)
unit.unit_ps.publish("U_RPS", rps_status)
--#endregion
unit.boiler_data_tbl = data[7]
for id = 1, #unit.boiler_data_tbl do
local boiler = unit.boiler_data_tbl[id] ---@type boilerv_session_db
local ps = unit.boiler_ps_tbl[id] ---@type psil
local boiler_status = 1
if unit.rtu_hw.boilers[id].connected then
if unit.rtu_hw.boilers[id].faulted then
boiler_status = 3
elseif boiler.formed then
boiler_status = 4
else
boiler_status = 2
end
end
ps.publish("BoilerStatus", boiler_status)
end
unit.turbine_data_tbl = data[8]
for id = 1, #unit.turbine_data_tbl do
local turbine = unit.turbine_data_tbl[id] ---@type turbinev_session_db
local ps = unit.turbine_ps_tbl[id] ---@type psil
local turbine_status = 1
if unit.rtu_hw.turbines[id].connected then
if unit.rtu_hw.turbines[id].faulted then
turbine_status = 3
elseif turbine.formed then
turbine_status = 4
else
turbine_status = 2
end
end
ps.publish("TurbineStatus", turbine_status)
end
unit.tank_data_tbl = data[9]
end
end
-- get the IO controller database -- get the IO controller database
function iocontrol.get_db() return io end function iocontrol.get_db() return io end

955
pocket/iorx.lua Normal file
View File

@@ -0,0 +1,955 @@
--
-- I/O Control's Data Receive (Rx) Handlers
--
local comms = require("scada-common.comms")
local const = require("scada-common.constants")
local types = require("scada-common.types")
local util = require("scada-common.util")
local DEV_TYPE = comms.DEVICE_TYPE
local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE
local BLR_STATE = types.BOILER_STATE
local TRB_STATE = types.TURBINE_STATE
local TNK_STATE = types.TANK_STATE
local MTX_STATE = types.IMATRIX_STATE
local SPS_STATE = types.SPS_STATE
local io ---@type pocket_ioctl
local iorx = {} ---@class iorx
-- populate facility data from API_GET_FAC
---@param data table
---@return boolean valid
function iorx.record_facility_data(data)
local valid = true
local fac = io.facility
fac.all_sys_ok = data[1]
fac.rtu_count = data[2]
fac.radiation = data[3]
-- auto control
if type(data[4]) == "table" and #data[4] == 4 then
fac.auto_ready = data[4][1]
fac.auto_active = data[4][2]
fac.auto_ramping = data[4][3]
fac.auto_saturated = data[4][4]
end
-- waste
if type(data[5]) == "table" and #data[5] == 2 then
fac.auto_current_waste_product = data[5][1]
fac.auto_pu_fallback_active = data[5][2]
end
fac.num_tanks = data[6]
fac.has_imatrix = data[7]
fac.has_sps = data[8]
return valid
end
local function tripped(state) return state == ALARM_STATE.TRIPPED or state == ALARM_STATE.ACKED end
local function _record_multiblock_status(faulted, data, ps)
ps.publish("formed", data.formed)
ps.publish("faulted", faulted)
if data.build then
for key, val in pairs(data.build) do ps.publish(key, val) end
end
for key, val in pairs(data.state) do ps.publish(key, val) end
for key, val in pairs(data.tanks) do ps.publish(key, val) end
end
-- update unit status data from API_GET_UNIT
---@param data table
function iorx.record_unit_data(data)
local unit = io.units[data[1]]
unit.connected = data[2]
local comp_statuses = data[3]
unit.a_group = data[4]
unit.alarms = data[5]
local next_c_stat = 1
unit.unit_ps.publish("auto_group_id", unit.a_group)
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
--#region Annunciator
unit.annunciator = data[6]
local rcs_disconn, rcs_warn, rcs_hazard = false, false, false
for key, val in pairs(unit.annunciator) do
if key == "BoilerOnline" or key == "TurbineOnline" then
local every = true
-- split up online arrays
for id = 1, #val do
every = every and val[id]
if key == "BoilerOnline" then
unit.boiler_ps_tbl[id].publish(key, val[id])
else
unit.turbine_ps_tbl[id].publish(key, val[id])
end
end
if not every then rcs_disconn = true end
unit.unit_ps.publish("U_" .. key, every)
elseif key == "HeatingRateLow" or key == "WaterLevelLow" then
-- split up array for all boilers
local any = false
for id = 1, #val do
any = any or val[id]
unit.boiler_ps_tbl[id].publish(key, val[id])
end
if key == "HeatingRateLow" and any then
rcs_warn = true
elseif key == "WaterLevelLow" and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
elseif key == "SteamDumpOpen" or key == "TurbineOverSpeed" or key == "GeneratorTrip" or key == "TurbineTrip" then
-- split up array for all turbines
local any = false
for id = 1, #val do
any = any or val[id]
unit.turbine_ps_tbl[id].publish(key, val[id])
end
if key == "GeneratorTrip" and any then
rcs_warn = true
elseif (key == "TurbineOverSpeed" or key == "TurbineTrip") and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
else
-- non-table fields
unit.unit_ps.publish(key, val)
end
end
local anc = unit.annunciator
rcs_hazard = rcs_hazard or anc.RCPTrip
rcs_warn = rcs_warn or anc.RCSFlowLow or anc.CoolantLevelLow or anc.RCSFault or anc.MaxWaterReturnFeed or
anc.CoolantFeedMismatch or anc.BoilRateMismatch or anc.SteamFeedMismatch
local rcs_status = 4
if rcs_hazard then
rcs_status = 2
elseif rcs_warn then
rcs_status = 3
elseif rcs_disconn then
rcs_status = 1
end
unit.unit_ps.publish("U_RCS", rcs_status)
--#endregion
--#region Reactor Data
unit.reactor_data = data[7]
local control_status = 1
local reactor_status = 1
local rps_status = 1
if unit.connected then
-- update RPS status
if unit.reactor_data.rps_tripped then
control_status = 2
rps_status = util.trinary(unit.reactor_data.rps_trip_cause == "manual", 3, 2)
else
rps_status = 4
end
reactor_status = 4 -- ok, until proven otherwise
-- update reactor/control status
if unit.reactor_data.mek_status.status then
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
else
if unit.reactor_data.no_reactor then
reactor_status = 2
elseif (not unit.reactor_data.formed) or unit.reactor_data.rps_status.force_dis then
reactor_status = 3
end
end
for key, val in pairs(unit.reactor_data) do
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
unit.unit_ps.publish(key, val)
end
end
for key, val in pairs(unit.reactor_data.rps_status) do
unit.unit_ps.publish(key, val)
end
for key, val in pairs(unit.reactor_data.mek_struct) do
unit.unit_ps.publish(key, val)
end
for key, val in pairs(unit.reactor_data.mek_status) do
unit.unit_ps.publish(key, val)
end
end
unit.unit_ps.publish("U_ControlStatus", control_status)
unit.unit_ps.publish("U_ReactorStatus", reactor_status)
unit.unit_ps.publish("U_ReactorStateStatus", comp_statuses[next_c_stat])
unit.unit_ps.publish("U_RPS", rps_status)
next_c_stat = next_c_stat + 1
--#endregion
--#region RTU Devices
unit.boiler_data_tbl = data[8]
for id = 1, #unit.boiler_data_tbl do
local boiler = unit.boiler_data_tbl[id]
local ps = unit.boiler_ps_tbl[id]
local c_stat = comp_statuses[next_c_stat]
local boiler_status = 1
if c_stat ~= BLR_STATE.OFFLINE then
if c_stat == BLR_STATE.FAULT then
boiler_status = 3
elseif c_stat ~= BLR_STATE.UNFORMED then
boiler_status = 4
else
boiler_status = 2
end
_record_multiblock_status(c_stat == BLR_STATE.FAULT, boiler, ps)
end
ps.publish("BoilerStatus", boiler_status)
ps.publish("BoilerStateStatus", c_stat)
next_c_stat = next_c_stat + 1
end
unit.turbine_data_tbl = data[9]
for id = 1, #unit.turbine_data_tbl do
local turbine = unit.turbine_data_tbl[id]
local ps = unit.turbine_ps_tbl[id]
local c_stat = comp_statuses[next_c_stat]
local turbine_status = 1
if c_stat ~= TRB_STATE.OFFLINE then
if c_stat == TRB_STATE.FAULT then
turbine_status = 3
elseif turbine.formed then
turbine_status = 4
else
turbine_status = 2
end
_record_multiblock_status(c_stat == TRB_STATE.FAULT, turbine, ps)
end
ps.publish("TurbineStatus", turbine_status)
ps.publish("TurbineStateStatus", c_stat)
next_c_stat = next_c_stat + 1
end
unit.tank_data_tbl = data[10]
for id = 1, #unit.tank_data_tbl do
local tank = unit.tank_data_tbl[id]
local ps = unit.tank_ps_tbl[id]
local c_stat = comp_statuses[next_c_stat]
local tank_status = 1
if c_stat ~= TNK_STATE.OFFLINE then
if c_stat == TNK_STATE.FAULT then
tank_status = 3
elseif tank.formed then
tank_status = 4
else
tank_status = 2
end
_record_multiblock_status(c_stat == TNK_STATE.FAULT, tank, ps)
end
ps.publish("DynamicTankStatus", tank_status)
ps.publish("DynamicTankStateStatus", c_stat)
next_c_stat = next_c_stat + 1
end
unit.last_rate_change_ms = data[11]
unit.turbine_flow_stable = data[12]
--#endregion
--#region Status Information Display
local ecam = {} -- aviation reference :)
-- local function red(text) return { text = text, color = colors.red } end
local function white(text) return { text = text, color = colors.white } end
local function blue(text) return { text = text, color = colors.blue } end
-- if unit.reactor_data.rps_status then
-- for k, _ in pairs(unit.alarms) do
-- unit.alarms[k] = ALARM_STATE.TRIPPED
-- end
-- end
if tripped(unit.alarms[ALARM.ContainmentBreach]) then
local items = { white("REACTOR MELTDOWN"), blue("DON HAZMAT SUIT") }
table.insert(ecam, { color = colors.red, text = "CONTAINMENT BREACH", help = "ContainmentBreach", items = items })
end
if tripped(unit.alarms[ALARM.ContainmentRadiation]) then
local items = {
white("RADIATION DETECTED"),
blue("DON HAZMAT SUIT"),
blue("RESOLVE LEAK"),
blue("AWAIT SAFE LEVELS")
}
table.insert(ecam, { color = colors.red, text = "RADIATION LEAK", help = "ContainmentRadiation", items = items })
end
if tripped(unit.alarms[ALARM.CriticalDamage]) then
local items = { white("MELTDOWN IMMINENT"), blue("EVACUATE") }
table.insert(ecam, { color = colors.red, text = "RCT DAMAGE CRITICAL", help = "CriticalDamage", items = items })
end
if tripped(unit.alarms[ALARM.ReactorLost]) then
local items = { white("REACTOR OFF-LINE"), blue("CHECK PLC") }
table.insert(ecam, { color = colors.red, text = "REACTOR CONN LOST", help = "ReactorLost", items = items })
end
if tripped(unit.alarms[ALARM.ReactorDamage]) then
local items = { white("REACTOR DAMAGED"), blue("CHECK RCS"), blue("AWAIT DMG REDUCED") }
table.insert(ecam, { color = colors.red, text = "REACTOR DAMAGE", help = "ReactorDamage", items = items })
end
if tripped(unit.alarms[ALARM.ReactorOverTemp]) then
local items = { white("DAMAGING TEMP"), blue("CHECK RCS"), blue("AWAIT COOLDOWN") }
table.insert(ecam, { color = colors.red, text = "REACTOR OVER TEMP", help = "ReactorOverTemp", items = items })
end
if tripped(unit.alarms[ALARM.ReactorHighTemp]) then
local items = { white("OVER EXPECTED TEMP"), blue("CHECK RCS") }
table.insert(ecam, { color = colors.yellow, text = "REACTOR HIGH TEMP", help = "ReactorHighTemp", items = items})
end
if tripped(unit.alarms[ALARM.ReactorWasteLeak]) then
local items = { white("AT WASTE CAPACITY"), blue("CHECK WASTE OUTPUT"), blue("KEEP RCT DISABLED") }
table.insert(ecam, { color = colors.red, text = "REACTOR WASTE LEAK", help = "ReactorWasteLeak", items = items})
end
if tripped(unit.alarms[ALARM.ReactorHighWaste]) then
local items = { blue("CHECK WASTE OUTPUT") }
table.insert(ecam, { color = colors.yellow, text = "REACTOR WASTE HIGH", help = "ReactorHighWaste", items = items})
end
if tripped(unit.alarms[ALARM.RPSTransient]) then
local items = {}
local stat = unit.reactor_data.rps_status
-- for k, _ in pairs(stat) do stat[k] = true end
local function insert(cond, key, text, color) if cond[key] then table.insert(items, { text = text, help = key, color = color }) end end
table.insert(items, white("REACTOR SCRAMMED"))
insert(stat, "high_dmg", "HIGH DAMAGE", colors.red)
insert(stat, "high_temp", "HIGH TEMPERATURE", colors.red)
insert(stat, "low_cool", "CRIT LOW COOLANT")
insert(stat, "ex_waste", "EXCESS WASTE")
insert(stat, "ex_hcool", "EXCESS HEATED COOL")
insert(stat, "no_fuel", "NO FUEL")
insert(stat, "fault", "HARDWARE FAULT")
insert(stat, "timeout", "SUPERVISOR DISCONN")
insert(stat, "manual", "MANUAL SCRAM", colors.white)
insert(stat, "automatic", "AUTOMATIC SCRAM")
insert(stat, "sys_fail", "NOT FORMED", colors.red)
insert(stat, "force_dis", "FORCE DISABLED", colors.red)
table.insert(items, blue("RESOLVE PROBLEM"))
table.insert(items, blue("RESET RPS"))
table.insert(ecam, { color = colors.yellow, text = "RPS TRANSIENT", help = "RPSTransient", items = items})
end
if tripped(unit.alarms[ALARM.RCSTransient]) then
local items = {}
local annunc = unit.annunciator
-- for k, v in pairs(annunc) do
-- if type(v) == "boolean" then annunc[k] = true end
-- if type(v) == "table" then
-- for a, _ in pairs(v) do
-- v[a] = true
-- end
-- end
-- end
local function insert(cond, key, text, color)
if cond == true or (type(cond) == "table" and cond[key]) then table.insert(items, { text = text, help = key, color = color }) end
end
table.insert(items, white("COOLANT PROBLEM"))
insert(annunc, "RCPTrip", "RCP TRIP", colors.red)
insert(annunc, "CoolantLevelLow", "LOW COOLANT")
if unit.num_boilers == 0 then
if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then
insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH")
end
if unit.turbine_flow_stable then
insert(annunc, "RCSFlowLow", "RCS FLOW LOW")
insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH")
insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH")
end
else
if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then
insert(annunc, "RCSFlowLow", "RCS FLOW LOW")
insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH")
insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH")
end
if unit.turbine_flow_stable then
insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH")
end
end
insert(annunc, "MaxWaterReturnFeed", "MAX WTR RTRN FEED")
for k, v in ipairs(annunc.WaterLevelLow) do insert(v, "WaterLevelLow", "BOILER " .. k .. " WTR LOW", colors.red) end
for k, v in ipairs(annunc.HeatingRateLow) do insert(v, "HeatingRateLow", "BOILER " .. k .. " HEAT RATE") end
for k, v in ipairs(annunc.TurbineOverSpeed) do insert(v, "TurbineOverSpeed", "TURBINE " .. k .. " OVERSPD", colors.red) end
for k, v in ipairs(annunc.GeneratorTrip) do insert(v, "GeneratorTrip", "TURBINE " .. k .. " GEN TRIP") end
table.insert(items, blue("CHECK COOLING SYS"))
table.insert(ecam, { color = colors.yellow, text = "RCS TRANSIENT", help = "RCSTransient", items = items})
end
if tripped(unit.alarms[ALARM.TurbineTrip]) then
local items = {}
for k, v in ipairs(unit.annunciator.TurbineTrip) do
if v then table.insert(items, { text = "TURBINE " .. k .. " TRIP", help = "TurbineTrip" }) end
end
table.insert(items, blue("CHECK ENERGY OUT"))
table.insert(ecam, { color = colors.red, text = "TURBINE TRIP", help = "TurbineTripAlarm", items = items})
end
if not (tripped(unit.alarms[ALARM.ReactorLost]) or unit.connected) then
local items = { blue("CHECK PLC") }
table.insert(ecam, { color = colors.yellow, text = "REACTOR OFF-LINE", items = items })
end
for k, v in ipairs(unit.annunciator.BoilerOnline) do
if not v then
local items = { blue("CHECK RTU") }
table.insert(ecam, { color = colors.yellow, text = "BOILER " .. k .. " OFF-LINE", items = items})
end
end
for k, v in ipairs(unit.annunciator.TurbineOnline) do
if not v then
local items = { blue("CHECK RTU") }
table.insert(ecam, { color = colors.yellow, text = "TURBINE " .. k .. " OFF-LINE", items = items})
end
end
-- if no alarms, put some basic status messages in
if #ecam == 0 then
table.insert(ecam, { color = colors.green, text = "REACTOR " .. util.trinary(unit.reactor_data.mek_status.status, "NOMINAL", "IDLE"), items = {}})
local plural = util.trinary(unit.num_turbines > 1, "S", "")
table.insert(ecam, { color = colors.green, text = "TURBINE" .. plural .. util.trinary(unit.turbine_flow_stable, " STABLE", " STABILIZING"), items = {}})
end
unit.unit_ps.publish("U_ECAM", textutils.serialize(ecam))
--#endregion
end
-- update control app with unit data from API_GET_CTRL
---@param data table
function iorx.record_control_data(data)
for u_id = 1, #data do
local unit = io.units[u_id]
local u_data = data[u_id]
unit.connected = u_data[1]
unit.reactor_data.rps_tripped = u_data[2]
unit.unit_ps.publish("rps_tripped", u_data[2])
unit.reactor_data.mek_status.status = u_data[3]
unit.unit_ps.publish("status", u_data[3])
unit.reactor_data.mek_status.temp = u_data[4]
unit.unit_ps.publish("temp", u_data[4])
unit.reactor_data.mek_status.burn_rate = u_data[5]
unit.unit_ps.publish("burn_rate", u_data[5])
unit.reactor_data.mek_status.act_burn_rate = u_data[6]
unit.unit_ps.publish("act_burn_rate", u_data[6])
unit.reactor_data.mek_struct.max_burn = u_data[7]
unit.unit_ps.publish("max_burn", u_data[7])
unit.annunciator.AutoControl = u_data[8]
unit.unit_ps.publish("AutoControl", u_data[8])
unit.a_group = u_data[9]
unit.unit_ps.publish("auto_group_id", unit.a_group)
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
local control_status = 1
if unit.connected then
if unit.reactor_data.rps_tripped then
control_status = 2
end
if unit.reactor_data.mek_status.status then
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
end
end
unit.unit_ps.publish("U_ControlStatus", control_status)
end
end
-- update process app with unit data from API_GET_PROC
---@param data table
function iorx.record_process_data(data)
-- get unit data
for u_id = 1, #io.units do
local unit = io.units[u_id]
local u_data = data[u_id]
unit.reactor_data.mek_status.status = u_data[1]
unit.reactor_data.mek_struct.max_burn = u_data[2]
unit.annunciator.AutoControl = u_data[6]
unit.a_group = u_data[7]
unit.unit_ps.publish("status", u_data[1])
unit.unit_ps.publish("max_burn", u_data[2])
unit.unit_ps.publish("burn_limit", u_data[3])
unit.unit_ps.publish("U_AutoReady", u_data[4])
unit.unit_ps.publish("U_AutoDegraded", u_data[5])
unit.unit_ps.publish("AutoControl", u_data[6])
unit.unit_ps.publish("auto_group_id", unit.a_group)
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
end
-- get facility data
local fac = io.facility
local f_data = data[#io.units + 1]
fac.status_lines = f_data[1]
fac.auto_ready = f_data[2][1]
fac.auto_active = f_data[2][2]
fac.auto_ramping = f_data[2][3]
fac.auto_saturated = f_data[2][4]
fac.auto_scram = f_data[3]
fac.ascram_status = f_data[4]
fac.ps.publish("status_line_1", fac.status_lines[1])
fac.ps.publish("status_line_2", fac.status_lines[2])
fac.ps.publish("auto_ready", fac.auto_ready)
fac.ps.publish("auto_active", fac.auto_active)
fac.ps.publish("auto_ramping", fac.auto_ramping)
fac.ps.publish("auto_saturated", fac.auto_saturated)
fac.ps.publish("auto_scram", fac.auto_scram)
fac.ps.publish("as_matrix_fault", fac.ascram_status.matrix_fault)
fac.ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill)
fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm)
fac.ps.publish("as_radiation", fac.ascram_status.radiation)
fac.ps.publish("as_gen_fault", fac.ascram_status.gen_fault)
fac.ps.publish("process_mode", f_data[5][1])
fac.ps.publish("process_burn_target", f_data[5][2])
fac.ps.publish("process_charge_target", f_data[5][3])
fac.ps.publish("process_gen_target", f_data[5][4])
end
-- update waste app with unit data from API_GET_WASTE
---@param data table
function iorx.record_waste_data(data)
-- get unit data
for u_id = 1, #io.units do
local unit = io.units[u_id]
local u_data = data[u_id]
unit.waste_mode = u_data[1]
unit.waste_product = u_data[2]
unit.num_snas = u_data[3]
unit.sna_peak_rate = u_data[4]
unit.sna_max_rate = u_data[5]
unit.sna_out_rate = u_data[6]
unit.waste_stats = u_data[7]
unit.unit_ps.publish("U_AutoWaste", unit.waste_mode == types.WASTE_MODE.AUTO)
unit.unit_ps.publish("U_WasteMode", unit.waste_mode)
unit.unit_ps.publish("U_WasteProduct", unit.waste_product)
unit.unit_ps.publish("sna_count", unit.num_snas)
unit.unit_ps.publish("sna_peak_rate", unit.sna_peak_rate)
unit.unit_ps.publish("sna_max_rate", unit.sna_max_rate)
unit.unit_ps.publish("sna_out_rate", unit.sna_out_rate)
unit.unit_ps.publish("pu_rate", unit.waste_stats[1])
unit.unit_ps.publish("po_rate", unit.waste_stats[2])
unit.unit_ps.publish("po_pl_rate", unit.waste_stats[3])
end
-- get facility data
local fac = io.facility
local f_data = data[#io.units + 1]
fac.auto_current_waste_product = f_data[1]
fac.auto_pu_fallback_active = f_data[2]
fac.auto_sps_disabled = f_data[3]
fac.ps.publish("current_waste_product", fac.auto_current_waste_product)
fac.ps.publish("pu_fallback_active", fac.auto_pu_fallback_active)
fac.ps.publish("sps_disabled_low_power", fac.auto_sps_disabled)
fac.ps.publish("process_waste_product", f_data[4])
fac.ps.publish("process_pu_fallback", f_data[5])
fac.ps.publish("process_sps_low_power", f_data[6])
fac.waste_stats = f_data[7]
fac.ps.publish("burn_sum", fac.waste_stats[1])
fac.ps.publish("pu_rate", fac.waste_stats[2])
fac.ps.publish("po_rate", fac.waste_stats[3])
fac.ps.publish("po_pl_rate", fac.waste_stats[4])
fac.ps.publish("po_am_rate", fac.waste_stats[5])
fac.ps.publish("spent_waste_rate", fac.waste_stats[6])
fac.sps_ps_tbl[1].publish("SPSStateStatus", f_data[8])
fac.ps.publish("sps_process_rate", f_data[9])
end
-- update facility app with facility and unit data from API_GET_FAC_DTL
---@param data table
function iorx.record_fac_detail_data(data)
local fac = io.facility
local tank_statuses = data[5]
local next_t_stat = 1
-- annunciator
fac.all_sys_ok = data[1]
fac.rtu_count = data[2]
fac.auto_scram = data[3]
fac.ascram_status = data[4]
fac.ps.publish("all_sys_ok", fac.all_sys_ok)
fac.ps.publish("rtu_count", fac.rtu_count)
fac.ps.publish("auto_scram", fac.auto_scram)
fac.ps.publish("as_matrix_fault", fac.ascram_status.matrix_fault)
fac.ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill)
fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm)
fac.ps.publish("as_radiation", fac.ascram_status.radiation)
fac.ps.publish("as_gen_fault", fac.ascram_status.gen_fault)
-- unit data
local units = data[12]
for i = 1, io.facility.num_units do
local unit = io.units[i]
local u_rx = units[i]
unit.connected = u_rx[1]
unit.annunciator = u_rx[2]
unit.reactor_data = u_rx[3]
local control_status = 1
if unit.connected then
if unit.reactor_data.rps_tripped then control_status = 2 end
if unit.reactor_data.mek_status.status then
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
end
end
unit.unit_ps.publish("U_ControlStatus", control_status)
unit.tank_data_tbl = u_rx[4]
for id = 1, #unit.tank_data_tbl do
local tank = unit.tank_data_tbl[id]
local ps = unit.tank_ps_tbl[id]
local c_stat = tank_statuses[next_t_stat]
local tank_status = 1
if c_stat ~= TNK_STATE.OFFLINE then
if c_stat == TNK_STATE.FAULT then
tank_status = 3
elseif tank.formed then
tank_status = 4
else
tank_status = 2
end
end
ps.publish("DynamicTankStatus", tank_status)
ps.publish("DynamicTankStateStatus", c_stat)
next_t_stat = next_t_stat + 1
end
end
-- facility dynamic tank data
fac.tank_data_tbl = data[6]
for id = 1, #fac.tank_data_tbl do
local tank = fac.tank_data_tbl[id]
local ps = fac.tank_ps_tbl[id]
local c_stat = tank_statuses[next_t_stat]
local tank_status = 1
if c_stat ~= TNK_STATE.OFFLINE then
if c_stat == TNK_STATE.FAULT then
tank_status = 3
elseif tank.formed then
tank_status = 4
else
tank_status = 2
end
_record_multiblock_status(c_stat == TNK_STATE.FAULT, tank, ps)
end
ps.publish("DynamicTankStatus", tank_status)
ps.publish("DynamicTankStateStatus", c_stat)
next_t_stat = next_t_stat + 1
end
-- induction matrix data
fac.induction_data_tbl[1] = data[8]
local matrix = fac.induction_data_tbl[1]
local m_ps = fac.induction_ps_tbl[1]
local m_stat = data[7]
local mtx_status = 1
if m_stat ~= MTX_STATE.OFFLINE then
if m_stat == MTX_STATE.FAULT then
mtx_status = 3
elseif matrix.formed then
mtx_status = 4
else
mtx_status = 2
end
_record_multiblock_status(m_stat == MTX_STATE.FAULT, matrix, m_ps)
end
m_ps.publish("InductionMatrixStatus", mtx_status)
m_ps.publish("InductionMatrixStateStatus", m_stat)
m_ps.publish("eta_string", data[9][1])
m_ps.publish("avg_charge", data[9][2])
m_ps.publish("avg_inflow", data[9][3])
m_ps.publish("avg_outflow", data[9][4])
m_ps.publish("is_charging", data[9][5])
m_ps.publish("is_discharging", data[9][6])
m_ps.publish("at_max_io", data[9][7])
-- sps data
fac.sps_data_tbl[1] = data[11]
local sps = fac.sps_data_tbl[1]
local s_ps = fac.sps_ps_tbl[1]
local s_stat = data[10]
local sps_status = 1
if s_stat ~= SPS_STATE.OFFLINE then
if s_stat == SPS_STATE.FAULT then
sps_status = 3
elseif sps.formed then
sps_status = 4
else
sps_status = 2
end
_record_multiblock_status(s_stat == SPS_STATE.FAULT, sps, s_ps)
end
s_ps.publish("SPSStatus", sps_status)
s_ps.publish("SPSStateStatus", s_stat)
end
-- update the radiation monitor app with radiation monitor data from API_GET_RAD
---@param data table
function iorx.record_radiation_data(data)
-- unit radiation monitors
for u_id = 1, #io.units do
local unit = io.units[u_id]
local max_rad = 0
local connected = {}
unit.radiation = types.new_zero_radiation_reading()
unit.rad_monitors = data[u_id]
for id, mon in pairs(unit.rad_monitors) do
table.insert(connected, id)
unit.unit_ps.publish("radiation@" .. id, mon.radiation)
if mon.raw > max_rad then
max_rad = mon.raw
unit.radiation = mon.radiation
end
end
unit.unit_ps.publish("radiation", unit.radiation)
unit.unit_ps.publish("radiation_monitors", textutils.serialize(connected))
end
-- facility radiation monitors
local fac = io.facility
fac.radiation = types.new_zero_radiation_reading()
fac.rad_monitors = data[#io.units + 1]
local max_rad = 0
local connected = {}
for id, mon in pairs(fac.rad_monitors) do
table.insert(connected, id)
fac.ps.publish("radiation@" .. id, mon.radiation)
if mon.raw > max_rad then
max_rad = mon.raw
fac.radiation = mon.radiation
end
end
fac.ps.publish("radiation", fac.radiation)
fac.ps.publish("radiation_monitors", textutils.serialize(connected))
end
local comp_record = {}
-- update the computers app with the network data from INFO_LIST_CMP
---@param data table
function iorx.record_network_data(data)
local ps = io.ps
local connected = {}
local crd_online = false
ps.publish("comp_online", #data)
-- add/update connected computers
for i = 1, #data do
local entry = data[i]
local type = entry[1]
local id = entry[2]
local pfx = "comp_" .. id
connected[id] = true
if type == DEV_TYPE.SVR then
ps.publish("comp_svr_addr", id)
ps.publish("comp_svr_fw", entry[3])
elseif type == DEV_TYPE.CRD then
crd_online = true
ps.publish("comp_crd_addr", id)
ps.publish("comp_crd_fw", entry[3])
ps.publish("comp_crd_rtt", entry[4])
else
ps.publish(pfx .. "_type", entry[1])
ps.publish(pfx .. "_addr", id)
ps.publish(pfx .. "_fw", entry[3])
ps.publish(pfx .. "_rtt", entry[4])
if type == DEV_TYPE.PLC then
ps.publish(pfx .. "_unit", entry[5])
end
if not comp_record[id] then
comp_record[id] = true
-- trigger the app to create the new element
ps.publish("comp_connect", id)
end
end
end
-- handle the coordinator being online or not
-- no need to worry about the supervisor since this data is from the supervisor, so it has to be 'online' if received
ps.publish("comp_crd_online", crd_online)
if not crd_online then
ps.publish("comp_crd_addr", "---")
ps.publish("comp_crd_fw", "---")
ps.publish("comp_crd_rtt", "---")
end
-- reset the published value
ps.publish("comp_connect", false)
-- remove disconnected computers
for id, state in pairs(comp_record) do
if state and not connected[id] then
comp_record[id] = false
-- trigger the app to delete the element
ps.publish("comp_disconnect", id)
end
end
-- reset the published value
ps.publish("comp_disconnect", false)
end
-- clear the tracked connected computer record
function iorx.clear_comp_record() comp_record = {} end
return function (io_obj)
io = io_obj
return iorx
end

View File

@@ -9,12 +9,21 @@ local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
local CRDN_TYPE = comms.CRDN_TYPE local CRDN_TYPE = comms.CRDN_TYPE
local UNIT_COMMAND = comms.UNIT_COMMAND
local FAC_COMMAND = comms.FAC_COMMAND
local LINK_STATE = iocontrol.LINK_STATE local LINK_STATE = iocontrol.LINK_STATE
local pocket = {} local pocket = {}
local MQ__RENDER_DATA = {
LOAD_APP = 1
}
pocket.MQ__RENDER_DATA = MQ__RENDER_DATA
---@type pkt_config ---@type pkt_config
---@diagnostic disable-next-line: missing-fields
local config = {} local config = {}
pocket.config = config pocket.config = config
@@ -23,6 +32,10 @@ pocket.config = config
function pocket.load_config() function pocket.load_config()
if not settings.load("/pocket.settings") then return false end if not settings.load("/pocket.settings") then return false end
config.GreenPuPellet = settings.get("GreenPuPellet")
config.TempScale = settings.get("TempScale")
config.EnergyScale = settings.get("EnergyScale")
config.SVR_Channel = settings.get("SVR_Channel") config.SVR_Channel = settings.get("SVR_Channel")
config.CRD_Channel = settings.get("CRD_Channel") config.CRD_Channel = settings.get("CRD_Channel")
config.PKT_Channel = settings.get("PKT_Channel") config.PKT_Channel = settings.get("PKT_Channel")
@@ -36,6 +49,12 @@ function pocket.load_config()
local cfv = util.new_validator() local cfv = util.new_validator()
cfv.assert_type_bool(config.GreenPuPellet)
cfv.assert_type_int(config.TempScale)
cfv.assert_range(config.TempScale, 1, 4)
cfv.assert_type_int(config.EnergyScale)
cfv.assert_range(config.EnergyScale, 1, 3)
cfv.assert_channel(config.SVR_Channel) cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.CRD_Channel) cfv.assert_channel(config.CRD_Channel)
cfv.assert_channel(config.PKT_Channel) cfv.assert_channel(config.PKT_Channel)
@@ -58,26 +77,332 @@ function pocket.load_config()
return cfv.valid() return cfv.valid()
end end
---@enum POCKET_APP_ID
local APP_ID = {
-- core UI
ROOT = 1,
LOADER = 2,
-- main apps
UNITS = 3,
FACILITY = 4,
CONTROL = 5,
PROCESS = 6,
WASTE = 7,
GUIDE = 8,
ABOUT = 9,
RADMON = 10,
-- diagnostic apps
ALARMS = 11,
COMPS = 12,
-- count
NUM_APPS = 12
}
pocket.APP_ID = APP_ID
---@class nav_tree_page
---@field _p nav_tree_page|nil page's parent
---@field _c nav_tree_page[] page's children
---@field nav_to function function to navigate to this page
---@field switcher function|nil function to switch between children
---@field tasks function[] tasks to run while viewing this page
-- initialize the page navigation system
---@param smem pkt_shared_memory
function pocket.init_nav(smem)
local self = {
pane = nil, ---@type AppMultiPane|MultiPane|nil
sidebar = nil, ---@type Sidebar|nil
apps = {}, ---@type pocket_app[]
containers = {}, ---@type Container[]
help_map = {}, ---@type { [string]: function }
help_return = nil, ---@type POCKET_APP_ID|nil
loader_return = nil, ---@type POCKET_APP_ID|nil
cur_app = APP_ID.ROOT
}
self.cur_page = self.root
---@class pocket_nav
local nav = {}
-- set the root pane element to switch between apps with
---@param root_pane MultiPane
function nav.set_pane(root_pane) self.pane = root_pane end
-- link sidebar element
---@param sidebar Sidebar
function nav.set_sidebar(sidebar) self.sidebar = sidebar end
-- register an app
---@param app_id POCKET_APP_ID app ID
---@param container Container element that contains this app (usually a Div)
---@param pane? AppMultiPane|MultiPane multipane if this is a simple paned app, then nav_to must be a number
---@param require_sv? boolean true to specifiy if this app should be unloaded when the supervisor connection is lost
---@param require_api? boolean true to specifiy if this app should be unloaded when the api connection is lost
function nav.register_app(app_id, container, pane, require_sv, require_api)
---@class pocket_app
local app = {
loaded = false,
cur_page = nil, ---@type nav_tree_page|nil
pane = pane,
paned_pages = {}, ---@type nav_tree_page[]
sidebar_items = {} ---@type sidebar_entry[]
}
app.load = function () app.loaded = true end
app.unload = function () app.loaded = false end
-- check which connections this requires (for unload)
---@return boolean requires_sv, boolean requires_api
function app.check_requires() return require_sv or false, require_api or false end
-- check if any connection is required (for load)
function app.requires_conn() return require_sv or require_api or false end
-- delayed set of the pane if it wasn't ready at the start
---@param root_pane AppMultiPane|MultiPane multipane
function app.set_root_pane(root_pane)
app.pane = root_pane
end
-- configure the sidebar
---@param items sidebar_entry[]
function app.set_sidebar(items)
app.sidebar_items = items
-- only modify the sidebar if this app is still open
if self.cur_app == app_id then
if self.sidebar then self.sidebar.update(items) end
end
end
-- function to run on initial load into memory
---@param on_load function callback
function app.set_load(on_load)
app.load = function ()
app.loaded = true -- must flag first so it can't be repeatedly attempted
on_load()
end
end
-- function to run to close out the app
---@param on_unload function callback
function app.set_unload(on_unload)
app.unload = function ()
app.loaded = false
on_unload()
end
end
-- if a pane was provided, this will switch between numbered pages
---@param idx integer page index
function app.switcher(idx)
if app.paned_pages[idx] then
app.paned_pages[idx].nav_to()
end
end
-- create a new page entry in the app's page navigation tree
---@param parent nav_tree_page|nil a parent page or nil to set this as the root
---@param nav_to function|integer function to navigate to this page or pane index
---@return nav_tree_page new_page this new page
function app.new_page(parent, nav_to)
---@type nav_tree_page
local page = { _p = parent, _c = {}, nav_to = function () end, switcher = function () end, tasks = {} }
if parent == nil and app.cur_page == nil then
app.cur_page = page
end
if type(nav_to) == "number" then
app.paned_pages[nav_to] = page
function page.nav_to()
app.cur_page = page
if app.pane then app.pane.set_value(nav_to) end
end
else
function page.nav_to()
app.cur_page = page
nav_to()
end
end
-- switch between children
---@param id integer child ID
function page.switcher(id) if page._c[id] then page._c[id].nav_to() end end
if parent ~= nil then
table.insert(page._p._c, page)
end
return page
end
-- delete paned pages and clear the current page
function app.delete_pages()
app.paned_pages = {}
app.cur_page = nil
end
-- get the currently active page
function app.get_current_page() return app.cur_page end
-- attempt to navigate up the tree
---@return boolean success true if successfully navigated up
function app.nav_up()
local parent = app.cur_page._p
if parent then parent.nav_to() end
return parent ~= nil
end
self.apps[app_id] = app
self.containers[app_id] = container
return app
end
-- open an app
---@param app_id POCKET_APP_ID
---@param on_ready? function
function nav.open_app(app_id, on_ready)
-- reset help return on navigating out of an app
if app_id == APP_ID.ROOT then self.help_return = nil end
local app = self.apps[app_id]
if app then
local p_comms = smem.pkt_sys.pocket_comms
local req_sv, req_api = app.check_requires()
if (req_sv and not p_comms.is_sv_linked()) or (req_api and not p_comms.is_api_linked()) then
-- report required connction(s)
iocontrol.get_db().loader_require = { sv = req_sv, api = req_api }
iocontrol.get_db().ps.toggle("loader_reqs")
-- bring up the app loader
self.loader_return = app_id
app_id = APP_ID.LOADER
app = self.apps[app_id]
else self.loader_return = nil end
if not app.loaded then smem.q.mq_render.push_data(MQ__RENDER_DATA.LOAD_APP, { app_id, on_ready }) end
self.cur_app = app_id
self.pane.set_value(app_id)
if #app.sidebar_items > 0 then
self.sidebar.update(app.sidebar_items)
end
if app.loaded and on_ready then on_ready() end
else
log.debug("tried to open unknown app")
end
end
-- go home (open the home screen app)
function nav.go_home() nav.open_app(APP_ID.ROOT) end
-- open the app that was blocked on connecting
function nav.on_loader_connected()
if self.loader_return then
nav.open_app(self.loader_return)
end
end
-- load a given app
---@param app_id POCKET_APP_ID
function nav.load_app(app_id)
self.apps[app_id].load()
end
-- unload api-dependent apps
function nav.unload_api()
for id, app in pairs(self.apps) do
local _, api = app.check_requires()
if app.loaded and api then
if id == self.cur_app then nav.open_app(APP_ID.ROOT) end
app.unload()
end
end
end
-- unload supervisor-dependent apps
function nav.unload_sv()
for id, app in pairs(self.apps) do
local sv, _ = app.check_requires()
if app.loaded and sv then
if id == self.cur_app then nav.open_app(APP_ID.ROOT) end
app.unload()
end
end
end
-- get a list of the app containers (usually Div elements)
function nav.get_containers() return self.containers end
-- get the currently active page
---@return nav_tree_page|nil
function nav.get_current_page()
return self.apps[self.cur_app].get_current_page()
end
-- attempt to navigate up within the active app, otherwise open home page<br>
-- except, this will go back to a prior app if leaving the help app after open_help was used
function nav.nav_up()
-- return out of help if opened with open_help
if self.help_return then
nav.open_app(self.help_return)
self.help_return = nil
return
end
local app = self.apps[self.cur_app]
log.debug("attempting app nav up for app " .. self.cur_app)
if not app.nav_up() then
log.debug("internal app nav up failed, going to home screen")
nav.open_app(APP_ID.ROOT)
end
end
-- open the help app, to show the reference for a key
function nav.open_help(key)
self.help_return = self.cur_app
nav.open_app(APP_ID.GUIDE, function ()
if self.help_map[key] then self.help_map[key]() end
end)
end
-- link the help map from the guide app
---@param map { [string]: function }
function nav.link_help(map) self.help_map = map end
return nav
end
-- pocket coordinator + supervisor communications -- pocket coordinator + supervisor communications
---@nodiscard ---@nodiscard
---@param version string pocket version ---@param version string pocket version
---@param nic nic network interface device ---@param nic nic network interface device
---@param sv_watchdog watchdog ---@param sv_watchdog watchdog
---@param api_watchdog watchdog ---@param api_watchdog watchdog
function pocket.comms(version, nic, sv_watchdog, api_watchdog) ---@param nav pocket_nav
function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
local self = { local self = {
sv = { sv = {
linked = false, linked = false,
addr = comms.BROADCAST, addr = comms.BROADCAST,
seq_num = 0, seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, ---@type nil|integer r_seq_num = nil, ---@type nil|integer
last_est_ack = ESTABLISH_ACK.ALLOW last_est_ack = ESTABLISH_ACK.ALLOW
}, },
api = { api = {
linked = false, linked = false,
addr = comms.BROADCAST, addr = comms.BROADCAST,
seq_num = 0, seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, ---@type nil|integer r_seq_num = nil, ---@type nil|integer
last_est_ack = ESTABLISH_ACK.ALLOW last_est_ack = ESTABLISH_ACK.ALLOW
}, },
establish_delay_counter = 0 establish_delay_counter = 0
@@ -135,11 +460,13 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
-- attempt supervisor connection establishment -- attempt supervisor connection establishment
local function _send_sv_establish() local function _send_sv_establish()
self.sv.r_seq_num = nil
_send_sv(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT }) _send_sv(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
end end
-- attempt coordinator API connection establishment -- attempt coordinator API connection establishment
local function _send_api_establish() local function _send_api_establish()
self.api.r_seq_num = nil
_send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT, comms.api_version }) _send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT, comms.api_version })
end end
@@ -163,6 +490,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
-- close connection to the supervisor -- close connection to the supervisor
function public.close_sv() function public.close_sv()
sv_watchdog.cancel() sv_watchdog.cancel()
nav.unload_sv()
self.sv.linked = false self.sv.linked = false
self.sv.r_seq_num = nil self.sv.r_seq_num = nil
self.sv.addr = comms.BROADCAST self.sv.addr = comms.BROADCAST
@@ -172,6 +500,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
-- close connection to coordinator API server -- close connection to coordinator API server
function public.close_api() function public.close_api()
api_watchdog.cancel() api_watchdog.cancel()
nav.unload_api()
self.api.linked = false self.api.linked = false
self.api.r_seq_num = nil self.api.r_seq_num = nil
self.api.addr = comms.BROADCAST self.api.addr = comms.BROADCAST
@@ -187,7 +516,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
-- attempt to re-link if any of the dependent links aren't active -- attempt to re-link if any of the dependent links aren't active
function public.link_update() function public.link_update()
if not self.sv.linked then if not self.sv.linked then
iocontrol.report_link_state(util.trinary(self.api.linked, LINK_STATE.API_LINK_ONLY, LINK_STATE.UNLINKED)) if self.api.linked then
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY, false, nil)
else
iocontrol.report_link_state(LINK_STATE.UNLINKED, false, false)
end
if self.establish_delay_counter <= 0 then if self.establish_delay_counter <= 0 then
_send_sv_establish() _send_sv_establish()
@@ -196,7 +529,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
self.establish_delay_counter = self.establish_delay_counter - 1 self.establish_delay_counter = self.establish_delay_counter - 1
end end
elseif not self.api.linked then elseif not self.api.linked then
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY) iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, nil, false)
if self.establish_delay_counter <= 0 then if self.establish_delay_counter <= 0 then
_send_api_establish() _send_api_establish()
@@ -204,9 +537,6 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
else else
self.establish_delay_counter = self.establish_delay_counter - 1 self.establish_delay_counter = self.establish_delay_counter - 1
end end
else
-- linked, all good!
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
end end
end end
@@ -229,11 +559,62 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end
end end
-- supervisor get connected computers
function public.diag__get_computers()
if self.sv.linked then _send_sv(MGMT_TYPE.INFO_LIST_CMP, {}) end
end
-- coordinator get facility app data
function public.api__get_facility()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_FAC_DTL, {}) end
end
-- coordinator get unit data -- coordinator get unit data
function public.api__get_unit(unit) function public.api__get_unit(unit)
if self.api.linked then _send_api(CRDN_TYPE.API_GET_UNIT, { unit }) end if self.api.linked then _send_api(CRDN_TYPE.API_GET_UNIT, { unit }) end
end end
-- coordinator get control app data
function public.api__get_control()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_CTRL, {}) end
end
-- coordinator get process app data
function public.api__get_process()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_PROC, {}) end
end
-- coordinator get waste app data
function public.api__get_waste()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_WASTE, {}) end
end
-- coordinator get radiation app data
function public.api__get_rad()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_RAD, {}) end
end
-- send a facility command
---@param cmd FAC_COMMAND command
---@param option any? optional option options for the optional options (like waste mode)
function public.send_fac_command(cmd, option)
_send_api(CRDN_TYPE.FAC_CMD, { cmd, option })
end
-- send the auto process control configuration with a start command
---@param auto_cfg [ PROCESS, number, number, number, number[] ]
function public.send_auto_start(auto_cfg)
_send_api(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.START, table.unpack(auto_cfg) })
end
-- send a unit command
---@param cmd UNIT_COMMAND command
---@param unit integer unit ID
---@param option any? optional option options for the optional options (like burn rate)
function public.send_unit_command(cmd, unit, option)
_send_api(CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
end
-- parse a packet -- parse a packet
---@param side string ---@param side string
---@param sender integer ---@param sender integer
@@ -274,7 +655,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
local ok = util.trinary(max == nil, packet.length == length, packet.length >= length and packet.length <= (max or 0)) local ok = util.trinary(max == nil, packet.length == length, packet.length >= length and packet.length <= (max or 0))
if not ok then if not ok then
local fmt = "[comms] RX_PACKET{r_chan=%d,proto=%d,type=%d}: packet length mismatch -> expect %d != actual %d" local fmt = "[comms] RX_PACKET{r_chan=%d,proto=%d,type=%d}: packet length mismatch -> expect %d != actual %d"
log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type)) log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type, length, packet.length))
end end
return ok return ok
end end
@@ -289,6 +670,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
---@param packet mgmt_frame|crdn_frame|nil ---@param packet mgmt_frame|crdn_frame|nil
function public.handle_packet(packet) function public.handle_packet(packet)
local diag = iocontrol.get_db().diag local diag = iocontrol.get_db().diag
local ps = iocontrol.get_db().ps
if packet ~= nil then if packet ~= nil then
local l_chan = packet.scada_frame.local_channel() local l_chan = packet.scada_frame.local_channel()
@@ -301,16 +683,16 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
elseif r_chan == config.CRD_Channel then elseif r_chan == config.CRD_Channel then
-- check sequence number -- check sequence number
if self.api.r_seq_num == nil then if self.api.r_seq_num == nil then
self.api.r_seq_num = packet.scada_frame.seq_num() self.api.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.connected and ((self.api.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then elseif self.api.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order (API): last = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) log.warning("sequence out-of-order (API): next = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return return
elseif self.api.linked and (src_addr ~= self.api.addr) then elseif self.api.linked and (src_addr ~= self.api.addr) then
log.debug("received packet from unknown computer " .. src_addr .. " while linked (API expected " .. self.api.addr .. log.debug("received packet from unknown computer " .. src_addr .. " while linked (API expected " .. self.api.addr ..
"); channel in use by another system?") "); channel in use by another system?")
return return
else else
self.api.r_seq_num = packet.scada_frame.seq_num() self.api.r_seq_num = packet.scada_frame.seq_num() + 1
end end
-- feed watchdog on valid sequence number -- feed watchdog on valid sequence number
@@ -319,13 +701,79 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
if protocol == PROTOCOL.SCADA_CRDN then if protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame ---@cast packet crdn_frame
if self.api.linked then if self.api.linked then
if packet.type == CRDN_TYPE.API_GET_FAC then if packet.type == CRDN_TYPE.FAC_CMD then
-- facility command acknowledgement
if packet.length >= 2 then
local cmd = packet.data[1]
local ack = packet.data[2] == true
if cmd == FAC_COMMAND.SCRAM_ALL then
iocontrol.get_db().facility.scram_ack(ack)
elseif cmd == FAC_COMMAND.STOP then
iocontrol.get_db().facility.stop_ack(ack)
elseif cmd == FAC_COMMAND.START then
iocontrol.get_db().facility.start_ack(ack)
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
iocontrol.get_db().facility.ack_alarms_ack(ack)
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
elseif cmd == FAC_COMMAND.SET_PU_FB then
elseif cmd == FAC_COMMAND.SET_SPS_LP then
else
log.debug(util.c("received facility command ack with unknown command ", cmd))
end
else
log.debug("SCADA_CRDN facility command ack packet length mismatch")
end
elseif packet.type == CRDN_TYPE.UNIT_CMD then
-- unit command acknowledgement
if packet.length == 3 then
local cmd = packet.data[1]
local unit_id = packet.data[2]
local ack = packet.data[3] == true
local unit = iocontrol.get_db().units[unit_id] ---@type pioctl_unit
if unit ~= nil then
if cmd == UNIT_COMMAND.SCRAM then
unit.scram_ack(ack)
elseif cmd == UNIT_COMMAND.START then
unit.start_ack(ack)
elseif cmd == UNIT_COMMAND.RESET_RPS then
unit.reset_rps_ack(ack)
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
unit.ack_alarms_ack(ack)
else
log.debug(util.c("received unsupported unit command ack for command ", cmd))
end
end
end
elseif packet.type == CRDN_TYPE.API_GET_FAC then
if _check_length(packet, 11) then if _check_length(packet, 11) then
iocontrol.record_facility_data(packet.data) iocontrol.rx.record_facility_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_FAC_DTL then
if _check_length(packet, 12) then
iocontrol.rx.record_fac_detail_data(packet.data)
end end
elseif packet.type == CRDN_TYPE.API_GET_UNIT then elseif packet.type == CRDN_TYPE.API_GET_UNIT then
if _check_length(packet, 9) then if _check_length(packet, 12) and type(packet.data[1]) == "number" and iocontrol.get_db().units[packet.data[1]] then
iocontrol.record_unit_data(packet.data) iocontrol.rx.record_unit_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_CTRL then
if _check_length(packet, #iocontrol.get_db().units) then
iocontrol.rx.record_control_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_PROC then
if _check_length(packet, #iocontrol.get_db().units + 1) then
iocontrol.rx.record_process_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_WASTE then
if _check_length(packet, #iocontrol.get_db().units + 1) then
iocontrol.rx.record_waste_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_RAD then
if _check_length(packet, #iocontrol.get_db().units + 1) then
iocontrol.rx.record_radiation_data(packet.data)
end end
else _fail_type(packet) end else _fail_type(packet) end
else else
@@ -353,6 +801,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
elseif packet.type == MGMT_TYPE.CLOSE then elseif packet.type == MGMT_TYPE.CLOSE then
-- handle session close -- handle session close
api_watchdog.cancel() api_watchdog.cancel()
nav.unload_api()
self.api.linked = false self.api.linked = false
self.api.r_seq_num = nil self.api.r_seq_num = nil
self.api.addr = comms.BROADCAST self.api.addr = comms.BROADCAST
@@ -371,18 +820,19 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
-- get configuration -- get configuration
local conf = { num_units = fac_config[1], cooling = fac_config[2] } local conf = { num_units = fac_config[1], cooling = fac_config[2] }
---@todo unit options iocontrol.init_fac(conf)
iocontrol.init_fac(conf, 1)
log.info("coordinator connection established") log.info("coordinator connection established")
self.establish_delay_counter = 0 self.establish_delay_counter = 0
self.api.linked = true self.api.linked = true
self.api.addr = src_addr self.api.addr = src_addr
iocontrol.report_crd_link_error("")
if self.sv.linked then if self.sv.linked then
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr) iocontrol.report_link_state(LINK_STATE.LINKED, nil, self.api.addr)
else else
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY) iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY, nil, self.api.addr)
end end
else else
log.debug("invalid facility configuration table received from coordinator, establish failed") log.debug("invalid facility configuration table received from coordinator, establish failed")
@@ -390,24 +840,29 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
else else
log.debug("received coordinator establish allow without facility configuration") log.debug("received coordinator establish allow without facility configuration")
end end
elseif est_ack == ESTABLISH_ACK.DENY then
if self.api.last_est_ack ~= est_ack then
log.info("coordinator connection denied")
end
elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.api.last_est_ack ~= est_ack then
log.info("coordinator connection denied due to collision")
end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.api.last_est_ack ~= est_ack then
log.info("coordinator comms version mismatch")
end
elseif est_ack == ESTABLISH_ACK.BAD_API_VERSION then
if self.api.last_est_ack ~= est_ack then
log.info("coordinator api version mismatch")
end
else else
log.debug("coordinator SCADA_MGMT establish packet reply unsupported") if self.api.last_est_ack ~= est_ack then
if est_ack == ESTABLISH_ACK.DENY then
log.info("coordinator connection denied")
iocontrol.report_crd_link_error("denied")
elseif est_ack == ESTABLISH_ACK.COLLISION then
log.info("coordinator connection denied due to collision")
iocontrol.report_crd_link_error("collision")
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
log.info("coordinator comms version mismatch")
iocontrol.report_crd_link_error("comms version mismatch")
elseif est_ack == ESTABLISH_ACK.BAD_API_VERSION then
log.info("coordinator api version mismatch")
iocontrol.report_crd_link_error("API version mismatch")
else
log.debug("coordinator SCADA_MGMT establish packet reply unsupported")
iocontrol.report_crd_link_error("unknown reply")
end
end
-- unlink
self.api.addr = comms.BROADCAST
self.api.linked = false
end end
self.api.last_est_ack = est_ack self.api.last_est_ack = est_ack
@@ -421,16 +876,16 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
elseif r_chan == config.SVR_Channel then elseif r_chan == config.SVR_Channel then
-- check sequence number -- check sequence number
if self.sv.r_seq_num == nil then if self.sv.r_seq_num == nil then
self.sv.r_seq_num = packet.scada_frame.seq_num() self.sv.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.connected and ((self.sv.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then elseif self.sv.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order (SVR): last = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) log.warning("sequence out-of-order (SVR): next = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return return
elseif self.sv.linked and (src_addr ~= self.sv.addr) then elseif self.sv.linked and (src_addr ~= self.sv.addr) then
log.debug("received packet from unknown computer " .. src_addr .. " while linked (SVR expected " .. self.sv.addr .. log.debug("received packet from unknown computer " .. src_addr .. " while linked (SVR expected " .. self.sv.addr ..
"); channel in use by another system?") "); channel in use by another system?")
return return
else else
self.sv.r_seq_num = packet.scada_frame.seq_num() self.sv.r_seq_num = packet.scada_frame.seq_num() + 1
end end
-- feed watchdog on valid sequence number -- feed watchdog on valid sequence number
@@ -459,6 +914,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
elseif packet.type == MGMT_TYPE.CLOSE then elseif packet.type == MGMT_TYPE.CLOSE then
-- handle session close -- handle session close
sv_watchdog.cancel() sv_watchdog.cancel()
nav.unload_sv()
self.sv.linked = false self.sv.linked = false
self.sv.r_seq_num = nil self.sv.r_seq_num = nil
self.sv.addr = comms.BROADCAST self.sv.addr = comms.BROADCAST
@@ -466,23 +922,23 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
elseif packet.type == MGMT_TYPE.DIAG_TONE_GET then elseif packet.type == MGMT_TYPE.DIAG_TONE_GET then
if _check_length(packet, 8) then if _check_length(packet, 8) then
for i = 1, #packet.data do for i = 1, #packet.data do
diag.tone_test.tone_indicators[i].update(packet.data[i] == true) ps.publish("alarm_tone_" .. i, packet.data[i] == true)
end end
end end
elseif packet.type == MGMT_TYPE.DIAG_TONE_SET then elseif packet.type == MGMT_TYPE.DIAG_TONE_SET then
if packet.length == 1 and packet.data[1] == false then if packet.length == 1 and packet.data[1] == false then
diag.tone_test.ready_warn.set_value("testing denied") ps.publish("alarm_ready_warn", "testing denied")
log.debug("supervisor SCADA diag tone set failed") log.debug("supervisor SCADA diag tone set failed")
elseif packet.length == 2 and type(packet.data[2]) == "table" then elseif packet.length == 2 and type(packet.data[2]) == "table" then
local ready = packet.data[1] local ready = packet.data[1]
local states = packet.data[2] local states = packet.data[2]
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready")) ps.publish("alarm_ready_warn", util.trinary(ready, "", "system not idle"))
for i = 1, #states do for i = 1, #states do
if diag.tone_test.tone_buttons[i] ~= nil then if diag.tone_test.tone_buttons[i] ~= nil then
diag.tone_test.tone_buttons[i].set_value(states[i] == true) diag.tone_test.tone_buttons[i].set_value(states[i] == true)
diag.tone_test.tone_indicators[i].update(states[i] == true) ps.publish("alarm_tone_" .. i, states[i] == true)
end end
end end
else else
@@ -490,13 +946,13 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
end end
elseif packet.type == MGMT_TYPE.DIAG_ALARM_SET then elseif packet.type == MGMT_TYPE.DIAG_ALARM_SET then
if packet.length == 1 and packet.data[1] == false then if packet.length == 1 and packet.data[1] == false then
diag.tone_test.ready_warn.set_value("testing denied") ps.publish("alarm_ready_warn", "testing denied")
log.debug("supervisor SCADA diag alarm set failed") log.debug("supervisor SCADA diag alarm set failed")
elseif packet.length == 2 and type(packet.data[2]) == "table" then elseif packet.length == 2 and type(packet.data[2]) == "table" then
local ready = packet.data[1] local ready = packet.data[1]
local states = packet.data[2] local states = packet.data[2]
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready")) ps.publish("alarm_ready_warn", util.trinary(ready, "", "system not idle"))
for i = 1, #states do for i = 1, #states do
if diag.tone_test.alarm_buttons[i] ~= nil then if diag.tone_test.alarm_buttons[i] ~= nil then
@@ -506,6 +962,8 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
else else
log.debug("supervisor SCADA diag alarm set packet length/type mismatch") log.debug("supervisor SCADA diag alarm set packet length/type mismatch")
end end
elseif packet.type == MGMT_TYPE.INFO_LIST_CMP then
iocontrol.rx.record_network_data(packet.data)
else _fail_type(packet) end else _fail_type(packet) end
elseif packet.type == MGMT_TYPE.ESTABLISH then elseif packet.type == MGMT_TYPE.ESTABLISH then
-- connection with supervisor established -- connection with supervisor established
@@ -518,25 +976,33 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
self.sv.linked = true self.sv.linked = true
self.sv.addr = src_addr self.sv.addr = src_addr
iocontrol.report_svr_link_error("")
if self.api.linked then if self.api.linked then
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr) iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, nil)
else else
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY) iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, self.sv.addr, nil)
end
elseif est_ack == ESTABLISH_ACK.DENY then
if self.sv.last_est_ack ~= est_ack then
log.info("supervisor connection denied")
end
elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.sv.last_est_ack ~= est_ack then
log.info("supervisor connection denied due to collision")
end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.sv.last_est_ack ~= est_ack then
log.info("supervisor comms version mismatch")
end end
else else
log.debug("supervisor SCADA_MGMT establish packet reply unsupported") if self.sv.last_est_ack ~= est_ack then
if est_ack == ESTABLISH_ACK.DENY then
log.info("supervisor connection denied")
iocontrol.report_svr_link_error("denied")
elseif est_ack == ESTABLISH_ACK.COLLISION then
log.info("supervisor connection denied due to collision")
iocontrol.report_svr_link_error("collision")
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
log.info("supervisor comms version mismatch")
iocontrol.report_svr_link_error("comms version mismatch")
else
log.debug("supervisor SCADA_MGMT establish packet reply unsupported")
iocontrol.report_svr_link_error("unknown reply")
end
end
-- unlink
self.sv.addr = comms.BROADCAST
self.sv.linked = false
end end
self.sv.last_est_ack = est_ack self.sv.last_est_ack = est_ack
@@ -559,6 +1025,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
---@nodiscard ---@nodiscard
function public.is_api_linked() return self.api.linked end function public.is_api_linked() return self.api.linked end
-- check if we are still linked with the supervisor and coordinator
---@nodiscard
function public.is_linked() return self.sv.linked and self.api.linked end
return public return public
end end

166
pocket/process.lua Normal file
View File

@@ -0,0 +1,166 @@
--
-- Process Control Management
--
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local util = require("scada-common.util")
local F_CMD = comms.FAC_COMMAND
local U_CMD = comms.UNIT_COMMAND
---@class pocket_process_controller
local process = {}
local self = {
io = nil, ---@type ioctl
comms = nil ---@type pocket_comms
}
-- initialize the process controller
---@param iocontrol pocket_ioctl iocontrl system
---@param pocket_comms pocket_comms pocket communications
function process.init(iocontrol, pocket_comms)
self.io = iocontrol
self.comms = pocket_comms
end
------------------------------
--#region FACILITY COMMANDS --
-- facility SCRAM command
function process.fac_scram()
self.comms.send_fac_command(F_CMD.SCRAM_ALL)
log.debug("PROCESS: FAC SCRAM ALL")
end
-- facility alarm acknowledge command
function process.fac_ack_alarms()
self.comms.send_fac_command(F_CMD.ACK_ALL_ALARMS)
log.debug("PROCESS: FAC ACK ALL ALARMS")
end
--#endregion
------------------------------
--------------------------
--#region UNIT COMMANDS --
-- start reactor
---@param id integer unit ID
function process.start(id)
self.io.units[id].control_state = true
self.comms.send_unit_command(U_CMD.START, id)
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
end
-- SCRAM reactor
---@param id integer unit ID
function process.scram(id)
self.io.units[id].control_state = false
self.comms.send_unit_command(U_CMD.SCRAM, id)
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
end
-- reset reactor protection system
---@param id integer unit ID
function process.reset_rps(id)
self.comms.send_unit_command(U_CMD.RESET_RPS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
end
-- set burn rate
---@param id integer unit ID
---@param rate number burn rate
function process.set_rate(id, rate)
self.comms.send_unit_command(U_CMD.SET_BURN, id, rate)
log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
end
-- assign a unit to a group
---@param unit_id integer unit ID
---@param group_id integer|0 group ID or 0 for independent
function process.set_group(unit_id, group_id)
self.comms.send_unit_command(U_CMD.SET_GROUP, unit_id, group_id)
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
end
-- set waste mode
---@param id integer unit ID
---@param mode integer waste mode
function process.set_unit_waste(id, mode)
self.comms.send_unit_command(U_CMD.SET_WASTE, id, mode)
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
end
-- acknowledge all alarms
---@param id integer unit ID
function process.ack_all_alarms(id)
self.comms.send_unit_command(U_CMD.ACK_ALL_ALARMS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
end
-- acknowledge an alarm
---@param id integer unit ID
---@param alarm integer alarm ID
function process.ack_alarm(id, alarm)
self.comms.send_unit_command(U_CMD.ACK_ALARM, id, alarm)
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
end
-- reset an alarm
---@param id integer unit ID
---@param alarm integer alarm ID
function process.reset_alarm(id, alarm)
self.comms.send_unit_command(U_CMD.RESET_ALARM, id, alarm)
log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
end
-- #endregion
--------------------------
---------------------------------
--#region AUTO PROCESS CONTROL --
-- process start command
---@param mode PROCESS process control mode
---@param burn_target number burn rate target
---@param charge_target number charge level target
---@param gen_target number generation rate target
---@param limits number[] unit burn rate limits
function process.process_start(mode, burn_target, charge_target, gen_target, limits)
self.comms.send_auto_start({ mode, burn_target, charge_target, gen_target, limits })
log.debug("PROCESS: START AUTO CTRL")
end
-- process stop command
function process.process_stop()
self.comms.send_fac_command(F_CMD.STOP)
log.debug("PROCESS: STOP AUTO CTRL")
end
-- set automatic process control waste mode
---@param product WASTE_PRODUCT waste product for auto control
function process.set_process_waste(product)
self.comms.send_fac_command(F_CMD.SET_WASTE_MODE, product)
log.debug(util.c("PROCESS: SET WASTE ", product))
end
-- set automatic process control plutonium fallback
---@param enabled boolean whether to enable plutonium fallback
function process.set_pu_fallback(enabled)
self.comms.send_fac_command(F_CMD.SET_PU_FB, enabled)
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
end
-- set automatic process control SPS usage at low power
---@param enabled boolean whether to enable SPS usage at low power
function process.set_sps_low_power(enabled)
self.comms.send_fac_command(F_CMD.SET_SPS_LP, enabled)
log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled))
end
-- #endregion
---------------------------------
return process

View File

@@ -8,7 +8,7 @@ local style = require("pocket.ui.style")
local core = require("graphics.core") local core = require("graphics.core")
local flasher = require("graphics.flasher") local flasher = require("graphics.flasher")
local DisplayBox = require("graphics.elements.displaybox") local DisplayBox = require("graphics.elements.DisplayBox")
---@class pocket_renderer ---@class pocket_renderer
local renderer = {} local renderer = {}
@@ -92,4 +92,20 @@ function renderer.handle_mouse(event)
end end
end end
-- handle a keyboard event
---@param event key_interaction|nil
function renderer.handle_key(event)
if ui.display ~= nil and event ~= nil then
ui.display.handle_key(event)
end
end
-- handle a paste event
---@param text string
function renderer.handle_paste(text)
if ui.display ~= nil then
ui.display.handle_paste(text)
end
end
return renderer return renderer

View File

@@ -2,27 +2,37 @@
-- SCADA System Access on a Pocket Computer -- SCADA System Access on a Pocket Computer
-- --
---@diagnostic disable-next-line: lowercase-global
pocket = pocket or periphemu -- luacheck: ignore pocket
local _is_pocket_env = pocket -- luacheck: ignore pocket
require("/initenv").init_env() require("/initenv").init_env()
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local network = require("scada-common.network") local network = require("scada-common.network")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core")
local configure = require("pocket.configure") local configure = require("pocket.configure")
local iocontrol = require("pocket.iocontrol") local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket") local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer") local renderer = require("pocket.renderer")
local threads = require("pocket.threads")
local POCKET_VERSION = "v0.9.1-alpha" local POCKET_VERSION = "v1.0.3"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
-- check environment (allows Pocket or CraftOS-PC)
if not _is_pocket_env then
println("You can only use this application on a pocket computer.")
return
end
---------------------------------------- ----------------------------------------
-- get configuration -- get configuration
---------------------------------------- ----------------------------------------
@@ -72,9 +82,51 @@ local function main()
iocontrol.get_db().version = POCKET_VERSION iocontrol.get_db().version = POCKET_VERSION
---------------------------------------- ----------------------------------------
-- setup communications & clocks -- memory allocation
---------------------------------------- ----------------------------------------
-- shared memory across threads
---@class pkt_shared_memory
local __shared_memory = {
-- pocket system state flags
---@class pkt_state
pkt_state = {
ui_ok = false,
ui_error = nil,
shutdown = false
},
-- core pocket devices
pkt_dev = {
modem = ppm.get_wireless_modem()
},
-- system objects
pkt_sys = {
nic = nil, ---@type nic
pocket_comms = nil, ---@type pocket_comms
sv_wd = nil, ---@type watchdog
api_wd = nil, ---@type watchdog
nav = nil ---@type pocket_nav
},
-- message queues
q = {
mq_render = mqueue.new()
}
}
local smem_dev = __shared_memory.pkt_dev
local smem_sys = __shared_memory.pkt_sys
local pkt_state = __shared_memory.pkt_state
----------------------------------------
-- setup system
----------------------------------------
smem_sys.nav = pocket.init_nav(__shared_memory)
-- message authentication init -- message authentication init
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
network.init_mac(config.AuthKey) network.init_mac(config.AuthKey)
@@ -83,112 +135,59 @@ local function main()
iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED) iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED)
-- get the communications modem -- get the communications modem
local modem = ppm.get_wireless_modem() if smem_dev.modem == nil then
if modem == nil then
println("startup> wireless modem not found: please craft the pocket computer with a wireless modem") println("startup> wireless modem not found: please craft the pocket computer with a wireless modem")
log.fatal("startup> no wireless modem on startup") log.fatal("startup> no wireless modem on startup")
return return
end end
-- create connection watchdogs -- create connection watchdogs
local conn_wd = { smem_sys.sv_wd = util.new_watchdog(config.ConnTimeout)
sv = util.new_watchdog(config.ConnTimeout), smem_sys.sv_wd.cancel()
api = util.new_watchdog(config.ConnTimeout) smem_sys.api_wd = util.new_watchdog(config.ConnTimeout)
} smem_sys.api_wd.cancel()
conn_wd.sv.cancel()
conn_wd.api.cancel()
log.debug("startup> conn watchdogs created") log.debug("startup> conn watchdogs created")
-- create network interface then setup comms -- create network interface then setup comms
local nic = network.nic(modem) smem_sys.nic = network.nic(smem_dev.modem)
local pocket_comms = pocket.comms(POCKET_VERSION, nic, conn_wd.sv, conn_wd.api) smem_sys.pocket_comms = pocket.comms(POCKET_VERSION, smem_sys.nic, smem_sys.sv_wd, smem_sys.api_wd, smem_sys.nav)
log.debug("startup> comms init") log.debug("startup> comms init")
-- base loop clock (2Hz, 10 ticks)
local MAIN_CLOCK = 0.5
local loop_clock = util.new_clock(MAIN_CLOCK)
-- init I/O control -- init I/O control
iocontrol.init_core(pocket_comms) iocontrol.init_core(smem_sys.pocket_comms, smem_sys.nav, config)
---------------------------------------- ----------------------------------------
-- start the UI -- start the UI
---------------------------------------- ----------------------------------------
local ui_ok, message = renderer.try_start_ui() local ui_message
if not ui_ok then pkt_state.ui_ok, ui_message = renderer.try_start_ui()
println(util.c("UI error: ", message)) if not pkt_state.ui_ok then
log.error(util.c("startup> GUI render failed with error ", message)) println(util.c("UI error: ", ui_message))
else log.error(util.c("startup> GUI render failed with error ", ui_message))
-- start clock
loop_clock.start()
end end
---------------------------------------- ----------------------------------------
-- main event loop -- start system
---------------------------------------- ----------------------------------------
if ui_ok then if pkt_state.ui_ok then
-- start connection watchdogs -- init threads
conn_wd.sv.feed() local main_thread = threads.thread__main(__shared_memory)
conn_wd.api.feed() local render_thread = threads.thread__render(__shared_memory)
log.debug("startup> conn watchdogs started")
local io_db = iocontrol.get_db() log.info("startup> completed")
local nav = io_db.nav
-- main event loop -- run threads
while true do parallel.waitForAll(main_thread.p_exec, render_thread.p_exec)
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event
if event == "timer" then
if loop_clock.is_clock(param1) then
-- main loop tick
-- relink if necessary
pocket_comms.link_update()
-- update any tasks for the active page
local page_tasks = nav.get_current_page().tasks
for i = 1, #page_tasks do page_tasks[i]() end
loop_clock.start()
elseif conn_wd.sv.is_timer(param1) then
-- supervisor watchdog timeout
log.info("supervisor server timeout")
pocket_comms.close_sv()
elseif conn_wd.api.is_timer(param1) then
-- coordinator watchdog timeout
log.info("coordinator api server timeout")
pocket_comms.close_api()
else
-- a non-clock/main watchdog timer event
-- notify timer callback dispatcher
tcd.handle(param1)
end
elseif event == "modem_message" then
-- got a packet
local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
pocket_comms.handle_packet(packet)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
event == "double_click" then
-- handle a monitor touch event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
end
-- check for termination request
if event == "terminate" or ppm.should_terminate() then
log.info("terminate requested, closing server connections...")
pocket_comms.close()
log.info("connections closed")
break
end
end
renderer.close_ui() renderer.close_ui()
if not pkt_state.ui_ok then
println(util.c("UI crashed with error: ", pkt_state.ui_error))
end
else
println_ts("UI creation failed")
end end
println_ts("exited") println_ts("exited")

220
pocket/threads.lua Normal file
View File

@@ -0,0 +1,220 @@
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer")
local core = require("graphics.core")
local threads = {}
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
local MQ__RENDER_DATA = pocket.MQ__RENDER_DATA
-- main thread
---@nodiscard
---@param smem pkt_shared_memory
function threads.thread__main(smem)
---@class parallel_thread
local public = {}
-- execute thread
function public.exec()
log.debug("main thread start")
local loop_clock = util.new_clock(MAIN_CLOCK)
-- start clock
loop_clock.start()
-- load in from shared memory
local pkt_state = smem.pkt_state
local pocket_comms = smem.pkt_sys.pocket_comms
local sv_wd = smem.pkt_sys.sv_wd
local api_wd = smem.pkt_sys.api_wd
local nav = smem.pkt_sys.nav
-- start connection watchdogs
sv_wd.feed()
api_wd.feed()
log.debug("startup> conn watchdogs started")
-- event loop
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event
if event == "timer" then
if loop_clock.is_clock(param1) then
-- main loop tick
-- relink if necessary
pocket_comms.link_update()
-- update any tasks for the active page
if nav.get_current_page() then
local page_tasks = nav.get_current_page().tasks
for i = 1, #page_tasks do page_tasks[i]() end
end
loop_clock.start()
elseif sv_wd.is_timer(param1) then
-- supervisor watchdog timeout
log.info("supervisor server timeout")
pocket_comms.close_sv()
elseif api_wd.is_timer(param1) then
-- coordinator watchdog timeout
log.info("coordinator api server timeout")
pocket_comms.close_api()
else
-- a non-clock/main watchdog timer event
-- notify timer callback dispatcher
tcd.handle(param1)
end
elseif event == "modem_message" then
-- got a packet
local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
pocket_comms.handle_packet(packet)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
event == "double_click" then
-- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
elseif event == "char" or event == "key" or event == "key_up" then
-- handle a keyboard event
renderer.handle_key(core.events.new_key_event(event, param1, param2))
elseif event == "paste" then
-- handle a paste event
renderer.handle_paste(param1)
end
-- check for termination request or UI crash
if event == "terminate" or ppm.should_terminate() then
log.info("terminate requested, main thread exiting")
pkt_state.shutdown = true
elseif not pkt_state.ui_ok then
pkt_state.shutdown = true
log.info("terminating due to fatal UI error")
end
if pkt_state.shutdown then
log.info("closing server connections...")
pocket_comms.close()
log.info("connections closed")
break
end
end
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
function public.p_exec()
local pkt_state = smem.pkt_state
while not pkt_state.shutdown do
local status, result = pcall(public.exec)
if status == false then
log.fatal(util.strval(result))
end
-- if status is true, then we are probably exiting, so this won't matter
-- this thread cannot be slept because it will miss events (namely "terminate")
if not pkt_state.shutdown then
log.info("main thread restarting now...")
end
end
end
return public
end
-- pocket renderer thread, tasked with long duration draws
---@nodiscard
---@param smem pkt_shared_memory
function threads.thread__render(smem)
---@class parallel_thread
local public = {}
-- execute thread
function public.exec()
log.debug("render thread start")
-- load in from shared memory
local pkt_state = smem.pkt_state
local nav = smem.pkt_sys.nav
local render_queue = smem.q.mq_render
local last_update = util.time()
-- thread loop
while true do
-- check for messages in the message queue
while render_queue.ready() and not pkt_state.shutdown do
local msg = render_queue.pop()
if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command
elseif msg.qtype == mqueue.TYPE.DATA then
-- received data
local cmd = msg.message ---@type queue_data
if cmd.key == MQ__RENDER_DATA.LOAD_APP then
log.debug("RENDER: load app " .. cmd.val[1])
local draw_start = util.time_ms()
pkt_state.ui_ok, pkt_state.ui_error = pcall(function () nav.load_app(cmd.val[1]) end)
if not pkt_state.ui_ok then
log.fatal(util.c("RENDER: app load failed with error ", pkt_state.ui_error))
else
log.debug("RENDER: app loaded in " .. (util.time_ms() - draw_start) .. "ms")
-- call the on loaded function if provided
if type(cmd.val[2]) == "function" then cmd.val[2]() end
end
end
elseif msg.qtype == mqueue.TYPE.PACKET then
-- received a packet
end
end
-- quick yield
util.nop()
end
-- check for termination request
if pkt_state.shutdown then
log.info("render thread exiting")
break
end
-- delay before next check
last_update = util.adaptive_delay(RENDER_SLEEP, last_update)
end
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
function public.p_exec()
local pkt_state = smem.pkt_state
while not pkt_state.shutdown do
local status, result = pcall(public.exec)
if status == false then
log.fatal(util.strval(result))
end
if not pkt_state.shutdown then
log.info("render thread restarting in 5 seconds...")
util.psleep(5)
end
end
end
return public
end
return threads

142
pocket/ui/apps/about.lua Normal file
View File

@@ -0,0 +1,142 @@
--
-- About Page
--
local comms = require("scada-common.comms")
local util = require("scada-common.util")
local lockbox = require("lockbox")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
-- create about page view
---@param root Container parent
local function create_pages(root)
local db = iocontrol.get_db()
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.ABOUT, frame)
local about_page = app.new_page(nil, 1)
local nt_page = app.new_page(about_page, 2)
local fw_page = app.new_page(about_page, 3)
local hw_page = app.new_page(about_page, 4)
local about = Div{parent=frame,x=1,y=2}
TextBox{parent=about,y=1,text="System Information",alignment=ALIGN.CENTER}
local btn_fg_bg = cpair(colors.lightBlue, colors.black)
local btn_active = cpair(colors.white, colors.black)
local label = cpair(colors.lightGray, colors.black)
PushButton{parent=about,x=2,y=3,text="Network >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=nt_page.nav_to}
PushButton{parent=about,x=2,y=4,text="Firmware >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fw_page.nav_to}
PushButton{parent=about,x=2,y=5,text="Host Details >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=hw_page.nav_to}
--#region Network Details
local config = pocket.config
local nt_div = Div{parent=frame,x=1,y=2}
TextBox{parent=nt_div,y=1,text="Network Details",alignment=ALIGN.CENTER}
PushButton{parent=nt_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
TextBox{parent=nt_div,x=2,y=3,text="Pocket Address",fg_bg=label}
---@diagnostic disable-next-line: undefined-field
TextBox{parent=nt_div,x=2,text=util.c(os.getComputerID(),":",config.PKT_Channel)}
nt_div.line_break()
TextBox{parent=nt_div,x=2,text="Supervisor Address",fg_bg=label}
local sv = TextBox{parent=nt_div,x=2,text=""}
nt_div.line_break()
TextBox{parent=nt_div,x=2,text="Coordinator Address",fg_bg=label}
local coord = TextBox{parent=nt_div,x=2,text=""}
sv.register(db.ps, "sv_addr", sv.set_value)
coord.register(db.ps, "api_addr", coord.set_value)
nt_div.line_break()
TextBox{parent=nt_div,x=2,text="Message Authentication",fg_bg=label}
local auth = util.trinary(type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0, "HMAC-MD5", "None")
TextBox{parent=nt_div,x=2,text=auth}
--#endregion
--#region Firmware Versions
local fw_div = Div{parent=frame,x=1,y=2}
TextBox{parent=fw_div,y=1,text="Firmware Versions",alignment=ALIGN.CENTER}
PushButton{parent=fw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
local fw_list_box = ListBox{parent=fw_div,x=1,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local fw_list = Div{parent=fw_list_box,x=1,y=2,height=18}
TextBox{parent=fw_list,x=2,text="Pocket Version",fg_bg=label}
TextBox{parent=fw_list,x=2,text=db.version}
fw_list.line_break()
TextBox{parent=fw_list,x=2,text="Comms Version",fg_bg=label}
TextBox{parent=fw_list,x=2,text=comms.version}
fw_list.line_break()
TextBox{parent=fw_list,x=2,text="API Version",fg_bg=label}
TextBox{parent=fw_list,x=2,text=comms.api_version}
fw_list.line_break()
TextBox{parent=fw_list,x=2,text="Common Lib Version",fg_bg=label}
TextBox{parent=fw_list,x=2,text=util.version}
fw_list.line_break()
TextBox{parent=fw_list,x=2,text="Graphics Version",fg_bg=label}
TextBox{parent=fw_list,x=2,text=core.version}
fw_list.line_break()
TextBox{parent=fw_list,x=2,text="Lockbox Version",fg_bg=label}
TextBox{parent=fw_list,x=2,text=lockbox.version}
--#endregion
--#region Host Versions
local hw_div = Div{parent=frame,x=1,y=2}
TextBox{parent=hw_div,y=1,text="Host Versions",alignment=ALIGN.CENTER}
PushButton{parent=hw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
hw_div.line_break()
TextBox{parent=hw_div,x=2,text="Lua Version",fg_bg=label}
TextBox{parent=hw_div,x=2,text=_VERSION}
hw_div.line_break()
TextBox{parent=hw_div,x=2,text="Environment",fg_bg=label}
TextBox{parent=hw_div,x=2,text=_HOST,height=6}
--#endregion
local root_pane = MultiPane{parent=frame,x=1,y=1,panes={about,nt_div,fw_div,hw_div}}
app.set_root_pane(root_pane)
end
return create_pages

184
pocket/ui/apps/alarm.lua Normal file
View File

@@ -0,0 +1,184 @@
--
-- Alarm Test App
--
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local Checkbox = require("graphics.elements.controls.Checkbox")
local PushButton = require("graphics.elements.controls.PushButton")
local SwitchButton = require("graphics.elements.controls.SwitchButton")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
local c_wht_gray = cpair(colors.white, colors.gray)
local c_red_gray = cpair(colors.red, colors.gray)
local c_yel_gray = cpair(colors.yellow, colors.gray)
local c_blue_gray = cpair(colors.blue, colors.gray)
-- create alarm test page view
---@param root Container parent
local function new_view(root)
local db = iocontrol.get_db()
local ps = db.ps
local ttest = db.diag.tone_test
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.ALARMS, frame, nil, true)
local main = Div{parent=frame,x=1,y=1}
local page_div = Div{parent=main,y=2,width=main.get_width()}
--#region alarm testing
local alarm_page = app.new_page(nil, 1)
alarm_page.tasks = { db.diag.tone_test.get_tone_states }
local alarms_div = Div{parent=page_div}
TextBox{parent=alarms_div,text="Alarm Sounder Tests",alignment=ALIGN.CENTER}
local alarm_ready_warn = TextBox{parent=alarms_div,y=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
alarm_ready_warn.register(ps, "alarm_ready_warn", alarm_ready_warn.set_value)
local alarm_page_states = Div{parent=alarms_div,x=2,y=3,height=5,width=8}
TextBox{parent=alarm_page_states,text="States",alignment=ALIGN.CENTER}
local ta_1 = IndicatorLight{parent=alarm_page_states,label="1",colors=c_blue_gray}
local ta_2 = IndicatorLight{parent=alarm_page_states,label="2",colors=c_blue_gray}
local ta_3 = IndicatorLight{parent=alarm_page_states,label="3",colors=c_blue_gray}
local ta_4 = IndicatorLight{parent=alarm_page_states,label="4",colors=c_blue_gray}
local ta_5 = IndicatorLight{parent=alarm_page_states,x=6,y=2,label="5",colors=c_blue_gray}
local ta_6 = IndicatorLight{parent=alarm_page_states,x=6,label="6",colors=c_blue_gray}
local ta_7 = IndicatorLight{parent=alarm_page_states,x=6,label="7",colors=c_blue_gray}
local ta_8 = IndicatorLight{parent=alarm_page_states,x=6,label="8",colors=c_blue_gray}
local ta = { ta_1, ta_2, ta_3, ta_4, ta_5, ta_6, ta_7, ta_8 }
for i = 1, #ta do
ta[i].register(ps, "alarm_tone_" .. i, ta[i].update)
end
local alarms = Div{parent=alarms_div,x=11,y=3,height=15,fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=alarms,text="Alarms (\x13)",alignment=ALIGN.CENTER,fg_bg=alarms_div.get_fg_bg()}
local alarm_btns = {}
alarm_btns[1] = Checkbox{parent=alarms,label="BREACH",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_breach}
alarm_btns[2] = Checkbox{parent=alarms,label="RADIATION",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_rad}
alarm_btns[3] = Checkbox{parent=alarms,label="RCT LOST",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_lost}
alarm_btns[4] = Checkbox{parent=alarms,label="CRIT DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_crit}
alarm_btns[5] = Checkbox{parent=alarms,label="DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_dmg}
alarm_btns[6] = Checkbox{parent=alarms,label="OVER TEMP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_overtemp}
alarm_btns[7] = Checkbox{parent=alarms,label="HIGH TEMP",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_hightemp}
alarm_btns[8] = Checkbox{parent=alarms,label="WASTE LEAK",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_wasteleak}
alarm_btns[9] = Checkbox{parent=alarms,label="WASTE HIGH",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_highwaste}
alarm_btns[10] = Checkbox{parent=alarms,label="RPS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rps}
alarm_btns[11] = Checkbox{parent=alarms,label="RCS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rcs}
alarm_btns[12] = Checkbox{parent=alarms,label="TURBINE TRP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_turbinet}
ttest.alarm_buttons = alarm_btns
local function stop_all_alarms()
for i = 1, #alarm_btns do alarm_btns[i].set_value(false) end
ttest.stop_alarms()
end
PushButton{parent=alarms,x=3,y=15,text="STOP \x13",min_width=8,fg_bg=cpair(colors.black,colors.red),active_fg_bg=c_wht_gray,callback=stop_all_alarms}
--#endregion
--#region direct tone testing
local tones_page = app.new_page(nil, 2)
tones_page.tasks = { db.diag.tone_test.get_tone_states }
local tones_div = Div{parent=page_div}
TextBox{parent=tones_div,text="Alarm Sounder Tests",alignment=ALIGN.CENTER}
local tone_ready_warn = TextBox{parent=tones_div,y=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
tone_ready_warn.register(ps, "alarm_ready_warn", tone_ready_warn.set_value)
local tone_page_states = Div{parent=tones_div,x=3,y=3,height=5,width=8}
TextBox{parent=tone_page_states,text="States",alignment=ALIGN.CENTER}
local tt_1 = IndicatorLight{parent=tone_page_states,label="1",colors=c_blue_gray}
local tt_2 = IndicatorLight{parent=tone_page_states,label="2",colors=c_blue_gray}
local tt_3 = IndicatorLight{parent=tone_page_states,label="3",colors=c_blue_gray}
local tt_4 = IndicatorLight{parent=tone_page_states,label="4",colors=c_blue_gray}
local tt_5 = IndicatorLight{parent=tone_page_states,x=6,y=2,label="5",colors=c_blue_gray}
local tt_6 = IndicatorLight{parent=tone_page_states,x=6,label="6",colors=c_blue_gray}
local tt_7 = IndicatorLight{parent=tone_page_states,x=6,label="7",colors=c_blue_gray}
local tt_8 = IndicatorLight{parent=tone_page_states,x=6,label="8",colors=c_blue_gray}
local tt = { tt_1, tt_2, tt_3, tt_4, tt_5, tt_6, tt_7, tt_8 }
for i = 1, #tt do
tt[i].register(ps, "alarm_tone_" .. i, tt[i].update)
end
local tones = Div{parent=tones_div,x=14,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)}
TextBox{parent=tones,text="Tones",alignment=ALIGN.CENTER,fg_bg=tones_div.get_fg_bg()}
local test_btns = {}
test_btns[1] = SwitchButton{parent=tones,text="TEST 1",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_1}
test_btns[2] = SwitchButton{parent=tones,text="TEST 2",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_2}
test_btns[3] = SwitchButton{parent=tones,text="TEST 3",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_3}
test_btns[4] = SwitchButton{parent=tones,text="TEST 4",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_4}
test_btns[5] = SwitchButton{parent=tones,text="TEST 5",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_5}
test_btns[6] = SwitchButton{parent=tones,text="TEST 6",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_6}
test_btns[7] = SwitchButton{parent=tones,text="TEST 7",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_7}
test_btns[8] = SwitchButton{parent=tones,text="TEST 8",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_8}
ttest.tone_buttons = test_btns
local function stop_all_tones()
for i = 1, #test_btns do test_btns[i].set_value(false) end
ttest.stop_tones()
end
PushButton{parent=tones,text="STOP",min_width=8,active_fg_bg=c_wht_gray,fg_bg=cpair(colors.black,colors.red),callback=stop_all_tones}
--#endregion
--#region info page
app.new_page(nil, 3)
local info_div = Div{parent=page_div}
TextBox{parent=info_div,x=2,y=1,text="This app provides tools to test alarm sounds by alarm and by tone (1-8)."}
TextBox{parent=info_div,x=2,y=6,text="The system must be idle (all units stopped with no alarms active) for testing to run."}
TextBox{parent=info_div,x=2,y=12,text="Currently, testing will be denied unless you have a Facility Authentication Key set (this will change in the future)."}
--#endregion
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes={alarms_div,tones_div,info_div}}
app.set_root_pane(u_pane)
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = " \x13 ", color = core.cpair(colors.black, colors.red), callback = function () app.switcher(1) end },
{ label = " \x0f ", color = core.cpair(colors.black, colors.yellow), callback = function () app.switcher(2) end },
{ label = " ? ", color = core.cpair(colors.black, colors.blue), callback = function () app.switcher(3) end }
}
app.set_sidebar(list)
end
return new_view

297
pocket/ui/apps/comps.lua Normal file
View File

@@ -0,0 +1,297 @@
--
-- Computer List App
--
local comms = require("scada-common.comms")
local const = require("scada-common.constants")
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local DEV_TYPE = comms.DEVICE_TYPE
local ALIGN = core.ALIGN
local cpair = core.cpair
local border = core.border
local APP_ID = pocket.APP_ID
local lu_col = style.label_unit_pair
local box_label = cpair(colors.lightGray, colors.gray)
-- new computer list page view
---@param root Container parent
local function new_view(root)
local db = iocontrol.get_db()
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.COMPS, frame, nil, true, false)
local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.orange,colors._INHERIT)}
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
local page_div = nil ---@type Div|nil
-- load the app (create the elements)
local function load()
local ps = db.ps
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {} ---@type Div[]
-- create all page divs
for _ = 1, 4 do
local div = Div{parent=page_div}
table.insert(panes, div)
end
local last_update = 0
-- refresh data callback, every 1s it will re-send the query
local function update()
if util.time_ms() - last_update >= 1000 then
db.diag.get_comps()
last_update = util.time_ms()
end
end
-- create indicators for the ID, firmware, and RTT
---@param pfx string
---@param rect Rectangle
local function create_common_indicators(pfx, rect)
local first = TextBox{parent=rect,text="Computer",fg_bg=box_label}
TextBox{parent=rect,text="Firmware",fg_bg=box_label}
TextBox{parent=rect,text="RTT (ms)",fg_bg=box_label}
local y = first.get_y()
local addr = TextBox{parent=rect,x=10,y=y,text="---"}
local fw = TextBox{parent=rect,x=10,y=y+1,text="---"}
local rtt = TextBox{parent=rect,x=10,y=y+2,text="---"}
addr.register(ps, pfx .. "_addr", function (v) addr.set_value(util.strval(v)) end)
fw.register(ps, pfx .. "_fw", function (v) fw.set_value(util.strval(v)) end)
rtt.register(ps, pfx .. "_rtt", function (value)
rtt.set_value(util.strval(value))
if value == "---" then
rtt.recolor(colors.white)
elseif value > const.HIGH_RTT then
rtt.recolor(colors.red)
elseif value > const.WARN_RTT then
rtt.recolor(colors.yellow)
else
rtt.recolor(colors.green)
end
end)
end
--#region main computer page
local m_div = Div{parent=panes[1],x=2,width=main.get_width()-2}
local main_page = app.new_page(nil, 1)
main_page.tasks = { update }
TextBox{parent=m_div,y=1,text="Connected Computers",alignment=ALIGN.CENTER}
local conns = DataIndicator{parent=m_div,y=3,lu_colors=lu_col,label="Total Online",unit="",format="%8d",value=0,commas=true,width=21}
conns.register(ps, "comp_online", conns.update)
local svr_div = Div{parent=m_div,y=4,height=6}
local svr_rect = Rectangle{parent=svr_div,height=6,width=22,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=svr_rect,text="Supervisor"}
TextBox{parent=svr_rect,text="Status",fg_bg=box_label}
TextBox{parent=svr_rect,x=10,y=2,text="Online",fg_bg=cpair(colors.green,colors._INHERIT)}
TextBox{parent=svr_rect,text="Computer",fg_bg=box_label}
TextBox{parent=svr_rect,text="Firmware",fg_bg=box_label}
local svr_addr = TextBox{parent=svr_rect,x=10,y=3,text="?"}
local svr_fw = TextBox{parent=svr_rect,x=10,y=4,text="?"}
svr_addr.register(ps, "comp_svr_addr", function (v) svr_addr.set_value(util.strval(v)) end)
svr_fw.register(ps, "comp_svr_fw", function (v) svr_fw.set_value(util.strval(v)) end)
local crd_div = Div{parent=m_div,y=11,height=7}
local crd_rect = Rectangle{parent=crd_div,height=7,width=21,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=crd_rect,text="Coordinator"}
TextBox{parent=crd_rect,text="Status",fg_bg=box_label}
local crd_online = TextBox{parent=crd_rect,x=10,y=2,width=8,text="Off-line",fg_bg=cpair(colors.red,colors._INHERIT)}
create_common_indicators("comp_crd", crd_rect)
crd_online.register(ps, "comp_crd_online", function (online)
if online then
crd_online.recolor(colors.green)
crd_online.set_value("Online")
else
crd_online.recolor(colors.red)
crd_online.set_value("Off-line")
end
end)
--#endregion
--#region PLC page
local p_div = Div{parent=panes[2],width=main.get_width()}
local plc_page = app.new_page(nil, 2)
plc_page.tasks = { update }
TextBox{parent=p_div,y=1,text="PLC Devices",alignment=ALIGN.CENTER}
local plc_list = ListBox{parent=p_div,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local plc_elems = {} ---@type graphics_element[]
--#endregion
--#region RTU gateway page
local r_div = Div{parent=panes[3],width=main.get_width()}
local rtu_page = app.new_page(nil, 3)
rtu_page.tasks = { update }
TextBox{parent=r_div,y=1,text="RTU Gateway Devices",alignment=ALIGN.CENTER}
local rtu_list = ListBox{parent=r_div,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local rtu_elems = {} ---@type graphics_element[]
--#endregion
--#region pocket computer page
local pk_div = Div{parent=panes[4],width=main.get_width()}
local pkt_page = app.new_page(nil, 4)
pkt_page.tasks = { update }
TextBox{parent=pk_div,y=1,text="Pocket Devices",alignment=ALIGN.CENTER}
local pkt_list = ListBox{parent=pk_div,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local pkt_elems = {} ---@type graphics_element[]
--#endregion
--#region connect/disconnect management
ps.subscribe("comp_connect", function (id)
if id == false then return end
local pfx = "comp_" .. id
local type = ps.get(pfx .. "_type")
if type == DEV_TYPE.PLC then
plc_elems[id] = Div{parent=plc_list,height=7}
local rect = Rectangle{parent=plc_elems[id],height=6,x=2,width=20,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
local title = TextBox{parent=rect,text="PLC (Unit ?)"}
title.register(ps, pfx .. "_unit", function (unit) title.set_value("PLC (Unit " .. unit .. ")") end)
create_common_indicators(pfx, rect)
elseif type == DEV_TYPE.RTU then
rtu_elems[id] = Div{parent=rtu_list,height=7}
local rect = Rectangle{parent=rtu_elems[id],height=6,x=2,width=20,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=rect,text="RTU Gateway"}
create_common_indicators(pfx, rect)
elseif type == DEV_TYPE.PKT then
pkt_elems[id] = Div{parent=pkt_list,height=7}
local rect = Rectangle{parent=pkt_elems[id],height=6,x=2,width=20,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=rect,text="Pocket Computer"}
create_common_indicators(pfx, rect)
end
end)
ps.subscribe("comp_disconnect", function (id)
if id == false then return end
local type = ps.get("comp_" ..id .. "_type")
if type == DEV_TYPE.PLC then
if plc_elems[id] then plc_elems[id].delete() end
plc_elems[id] = nil
elseif type == DEV_TYPE.RTU then
if rtu_elems[id] then rtu_elems[id].delete() end
rtu_elems[id] = nil
elseif type == DEV_TYPE.PKT then
if pkt_elems[id] then pkt_elems[id].delete() end
pkt_elems[id] = nil
end
end)
--#endregion
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)
-- setup sidebar
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = " @ ", color = core.cpair(colors.black, colors.blue), callback = main_page.nav_to },
{ label = "PLC", color = core.cpair(colors.black, colors.red), callback = plc_page.nav_to },
{ label = "RTU", color = core.cpair(colors.black, colors.orange), callback = rtu_page.nav_to },
{ label = "PKT", color = core.cpair(colors.black, colors.lightGray), callback = pkt_page.nav_to }
}
app.set_sidebar(list)
-- done, show the app
main_page.nav_to()
load_pane.set_value(2)
end
-- delete the elements and switch back to the loading screen
local function unload()
if page_div then
page_div.delete()
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.delete_pages()
-- show loading screen
load_pane.set_value(1)
-- clear the list of connected computers so that connections re-appear on reload of this app
iocontrol.rx.clear_comp_record()
end
app.set_load(load)
app.set_unload(unload)
return main
end
return new_view

233
pocket/ui/apps/control.lua Normal file
View File

@@ -0,0 +1,233 @@
--
-- Facility & Unit Control App
--
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local process = require("pocket.process")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local HazardButton = require("graphics.elements.controls.HazardButton")
local PushButton = require("graphics.elements.controls.PushButton")
local NumberField = require("graphics.elements.form.NumberField")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
local AUTO_GROUP = types.AUTO_GROUP
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
local label_fg_bg = style.label
local lu_col = style.label_unit_pair
local text_fg = style.text_fg
local mode_states = style.icon_states.mode_states
local btn_active = cpair(colors.white, colors.black)
local hzd_fg_bg = style.hzd_fg_bg
local hzd_dis_colors = style.hzd_dis_colors
-- new unit control page view
---@param root Container parent
local function new_view(root)
local btn_fg_bg = cpair(colors.green, colors.black)
local db = iocontrol.get_db()
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.CONTROL, frame, nil, false, true)
local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.green,colors._INHERIT)}
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
local page_div = nil ---@type Div|nil
-- set sidebar to display unit-specific fields based on a specified unit
local function set_sidebar()
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = "FAC", color = core.cpair(colors.black, colors.orange), callback = function () app.switcher(db.facility.num_units + 1) end }
}
for i = 1, db.facility.num_units do
table.insert(list, { label = "U-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = function () app.switcher(i) end })
end
app.set_sidebar(list)
end
-- load the app (create the elements)
local function load()
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {} ---@type Div[]
local active_unit = 1
-- create all page divs
for _ = 1, db.facility.num_units + 1 do
local div = Div{parent=page_div}
table.insert(panes, div)
end
-- previous unit
local function prev(x)
active_unit = util.trinary(x == 1, db.facility.num_units, x - 1)
app.switcher(active_unit)
end
-- next unit
local function next(x)
active_unit = util.trinary(x == db.facility.num_units, 1, x + 1)
app.switcher(active_unit)
end
local last_update = 0
-- refresh data callback, every 500ms it will re-send the query
local function update()
if util.time_ms() - last_update >= 500 then
db.api.get_ctrl()
last_update = util.time_ms()
end
end
for i = 1, db.facility.num_units do
local u_pane = panes[i]
local u_div = Div{parent=u_pane,x=2,width=main.get_width()-2}
local unit = db.units[i]
local u_ps = unit.unit_ps
local u_page = app.new_page(nil, i)
u_page.tasks = { update }
TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,alignment=ALIGN.CENTER}
PushButton{parent=u_div,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()prev(i)end}
PushButton{parent=u_div,x=21,y=1,text=">",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()next(i)end}
local rate = DataIndicator{parent=u_div,y=3,lu_colors=lu_col,label="Burn",unit="mB/t",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
local temp = DataIndicator{parent=u_div,lu_colors=lu_col,label="Temp",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
local ctrl = IconIndicator{parent=u_div,x=1,y=6,label="Control State",states=mode_states}
rate.register(u_ps, "act_burn_rate", rate.update)
temp.register(u_ps, "temp", function (t) temp.update(db.temp_convert(t)) end)
ctrl.register(u_ps, "U_ControlStatus", ctrl.update)
u_div.line_break()
TextBox{parent=u_div,y=8,text="CMD",width=4,fg_bg=label_fg_bg}
TextBox{parent=u_div,x=14,y=8,text="mB/t",width=4,fg_bg=label_fg_bg}
local burn_cmd = NumberField{parent=u_div,x=5,y=8,width=8,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=style.field,dis_fg_bg=style.field_disable}
local set_burn = function () unit.set_burn(burn_cmd.get_numeric()) end
local set_burn_btn = PushButton{parent=u_div,x=19,y=8,text="SET",min_width=5,fg_bg=cpair(colors.green,colors.black),active_fg_bg=cpair(colors.white,colors.black),dis_fg_bg=style.btn_disable,callback=set_burn}
-- enable/disable controls based on group assignment (start button is separate)
burn_cmd.register(u_ps, "auto_group_id", function (gid)
if gid == AUTO_GROUP.MANUAL then burn_cmd.enable() else burn_cmd.disable() end
end)
set_burn_btn.register(u_ps, "auto_group_id", function (gid)
if gid == AUTO_GROUP.MANUAL then set_burn_btn.enable() else set_burn_btn.disable() end
end)
burn_cmd.register(u_ps, "burn_rate", burn_cmd.set_value)
burn_cmd.register(u_ps, "max_burn", burn_cmd.set_max)
local start = HazardButton{parent=u_div,x=2,y=11,text="START",accent=colors.lightBlue,callback=unit.start,timeout=3,fg_bg=hzd_fg_bg,dis_colors=hzd_dis_colors}
local ack_a = HazardButton{parent=u_div,x=12,y=11,text="ACK \x13",accent=colors.orange,callback=unit.ack_alarms,timeout=3,fg_bg=hzd_fg_bg,dis_colors=hzd_dis_colors}
local scram = HazardButton{parent=u_div,x=2,y=15,text="SCRAM",accent=colors.yellow,callback=unit.scram,timeout=3,fg_bg=hzd_fg_bg,dis_colors=hzd_dis_colors}
local reset = HazardButton{parent=u_div,x=12,y=15,text="RESET",accent=colors.red,callback=unit.reset_rps,timeout=3,fg_bg=hzd_fg_bg,dis_colors=hzd_dis_colors}
unit.start_ack = start.on_response
unit.ack_alarms_ack = ack_a.on_response
unit.scram_ack = scram.on_response
unit.reset_rps_ack = reset.on_response
local function start_button_en_check()
local can_start = (not unit.reactor_data.mek_status.status) and
(not unit.reactor_data.rps_tripped) and
(unit.a_group == AUTO_GROUP.MANUAL)
if can_start then start.enable() else start.disable() end
end
start.register(u_ps, "status", start_button_en_check)
start.register(u_ps, "rps_tripped", start_button_en_check)
start.register(u_ps, "auto_group_id", start_button_en_check)
start.register(u_ps, "AutoControl", start_button_en_check)
reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
util.nop()
end
-- facility controls
local f_pane = panes[db.facility.num_units + 1]
local f_div = Div{parent=f_pane,x=2,width=main.get_width()-2}
app.new_page(nil, db.facility.num_units + 1)
TextBox{parent=f_div,y=1,text="Facility Commands",alignment=ALIGN.CENTER}
local scram = HazardButton{parent=f_div,x=5,y=6,text="FAC SCRAM",accent=colors.yellow,dis_colors=hzd_dis_colors,callback=process.fac_scram,timeout=3,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=f_div,x=7,y=11,text="ACK \x13",accent=colors.orange,dis_colors=hzd_dis_colors,callback=process.fac_ack_alarms,timeout=3,fg_bg=hzd_fg_bg}
db.facility.scram_ack = scram.on_response
db.facility.ack_alarms_ack = ack_a.on_response
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)
set_sidebar()
-- done, show the app
load_pane.set_value(2)
end
-- delete the elements and switch back to the loading screen
local function unload()
if page_div then
page_div.delete()
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.delete_pages()
-- show loading screen
load_pane.set_value(1)
end
app.set_load(load)
app.set_unload(unload)
return main
end
return new_view

View File

@@ -1,116 +0,0 @@
--
-- Diagnostic Apps
--
local iocontrol = require("pocket.iocontrol")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local IndicatorLight = require("graphics.elements.indicators.light")
local Checkbox = require("graphics.elements.controls.checkbox")
local PushButton = require("graphics.elements.controls.push_button")
local SwitchButton = require("graphics.elements.controls.switch_button")
local cpair = core.cpair
local ALIGN = core.ALIGN
-- create diagnostic app pages
---@param root graphics_element parent
local function create_pages(root)
local db = iocontrol.get_db()
------------------------
-- Alarm Testing Page --
------------------------
local alarm_test = Div{parent=root,x=1,y=1}
local alarm_app = db.nav.register_app(iocontrol.APP_ID.ALARMS, alarm_test)
local page = alarm_app.new_page(nil, function () end)
page.tasks = { db.diag.tone_test.get_tone_states }
local ttest = db.diag.tone_test
local c_wht_gray = cpair(colors.white, colors.gray)
local c_red_gray = cpair(colors.red, colors.gray)
local c_yel_gray = cpair(colors.yellow, colors.gray)
local c_blue_gray = cpair(colors.blue, colors.gray)
local audio = Div{parent=alarm_test,x=1,y=1}
TextBox{parent=audio,y=1,text="Alarm Sounder Tests",height=1,alignment=ALIGN.CENTER}
ttest.ready_warn = TextBox{parent=audio,y=2,text="",height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
local tones = Div{parent=audio,x=2,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)}
TextBox{parent=tones,text="Tones",height=1,alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
local test_btns = {}
test_btns[1] = SwitchButton{parent=tones,text="TEST 1",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_1}
test_btns[2] = SwitchButton{parent=tones,text="TEST 2",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_2}
test_btns[3] = SwitchButton{parent=tones,text="TEST 3",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_3}
test_btns[4] = SwitchButton{parent=tones,text="TEST 4",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_4}
test_btns[5] = SwitchButton{parent=tones,text="TEST 5",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_5}
test_btns[6] = SwitchButton{parent=tones,text="TEST 6",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_6}
test_btns[7] = SwitchButton{parent=tones,text="TEST 7",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_7}
test_btns[8] = SwitchButton{parent=tones,text="TEST 8",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_8}
ttest.tone_buttons = test_btns
local function stop_all_tones()
for i = 1, #test_btns do test_btns[i].set_value(false) end
ttest.stop_tones()
end
PushButton{parent=tones,text="STOP",min_width=8,active_fg_bg=c_wht_gray,fg_bg=cpair(colors.black,colors.red),callback=stop_all_tones}
local alarms = Div{parent=audio,x=11,y=3,height=15,fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=alarms,text="Alarms (\x13)",height=1,alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
local alarm_btns = {}
alarm_btns[1] = Checkbox{parent=alarms,label="BREACH",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_breach}
alarm_btns[2] = Checkbox{parent=alarms,label="RADIATION",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_rad}
alarm_btns[3] = Checkbox{parent=alarms,label="RCT LOST",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_lost}
alarm_btns[4] = Checkbox{parent=alarms,label="CRIT DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_crit}
alarm_btns[5] = Checkbox{parent=alarms,label="DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_dmg}
alarm_btns[6] = Checkbox{parent=alarms,label="OVER TEMP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_overtemp}
alarm_btns[7] = Checkbox{parent=alarms,label="HIGH TEMP",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_hightemp}
alarm_btns[8] = Checkbox{parent=alarms,label="WASTE LEAK",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_wasteleak}
alarm_btns[9] = Checkbox{parent=alarms,label="WASTE HIGH",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_highwaste}
alarm_btns[10] = Checkbox{parent=alarms,label="RPS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rps}
alarm_btns[11] = Checkbox{parent=alarms,label="RCS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rcs}
alarm_btns[12] = Checkbox{parent=alarms,label="TURBINE TRP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_turbinet}
ttest.alarm_buttons = alarm_btns
local function stop_all_alarms()
for i = 1, #alarm_btns do alarm_btns[i].set_value(false) end
ttest.stop_alarms()
end
PushButton{parent=alarms,x=3,y=15,text="STOP \x13",min_width=8,fg_bg=cpair(colors.black,colors.red),active_fg_bg=c_wht_gray,callback=stop_all_alarms}
local states = Div{parent=audio,x=2,y=14,height=5,width=8}
TextBox{parent=states,text="States",height=1,alignment=ALIGN.CENTER}
local t_1 = IndicatorLight{parent=states,label="1",colors=c_blue_gray}
local t_2 = IndicatorLight{parent=states,label="2",colors=c_blue_gray}
local t_3 = IndicatorLight{parent=states,label="3",colors=c_blue_gray}
local t_4 = IndicatorLight{parent=states,label="4",colors=c_blue_gray}
local t_5 = IndicatorLight{parent=states,x=6,y=2,label="5",colors=c_blue_gray}
local t_6 = IndicatorLight{parent=states,x=6,label="6",colors=c_blue_gray}
local t_7 = IndicatorLight{parent=states,x=6,label="7",colors=c_blue_gray}
local t_8 = IndicatorLight{parent=states,x=6,label="8",colors=c_blue_gray}
ttest.tone_indicators = { t_1, t_2, t_3, t_4, t_5, t_6, t_7, t_8 }
end
return create_pages

View File

@@ -1,26 +0,0 @@
--
-- Placeholder App
--
local iocontrol = require("pocket.iocontrol")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
-- create placeholder app page
---@param root graphics_element parent
local function create_pages(root)
local db = iocontrol.get_db()
local main = Div{parent=root,x=1,y=1}
db.nav.register_app(iocontrol.APP_ID.DUMMY, main).new_page(nil, function () end)
TextBox{parent=main,text="This app is not implemented yet.",x=1,y=2,alignment=core.ALIGN.CENTER}
TextBox{parent=main,text=" pretend something cool is here \x03",x=1,y=10,alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.gray,colors.black)}
end
return create_pages

258
pocket/ui/apps/facility.lua Normal file
View File

@@ -0,0 +1,258 @@
--
-- Facility Overview App
--
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local style = require("pocket.ui.style")
local dyn_tank = require("pocket.ui.pages.dynamic_tank")
local facility_sps = require("pocket.ui.pages.facility_sps")
local induction_mtx = require("pocket.ui.pages.facility_matrix")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
local label_fg_bg = style.label
local lu_col = style.label_unit_pair
local basic_states = style.icon_states.basic_states
local mode_states = style.icon_states.mode_states
local red_ind_s = style.icon_states.red_ind_s
local yel_ind_s = style.icon_states.yel_ind_s
local grn_ind_s = style.icon_states.grn_ind_s
-- new unit page view
---@param root Container parent
local function new_view(root)
local db = iocontrol.get_db()
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.FACILITY, frame, nil, false, true)
local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.orange,colors._INHERIT)}
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
local tank_page_navs = {}
local page_div = nil ---@type Div|nil
-- load the app (create the elements)
local function load()
local fac = db.facility
local f_ps = fac.ps
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {} ---@type Div[]
-- refresh data callback, every 500ms it will re-send the query
local last_update = 0
local function update()
if util.time_ms() - last_update >= 500 then
db.api.get_fac()
last_update = util.time_ms()
end
end
--#region facility overview
local main_pane = Div{parent=page_div}
local f_div = Div{parent=main_pane,x=2,width=main.get_width()-2}
table.insert(panes, main_pane)
local fac_page = app.new_page(nil, #panes)
fac_page.tasks = { update }
TextBox{parent=f_div,y=1,text="Facility",alignment=ALIGN.CENTER}
local mtx_state = IconIndicator{parent=f_div,y=3,label="Matrix Status",states=basic_states}
local sps_state = IconIndicator{parent=f_div,label="SPS Status",states=basic_states}
mtx_state.register(fac.induction_ps_tbl[1], "InductionMatrixStatus", mtx_state.update)
sps_state.register(fac.sps_ps_tbl[1], "SPSStatus", sps_state.update)
TextBox{parent=f_div,y=6,text="RTU Gateways",fg_bg=label_fg_bg}
local rtu_count = DataIndicator{parent=f_div,x=19,y=6,label="",format="%3d",value=0,lu_colors=lu_col,width=3}
rtu_count.register(f_ps, "rtu_count", rtu_count.update)
TextBox{parent=f_div,y=8,text="Induction Matrix",alignment=ALIGN.CENTER}
local eta = TextBox{parent=f_div,x=1,y=10,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,colors.gray)}
eta.register(fac.induction_ps_tbl[1], "eta_string", eta.set_value)
TextBox{parent=f_div,y=12,text="Unit Statuses",alignment=ALIGN.CENTER}
f_div.line_break()
for i = 1, fac.num_units do
local ctrl = IconIndicator{parent=f_div,label="U"..i.." Control State",states=mode_states}
ctrl.register(db.units[i].unit_ps, "U_ControlStatus", ctrl.update)
end
--#endregion
--#region facility annunciator
local a_pane = Div{parent=page_div}
local a_div = Div{parent=a_pane,x=2,width=main.get_width()-2}
table.insert(panes, a_pane)
local annunc_page = app.new_page(nil, #panes)
annunc_page.tasks = { update }
TextBox{parent=a_div,y=1,text="Annunciator",alignment=ALIGN.CENTER}
local all_ok = IconIndicator{parent=a_div,y=3,label="Units Online",states=grn_ind_s}
local ind_mat = IconIndicator{parent=a_div,label="Induction Matrix",states=grn_ind_s}
local sps = IconIndicator{parent=a_div,label="SPS Connected",states=grn_ind_s}
all_ok.register(f_ps, "all_sys_ok", all_ok.update)
ind_mat.register(fac.induction_ps_tbl[1], "InductionMatrixStateStatus", function (status) ind_mat.update(status > 1) end)
sps.register(fac.sps_ps_tbl[1], "SPSStateStatus", function (status) sps.update(status > 1) end)
a_div.line_break()
local auto_scram = IconIndicator{parent=a_div,label="Automatic SCRAM",states=red_ind_s}
local matrix_flt = IconIndicator{parent=a_div,label="Ind. Matrix Fault",states=yel_ind_s}
local matrix_fill = IconIndicator{parent=a_div,label="Matrix Charge Hi",states=red_ind_s}
local unit_crit = IconIndicator{parent=a_div,label="Unit Crit. Alarm",states=red_ind_s}
local fac_rad_h = IconIndicator{parent=a_div,label="FAC Radiation Hi",states=red_ind_s}
local gen_fault = IconIndicator{parent=a_div,label="Gen Control Fault",states=yel_ind_s}
auto_scram.register(f_ps, "auto_scram", auto_scram.update)
matrix_flt.register(f_ps, "as_matrix_fault", matrix_flt.update)
matrix_fill.register(f_ps, "as_matrix_fill", matrix_fill.update)
unit_crit.register(f_ps, "as_crit_alarm", unit_crit.update)
fac_rad_h.register(f_ps, "as_radiation", fac_rad_h.update)
gen_fault.register(f_ps, "as_gen_fault", gen_fault.update)
--#endregion
--#region induction matrix
local mtx_page_nav = induction_mtx(app, panes, Div{parent=page_div}, fac.induction_ps_tbl[1], update)
--#endregion
--#region SPS
local sps_page_nav = facility_sps(app, panes, Div{parent=page_div}, fac.sps_ps_tbl[1], update)
--#endregion
--#region facility tank pages
local t_pane = Div{parent=page_div}
local t_div = Div{parent=t_pane,x=2,width=main.get_width()-2}
table.insert(panes, t_pane)
local tank_page = app.new_page(nil, #panes)
tank_page.tasks = { update }
TextBox{parent=t_div,y=1,text="Facility Tanks",alignment=ALIGN.CENTER}
local f_tank_id = 1
for t = 1, #fac.tank_list do
if fac.tank_list[t] == 1 then
t_div.line_break()
local tank = IconIndicator{parent=t_div,x=1,label="Unit Tank "..t.." (U-"..t..")",states=basic_states}
tank.register(db.units[t].tank_ps_tbl[1], "DynamicTankStatus", tank.update)
TextBox{parent=t_div,x=5,text="\x07 Unit "..t,fg_bg=label_fg_bg}
elseif fac.tank_list[t] == 2 then
tank_page_navs[f_tank_id] = dyn_tank(app, nil, panes, Div{parent=page_div}, t, fac.tank_ps_tbl[f_tank_id], update)
t_div.line_break()
local tank = IconIndicator{parent=t_div,x=1,label="Fac. Tank "..f_tank_id.." (F-"..f_tank_id..")",states=basic_states}
tank.register(fac.tank_ps_tbl[f_tank_id], "DynamicTankStatus", tank.update)
local connections = ""
for i = 1, #fac.tank_conns do
if fac.tank_conns[i] == t then
if connections ~= "" then
connections = connections .. "\n\x07 Unit " .. i
else
connections = "\x07 Unit " .. i
end
end
end
TextBox{parent=t_div,x=5,text=connections,fg_bg=label_fg_bg}
f_tank_id = f_tank_id + 1
end
end
--#endregion
-- setup multipane
local f_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(f_pane)
-- setup sidebar
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = "FAC", tall = true, color = core.cpair(colors.black, colors.orange), callback = fac_page.nav_to },
{ label = "ANN", color = core.cpair(colors.black, colors.yellow), callback = annunc_page.nav_to },
{ label = "MTX", color = core.cpair(colors.black, colors.white), callback = mtx_page_nav },
{ label = "SPS", color = core.cpair(colors.black, colors.purple), callback = sps_page_nav },
{ label = "TNK", tall = true, color = core.cpair(colors.black, colors.blue), callback = tank_page.nav_to }
}
for i = 1, #fac.tank_data_tbl do
table.insert(list, { label = "F-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = tank_page_navs[i] })
end
app.set_sidebar(list)
-- done, show the app
load_pane.set_value(2)
end
-- delete the elements and switch back to the loading screen
local function unload()
if page_div then
page_div.delete()
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.delete_pages()
-- show loading screen
load_pane.set_value(1)
end
app.set_load(load)
app.set_unload(unload)
return main
end
return new_view

Some files were not shown because too many files have changed in this diff Show More