Compare commits

...

1641 Commits
alpha ... main

Author SHA1 Message Date
9d4cd13911 changed to raw
Some checks failed
Lua Checks / check (push) Successful in 6s
Deploy Installation Data / deploy (push) Failing after 10s
2025-10-29 13:53:09 +01:00
cc2cb8feab changed to http
Some checks failed
Lua Checks / check (push) Successful in 6s
Deploy Installation Data / deploy (push) Failing after 11s
2025-10-29 13:51:54 +01:00
c0dcffe14a Changed installer to use Befator inc git
Some checks failed
Lua Checks / check (push) Successful in 5s
Deploy Installation Data / deploy (push) Failing after 10s
2025-10-29 13:48:18 +01:00
4b8aa3e858 allowed for non-wireless modem setup
Some checks failed
Lua Checks / check (push) Successful in 9s
Deploy Installation Data / deploy (push) Failing after 1m25s
Hardcoded that the Application thinks its in an emulated env to thus allow the use of Wired modems instead of Wireless (because who wants to use wireless modems??!)
2025-10-29 13:45:08 +01: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
b96eb7d89d
Merge pull request #492 from MikaylaFischler/devel
2024.05.16 Beta Hotfix
2024-05-16 22:13:01 -04:00
Mikayla Fischler
9b8947fba2 #491 fixed ps table indexing for boiler/turbine online 2024-05-16 22:06:53 -04:00
Mikayla
afc38e7e7a
Merge pull request #490 from MikaylaFischler/devel
2024.05.15 Release
2024-05-15 17:22:18 -04:00
Mikayla Fischler
41b7a68f3e #487 stop retrying failed disable when needing to enable 2024-05-14 20:10:21 -04:00
Mikayla Fischler
8968ebede3 #486 fixed pcall messages for newer CC versions 2024-05-14 20:00:34 -04:00
Mikayla
50a9168b06
Merge pull request #489 from MikaylaFischler/pocket-alpha-dev
Pocket Update
2024-05-12 18:27:03 -04:00
Mikayla Fischler
76e85da9d5 version increments and small fix 2024-05-12 18:19:21 -04:00
Mikayla Fischler
be560cd532 luacheck fixes 2024-05-12 15:19:01 -04:00
Mikayla Fischler
2b0a536292 #200 pocket RCS overview 2024-05-12 15:14:58 -04:00
Mikayla Fischler
afed6f514d removed stray space in annunciator Coolant Level Low 2024-05-12 15:05:36 -04:00
Mikayla Fischler
3e6a0a8869 #200 updated RPS indicator text 2024-05-12 14:20:48 -04:00
Mikayla Fischler
b99cf19be0 #200 placeholder for alarm page, start of RCS page 2024-05-12 13:36:46 -04:00
Mikayla Fischler
7f009f9c86 #200 RPS view on pocket 2024-05-12 13:28:34 -04:00
Mikayla Fischler
6a8ed311f3 #200 functioning pocket unit overview 2024-05-11 19:19:52 -04:00
Mikayla Fischler
c181142f75 added network details to about app 2024-05-11 15:03:14 -04:00
Mikayla Fischler
0cb964a177 diagnostic disables 2024-05-10 19:18:21 -04:00
Mikayla Fischler
3cd832ca20 sidebar updates 2024-05-10 19:17:52 -04:00
Mikayla Fischler
0c6c7bf9a5 #200 work in progress unit views and sidebar/app updates 2024-05-09 23:05:55 -04:00
Mikayla Fischler
3bfcb1d83c #485 fixed assertion with height auto incrementing y when inheriting height 2024-05-04 13:50:53 -04:00
Mikayla
4fe6792804
Merge pull request #483 from MikaylaFischler/devel
2024.04.30 Release
2024-04-30 20:35:24 -04:00
Mikayla Fischler
25dc47d520 fixed recording bad stats on induction matrix faults 2024-04-30 20:28:07 -04:00
Mikayla Fischler
f958b0e3b7 fixed at max i/o indicator 2024-04-30 20:27:04 -04:00
Mikayla Fischler
f621ff2482 added some value inits and unit labels 2024-04-30 18:54:01 -04:00
Mikayla Fischler
eb45ff899b #455 calculate reactor temp high limit 2024-04-29 22:03:54 -04:00
Mikayla
e91fd2fcaa
Merge pull request #482 from MikaylaFischler/412-additional-matrix-integrations
412 additional matrix integrations
2024-04-28 13:08:51 -04:00
Mikayla Fischler
d35b824458 luacheck fix and cleanup 2024-04-28 13:08:16 -04:00
Mikayla Fischler
165d1497f8 reverted test change that got committed 2024-04-28 02:01:40 -04:00
Mikayla Fischler
50bf057ca6 #412 optionally disable SPS at low power 2024-04-28 02:01:21 -04:00
Mikayla Fischler
6f768ef6b3 #469 made ETA tolerant to induction matrix capacity changes 2024-04-28 01:26:44 -04:00
Mikayla Fischler
826086951e return zero on mov_avg compute if no samples 2024-04-27 19:50:35 -04:00
Mikayla Fischler
35bf56663f #469 induction matrix charge ETAs and misc cleanup/updates 2024-04-27 16:27:01 -04:00
Mikayla
43b44cc425 #465 first pass at minifier 2024-04-24 21:11:41 +00:00
Mikayla Fischler
7b8cea4a5c Merge branch 'devel' into 412-additional-matrix-integrations 2024-04-21 13:57:47 -04:00
Mikayla Fischler
51d4a22532 #478 simplified reactor PLC reactor formed handling 2024-04-21 13:55:39 -04:00
Mikayla Fischler
fb85c2f05b RTU configurator updates for redstone I/O clarity 2024-04-21 13:54:14 -04:00
Mikayla Fischler
712d018806 Merge branch 'devel' into 412-additional-matrix-integrations 2024-04-21 12:09:53 -04:00
Mikayla Fischler
00a8d64a88 fixed coordinator not showing FAIL on unit count mismatch when connecting 2024-04-20 20:38:55 -04:00
Mikayla Fischler
d9efd5b8d2 #412 updates to RSIO for induction matrix low, high, and analog charge level 2024-04-20 16:32:18 -04:00
Mikayla Fischler
a786404092 #476 fixed PPM not initing fault counter in __index handler when a function is found 2024-04-16 15:55:40 -04:00
Mikayla
1bd03c0b1a
Merge pull request #473 from MikaylaFischler/devel
2024.04.14 Release
2024-04-15 11:59:40 -04:00
Mikayla Fischler
0d8d7aeb15 readme updates 2024-04-15 10:20:48 -04:00
Mikayla Fischler
0b60dc9fa4 #474 run main reactor-plc loop clock when not networked 2024-04-14 19:24:12 -04:00
Mikayla Fischler
48b91ac808 fixed reactor-plc configurator auth key displaying *** when not networked 2024-04-14 19:08:19 -04:00
Mikayla Fischler
4c9efc78b1 bumped up scada-common version 2024-04-14 16:30:50 -04:00
Mikayla
3c5857cd68
Merge pull request #472 from MikaylaFischler/pocket-alpha-dev
Work on Pocket Alpha App
2024-04-14 16:17:14 -04:00
Mikayla Fischler
473b0dd10d fixes and comments 2024-04-14 16:16:18 -04:00
Mikayla Fischler
d2df7db18b luacheck fixes 2024-04-14 15:30:13 -04:00
Mikayla Fischler
0ca002316f work on pocket, bumped comms version for PR 2024-04-14 15:28:38 -04:00
Mikayla Fischler
9fe34648c2 added system information/about app 2024-04-13 16:18:27 -04:00
Mikayla Fischler
57dc20692b need to use a compact signal bar due to how edge extensions work 2024-04-13 16:15:20 -04:00
Mikayla Fischler
f22d699baf flipped app pane scroll direction 2024-04-13 16:14:38 -04:00
Mikayla Fischler
23b31e0049 #410 pocket nav overhaul 2024-04-13 14:47:20 -04:00
Mikayla Fischler
2b4309afa7 pocket coordinator linking fixes 2024-04-13 11:02:41 -04:00
Mikayla
89cd5a07f2 Merge branch 'pocket-alpha-dev' of https://github.com/MikaylaFischler/cc-mek-scada into pocket-alpha-dev 2024-04-13 00:42:01 +00:00
Mikayla
99213da760 #200 work on pocket comms for unit data 2024-04-13 00:41:47 +00:00
Mikayla Fischler
f23f69d441 Merge branch 'devel' into pocket-alpha-dev 2024-04-12 20:27:21 -04:00
Mikayla
a99b7912f8
Merge pull request #471 from MikaylaFischler/366-charge-control-mode-idling
366 charge control mode idling
2024-04-12 20:07:21 -04:00
Mikayla Fischler
23ca5fb69e clear flow monitor on coordinator ui close 2024-04-12 20:06:39 -04:00
Mikayla Fischler
c243d064ef fixed incorrect render behavior on quick supervisor reconnects 2024-04-12 00:18:35 -04:00
Mikayla Fischler
878c3b92e1 #366 added idling to config and adjusted/fixed some behaviors 2024-04-12 00:13:05 -04:00
Mikayla Fischler
2aa52d2e2c simplified a config validation assert 2024-04-11 20:30:54 -04:00
Mikayla Fischler
dfc1ee6497 cleanup and constants 2024-04-10 22:16:41 -04:00
Mikayla Fischler
a6a1a61954 #470 reworked flow stability logic 2024-04-10 21:30:51 -04:00
Mikayla Fischler
d0b50c834c fixed coordinator not exiting on failed connection 2024-04-10 20:49:14 -04:00
Mikayla Fischler
612a06ba98 use os.clock rather than unix time for control 2024-04-09 22:24:03 -04:00
Mikayla Fischler
65d43d55c7 #366 idling and charge level PD gain changes 2024-04-09 22:23:43 -04:00
Mikayla Fischler
4fcd375ee2 show average charge rather than current charge below charge target 2024-04-09 22:20:46 -04:00
Mikayla Fischler
a22f5562cf only ramp up on reactor plc 2024-04-09 22:19:53 -04:00
Mikayla
0365ea5e8a Merge branch 'pocket-alpha-dev' of https://github.com/MikaylaFischler/cc-mek-scada into pocket-alpha-dev 2024-04-09 14:27:50 +00:00
Mikayla
da300607f1
Merge pull request #468 from MikaylaFischler/460-create-coordinator-ui-thread
460 create coordinator UI thread
2024-04-09 00:10:44 -04:00
Mikayla Fischler
cc3174ee76 comments and whitespace 2024-04-09 00:08:47 -04:00
Mikayla Fischler
98c37caecd #460 cleanup 2024-04-08 23:59:16 -04:00
Mikayla Fischler
cc50e4a020 #460 removed coordinator connecting grace period 2024-04-07 22:33:48 -04:00
Mikayla Fischler
f734c4307b #460 exit on UI crash 2024-04-07 20:55:07 -04:00
Mikayla Fischler
45573be8c7 #460 renderer logging 2024-04-07 20:47:31 -04:00
Mikayla Fischler
eab1ffbe03 #460 cleanup and moved date and time update to be behind UI ready rather than linked 2024-04-07 20:24:27 -04:00
Mikayla Fischler
1504247b33 #460 added yields throughout main UI rendering 2024-04-07 20:21:57 -04:00
Mikayla Fischler
92a4277f73 #460 moved connect/disconnect/resize to render thread 2024-04-07 19:54:22 -04:00
Mikayla Fischler
31a663e86b #460 threaded coordinator and moved UI start to render thread 2024-04-07 19:37:06 -04:00
Mikayla Fischler
5c0e2c32ee #461 prevent log spam in standalone mode when RPS trips continuously 2024-04-07 18:04:11 -04:00
Mikayla
3537c59365
Merge pull request #466 from MikaylaFischler/devel
2024.03.31 Release
2024-03-31 18:53:18 -04:00
Mikayla Fischler
1d7104ae74 Merge branch 'devel' into pocket-alpha-dev 2024-03-31 13:35:23 -04:00
Mikayla Fischler
4629e1ba2a #464 fixed color options button not appearing disabled when disabled 2024-03-31 00:03:28 -04:00
Mikayla Fischler
659644865a #449 include disconnected configured monitors in monitor list 2024-03-30 12:46:01 -04:00
Mikayla Fischler
03d2b3f087 #452 enable emergency coolant on boiler water level low when reactor can't cool 2024-03-29 18:31:17 -04:00
Mikayla
bea7b91ff1
Merge pull request #459 from MikaylaFischler/457-colorblind-independent-color-accessibility-modifiers
457 colorblind independent color accessibility modifiers
2024-03-25 10:13:15 -04:00
Mikayla Fischler
b94c89f4ec #457 cleanup 2024-03-25 10:11:35 -04:00
Mikayla Fischler
55e4e5a68b #457 fix for indicator background 2024-03-24 23:05:52 -04:00
Mikayla Fischler
e1ad76a00d #457 fixes and adjusted text 2024-03-24 13:56:19 -04:00
Mikayla Fischler
93e4590947 #457 added standard with black off 2024-03-24 13:39:24 -04:00
Mikayla Fischler
2442e7f972 #457 added ind_bkg for front panels 2024-03-24 13:03:48 -04:00
Mikayla Fischler
bb2c07963b #457 work on blue indicator modes 2024-03-24 12:56:51 -04:00
Mikayla Fischler
44c6352a8c #458 changed induction matrix title text to be white in dark mode 2024-03-23 01:15:42 -04:00
Mikayla Fischler
968b0a9122 log environment versions when debug logs are enabled 2024-03-23 00:49:19 -04:00
Mikayla Fischler
19869416af #434 #454 PPM improvements and undefined function overhaul 2024-03-23 00:26:58 -04:00
Mikayla
598f6f08af Merge branch 'devel' into pocket-alpha-dev 2024-03-13 13:55:51 +00:00
Mikayla
ca2d8ab7da
Merge pull request #450 from MikaylaFischler/devel
2024.03.12 Release
2024-03-12 21:15:24 -04:00
Mikayla Fischler
1c0f61b3e0 changed config defaults to use theme enums 2024-03-12 21:09:37 -04:00
Mikayla Fischler
ecd3575643 Merge branch 'color-update' into devel 2024-03-12 13:35:39 -04:00
Mikayla Fischler
9dc3a09f4d bumped up pocket version for configurator change 2024-03-12 13:35:15 -04:00
Mikayla
7a5d14d67f
Merge pull request #448 from MikaylaFischler/color-update
Colorblind Modes
2024-03-12 13:25:36 -04:00
Mikayla Fischler
d6175e5cec switched to using LEDPair elements for colorblind mode network lights 2024-03-12 13:23:39 -04:00
Mikayla Fischler
a00a824a7f cleanup and fixes 2024-03-12 13:15:28 -04:00
Mikayla Fischler
9393632428 removed unused value specifiers 2024-03-12 12:54:31 -04:00
Mikayla Fischler
886bd0d5d5 luacheck fixes 2024-03-12 12:49:33 -04:00
Mikayla Fischler
9c1d83fdfc Merge branch 'devel' into color-update 2024-03-12 12:46:17 -04:00
Mikayla Fischler
bd88244681 #439 remind user to configure peripherals and redstone, and provide buttons to do so 2024-03-12 12:45:47 -04:00
Mikayla
8dae632b25 simplified checks for colorblind mode 2024-03-12 16:24:32 +00:00
Mikayla Fischler
89d56d3101 moved main UI palettes to themes.lua and set configurators to use it 2024-03-11 23:54:03 -04:00
Mikayla Fischler
1f451ff92a #340 coordinator colorblind support 2024-03-11 23:31:31 -04:00
Mikayla Fischler
9d08b51f84 #340 restored softer blue for deuteranopia/protanopia basalt theme 2024-03-11 21:43:11 -04:00
Mikayla Fischler
047ff5c203 Merge branch 'devel' into color-update 2024-03-11 21:26:23 -04:00
Mikayla Fischler
895f768e58 Merge branch 'color-update' of github.com:MikaylaFischler/cc-mek-scada into color-update 2024-03-11 21:25:55 -04:00
Mikayla Fischler
b3f29566ea #340 colorblind modes for rtu, reactor-plc, and supervisor 2024-03-11 21:25:34 -04:00
Mikayla
c0a5c8d504
Merge pull request #447 from MikaylaFischler/color-update
Dark Mode Themes
2024-03-11 12:38:22 -04:00
Mikayla
bbe7b52662 bugfixes and cleanup 2024-03-11 16:35:06 +00:00
Mikayla Fischler
d5b166dcc6 Merge branch 'devel' into color-update 2024-03-09 18:51:21 -05:00
Mikayla
b0aa6d54ac
Merge pull request #446 from MikaylaFischler/devel
2024.03.09 Beta Hotfix
2024-03-09 18:14:02 -05:00
Mikayla Fischler
ad240ae44c #445 increment common version 2024-03-09 17:54:04 -05:00
Mikayla Fischler
79c93f1562 #445 fixed PPM undefined field logic and improved RTU unit fault handling 2024-03-09 13:24:06 -05:00
Mikayla Fischler
5d760a0524 #405 helper functions, enums, and name tables added to themes.lua 2024-03-09 12:41:45 -05:00
Mikayla Fischler
6c89b3134c #405 make pu fallback selector visible 2024-03-09 12:39:37 -05:00
Mikayla
3c7fff28c9
Merge pull request #443 from MikaylaFischler/devel
2024.03.08 Release
2024-03-08 22:40:28 -05:00
Mikayla Fischler
814043bf04 #405 basalt theme color adjustments 2024-03-07 20:46:10 -05:00
Mikayla Fischler
fc7896ebd3 #405 #340 rtu and supervisor configurator control of theme and color mode 2024-03-07 19:23:46 -05:00
Mikayla Fischler
48a8eadc55 #405 #340 reactor plc configurator control of theme and color mode 2024-03-07 18:00:33 -05:00
Mikayla
f0f2aadf53 #200 work on pocket comms 2024-03-07 17:27:25 +00:00
Mikayla
ce37a672a3 Merge branch 'devel' into pocket-alpha-dev 2024-03-07 16:21:37 +00:00
Mikayla Fischler
510995b04f #405 #340 coordinator configurator control of theme and color mode 2024-03-06 23:35:30 -05:00
Mikayla Fischler
560061d4ad removed latest shield and added missing common version shield 2024-03-06 20:51:23 -05:00
Mikayla Fischler
d87e3893f0 #405 plc and rtu front panel themes 2024-03-06 12:18:50 -05:00
Mikayla Fischler
fc198cd9d2 #405 supervisor and coordinator front panel themes 2024-03-06 11:43:31 -05:00
Mikayla Fischler
c714e49ad8 Merge branch 'devel' into color-update 2024-03-05 22:03:24 -05:00
Mikayla Fischler
d1e4ea586e supervisor comment cleanup 2024-03-05 21:47:14 -05:00
Mikayla Fischler
4e789ab92d cleanup and refactors 2024-03-05 21:24:17 -05:00
Mikayla Fischler
0892a57d35 #442 return rather than assert on configuration error 2024-03-05 20:17:52 -05:00
Mikayla Fischler
1bc4828010 #438 use reported polonium rate rather than an estimate 2024-03-05 19:35:54 -05:00
Mikayla Fischler
fb5a9d5d9e #432 fixes and enhancements to coordinator waiting on chunk loads 2024-03-05 17:16:35 -05:00
Mikayla Fischler
adbf1f2f78 #441 #431 bugfixes to the bugfixes 2024-03-05 17:12:12 -05:00
Mikayla Fischler
2f99aaeedb #431 handle ppm mount of unformed reactor race condition 2024-03-05 10:56:27 -05:00
Mikayla Fischler
a318ffb283 #405 styling improvements to PLC front panel 2024-03-05 10:51:18 -05:00
Mikayla Fischler
a677e994d6 Merge branch 'devel' into color-update 2024-02-26 14:44:47 -05:00
Mikayla Fischler
f9917b786c #432 wait 20s on computer power on before assuming monitor configuration problem 2024-02-25 18:02:13 -05:00
Mikayla Fischler
dbc1f41c5d #406 bounds check all controls 2024-02-25 15:53:14 -05:00
Mikayla Fischler
d6185e0183 #433 use os.clock instead of util.time_s for coordinator connection timeout to supervisor 2024-02-25 13:13:36 -05:00
Mikayla Fischler
6ef049baa1 #405 reverted yellow, desaturated orange 2024-02-25 13:09:30 -05:00
Mikayla Fischler
a4214e8a4f #405 fixed waste line still always being black and removed colormap test 2024-02-24 19:34:43 -05:00
Mikayla Fischler
45881067df fixed extra space in RCS indicator list 2024-02-24 18:01:30 -05:00
Mikayla Fischler
c40aa229bf #405 flow monitor theme implementation 2024-02-24 17:35:10 -05:00
Mikayla Fischler
51f2bba4d1 #405 theme implementation for unit displays 2024-02-24 15:37:39 -05:00
Mikayla Fischler
628a50e1bd #405 WIP themes and completed main display theme implementation 2024-02-24 14:35:04 -05:00
Mikayla Fischler
cdd31508d9 #430 fixed unit boiler, turbine, and tank status indicators flashing OFF-LINE when online 2024-02-22 21:35:08 -05:00
Mikayla
83d62991f8
Merge pull request #429 from MikaylaFischler/328-coordinator-temperature-unit-options
328 coordinator temperature unit options
2024-02-22 20:50:01 -05:00
Mikayla Fischler
f207a950e4 fixed bug with hmac still being used for connecting in coordinator configurator after clearing key 2024-02-22 19:25:55 -05:00
Mikayla Fischler
0b0051dc2f #328 K, C, F, and R temperature unit options 2024-02-22 19:25:16 -05:00
Mikayla Fischler
f152c37ea9 #387 handle resizing, improved reconnect handling, fixed disconnect detection bug 2024-02-21 20:33:07 -05:00
Mikayla Fischler
372fd426d8 test code for psil allocations 2024-02-21 18:48:55 -05:00
Mikayla
8347afb6d0
Merge pull request #420 from MikaylaFischler/devel
2024.02.19 Release
2024-02-21 13:12:51 -05:00
Mikayla Fischler
10d0a9763a disabled a verbose log message 2024-02-21 12:59:48 -05:00
Mikayla Fischler
96691d773a supervisor debug log messages and #427 fix 2024-02-21 12:58:49 -05:00
Mikayla Fischler
910509d764 coordinator configurator bugfixes 2024-02-20 19:33:14 -05:00
Mikayla Fischler
158cc39b80 #421 remove 'latest' branch 2024-02-19 20:40:05 -05:00
Mikayla
940f71aa35
Merge pull request #422 from MikaylaFischler/145-graphical-configure-utilities
Pocket Configurator and Other Fixes
2024-02-19 20:34:41 -05:00
Mikayla Fischler
788cef8f86 comment cleanup and absolute paths while saving 2024-02-19 20:32:33 -05:00
Mikayla Fischler
baaef862ab #145 coordinator configurator enhancements 2024-02-19 20:26:05 -05:00
Mikayla Fischler
96db709ced ccmsi print fix 2024-02-19 20:24:30 -05:00
Mikayla Fischler
c47fa5433f #408 improvements to pocket configurator 2024-02-19 19:36:27 -05:00
Mikayla Fischler
440b724798 #424 tick comms version up 2024-02-19 19:33:08 -05:00
Mikayla Fischler
126d6eb163 #424 fixed key derivation init 2024-02-19 19:28:12 -05:00
Mikayla Fischler
8ac46faf36 set text scales before checking monitor dimensions 2024-02-19 18:56:24 -05:00
Mikayla Fischler
f112746e12 #408 increment bootloader version for pocket configurator, minification 2024-02-19 14:27:02 -05:00
Mikayla Fischler
76f21e925b #145 removed unneeded references to config.lua files + some minification 2024-02-19 14:18:23 -05:00
Mikayla Fischler
a330249c7e #408 integrate new settings with pocket 2024-02-19 14:07:26 -05:00
Mikayla Fischler
bbc64c8dc2 #145 fixed oversized listboxes 2024-02-19 13:54:23 -05:00
Mikayla Fischler
bb062cf397 #408 added pocket configure to configure launcher 2024-02-19 13:50:38 -05:00
Mikayla Fischler
53bb36ce8d #145 fixed change log page on coordinator 2024-02-19 13:50:03 -05:00
Mikayla Fischler
8237113577 #408 pocket configurator 2024-02-19 13:49:50 -05:00
Mikayla Fischler
02906ae707 add FUNDING.yml 2024-02-19 12:49:26 -05:00
Mikayla Fischler
6d0e777e68 updated copyright and removed coordinator from list mentioning config.lua 2024-02-18 21:40:25 -05:00
Mikayla
36468c4043
Merge pull request #419 from MikaylaFischler/145-graphical-configure-utilities
Coordinator Configurator
2024-02-18 21:36:50 -05:00
Mikayla Fischler
1cf7375311 #309 cleanup 2024-02-18 21:34:25 -05:00
Mikayla Fischler
ca55948286 #309 remove legacy config.lua 2024-02-18 21:25:21 -05:00
Mikayla Fischler
195f59178f #309 bugfix to apisessions still using old config 2024-02-18 21:24:30 -05:00
Mikayla Fischler
e416faf313 #309 integrated process control with new settings file 2024-02-18 20:47:37 -05:00
Mikayla Fischler
20bff48cfd #309 cleanup, fixes, optimizations 2024-02-18 20:21:07 -05:00
Mikayla Fischler
1a9892b291 cleanup and optimizations 2024-02-18 16:49:39 -05:00
Mikayla Fischler
827953c0a1 more luacheck fixes 2024-02-18 15:31:45 -05:00
Mikayla Fischler
56e69e3a29 luacheck fixes 2024-02-18 15:30:18 -05:00
Mikayla Fischler
1984b63837 additional config validations 2024-02-18 15:25:30 -05:00
Mikayla Fischler
36b12d5dea #309 integrated new configuration into coordinator 2024-02-18 15:21:00 -05:00
Mikayla Fischler
3e83c8e2c6 #309 moved monitor block size to ppm and fixed size estimation for monitor requirements 2024-02-18 13:00:18 -05:00
Mikayla Fischler
1fa8c03dff #309 import legacy configs 2024-02-18 00:56:36 -05:00
Mikayla Fischler
d6de9c266b #309 viewing and saving coordinator config 2024-02-17 22:39:50 -05:00
Mikayla Fischler
2142c1b4f7 updated license year 2024-02-17 18:39:02 -05:00
Mikayla Fischler
cafba6c67a #309 legacy options and general improvements 2024-02-17 18:38:36 -05:00
Mikayla Fischler
6eccebbe39 #409 fixed positioning 2024-02-17 18:16:21 -05:00
Mikayla Fischler
5e9f03c900 #418 fixed graphics bug with redraw 2024-02-17 18:12:28 -05:00
Mikayla Fischler
7374bb02d1 #309 speaker and time format configuration 2024-02-14 14:41:34 -05:00
Mikayla Fischler
d19794ae4f #309 unassign unused monitors on unit count reduction and support autofilling fields when editing existing monitor configs 2024-02-14 10:24:27 -05:00
Mikayla Fischler
108cf75cad #309 coordinator monitor configuration 2024-02-14 09:43:30 -05:00
Mikayla Fischler
34cbb6be39 bugfixes 2024-02-03 01:48:56 -05:00
Mikayla
907f27baf8 #309 show data received from supervisor 2024-02-02 23:01:51 +00:00
Mikayla Fischler
4710fa7cee #309 WIP coordinator configurator 2024-01-31 14:10:03 -05:00
Mikayla Fischler
526a54903e #410 moved diagnostic apps to main app list and added app page nav 2024-01-15 17:40:43 -05:00
Mikayla Fischler
e1a632dcc7 Merge branch 'devel' into pocket-alpha-dev 2024-01-14 14:21:09 -05:00
Mikayla Fischler
d5d818e625 pocket signal bars 2024-01-14 14:20:59 -05:00
Mikayla
bfa1f4d0c6
Merge pull request #404 from MikaylaFischler/devel
2023.12.31 Release
2023-12-31 21:34:40 -05:00
Mikayla Fischler
737e0d72b0 changed supervisor facility config color theme as green is for the summary already 2023-12-31 15:41:28 -05:00
Mikayla Fischler
6edeb3e3b8 add default value to sounder volume for very old RTU config imports 2023-12-31 15:00:38 -05:00
Mikayla Fischler
fb00e98a5b more supervisor configurator bugfixes 2023-12-31 15:00:08 -05:00
Mikayla Fischler
4f952eff83 fixed supervisor incorrectly trying to validate tank defs when tank mode is zero 2023-12-31 14:14:35 -05:00
Mikayla Fischler
1eede97c08 fixed supervisor not using proper config on front panel 2023-12-31 14:04:22 -05:00
Mikayla Fischler
95419562ee no longer mention config.lua for supervisor update 2023-12-31 13:17:49 -05:00
Mikayla Fischler
03de90c3d8 fixes to page navigation nav_up 2023-12-31 12:58:24 -05:00
Mikayla Fischler
99096e0fc9 Merge branch 'devel' into pocket-alpha-dev 2023-12-30 22:58:53 -05:00
Mikayla Fischler
7b85d947c4 fixed supervisor always using MAC 2023-12-30 22:57:30 -05:00
Mikayla Fischler
08e670091a #396 fixed fractional connection timeouts being treated as invalid 2023-12-30 20:25:57 -05:00
Mikayla
1348b632a8
Merge pull request #397 from MikaylaFischler/145-graphical-configure-utilities
Supervisor Configurator
2023-12-30 20:19:34 -05:00
Mikayla Fischler
8cd5162362 fixed PLCs not connecting, fixed facility tank mode checkbox not changing after import, and reordered info on tank mode vis about page 2023-12-30 20:18:58 -05:00
Mikayla Fischler
1c410a89d8 incremented comms version due to data change 2023-12-30 19:40:53 -05:00
Mikayla Fischler
6a931fced4 cleanup and fixes 2023-12-30 19:21:44 -05:00
Mikayla Fischler
622e2eeb90 more useful messages, incremented bootloader version 2023-12-30 14:51:25 -05:00
Mikayla Fischler
42cd9fff0c improved number field precision handling and limited decimal precision of timeouts #396 2023-12-30 14:41:03 -05:00
Mikayla Fischler
2a85a438ba #396 connection timeouts can now have a fractional part 2023-12-29 14:33:22 -05:00
Mikayla Fischler
338b3b1615 addressed luacheck warning 2023-12-29 14:29:46 -05:00
Mikayla Fischler
363f164f47 #308 deleted old config.lua 2023-12-29 14:12:54 -05:00
Mikayla Fischler
739f04ece9 #308 integrated new settings file with supervisor 2023-12-29 13:58:28 -05:00
Mikayla Fischler
c6ade68ce2 #308 importing legacy config 2023-12-29 12:40:48 -05:00
Mikayla Fischler
7d60e259e2 #308 supervisor configurator bugfixes and saving of settings 2023-12-29 01:07:50 -05:00
Mikayla Fischler
cd71c6a9c1 #308 summary display of supervisor config 2023-12-29 00:19:17 -05:00
Mikayla Fischler
26fe130609 #308 supervisor configurator completed facility tank mode and network config pages 2023-12-28 15:06:30 -05:00
Mikayla Fischler
95f87b1b05 #308 significantly improved facility dynamic tank configuration visualization 2023-12-26 13:13:05 -05:00
Mikayla Fischler
aebdf3e8df fixed include ordering 2023-12-26 13:11:46 -05:00
Mikayla Fischler
0395aa95b6 indicate pocket app is a work in progress 2023-12-22 22:30:22 -05:00
Mikayla Fischler
c624baed2c #395 fixes to new pocket navigation 2023-12-22 12:46:36 -05:00
Mikayla
1a40321c0f #395 pocket navigation system 2023-12-22 16:12:47 +00:00
Mikayla Fischler
5d4fc36256 #308 WIP supervisor configurator 2023-12-18 15:23:51 -05:00
Mikayla
b799d785b9
Merge pull request #394 from MikaylaFischler/devel
2023.12.17 Release
2023-12-17 21:02:32 -05:00
Mikayla
d55442fa53
Merge pull request #393 from MikaylaFischler/145-graphical-configure-utilities
Bring in changes from 145 branch to devel for release
2023-12-17 20:51:05 -05:00
Mikayla Fischler
c870b749a4 cleanup 2023-12-17 20:48:02 -05:00
Mikayla Fischler
4421cbc0c5 fixed input/output side text being sometimes wrong on rtu configurator redstone editing 2023-12-17 20:47:17 -05:00
Mikayla Fischler
b6a3305f23 minor minification 2023-12-17 20:43:40 -05:00
Mikayla Fischler
5680260136 use existing is_valid_port rather than repeating the code 2023-12-17 20:43:08 -05:00
Mikayla Fischler
bc66ea6ecb #381 fixed plc main thread crash on modem connect after boot with no modem 2023-12-17 20:28:26 -05:00
Mikayla Fischler
1b20218445 #194 #382 ccmsi no longer deletes drive mounts and now prompts to delete unknown files/folders in root 2023-12-17 20:10:11 -05:00
Mikayla Fischler
466e442353 #389 added width to RTU front panel entry name box 2023-12-17 19:39:00 -05:00
Mikayla Fischler
9e6751f47f #391 fixed editing of redstone entries 2023-12-17 19:32:01 -05:00
Mikayla Fischler
f868923905 #392 fixed typo preventing water level low indicator from working 2023-12-17 18:04:09 -05:00
Mikayla Fischler
5d3fd6d939 #390 fixed not being able to edit entries after using ALL_WASTE shortcut 2023-12-17 17:46:18 -05:00
Mikayla Fischler
37659d687e #388 fixed peripherals list not updating on add/delete of config entry 2023-12-17 17:22:29 -05:00
Mikayla Fischler
f23b7e2c2f fixed out of bounds coordinates crashing GUI for form fields 2023-12-17 12:56:08 -05:00
Mikayla Fischler
55ccdd63d4 don't mention config.lua on update for apps that don't have it 2023-12-17 12:55:00 -05:00
Mikayla Fischler
5c88890ed4 removed redundant min_width values 2023-12-14 20:51:54 -05:00
Mikayla Fischler
fa0185c9a4 fixed checkbox width 2023-12-13 12:20:12 -05:00
Mikayla Fischler
e1ed9a8e5e fixed error messages not fitting and say input side when configuring inputs on RTU configurator 2023-11-29 22:25:34 -05:00
Mikayla
9f7e3bc282
Merge pull request #383 from MikaylaFischler/devel
2023.11.28 Beta Hotfix
2023-11-28 20:35:56 -05:00
Mikayla Fischler
4ec060ba24 #378 fixed unit input not being re-shown on RTU configurator 2023-11-28 18:05:07 -05:00
Mikayla
b1f1753a8d
Merge pull request #379 from MikaylaFischler/377-compatibility-fixes-for-lua-5.2
#377 switched to using ... for vararg
2023-11-28 17:54:36 -05:00
Mikayla Fischler
94a62f8c31 #377 switched to using ... for vararg 2023-11-27 19:32:52 -05:00
Mikayla
e6f49f256c
Merge pull request #372 from MikaylaFischler/devel
2023.11.15 Hotfix
2023-11-15 19:41:29 -05:00
Mikayla Fischler
a048b0aa4a #371 fixed RTU configurator bug with empty peripherals or redstone, re-ordered settings load, added type checks 2023-11-15 19:30:49 -05:00
Mikayla
b6617c140c
Merge pull request #369 from MikaylaFischler/devel
2023.11.14 Release
2023-11-14 22:10:12 -05:00
Mikayla Fischler
1fdf012f65 properly clear peripherals and redstone when importing 2023-11-14 22:00:01 -05:00
Mikayla Fischler
8fe0321ac0 fixed RTU authkey check 2023-11-14 19:40:55 -05:00
Mikayla Fischler
4a2199fa13 readme update 2023-11-14 19:40:29 -05:00
Mikayla
69680a53a0
Merge pull request #368 from MikaylaFischler/145-graphical-configure-utilities
RTU configurator and other updates
2023-11-12 18:37:23 -05:00
Mikayla Fischler
785dea6545 #306 fixed incorrect screenflow and changed peripheral import validation symbols 2023-11-12 18:36:16 -05:00
Mikayla Fischler
885932afe1 don't try to log if log.init wasn't called 2023-11-12 18:35:46 -05:00
Mikayla Fischler
a38ccf3dcc #145 #306 improvements and fixes, better peripheral import 2023-11-12 18:27:24 -05:00
Mikayla Fischler
d7b1f9cc7e #306 #362 bugfixes 2023-11-12 16:55:24 -05:00
Mikayla Fischler
6e92097544 fixed util.concat handling of nil parameters 2023-11-12 16:06:16 -05:00
Mikayla Fischler
76403b4ddc cleanup and grammar 2023-11-12 15:38:25 -05:00
Mikayla Fischler
41ad8d8edb #306 prevent duplicate redstone inputs 2023-11-12 14:35:53 -05:00
Mikayla Fischler
68754977b0 cleanup and fixes 2023-11-12 14:21:48 -05:00
Mikayla Fischler
78ad6d5457 luacheck fix 2023-11-12 12:00:42 -05:00
Mikayla Fischler
cb049ebf41 #194 changed 'newer' to 'different' in ccmsi 2023-11-12 11:57:05 -05:00
Mikayla Fischler
f2f5c3201f #362 taking max of connected radiation monitors 2023-11-12 11:54:47 -05:00
Mikayla Fischler
1ba178eae8 #306 delete legacy RTU config 2023-11-06 10:22:52 -05:00
Mikayla Fischler
838f80c30c #306 #362 supervisor updates for RTU config changes 2023-11-06 10:21:42 -05:00
Mikayla Fischler
dc0408881e #145 use rsio.color_name on PLC configurator 2023-11-06 10:11:57 -05:00
Mikayla Fischler
1b5e8cb69c #306 RTU integration with new settings 2023-11-06 09:25:44 -05:00
Mikayla Fischler
9e13a3a467 added ability to view reactor PLC config after importing if cancelled before deleting 2023-11-05 13:23:45 -05:00
Mikayla Fischler
32653c3b8a param type change and added validator.assert 2023-11-05 13:23:22 -05:00
Mikayla Fischler
16258a2631 rtu configurator config import 2023-11-04 15:06:29 -04:00
Mikayla Fischler
4c646249ad plc configurator cleanup 2023-11-04 14:57:17 -04:00
Mikayla Fischler
45c8a8d8a9 added peripheral connections to rtu configurator 2023-11-04 13:29:38 -04:00
Mikayla Fischler
eff3444834 added type def to ppm and return a copy of the peripherals list rather than the table itself 2023-11-04 12:56:49 -04:00
Mikayla Fischler
25f68f338c bootloader cleanup and added license to installer downloads 2023-11-04 12:51:24 -04:00
Mikayla Fischler
7ef363a3c2 fixed installer typo 2023-11-04 12:49:54 -04:00
Mikayla Fischler
3065e2bece plc configurator clear settings when loading settings and show actual current settings on view 2023-11-04 12:49:14 -04:00
Mikayla Fischler
1075d66122 graphics bugfix with disabled input fields 2023-11-04 12:48:06 -04:00
Mikayla Fischler
d477b33774 fixed reposition not repositioning frame for mouse events 2023-10-21 13:58:42 -04:00
Mikayla Fischler
7b374f8618 rtu redstone configuration 2023-10-19 23:35:18 -04:00
Mikayla Fischler
4869c00c0e added side type alias and added some validation to RSIO 2023-10-19 23:22:04 -04:00
Mikayla Fischler
ff4a5a68d9 reactor PLC configurator emercoolcolor correction 2023-10-19 23:20:41 -04:00
Mikayla Fischler
d77a527b15 added text alignment to push buttons and added keyboard events to listbox 2023-10-19 23:20:04 -04:00
Mikayla Fischler
01caca48dc listbox improvements, tabbing while staying in frame (autoscroll) 2023-10-15 17:02:48 -04:00
Mikayla Fischler
43e545b6ae fixed unfocus all 2023-10-15 16:49:03 -04:00
Mikayla Fischler
8b65956dcc #306 base RTU configurator 2023-10-15 13:26:49 -04:00
Mikayla
7b522ae120
Merge pull request #360 from MikaylaFischler/devel
2023.10.14 Release
2023-10-14 19:46:33 -04:00
Mikayla Fischler
43d134d9ad comment clarity 2023-10-14 18:49:51 -04:00
Mikayla Fischler
24c787f47d #353 fixed auto lock not restoring on reconnect 2023-10-14 17:57:50 -04:00
Mikayla Fischler
f95ac8be8c #359 drop packets with nil distances if using trusted range feature 2023-10-14 12:17:25 -04:00
Mikayla Fischler
41442012c2 #357 min length of auth key and some cleanup, added config change log 2023-10-14 12:02:25 -04:00
Mikayla Fischler
6f1195dded fixed element assertion scope on frame validations 2023-10-14 11:40:36 -04:00
Mikayla Fischler
86e8feaabc #358 fixed non-networked PLC operation 2023-10-14 10:07:56 -04:00
Mikayla Fischler
670ca78a5b #356 fixed extract assert msg 2023-10-14 00:38:13 -04:00
Mikayla Fischler
73ceed0f60 #354 another reversion 2023-10-13 23:53:15 -04:00
Mikayla Fischler
8412270772 #354 some reversions 2023-10-13 23:48:30 -04:00
Mikayla Fischler
686b47898c #354 optimizations/minification 2023-10-08 12:47:51 -04:00
Mikayla
e03eaf2982 #354 type check functions 2023-10-07 20:57:24 +00:00
Mikayla
8b1775b0af
Merge pull request #352 from MikaylaFischler/devel
2023.10.04 Hotfix
2023-10-04 20:03:06 -04:00
Mikayla
8bbd04d133
Update feature_request.md 2023-10-04 17:32:43 -04:00
Mikayla
26dc6ff6d1 #350 handle RPS_DISABLE ack packet on supervisor 2023-10-04 21:28:14 +00:00
Mikayla
24d190921d #349 F_ALARM_ANY rsio output added 2023-10-04 21:26:07 +00:00
Mikayla
4fec116e93
Merge pull request #348 from MikaylaFischler/devel
2023.10.03 Release
2023-10-04 00:11:47 -04:00
Mikayla Fischler
ef6fdaa3ac don't report settings files as not used 2023-10-03 23:39:14 -04:00
Mikayla
0160d4c53a
Merge pull request #347 from MikaylaFischler/145-graphical-configure-utilities
#307 Reactor PLC Configurator
2023-10-03 23:18:02 -04:00
Mikayla Fischler
d2a1951b66 refactored TEXT_ALIGN to ALIGN 2023-10-03 23:16:46 -04:00
Mikayla Fischler
5d7a0b266a handle new settings file and not deleting legacy config 2023-10-03 23:11:52 -04:00
Mikayla Fischler
ebabd99f2b #307 fixes and cleanup 2023-10-03 22:52:13 -04:00
Mikayla Fischler
b5e0183e54 luacheck fix and added keys to luacheck globals 2023-10-01 19:16:44 -04:00
Mikayla Fischler
d450c9ca3e Merge branch 'devel' into 145-graphical-configure-utilities 2023-10-01 18:43:28 -04:00
Mikayla Fischler
894229831d #307 configure bugfixes and settings file rename 2023-10-01 17:12:59 -04:00
Mikayla Fischler
bfa24b3665 #307 PLC integration with new config storage 2023-10-01 17:10:16 -04:00
Mikayla Fischler
b1446637ad checkbox default val and radio type checks for set_value 2023-10-01 17:06:24 -04:00
Mikayla Fischler
02e9c09daf #307 configurator error reporting 2023-10-01 15:30:49 -04:00
Mikayla Fischler
21d5cb3858 #307 reactor PLC configurator 2023-10-01 00:21:46 -04:00
Mikayla Fischler
c0a602385d recycle log at <512B free 2023-10-01 00:20:19 -04:00
Mikayla Fischler
4d4dd4ed39 fix to redraw and improvements to hide() 2023-10-01 00:19:16 -04:00
Mikayla Fischler
3a5d69d96f improvements to number field 2023-10-01 00:18:57 -04:00
Mikayla Fischler
d38a2dea7c #344 renderer integration with new assertion handling 2023-09-30 13:31:41 -04:00
Mikayla Fischler
560d48084a #344 coordinator renderer assert handling 2023-09-30 12:19:04 -04:00
Mikayla Fischler
625feb3fd1 #344 graphics assertion overhaul 2023-09-30 11:46:47 -04:00
Mikayla Fischler
ed4180a072 #344 element redraws and shorter assert messages 2023-09-29 19:34:10 -04:00
Mikayla Fischler
70831b49d2 #344 2D radio button array 2023-09-24 22:27:39 -04:00
Mikayla Fischler
881a120d34 #145 more work on plc configurator 2023-09-23 20:22:02 -04:00
Mikayla Fischler
8ab1307b2b #344 include holding down keys for number fields 2023-09-23 16:50:54 -04:00
Mikayla Fischler
689d474796 #344 support hiding characters in text fields 2023-09-23 16:45:33 -04:00
Mikayla Fischler
18bcfb4014 #344 nav to start/end of fields 2023-09-23 15:30:53 -04:00
Mikayla Fischler
645a5f5137 #344 added focus navigation to checkboxes and radio buttons, refactor of enable handlers 2023-09-23 14:31:37 -04:00
Mikayla Fischler
1f9743efd0 #344 don't hide cursor at end of input length 2023-09-23 13:45:00 -04:00
Mikayla Fischler
9cef6e6175 #344 added double click events to event handlers 2023-09-23 12:58:09 -04:00
Mikayla Fischler
70b03896d5 added double click to custom events 2023-09-23 12:53:05 -04:00
Mikayla Fischler
f9d0ef60b4 #344 select all and improved input fields 2023-09-23 12:49:31 -04:00
Mikayla Fischler
09ab60f79d #344 double click support 2023-09-23 00:11:45 -04:00
Mikayla Fischler
d21604ea09 #344 improvements to text fields 2023-09-23 00:09:37 -04:00
Mikayla Fischler
611b048cb4 #307 work in progress PLC configurator 2023-09-20 00:02:30 -04:00
Mikayla Fischler
a2182d9566 #344 work in progress on text field & paste events, re-show number field on val/min/max changes 2023-09-20 00:02:21 -04:00
Mikayla Fischler
7a87499aa4 #344 radio button appearance changes 2023-09-20 00:02:05 -04:00
Mikayla Fischler
b173b72f21 #344 numeric field cleanup 2023-09-20 00:01:49 -04:00
Mikayla
29cc107ea5 #329 updated comment 2023-09-19 20:40:11 +00:00
Mikayla
c24766a4db #329 disable reactor rather than trip on auto control stop 2023-09-19 20:37:15 +00:00
Mikayla Fischler
29e910ba3c #342 added element focusing feature to graphics library 2023-09-16 21:08:28 -04:00
Mikayla Fischler
1cb240b1b0 improved ignoring mouse events for hidden elements 2023-09-05 15:32:45 -04:00
Mikayla Fischler
1525ed9d60 Merge branch 'devel' into 145-graphical-configure-utilities 2023-09-03 19:38:17 -04:00
Mikayla Fischler
b1c2c4d291 #339 added sum of raw waste stat to flow monitor 2023-09-03 18:07:34 -04:00
Mikayla Fischler
5585088e3a #338 resolved diagnostic warnings 2023-09-03 17:54:39 -04:00
Mikayla
b9073153b3
Merge pull request #341 from MikaylaFischler/300-code-footprint-cleanup-tasks
300 code footprint cleanup tasks
2023-09-01 22:52:47 -04:00
Mikayla Fischler
cb554e5d16 luacheck fixes 2023-09-01 22:51:02 -04:00
Mikayla Fischler
71d8b5ba0a #300 utilizing style file for common color pairs 2023-09-01 22:24:31 -04:00
Mikayla
e4f49e9949 #145 configure bootstrap command and size reduction of startup/initenv 2023-09-01 14:23:39 +00:00
Mikayla Fischler
3afc765f72 #300 graphics alias functions 2023-08-30 21:11:57 -04:00
Mikayla Fischler
048714817e #300 replaced util.strrep with string.rep 2023-08-30 19:30:46 -04:00
Mikayla
f267a4e569 #300 comms device type cleanup 2023-08-30 21:15:42 +00:00
Mikayla
cfc6479dd5 #300 comms cleanup 2023-08-30 20:45:48 +00:00
Mikayla
70f24edb53
Merge pull request #337 from MikaylaFischler/305-detailed-info-on-multi-condition-alarms
305 detailed info on multi condition alarms
2023-08-30 09:00:16 -04:00
Mikayla Fischler
31df4a7f7e removed unnecessary parentheses 2023-08-29 22:41:56 -04:00
Mikayla Fischler
ca49cf90b4 #305 improved log message clarity 2023-08-29 22:34:30 -04:00
Mikayla
785dbe9533 #305 print out cause of multi-condition alarms 2023-08-29 13:19:50 +00:00
Mikayla Fischler
a9d1bc2b50 #336 consolidated remove and purge into uninstall, added clarification on low space handling 2023-08-28 23:19:30 -04:00
Mikayla
f7766d8cba
Merge pull request #334 from MikaylaFischler/devel
2023.08.27 Release
2023-08-27 14:02:43 -04:00
Mikayla Fischler
37f8b85924 #333 always set emergency coolant state 2023-08-27 13:42:25 -04:00
Mikayla Fischler
2ed28cf74d #324 fixed alarm sounder lag 2023-08-26 19:01:22 -04:00
Mikayla Fischler
17698b7fb4 #332 fixed turbine production rate on coordinator UI 2023-08-26 12:22:47 -04:00
Mikayla Fischler
386a33ffd8 #298 consistent log tags 2023-08-26 11:54:58 -04:00
Mikayla Fischler
b7d4468cea #327 close connections on timeout 2023-08-25 21:42:35 -04:00
Mikayla Fischler
8b0a5d529e #330 close coordinator comms on error exit 2023-08-25 21:02:24 -04:00
Mikayla Fischler
d18a93f7d2 #326 added commas to dynamic tank fill 2023-08-25 20:53:28 -04:00
Mikayla Fischler
89d1087b1c updated flow monitor to say boiler when 1, boilers when 2 2023-08-25 20:49:38 -04:00
Mikayla Fischler
d9e48f5cac #325 fixed coordinator unit overview height calcs 2023-08-25 20:02:59 -04:00
Mikayla
56377ef595
Merge pull request #322 from MikaylaFischler/devel
2023.08.22 Hotfix 2
2023-08-22 21:49:32 -04:00
Mikayla Fischler
95c300e450 #321 fixed boiler flow indicators on flow monitor 2023-08-22 21:46:34 -04:00
Mikayla
2985898b7e
Merge pull request #320 from MikaylaFischler/devel
2023.08.22 Hotfix
2023-08-22 20:05:53 -04:00
Mikayla Fischler
57d50e6745 #319 updated installer version 2023-08-22 19:56:47 -04:00
Mikayla Fischler
3dc1a06969 #319 fixed installer bug on fresh install 2023-08-22 19:55:34 -04:00
Mikayla Fischler
fcba935240 updated readme with new installer 2023-08-22 19:13:34 -04:00
Mikayla Fischler
9b32bb4675 deleted legacy install manifest for v1.0 installer 2023-08-22 19:06:36 -04:00
Mikayla
f59f484e7b
Merge pull request #317 from MikaylaFischler/devel
2023.08.22 Release
2023-08-22 18:42:21 -04:00
Mikayla Fischler
0fe9b391d8 #313 installer self-update fix, added update command for it 2023-08-21 22:47:00 -04:00
Mikayla
97f0191875 #313 installer self update 2023-08-22 02:18:25 +00:00
Mikayla Fischler
70db8d782c fixed unit dynamic tank state indicator 2023-08-21 22:05:02 -04:00
Mikayla
2acd166c3e
Merge pull request #316 from MikaylaFischler/232-waste-valve-and-flow-monitoring-display
232 Waste Valve and Flow Monitoring Display
2023-08-21 21:54:22 -04:00
Mikayla Fischler
c78f7e173a #232 cleanup, changed antimatter rate to be integer on main display 2023-08-21 21:53:31 -04:00
Mikayla Fischler
99a0b0a55a #232 documentation and refactor 2023-08-21 21:44:15 -04:00
Mikayla Fischler
6e51e70b62 #232 cleanup and fixes 2023-08-21 21:37:56 -04:00
Mikayla Fischler
fd2abad5cf changed some green/red indicators to be green/gray for contrast 2023-08-21 21:35:32 -04:00
Mikayla Fischler
b93c6b7c6e fixes per luacheck 2023-08-20 23:53:49 -04:00
Mikayla Fischler
8b3f558f68 Merge branch 'devel' into 232-waste-valve-and-flow-monitoring-display 2023-08-20 23:43:07 -04:00
Mikayla Fischler
8c5289867c #232 updated coordinator monitor disconnect/reconnect handling for changes 2023-08-20 23:28:48 -04:00
Mikayla Fischler
d179920565 #232 option to disable flow view screen for legacy setups 2023-08-20 23:23:23 -04:00
Mikayla Fischler
504ee0594f #315 switch off dynamic tank fill mode if emergency coolant is required 2023-08-20 22:56:51 -04:00
Mikayla Fischler
a92f182156 #232 fixed incorrect arrows on turbine flow view 2023-08-20 22:53:14 -04:00
Mikayla Fischler
c5d38a5584 #232 added container mode indicators for tanks 2023-08-20 17:30:34 -04:00
Mikayla Fischler
9bf07e6c3e completed work on updated pipenet 2023-08-20 17:04:14 -04:00
Mikayla Fischler
7656936982 #232 cleanup, added general stats 2023-08-20 16:52:12 -04:00
Mikayla Fischler
f477ad9426 #232 re-indexed valve IDs 2023-08-19 23:28:03 -04:00
Mikayla Fischler
59950e9d15 #232 connected valve indicators 2023-08-19 23:24:20 -04:00
Mikayla Fischler
11d86d92eb #232 bugfixes and linked up indicators to data 2023-08-19 20:06:37 -04:00
Mikayla Fischler
1275f61113 #232 refactor and fixed sv config verify 2023-08-19 13:42:07 -04:00
Mikayla Fischler
d17e2b8321 #232 completed display of flow/dynamic tank/sps, dynamically sized 2023-08-19 13:38:05 -04:00
Mikayla Fischler
ce780c3d72 added common color pairs to coordinator style 2023-08-13 00:51:37 -04:00
Mikayla Fischler
76ab4e17bf #232 WIP full flow view drawn out 2023-08-13 00:11:58 -04:00
Mikayla Fischler
ac1733c46e #314 20s grace period for coordinator render to finish to prevent timeouts 2023-08-12 15:16:37 -04:00
Mikayla
17731de61b #312 improved reactor peripheral handling 2023-08-11 14:20:13 +00:00
Mikayla Fischler
d85385c1fe #232 continued work on flow monitor, added SPS display 2023-08-10 23:31:38 -04:00
Mikayla Fischler
e0809f52a6 #232 WIP coordinator flow view 2023-08-09 23:26:06 -04:00
Mikayla Fischler
b2c55f9d4b #303 check modem message distance for nil 2023-08-02 10:13:54 -04:00
Mikayla
ba896ea163
Merge pull request #302 from MikaylaFischler/common-cleanup
Common Cleanup
2023-07-30 21:38:18 -04:00
Mikayla Fischler
1a64591256 #282 version the common directory 2023-07-30 20:46:04 -04:00
Mikayla Fischler
9ce75eb4bd #283 common cleanup, added lockbox version to crash dump, changed crash handler to pcall requires for graphics/lockbox 2023-07-30 12:24:54 -04:00
Mikayla
451f804f87
Merge pull request #301 from MikaylaFischler/rtu-speaker-system
RTU Speaker System and Pocket Diagnostics
2023-07-30 00:14:49 -04:00
Mikayla Fischler
724d13510d optimizations and cleanup for pull request 2023-07-30 00:13:26 -04:00
Mikayla Fischler
3f01ce7ec5 lowered SVS queue process time limit warning to debug level 2023-07-29 18:44:17 -04:00
Mikayla Fischler
df67795239 #290 pocket page management and alarm test tool, supervisor pocket diagnostics system 2023-07-29 18:16:59 -04:00
Mikayla Fischler
775d4dc95b #264 improvements to RTU speaker sounder 2023-07-29 17:57:51 -04:00
Mikayla Fischler
b3c7263bc4 #299 fixed mouse events passing to hidden elements 2023-07-29 00:25:20 -04:00
Mikayla Fischler
9f8732830c #264, #280 fixed sounder issues 2023-07-26 22:37:25 -04:00
Mikayla Fischler
1c87ef18a1 #297 added tone packet to valid MGMT packet types 2023-07-26 21:33:43 -04:00
Mikayla Fischler
f111b711c5 #264, #280 send tones to RTUs 2023-07-26 21:02:34 -04:00
Mikayla Fischler
92d1945bea #264 WIP RTU alarm sounders 2023-07-26 20:48:44 -04:00
Mikayla Fischler
4192ea426c #280 moved alarm sounder logic to supervisor and tone control to common 2023-07-26 20:48:11 -04:00
Mikayla
7bd8f34773
update README.md 2023-07-19 20:06:13 -04:00
Mikayla
bdbb3071b3
Merge pull request #294 from MikaylaFischler/devel
2023.07.19 Hotfix
2023-07-19 19:18:48 -04:00
Mikayla Fischler
def02a94d2 #293 fixed race condition with graphics element IDs 2023-07-19 11:27:33 -04:00
Mikayla Fischler
681bb0963e #291 RTU comms thread no longer yields every packet 2023-07-18 22:28:43 -04:00
Mikayla
8f7d7c3ead
Merge pull request #288 from MikaylaFischler/devel
2023.07.17 Hotfix
2023-07-17 22:48:03 -04:00
Mikayla Fischler
c0f45cfb8b updated comments 2023-07-17 22:47:19 -04:00
Mikayla Fischler
455653074a #287 fixed coordinator not notifying supervisor of auto waste config 2023-07-17 22:09:21 -04:00
Mikayla Fischler
1202289fab #285 #286 mitigated false trips 2023-07-17 20:59:45 -04:00
Mikayla
acb7b5b4cb
update README.md with new installer pastebin 2023-07-16 21:29:41 -04:00
Mikayla
9bd79dacad
Merge pull request #281 from MikaylaFischler/devel
2023.07.16 Release
2023-07-16 21:25:00 -04:00
Mikayla Fischler
c544d140bf installer key handling improvements 2023-07-16 21:21:33 -04:00
Mikayla Fischler
353cb3622b improved installer any key detection 2023-07-16 21:11:27 -04:00
Mikayla Fischler
b54f15bad6 #274 bugfixes and optimizations 2023-07-16 21:07:37 -04:00
Mikayla Fischler
4d9783beca fixed installer clean bug 2023-07-16 20:58:34 -04:00
Mikayla Fischler
5529774b0e changed installer press enter to continue to any key, fixed some text colors 2023-07-16 20:53:39 -04:00
Mikayla Fischler
2a541ef3fe #274 cleanup functionality added to installer 2023-07-16 19:42:20 -04:00
Mikayla Fischler
e1b4d72ef8 updated legacy install manifest 2023-07-15 13:33:51 -04:00
Mikayla Fischler
6a0992c7a4 removed unused variable 2023-07-15 13:33:18 -04:00
Mikayla Fischler
cff7c724be #272 fixed bug with transmitting unit dynamic tank table 2023-07-15 13:31:48 -04:00
Mikayla Fischler
47bda73afe #272 basic dynamic tank data in supervisor and coordinator 2023-07-15 13:16:36 -04:00
Mikayla
8daedc109c
added discord info to readme 2023-07-13 13:50:17 -04:00
Mikayla Fischler
a164c18a50 removed unused fields from dynamic tank rtu 2023-07-13 12:19:25 -04:00
Mikayla Fischler
4d663ada8d added high contrast yellow to rtu/plc/coord front panels 2023-07-12 13:38:37 -04:00
Mikayla Fischler
084a153a79 #268 fixed incorrect info print on extra wireless modem connection 2023-07-11 21:06:47 -04:00
Mikayla Fischler
4ed6ec1c63 correctly print new messages without overwrites in dmesg even if a prior message is a progress one 2023-07-11 21:01:24 -04:00
Mikayla Fischler
d3c2ba7bee update install manifest 2023-07-11 20:32:37 -04:00
Mikayla Fischler
55ff9dad4b #249 coordinator handle monitor disconnects/reconnects 2023-07-11 20:32:10 -04:00
Mikayla Fischler
0d6022f5e3 fixed always reporting failure to connect to supervisor even when inaccurate 2023-07-11 18:31:53 -04:00
Mikayla Fischler
8b136d78a8 #268 better handling of wireless modem peripherals 2023-07-11 18:22:09 -04:00
Mikayla Fischler
a5214730ef #260 added dynamic tank RTU 2023-07-11 17:27:03 -04:00
Mikayla Fischler
9f3ad3caf0 removed PLC establish packet handling when already linked 2023-07-11 16:17:24 -04:00
Mikayla
9bb2a99be5
Merge pull request #279 from MikaylaFischler/265-coordinator-front-panel
265 coordinator front panel
2023-07-11 15:37:17 -04:00
Mikayla Fischler
65ace26258 corrected comments 2023-07-11 15:36:41 -04:00
Mikayla Fischler
61d975d13f updated error messages for consistency 2023-07-11 15:15:44 -04:00
Mikayla Fischler
1d7d6e9817 update legacy install manifest 2023-07-11 13:38:21 -04:00
Mikayla Fischler
a2e0999cea combine coordinator supervisor connection event loop with main loop 2023-07-11 13:32:26 -04:00
Mikayla Fischler
1edee7f64b updated graphics comments 2023-07-09 23:42:44 -04:00
Mikayla Fischler
df61ec2c62 #265 coordinator front panel 2023-07-09 23:31:56 -04:00
Mikayla Fischler
bf7a316b04 don't start flasher if already started 2023-07-09 23:24:41 -04:00
Mikayla Fischler
96c4444184 corrected some comments 2023-07-09 23:22:24 -04:00
Mikayla Fischler
59eac62c33 #270 validate reactor PLC status packet types 2023-07-08 18:07:40 -04:00
Mikayla
ab193db153
Merge pull request #277 from MikaylaFischler/25-process-waste-control
25 process waste control
2023-07-08 17:12:23 -04:00
Mikayla Fischler
7d65bba589 fixes/cleanups for pull request 2023-07-08 17:11:51 -04:00
Mikayla Fischler
dcef5a96f0 removed unused function 2023-07-08 16:57:41 -04:00
Mikayla Fischler
ba0900ac65 #25 sna/sps integration, plutonium fallback, waste rate reporting 2023-07-08 16:57:13 -04:00
Mikayla Fischler
8f54e95519 #25 continued WIP waste control, main view updated and unit fields modified 2023-07-06 01:36:06 -04:00
Mikayla Fischler
7b9824b6f9 added checkbox graphics element 2023-07-01 19:40:33 -04:00
Mikayla Fischler
b6835fc7d1 #276 updated readme 2023-06-29 16:54:46 -04:00
Mikayla
bc5a94cd3b
Update README.md 2023-06-29 12:57:25 -04:00
Mikayla
2a3d868402
Merge pull request #273 from MikaylaFischler/devel
2023.06.29 Release
2023-06-29 12:33:12 -04:00
Mikayla Fischler
b998634da1 installer fixes 2023-06-29 12:29:30 -04:00
Mikayla
5225380523
Merge pull request #271 from MikaylaFischler/51-hmac-message-authentication
HMAC Message Authentication
2023-06-27 19:09:38 -04:00
Mikayla Fischler
0e7ea7102c removed extra verbose comment in configs 2023-06-27 19:08:33 -04:00
Mikayla Fischler
8924ba4e99 cleanup and luacheck fixes 2023-06-27 19:05:51 -04:00
Mikayla Fischler
a8071db08e #51 send serialized data to properly MAC 2023-06-27 18:36:16 -04:00
Mikayla Fischler
fb3c7ded06 updated lockbox benchmark 2023-06-26 20:44:55 -04:00
Mikayla Fischler
f6b0a49904 added graphics version to crash dump 2023-06-26 14:03:36 -04:00
Mikayla Fischler
bfbbfb164b #51 include versioned lockbox in installer, reduced installer file size 2023-06-25 17:53:02 -04:00
Mikayla Fischler
57763702ff #51 init mac component from config key 2023-06-25 14:00:18 -04:00
Mikayla Fischler
f469754bb7 #51 network file cleanup 2023-06-25 13:06:03 -04:00
Mikayla Fischler
336662de62 #51 nic integration with rtu and supervisor 2023-06-25 12:59:38 -04:00
Mikayla Fischler
9073009eb0 #51 usage of nic.receive and some cleanup 2023-06-23 14:12:41 -04:00
Mikayla Fischler
ffac6996ed #51 PLC changes for new networking 2023-06-23 13:52:24 -04:00
Mikayla Fischler
da3c92b3bf Merge branch 'devel' into 51-hmac-message-authentication 2023-06-22 16:05:46 -04:00
Mikayla
712c7a8f3b #266 added health check to ppm and strengthened reliability of RTU hw state reporting 2023-06-22 19:46:17 +00:00
Mikayla
737afe586d renamed lockbox benchmark 2023-06-22 14:22:32 +00:00
Mikayla
d69796b607 lockbox benchmark cleanup 2023-06-22 14:21:00 +00:00
Mikayla
1cdf66a8c3 #51 WIP network interface controller 2023-06-21 23:04:39 +00:00
Mikayla
282c7db3eb Merge branch 'devel' into 51-hmac-message-authentication 2023-06-18 19:23:56 +00:00
Mikayla Fischler
a02529b9f7 #263 fixed bug with supervisor group map length not matching number of reactors 2023-06-18 15:19:01 -04:00
Mikayla Fischler
af38025f50 #262 don't ever abort RTU unit parsing on error, just skip 2023-06-18 14:26:38 -04:00
Mikayla Fischler
b28e4d1e95 #258 installer bugfix 2023-06-18 14:04:49 -04:00
Mikayla Fischler
75dfa3ae73 #258 luacheck fix 2023-06-18 13:16:28 -04:00
Mikayla Fischler
4a3455fa60 #258 luacheck fix 2023-06-18 13:13:34 -04:00
Mikayla Fischler
a2fa6570dc #258 installer improvement 2023-06-18 13:12:34 -04:00
Mikayla Fischler
aef8281ad6 #258 more installer fixes 2023-06-18 01:19:00 -04:00
Mikayla Fischler
d42327a20d #258 bugfixes 2023-06-18 01:09:46 -04:00
Mikayla Fischler
49db75f34d #258 installer bugfix 2023-06-18 01:04:40 -04:00
Mikayla Fischler
bc87030491 #258 installer improvements and test change to graphics version 2023-06-18 00:48:06 -04:00
Mikayla Fischler
9266d7d8e1 #258 versioned graphics component 2023-06-18 00:40:01 -04:00
Mikayla
ef5567ad46 #51 hmac verification 2023-06-11 18:26:55 +00:00
Mikayla Fischler
302f3d913f unlikely to use ldoc due to incompatibilities with vscode lua extension luadocs 2023-06-08 11:39:07 -04:00
Mikayla Fischler
650b9c1811 #244 luadoc actions fixes 2023-06-08 10:58:06 -04:00
Mikayla Fischler
543ac8c9fe updated ldoc version 2023-06-08 10:50:39 -04:00
Mikayla Fischler
7f19f76c0b update comment to force re-run 2023-06-08 10:46:35 -04:00
Mikayla
8d76c86309
fixed pages.yml format error 2023-06-08 10:42:49 -04:00
Mikayla Fischler
a4be6a6dde #244 luadoc in github actions 2023-06-08 10:41:44 -04:00
Mikayla Fischler
8b926a0978 #257 tick supervisor version to force installers to re-pull graphics 2023-06-07 21:42:21 -04:00
Mikayla Fischler
775ffc8094 added graphics to supervisor depends 2023-06-07 21:25:42 -04:00
Mikayla
13a8435f6c
Merge pull request #256 from MikaylaFischler/devel
2023.06.07 Hotfix
2023-06-07 18:42:29 -04:00
Mikayla Fischler
5d6dda5619 #255 fixed rectangle element not offsetting mouse events 2023-06-07 18:38:00 -04:00
Mikayla
f8221ad0f1
update README.md with new pastebin link 2023-06-07 18:16:36 -04:00
Mikayla
193aeed6df
Merge pull request #254 from MikaylaFischler/devel
2023.06.07 Release
2023-06-07 17:46:50 -04:00
Mikayla Fischler
8c87cb3e26 actions fixed maybe 2023-06-07 16:07:12 -04:00
Mikayla Fischler
1548cd706d actions test 6 2023-06-07 16:04:39 -04:00
Mikayla Fischler
996272e108 actions test 5 2023-06-07 16:01:04 -04:00
Mikayla Fischler
ef673bdf1b actions test 4 2023-06-07 15:57:07 -04:00
Mikayla Fischler
7aa236e987 actions test 3 2023-06-07 15:55:46 -04:00
Mikayla Fischler
35d857a5f4 actions testing 2 2023-06-07 15:51:54 -04:00
Mikayla Fischler
c2c87ec6c6 github actions testing 2023-06-07 15:45:37 -04:00
Mikayla Fischler
5ce54d78e1 github actions 2023-06-07 15:44:07 -04:00
Mikayla Fischler
c05a312f6c actions testing 2023-06-07 15:43:03 -04:00
Mikayla Fischler
86325d9527 more possible actions fixes 2023-06-07 15:37:48 -04:00
Mikayla Fischler
5074ca89f0 possible actions fix? 2023-06-07 15:35:25 -04:00
Mikayla Fischler
c22b048608 actions fix maybe 2023-06-07 15:28:49 -04:00
Mikayla Fischler
7ae3014e06 updated step conditions 2023-06-07 15:24:44 -04:00
Mikayla Fischler
8fa37cc9be manifest update: don't clean on checkout 2023-06-07 15:17:05 -04:00
Mikayla Fischler
2c730fbdc2 update to manifest generation to skip failed branches 2023-06-07 15:11:42 -04:00
Mikayla Fischler
5c21140025 updated manifest generation to include all data in each go 2023-06-07 15:05:36 -04:00
Mikayla Fischler
7859e5ea4c updated old install manifest 2023-06-07 14:55:56 -04:00
Mikayla
0b5ee8eabc
Merge pull request #252 from MikaylaFischler/225-consolidate-network-channels
225 Consolidate Network Channels
2023-06-07 14:27:10 -04:00
Mikayla Fischler
1decd88415 last few cleanups 2023-06-07 14:22:35 -04:00
Mikayla Fischler
f1b1f0b75a updated supervisor front panel default computer ID place holders and fixed PDG establish using channel in messages 2023-06-07 14:18:13 -04:00
Mikayla Fischler
f37f2f009f comms only set nil max distance if requested max is exactly 0 2023-06-07 14:17:10 -04:00
Mikayla Fischler
15b071378c #225 removed redundant checks on remote address, added clarity to log messages 2023-06-07 12:48:43 -04:00
Mikayla Fischler
5ba06dcdaf #225 log message fixes and sv addr checks for RTU 2023-06-07 12:35:17 -04:00
Mikayla Fischler
f4e7137eb3 #225 pocket verify packets are from linked computer 2023-06-07 12:27:13 -04:00
Mikayla Fischler
cf881548d7 config file comments 2023-06-07 12:25:50 -04:00
Mikayla Fischler
0a6fd35f93 supervisor front panel computer IDs cleanup 2023-06-06 21:56:17 -04:00
Mikayla Fischler
671f8b55bc updated supervisor front panel RTT coloring limits 2023-06-06 19:49:28 -04:00
Mikayla Fischler
e16b0d237e #225 fixed svsessions __tostring for sessions, refactored s_addr to src_addr 2023-06-06 19:45:04 -04:00
Mikayla Fischler
55dab6d675 don't print brackets in util.strval if metatable __tostring is present 2023-06-06 19:41:55 -04:00
Mikayla Fischler
0f5ae9a756 #225 coordinator changes for new comms 2023-06-06 19:41:09 -04:00
Mikayla Fischler
cdff7af431 #225 pocket properly handle disconnects and address validation 2023-06-05 20:59:28 -04:00
Mikayla Fischler
c536b823e7 #225 PLC/RTUs drop incoming packets from devices other than the configured supervisor while linked 2023-06-05 19:12:43 -04:00
Mikayla Fischler
b20d42ff38 #225 added computer IDs to PLC/RTU front panels, updated supervisor front panel to use computer ID terminology 2023-06-05 18:10:53 -04:00
Mikayla Fischler
63147bfab5 #248 fixed network light not going out on PLC/RTU when disconnected 2023-06-05 17:47:43 -04:00
Mikayla Fischler
360609df1f #225 network changes for supervisor sessions 2023-06-05 17:24:00 -04:00
Mikayla Fischler
9a5fc92c86 Merge branch 'devel' into 225-consolidate-network-channels 2023-06-05 01:18:13 -04:00
Mikayla
337fca7e7c #225 work in progress comms changes 2023-06-05 05:13:22 +00:00
Mikayla Fischler
38fc7189ba #245 fixed coordinator missing pocket packets and connection timeouts 2023-06-03 18:51:59 -04:00
Mikayla Fischler
0b939be412 #231 renamed unit a_ functions to auto_ 2023-06-03 17:59:20 -04:00
Mikayla Fischler
351842c9a1 updated RTU to say STATUS instead of POWER on first LED 2023-06-03 17:44:32 -04:00
Mikayla Fischler
8d248408d4 #247 renamed tcallbackdsp to tcd and added handler to RTU 2023-06-03 17:40:57 -04:00
Mikayla
2427561dc5
Merge pull request #246 from MikaylaFischler/front-panels
Supervisor Front Panel
2023-06-03 17:32:37 -04:00
Mikayla Fischler
b4932b33b6 code cleanup 2023-06-03 17:31:06 -04:00
Mikayla Fischler
24a7275543 fixed trailing whitespace 2023-06-03 15:50:44 -04:00
Mikayla Fischler
529371a0fd #184 support supervisor running without front panel, halved heartbeat blink rate 2023-06-03 15:45:48 -04:00
Mikayla Fischler
69df5edbeb #184 RTU and pocket lists on supervisor front panel, element delete() bugfix 2023-06-03 14:33:08 -04:00
Mikayla Fischler
153a83e569 listbox improvements 2023-06-01 13:00:45 -04:00
Mikayla Fischler
ef1ec220a4 #184 initial draft of listbox element and associated supervisor front panel test example 2023-05-31 11:44:41 -04:00
Mikayla Fischler
8f2e9fe319 more manifest.yml fixes 2023-05-31 11:19:32 -04:00
Mikayla Fischler
86ad2a1069 fixed a typo in manifest.yml 2023-05-31 11:17:44 -04:00
Mikayla Fischler
494dc437a5 fixed error in manifest.yml 2023-05-31 11:16:56 -04:00
Mikayla Fischler
deec1ff1df fix shields deploy 2023-05-31 11:15:55 -04:00
Mikayla Fischler
4c35233289 #243 changed rectangle to use content window, significant simplification of offset logic, improved delete rendering 2023-05-30 20:43:33 -04:00
Mikayla Fischler
de9cb3bd3a #243 graphics core updates for content windows, redrawing, and handling of addition/removal of children 2023-05-30 19:51:10 -04:00
Mikayla Fischler
270726e276 Merge branch 'devel' into front-panels 2023-05-30 00:30:08 -04:00
Mikayla Fischler
dbd74afbe6 #242 update per luacheck 2023-05-27 23:53:20 -04:00
Mikayla Fischler
37a91986e5 #242 updated installer for github pages manifest 2023-05-27 23:50:00 -04:00
Mikayla Fischler
a892c0cf41 #242 create manifest.yml 2023-05-27 23:36:32 -04:00
Mikayla Fischler
b7d90872d5 Merge branch 'devel' into front-panels 2023-05-25 17:48:20 -04:00
Mikayla Fischler
82ab85daa5 updated install manifest hotfix 2023.05.23 2023-05-25 17:41:44 -04:00
Mikayla Fischler
f9aa75a105 graphics element hidden on creation option, changed hide/show logic to only hide/show current element 2023-05-25 17:40:16 -04:00
Mikayla Fischler
e313b77abc Merge branch 'devel' into front-panels 2023-05-23 20:25:19 -04:00
Mikayla
a14ffea6f0
Merge pull request #239 from MikaylaFischler/devel
2023.05.23 Hotfix
2023-05-23 19:54:35 -04:00
Mikayla Fischler
43a0ff86d7 #238 bugfix for push button and sidebar in bounds checks 2023-05-23 19:51:48 -04:00
Mikayla Fischler
ece7c0fe9a #184 supervisor graphics updates for new system, added PLC and CRD pages on supervisor front panel 2023-05-23 19:22:22 -04:00
Mikayla
97cee58e5a
Merge pull request #236 from MikaylaFischler/devel
2023.05.22 Release
2023-05-22 10:06:18 -04:00
Mikayla Fischler
4aba79f232 Merge branch 'devel' into front-panels 2023-05-20 09:44:26 -04:00
Mikayla Fischler
b8c81e2e70 Merge branch 'graphics-rearchitect' into devel 2023-05-20 08:38:29 -04:00
Mikayla Fischler
142f2c363a #234 made debug config setting optional, defaults to false 2023-05-19 19:12:27 -04:00
Mikayla
de99169db8
Merge pull request #235 from MikaylaFischler/graphics-rearchitect
Graphics Rearchitect: Part 2
2023-05-19 18:15:11 -04:00
Mikayla Fischler
d5446f970b updated install manifest and removed early ref to listbox 2023-05-19 17:38:55 -04:00
Mikayla Fischler
792cb46ce6 resolved register simplification 2023-05-19 17:38:08 -04:00
Mikayla Fischler
86615b03ff fixed unused variable 2023-05-18 20:42:15 -04:00
Mikayla Fischler
d5fe790c86 #227 move graphics windows 2023-05-18 20:21:23 -04:00
Mikayla Fischler
beda7624f4 #233 fixed mouse enter/exit behavior via simplification 2023-05-18 10:58:42 -04:00
Mikayla Fischler
82e3fa494c #229 pocket changes for UI element register change 2023-05-14 19:13:44 -04:00
Mikayla Fischler
466902371a #229 coordinator changes for UI element register change 2023-05-14 19:13:12 -04:00
Mikayla Fischler
e763af9981 #229 PLC changes for UI element register change 2023-05-13 09:43:42 -04:00
Mikayla Fischler
b2115fd077 #229 element PSIL register/deletion, changes for RTU to use new PSIL register 2023-05-13 08:50:13 -04:00
Mikayla Fischler
36bd2c5e08 enabled debug logs on turbine modbustest 2023-05-12 13:52:42 -04:00
Mikayla Fischler
f6610489c2 #224 fix for RTU unit indexing on supervisor when virtual units were present 2023-05-11 20:54:43 -04:00
Mikayla Fischler
e159dbb850 #184 updated supervisor for new mouse events 2023-05-11 20:06:41 -04:00
Mikayla Fischler
513c72ea79 Merge branch 'devel' into front-panels 2023-05-11 20:02:42 -04:00
Mikayla Fischler
a81fd49604 updated manifest 2023-05-11 20:01:04 -04:00
Mikayla
b430a22f08
Merge pull request #230 from MikaylaFischler/graphics-rearchitect
Graphics Rearchitect Part 1: Mouse Events
2023-05-11 19:59:52 -04:00
Mikayla
a220713385
Merge branch 'devel' into graphics-rearchitect 2023-05-11 19:57:57 -04:00
Mikayla Fischler
fac9a8d104 updated install manifest 2023-05-11 19:56:45 -04:00
Mikayla Fischler
0783c4c01f #226 bugfixes and pocket mouse events 2023-05-11 19:55:02 -04:00
Mikayla Fischler
676dfc8c22 #226 mouse events in coordinator 2023-05-10 20:01:06 -04:00
Mikayla
50c0a4a3eb #222 added debug log enable to configs 2023-05-10 20:57:23 +00:00
Mikayla
032284e90d #224 skip virtual RTU units when parsing advertisements instead of aborting 2023-05-10 20:40:52 +00:00
Mikayla
3a0d677c16 #226 updated PLC/RTU front panels to use new mouse events 2023-05-10 19:21:54 +00:00
Mikayla Fischler
2c2f936232 #226 updated the other controls for new mouse events, added tabbar control 2023-05-10 11:46:06 -04:00
Mikayla Fischler
4ef1915137 #226 multi button updated for new graphics mouse events 2023-05-10 11:08:24 -04:00
Mikayla Fischler
40fa0de7a3 #226 hazard and push buttons updated for new graphics mouse events 2023-05-10 10:56:56 -04:00
Mikayla Fischler
b8a8da1ac4 #226 graphics core changes for mouse events 2023-05-09 20:29:07 -04:00
Mikayla
e26dc905f8 #226 updated mouse events WIP 2023-05-07 01:27:36 +00:00
Mikayla Fischler
c7edd8c487 updated install manifest after luacheck changes 2023-05-05 14:12:35 -04:00
Mikayla Fischler
d3249da102 removed check.yml comment about -a 2023-05-05 14:11:15 -04:00
Mikayla Fischler
0e1f23efe8 fixed luacheck comments 2023-05-05 14:09:50 -04:00
Mikayla Fischler
5a139c2dd6 possible luacheck fixes 2023-05-05 14:07:15 -04:00
Mikayla Fischler
30ba8bdccf luacheck fixes continued 2023-05-05 14:04:28 -04:00
Mikayla Fischler
b2e21cb6d9 luacheck fixes 2023-05-05 14:02:25 -04:00
Mikayla Fischler
8064b33a36 some luacheck fixes 2023-05-05 13:55:14 -04:00
Mikayla Fischler
7e33f22577 luacheck suppression attempt 2023-05-05 13:15:17 -04:00
Mikayla Fischler
464451c378 unused vararg suppression, re-enable unused args luacheck 2023-05-05 13:09:53 -04:00
Mikayla Fischler
0778a442b1 diagnostic suppression 2023-05-05 13:07:48 -04:00
Mikayla Fischler
2c7b98ba42 #184 WIP supervisor front panel 2023-05-05 13:04:13 -04:00
Mikayla Fischler
ff9a18a019 rtu log message cleanup 2023-04-29 23:49:04 -04:00
Mikayla Fischler
81005d3e2c pocket cleanup 2023-04-29 23:48:50 -04:00
Mikayla
d7e2884634
Merge pull request #221 from MikaylaFischler/devel
2023.04.22 Release
2023-04-22 11:03:47 -04:00
Mikayla Fischler
43e708aa0d #219 bugfixes with renderer exit handling 2023-04-21 23:43:28 -04:00
Mikayla
783c4936cc #213 strict sequence verification 2023-04-21 21:10:15 +00:00
Mikayla
c75f08a9f7 added python to devcontainer and recommendations 2023-04-21 18:56:32 +00:00
Mikayla
e1da8b59d3 #219 properly close out GUI on error on pocket and coordinator 2023-04-21 18:53:28 +00:00
Mikayla
706fb5ea74 updated devcontainer and workspace extension recommendations 2023-04-21 13:34:46 +00:00
Mikayla Fischler
419ca2e6ef #220 close ui on crash 2023-04-20 21:19:16 -04:00
Mikayla Fischler
4c8723eb32 #217 close log file on pocket too 2023-04-20 21:01:41 -04:00
Mikayla Fischler
5db517cedc #217 close log files on exit (including crash) 2023-04-20 21:00:10 -04:00
Mikayla Fischler
e9788abde7 #219 fixed PLC renderer crash handling 2023-04-20 20:47:14 -04:00
Mikayla
be077aa1fb
Merge pull request #218 from MikaylaFischler/front-panels
#183 RTU front panel
2023-04-20 20:42:28 -04:00
Mikayla Fischler
d143015cc7 #183 RTU front panel 2023-04-20 20:40:28 -04:00
Mikayla
df45f6c984
Merge pull request #215 from MikaylaFischler/193-pocket-main-application
193 pocket main application
2023-04-19 23:01:39 -04:00
Mikayla
f6fe99a5fd
Merge branch 'devel' into 193-pocket-main-application 2023-04-19 23:01:10 -04:00
Mikayla Fischler
a843c8eb79 fixes and cleanup 2023-04-19 23:00:27 -04:00
Mikayla Fischler
a614b97d02 cleanup to pass checks 2023-04-19 21:26:54 -04:00
Mikayla Fischler
eca303e289 #208 ui cleanup for indicating emergency coolant status 2023-04-19 21:21:19 -04:00
Mikayla Fischler
ccdc31ed87 fixed typo in check workflow 2023-04-19 20:40:09 -04:00
Mikayla Fischler
c49ad63d6a Merge branch 'devel' into 193-pocket-main-application 2023-04-19 20:37:19 -04:00
Mikayla Fischler
7929318096 #201 functional pocket comms with supervisor and coordinator, adjusted some UI element positioning, bugfixes with apisessions and svsessions 2023-04-19 20:35:42 -04:00
Mikayla
2371a75130 #214 log level cleanup 2023-04-19 13:30:17 +00:00
Mikayla
fee54db43e #203 removed log message on failed structure send, lowered some other log levels to debug 2023-04-18 22:01:35 +00:00
Mikayla Fischler
b48c956354 #201 coordinator apisessions for pocket access 2023-04-18 13:55:18 -04:00
Mikayla Fischler
449e393b73 #201 supervisor pocket diagnostics session 2023-04-18 13:49:59 -04:00
Mikayla Fischler
d295c2b3c3 #201 added pocket connecting screens 2023-04-18 13:47:06 -04:00
Mikayla Fischler
438ab55f4f updated lua diagnostics config 2023-04-18 13:46:00 -04:00
Mikayla
46607dd690 #208 indicate emergency coolant control on PLC front panel 2023-04-18 15:28:46 +00:00
Mikayla Fischler
33c570075c supervisor code cleanup 2023-04-17 19:48:03 -04:00
Mikayla Fischler
93776a0421 update luacheck args and copied lua extension configs to workspace 2023-04-17 15:40:30 -04:00
Mikayla
14dc814925 #201 removed redundant close handling 2023-04-17 00:22:47 +00:00
Mikayla
a7ba0e43e8 #201 pocket comms establishes 2023-04-16 23:50:16 +00:00
Mikayla Fischler
e9290540f5 #193 pocket main application core 2023-04-16 15:05:28 -04:00
Mikayla
b35bf98dec update devcontainer with extensions 2023-04-13 14:45:02 +00:00
Mikayla
59512bb0cf
Create devcontainer.json 2023-04-13 10:43:03 -04:00
Mikayla
64449c6674
restore shields action to just main branch 2023-04-13 09:41:56 -04:00
Mikayla
5bcd885f53
shortened shields URLs after adjustment to action 2023-04-13 09:33:44 -04:00
Mikayla
ba70aa31dc
test update of shields.yml 2023-04-13 09:29:16 -04:00
Mikayla Fischler
d9ec3d7825 Merge branch 'devel' into 193-pocket-main-application 2023-04-12 18:06:24 -04:00
Mikayla Fischler
9b9ce7eae1 finally got shields component versions working with github actions 2023-04-12 18:03:48 -04:00
Mikayla Fischler
e2a3252d8a possible fix for actions 10 2023-04-12 17:55:18 -04:00
Mikayla Fischler
c0547fe463 possible fix for actions 9 2023-04-12 17:54:18 -04:00
Mikayla Fischler
36b86a4825 possible fix for actions 8 2023-04-12 17:52:45 -04:00
Mikayla Fischler
37dd52b12b possible fix for actions 7 2023-04-12 17:50:43 -04:00
Mikayla Fischler
6b8b38b8cb possible fix for actions 6 2023-04-12 17:49:08 -04:00
Mikayla Fischler
2b23dac1fe possible fix for actions 4 2023-04-12 17:44:51 -04:00
Mikayla Fischler
76f6cca42d possible fix for actions 3 2023-04-12 17:44:00 -04:00
Mikayla Fischler
ab9e487a2d possible fix for actions 2 2023-04-12 17:41:06 -04:00
Mikayla Fischler
982fded31d possible fix for actions 2023-04-12 17:39:38 -04:00
Mikayla Fischler
a8e0538804 debugging actions 2023-04-12 17:37:16 -04:00
Mikayla Fischler
8c42a05bbd test for sheilds 2023-04-12 17:34:46 -04:00
Mikayla Fischler
60a3fc8c37 Merge branch 'main' into devel 2023-04-12 17:33:16 -04:00
Mikayla
83cc4d3067
pages fix 2023-04-12 17:25:16 -04:00
Mikayla
fb31afc89c
Merge pull request #211 from MikaylaFischler/sheilds-pages
create shields.yml
2023-04-12 17:23:36 -04:00
Mikayla
36c8a9ccfa
Create shields.yml 2023-04-12 17:22:44 -04:00
Mikayla Fischler
f108db9cfc alternate plan for shields 2023-04-12 17:21:39 -04:00
Mikayla Fischler
f48266e27c added subversions to readme 2023-04-12 17:09:53 -04:00
Mikayla Fischler
5c333c2a07 test for adding subversions to shields.io 2023-04-12 17:04:28 -04:00
Mikayla Fischler
df0ee7c4f7 updated shields readme elements 2023-04-12 16:07:15 -04:00
Mikayla
c987d14d8d
added Luacheck GitHub action (#210)
* added shields.io elements
* #209 luacheck action
* #209 cleanup to pass luacheck
* added check statuses to readme
2023-04-12 16:02:29 -04:00
Mikayla Fischler
075a0280ac #193 WIP pocket initial app, sidebar added 2023-04-12 12:40:13 -04:00
Mikayla Fischler
4b1c982292 #209 luacheck action 2023-04-12 12:13:11 -04:00
Mikayla
e276a99cb3
added shields.io elements 2023-04-12 09:51:40 -04:00
Mikayla Fischler
3ae39b2455 #204 replaced util.strwrap implementation with cc.strings.wrap 2023-04-11 23:53:42 -04:00
Mikayla Fischler
fc9d86f23e Merge branch 'latest' 2023-04-09 18:11:20 -04:00
Mikayla Fischler
b325992a0d disabled emergency coolant example config 2023-04-09 18:10:20 -04:00
Mikayla
04d73cdcd3
Merge pull request #199 from MikaylaFischler/latest
2023.04.09 Release
2023-04-09 18:04:01 -04:00
Mikayla Fischler
0c0055d5ae disabled debug logs for release 2023-04-09 18:03:28 -04:00
Mikayla Fischler
4ef73a8580 #147 fixed bug with the fix for startup race condition with RTUs 2023-04-09 14:25:22 -04:00
Mikayla Fischler
fa88392438 someday i'll remember to gen the install manifest with the actual commit 2023-04-09 12:57:42 -04:00
Mikayla Fischler
6e95755db4 rectangle fix and RCS annunciator cleanup 2023-04-09 12:56:18 -04:00
Mikayla Fischler
5b1f304467 updated install manifest 2023-04-09 12:30:05 -04:00
Mikayla Fischler
a2ea6438b5 #190 updated high startup rate warning per wiki 2023-04-09 12:29:29 -04:00
Mikayla
c9b67f68dd
Merge pull request #196 from MikaylaFischler/front-panels
PLC Front Panel
2023-04-08 22:01:09 -04:00
Mikayla Fischler
d624690b6b comment/indentation fixes 2023-04-08 22:00:51 -04:00
Mikayla Fischler
527f3446a1 Merge branch 'devel' of github.com:MikaylaFischler/cc-mek-scada into front-panels 2023-04-08 21:51:34 -04:00
Mikayla Fischler
27a697c27e #182 added scram/reset buttons to PLC front panel 2023-04-08 21:35:44 -04:00
Mikayla Fischler
6bd7dd0271 improved rectangle graphics element feature set 2023-04-08 21:35:16 -04:00
Mikayla Fischler
67872a1053 updated graphics touch events to be mouse events 2023-04-08 21:33:54 -04:00
Mikayla Fischler
4aad591d3a #182 linked up PLC front panel indicators, cleaned up panel display and added flashing to RPS trip 2023-04-08 16:49:54 -04:00
Mikayla Fischler
9bc4f0f7a6 #182 WIP work on PLC PSIL 2023-04-08 00:38:46 -04:00
Mikayla Fischler
d642f28fa9 updated manifest 2023-04-07 08:07:58 -04:00
Mikayla Fischler
ccc0aa18ff #181 independent emergency coolant valve control 2023-04-07 08:05:14 -04:00
Mikayla Fischler
6ea530635f #182 WIP PLC front panel 2023-04-06 22:10:33 -04:00
Mikayla Fischler
c2132ea7eb #147 possible fix for MODBUS failures on server startup 2023-04-06 12:52:25 -04:00
Mikayla Fischler
40b11dbfd3 change generation to use same paths on unix vs windows 2023-04-06 12:49:23 -04:00
Mikayla Fischler
6e1edce8e7 remove unicode
make python on windows happy
2023-04-06 12:48:54 -04:00
Mikayla Fischler
efef4a845f Merge branch 'main' into devel 2023-04-04 15:48:06 -04:00
Mikayla Fischler
91f72ace24 #176 generator trip display on coordinator 2023-04-03 17:18:30 -04:00
Mikayla Fischler
0f735d049e #176 generator trip detection on supervisor 2023-04-02 09:57:57 -04:00
Mikayla
18e4e309a7
Update README.md 2023-03-05 12:47:21 -05:00
Mikayla
55362d4e66
Merge pull request #189 from MikaylaFischler/devel
Beta Release
2023-03-05 12:36:11 -05:00
Mikayla Fischler
8b1e7cb933 added energy bar to turbine overview 2023-03-05 12:35:36 -05:00
Mikayla Fischler
66deabcf5d HIGH CHARGE on induction matrix is now yellow not red 2023-03-05 11:52:03 -05:00
Mikayla Fischler
2a681d1d37 disable debug prints and update ccmsi version for release 2023-03-04 23:01:24 -05:00
Mikayla Fischler
9eddab2c23 #188 refactored RPS dmg_high to high_dmg 2023-03-04 22:32:13 -05:00
Mikayla Fischler
83dc1064f7 #188 refactored RPS no_cool to low_cool 2023-03-04 22:19:53 -05:00
Mikayla Fischler
c9f1bddb36 #188 refactored RPS dmg_crit to dmg_high 2023-03-04 21:55:40 -05:00
Mikayla Fischler
85a9532962 #186 fixed incorrect constant usage, add RCS flow low to flow stability holdoff when not using a boiler 2023-03-04 21:35:54 -05:00
Mikayla Fischler
edb5d8b96f #186 different steam feed mismatch and RCS flow low tolerances for water vs sodium cooling 2023-03-04 21:19:35 -05:00
Mikayla Fischler
0279ecdec9 #186 second attempt at improving damage status text 2023-03-04 19:49:56 -05:00
Mikayla Fischler
9a500d53d8 #187 installer bugfix 2023-03-04 14:40:49 -05:00
Mikayla Fischler
5b7a11d157 #187 installer bugfix 2023-03-04 14:38:42 -05:00
Mikayla Fischler
57ccb73efe #187 installer bugfix 2023-03-04 14:21:42 -05:00
Mikayla Fischler
d494abe8af #187 improved installer version check 2023-03-04 14:19:17 -05:00
Mikayla Fischler
2e9f52dc89 #187 added installer version to manifest 2023-03-04 14:13:47 -05:00
Mikayla Fischler
8c236eca85 #186 fixed bug with facility update returning, improved damage status message 2023-03-04 13:38:41 -05:00
Mikayla Fischler
be8e8d767c #186 fixed ccmsi insufficient space update overwriting config 2023-03-04 12:49:05 -05:00
Mikayla Fischler
94fb02a46b #186 fixed ccmsi install/update insufficient space confirm 2023-03-04 12:44:37 -05:00
Mikayla Fischler
3586d335a6 #186 don't includes assigned monitors in list of monitors to assign 2023-03-04 12:27:38 -05:00
Mikayla Fischler
f7828dd05b #186 F_ALARM use emergency+ level 2023-03-04 11:46:59 -05:00
Mikayla Fischler
d01a6d548f #186 fixed radiation warning condition 2023-03-04 11:40:06 -05:00
Mikayla Fischler
b12f3206e2 #186 additional messages for radiation alarm/warning with added urgency/level-specific messages 2023-03-04 02:05:36 -05:00
Mikayla Fischler
0e5113918c #186 improved sv config validation, changed waste high thresholds, fixed monitored max burn not showing as active, fixed redstone R_ENABLE and U_ALARM, changed RPS high waste trip to 95% 2023-03-04 01:37:15 -05:00
Mikayla Fischler
11115633cf #186 fixed manifest size in install_manifest.json, fixed unit display not connected prompt, added message about bad cooling config 2023-03-02 22:29:50 -05:00
Mikayla Fischler
58cf383c91 #185 disable auto mode changing if auto mode is active regardless of assignment 2023-03-01 22:37:28 -05:00
Mikayla Fischler
3f15ae6b6f #179 remove recolor option from coordinator config 2023-02-27 23:59:46 -05:00
Mikayla Fischler
0d7fde635d updated readme 2023-02-27 23:52:18 -05:00
Mikayla Fischler
ae3315e4a0 #180 include manifest size in sizes 2023-02-27 23:51:26 -05:00
Mikayla Fischler
523d478739 changed trip time warning to 750ms 2023-02-26 14:49:16 -05:00
Mikayla Fischler
2b8f71fc43 status message cleanup and some updated comments 2023-02-26 14:22:25 -05:00
Mikayla Fischler
b150072234 #177 correctly set Water Level Low 2023-02-26 14:17:35 -05:00
Mikayla Fischler
fbb992ff12 #173 dump excess steam on opening emergency coolant 2023-02-25 14:11:40 -05:00
Mikayla Fischler
523ac91c3b fixed coordinator RCS annunciator dimensions 2023-02-25 13:25:23 -05:00
Mikayla Fischler
bd1625c42e #166 removed sounder test code from GUI 2023-02-25 12:51:37 -05:00
Mikayla Fischler
7508acb1a7 #174 fixed sounder not resuming on supervisor reconnect with same alarm states 2023-02-25 12:20:03 -05:00
Mikayla
6eee0d0c72
Merge pull request #175 from MikaylaFischler/118-code-cleanup-pass
#118 Code Cleanup
2023-02-25 12:08:06 -05:00
Mikayla Fischler
446fff04da #118 PLC RPS fuel check fixed 2023-02-25 12:07:25 -05:00
Mikayla Fischler
4f285cf2b5 #118 safety/constants common file 2023-02-25 02:25:35 -05:00
Mikayla Fischler
16d6372d7b #118 bugfixes with cleanup 2023-02-24 23:59:39 -05:00
Mikayla Fischler
b7895080cb #118 supervisor cleanup 2023-02-24 23:36:16 -05:00
Mikayla Fischler
38ac552613 #118 graphics cleanup 2023-02-24 19:50:01 -05:00
Mikayla Fischler
225ed7baa1 #118 removed some coordinator nodiscard tags 2023-02-22 23:20:59 -05:00
Mikayla Fischler
4340518ecf #118 coordinator code cleanup 2023-02-22 23:09:47 -05:00
Mikayla Fischler
79494f0587 #118 RTU/PLC code cleanup 2023-02-21 23:50:43 -05:00
Mikayla Fischler
ce0198f389 #118 PLC code cleanup 2023-02-21 16:57:33 -05:00
Mikayla Fischler
82ea35168b #118 type cleanup 2023-02-21 12:40:34 -05:00
Mikayla Fischler
424097973d #118 refactored RTU unit types 2023-02-21 12:27:16 -05:00
Mikayla Fischler
7247d8a828 #118 refactored fluid 2023-02-21 11:32:56 -05:00
Mikayla Fischler
a07086907e #118 refactored DUMPING_MODE 2023-02-21 11:30:49 -05:00
Mikayla Fischler
7c64a66dd3 #118 refactored rps_status_t 2023-02-21 11:29:04 -05:00
Mikayla Fischler
6e0dde3f30 #118 refactoring of comms types 2023-02-21 11:05:57 -05:00
Mikayla Fischler
34cac6a8b8 #118 cleanup started of scada-common 2023-02-21 10:31:05 -05:00
Mikayla Fischler
e2d2a0f1dc #172 fixed bug with full builds not being sent 2023-02-20 14:50:20 -05:00
Mikayla Fischler
8df67245c5 #171 unit auto SCRAM and improvements to emergency coolant control 2023-02-20 12:08:51 -05:00
Mikayla Fischler
1be57aaf13 #140 partial build packet updates 2023-02-20 00:49:37 -05:00
Mikayla Fischler
c4f6c1b289 #159 fixed RTU facility level redstone linking 2023-02-19 22:41:32 -05:00
Mikayla Fischler
c9526ba601 #117 installer v0.9e fixed missing newlines on reinstalling message 2023-02-19 21:55:32 -05:00
Mikayla Fischler
00263b2feb #117 installer v0.9d prevent updating when installation isn't present 2023-02-19 21:52:43 -05:00
Mikayla Fischler
d74a2db8e9 #117 installer v0.9c fixes to check list 2023-02-19 20:45:48 -05:00
Mikayla Fischler
632e96c8b3 #117 installer v0.9b cleanup and improvements to check list 2023-02-19 20:43:39 -05:00
Mikayla Fischler
279a40e335 #117 installer v0.9a added support for different targets 2023-02-19 20:17:03 -05:00
Mikayla Fischler
e6632c3bd9 #117 installer v0.8b fixed to folder deletion, check command, and preserving comms version in manifest 2023-02-19 19:56:12 -05:00
Mikayla Fischler
950ad2931f #117 installer v0.8a fixes to deletion of directories and check command 2023-02-19 19:41:32 -05:00
Mikayla Fischler
bc38a9ea27 #117 installer v0.8 fixed purge, added check 2023-02-19 19:30:03 -05:00
Mikayla Fischler
960c016f4c #117 installer v0.7 fixed bug with checking local manifest 2023-02-19 19:18:06 -05:00
Mikayla Fischler
fa6524d934 #117 installer v0.6 saving manifest after operation, checking install state before proceeding 2023-02-19 19:14:47 -05:00
Mikayla Fischler
726d15b48f #117 installer v0.5 fixed colors and move not working 2023-02-19 18:52:21 -05:00
Mikayla Fischler
df57e1859e #117 installer v0.4 with package version checking for skips, fixes to file overwriting 2023-02-19 18:49:04 -05:00
Mikayla Fischler
0493f572a2 #117 installer v0.3 with colors and fixes 2023-02-19 17:15:26 -05:00
Mikayla Fischler
1dea5b1b7a #117 removed util dependency from installer, whoops 2023-02-19 12:56:53 -05:00
Mikayla Fischler
72eb2432cc #117 installation files, first pass 2023-02-19 12:54:02 -05:00
Mikayla Fischler
052b2f3848 improved RCS flow low detection 2023-02-19 12:37:07 -05:00
Mikayla Fischler
35dfd61df1 #169 startup rate high; also changed how clearing ASCRAM status and updating indicators works 2023-02-19 12:20:16 -05:00
Mikayla Fischler
cc5ea0dbb0 #159 linked up redstone I/O 2023-02-19 00:14:27 -05:00
Mikayla Fischler
caa6cc81b1 #163 changed formed/faulted display priority on coordinator for RTUs 2023-02-18 17:37:28 -05:00
Mikayla Fischler
9f95801bfc moved supervisor unit/facility files out of sessions folder 2023-02-18 17:36:44 -05:00
Mikayla Fischler
c18e7ef4d0 display turbine generation as rate instead of charge 2023-02-16 20:48:40 -05:00
Mikayla Fischler
5e65ca636e #164 reporting comms version mismatches 2023-02-15 19:59:58 -05:00
Mikayla Fischler
2babd67198 #162 #168 status indicator for emergency coolant, display number of connected RTUs, added RCS hardware fault and radiation warning indicators 2023-02-15 19:52:28 -05:00
Mikayla Fischler
199ce53f52 #160 #161 linked up ASCRAM lights and added ASCRAM radiation condition 2023-02-14 22:55:40 -05:00
Mikayla Fischler
8ebdf2686b #118 created constructors for basic types 2023-02-14 15:15:34 -05:00
Mikayla Fischler
9d5a55bf58 fixed the commit just now that broke status data to coordinator 2023-02-13 22:14:47 -05:00
Mikayla Fischler
655213e174 updated license 2023-02-13 22:11:45 -05:00
Mikayla Fischler
1fe2acb5c5 #144 added radiation monitor integration; displays, unit alarms, connection states, other bugfixes 2023-02-13 22:11:31 -05:00
Mikayla Fischler
ef27da8daf fixed incorrect text for boiler status on coordinator 2023-02-13 18:53:24 -05:00
Mikayla Fischler
5751c320b1 only report not formed if its a multiblock 2023-02-13 18:53:00 -05:00
Mikayla Fischler
2affe1b31c #139 emergency coolant enabled on RPS low coolant 2023-02-13 18:20:48 -05:00
Mikayla Fischler
ccd9f4b6cc #158 fixed race conditions and cleaned up ascram logic 2023-02-13 18:08:32 -05:00
Mikayla Fischler
fdf75350c0 #146 increased minimum timeout 2023-02-13 12:29:59 -05:00
Mikayla Fischler
9784b4e165 #146 increased timeout times and added to config files 2023-02-13 12:27:22 -05:00
Mikayla Fischler
4d40d08a7a #157 fixed bug with RTU remount messages 2023-02-12 13:06:44 -05:00
Mikayla Fischler
42ff61a8a1 #155 gen rate mode pausing on units no longer being ready 2023-02-11 14:27:29 -05:00
Mikayla Fischler
ff1bd02739 #20 process target charge level 2023-02-11 00:21:00 -05:00
Mikayla Fischler
da9eead2d5 #19 #156 gain changes for generation rate control, fixed plc ready checks 2023-02-10 20:26:25 -05:00
Mikayla Fischler
44d5cec1f8 #19 decent rate PID gains, fixed blade counting and added checks, bugfix with PLC reconnects not being in auto mode, logging cleanups 2023-02-09 22:52:10 -05:00
Mikayla Fischler
37f7319494 #154 increased auto burn rate precision 2023-02-08 20:26:13 -05:00
Mikayla Fischler
ee739c214d #19 gen rate target process control working, some tweaks will be needed as I term is unstable due to limiting decimal precision 2023-02-07 23:47:58 -05:00
Mikayla Fischler
07ee792163 #153 facility alarm acknowledge button 2023-02-07 18:44:34 -05:00
Mikayla Fischler
678dafa62f #152 supervisor cleanups and improvements to alarms 2023-02-07 17:51:55 -05:00
Mikayla Fischler
6c09772a74 #76 added trusted connection ranges for modem messages 2023-02-07 17:31:22 -05:00
Mikayla Fischler
1d3a1672c8 #102 #21 auto control loop with induction matrix and unit alarm checks and handling 2023-02-07 00:32:50 -05:00
Mikayla Fischler
1100051585 #151 improved RCS alarm behavior 2023-02-05 13:04:42 -05:00
Mikayla Fischler
c77993d3a0 bottom align process control panel and induction matrix view 2023-02-05 12:15:41 -05:00
Mikayla Fischler
3e74d6c998 #101 initial coordinator control interface completed 2023-02-05 02:07:54 -05:00
Mikayla Fischler
b5c70b0d37 fixed process controller assuming ramp complete if burn rate setpoint was identical to setpoint before process control start 2023-02-04 13:47:00 -05:00
Mikayla Fischler
ba8bfb6e14 #101 fixed averages and display them 2023-02-03 21:05:21 -05:00
Mikayla Fischler
a117d5ee97 #150 save and automatically set priority groups, added checks to set waste and set group commands, restore waste mode control if operation failed 2023-02-03 16:40:58 -05:00
Mikayla Fischler
72791d042b #149 validate display sizes on startup 2023-02-03 15:19:00 -05:00
Mikayla Fischler
53e4576547 some coordinator code cleanup and refactoring 2023-02-02 23:07:09 -05:00
Mikayla Fischler
2e78aa895d #101 #102 burn rate process mode functional 2023-02-02 22:58:51 -05:00
Mikayla Fischler
eb8aab175f #148 okay turns out that variable was important, ramping now works as intended, correctly 2023-02-02 22:51:21 -05:00
Mikayla Fischler
5721231ffd #148 fixed burn rate ramping again for real this time 2023-02-02 22:04:26 -05:00
Mikayla Fischler
846f9685ad #148 fixed burn rate ramping, adjusted auto burn rate ramping 2023-02-02 20:17:23 -05:00
Mikayla Fischler
fe71615c12 #101 #102 work on bugfixes; disable unit controls while in auto mode 2023-02-01 21:55:02 -05:00
Mikayla Fischler
e9562a140c #143 #103 #101 #102 work in progress auto control, added coordinator controls, save/auto load configuration, auto enable/disable on reactor PLC for auto control (untested) 2023-01-26 18:26:26 -05:00
Mikayla Fischler
e808ee2be0 #137 save/recall waste configuration with config file 2023-01-23 20:47:45 -05:00
Mikayla Fischler
8abac3fdcb refactoring and adjusted spinbox and hazard button elements 2023-01-23 15:10:41 -05:00
Mikayla Fischler
4145949ba7 #141 setting unit limits with coordinator 2023-01-15 13:11:46 -05:00
Mikayla Fischler
b7d4bc3a5b #142 fixed bug with setting burn rates 2023-01-13 14:03:47 -05:00
Mikayla Fischler
a1c1125d54 fixed bug with automatic limit update 2023-01-03 17:03:20 -05:00
Mikayla Fischler
41838ee340 #102 #20 #19 #21 work in progress on auto control, added control loop, started auto scram checks, implemented limiting and balancing, re-organized for priority groups 2023-01-03 16:50:31 -05:00
Mikayla Fischler
6fe257d1d7 #138 fixed bug with dmesg output resetting to default if log file is recycled 2022-12-18 14:11:25 -05:00
Mikayla Fischler
ca2983506e #24 coordinator/supervisor setting process groups and unit burn rate limits 2022-12-18 13:56:04 -05:00
Mikayla Fischler
93a0dedcb1 #24 GUI for unit displays to set unit group 2022-12-13 15:18:29 -05:00
Mikayla Fischler
a591cab338 color reactor coolant bars based on coolant type 2022-12-11 10:51:45 -05:00
Mikayla Fischler
a633f5b4c3 #132 expanded unit displays to use 4x4 monitors 2022-12-10 23:56:07 -05:00
Mikayla Fischler
6517f78c1c #129 induction matrix view 2022-12-10 15:44:11 -05:00
Mikayla Fischler
03f0216d51 #130 facility data object, some code cleanup, comms protocol changed from 1.0.1 to 1.1.0 2022-12-10 13:58:17 -05:00
Mikayla Fischler
41913441d5 RTU support for non reactor specific devices 2022-12-07 23:17:11 -05:00
Mikayla Fischler
2a99d1d385 #136 send rps trip cause with status, moved rps is_tripped to rps status from main status, increased plc status send rate to 2 Hz 2022-12-07 12:59:21 -05:00
Mikayla Fischler
52603e3579 #131 first pass of unit status text 2022-12-06 23:39:35 -05:00
Mikayla Fischler
c23ddaf5ea #135 added clock and supervisor trip time to coordinator main view 2022-12-06 11:40:13 -05:00
Mikayla Fischler
6bdde02268 #131 start of unit status text, added updating coordinator waste processing option on reconnect 2022-12-05 16:17:09 -05:00
Mikayla Fischler
5224dcbd25 reconnect alarm sounder speaker on peripheral reconnect 2022-12-04 14:36:29 -05:00
Mikayla Fischler
9475700930 added sounder volume to config 2022-12-04 14:29:39 -05:00
Mikayla Fischler
4030fdc5c9 #77 alarm sounder 2022-12-04 13:59:10 -05:00
Mikayla Fischler
518ee8272a updated modbustest 2022-11-30 23:32:29 -05:00
Mikayla Fischler
e1d7c7b1c0 #134 #104 redstone RTU integration with supervisor unit, waste routing implemented, changed how redstone I/O works (again, should be good now), modbus fixes 2022-11-30 23:31:14 -05:00
Mikayla Fischler
9c27ac7ae6 bugfix with reset/ack button mappings on coordinator GUI 2022-11-27 22:53:44 -05:00
Mikayla Fischler
afb3b0957e bugfix for RTU re-formed detection 2022-11-27 22:44:47 -05:00
Mikayla Fischler
d4ae18eee7 #10 #133 alarm system logic and display, change to comms to support alarm actions, get_x get_y to graphics elements, bugfixes to coord establish and rtu establish, flashing trilight and alarm light indicators 2022-11-26 16:18:31 -05:00
Mikayla Fischler
f68c38ccee cleanup of requires 2022-11-24 22:49:35 -05:00
Mikayla Fischler
5628df56a2 removed hardcoded push button padding 2022-11-24 14:20:11 -05:00
Mikayla Fischler
3685e25713 likely finalized color palette, removed color map from unit displays 2022-11-21 21:32:45 -05:00
Mikayla Fischler
657cd15c59 #127 uncommitted changes for annunciator changes 2022-11-17 12:04:30 -05:00
Mikayla Fischler
29793ba7c4 #128 element changes and show number after setting min/max for spinbox 2022-11-17 12:00:00 -05:00
Mikayla Fischler
9c32074b56 #128 limit max burn rate control to actual max burn rate 2022-11-17 11:58:14 -05:00
Mikayla Fischler
c93a386e74 #127 adjusted annunciator rate/feed checks 2022-11-17 11:20:53 -05:00
Mikayla Fischler
6fcd18e17a #125 moved environmental loss on boilers from build to state category 2022-11-14 21:50:32 -05:00
Mikayla Fischler
7c39e8c72b #126 fixed RTU builds not being sent to coordinator at the correct times 2022-11-14 21:43:02 -05:00
Mikayla Fischler
9761228b8e #124 debug stack trace on error 2022-11-13 15:56:27 -05:00
Mikayla Fischler
e679b5a25a #122 versioned comms protocol with unified establish protocol 2022-11-13 14:13:30 -05:00
Mikayla Fischler
1a01bec7e4 #123 RTU startup without devices, fixed repeat RTU advert handling, added PPM virtual devices, fixed log out of space detection, updated RTU type conversion functions in comms 2022-11-12 01:35:31 -05:00
Mikayla Fischler
f940c136bf fixes to rtu modbus 2022-11-11 23:49:45 -05:00
Mikayla Fischler
8e28dbf2a6 #120 fixed steam dump indicator, fixed index tags 2022-11-11 16:59:28 -05:00
Mikayla Fischler
8b65bf4852 fixed rps alarm packet length check 2022-11-11 16:46:38 -05:00
Mikayla Fischler
ffeff86507 adjusted containment integrity to just be damage percent, moved up radiation indicator 2022-11-11 16:32:14 -05:00
Mikayla Fischler
af57c3b1fc automatic reactor scram functionality for future use 2022-11-11 16:15:44 -05:00
Mikayla Fischler
c221ffa129 #81 handle force disabled 2022-11-11 15:45:46 -05:00
Mikayla Fischler
83cf645da4 #107, #121 RTU build changes, formed handling 2022-11-11 14:59:53 -05:00
Mikayla Fischler
bc63a06b09 someone had PFE in an induction matrix so now i've gotta support some bigger numbers in the power format 2022-11-10 12:00:23 -05:00
Mikayla Fischler
806b217d58 #100 interactive reactor controls (start, scram, reset) 2022-11-06 18:41:52 -05:00
Mikayla Fischler
aaab34f1a8 #115, #116 multiple bugfixes with reactor PLC code 2022-11-05 12:44:40 -04:00
Mikayla
2851331fda
Update issue templates 2022-11-04 13:07:00 -04:00
Mikayla Fischler
1828920873 #110, #114 no longer use mekanism energy helper functions as those are event consuming 2022-11-02 17:00:33 -04:00
Mikayla Fischler
c620310e51 #113 power formatting on turbine energy in main overview 2022-11-02 14:47:18 -04:00
Mikayla Fischler
54264f5149 #111 support unformed reactors 2022-11-02 13:45:52 -04:00
Mikayla Fischler
d87dfb9ebd #112 fixed bug with flasher 2022-11-02 12:02:52 -04:00
Mikayla Fischler
004c960e4d #106 fixes to reactor isFormed support 2022-10-25 23:45:59 -04:00
Mikayla Fischler
57bac57e3f adjusted TCD unserviced call delay 2022-10-25 13:30:41 -04:00
Mikayla Fischler
b2be3ef5fc #106 reactor formed support and remounting 2022-10-25 13:29:57 -04:00
Mikayla Fischler
a02fb6f691 #110 periodically call unserviced TCD callbacks 2022-10-23 12:21:17 -04:00
Mikayla Fischler
307bf6e2c8 added util timer functions, tweaks to flasher and some debug prints for #110 2022-10-23 01:41:02 -04:00
Mikayla Fischler
d202a49011 #108 resolved TCD race condition 2022-10-21 15:15:56 -04:00
Mikayla Fischler
93286174d4 some sneaky semicolons 2022-10-20 13:59:35 -04:00
Mikayla Fischler
788fae44aa #105 single coordinator configuration 2022-10-20 13:53:39 -04:00
Mikayla Fischler
2f55ad76f2 round burn rate to prevent weird floating point issues, added debug prints 2022-10-20 13:27:33 -04:00
Mikayla Fischler
1bf8fe557c flasher callback now private function 2022-10-20 12:23:00 -04:00
Mikayla Fischler
6d5af98310 graphics element enable/disable, click indication on hazard buttons 2022-10-20 12:22:45 -04:00
Mikayla Fischler
ab757e14a7 #100 work in progress on command acks for reactive buttons 2022-10-20 12:22:03 -04:00
Mikayla Fischler
bfa87815fa #90 flashing GUI indicator lights 2022-10-12 16:37:11 -04:00
Mikayla Fischler
77dc7ec0c9 fixed rps reset infinte retry, improved time delta calculations, added last_update to rtu device databases 2022-10-07 11:43:18 -04:00
Mikayla Fischler
5dfbe650c6 #93 don't send out-of-range burn rates (won't get a good ack), fixed unit command packet ordering 2022-10-07 11:28:56 -04:00
Mikayla Fischler
529951f998 automatically show current burn rate in burn rate spinbox 2022-10-07 11:21:17 -04:00
Mikayla Fischler
573c263548 same ppm fault check as with scram for enabling an enabled reactor 2022-10-07 10:29:25 -04:00
Mikayla Fischler
d4da6a7f3a fixed up types/names for hazard button 2022-10-07 10:28:46 -04:00
Mikayla Fischler
9d60777223 #93 added reset RPS command to iocontrol/gui 2022-10-07 10:19:37 -04:00
Mikayla Fischler
62ac993dae #93, #94, unit commands and range/type checks on unit IDs on PLC/RTU connections 2022-10-06 13:54:52 -04:00
Mikayla Fischler
c02479b52e #99 updating/sending builds 2022-10-02 21:17:13 -04:00
Mikayla Fischler
1b553ad495 #83 additional reactor structure fields, bugfix to rps alarm on sv, removed spam-prone rps error messages 2022-09-30 17:33:35 -04:00
Mikayla Fischler
7a90ea7e4e #87 check if the reactor is active on startup/reconnect before scram'ing, rps now ignores scram errors if the error is due to the reactor being inactive 2022-09-29 11:02:03 -04:00
Mikayla Fischler
4f7775ccb6 check for table type before checking length, added power conversion/formatting helpers 2022-09-22 21:31:07 -04:00
Mikayla Fischler
50be7f9ca2 #97 fixed issue where traffic on other channels gets processed if channels are left open 2022-09-22 20:42:06 -04:00
Mikayla Fischler
a87e557d2d updated readme, removed #29 from known issues due to updating to requiring 10.1+ 2022-09-21 17:30:20 -04:00
Mikayla Fischler
36557fc345 code cleanup, type hints, bugfixes, and #98 removal of support for mek 10.0 RTU peripherals 2022-09-21 15:53:51 -04:00
Mikayla Fischler
d0d20b1299 #95 added boiler/turbine RTUs to supervisor, tons of RTU/MODBUS related bugfixes, adjusted annunciator conditions 2022-09-18 22:25:59 -04:00
Mikayla Fischler
88c34d8bca fixed acknowledge packets to use error flag, fixed 'static'-like function scope of modbus functions 2022-09-18 22:02:17 -04:00
Mikayla Fischler
3267e7ff13 #96 RTU starts unlinked now on main thread start 2022-09-17 17:04:57 -04:00
Mikayla Fischler
6686d8ea62 changed reactor status message text on main view 2022-09-13 16:08:11 -04:00
Mikayla Fischler
c47e0044b1 addresed monitor disconnect to-do, changed monitor requirement to minimum, fixed up connect/reconnect for #92 2022-09-13 16:07:21 -04:00
Mikayla Fischler
265368f9b2 fixed integrity % and changed to actual burn rate on main screen 2022-09-12 16:01:18 -04:00
Mikayla Fischler
70d9da847e graphics elements comments 2022-09-12 15:58:43 -04:00
Mikayla Fischler
1bf21564f9 #91 recoloring of horizontal and vertical bar indicators 2022-09-12 14:43:01 -04:00
Mikayla Fischler
cd6bb7376d #91 adjusted resizing logic for core map 2022-09-12 14:38:48 -04:00
Mikayla Fischler
e0ab2ade89 #91 support resizing core map per reactor dimension updates 2022-09-12 13:53:39 -04:00
Mikayla Fischler
10c53ac4b3 #91 get and set values for all controls/indicators and textbox 2022-09-12 12:59:28 -04:00
Mikayla Fischler
d9be5ccb47 #89 fixed up ui closing to be cleaner on restart 2022-09-10 22:08:29 -04:00
Mikayla Fischler
c14fc048a1 #88 not going to actually hold UI since that hides the PLC offline state and other offline indicators, instead should expose property update capability 2022-09-10 15:26:52 -04:00
Mikayla Fischler
4275c9d408 unit detail view in div and hide waiting indicator on connect 2022-09-10 15:15:24 -04:00
Mikayla Fischler
98c826e762 start/stop animations with show/hide and pass show/hide down children 2022-09-10 15:14:48 -04:00
Mikayla Fischler
dcf275784c removed debug print 2022-09-10 10:43:48 -04:00
Mikayla Fischler
6f3405949d #88 hold on rendering unit detail view until we get a status, added waiting animation 2022-09-10 10:42:56 -04:00
Mikayla Fischler
33695b2ed6 #74 #86 removed redundant overridden field (use rps_tripped) 2022-09-08 14:49:01 -04:00
Mikayla Fischler
350370a084 notify subscriber right away if there is already a value present 2022-09-08 12:19:19 -04:00
Mikayla Fischler
17954ef3d0 #86 supervisor fixes and changes for annunciator/units; send annunciator, fixed heartbeat, change to max return flow detection 2022-09-08 10:25:00 -04:00
Mikayla Fischler
c5ba95449f bugfix to trilight, change to test code in unit view 2022-09-08 10:22:11 -04:00
Mikayla Fischler
3621f53c45 #78 linked up the rest of the fields that we currently have, holding off on a few that are still WIP features 2022-09-07 11:10:20 -04:00
Mikayla Fischler
e084ae1eea removed redundant c_off from trilight 2022-09-07 10:42:12 -04:00
Mikayla Fischler
49605e5966 added tri-state indicator light 2022-09-07 10:39:51 -04:00
Mikayla Fischler
0f6b3fdd98 fixed incorrect comment 2022-09-07 10:25:48 -04:00
Mikayla Fischler
c2ac7fc973 #78 removed redundant device index from boiler/turbine ps keys 2022-09-07 10:25:22 -04:00
Mikayla Fischler
b53d2d6694 code cleanup and work on #78 linking for annunciator 2022-09-06 22:38:27 -04:00
Mikayla Fischler
117784500a #78 functional reactor stats on main view 2022-09-05 19:40:20 -04:00
Mikayla Fischler
397e311f1b #85 handling supervisor disconnected, bugfix with renderer 2022-09-05 16:24:57 -04:00
Mikayla Fischler
e456d34468 svsessions bugfixes 2022-09-05 16:23:03 -04:00
Mikayla Fischler
4359cc3e63 formatting 2022-09-05 16:21:59 -04:00
Mikayla Fischler
473763fd27 #78 removed use of data in graphics layouts since we don't have data at construct time 2022-09-05 16:04:32 -04:00
Mikayla Fischler
621adbbcbc #86 type bug fix 2022-09-05 11:49:23 -04:00
Mikayla Fischler
564b89d19c #78 linked up unit overview using psil 2022-09-03 13:10:51 -04:00
Mikayla Fischler
17fce01ff5 added rps_trip_cause type 2022-09-03 13:10:09 -04:00
Mikayla Fischler
f36b0c7e37 #85 version for reconnecting 2022-09-03 11:54:34 -04:00
Mikayla Fischler
5a8bba5108 #85 handle loss of supervisor conn or comms modem 2022-09-03 11:51:27 -04:00
Mikayla Fischler
c3f7407689 #86 work on supervisor/coordinator comms 2022-09-03 10:50:14 -04:00
Mikayla Fischler
d38e5ca5ec #86 send builds and statuses periodically 2022-08-28 12:57:36 -04:00
Mikayla Fischler
eadf5c488a #86 improvements to supervisor units, code cleanup 2022-08-28 12:12:30 -04:00
Mikayla Fischler
c985e90ec3 #73 test unit view completed, additional features held for after data integration is set 2022-08-28 11:52:43 -04:00
Mikayla Fischler
c80d861b28 #73 unit view reorganization 2022-08-16 13:56:42 -04:00
Mikayla Fischler
395c1ff9ce #73 add indicators for radiation monitor and boilers/turbines 2022-08-16 13:04:02 -04:00
Mikayla Fischler
8dac59fba4 #73 waste selection 2022-08-16 11:22:58 -04:00
Mikayla Fischler
7f011369c4 util pad function 2022-08-16 11:22:06 -04:00
Mikayla Fischler
3c2f631451 #73 additional indicators next to core map 2022-08-09 00:40:50 -04:00
Mikayla Fischler
02c3c5c53c fixed bug with textbox alignment 2022-08-09 00:40:02 -04:00
Mikayla Fischler
252c48a02c #73 core map changes 2022-08-02 11:46:21 -03:00
Mikayla Fischler
6b23a32744 renamed core_view to core_map 2022-08-01 13:11:20 -03:00
Mikayla Fischler
826114e5bf #73 core map and bugfixes 2022-08-01 13:05:39 -03:00
Mikayla Fischler
17dd35e6de bugfixes to tiling element 2022-08-01 10:30:53 -03:00
Mikayla Fischler
42c2b1bda1 coordinator use tcallbackdsp, #73 burn rate set button click effect, test blinks of lights 2022-07-28 12:10:52 -04:00
Mikayla Fischler
f5c703a8b3 fixed push button touch redraw 2022-07-28 11:42:22 -04:00
Mikayla Fischler
2918608326 #73 updated unit layout for graphics library changes 2022-07-28 11:17:58 -04:00
Mikayla Fischler
14b24678f9 #84 auto-incrementing x with line break function, removed need for get_offset by having parent prepare child template 2022-07-28 11:17:34 -04:00
Mikayla Fischler
f4f36b020b #84 recursive get element by id 2022-07-28 10:15:12 -04:00
Mikayla Fischler
f1a50990f2 #84 improved element creation process for adding children 2022-07-28 10:09:34 -04:00
Mikayla Fischler
01a364b5cf fixed bug with spinbox 2022-07-23 20:08:52 -04:00
Mikayla Fischler
fc14141321 #73 unit overview parent/child setup, fixed touch events by setting up children for elements 2022-07-23 20:08:37 -04:00
Mikayla Fischler
9b21a971fe #74 close supervisor connection on exit, start of touch event handling 2022-07-20 13:28:58 -04:00
Mikayla Fischler
1afafba501 wrap os.pullEventRaw to have return types 2022-07-19 15:18:11 -04:00
Mikayla Fischler
d6a201a45f #73 initial unit view 2022-07-19 14:03:02 -04:00
Mikayla Fischler
41cc6b9acc support for craftos-pc env by supporting modems instead of wireless modems for comms 2022-07-19 14:02:20 -04:00
Mikayla Fischler
2aedc015c8 correctly find mek 10.1+ fission reactors 2022-07-17 15:05:27 -04:00
Mikayla Fischler
c3d6d900a1 bugfixes to graphics elements 2022-07-16 13:25:07 -04:00
Mikayla Fischler
525dedb830 added missing RPS fields to supervisor session 2022-07-16 12:54:02 -04:00
Mikayla Fischler
88bf4d5653 #80 mek 10.1+ support for reactor plc 2022-07-15 09:58:04 -04:00
Mikayla Fischler
6643c7e6ed removed debug fg_bg set 2022-07-14 14:29:48 -04:00
Mikayla Fischler
bd1ab11686 #79 water cooling only support, dynamic height, changed 2 turbine 1 boiler layout 2022-07-14 13:47:39 -04:00
Mikayla Fischler
8704d845bd fixed bug with cpair blit_a/blit_b colors 2022-07-14 13:45:40 -04:00
Mikayla Fischler
6f61203db3 #72, #78 updated main view to adapt to facility configuration, initial use of pub/sub for main view 2022-07-10 16:19:04 -04:00
Mikayla Fischler
5a96818c97 #72 ui formatting 2022-07-09 13:43:38 -04:00
Mikayla Fischler
b25ebdf959 fixed supervisor keep alive periodics timing 2022-07-07 13:18:10 -04:00
Mikayla Fischler
4b60c038f4 removed debug prints 2022-07-07 00:37:58 -04:00
Mikayla Fischler
ea17ba41fe #74 supervisor-coordinator comms establish 2022-07-07 00:34:42 -04:00
Mikayla Fischler
39672fedb4 code cleanup 2022-07-05 23:49:48 -04:00
Mikayla Fischler
1444008479 #74 comms establish on boot 2022-07-05 23:48:01 -04:00
Mikayla Fischler
409e8083a7 dmesg working status animation 2022-07-05 23:47:13 -04:00
Mikayla Fischler
335e0f5ee9 gitignore for notes directory 2022-07-05 12:49:46 -04:00
Mikayla Fischler
9bd220cbb2 removed unused requires 2022-07-05 12:48:21 -04:00
Mikayla Fischler
33159bc677 main loop and work on #74 comms 2022-07-05 12:47:02 -04:00
Mikayla Fischler
bd33240515 #62 modifing color palette 2022-07-05 12:46:31 -04:00
Mikayla Fischler
f6708ca988 coordinator dmesg wrapper functions 2022-07-05 11:18:26 -04:00
Mikayla Fischler
ed0982a832 handle nil tag color 2022-07-05 11:18:07 -04:00
Mikayla Fischler
7ad115bc03 #72 unit overview layout completed 2022-07-02 17:24:52 -04:00
Mikayla Fischler
3048fbed8b moved pipenet to be basic element not an indicator 2022-07-02 15:09:35 -04:00
Mikayla Fischler
35c408883a fixes to pipes/pipenet 2022-07-02 15:08:24 -04:00
Mikayla Fischler
20a1fab611 #72 added pipes to main overview, changed text of reactor overview 2022-06-29 17:40:46 -04:00
Mikayla Fischler
ef73c52417 pipenet indicator instead of pipe indicator 2022-06-29 17:40:08 -04:00
Mikayla Fischler
01caf3d914 pipe indicator graphics element 2022-06-26 16:36:21 -04:00
Mikayla Fischler
f32cdf5563 ticked version and fixed wording 2022-06-25 18:39:29 -04:00
Mikayla Fischler
1188d2f7df #72 work on main layout, reactor and boiler views exist now 2022-06-25 16:21:57 -04:00
Mikayla Fischler
e137953f93 fixed vbar bugs 2022-06-25 16:20:58 -04:00
Mikayla Fischler
316b255a04 fixed hbar percentage position 2022-06-25 14:51:59 -04:00
Mikayla Fischler
6397f29d4f fixed offsets/inner width for real this time 2022-06-25 14:51:38 -04:00
Mikayla Fischler
47599b8ff6 fixes to offsets and width calculations, init hbar to 0 2022-06-25 14:27:15 -04:00
Mikayla Fischler
e54d5b3d85 #74 coordinator comms and work on database 2022-06-25 13:39:47 -04:00
Mikayla Fischler
cf6f0e3153 publisher-subscriber interconnect layer 2022-06-25 13:38:31 -04:00
Mikayla Fischler
d3f28a6882 #75 handle edge case on rectangle border width, renamed inner_* to offset_* 2022-06-19 11:35:17 -04:00
Mikayla Fischler
15595ca81b #75 offset children of rectangles with borders 2022-06-19 11:20:09 -04:00
Mikayla Fischler
5a3897572d fixed bug with single word strings in strwrap 2022-06-18 02:15:03 -04:00
Mikayla Fischler
e4b7f807fe commas in data indicators 2022-06-18 02:14:48 -04:00
Mikayla Fischler
9bd2229e27 improvements to rectangle graphics element even rendering 2022-06-18 01:33:45 -04:00
Mikayla Fischler
27038f64f7 SCRAM button graphics element 2022-06-16 12:17:41 -04:00
Mikayla Fischler
6980e73658 default to not even border 2022-06-16 11:31:52 -04:00
Mikayla Fischler
ea9e9288f7 bugfix to hbar 2022-06-16 11:29:47 -04:00
Mikayla Fischler
7f007e032d #62, #72 work on main layout, not using layout class, refactoring and bugfixes 2022-06-16 11:24:35 -04:00
Mikayla Fischler
971657c3d2 graphics library refactoring and bugfixes 2022-06-16 11:19:32 -04:00
Mikayla Fischler
b628472d81 #74 work on coordinator comms 2022-06-15 15:35:34 -04:00
Mikayla Fischler
2e4a533148 comments 2022-06-14 12:05:49 -04:00
Mikayla Fischler
13513a9ce6 #62 graphics layouts 2022-06-14 12:02:42 -04:00
Mikayla Fischler
3593493c98 #62 basic start of the UI 2022-06-11 17:58:29 -04:00
Mikayla Fischler
7dbc5594b0 #63 div graphics element 2022-06-11 17:09:14 -04:00
Mikayla Fischler
89437b2be9 #63 cleanup and assertions 2022-06-11 17:06:32 -04:00
Mikayla Fischler
4488a0594f #63 numeric spinbox element 2022-06-11 16:44:31 -04:00
Mikayla Fischler
3004902ce5 #63 bugfixes 2022-06-11 16:38:15 -04:00
Mikayla Fischler
0950fc045d #63 new indicators and fixed up old ones 2022-06-11 12:21:14 -04:00
Mikayla Fischler
dc867095fd util spaces function 2022-06-11 12:20:49 -04:00
Mikayla Fischler
1fa87132d6 #63 allow hbar to have variable height, other bar improvement 2022-06-09 11:59:55 -04:00
Mikayla Fischler
11e4d89b1d #63 vertical fill bar indicator 2022-06-09 10:18:37 -04:00
Mikayla Fischler
307883e6e7 #63 use util string wrap and support text alignment 2022-06-08 18:53:24 -04:00
Mikayla Fischler
1dad4bcf77 util string wrap function 2022-06-08 18:48:20 -04:00
Mikayla Fischler
bc844d21bd #63 use util.strrep where appropriate 2022-06-08 17:22:20 -04:00
Mikayla Fischler
d8bbe4b459 #63 added indicator icon/light, added util.strrep string repeater 2022-06-08 17:16:53 -04:00
Mikayla Fischler
6f645579f8 #63 removed gframe as an argument to buttons 2022-06-08 16:52:41 -04:00
Mikayla Fischler
ac607f9dc6 #63 latching button in addition to pushbutton 2022-06-08 16:21:49 -04:00
Mikayla Fischler
15bc816d7e #63 button control element 2022-06-08 14:48:17 -04:00
Mikayla Fischler
254e85f3ed timer callback dispatcher 2022-06-08 14:47:45 -04:00
Mikayla Fischler
9d107da8d9 #63 horizontal fill bar indicator 2022-06-08 14:16:05 -04:00
Mikayla Fischler
b99f57e480 #62 redrawing 2022-06-08 14:15:34 -04:00
Mikayla Fischler
2ac9bab92e #63 basketweave tiling pattern element 2022-06-08 13:18:14 -04:00
Mikayla Fischler
29c4c39d23 #62 uneven border support because rectangular pixels 2022-06-08 13:08:48 -04:00
Mikayla Fischler
8002698dd0 #63 rectangle construct asserts 2022-06-08 12:29:53 -04:00
Mikayla Fischler
ce227a175a #63 rectangle element 2022-06-08 12:27:28 -04:00
Mikayla Fischler
8ea75b9501 #62, #63 graphics primatives and added display boxes to renderer 2022-06-06 15:42:39 -04:00
Mikayla Fischler
285026c1fa docs cleanup 2022-06-06 15:40:08 -04:00
Mikayla Fischler
8b307ea030 alias for color type and added read() to globals 2022-06-05 23:24:18 -04:00
Mikayla Fischler
b75d482f4a use is_int in validator 2022-06-05 16:54:34 -04:00
Mikayla Fischler
ebcc911b81 #70 validate RTU advertisements on the supervisor 2022-06-05 16:53:36 -04:00
Mikayla Fischler
0bc0decbf2 util.is_int 2022-06-05 16:51:38 -04:00
Mikayla Fischler
1c819779c7 #69 config file validation 2022-06-05 15:09:02 -04:00
Mikayla Fischler
d6c8eb4d56 #68 check RTU unit configs while parsing 2022-06-05 14:49:50 -04:00
Mikayla Fischler
81345f5325 #71 validate frame data types 2022-06-05 13:22:36 -04:00
Mikayla Fischler
f0c97e8b70 #65 safe concat where appropriate 2022-06-05 11:16:25 -04:00
Mikayla Fischler
5068e47590 #67 turbine valve RTU supervisor session, bugfixes with redstone RTU session 2022-06-05 09:30:56 -04:00
Mikayla Fischler
c764506999 #67 boilerv RTU supervisor session, supervisor session cleanup 2022-06-04 17:59:24 -04:00
Mikayla Fischler
6d97d45227 #67 imatrix RTU supervisor session 2022-06-04 17:45:52 -04:00
Mikayla Fischler
e443beec19 #66 SNA RTU supervisor session 2022-06-04 16:25:23 -04:00
Mikayla Fischler
0f7e77b0cb #28 fixed addresses for RTU session 2022-06-04 15:36:47 -04:00
Mikayla Fischler
27a86cc893 #28 SPS RTU supervisor session 2022-06-04 15:33:04 -04:00
Mikayla Fischler
07574aa116 alignment and fixed has_build bugs 2022-06-04 15:00:50 -04:00
Mikayla Fischler
dcb517d1cb trailing case of not using TXN_TAGS 2022-06-04 11:23:06 -04:00
Mikayla Fischler
1242c5a81c use TXN_TAGS for consistency 2022-06-04 11:17:54 -04:00
Mikayla Fischler
5cba8ff9f1 #59 environment detector RTU 2022-06-04 11:11:35 -04:00
Mikayla Fischler
fc7b83a18a #28 #66 #59 new RTUs 2022-06-04 10:49:36 -04:00
Mikayla Fischler
3bb95eb441 #64 util code cleanup 2022-05-31 16:09:06 -04:00
Mikayla Fischler
341df1a739 simplification of initenv file 2022-05-31 16:05:05 -04:00
Mikayla Fischler
ccc5220ca8 util round and trinary 2022-05-31 15:55:40 -04:00
Mikayla Fischler
e52b76aa24 supervisor unit sessions now actually call txnctrl.cleanup 2022-05-31 15:40:17 -04:00
Mikayla Fischler
43d5c0f8ad #64 supervisor code cleanup 2022-05-31 15:36:17 -04:00
Mikayla Fischler
4ec07ca053 #64 rtu code cleanup and device bugfixes 2022-05-31 14:54:55 -04:00
Mikayla Fischler
1705d8993e #64 plc code cleanup 2022-05-31 14:14:17 -04:00
Mikayla Fischler
309ba06f8a #51 crypto system 2022-05-29 15:05:57 -04:00
Mikayla Fischler
e65a1bf6e1 #61 monitor configuration and init, render engine started, dmesg changes, ppm monitor listing changes 2022-05-29 14:34:09 -04:00
Mikayla Fischler
ff5b163c1d ppm patch to support multiple return value functions, changed lack of modem to emit fatal error 2022-05-29 14:26:40 -04:00
234 changed files with 50296 additions and 4709 deletions

View File

@ -0,0 +1,16 @@
{
"image": "mcr.microsoft.com/devcontainers/universal:2",
"features": {
},
"customizations": {
"vscode": {
"extensions": [
"sumneko.lua",
"jackmacwindows.vscode-computercraft",
"ms-python.python",
"Catppuccin.catppuccin-vsc-icons",
"melishev.feather-vscode"
]
}
}
}

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
ko_fi: mikayla_f

27
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,27 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots and Logs**
If applicable, add screenshots to help explain your problem. Please include a text snippet from the log.txt files if possible, otherwise include a screenshot.
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: "enhancement,feature request"
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.

29
.github/workflows/check.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: Lua Checks
on:
workflow_dispatch:
push:
branches:
- main
- devel
pull_request:
branches:
- main
- devel
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
- name: Luacheck
uses: lunarmodules/luacheck@v1.1.0
with:
# Argument Explanations
# -i 121 = Setting a read-only global variable
# 512 = Loop can be executed at most once
# 542 = An empty if branch
# --no-max-line-length = Disable warnings for long line lengths
# --exclude-files ... = Exclude lockbox library (external) and config files
# --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os'
args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* --globals os _HOST bit colors fs http keys parallel periphemu peripheral read rs settings shell term textutils window

81
.github/workflows/manifest.yml vendored Normal file
View File

@ -0,0 +1,81 @@
# Deploy installation manifests and shields versions
name: Deploy Installation Data
on:
workflow_dispatch:
push:
branches:
- main
- devel
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
# Generate manifest + shields files for main branch
- name: Checkout main
id: checkout-main
uses: actions/checkout@v4
with:
ref: 'main'
clean: false
- name: Create outputs folders
if: success() || failure()
shell: bash
run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/devel
- name: Generate manifest and shields for main branch
id: manifest-main
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
run: python build/imgen.py shields
- name: Save main's manifest
if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/main
# Generate manifest for devel branch
- name: Checkout devel
id: checkout-devel
if: success() || failure()
uses: actions/checkout@v4
with:
ref: 'devel'
clean: false
- name: Generate manifest for devel
id: manifest-devel
if: ${{ (success() || failure()) && steps.checkout-devel.outcome == 'success' }}
run: python build/imgen.py
- name: Save devel's manifest
if: ${{ (success() || failure()) && steps.manifest-devel.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/devel
# All artifacts ready now, upload deploy directory
- name: Upload artifacts
id: upload-artifacts
if: ${{ (success() || failure()) && (steps.manifest-main.outcome == 'success' || steps.manifest-latest.outcome == 'success' || steps.manifest-devel.outcome == 'success') }}
uses: actions/upload-pages-artifact@v3
with:
# Upload manifest JSON
path: 'deploy/'
- name: Deploy to GitHub Pages
if: ${{ (success() || failure()) && steps.upload-artifacts.outcome == 'success' }}
id: deployment
uses: actions/deploy-pages@v4

2
.gitignore vendored Normal file
View File

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

7
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"recommendations": [
"sumneko.lua",
"jackmacwindows.vscode-computercraft",
"ms-python.python"
]
}

30
.vscode/settings.json vendored
View File

@ -1,13 +1,31 @@
{
"Lua.diagnostics.globals": [
"term",
"fs",
"peripheral",
"rs",
"_HOST",
"bit",
"parallel",
"colors",
"fs",
"http",
"keys",
"parallel",
"periphemu",
"peripheral",
"read",
"rs",
"settings",
"shell",
"term",
"textutils",
"shell"
"window"
],
"Lua.diagnostics.severity": {
"unused-local": "Information",
"unused-vararg": "Information",
"unused-function": "Warning",
"unused-label": "Information"
},
"Lua.hint.setType": true,
"Lua.diagnostics.disable": [
"duplicate-set-field",
"inject-field"
]
}

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
Copyright (c) 2022 Mikayla Fischler
Copyright 2022 - 2024 Mikayla Fischler
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,13 +1,62 @@
# cc-mek-scada
Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fission reactors with a GUI, automatic safety features, waste processing control, and more!
This requires CC: Tweaked and Mekanism v10.0+ (10.1 recommended for full feature set).
![GitHub](https://img.shields.io/github/license/MikaylaFischler/cc-mek-scada)
![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/MikaylaFischler/cc-mek-scada?include_prereleases)
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=main&label=main)
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=devel&label=devel)
### Join [the Discord](https://discord.gg/R9NSCkhcwt)!
![Discord](https://img.shields.io/discord/1129075839288496259?logo=Discord&logoColor=white&label=discord)
## Released Component Versions
![Installer](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Finstaller.json)
![Bootloader](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fbootloader.json)
![Comms](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fcommon.json)
![Comms](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fcomms.json)
![Graphics](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fgraphics.json)
![Lockbox](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Flockbox.json)
![Reactor PLC](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Freactor-plc.json)
![RTU](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Frtu.json)
![Supervisor](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fsupervisor.json)
![Coordinator](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fcoordinator.json)
![Pocket](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fpocket.json)
## Requirements
Mod Requirements:
- CC: Tweaked
- Mekanism v10.1+
Mod Recommendations:
- Advanced Peripherals (adds the capability to detect environmental radiation levels)
- Immersive Engineering (provides bundled redstone, though any mod containing bundled redstone will do)
v10.1+ is required due to the complete support of CC:Tweaked added in Mekanism v10.1
## Installation
You can install this on a ComputerCraft computer using either:
* `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua`
* `pastebin get sqUN6VUb ccmsi.lua`
* Off-line (when HTTP is disabled) installation via [release bundles](https://github.com/MikaylaFischler/cc-mek-scada/wiki/Alternative-Installation-Strategies#release-bundles)
## Contributing
Please reach out to me via Discord or email (or GitHub in some way) if you are thinking of making any contributions at this time. I started this project as a challenge for myself and have been enjoying having something I can work on in my own way.
Once this is out of beta I will be more open to contributions, but for now I am hoping to keep them to a minimum as the remaining challenges are ones I am looking forward to solving.
## [SCADA](https://en.wikipedia.org/wiki/SCADA)
> Supervisory control and data acquisition (SCADA) is a control system architecture comprising computers, networked data communications and graphical user interfaces for high-level supervision of machines and processes. It also covers sensors and other devices, such as programmable logic controllers, which interface with process plant or machinery.
This project implements concepts of a SCADA system in ComputerCraft (because why not? ..okay don't answer that). I recommend reviewing that linked wikipedia page on SCADA if you want to understand the concepts used here.
This project implements concepts of a SCADA system in ComputerCraft (because why not? ..okay don't answer that). I recommend reviewing that linked wikipedia page on SCADA if you *want* to understand the concepts used here.
![Architecture](https://upload.wikimedia.org/wikipedia/commons/thumb/1/10/Functional_levels_of_a_Distributed_Control_System.svg/1000px-Functional_levels_of_a_Distributed_Control_System.svg.png)
@ -35,7 +84,7 @@ The RTU control code is relatively unique, as instead of having instructions be
### PLCs
PLCs are advanced devices that allow for both reporting and control to/from the SCADA system in addition to programed behaviors independent of the SCADA system. Currently there is only one type of PLC, and that is the reactor PLC. This is responsible for reporting on and controlling the reactor as a part of the SCADA system, and independently regulating the safety of the reactor. It checks the status for multiple hazard scenarios and shuts down the reactor if any condition is satisfied.
PLCs are advanced devices that allow for both reporting and control to/from the SCADA system in addition to programed behaviors independent of the SCADA system. Currently there is only one type of PLC, and that is the reactor PLC. This is responsible for reporting on and controlling the reactor as a part of the SCADA system, and independently regulating the safety of the reactor. It checks the status for multiple hazard scenarios and shuts down the reactor if any condition is met.
There can and should only be one of these per reactor. A single Advanced Computer will act as the PLC, with either a direct connection (physical contact) or a wired modem connection to the reactor logic port.
@ -47,15 +96,8 @@ A vaguely-modbus [modbus](https://en.wikipedia.org/wiki/Modbus) communication pr
- Input Registers: Multi-Byte Read-Only (analog inputs)
- Holding Registers: Multi-Byte Read/Write (analog I/O)
### Security and Encryption
### Security
TBD, I am planning on AES symmetric encryption for security + HMAC to prevent replay attacks. This will be done utilizing this codebase: https://github.com/somesocks/lua-lockbox.
HMAC message authentication is available as a configuration option to prevent replay attacks and generally prevent control or false data reporting within a system's network. This is done utilizing the [lua-lockbox](https://github.com/somesocks/lua-lockbox) project.
This is somewhat important here as otherwise anyone can just control your setup, which is undeseriable. Unlike normal Minecraft PVP chaos, it would be very difficult to identify who is messing with your system, as with an Ender Modem they can do it from effectively anywhere and the server operators would have to check every computer's filesystem to find suspicious code.
The only other possible security mitigation for commanding (no effect on monitoring) is to enforce a maximum authorized transmission range (which I will probably also do, or maybe fall back to), as modem message events contain the transmission distance.
## Known Issues
GitHub issue \#29:
It appears that with Mekanism 10.0, a boiler peripheral may rapidly disconnect/reconnect constantly while running. This will prevent that RTU from operating correctly while also filling up the log file. This may be due to a very specific version interaction of CC: Tweaked and Mekansim, so you are welcome to try this on Mekanism 10.0 servers, but do be aware it may not work.
The other, simpler security feature is to enforce a maximum authorized transmission range, which is also a configurable feature on each device.

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()

133
build/imgen.py Normal file
View File

@ -0,0 +1,133 @@
import json
import os
import sys
# list files in a directory
def list_files(path):
list = []
for (root, dirs, files) in os.walk(path):
for f in files:
list.append((root[2:] + "/" + f).replace('\\','/'))
return list
# get size of all files in a directory
def dir_size(path):
total = 0
for (root, dirs, files) in os.walk(path):
for f in files:
total += os.path.getsize(root + "/" + f)
return total
# get the version of an application at the provided path
def get_version(path, is_lib = False):
ver = ""
string = ".version = \""
if not is_lib:
string = "_VERSION = \""
f = open(path, "r")
for line in f:
pos = line.find(string)
if pos >= 0:
ver = line[(pos + len(string)):(len(line) - 2)]
break
f.close()
return ver
# generate installation manifest object
def make_manifest(size):
manifest = {
"versions" : {
"installer" : get_version("./ccmsi.lua"),
"bootloader" : get_version("./startup.lua"),
"common" : get_version("./scada-common/util.lua", True),
"comms" : get_version("./scada-common/comms.lua", True),
"graphics" : get_version("./graphics/core.lua", True),
"lockbox" : get_version("./lockbox/init.lua", True),
"reactor-plc" : get_version("./reactor-plc/startup.lua"),
"rtu" : get_version("./rtu/startup.lua"),
"supervisor" : get_version("./supervisor/startup.lua"),
"coordinator" : get_version("./coordinator/startup.lua"),
"pocket" : get_version("./pocket/startup.lua")
},
"files" : {
# common files
"system" : [ "initenv.lua", "startup.lua", "configure.lua", "LICENSE" ],
"common" : list_files("./scada-common"),
"graphics" : list_files("./graphics"),
"lockbox" : list_files("./lockbox"),
# platform files
"reactor-plc" : list_files("./reactor-plc"),
"rtu" : list_files("./rtu"),
"supervisor" : list_files("./supervisor"),
"coordinator" : list_files("./coordinator"),
"pocket" : list_files("./pocket"),
},
"depends" : {
"reactor-plc" : [ "system", "common", "graphics", "lockbox" ],
"rtu" : [ "system", "common", "graphics", "lockbox" ],
"supervisor" : [ "system", "common", "graphics", "lockbox" ],
"coordinator" : [ "system", "common", "graphics", "lockbox" ],
"pocket" : [ "system", "common", "graphics", "lockbox" ]
},
"sizes" : {
# manifest file estimate
"manifest" : size,
# common files
"system" : os.path.getsize("initenv.lua") + os.path.getsize("startup.lua") + os.path.getsize("configure.lua"),
"common" : dir_size("./scada-common"),
"graphics" : dir_size("./graphics"),
"lockbox" : dir_size("./lockbox"),
# platform files
"reactor-plc" : dir_size("./reactor-plc"),
"rtu" : dir_size("./rtu"),
"supervisor" : dir_size("./supervisor"),
"coordinator" : dir_size("./coordinator"),
"pocket" : dir_size("./pocket"),
}
}
return manifest
# write initial manifest with placeholder size
f = open("install_manifest.json", "w")
json.dump(make_manifest("-----"), f)
f.close()
manifest_size = os.path.getsize("install_manifest.json")
final_manifest = make_manifest(manifest_size)
# calculate file size then regenerate with embedded size
f = open("install_manifest.json", "w")
json.dump(final_manifest, f)
f.close()
if len(sys.argv) > 1 and sys.argv[1] == "shields":
# write all the JSON files for shields.io
for key, version in final_manifest["versions"].items():
f = open("./deploy/" + key + ".json", "w")
if version.find("alpha") >= 0:
color = "yellow"
elif version.find("beta") >= 0:
color = "orange"
else:
color = "blue"
json.dump({
"schemaVersion": 1,
"label": key,
"message": "" + version,
"color": color
}, f)
f.close()

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()

779
ccmsi.lua Normal file
View File

@ -0,0 +1,779 @@
--[[
CC-MEK-SCADA Installer Utility
Copyright (c) 2023 - 2024 Mikayla Fischler
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]--
local CCMSI_VERSION = "v1.21"
local install_dir = "/.install-cache"
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
local repo_path = "http://git.befatorinc.de/TheHomecraft/cc-mek-scada/raw/"
---@diagnostic disable-next-line: undefined-global
local _is_pkt_env = pocket -- luacheck: ignore pocket
local function println(msg) print(tostring(msg)) end
-- stripped down & modified copy of log.dmesg
local function print(msg)
msg = tostring(msg)
local cur_x, cur_y = term.getCursorPos()
local out_w, out_h = term.getSize()
-- jump to next line if needed
if cur_x == out_w then
cur_x = 1
if cur_y == out_h then
term.scroll(1)
term.setCursorPos(1, cur_y)
else
term.setCursorPos(1, cur_y + 1)
end
end
-- wrap
local lines, remaining, s_start, s_end, ln = {}, true, 1, out_w + 1 - cur_x, 1
while remaining do
local line = string.sub(msg, s_start, s_end)
if line == "" then
remaining = false
else
lines[ln] = line
s_start = s_end + 1
s_end = s_end + out_w
ln = ln + 1
end
end
-- print
for i = 1, #lines do
cur_x, cur_y = term.getCursorPos()
if i > 1 and cur_x > 1 then
if cur_y == out_h then
term.scroll(1)
term.setCursorPos(1, cur_y)
else term.setCursorPos(1, cur_y + 1) end
end
term.write(lines[i])
end
end
local opts = { ... }
local mode, app, target
local install_manifest = manifest_path.."main/install_manifest.json"
local function red() term.setTextColor(colors.red) end
local function orange() term.setTextColor(colors.orange) end
local function yellow() term.setTextColor(colors.yellow) end
local function green() term.setTextColor(colors.green) end
local function cyan() term.setTextColor(colors.cyan) end
local function blue() term.setTextColor(colors.blue) end
local function white() term.setTextColor(colors.white) end
local function lgray() term.setTextColor(colors.lightGray) end
-- get command line option in list
local function get_opt(opt, options)
for _, v in pairs(options) do if opt == v then return v end end
return nil
end
-- wait for any key to be pressed
---@diagnostic disable-next-line: undefined-field
local function any_key() os.pullEvent("key_up") end
-- ask the user yes or no
local function ask_y_n(question, default)
print(question)
if default == true then print(" (Y/n)? ") else print(" (y/N)? ") end
local response = read();any_key()
if response == "" then return default
elseif response == "Y" or response == "y" then return true
elseif response == "N" or response == "n" then return false
else return nil end
end
-- print out a white + blue text message
local function pkg_message(message, package) white();print(message.." ");blue();println(package);white() end
-- indicate actions to be taken based on package differences for installs/updates
local function show_pkg_change(name, v)
if v.v_local ~= nil then
if v.v_local ~= v.v_remote then
print("["..name.."] updating ");blue();print(v.v_local);white();print(" \xbb ");blue();println(v.v_remote);white()
elseif mode == "install" then
pkg_message("["..name.."] reinstalling", v.v_local)
end
else pkg_message("["..name.."] new install of", v.v_remote) end
return v.v_local ~= v.v_remote
end
-- read the local manifest file
local function read_local_manifest()
local local_ok = false
local local_manifest = {}
local imfile = fs.open("install_manifest.json", "r")
if imfile ~= nil then
local_ok, local_manifest = pcall(function () return textutils.unserializeJSON(imfile.readAll()) end)
imfile.close()
end
return local_ok, local_manifest
end
-- get the manifest from GitHub
local function get_remote_manifest()
local response, error = http.get(install_manifest)
if response == nil then
orange();println("Failed to get installation manifest from GitHub, cannot update or install.")
red();println("HTTP error: "..error);white()
return false, {}
end
local ok, manifest = pcall(function () return textutils.unserializeJSON(response.readAll()) end)
if not ok then red();println("error parsing remote installation manifest");white() end
return ok, manifest
end
-- record the local installation manifest
local function write_install_manifest(manifest, deps)
local versions = {}
for key, value in pairs(manifest.versions) do
local is_dep = false
for _, dep in pairs(deps) do
if (key == "bootloader" and dep == "system") or key == dep then
is_dep = true;break
end
end
if key == app or key == "comms" or is_dep then versions[key] = value end
end
manifest.versions = versions
local imfile = fs.open("install_manifest.json", "w")
imfile.write(textutils.serializeJSON(manifest))
imfile.close()
end
-- try at most 3 times to download a file from the repository and write into w_path base directory
---@return 0|1|2|3 success 0: ok, 1: download fail, 2: file open fail, 3: out of space
local function http_get_file(file, w_path)
local dl, err
for i = 1, 3 do
dl, err = http.get(repo_path..file)
if dl then
if i > 1 then green();println("success!");lgray() end
local f = fs.open(w_path..file, "w")
if not f then return 2 end
local ok, msg = pcall(function() f.write(dl.readAll()) end)
f.close()
if not ok then
if string.find(msg or "", "Out of space") ~= nil then
red();println("[out of space]");lgray()
return 3
else return 2 end
end
break
else
red();println("HTTP Error: "..err)
if i < 3 then
lgray();print("> retrying...")
---@diagnostic disable-next-line: undefined-field
os.sleep(i/3.0)
else
return 1
end
end
end
return 0
end
-- recursively build a tree out of the file manifest
local function gen_tree(manifest, 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 ---@todo remove config.lua on full release
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, "ccmsi.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
if _is_pkt_env then println("- SCADA Installer "..CCMSI_VERSION.." -")
else println("-- CC Mekanism SCADA Installer "..CCMSI_VERSION.." --") end
if #opts == 0 or opts[1] == "help" then
println("usage: ccmsi <mode> <app> <branch>")
if _is_pkt_env then
yellow();println("<mode>");lgray()
println(" check - check latest")
println(" install - fresh install")
println(" update - update app")
println(" uninstall - remove app")
yellow();println("<app>");lgray()
println(" reactor-plc")
println(" rtu")
println(" supervisor")
println(" coordinator")
println(" pocket")
println(" installer (update only)")
yellow();println("<branch>");lgray();
println(" main (default) | devel");white()
else
println("<mode>")
lgray()
println(" check - check latest versions available")
yellow()
println(" ccmsi check <branch> for target")
lgray()
println(" install - fresh install")
println(" update - update files")
println(" uninstall - delete files INCLUDING config/logs")
white();println("<app>");lgray()
println(" reactor-plc - reactor PLC firmware")
println(" rtu - RTU firmware")
println(" supervisor - supervisor server application")
println(" coordinator - coordinator application")
println(" pocket - pocket application")
println(" installer - ccmsi installer (update only)")
white();println("<branch>")
lgray();println(" main (default) | devel");white()
end
return
else
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
if mode == nil then
red();println("Unrecognized mode.");white()
return
end
local next_opt = 3
local apps = { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket", "installer" }
app = get_opt(opts[2], apps)
if app == nil then
for _, a in pairs(apps) do
if fs.exists(a) and fs.isDir(a) then
app = a
next_opt = 2
break
end
end
end
if app == nil and mode ~= "check" then
red();println("Unrecognized application.");white()
return
elseif mode == "check" then
next_opt = 2
elseif app == "installer" and mode ~= "update" then
red();println("Installer app only supports 'update' option.");white()
return
end
-- determine target
target = opts[next_opt]
if (target ~= "main") and (target ~= "devel") then
if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end
target = "main"
end
-- set paths
install_manifest = manifest_path..target.."/install_manifest.json"
repo_path = repo_path..target.."/"
end
-- run selected mode
if mode == "check" then
local ok, manifest = get_remote_manifest()
if not ok then return end
local local_ok, local_manifest = read_local_manifest()
if not local_ok then
yellow();println("failed to load local installation information");white()
local_manifest = { versions = { installer = CCMSI_VERSION } }
else
local_manifest.versions.installer = CCMSI_VERSION
end
-- list all versions
for key, value in pairs(manifest.versions) do
term.setTextColor(colors.purple)
local tag = string.format("%-14s", "["..key.."]")
if not _is_pkt_env then print(tag) end
if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then
if _is_pkt_env then println(tag) end
blue();print(local_manifest.versions[key])
if value ~= local_manifest.versions[key] then
white();print(" (")
cyan();print(value);white();println(" available)")
else green();println(" (up to date)") end
elseif not _is_pkt_env then
lgray();print("not installed");white();print(" (latest ")
cyan();print(value);white();println(")")
end
end
if manifest.versions.installer ~= local_manifest.versions.installer and not _is_pkt_env then
yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white()
end
elseif mode == "install" or mode == "update" then
local ok, r_manifest, l_manifest
local update_installer = app == "installer"
ok, r_manifest = get_remote_manifest()
if not ok then return end
local ver = {
app = { v_local = nil, v_remote = nil, changed = false },
boot = { v_local = nil, v_remote = nil, changed = false },
comms = { v_local = nil, v_remote = nil, changed = false },
common = { v_local = nil, v_remote = nil, changed = false },
graphics = { v_local = nil, v_remote = nil, changed = false },
lockbox = { v_local = nil, v_remote = nil, changed = false }
}
-- try to find local versions
ok, l_manifest = read_local_manifest()
if mode == "update" and not update_installer then
if not ok then
red();println("Failed to load local installation information, cannot update.");white()
return
else
ver.boot.v_local = l_manifest.versions.bootloader
ver.app.v_local = l_manifest.versions[app]
ver.comms.v_local = l_manifest.versions.comms
ver.common.v_local = l_manifest.versions.common
ver.graphics.v_local = l_manifest.versions.graphics
ver.lockbox.v_local = l_manifest.versions.lockbox
if l_manifest.versions[app] == nil then
red();println("Another application is already installed, please uninstall it before installing a new application.");white()
return
end
end
end
if r_manifest.versions.installer ~= CCMSI_VERSION then
if not update_installer then yellow();println("A different version of the installer is available, it is recommended to update to it.");white() end
if update_installer or ask_y_n("Would you like to update now", true) then
lgray();println("GET ccmsi.lua")
local dl, err = http.get(repo_path.."ccmsi.lua")
if dl == nil then
red();println("HTTP Error: "..err)
println("Installer download failed.");white()
else
local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location
handle.write(dl.readAll())
handle.close()
green();println("Installer updated successfully.");white()
end
return
end
elseif update_installer then
green();println("Installer already up-to-date.");white()
return
end
ver.boot.v_remote = r_manifest.versions.bootloader
ver.app.v_remote = r_manifest.versions[app]
ver.comms.v_remote = r_manifest.versions.comms
ver.common.v_remote = r_manifest.versions.common
ver.graphics.v_remote = r_manifest.versions.graphics
ver.lockbox.v_remote = r_manifest.versions.lockbox
green()
if mode == "install" then print("Installing ") else print("Updating ") end
println(app.." files...");white()
ver.boot.changed = show_pkg_change("bootldr", ver.boot)
ver.common.changed = show_pkg_change("common", ver.common)
ver.comms.changed = show_pkg_change("comms", ver.comms)
if ver.comms.changed and ver.comms.v_local ~= nil then
print("[comms] ");yellow();println("other devices on the network will require an update");white()
end
ver.app.changed = show_pkg_change(app, ver.app)
ver.graphics.changed = show_pkg_change("graphics", ver.graphics)
ver.lockbox.changed = show_pkg_change("lockbox", ver.lockbox)
-- start install/update
local space_req = r_manifest.sizes.manifest
local space_avail = fs.getFreeSpace("/")
local file_list = r_manifest.files
local size_list = r_manifest.sizes
local deps = r_manifest.depends[app]
table.insert(deps, app)
-- helper function to check if a dependency is unchanged
local function unchanged(dep)
if dep == "system" then return not ver.boot.changed
elseif dep == "graphics" then return not ver.graphics.changed
elseif dep == "lockbox" then return not ver.lockbox.changed
elseif dep == "common" then return not (ver.common.changed or ver.comms.changed)
elseif dep == app then return not ver.app.changed
else return true end
end
local any_change = false
for _, dep in pairs(deps) do
local size = size_list[dep]
space_req = space_req + size
any_change = any_change or not unchanged(dep)
end
if mode == "update" and not any_change then
yellow();println("Nothing to do, everything is already up-to-date!");white()
return
end
-- ask for confirmation
if not ask_y_n("Continue", false) then return end
local single_file_mode = space_avail < space_req
local success = true
-- delete a file if the capitalization changes so that things work on Windows
---@param path string
local function mitigate_case(path)
local dir, file = fs.getDir(path), fs.getName(path)
if not fs.isDir(dir) then return end
for _, p in ipairs(fs.list(dir)) do
if string.lower(p) == string.lower(file) then
if p ~= file then fs.delete(path) end
return
end
end
end
---@param dl_stat 1|2|3 download status
---@param file string file name
---@param attempt integer recursive attempt #
---@param sf_install function installer function for recursion
local function handle_dl_fail(dl_stat, file, attempt, sf_install)
red()
if dl_stat == 1 then
println("failed to download "..file)
elseif dl_stat > 1 then
if dl_stat == 2 then println("filesystem error with "..file) else println("no space for "..file) end
if attempt == 1 then
orange();println("re-attempting operation...");white()
sf_install(2)
elseif attempt == 2 then
yellow()
if dl_stat == 2 then println("There was an error writing to a file.") else println("Insufficient space available.") end
lgray()
if dl_stat == 2 then
println("This may be due to insufficent space available or file permission issues. The installer can now attempt to delete files not used by the SCADA system.")
else
println("The installer can now attempt to delete files not used by the SCADA system.")
end
white()
if not ask_y_n("Continue", false) then
success = false
return
end
clean(r_manifest)
sf_install(3)
elseif attempt == 3 then
yellow()
if dl_stat == 2 then println("There again was an error writing to a file.") else println("Insufficient space available.") end
lgray()
if dl_stat == 2 then
println("This may be due to insufficent space available or file permission issues. Please delete any unused files you have on this computer then try again. Do not delete the "..app..".settings file unless you want to re-configure.")
else
println("Please delete any unused files you have on this computer then try again. Do not delete the "..app..".settings file unless you want to re-configure.")
end
white()
success = false
end
end
end
-- single file update routine: go through all files and replace one by one
---@param attempt integer recursive attempt #
local function sf_install(attempt)
---@diagnostic disable-next-line: undefined-field
if attempt > 1 then os.sleep(2.0) end
local abort_attempt = false
success = true
for _, dep in pairs(deps) do
if mode == "update" and unchanged(dep) then
pkg_message("skipping install of unchanged package", dep)
else
pkg_message("installing package", dep)
lgray()
-- beginning on the second try, delete the directory before starting
if attempt >= 2 then
if dep == "system" then
elseif dep == "common" then
if fs.exists("/scada-common") then
fs.delete("/scada-common")
println("deleted /scada-common")
end
else
if fs.exists("/"..dep) then
fs.delete("/"..dep)
println("deleted /"..dep)
end
end
end
local files = file_list[dep]
for _, file in pairs(files) do
println("GET "..file)
mitigate_case(file)
local dl_stat = http_get_file(file, "/")
if dl_stat ~= 0 then
abort_attempt = true
---@diagnostic disable-next-line: param-type-mismatch
handle_dl_fail(dl_stat, file, attempt, sf_install)
break
end
end
end
if abort_attempt or not success then break end
end
end
-- handle update/install
if single_file_mode then sf_install(1)
else
if fs.exists(install_dir) then fs.delete(install_dir);fs.makeDir(install_dir) end
-- download all dependencies
for _, dep in pairs(deps) do
if mode == "update" and unchanged(dep) then
pkg_message("skipping download of unchanged package", dep)
else
pkg_message("downloading package", dep)
lgray()
local files = file_list[dep]
for _, file in pairs(files) do
println("GET "..file)
local dl_stat = http_get_file(file, install_dir.."/")
success = dl_stat == 0
if dl_stat == 1 then
red();println("failed to download "..file)
break
elseif dl_stat == 2 then
red();println("filesystem error with "..file)
break
elseif dl_stat == 3 then
-- this shouldn't occur in this mode
red();println("no space for "..file)
break
end
end
end
if not success then break end
end
-- copy in downloaded files (installation)
if success then
for _, dep in pairs(deps) do
if mode == "update" and unchanged(dep) then
pkg_message("skipping install of unchanged package", dep)
else
pkg_message("installing package", dep)
lgray()
local files = file_list[dep]
for _, file in pairs(files) do
local temp_file = install_dir.."/"..file
if fs.exists(file) then fs.delete(file) end
fs.move(temp_file, file)
end
end
end
end
fs.delete(install_dir)
end
if success then
write_install_manifest(r_manifest, deps)
green()
if mode == "install" then
println("Installation completed successfully.")
else println("Update completed successfully.") end
white();println("Ready to clean up unused files, press any key to continue...")
any_key();clean(r_manifest)
white();println("Done.")
else
red()
if single_file_mode then
if mode == "install" then
println("Installation failed, files may have been skipped.")
else println("Update failed, files may have been skipped.") end
else
if mode == "install" then
println("Installation failed.")
else orange();println("Update failed, existing files unmodified.") end
end
end
elseif mode == "uninstall" then
local ok, manifest = read_local_manifest()
if not ok then
red();println("Error parsing local installation manifest.");white()
return
end
if manifest.versions[app] == nil then
red();println("Error: '"..app.."' is not installed.")
return
end
orange();println("Uninstalling all "..app.." files...")
-- ask for confirmation
if not ask_y_n("Continue", false) then return end
-- delete unused files first
clean(manifest)
local file_list = manifest.files
local deps = manifest.depends[app]
table.insert(deps, app)
-- delete all installed files
lgray()
for _, dep in pairs(deps) do
local files = file_list[dep]
for _, file in pairs(files) do
if fs.exists(file) then fs.delete(file);println("deleted "..file) end
end
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 fs.exists(settings_file) then
fs.delete(settings_file);println("deleted "..settings_file)
end
fs.delete("install_manifest.json")
println("deleted install_manifest.json")
green();println("Done!")
end
white()

12
configure.lua Normal file
View File

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

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

391
coordinator/configure.lua Normal file
View File

@ -0,0 +1,391 @@
--
-- Configuration GUI
--
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local types = require("scada-common.types")
local util = require("scada-common.util")
local facility = require("coordinator.config.facility")
local hmi = require("coordinator.config.hmi")
local system = require("coordinator.config.system")
local core = require("graphics.core")
local themes = require("graphics.themes")
local DisplayBox = require("graphics.elements.DisplayBox")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton")
local println = util.println
local tri = util.trinary
local cpair = core.cpair
local CENTER = core.ALIGN.CENTER
-- changes to the config data/format to let the user know
local changes = {
{ "v1.2.4", { "Added temperature scale options" } },
{ "v1.2.12", { "Added main UI theme", "Added front panel UI theme", "Added color accessibility modes" } },
{ "v1.3.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
{ "v1.5.1", { "Added energy scale options" } },
{ "v1.6.13", { "Added option for Po/Pu pellet green/cyan pairing" } }
}
---@class crd_configurator
local configurator = {}
local style = {}
style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray)
style.colors = themes.smooth_stone.colors
style.bw_fg_bg = cpair(colors.black, colors.white)
style.g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
style.nav_fg_bg = style.bw_fg_bg
style.btn_act_fg_bg = cpair(colors.white, colors.gray)
style.btn_dis_fg_bg = cpair(colors.lightGray,colors.white)
---@class _crd_cfg_tool_ctl
local tool_ctl = {
sv_cool_conf = nil, ---@type [ integer, integer ][] list of boiler & turbine counts
launch_startup = false,
start_fail = 0,
fail_message = "",
has_config = false,
viewing_config = false,
jumped_to_color = false,
view_cfg = nil, ---@type PushButton
color_cfg = nil, ---@type PushButton
color_next = nil, ---@type PushButton
color_apply = nil, ---@type PushButton
settings_apply = nil, ---@type PushButton
gen_summary = nil, ---@type function
load_legacy = nil, ---@type function
-- settings elements from hmi
dis_flow_view = nil, ---@type Checkbox
s_vol = nil, ---@type NumberField
pellet_color = nil, ---@type RadioButton
clock_fmt = nil, ---@type RadioButton
temp_scale = nil, ---@type RadioButton
energy_scale = nil, ---@type RadioButton
-- settings elements and functions from facility
num_units = nil, ---@type NumberField
init_sv_connect_ui = nil, ---@type function
is_int_min_max = nil, ---@type function
update_mon_reqs = nil, ---@type function
gen_mon_list = function () end
}
---@class crd_config
local tmp_cfg = {
UnitCount = 1,
SpeakerVolume = 1.0,
Time24Hour = true,
GreenPuPellet = false,
TempScale = 1, ---@type TEMP_SCALE
EnergyScale = 1, ---@type ENERGY_SCALE
DisableFlowView = false,
MainDisplay = nil, ---@type string
FlowDisplay = nil, ---@type string
UnitDisplays = {}, ---@type string[]
SVR_Channel = nil, ---@type integer
CRD_Channel = nil, ---@type integer
PKT_Channel = nil, ---@type integer
SVR_Timeout = nil, ---@type number
API_Timeout = nil, ---@type number
TrustedRange = nil, ---@type number
AuthKey = nil, ---@type string|nil
LogMode = 0, ---@type LOG_MODE
LogPath = "",
LogDebug = false,
MainTheme = 1, ---@type UI_THEME
FrontPanelTheme = 1, ---@type FP_THEME
ColorMode = 1 ---@type COLOR_MODE
}
---@class crd_config
local ini_cfg = {}
---@class crd_config
local settings_cfg = {}
-- all settings fields, their nice names, and their default values
local fields = {
{ "UnitCount", "Number of Reactors", 1 },
{ "MainDisplay", "Main Monitor", nil },
{ "FlowDisplay", "Flow Monitor", nil },
{ "UnitDisplays", "Unit Monitors", {} },
{ "SpeakerVolume", "Speaker Volume", 1.0 },
{ "Time24Hour", "Use 24-hour Time Format", true },
{ "GreenPuPellet", "Pellet Colors", false },
{ "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN },
{ "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE },
{ "DisableFlowView", "Disable Flow Monitor (legacy, discouraged)", false },
{ "SVR_Channel", "SVR Channel", 16240 },
{ "CRD_Channel", "CRD Channel", 16243 },
{ "PKT_Channel", "PKT Channel", 16244 },
{ "SVR_Timeout", "Supervisor Connection Timeout", 5 },
{ "API_Timeout", "API Connection Timeout", 5 },
{ "TrustedRange", "Trusted Range", 0 },
{ "AuthKey", "Facility Auth Key" , ""},
{ "LogMode", "Log Mode", log.MODE.APPEND },
{ "LogPath", "Log Path", "/log.txt" },
{ "LogDebug", "Log Debug Messages", false },
{ "MainTheme", "Main UI Theme", themes.UI_THEME.SMOOTH_STONE },
{ "FrontPanelTheme", "Front Panel Theme", themes.FP_THEME.SANDSTONE },
{ "ColorMode", "Color Mode", themes.COLOR_MODE.STANDARD }
}
-- load tmp_cfg fields from ini_cfg fields for displays
local function preset_monitor_fields()
tmp_cfg.DisableFlowView = ini_cfg.DisableFlowView
tmp_cfg.MainDisplay = ini_cfg.MainDisplay
tmp_cfg.FlowDisplay = ini_cfg.FlowDisplay
for i = 1, ini_cfg.UnitCount do
tmp_cfg.UnitDisplays[i] = ini_cfg.UnitDisplays[i]
end
end
-- load data from the settings file
---@param target crd_config
---@param raw boolean? true to not use default values
local function load_settings(target, raw)
for _, v in pairs(fields) do settings.unset(v[1]) end
local loaded = settings.load("/coordinator.settings")
for _, v in pairs(fields) do target[v[1]] = settings.get(v[1], tri(raw, nil, v[3])) end
return loaded
end
-- create the config view
---@param display DisplayBox
local function config_view(display)
local bw_fg_bg = style.bw_fg_bg
local g_lg_fg_bg = style.g_lg_fg_bg
local nav_fg_bg = style.nav_fg_bg
local btn_act_fg_bg = style.btn_act_fg_bg
local btn_dis_fg_bg = style.btn_dis_fg_bg
---@diagnostic disable-next-line: undefined-field
local function exit() os.queueEvent("terminate") end
TextBox{parent=display,y=1,text="Coordinator Configurator",alignment=CENTER,fg_bg=style.header}
local root_pane_div = Div{parent=display,x=1,y=2}
local main_page = Div{parent=root_pane_div,x=1,y=1}
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
local fac_cfg = Div{parent=root_pane_div,x=1,y=1}
local mon_cfg = Div{parent=root_pane_div,x=1,y=1}
local spkr_cfg = Div{parent=root_pane_div,x=1,y=1}
local crd_cfg = Div{parent=root_pane_div,x=1,y=1}
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
local clr_cfg = Div{parent=root_pane_div,x=1,y=1}
local summary = Div{parent=root_pane_div,x=1,y=1}
local changelog = Div{parent=root_pane_div,x=1,y=1}
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,net_cfg,fac_cfg,mon_cfg,spkr_cfg,crd_cfg,log_cfg,clr_cfg,summary,changelog}}
--#region Main Page
local y_start = 5
TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the Coordinator configurator! Please select one of the following options."}
if tool_ctl.start_fail == 2 then
local msg = util.c("Notice: There is a problem with your monitor configuration. ", tool_ctl.fail_message, " Please reconfigure monitors or correct their sizes.")
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text=msg,fg_bg=cpair(colors.red,colors.lightGray)}
y_start = y_start + 5
elseif tool_ctl.start_fail > 0 then
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device is not configured for this version of the coordinator. If you previously had a valid config, it's not lost. You may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
y_start = y_start + 5
end
local function view_config()
tool_ctl.viewing_config = true
tool_ctl.gen_summary(settings_cfg)
tool_ctl.settings_apply.hide(true)
main_pane.set_value(9)
end
if fs.exists("/coordinator/config.lua") then
PushButton{parent=main_page,x=2,y=y_start,min_width=28,text="Import Legacy 'config.lua'",callback=function()tool_ctl.load_legacy()end,fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=btn_act_fg_bg}
y_start = y_start + 2
end
PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure System",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
local function jump_color()
tool_ctl.jumped_to_color = true
tool_ctl.color_next.hide(true)
tool_ctl.color_apply.show()
main_pane.set_value(8)
end
local function startup()
tool_ctl.launch_startup = true
exit()
end
PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
local start_btn = PushButton{parent=main_page,x=42,y=17,min_width=9,text="Startup",callback=startup,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
tool_ctl.color_cfg = PushButton{parent=main_page,x=36,y=y_start,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
PushButton{parent=main_page,x=39,y=y_start+2,min_width=12,text="Change Log",callback=function()main_pane.set_value(10)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
if tool_ctl.start_fail ~= 0 then start_btn.disable() end
if not tool_ctl.has_config then
tool_ctl.view_cfg.disable()
tool_ctl.color_cfg.disable()
end
--#endregion
local settings = { settings_cfg, ini_cfg, tmp_cfg, fields, load_settings }
--#region Facility Configuration
local fac_pane = facility.create(tool_ctl, main_pane, settings, fac_cfg, style)
--#endregion
--#region HMI Configuration
local mon_pane = hmi.create(tool_ctl, main_pane, settings, { mon_cfg, spkr_cfg, crd_cfg }, style)
--#endregion
--#region System Configuration
local divs = { net_cfg, log_cfg, clr_cfg, summary }
local ext = { fac_pane, mon_pane, preset_monitor_fields, exit }
system.create(tool_ctl, main_pane, settings, divs, ext, style)
--#endregion
--#region Config Change Log
local cl = Div{parent=changelog,x=2,y=4,width=49}
TextBox{parent=changelog,x=1,y=2,text=" Config Change Log",fg_bg=bw_fg_bg}
local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
for _, change in ipairs(changes) do
TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
for _, v in ipairs(change[2]) do
local e = Div{parent=c_log,height=#util.strwrap(v,46)}
TextBox{parent=e,y=1,x=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
end
end
PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
end
-- reset terminal screen
local function reset_term()
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
end
-- run the coordinator configurator<br>
-- start_fail of 0 is OK (default if not provided), 1 is bad config, 2 is bad monitor config
---@param start_code? 0|1|2 indicate error state when called from the startup app
---@param message? any string message to display on a start_fail of 2
function configurator.configure(start_code, message)
tool_ctl.start_fail = start_code or 0
tool_ctl.fail_message = util.trinary(type(message) == "string", message, "")
load_settings(settings_cfg, true)
tool_ctl.has_config = load_settings(ini_cfg)
-- copy in some important values to start with
preset_monitor_fields()
reset_term()
ppm.mount_all()
-- set overridden colors
for i = 1, #style.colors do
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
end
local status, error = pcall(function ()
local display = DisplayBox{window=term.current(),fg_bg=style.root}
config_view(display)
tool_ctl.gen_mon_list()
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event
if event == "timer" then
tcd.handle(param1)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
local m_e = core.events.new_mouse_event(event, param1, param2, param3)
if m_e then display.handle_mouse(m_e) end
elseif event == "char" or event == "key" or event == "key_up" then
local k_e = core.events.new_key_event(event, param1, param2)
if k_e then display.handle_key(k_e) end
elseif event == "paste" then
display.handle_paste(param1)
elseif event == "peripheral_detach" then
---@diagnostic disable-next-line: discard-returns
ppm.handle_unmount(param1)
tool_ctl.gen_mon_list()
elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1)
tool_ctl.gen_mon_list()
elseif event == "monitor_resize" then
tool_ctl.gen_mon_list()
elseif event == "modem_message" then
facility.receive_sv(param1, param2, param3, param4, param5)
end
if event == "terminate" then return end
end
end)
-- restore colors
for i = 1, #style.colors do
local r, g, b = term.nativePaletteColor(style.colors[i].c)
term.setPaletteColor(style.colors[i].c, r, g, b)
end
reset_term()
if not status then
println("configurator error: " .. error)
end
return status, error, tool_ctl.launch_startup
end
return configurator

View File

@ -1,12 +1,790 @@
local comms = require("scada-common.comms")
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local types = require("scada-common.types")
local themes = require("graphics.themes")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local apisessions = require("coordinator.session.apisessions")
local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE
local CRDN_TYPE = comms.CRDN_TYPE
local UNIT_COMMAND = comms.UNIT_COMMAND
local FAC_COMMAND = comms.FAC_COMMAND
local LINK_TIMEOUT = 60.0
local coordinator = {}
-- coordinator communications
coordinator.coord_comms = function ()
local self = {
reactor_struct_cache = nil
---@type crd_config
---@diagnostic disable-next-line: missing-fields
local config = {}
coordinator.config = config
-- load the coordinator configuration<br>
-- status of 0 is OK, 1 is bad config, 2 is bad monitor config
---@return 0|1|2 status, nil|monitors_struct|string monitors (or error message)
function coordinator.load_config()
if not settings.load("/coordinator.settings") then return 1 end
config.UnitCount = settings.get("UnitCount")
config.SpeakerVolume = settings.get("SpeakerVolume")
config.Time24Hour = settings.get("Time24Hour")
config.GreenPuPellet = settings.get("GreenPuPellet")
config.TempScale = settings.get("TempScale")
config.EnergyScale = settings.get("EnergyScale")
config.DisableFlowView = settings.get("DisableFlowView")
config.MainDisplay = settings.get("MainDisplay")
config.FlowDisplay = settings.get("FlowDisplay")
config.UnitDisplays = settings.get("UnitDisplays")
config.SVR_Channel = settings.get("SVR_Channel")
config.CRD_Channel = settings.get("CRD_Channel")
config.PKT_Channel = settings.get("PKT_Channel")
config.SVR_Timeout = settings.get("SVR_Timeout")
config.API_Timeout = settings.get("API_Timeout")
config.TrustedRange = settings.get("TrustedRange")
config.AuthKey = settings.get("AuthKey")
config.LogMode = settings.get("LogMode")
config.LogPath = settings.get("LogPath")
config.LogDebug = settings.get("LogDebug")
config.MainTheme = settings.get("MainTheme")
config.FrontPanelTheme = settings.get("FrontPanelTheme")
config.ColorMode = settings.get("ColorMode")
local cfv = util.new_validator()
cfv.assert_type_int(config.UnitCount)
cfv.assert_range(config.UnitCount, 1, 4)
cfv.assert_type_bool(config.Time24Hour)
cfv.assert_type_bool(config.GreenPuPellet)
cfv.assert_type_int(config.TempScale)
cfv.assert_range(config.TempScale, 1, 4)
cfv.assert_type_int(config.EnergyScale)
cfv.assert_range(config.EnergyScale, 1, 3)
cfv.assert_type_bool(config.DisableFlowView)
cfv.assert_type_table(config.UnitDisplays)
cfv.assert_type_num(config.SpeakerVolume)
cfv.assert_range(config.SpeakerVolume, 0, 3)
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.CRD_Channel)
cfv.assert_channel(config.PKT_Channel)
cfv.assert_type_num(config.SVR_Timeout)
cfv.assert_min(config.SVR_Timeout, 2)
cfv.assert_type_num(config.API_Timeout)
cfv.assert_min(config.API_Timeout, 2)
cfv.assert_type_num(config.TrustedRange)
cfv.assert_min(config.TrustedRange, 0)
cfv.assert_type_str(config.AuthKey)
if type(config.AuthKey) == "string" then
local len = string.len(config.AuthKey)
cfv.assert(len == 0 or len >= 8)
end
cfv.assert_type_int(config.LogMode)
cfv.assert_range(config.LogMode, 0, 1)
cfv.assert_type_str(config.LogPath)
cfv.assert_type_bool(config.LogDebug)
cfv.assert_type_int(config.MainTheme)
cfv.assert_range(config.MainTheme, 1, 2)
cfv.assert_type_int(config.FrontPanelTheme)
cfv.assert_range(config.FrontPanelTheme, 1, 2)
cfv.assert_type_int(config.ColorMode)
cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
-- Monitor Setup
---@class monitors_struct
local monitors = {
main = nil, ---@type Monitor|nil
main_name = "",
flow = nil, ---@type Monitor|nil
flow_name = "",
unit_displays = {}, ---@type Monitor[]
unit_name_map = {} ---@type string[]
}
local mon_cfv = util.new_validator()
-- get all interface names
local names = {}
for iface, _ in pairs(ppm.get_monitor_list()) do table.insert(names, iface) end
local function setup_monitors()
mon_cfv.assert_type_str(config.MainDisplay)
if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end
mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount)
if mon_cfv.valid() then
local w, h, _
if not util.table_contains(names, config.MainDisplay) then
return 2, "Main monitor is not connected."
end
monitors.main = ppm.get_periph(config.MainDisplay)
monitors.main_name = config.MainDisplay
monitors.main.setTextScale(0.5)
w, _ = ppm.monitor_block_size(monitors.main.getSize())
if w ~= 8 then
return 2, util.c("Main monitor width is incorrect (was ", w, ", must be 8).")
end
if not config.DisableFlowView then
if not util.table_contains(names, config.FlowDisplay) then
return 2, "Flow monitor is not connected."
end
monitors.flow = ppm.get_periph(config.FlowDisplay)
monitors.flow_name = config.FlowDisplay
monitors.flow.setTextScale(0.5)
w, _ = ppm.monitor_block_size(monitors.flow.getSize())
if w ~= 8 then
return 2, util.c("Flow monitor width is incorrect (was ", w, ", must be 8).")
end
end
for i = 1, config.UnitCount do
local display = config.UnitDisplays[i]
if type(display) ~= "string" or not util.table_contains(names, display) then
return 2, "Unit " .. i .. " monitor is not connected."
end
monitors.unit_displays[i] = ppm.get_periph(display)
monitors.unit_name_map[i] = display
monitors.unit_displays[i].setTextScale(0.5)
w, h = ppm.monitor_block_size(monitors.unit_displays[i].getSize())
if w ~= 4 or h ~= 4 then
return 2, util.c("Unit ", i, " monitor size is incorrect (was ", w, " by ", h,", must be 4 by 4).")
end
end
else return 2, "Monitor configuration invalid." end
end
if cfv.valid() then
local ok, result, message = pcall(setup_monitors)
assert(ok, util.c("fatal error while trying to verify monitors: ", result))
if result == 2 then return 2, message end
else return 1 end
return 0, monitors
end
-- dmesg print wrapper
---@param message string message
---@param dmesg_tag string tag
---@param working? boolean to use dmesg_working
---@return function? update, function? done
local function log_dmesg(message, dmesg_tag, working)
local colors = {
RENDER = colors.green,
SYSTEM = colors.cyan,
BOOT = colors.blue,
COMMS = colors.purple,
CRYPTO = colors.yellow
}
if working then
return log.dmesg_working(message, dmesg_tag, colors[dmesg_tag])
else
log.dmesg(message, dmesg_tag, colors[dmesg_tag])
end
end
function coordinator.log_render(message) log_dmesg(message, "RENDER") end
function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end
function coordinator.log_boot(message) log_dmesg(message, "BOOT") end
function coordinator.log_comms(message) log_dmesg(message, "COMMS") end
function coordinator.log_crypto(message) log_dmesg(message, "CRYPTO") end
-- log a message for communications connecting, providing access to progress indication control functions
---@nodiscard
---@param message string
---@return function update, function done
function coordinator.log_comms_connecting(message)
local update, done = log_dmesg(message, "COMMS", true)
---@cast update function
---@cast done function
return update, done
end
-- coordinator communications
---@nodiscard
---@param version string coordinator version
---@param nic nic network interface device
---@param sv_watchdog watchdog
function coordinator.comms(version, nic, sv_watchdog)
local self = {
sv_linked = false,
sv_addr = comms.BROADCAST,
sv_seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
sv_r_seq_num = nil, ---@type nil|integer
sv_config_err = false,
last_est_ack = ESTABLISH_ACK.ALLOW,
last_api_est_acks = {},
est_start = 0,
est_last = 0,
est_tick_waiting = nil,
est_task_done = nil
}
comms.set_trusted_range(config.TrustedRange)
-- configure network channels
nic.closeAll()
nic.open(config.CRD_Channel)
-- pass config to apisessions
apisessions.init(nic, config)
-- PRIVATE FUNCTIONS --
-- send a packet to the supervisor
---@param msg_type MGMT_TYPE|CRDN_TYPE
---@param msg table
local function _send_sv(protocol, msg_type, msg)
local s_pkt = comms.scada_packet()
local pkt ---@type mgmt_packet|crdn_packet
if protocol == PROTOCOL.SCADA_MGMT then
pkt = comms.mgmt_packet()
elseif protocol == PROTOCOL.SCADA_CRDN then
pkt = comms.crdn_packet()
else
return
end
pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable())
nic.transmit(config.SVR_Channel, config.CRD_Channel, s_pkt)
self.sv_seq_num = self.sv_seq_num + 1
end
-- send an API establish request response
---@param packet scada_packet
---@param ack ESTABLISH_ACK
---@param data any?
local function _send_api_establish_ack(packet, ack, data)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data })
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt)
self.last_api_est_acks[packet.src_addr()] = ack
end
-- attempt connection establishment
local function _send_establish()
self.sv_r_seq_num = nil
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRD })
end
-- keep alive ack
---@param srv_time integer
local function _send_keep_alive_ack(srv_time)
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
end
-- PUBLIC FUNCTIONS --
---@class coord_comms
local public = {}
-- try to connect to the supervisor if not already linked
---@param abort boolean? true to print out cancel info if not linked (use on program terminate)
---@return boolean ok, boolean start_ui
function public.try_connect(abort)
local ok = true
local start_ui = false
if not self.sv_linked then
if self.est_tick_waiting == nil then
self.est_start = os.clock()
self.est_last = self.est_start
self.est_tick_waiting, self.est_task_done =
coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SVR_Channel)
_send_establish()
else
self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (os.clock() - self.est_start)))
end
if abort or (os.clock() - self.est_start) >= LINK_TIMEOUT then
self.est_task_done(false)
if abort then
coordinator.log_comms("supervisor connection attempt cancelled by user")
elseif self.sv_config_err then
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
elseif not self.sv_linked then
if self.last_est_ack == ESTABLISH_ACK.DENY then
coordinator.log_comms("supervisor connection attempt denied")
elseif self.last_est_ack == ESTABLISH_ACK.COLLISION then
coordinator.log_comms("supervisor connection failed due to collision")
elseif self.last_est_ack == ESTABLISH_ACK.BAD_VERSION then
coordinator.log_comms("supervisor connection failed due to version mismatch")
else
coordinator.log_comms("supervisor connection failed with no valid response")
end
end
ok = false
elseif self.sv_config_err then
self.est_task_done(false)
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
ok = false
elseif (os.clock() - self.est_last) > 1.0 then
_send_establish()
self.est_last = os.clock()
end
elseif self.est_tick_waiting ~= nil then
self.est_task_done(true)
self.est_tick_waiting = nil
self.est_task_done = nil
start_ui = true
end
return ok, start_ui
end
-- close the connection to the server
function public.close()
sv_watchdog.cancel()
self.sv_addr = comms.BROADCAST
self.sv_linked = false
self.sv_r_seq_num = nil
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {})
end
-- send the resume ready state to the supervisor
---@param mode PROCESS process control mode
---@param burn_target number burn rate target
---@param charge_target number charge level target
---@param gen_target number generation rate target
---@param limits number[] unit burn rate limits
function public.send_ready(mode, burn_target, charge_target, gen_target, limits)
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.PROCESS_READY, {
mode, burn_target, charge_target, gen_target, limits
})
end
-- send a facility command
---@param cmd FAC_COMMAND command
---@param option any? optional option options for the optional options (like waste mode)
function public.send_fac_command(cmd, option)
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, { cmd, option })
end
-- send the auto process control configuration with a start command
---@param mode PROCESS process control mode
---@param burn_target number burn rate target
---@param charge_target number charge level target
---@param gen_target number generation rate target
---@param limits number[] unit burn rate limits
function public.send_auto_start(mode, burn_target, charge_target, gen_target, limits)
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
FAC_COMMAND.START, mode, burn_target, charge_target, gen_target, limits
})
end
-- send a unit command
---@param cmd UNIT_COMMAND command
---@param unit integer unit ID
---@param option any? optional option options for the optional options (like burn rate)
function public.send_unit_command(cmd, unit, option)
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
end
-- parse a packet
---@param side string
---@param sender integer
---@param reply_to integer
---@param message any
---@param distance integer
---@return mgmt_frame|crdn_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance)
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
local pkt = nil
if s_pkt then
-- get as SCADA management packet
if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get()
end
-- get as coordinator packet
elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then
local crdn_pkt = comms.crdn_packet()
if crdn_pkt.decode(s_pkt) then
pkt = crdn_pkt.get()
end
else
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
end
end
return pkt
end
-- handle a packet
---@param packet mgmt_frame|crdn_frame|nil
---@return boolean close_ui
function public.handle_packet(packet)
local was_linked = self.sv_linked
if packet ~= nil then
local l_chan = packet.scada_frame.local_channel()
local r_chan = packet.scada_frame.remote_channel()
local src_addr = packet.scada_frame.src_addr()
local protocol = packet.scada_frame.protocol()
if l_chan ~= config.CRD_Channel then
log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == config.PKT_Channel then
if not self.sv_linked then
log.debug("discarding pocket API packet before linked to supervisor")
elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
-- look for an associated session
local session = apisessions.find_session(src_addr)
-- coordinator packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug("discarding SCADA_CRDN packet without a known session")
end
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- look for an associated session
local session = apisessions.find_session(src_addr)
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
-- validate packet and continue
if packet.length == 4 then
local comms_v = util.strval(packet.data[1])
local firmware_v = util.strval(packet.data[2])
local dev_type = packet.data[3]
local api_v = util.strval(packet.data[4])
if comms_v ~= comms.version then
if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping API establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif api_v ~= comms.api_version then
if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_API_VERSION then
log.info(util.c("dropping API establish packet with incorrect api version v", api_v, " (expected v", comms.api_version, ")"))
end
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_API_VERSION)
elseif dev_type == DEVICE_TYPE.PKT then
-- pocket linking request
local id = apisessions.establish_session(src_addr, packet.scada_frame.seq_num(), firmware_v)
coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id))
local conf = iocontrol.get_db().facility.conf
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW, { conf.num_units, conf.cooling })
else
log.debug(util.c("API_ESTABLISH: illegal establish packet for device ", dev_type, " on pocket channel"))
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug("invalid establish packet (on API listening channel)")
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding pocket SCADA_MGMT packet without a known session from computer ", src_addr))
end
else
log.debug("illegal packet type " .. protocol .. " on pocket channel", true)
end
elseif r_chan == config.SVR_Channel then
-- check sequence number
if self.sv_r_seq_num == nil then
self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.sv_r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: next = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return false
elseif self.sv_linked and src_addr ~= self.sv_addr then
log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?")
return false
else
self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
end
-- feed watchdog on valid sequence number
sv_watchdog.feed()
-- handle packet
if protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
if self.sv_linked then
if packet.type == CRDN_TYPE.INITIAL_BUILDS then
if packet.length == 2 then
-- record builds
local fac_builds = iocontrol.record_facility_builds(packet.data[1])
local unit_builds = iocontrol.record_unit_builds(packet.data[2])
if fac_builds and unit_builds then
-- acknowledge receipt of builds
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.INITIAL_BUILDS, {})
else
log.debug("received invalid INITIAL_BUILDS packet")
end
else
log.debug("INITIAL_BUILDS packet length mismatch")
end
elseif packet.type == CRDN_TYPE.FAC_BUILDS then
if packet.length == 1 then
-- record facility builds
if iocontrol.record_facility_builds(packet.data[1]) then
-- acknowledge receipt of builds
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_BUILDS, {})
else
log.debug("received invalid FAC_BUILDS packet")
end
else
log.debug("FAC_BUILDS packet length mismatch")
end
elseif packet.type == CRDN_TYPE.FAC_STATUS then
-- update facility status
if not iocontrol.update_facility_status(packet.data) then
log.debug("received invalid FAC_STATUS packet")
end
elseif packet.type == CRDN_TYPE.FAC_CMD then
-- facility command acknowledgement
if packet.length >= 2 then
local cmd = packet.data[1]
local ack = packet.data[2] == true
if cmd == FAC_COMMAND.SCRAM_ALL then
process.fac_ack(cmd, ack)
elseif cmd == FAC_COMMAND.STOP then
process.fac_ack(cmd, ack)
elseif cmd == FAC_COMMAND.START then
if packet.length == 7 then
process.start_ack_handle({ table.unpack(packet.data, 2) })
else
log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch")
end
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
process.fac_ack(cmd, ack)
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
process.waste_ack_handle(packet.data[2])
elseif cmd == FAC_COMMAND.SET_PU_FB then
process.pu_fb_ack_handle(packet.data[2])
elseif cmd == FAC_COMMAND.SET_SPS_LP then
process.sps_lp_ack_handle(packet.data[2])
else
log.debug(util.c("received facility command ack with unknown command ", cmd))
end
else
log.debug("SCADA_CRDN facility command ack packet length mismatch")
end
elseif packet.type == CRDN_TYPE.UNIT_BUILDS then
-- record builds
if packet.length == 1 then
if iocontrol.record_unit_builds(packet.data[1]) then
-- acknowledge receipt of builds
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_BUILDS, {})
else
log.debug("received invalid UNIT_BUILDS packet")
end
else
log.debug("UNIT_BUILDS packet length mismatch")
end
elseif packet.type == CRDN_TYPE.UNIT_STATUSES then
-- update statuses
if not iocontrol.update_unit_statuses(packet.data) then
log.debug("received invalid UNIT_STATUSES packet")
end
elseif packet.type == CRDN_TYPE.UNIT_CMD then
-- unit command acknowledgement
if packet.length == 3 then
local cmd = packet.data[1]
local unit_id = packet.data[2]
local ack = packet.data[3] == true
local unit = iocontrol.get_db().units[unit_id]
if unit ~= nil then
if cmd == UNIT_COMMAND.SCRAM then
process.unit_ack(unit_id, cmd, ack)
elseif cmd == UNIT_COMMAND.START then
process.unit_ack(unit_id, cmd, ack)
elseif cmd == UNIT_COMMAND.RESET_RPS then
process.unit_ack(unit_id, cmd, ack)
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
process.unit_ack(unit_id, cmd, ack)
else
log.debug(util.c("received unsupported unit command ack for command ", cmd))
end
else
log.debug(util.c("received unit command ack with unknown unit ", unit_id))
end
else
log.debug("SCADA_CRDN unit command ack packet length mismatch")
end
else
log.debug("received unknown SCADA_CRDN packet type " .. packet.type)
end
else
log.debug("discarding SCADA_CRDN packet before linked")
end
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
if self.sv_linked then
if packet.type == MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back
if packet.length == 1 then
local timestamp = packet.data[1]
local trip_time = util.time() - timestamp
if trip_time > 750 then
log.warning("coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
end
-- log.debug("coordinator RTT = " .. trip_time .. "ms")
iocontrol.get_db().facility.ps.publish("sv_ping", trip_time)
_send_keep_alive_ack(timestamp)
else
log.debug("SCADA keep alive packet length mismatch")
end
elseif packet.type == MGMT_TYPE.CLOSE then
-- handle session close
sv_watchdog.cancel()
self.sv_addr = comms.BROADCAST
self.sv_linked = false
self.sv_r_seq_num = nil
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
log.info("server connection closed by remote host")
else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
end
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- connection with supervisor established
if packet.length == 2 then
local est_ack = packet.data[1]
local sv_config = packet.data[2]
if est_ack == ESTABLISH_ACK.ALLOW then
-- reset to disconnected before validating
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
if type(sv_config) == "table" and #sv_config == 2 then
-- get configuration
---@class facility_conf
local conf = {
num_units = sv_config[1], ---@type integer
cooling = sv_config[2] ---@type sv_cooling_conf
}
if conf.num_units == config.UnitCount then
-- init io controller
iocontrol.init(conf, public, config.TempScale, config.EnergyScale)
self.sv_addr = src_addr
self.sv_linked = true
self.sv_config_err = false
iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED)
else
self.sv_config_err = true
log.warning("supervisor config's number of units don't match coordinator's config, establish failed")
end
else
log.debug("invalid supervisor configuration table received, establish failed")
end
else
log.debug("SCADA_MGMT establish packet reply (len = 2) unsupported")
end
self.last_est_ack = est_ack
elseif packet.length == 1 then
local est_ack = packet.data[1]
if est_ack == ESTABLISH_ACK.DENY then
if self.last_est_ack ~= est_ack then
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DENIED)
log.info("supervisor connection denied")
end
elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.last_est_ack ~= est_ack then
iocontrol.fp_link_state(types.PANEL_LINK_STATE.COLLISION)
log.warning("supervisor connection denied due to collision")
end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.last_est_ack ~= est_ack then
iocontrol.fp_link_state(types.PANEL_LINK_STATE.BAD_VERSION)
log.warning("supervisor comms version mismatch")
end
else
log.debug("SCADA_MGMT establish packet reply (len = 1) unsupported")
end
self.last_est_ack = est_ack
else
log.debug("SCADA_MGMT establish packet length mismatch")
end
else
log.debug("discarding non-link SCADA_MGMT packet before linked")
end
else
log.debug("illegal packet type " .. protocol .. " on supervisor listening channel", true)
end
else
log.debug("received packet for unknown channel " .. r_chan, true)
end
end
return was_linked and not self.sv_linked
end
-- check if the coordinator is still linked to the supervisor
---@nodiscard
function public.is_linked() return self.sv_linked end
return public
end
return coordinator

1317
coordinator/iocontrol.lua Normal file

File diff suppressed because it is too large Load Diff

550
coordinator/process.lua Normal file
View File

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

524
coordinator/renderer.lua Normal file
View File

@ -0,0 +1,524 @@
--
-- Graphics Rendering Control
--
local log = require("scada-common.log")
local util = require("scada-common.util")
local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local pgi = require("coordinator.ui.pgi")
local flow_view = require("coordinator.ui.layout.flow_view")
local panel_view = require("coordinator.ui.layout.front_panel")
local main_view = require("coordinator.ui.layout.main_view")
local unit_view = require("coordinator.ui.layout.unit_view")
local core = require("graphics.core")
local flasher = require("graphics.flasher")
local DisplayBox = require("graphics.elements.DisplayBox")
local log_render = coordinator.log_render
---@class coord_renderer
local renderer = {}
-- render engine
local engine = {
color_mode = 1, ---@type COLOR_MODE
monitors = nil, ---@type monitors_struct|nil
dmesg_window = nil, ---@type Window|nil
ui_ready = false,
fp_ready = false,
ui = {
front_panel = nil, ---@type DisplayBox|nil
main_display = nil, ---@type DisplayBox|nil
flow_display = nil, ---@type DisplayBox|nil
unit_displays = {} ---@type (DisplayBox|nil)[]
},
disable_flow_view = false
}
-- init a display to the "default", but set text scale to 0.5
---@param monitor Monitor monitor
local function _init_display(monitor)
monitor.setTextScale(0.5)
monitor.setTextColor(colors.white)
monitor.setBackgroundColor(colors.black)
monitor.clear()
monitor.setCursorPos(1, 1)
-- set overridden colors
for i = 1, #style.theme.colors do
monitor.setPaletteColor(style.theme.colors[i].c, style.theme.colors[i].hex)
end
-- apply color mode
local c_mode_overrides = style.theme.color_modes[engine.color_mode]
for i = 1, #c_mode_overrides do
monitor.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
end
end
-- print out that the monitor is too small
---@param monitor Monitor monitor
local function _print_too_small(monitor)
monitor.setCursorPos(1, 1)
monitor.setBackgroundColor(colors.black)
monitor.setTextColor(colors.red)
monitor.clear()
monitor.write("monitor too small")
end
-- apply renderer configurations
---@param config crd_config
function renderer.configure(config)
style.set_themes(config.MainTheme, config.FrontPanelTheme, config.ColorMode)
engine.color_mode = config.ColorMode
engine.disable_flow_view = config.DisableFlowView
end
-- link to the monitor peripherals
---@param monitors monitors_struct
function renderer.set_displays(monitors)
engine.monitors = monitors
-- report to front panel as connected
iocontrol.fp_monitor_state("main", engine.monitors.main ~= nil)
iocontrol.fp_monitor_state("flow", engine.monitors.flow ~= nil)
for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end
end
-- init all displays in use by the renderer
function renderer.init_displays()
-- init main and flow monitors
_init_display(engine.monitors.main)
if not engine.disable_flow_view then _init_display(engine.monitors.flow) end
-- init unit displays
for _, monitor in ipairs(engine.monitors.unit_displays) do
_init_display(monitor)
end
-- init terminal
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
-- set overridden colors
for i = 1, #style.fp_theme.colors do
term.setPaletteColor(style.fp_theme.colors[i].c, style.fp_theme.colors[i].hex)
end
-- apply color mode
local c_mode_overrides = style.fp_theme.color_modes[engine.color_mode]
for i = 1, #c_mode_overrides do
term.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
end
end
-- initialize the dmesg output window
function renderer.init_dmesg()
local disp_w, disp_h = engine.monitors.main.getSize()
engine.dmesg_window = window.create(engine.monitors.main, 1, 1, disp_w, disp_h)
log.direct_dmesg(engine.dmesg_window)
end
-- try to start the front panel
---@return boolean success, any error_msg
function renderer.try_start_fp()
local status, msg = true, nil
if not engine.fp_ready then
-- show front panel view on terminal
status, msg = pcall(function ()
engine.ui.front_panel = DisplayBox{window=term.current(),fg_bg=style.fp.root}
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
end)
if status then
-- start flasher callback task and report ready
flasher.run()
engine.fp_ready = true
else
-- report fail and close front panel
msg = core.extract_assert_msg(msg)
renderer.close_fp()
end
end
return status, msg
end
-- close out the front panel
function renderer.close_fp()
if engine.fp_ready then
if not engine.ui_ready then
-- stop blinking indicators
flasher.clear()
end
-- disable PGI
pgi.unlink()
-- hide to stop animation callbacks and clear root UI elements
engine.ui.front_panel.hide()
engine.ui.front_panel = nil
engine.fp_ready = false
-- restore colors
for i = 1, #style.fp_theme.colors do
local r, g, b = term.nativePaletteColor(style.fp_theme.colors[i].c)
term.setPaletteColor(style.fp_theme.colors[i].c, r, g, b)
end
-- reset terminal
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
end
end
-- try to start the main GUI
---@return boolean success, any error_msg
function renderer.try_start_ui()
local status, msg = true, nil
if not engine.ui_ready then
-- hide dmesg
engine.dmesg_window.setVisible(false)
status, msg = pcall(function ()
-- show main view on main monitor
if engine.monitors.main ~= nil then
engine.ui.main_display = DisplayBox{window=engine.monitors.main,fg_bg=style.root}
main_view(engine.ui.main_display)
util.nop()
end
-- show flow view on flow monitor
if engine.monitors.flow ~= nil then
engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root}
flow_view(engine.ui.flow_display)
util.nop()
end
-- show unit views on unit displays
for idx, display in pairs(engine.monitors.unit_displays) do
engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
unit_view(engine.ui.unit_displays[idx], idx)
util.nop()
end
end)
if status then
-- start flasher callback task and report ready
flasher.run()
engine.ui_ready = true
else
-- report fail and close ui
msg = core.extract_assert_msg(msg)
renderer.close_ui()
end
end
return status, msg
end
-- close out the UI
function renderer.close_ui()
if not engine.fp_ready then
-- stop blinking indicators
flasher.clear()
end
-- delete element trees
if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end
if engine.ui.flow_display ~= nil then engine.ui.flow_display.delete() end
for _, display in pairs(engine.ui.unit_displays) do display.delete() end
-- report ui as not ready
engine.ui_ready = false
-- clear root UI elements
engine.ui.main_display = nil
engine.ui.flow_display = nil
engine.ui.unit_displays = {}
-- clear unit monitors
for _, monitor in ipairs(engine.monitors.unit_displays) do monitor.clear() end
if not engine.disable_flow_view then
-- clear flow monitor
engine.monitors.flow.clear()
end
-- re-draw dmesg
engine.dmesg_window.setVisible(true)
engine.dmesg_window.redraw()
end
-- is the front panel ready?
---@nodiscard
---@return boolean ready
function renderer.fp_ready() return engine.fp_ready end
-- is the UI ready?
---@nodiscard
---@return boolean ready
function renderer.ui_ready() return engine.ui_ready end
-- handle a monitor peripheral being disconnected
---@param device Monitor monitor
---@return boolean is_used if the monitor is one of the configured monitors
function renderer.handle_disconnect(device)
local is_used = false
if not engine.monitors then return false end
if engine.monitors.main == device then
if engine.ui.main_display ~= nil then
-- delete element tree and clear root UI elements
engine.ui.main_display.delete()
end
is_used = true
engine.monitors.main = nil
engine.ui.main_display = nil
iocontrol.fp_monitor_state("main", false)
elseif engine.monitors.flow == device then
if engine.ui.flow_display ~= nil then
-- delete element tree and clear root UI elements
engine.ui.flow_display.delete()
end
is_used = true
engine.monitors.flow = nil
engine.ui.flow_display = nil
iocontrol.fp_monitor_state("flow", false)
else
for idx, monitor in pairs(engine.monitors.unit_displays) do
if monitor == device then
if engine.ui.unit_displays[idx] ~= nil then
engine.ui.unit_displays[idx].delete()
end
is_used = true
engine.monitors.unit_displays[idx] = nil
engine.ui.unit_displays[idx] = nil
iocontrol.fp_monitor_state(idx, false)
break
end
end
end
return is_used
end
-- handle a monitor peripheral being reconnected
---@param name string monitor name
---@param device Monitor monitor
---@return boolean is_used if the monitor is one of the configured monitors
function renderer.handle_reconnect(name, device)
local is_used = false
if not engine.monitors then return false end
-- note: handle_resize is a more adaptive way of re-initializing a connected monitor
-- since it can handle a monitor being reconnected that isn't the right size
if engine.monitors.main_name == name then
is_used = true
engine.monitors.main = device
renderer.handle_resize(name)
elseif engine.monitors.flow_name == name then
is_used = true
engine.monitors.flow = device
renderer.handle_resize(name)
else
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
if monitor == name then
is_used = true
engine.monitors.unit_displays[idx] = device
renderer.handle_resize(name)
break
end
end
end
return is_used
end
-- handle a monitor being resized<br>
-- returns if this monitor is assigned + if the assigned screen still fits
---@param name string monitor name
---@return boolean is_used, boolean is_ok
function renderer.handle_resize(name)
local is_used = false
local is_ok = true
local ui = engine.ui
if not engine.monitors then return false, false end
if engine.monitors.main_name == name and engine.monitors.main then
local device = engine.monitors.main ---@type Monitor
-- this is necessary if the bottom left block was broken and on reconnect
_init_display(device)
is_used = true
-- resize dmesg window if needed, but don't make it thinner
local disp_w, disp_h = engine.monitors.main.getSize()
local dmsg_w, _ = engine.dmesg_window.getSize()
engine.dmesg_window.reposition(1, 1, math.max(disp_w, dmsg_w), disp_h, engine.monitors.main)
if ui.main_display then
ui.main_display.delete()
ui.main_display = nil
end
iocontrol.fp_monitor_state("main", true)
engine.dmesg_window.setVisible(not engine.ui_ready)
if engine.ui_ready then
local draw_start = util.time_ms()
local ok = pcall(function ()
ui.main_display = DisplayBox{window=device,fg_bg=style.root}
main_view(ui.main_display)
end)
if ok then
log_render("main view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
else
if ui.main_display then
ui.main_display.delete()
ui.main_display = nil
end
_print_too_small(device)
iocontrol.fp_monitor_state("main", false)
is_ok = false
end
else engine.dmesg_window.redraw() end
elseif engine.monitors.flow_name == name and engine.monitors.flow then
local device = engine.monitors.flow ---@type Monitor
-- this is necessary if the bottom left block was broken and on reconnect
_init_display(device)
is_used = true
if ui.flow_display then
ui.flow_display.delete()
ui.flow_display = nil
end
iocontrol.fp_monitor_state("flow", true)
if engine.ui_ready then
local draw_start = util.time_ms()
local ok = pcall(function ()
ui.flow_display = DisplayBox{window=device,fg_bg=style.root}
flow_view(ui.flow_display)
end)
if ok then
log_render("flow view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
else
if ui.flow_display then
ui.flow_display.delete()
ui.flow_display = nil
end
_print_too_small(device)
iocontrol.fp_monitor_state("flow", false)
is_ok = false
end
end
else
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
local device = engine.monitors.unit_displays[idx]
if monitor == name and device then
-- this is necessary if the bottom left block was broken and on reconnect
_init_display(device)
is_used = true
if ui.unit_displays[idx] then
ui.unit_displays[idx].delete()
ui.unit_displays[idx] = nil
end
iocontrol.fp_monitor_state(idx, true)
if engine.ui_ready then
local draw_start = util.time_ms()
local ok = pcall(function ()
ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
unit_view(ui.unit_displays[idx], idx)
end)
if ok then
log_render("unit " .. idx .. " view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
else
if ui.unit_displays[idx] then
ui.unit_displays[idx].delete()
ui.unit_displays[idx] = nil
end
_print_too_small(device)
iocontrol.fp_monitor_state(idx, false)
is_ok = false
end
end
break
end
end
end
return is_used, is_ok
end
-- handle a touch event
---@param event mouse_interaction|nil
function renderer.handle_mouse(event)
if event ~= nil then
if engine.fp_ready and event.monitor == "terminal" then
engine.ui.front_panel.handle_mouse(event)
elseif engine.ui_ready then
if event.monitor == engine.monitors.main_name then
if engine.ui.main_display then engine.ui.main_display.handle_mouse(event) end
elseif event.monitor == engine.monitors.flow_name then
if engine.ui.flow_display then engine.ui.flow_display.handle_mouse(event) end
else
for id, monitor in ipairs(engine.monitors.unit_name_map) do
local display = engine.ui.unit_displays[id]
if event.monitor == monitor and display then
if display then display.handle_mouse(event) end
end
end
end
end
end
end
return renderer

View File

@ -0,0 +1,178 @@
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local pocket = require("coordinator.session.pocket")
local apisessions = {}
local self = {
nic = nil, ---@type nic
config = nil, ---@type crd_config
next_id = 0,
sessions = {} ---@type pkt_session_struct[]
}
-- PRIVATE FUNCTIONS --
-- handle a session output queue
---@param session pkt_session_struct
local function _api_handle_outq(session)
-- record handler start time
local handle_start = util.time()
-- process output queue
while session.out_queue.ready() do
-- get a new message to process
local msg = session.out_queue.pop()
if msg ~= nil then
if msg.qtype == mqueue.TYPE.PACKET then
-- handle a packet to be sent
self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message)
elseif msg.qtype == mqueue.TYPE.COMMAND then
-- handle instruction/notification
elseif msg.qtype == mqueue.TYPE.DATA then
-- instruction/notification with body
end
end
-- max 100ms spent processing queue
if util.time() - handle_start > 100 then
log.warning("API: out queue handler exceeded 100ms queue process limit")
log.warning(util.c("API: offending session: ", session))
break
end
end
end
-- cleanly close a session
---@param session pkt_session_struct
local function _shutdown(session)
session.open = false
session.instance.close()
-- send packets in out queue (namely the close packet)
while session.out_queue.ready() do
local msg = session.out_queue.pop()
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then
self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message)
end
end
log.debug(util.c("API: closed session ", session))
end
-- PUBLIC FUNCTIONS --
-- initialize apisessions
---@param nic nic network interface
---@param config crd_config coordinator config
function apisessions.init(nic, config)
self.nic = nic
self.config = config
end
-- find a session by remote port
---@nodiscard
---@param source_addr integer
---@return pkt_session_struct|nil
function apisessions.find_session(source_addr)
for i = 1, #self.sessions do
if self.sessions[i].s_addr == source_addr then return self.sessions[i] end
end
return nil
end
-- establish a new API session
---@nodiscard
---@param source_addr integer pocket computer ID
---@param i_seq_num integer initial (most recent) sequence number
---@param version string pocket version
---@return integer session_id
function apisessions.establish_session(source_addr, i_seq_num, version)
---@class pkt_session_struct
local pkt_s = {
open = true,
version = version,
s_addr = source_addr,
in_queue = mqueue.new(),
out_queue = mqueue.new(),
instance = nil ---@type pkt_session
}
local id = self.next_id
pkt_s.instance = pocket.new_session(id, source_addr, i_seq_num, pkt_s.in_queue, pkt_s.out_queue, self.config.API_Timeout)
table.insert(self.sessions, pkt_s)
local mt = {
---@param s pkt_session_struct
__tostring = function (s) return util.c("PKT [", id, "] (@", s.s_addr, ")") end
}
setmetatable(pkt_s, mt)
iocontrol.fp_pkt_connected(id, version, source_addr)
log.debug(util.c("API: established new session: ", pkt_s))
self.next_id = id + 1
-- success
return pkt_s.instance.get_id()
end
-- attempt to identify which session's watchdog timer fired
---@param timer_event number
function apisessions.check_all_watchdogs(timer_event)
for i = 1, #self.sessions do
local session = self.sessions[i]
if session.open then
local triggered = session.instance.check_wd(timer_event)
if triggered then
log.debug(util.c("API: watchdog closing session ", session, "..."))
_shutdown(session)
end
end
end
end
-- iterate all the API sessions
function apisessions.iterate_all()
for i = 1, #self.sessions do
local session = self.sessions[i]
if session.open and session.instance.iterate() then
_api_handle_outq(session)
else
session.open = false
end
end
end
-- delete all closed sessions
function apisessions.free_all_closed()
local f = function (session) return session.open end
---@param session pkt_session_struct
local on_delete = function (session)
log.debug(util.c("API: free'ing closed session ", session))
end
util.filter_table(self.sessions, f, on_delete)
end
-- close all open connections
function apisessions.close_all()
for i = 1, #self.sessions do
local session = self.sessions[i]
if session.open then _shutdown(session) end
end
apisessions.free_all_closed()
end
return apisessions

View File

@ -0,0 +1,567 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local pocket = {}
local PROTOCOL = comms.PROTOCOL
local CRDN_TYPE = comms.CRDN_TYPE
local MGMT_TYPE = comms.MGMT_TYPE
local FAC_COMMAND = comms.FAC_COMMAND
local UNIT_COMMAND = comms.UNIT_COMMAND
local AUTO_GROUP = types.AUTO_GROUP
local WASTE_MODE = types.WASTE_MODE
-- retry time constants in ms
-- local INITIAL_WAIT = 1500
-- local RETRY_PERIOD = 1000
local API_S_CMDS = {
}
local API_S_DATA = {
}
pocket.API_S_CMDS = API_S_CMDS
pocket.API_S_DATA = API_S_DATA
local PERIODICS = {
KEEP_ALIVE = 2000
}
-- pocket API session
---@nodiscard
---@param id integer session ID
---@param s_addr integer device source address
---@param i_seq_num integer initial sequence number
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
local log_tag = "pkt_session(" .. id .. "): "
local self = {
-- connection properties
seq_num = i_seq_num + 2, -- next after the establish approval was sent
r_seq_num = i_seq_num + 1,
connected = true,
conn_watchdog = util.new_watchdog(timeout),
last_rtt = 0,
-- process accessor handle
proc_handle = process.create_handle(),
-- periodic messages
periodics = {
last_update = 0,
keep_alive = 0
},
-- when to next retry one of these requests
retry_times = {
},
-- command acknowledgements
acks = {
},
-- session database
---@class api_db
sDB = {
}
}
---@class pkt_session
local public = {}
-- mark this pocket session as closed, stop watchdog
local function _close()
self.conn_watchdog.cancel()
self.connected = false
iocontrol.fp_pkt_disconnected(id)
end
-- send a CRDN packet
---@param msg_type CRDN_TYPE
---@param msg table
local function _send(msg_type, msg)
local s_pkt = comms.scada_packet()
local c_pkt = comms.crdn_packet()
c_pkt.make(msg_type, msg)
s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable())
out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1
end
-- send a SCADA management packet
---@param msg_type MGMT_TYPE
---@param msg table
local function _send_mgmt(msg_type, msg)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg)
s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1
end
-- link callback transmissions
local f_ack = self.proc_handle.fac_ack
f_ack.on_scram = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.SCRAM_ALL, success }) end
f_ack.on_ack_alarms = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.ACK_ALL_ALARMS, success }) end
f_ack.on_start = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.START, success }) end
f_ack.on_stop = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.STOP, success }) end
for u = 1, iocontrol.get_db().facility.num_units do
local u_ack = self.proc_handle.unit_ack[u]
u_ack.on_start = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.START, u, success }) end
u_ack.on_scram = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.SCRAM, u, success }) end
u_ack.on_rps_reset = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.RESET_RPS, u, success }) end
u_ack.on_ack_alarms = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.ACK_ALL_ALARMS, u, success }) end
end
-- handle a packet
---@param pkt mgmt_frame|crdn_frame
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return
else
self.r_seq_num = pkt.scada_frame.seq_num() + 1
end
-- feed watchdog
self.conn_watchdog.feed()
-- process packet
if pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
---@cast pkt crdn_frame
local db = iocontrol.get_db()
-- handle packet by type
if pkt.type == CRDN_TYPE.FAC_CMD then
if pkt.length >= 1 then
local cmd = pkt.data[1]
if cmd == FAC_COMMAND.SCRAM_ALL then
log.info(log_tag .. "FAC SCRAM ALL")
self.proc_handle.fac_scram()
elseif cmd == FAC_COMMAND.STOP then
log.info(log_tag .. "STOP PROCESS CTRL")
self.proc_handle.process_stop()
elseif cmd == FAC_COMMAND.START then
if pkt.length == 6 then
log.info(log_tag .. "START PROCESS CTRL")
self.proc_handle.process_start_remote(pkt.data[2], pkt.data[3], pkt.data[4], pkt.data[5], pkt.data[6])
else
log.debug(log_tag .. "CRDN auto start (with configuration) packet length mismatch")
end
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
log.info(log_tag .. "FAC ACK ALL ALARMS")
self.proc_handle.fac_ack_alarms()
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
if pkt.length == 2 then
log.info(util.c(log_tag, " SET WASTE ", pkt.data[2]))
process.set_process_waste(pkt.data[2])
else
log.debug(log_tag .. "CRDN set waste mode packet length mismatch")
end
elseif cmd == FAC_COMMAND.SET_PU_FB then
if pkt.length == 2 then
log.info(util.c(log_tag, " SET PU FALLBACK ", pkt.data[2]))
process.set_pu_fallback(pkt.data[2] == true)
else
log.debug(log_tag .. "CRDN set pu fallback packet length mismatch")
end
elseif cmd == FAC_COMMAND.SET_SPS_LP then
if pkt.length == 2 then
log.info(util.c(log_tag, " SET SPS LOW POWER ", pkt.data[2]))
process.set_sps_low_power(pkt.data[2] == true)
else
log.debug(log_tag .. "CRDN set sps low power packet length mismatch")
end
else
log.debug(log_tag .. "CRDN facility command unknown")
end
else
log.debug(log_tag .. "CRDN facility command packet length mismatch")
end
elseif pkt.type == CRDN_TYPE.UNIT_CMD then
if pkt.length >= 2 then
-- get command and unit id
local cmd = pkt.data[1]
local uid = pkt.data[2]
-- continue if valid unit id
if util.is_int(uid) and uid > 0 and uid <= #db.units then
if cmd == UNIT_COMMAND.SCRAM then
log.info(util.c(log_tag, "UNIT[", uid, "] SCRAM"))
self.proc_handle.scram(uid)
elseif cmd == UNIT_COMMAND.START then
log.info(util.c(log_tag, "UNIT[", uid, "] START"))
self.proc_handle.start(uid)
elseif cmd == UNIT_COMMAND.RESET_RPS then
log.info(util.c(log_tag, "UNIT[", uid, "] RESET RPS"))
self.proc_handle.reset_rps(uid)
elseif cmd == UNIT_COMMAND.SET_BURN then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") then
log.info(util.c(log_tag, "UNIT[", uid, "] SET BURN ", pkt.data[3]))
process.set_rate(uid, pkt.data[3])
else
log.debug(log_tag .. "CRDN unit command burn rate missing option")
end
elseif cmd == UNIT_COMMAND.SET_WASTE then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and
(pkt.data[3] >= WASTE_MODE.AUTO) and (pkt.data[3] <= WASTE_MODE.MANUAL_ANTI_MATTER) then
log.info(util.c(log_tag, "UNIT[", id, "] SET WASTE ", pkt.data[3]))
process.set_unit_waste(uid, pkt.data[3])
else
log.debug(log_tag .. "CRDN unit command set waste missing/invalid option")
end
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
log.info(util.c(log_tag, "UNIT[", uid, "] ACK ALL ALARMS"))
self.proc_handle.ack_all_alarms(uid)
elseif cmd == UNIT_COMMAND.ACK_ALARM then
elseif cmd == UNIT_COMMAND.RESET_ALARM then
elseif cmd == UNIT_COMMAND.SET_GROUP then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and
(pkt.data[3] >= AUTO_GROUP.MANUAL) and (pkt.data[3] <= AUTO_GROUP.BACKUP) then
log.info(util.c(log_tag, "UNIT[", uid, "] SET GROUP ", pkt.data[3]))
process.set_group(uid, pkt.data[3])
else
log.debug(log_tag .. "CRDN unit set group missing option")
end
else
log.debug(log_tag .. "CRDN unit command unknown")
end
else
log.debug(log_tag .. "CRDN unit command invalid")
end
else
log.debug(log_tag .. "CRDN unit command packet length mismatch")
end
elseif pkt.type == CRDN_TYPE.API_GET_FAC then
local fac = db.facility
local data = {
fac.all_sys_ok,
fac.rtu_count,
fac.radiation,
{ fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated },
{ fac.auto_current_waste_product, fac.auto_pu_fallback_active },
util.table_len(fac.tank_data_tbl),
fac.induction_data_tbl[1] ~= nil, ---@fixme this means nothing
fac.sps_data_tbl[1] ~= nil ---@fixme this means nothing
}
_send(CRDN_TYPE.API_GET_FAC, data)
elseif pkt.type == CRDN_TYPE.API_GET_FAC_DTL then
local fac = db.facility
local mtx_sps = fac.induction_ps_tbl[1]
local units = {}
local tank_statuses = {}
for i = 1, #db.units do
local u = db.units[i]
units[i] = { u.connected, u.annunciator, u.reactor_data, u.tank_data_tbl }
for t = 1, #u.tank_ps_tbl do table.insert(tank_statuses, u.tank_ps_tbl[t].get("computed_status")) end
end
for i = 1, #fac.tank_ps_tbl do table.insert(tank_statuses, fac.tank_ps_tbl[i].get("computed_status")) end
local matrix_data = {
mtx_sps.get("eta_string"),
mtx_sps.get("avg_charge"),
mtx_sps.get("avg_inflow"),
mtx_sps.get("avg_outflow"),
mtx_sps.get("is_charging"),
mtx_sps.get("is_discharging"),
mtx_sps.get("at_max_io")
}
local data = {
fac.all_sys_ok,
fac.rtu_count,
fac.auto_scram,
fac.ascram_status,
tank_statuses,
fac.tank_data_tbl,
fac.induction_ps_tbl[1].get("computed_status") or types.IMATRIX_STATE.OFFLINE,
fac.induction_data_tbl[1],
matrix_data,
fac.sps_ps_tbl[1].get("computed_status") or types.SPS_STATE.OFFLINE,
fac.sps_data_tbl[1],
units
}
_send(CRDN_TYPE.API_GET_FAC_DTL, data)
elseif pkt.type == CRDN_TYPE.API_GET_UNIT then
if pkt.length == 1 and type(pkt.data[1]) == "number" then
local u = db.units[pkt.data[1]]
local statuses = { u.unit_ps.get("computed_status") }
for i = 1, #u.boiler_ps_tbl do table.insert(statuses, u.boiler_ps_tbl[i].get("computed_status")) end
for i = 1, #u.turbine_ps_tbl do table.insert(statuses, u.turbine_ps_tbl[i].get("computed_status")) end
for i = 1, #u.tank_ps_tbl do table.insert(statuses, u.tank_ps_tbl[i].get("computed_status")) end
if u then
local data = {
u.unit_id,
u.connected,
statuses,
u.a_group,
u.alarms,
u.annunciator,
u.reactor_data,
u.boiler_data_tbl,
u.turbine_data_tbl,
u.tank_data_tbl,
u.last_rate_change_ms,
u.turbine_flow_stable
}
_send(CRDN_TYPE.API_GET_UNIT, data)
end
end
elseif pkt.type == CRDN_TYPE.API_GET_CTRL then
local data = {}
for i = 1, #db.units do
local u = db.units[i]
data[i] = {
u.connected,
u.reactor_data.rps_tripped,
u.reactor_data.mek_status.status,
u.reactor_data.mek_status.temp,
u.reactor_data.mek_status.burn_rate,
u.reactor_data.mek_status.act_burn_rate,
u.reactor_data.mek_struct.max_burn,
u.annunciator.AutoControl,
u.a_group
}
end
_send(CRDN_TYPE.API_GET_CTRL, data)
elseif pkt.type == CRDN_TYPE.API_GET_PROC then
local data = {}
local fac = db.facility
local proc = process.get_control_states().process
-- unit data
for i = 1, #db.units do
local u = db.units[i]
data[i] = {
u.reactor_data.mek_status.status,
u.reactor_data.mek_struct.max_burn,
proc.limits[i],
u.auto_ready,
u.auto_degraded,
u.annunciator.AutoControl,
u.a_group
}
end
-- facility data
data[#db.units + 1] = {
fac.status_lines,
{ fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated },
fac.auto_scram,
fac.ascram_status,
{ proc.mode, proc.burn_target, proc.charge_target, proc.gen_target }
}
_send(CRDN_TYPE.API_GET_PROC, data)
elseif pkt.type == CRDN_TYPE.API_GET_WASTE then
local data = {}
local fac = db.facility
local proc = process.get_control_states().process
-- unit data
for i = 1, #db.units do
local u = db.units[i]
data[i] = {
u.waste_mode,
u.waste_product,
u.num_snas,
u.sna_peak_rate,
u.sna_max_rate,
u.sna_out_rate,
u.waste_stats
}
end
local process_rate = 0
if fac.sps_data_tbl[1].state then
process_rate = fac.sps_data_tbl[1].state.process_rate
end
-- facility data
data[#db.units + 1] = {
fac.auto_current_waste_product,
fac.auto_pu_fallback_active,
fac.auto_sps_disabled,
proc.waste_product,
proc.pu_fallback,
proc.sps_low_power,
fac.waste_stats,
fac.sps_ps_tbl[1].get("computed_status") or types.SPS_STATE.OFFLINE,
process_rate
}
_send(CRDN_TYPE.API_GET_WASTE, data)
elseif pkt.type == CRDN_TYPE.API_GET_RAD then
local data = {}
for i = 1, #db.units do data[i] = db.units[i].rad_monitors end
data[#db.units + 1] = db.facility.rad_monitors
_send(CRDN_TYPE.API_GET_RAD, data)
else
log.debug(log_tag .. "handler received unsupported CRDN packet type " .. pkt.type)
end
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
---@cast pkt mgmt_frame
if pkt.type == MGMT_TYPE.KEEP_ALIVE then
-- keep alive reply
if pkt.length == 2 then
local srv_start = pkt.data[1]
-- local api_send = pkt.data[2]
local srv_now = util.time()
self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then
log.warning(log_tag .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
end
-- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "PKT TT = " .. (srv_now - api_send) .. "ms")
iocontrol.fp_pkt_rtt(id, self.last_rtt)
else
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
end
elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session
_close()
elseif pkt.type == MGMT_TYPE.ESTABLISH then
-- something is wrong, kill the session
_close()
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
else
log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end
end
end
-- PUBLIC FUNCTIONS --
-- get the session ID
---@nodiscard
function public.get_id() return id end
-- get the session database
---@nodiscard
function public.get_db() return self.sDB end
-- check if a timer matches this session's watchdog
---@nodiscard
function public.check_wd(timer)
return self.conn_watchdog.is_timer(timer) and self.connected
end
-- close the connection
function public.close()
_close()
_send_mgmt(MGMT_TYPE.CLOSE, {})
log.info(log_tag .. "session closed by server")
end
-- iterate the session
---@nodiscard
---@return boolean connected
function public.iterate()
if self.connected then
------------------
-- handle queue --
------------------
local handle_start = util.time()
while in_queue.ready() and self.connected do
-- get a new message to process
local message = in_queue.pop()
if message ~= nil then
if message.qtype == mqueue.TYPE.PACKET then
-- handle a packet
_handle_packet(message.message)
elseif message.qtype == mqueue.TYPE.COMMAND then
-- handle instruction
elseif message.qtype == mqueue.TYPE.DATA then
-- instruction with body
end
end
-- max 100ms spent processing queue
if util.time() - handle_start > 100 then
log.warning(log_tag .. "exceeded 100ms queue process limit")
break
end
end
-- exit if connection was closed
if not self.connected then
log.info(log_tag .. "session closed by remote host")
return self.connected
end
----------------------
-- update periodics --
----------------------
local elapsed = util.time() - self.periodics.last_update
local periodics = self.periodics
-- keep alive
periodics.keep_alive = periodics.keep_alive + elapsed
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
_send_mgmt(MGMT_TYPE.KEEP_ALIVE, { util.time() })
periodics.keep_alive = 0
end
self.periodics.last_update = util.time()
---------------------
-- attempt retries --
---------------------
-- local rtimes = self.retry_times
end
return self.connected
end
return public
end
return pocket

79
coordinator/sounder.lua Normal file
View File

@ -0,0 +1,79 @@
--
-- Alarm Sounder
--
local audio = require("scada-common.audio")
local log = require("scada-common.log")
---@class sounder
local sounder = {}
local alarm_ctl = {
speaker = nil, ---@type Speaker
volume = 0.5,
stream = audio.new_stream()
}
-- start audio or continue audio on buffer empty
---@return boolean success successfully added buffer to audio output
local function play()
if not alarm_ctl.playing then
alarm_ctl.playing = true
return sounder.continue()
else return true end
end
-- initialize the annunciator alarm system
---@param speaker Speaker speaker peripheral
---@param volume number speaker volume
function sounder.init(speaker, volume)
alarm_ctl.speaker = speaker
alarm_ctl.speaker.stop()
alarm_ctl.volume = volume
alarm_ctl.stream.stop()
audio.generate_tones()
end
-- reconnect the speaker peripheral
---@param speaker Speaker speaker peripheral
function sounder.reconnect(speaker)
alarm_ctl.speaker = speaker
alarm_ctl.playing = false
alarm_ctl.stream.stop()
end
-- set alarm tones
---@param states { [TONE]: boolean } alarm tone commands from supervisor
function sounder.set(states)
-- set tone states
for id = 1, #states do alarm_ctl.stream.set_active(id, states[id]) end
-- re-compute output if needed, then play audio if available
if alarm_ctl.stream.is_recompute_needed() then alarm_ctl.stream.compute_buffer() end
if alarm_ctl.stream.any_active() then play() else sounder.stop() end
end
-- stop all audio and clear output buffer
function sounder.stop()
alarm_ctl.playing = false
alarm_ctl.speaker.stop()
alarm_ctl.stream.stop()
end
-- continue audio on buffer empty
---@return boolean success successfully added buffer to audio output
function sounder.continue()
local success = false
if alarm_ctl.playing then
if alarm_ctl.speaker ~= nil and alarm_ctl.stream.has_next_block() then
success = alarm_ctl.speaker.playAudio(alarm_ctl.stream.get_next_block(), alarm_ctl.volume)
if not success then log.error("SOUNDER: error playing audio") end
end
end
return success
end
return sounder

View File

@ -4,34 +4,273 @@
require("/initenv").init_env()
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local comms = require("scada-common.comms")
local crash = require("scada-common.crash")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local config = require("coordinator.config")
local configure = require("coordinator.configure")
local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol")
local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder")
local threads = require("coordinator.threads")
local COORDINATOR_VERSION = "alpha-v0.1.2"
local COORDINATOR_VERSION = "v1.6.16"
local print = util.print
local println = util.println
local print_ts = util.print_ts
local CHUNK_LOAD_DELAY_S = 30.0
local println = util.println
local println_ts = util.println_ts
log.init("/log.txt", log.MODE.APPEND)
local log_render = coordinator.log_render
local log_sys = coordinator.log_sys
local log_boot = coordinator.log_boot
local log_comms = coordinator.log_comms
local log_crypto = coordinator.log_crypto
----------------------------------------
-- get configuration
----------------------------------------
-- mount connected devices (required for monitor setup)
ppm.mount_all()
local wait_on_load = true
local loaded, monitors = coordinator.load_config()
-- if the computer just started, its chunk may have just loaded (...or the user rebooted)
-- if monitor config failed, maybe an adjacent chunk containing all or part of a monitor has not loaded yet, so keep trying
while wait_on_load and loaded == 2 and os.clock() < CHUNK_LOAD_DELAY_S do
term.clear()
term.setCursorPos(1, 1)
println("There was a monitor configuration problem at boot.\n")
println("Startup will keep trying every 2s in case of chunk load delays.\n")
println(util.sprintf("The configurator will be started in %ds if all attempts fail.\n", math.max(0, CHUNK_LOAD_DELAY_S - os.clock())))
println("(click to skip to the configurator)")
local timer_id = util.start_timer(2)
while true do
local event, param1 = util.pull_event()
if event == "timer" and param1 == timer_id then
-- remount and re-attempt
ppm.mount_all()
loaded, monitors = coordinator.load_config()
break
elseif event == "mouse_click" or event == "terminate" then
wait_on_load = false
break
end
end
end
if loaded ~= 0 then
-- try to reconfigure (user action)
local success, error = configure.configure(loaded, monitors)
if success then
loaded, monitors = coordinator.load_config()
if loaded ~= 0 then
println(util.trinary(loaded == 2, "monitor configuration invalid", "failed to load a valid configuration") .. ", please reconfigure")
return
end
else
println("configuration error: " .. error)
return
end
end
-- passed checks, good now
---@cast monitors monitors_struct
local config = coordinator.config
----------------------------------------
-- log init
----------------------------------------
log.init(config.LogPath, config.LogMode, config.LogDebug)
log.info("========================================")
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
log.info("========================================")
println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<")
-- mount connected devices
ppm.mount_all()
crash.set_env("coordinator", COORDINATOR_VERSION)
crash.dbg_log_env()
local modem = ppm.get_wireless_modem()
----------------------------------------
-- main application
----------------------------------------
-- we need a modem
if modem == nil then
println("please connect a wireless modem")
return
local function main()
----------------------------------------
-- system startup
----------------------------------------
-- log mounts now since mounting was done before logging was ready
ppm.log_mounts()
-- report versions/init fp PSIL
iocontrol.init_fp(COORDINATOR_VERSION, comms.version)
-- init renderer
renderer.configure(config)
renderer.set_displays(monitors)
renderer.init_displays()
renderer.init_dmesg()
-- lets get started!
log.info("monitors ready, dmesg output incoming...")
log_render("displays connected and reset")
log_sys("system start on " .. os.date("%c"))
log_boot("starting " .. COORDINATOR_VERSION)
----------------------------------------
-- memory allocation
----------------------------------------
-- shared memory across threads
---@class crd_shared_memory
local __shared_memory = {
-- time and date format for display
date_format = util.trinary(config.Time24Hour, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y"),
-- coordinator system state flags
---@class crd_state
crd_state = {
fp_ok = false,
ui_ok = true, -- default true, used to abort on fail
link_fail = false,
shutdown = false
},
-- core coordinator devices
crd_dev = {
modem = ppm.get_wireless_modem(),
speaker = ppm.get_device("speaker") ---@type Speaker|nil
},
-- system objects
crd_sys = {
nic = nil, ---@type nic
coord_comms = nil, ---@type coord_comms
conn_watchdog = nil ---@type watchdog
},
-- message queues
q = {
mq_render = mqueue.new()
}
}
local smem_dev = __shared_memory.crd_dev
local smem_sys = __shared_memory.crd_sys
local crd_state = __shared_memory.crd_state
----------------------------------------
-- setup alarm sounder subsystem
----------------------------------------
if smem_dev.speaker == nil then
log_boot("annunciator alarm speaker not found")
println("startup> speaker not found")
log.fatal("no annunciator alarm speaker found")
return
else
local sounder_start = util.time_ms()
log_boot("annunciator alarm speaker connected")
sounder.init(smem_dev.speaker, config.SpeakerVolume)
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
log_sys("annunciator alarm configured")
iocontrol.fp_has_speaker(true)
end
----------------------------------------
-- setup communications
----------------------------------------
-- message authentication init
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
local init_time = network.init_mac(config.AuthKey)
log_crypto("HMAC init took " .. init_time .. "ms")
end
-- get the communications modem
if smem_dev.modem == nil then
log_comms("wireless modem not found")
println("startup> wireless modem not found")
log.fatal("no wireless modem on startup")
return
else
log_comms("wireless modem connected")
iocontrol.fp_has_modem(true)
end
-- create connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.SVR_Timeout)
smem_sys.conn_watchdog.cancel()
log.debug("startup> conn watchdog created")
-- create network interface then setup comms
smem_sys.nic = network.nic(smem_dev.modem)
smem_sys.coord_comms = coordinator.comms(COORDINATOR_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
log.debug("startup> comms init")
log_comms("comms initialized")
----------------------------------------
-- start front panel
----------------------------------------
log_render("starting front panel UI...")
local fp_message
crd_state.fp_ok, fp_message = renderer.try_start_fp()
if not crd_state.fp_ok then
log_render(util.c("front panel UI error: ", fp_message))
println_ts("front panel UI creation failed")
log.fatal(util.c("front panel GUI render failed with error ", fp_message))
return
else log_render("front panel ready") end
----------------------------------------
-- start system
----------------------------------------
-- init threads
local main_thread = threads.thread__main(__shared_memory)
local render_thread = threads.thread__render(__shared_memory)
log.info("startup> completed")
-- run threads
parallel.waitForAll(main_thread.p_exec, render_thread.p_exec)
renderer.close_ui()
renderer.close_fp()
sounder.stop()
log_sys("system shutdown")
if crd_state.link_fail then println_ts("failed to connect to supervisor") end
if not crd_state.ui_ok then println_ts("main UI creation failed") end
-- close on error exit (such as UI error)
if smem_sys.coord_comms.is_linked() then smem_sys.coord_comms.close() end
println_ts("exited")
log.info("exited")
end
if not xpcall(main, crash.handler) then
pcall(renderer.close_ui)
pcall(renderer.close_fp)
pcall(sounder.stop)
crash.exit()
else
log.close()
end

381
coordinator/threads.lua Normal file
View File

@ -0,0 +1,381 @@
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder")
local apisessions = require("coordinator.session.apisessions")
local core = require("graphics.core")
local log_render = coordinator.log_render
local log_sys = coordinator.log_sys
local log_comms = coordinator.log_comms
local threads = {}
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
local MQ__RENDER_CMD = {
START_MAIN_UI = 1,
CLOSE_MAIN_UI = 2
}
local MQ__RENDER_DATA = {
MON_CONNECT = 1,
MON_DISCONNECT = 2,
MON_RESIZE = 3
}
-- main thread
---@nodiscard
---@param smem crd_shared_memory
function threads.thread__main(smem)
---@class parallel_thread
local public = {}
-- execute thread
function public.exec()
iocontrol.fp_rt_status("main", true)
log.debug("main thread start")
local loop_clock = util.new_clock(MAIN_CLOCK)
-- start clock
loop_clock.start()
log_sys("system started successfully")
-- load in from shared memory
local crd_state = smem.crd_state
local nic = smem.crd_sys.nic
local coord_comms = smem.crd_sys.coord_comms
local conn_watchdog = smem.crd_sys.conn_watchdog
-- event loop
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event
if event == "peripheral_detach" then
local type, device = ppm.handle_unmount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
---@cast device Modem
-- we only really care if this is our wireless modem
-- if it is another modem, handle other peripheral losses separately
if nic.is_modem(device) then
nic.disconnect()
log_sys("comms modem disconnected")
local other_modem = ppm.get_wireless_modem()
if other_modem then
log_sys("found another wireless modem, using it for comms")
nic.connect(other_modem)
else
-- close out main UI
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
-- alert user to status
log_sys("awaiting comms modem reconnect...")
iocontrol.fp_has_modem(false)
end
else
log_sys("non-comms modem disconnected")
end
elseif type == "monitor" then
---@cast device Monitor
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, device)
elseif type == "speaker" then
---@cast device Speaker
log_sys("lost alarm sounder speaker")
iocontrol.fp_has_speaker(false)
end
end
elseif event == "peripheral" then
local type, device = ppm.mount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
---@cast device Modem
if device.isWireless() and not nic.is_connected() then
-- reconnected modem
log_sys("comms modem reconnected")
nic.connect(device)
iocontrol.fp_has_modem(true)
elseif device.isWireless() then
log.info("unused wireless modem reconnected")
else
log_sys("wired modem reconnected")
end
elseif type == "monitor" then
---@cast device Monitor
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, { name = param1, device = device })
elseif type == "speaker" then
---@cast device Speaker
log_sys("alarm sounder speaker reconnected")
sounder.reconnect(device)
iocontrol.fp_has_speaker(true)
end
end
elseif event == "monitor_resize" then
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_RESIZE, param1)
elseif event == "timer" then
if loop_clock.is_clock(param1) then
-- main loop tick
-- toggle heartbeat
iocontrol.heartbeat()
-- maintain connection
if nic.is_connected() then
local ok, start_ui = coord_comms.try_connect()
if not ok then
crd_state.link_fail = true
crd_state.shutdown = true
log_sys("supervisor connection failed, shutting down...")
log.fatal("failed to connect to supervisor")
break
elseif start_ui then
log_sys("supervisor connected, dispatching main UI start")
smem.q.mq_render.push_command(MQ__RENDER_CMD.START_MAIN_UI)
end
end
-- iterate sessions and free any closed ones
apisessions.iterate_all()
apisessions.free_all_closed()
-- clear timed out process commands
process.clear_timed_out()
if renderer.ui_ready() then
-- update clock used on main and flow monitors
iocontrol.get_db().facility.ps.publish("date_time", os.date(smem.date_format))
end
loop_clock.start()
elseif conn_watchdog.is_timer(param1) then
-- supervisor watchdog timeout
log_comms("supervisor server timeout")
-- close main UI, connection, and stop sounder
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
coord_comms.close()
sounder.stop()
else
-- a non-clock/main watchdog timer event
-- check API watchdogs
apisessions.check_all_watchdogs(param1)
-- notify timer callback dispatcher
tcd.handle(param1)
end
elseif event == "modem_message" then
-- got a packet
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
-- handle then check if it was a disconnect
if coord_comms.handle_packet(packet) then
log_comms("supervisor closed connection")
-- close main UI, connection, and stop sounder
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
coord_comms.close()
sounder.stop()
end
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
-- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
elseif event == "speaker_audio_empty" then
-- handle speaker buffer emptied
sounder.continue()
end
-- check for termination request or UI crash
if event == "terminate" or ppm.should_terminate() then
crd_state.shutdown = true
log.info("terminate requested, main thread exiting")
elseif not crd_state.ui_ok then
crd_state.shutdown = true
log.info("terminating due to fatal UI error")
end
if crd_state.shutdown then
-- handle closing supervisor connection
coord_comms.try_connect(true)
if coord_comms.is_linked() then
log_comms("closing supervisor connection...")
else crd_state.link_fail = true end
coord_comms.close()
log_comms("supervisor connection closed")
-- handle API sessions
log_comms("closing api sessions...")
apisessions.close_all()
log_comms("api sessions closed")
break
end
end
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
function public.p_exec()
local crd_state = smem.crd_state
while not crd_state.shutdown do
local status, result = pcall(public.exec)
if status == false then
log.fatal(util.strval(result))
end
iocontrol.fp_rt_status("main", false)
-- if status is true, then we are probably exiting, so this won't matter
-- this thread cannot be slept because it will miss events (namely "terminate")
if not crd_state.shutdown then
log.info("main thread restarting now...")
end
end
end
return public
end
-- coordinator renderer thread, tasked with long duration draws
---@nodiscard
---@param smem crd_shared_memory
function threads.thread__render(smem)
---@class parallel_thread
local public = {}
-- execute thread
function public.exec()
iocontrol.fp_rt_status("render", true)
log.debug("render thread start")
-- load in from shared memory
local crd_state = smem.crd_state
local render_queue = smem.q.mq_render
local last_update = util.time()
-- thread loop
while true do
-- check for messages in the message queue
while render_queue.ready() and not crd_state.shutdown do
local msg = render_queue.pop()
if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command
if msg.message == MQ__RENDER_CMD.START_MAIN_UI then
-- stop the UI if it was already started
-- this may occur on a quick supervisor disconnect -> connect
if renderer.ui_ready() then
log_render("closing main UI before executing new request to start")
renderer.close_ui()
end
-- start up the main UI
log_render("starting main UI...")
local draw_start = util.time_ms()
local ui_message
crd_state.ui_ok, ui_message = renderer.try_start_ui()
if not crd_state.ui_ok then
log_render(util.c("main UI error: ", ui_message))
log.fatal(util.c("main GUI render failed with error ", ui_message))
else
log_render("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
end
elseif msg.message == MQ__RENDER_CMD.CLOSE_MAIN_UI then
-- close the main UI if it has been drawn
if renderer.ui_ready() then
log_render("closing main UI...")
renderer.close_ui()
log_render("main UI closed")
end
end
elseif msg.qtype == mqueue.TYPE.DATA then
-- received data
local cmd = msg.message ---@type queue_data
if cmd.key == MQ__RENDER_DATA.MON_CONNECT then
-- monitor connected
if renderer.handle_reconnect(cmd.val.name, cmd.val.device) then
log_sys(util.c("configured monitor ", cmd.val.name, " reconnected"))
else
log_sys(util.c("unused monitor ", cmd.val.name, " connected"))
end
elseif cmd.key == MQ__RENDER_DATA.MON_DISCONNECT then
-- monitor disconnected
if renderer.handle_disconnect(cmd.val) then
log_sys("lost a configured monitor")
else
log_sys("lost an unused monitor")
end
elseif cmd.key == MQ__RENDER_DATA.MON_RESIZE then
-- monitor resized
local is_used, is_ok = renderer.handle_resize(cmd.val)
if is_used then
log_sys(util.c("configured monitor ", cmd.val, " resized, ", util.trinary(is_ok, "display fits", "display does not fit")))
end
end
elseif msg.qtype == mqueue.TYPE.PACKET then
-- received a packet
end
end
-- quick yield
util.nop()
end
-- check for termination request
if crd_state.shutdown then
log.info("render thread exiting")
break
end
-- delay before next check
last_update = util.adaptive_delay(RENDER_SLEEP, last_update)
end
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
function public.p_exec()
local crd_state = smem.crd_state
while not crd_state.shutdown do
local status, result = pcall(public.exec)
if status == false then
log.fatal(util.strval(result))
end
iocontrol.fp_rt_status("render", false)
if not crd_state.shutdown then
log.info("render thread restarting in 5 seconds...")
util.psleep(5)
end
end
end
return public
end
return threads

View File

@ -0,0 +1,54 @@
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local VerticalBar = require("graphics.elements.indicators.VerticalBar")
local cpair = core.cpair
local border = core.border
-- new boiler view
---@param root Container parent
---@param x integer top left x
---@param y integer top left y
---@param ps psil ps interface
local function new_view(root, x, y, ps)
local text_fg = style.theme.text_fg
local lu_col = style.lu_colors
local db = iocontrol.get_db()
local boiler = Rectangle{parent=root,border=border(1,colors.gray,true),width=31,height=7,x=x,y=y}
local status = StateIndicator{parent=boiler,x=9,y=1,states=style.boiler.states,value=1,min_width=12}
local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=lu_col,label="Temp:",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=22,fg_bg=text_fg}
local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg}
status.register(ps, "computed_status", status.update)
temp.register(ps, "temperature", function (t) temp.update(db.temp_convert(t)) end)
boil_r.register(ps, "boil_rate", boil_r.update)
TextBox{parent=boiler,text="H",x=2,y=5,width=1,fg_bg=text_fg}
TextBox{parent=boiler,text="W",x=3,y=5,width=1,fg_bg=text_fg}
TextBox{parent=boiler,text="S",x=27,y=5,width=1,fg_bg=text_fg}
TextBox{parent=boiler,text="C",x=28,y=5,width=1,fg_bg=text_fg}
local hcool = VerticalBar{parent=boiler,x=2,y=1,fg_bg=cpair(colors.orange,colors.gray),height=4,width=1}
local water = VerticalBar{parent=boiler,x=3,y=1,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1}
local steam = VerticalBar{parent=boiler,x=27,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
local ccool = VerticalBar{parent=boiler,x=28,y=1,fg_bg=cpair(colors.lightBlue,colors.gray),height=4,width=1}
hcool.register(ps, "hcool_fill", hcool.update)
water.register(ps, "water_fill", water.update)
steam.register(ps, "steam_fill", steam.update)
ccool.register(ps, "ccool_fill", ccool.update)
end
return new_view

View File

@ -0,0 +1,111 @@
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local PowerIndicator = require("graphics.elements.indicators.PowerIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local VerticalBar = require("graphics.elements.indicators.VerticalBar")
local cpair = core.cpair
local border = core.border
local ALIGN = core.ALIGN
-- new induction matrix view
---@param root Container parent
---@param x integer top left x
---@param y integer top left y
---@param ps psil ps interface
---@param id number? matrix ID
local function new_view(root, x, y, ps, id)
local label_fg = style.theme.label_fg
local text_fg = style.theme.text_fg
local lu_col = style.lu_colors
local ind_yel = style.ind_yel
local ind_wht = style.ind_wht
local db = iocontrol.get_db()
local title = "INDUCTION MATRIX"
if type(id) == "number" then title = title .. id end
local matrix = Div{parent=root,fg_bg=style.root,width=33,height=24,x=x,y=y}
-- black has low contrast with dark gray, so if background is black use white instead
local cutout_fg_bg = cpair(util.trinary(style.theme.bg == colors.black, colors.white, style.theme.bg), colors.gray)
TextBox{parent=matrix,text=" ",width=33,x=1,y=1,fg_bg=cutout_fg_bg}
TextBox{parent=matrix,text=title,alignment=ALIGN.CENTER,width=33,x=1,y=2,fg_bg=cutout_fg_bg}
local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
local capacity = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Capacity:",unit=db.energy_label,format="%8.2f",value=0,width=26,fg_bg=text_fg}
local energy = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Energy: ",unit=db.energy_label,format="%8.2f",value=0,width=26,fg_bg=text_fg}
local avg_chg = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="\xb7Average:",unit=db.energy_label,format="%8.2f",value=0,width=26,fg_bg=text_fg}
local input = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Input: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local avg_in = PowerIndicator{parent=rect,x=7,y=7,lu_colors=lu_col,label="\xb7Average:",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local output = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Output: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local avg_out = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="\xb7Average:",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local trans_cap = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Max I/O: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
status.register(ps, "computed_status", status.update)
capacity.register(ps, "max_energy", function (val) capacity.update(db.energy_convert(val)) end)
energy.register(ps, "energy", function (val) energy.update(db.energy_convert(val)) end)
avg_chg.register(ps, "avg_charge", avg_chg.update)
input.register(ps, "last_input", function (val) input.update(db.energy_convert(val)) end)
avg_in.register(ps, "avg_inflow", avg_in.update)
output.register(ps, "last_output", function (val) output.update(db.energy_convert(val)) end)
avg_out.register(ps, "avg_outflow", avg_out.update)
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(db.energy_convert(val)) end)
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill: ",format="%7.2f",unit="%",value=0,width=20,fg_bg=text_fg}
local cells = DataIndicator{parent=rect,x=11,y=13,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg}
local providers = DataIndicator{parent=rect,x=11,y=14,lu_colors=lu_col,label="Providers:",format="%7d",value=0,width=18,fg_bg=text_fg}
fill.register(ps, "energy_fill", function (val) fill.update(val * 100) end)
cells.register(ps, "cells", cells.update)
providers.register(ps, "providers", providers.update)
local chging = IndicatorLight{parent=rect,x=11,y=16,label="Charging",colors=ind_wht}
local dischg = IndicatorLight{parent=rect,x=11,y=17,label="Discharging",colors=ind_wht}
local max_io = IndicatorLight{parent=rect,x=11,y=18,label="Max I/O Rate",colors=ind_yel}
chging.register(ps, "is_charging", chging.update)
dischg.register(ps, "is_discharging", dischg.update)
max_io.register(ps, "at_max_io", max_io.update)
local charge = VerticalBar{parent=rect,x=2,y=2,fg_bg=cpair(colors.green,colors.gray),height=17,width=4}
local in_cap = VerticalBar{parent=rect,x=7,y=12,fg_bg=cpair(colors.red,colors.gray),height=7,width=1}
local out_cap = VerticalBar{parent=rect,x=9,y=12,fg_bg=cpair(colors.blue,colors.gray),height=7,width=1}
TextBox{parent=rect,text="FILL I/O",x=2,y=20,width=8,fg_bg=label_fg}
local function calc_saturation(val)
local data = db.facility.induction_data_tbl[id or 1]
if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then
return val / data.build.transfer_cap
else return 0 end
end
charge.register(ps, "energy_fill", charge.update)
in_cap.register(ps, "last_input", function (val) in_cap.update(calc_saturation(val)) end)
out_cap.register(ps, "last_output", function (val) out_cap.update(calc_saturation(val)) end)
local eta = TextBox{parent=rect,x=11,y=20,width=20,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=style.theme.field_box}
eta.register(ps, "eta_string", eta.set_value)
end
return new_view

View File

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

View File

@ -0,0 +1,368 @@
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local RadIndicator = require("graphics.elements.indicators.RadIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local TriIndicatorLight = require("graphics.elements.indicators.TriIndicatorLight")
local Checkbox = require("graphics.elements.controls.Checkbox")
local HazardButton = require("graphics.elements.controls.HazardButton")
local NumericSpinbox = require("graphics.elements.controls.NumericSpinbox")
local RadioButton = require("graphics.elements.controls.RadioButton")
local ALIGN = core.ALIGN
local cpair = core.cpair
local border = core.border
local bw_fg_bg = style.bw_fg_bg
local period = core.flasher.PERIOD
-- new process control view
---@param root Container parent
---@param x integer top left x
---@param y integer top left y
local function new_view(root, x, y)
local s_hi_box = style.theme.highlight_box
local s_field = style.theme.field_box
local lu_cpair = style.lu_colors
local hzd_fg_bg = style.hzd_fg_bg
local dis_colors = style.dis_colors
local arrow_fg_bg = cpair(style.theme.label, s_hi_box.bkg)
local ind_grn = style.ind_grn
local ind_yel = style.ind_yel
local ind_red = style.ind_red
local ind_wht = style.ind_wht
assert(root.get_height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
local black = cpair(colors.black, colors.black)
local blk_brn = cpair(colors.black, colors.brown)
local blk_pur = cpair(colors.black, colors.purple)
local db = iocontrol.get_db()
local facility = db.facility
local units = db.units
local main = Div{parent=root,width=128,height=24,x=x,y=y}
local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=db.process.fac_scram,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=db.process.fac_ack_alarms,fg_bg=hzd_fg_bg}
db.process.fac_ack.on_scram = scram.on_response
db.process.fac_ack.on_ack_alarms = ack_a.on_response
local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=ind_grn}
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=style.ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd}
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=ind_grn}
local sps = IndicatorLight{parent=main,label="SPS Connected",colors=ind_grn}
all_ok.register(facility.ps, "all_sys_ok", all_ok.update)
rad_mon.register(facility.ps, "rad_computed_status", rad_mon.update)
ind_mat.register(facility.induction_ps_tbl[1], "computed_status", function (status) ind_mat.update(status > 1) end)
sps.register(facility.sps_ps_tbl[1], "computed_status", function (status) sps.update(status > 1) end)
main.line_break()
local auto_ready = IndicatorLight{parent=main,label="Configured Units Ready",colors=ind_grn}
local auto_act = IndicatorLight{parent=main,label="Process Active",colors=ind_grn}
local auto_ramp = IndicatorLight{parent=main,label="Process Ramping",colors=ind_wht,flash=true,period=period.BLINK_250_MS}
local auto_sat = IndicatorLight{parent=main,label="Min/Max Burn Rate",colors=ind_yel}
auto_ready.register(facility.ps, "auto_ready", auto_ready.update)
auto_act.register(facility.ps, "auto_active", auto_act.update)
auto_ramp.register(facility.ps, "auto_ramping", auto_ramp.update)
auto_sat.register(facility.ps, "auto_saturated", auto_sat.update)
main.line_break()
local auto_scram = IndicatorLight{parent=main,label="Automatic SCRAM",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local matrix_flt = IndicatorLight{parent=main,label="Induction Matrix Fault",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
local matrix_fill = IndicatorLight{parent=main,label="Matrix Charge High",colors=ind_red,flash=true,period=period.BLINK_500_MS}
local unit_crit = IndicatorLight{parent=main,label="Unit Critical Alarm",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local fac_rad_h = IndicatorLight{parent=main,label="Facility Radiation High",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local gen_fault = IndicatorLight{parent=main,label="Gen. Control Fault",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
auto_scram.register(facility.ps, "auto_scram", auto_scram.update)
matrix_flt.register(facility.ps, "as_matrix_fault", matrix_flt.update)
matrix_fill.register(facility.ps, "as_matrix_fill", matrix_fill.update)
unit_crit.register(facility.ps, "as_crit_alarm", unit_crit.update)
fac_rad_h.register(facility.ps, "as_radiation", fac_rad_h.update)
gen_fault.register(facility.ps, "as_gen_fault", gen_fault.update)
TextBox{parent=main,y=23,text="Radiation",width=13,fg_bg=style.label}
local radiation = RadIndicator{parent=main,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=s_field}
radiation.register(facility.ps, "radiation", radiation.update)
TextBox{parent=main,x=15,y=23,text="Linked RTUs",width=11,fg_bg=style.label}
local rtu_count = DataIndicator{parent=main,x=15,y=24,label="",format="%11d",value=0,lu_colors=lu_cpair,width=11,fg_bg=s_field}
rtu_count.register(facility.ps, "rtu_count", rtu_count.update)
---------------------
-- process control --
---------------------
local proc = Div{parent=main,width=103,height=24,x=27,y=1}
-----------------------------
-- process control targets --
-----------------------------
local targets = Div{parent=proc,width=31,height=24,x=1,y=1}
local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=blk_pur}
TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2}
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=s_hi_box}
local b_target = NumericSpinbox{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=burn_target,x=18,y=2,text="mB/t",fg_bg=style.theme.label_fg}
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
b_target.register(facility.ps, "process_burn_target", b_target.set_value)
burn_sum.register(facility.ps, "burn_sum", burn_sum.update)
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=blk_pur}
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=s_hi_box}
local c_target = NumericSpinbox{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=chg_target,x=18,y=2,text="M"..db.energy_label,fg_bg=style.theme.label_fg}
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="M"..db.energy_label,commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
c_target.register(facility.ps, "process_charge_target", c_target.set_value)
cur_charge.register(facility.induction_ps_tbl[1], "avg_charge", function (fe) cur_charge.update(db.energy_convert_from_fe(fe) / 1000000) end)
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur}
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=s_hi_box}
local g_target = NumericSpinbox{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=gen_target,x=18,y=2,text="k"..db.energy_label.."/t",fg_bg=style.theme.label_fg}
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="k"..db.energy_label.."/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
g_target.register(facility.ps, "process_gen_target", g_target.set_value)
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(db.energy_convert(j) / 1000)) end)
-----------------
-- unit limits --
-----------------
local limit_div = Div{parent=proc,width=21,height=19,x=34,y=6}
local rate_limits = {}
for i = 1, 4 do
local unit
local tag_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
local lim_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
local label_fg = style.theme.disabled_fg
local cur_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
local cur_lu = style.theme.disabled
if i <= facility.num_units then
unit = units[i]
tag_fg_bg = cpair(colors.black, colors.lightBlue)
lim_fg_bg = s_hi_box
label_fg = style.theme.label_fg
cur_fg_bg = blk_brn
cur_lu = colors.black
end
local _y = ((i - 1) * 5) + 1
local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2}
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=s_hi_box}
local lim = NumericSpinbox{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled,fg_bg=lim_fg_bg}
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,fg_bg=label_fg}
local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(cur_lu,cur_lu),width=14,fg_bg=cur_fg_bg}
if i <= facility.num_units then
rate_limits[i] = lim
rate_limits[i].register(unit.unit_ps, "max_burn", rate_limits[i].set_max)
rate_limits[i].register(unit.unit_ps, "burn_limit", rate_limits[i].set_value)
cur_burn.register(unit.unit_ps, "act_burn_rate", cur_burn.update)
else
lim.disable()
end
end
-------------------
-- unit statuses --
-------------------
local stat_div = Div{parent=proc,width=22,height=24,x=57,y=6}
for i = 1, 4 do
local tag_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
local ind_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
local ind_off = style.theme.disabled
if i <= facility.num_units then
tag_fg_bg = cpair(colors.black, colors.cyan)
ind_fg_bg = cpair(style.theme.text, s_hi_box.bkg)
ind_off = style.ind_hi_box_bg
end
local _y = ((i - 1) * 5) + 1
local unit_tag = Div{parent=stat_div,x=1,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Status",width=7,height=2}
local lights = Div{parent=stat_div,x=9,y=_y,width=14,height=4,fg_bg=ind_fg_bg}
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(ind_grn.fgd,ind_off)}
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(ind_red.fgd,ind_off),flash=true,period=period.BLINK_250_MS}
if i <= facility.num_units then
local unit = units[i]
ready.register(unit.unit_ps, "U_AutoReady", ready.update)
degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update)
end
end
-------------------------
-- controls and status --
-------------------------
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
mode.register(facility.ps, "process_mode", mode.set_value)
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.white)}
stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value)
stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value)
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=s_hi_box}
-- save the automatic process control configuration without starting
local function _save_cfg()
local limits = {}
for i = 1, #rate_limits do limits[i] = rate_limits[i].get_value() end
process.save(mode.get_value(), b_target.get_value(), db.energy_convert_to_fe(c_target.get_value()),
db.energy_convert_to_fe(g_target.get_value()), limits)
end
-- start automatic control after saving process control settings
local function _start_auto()
_save_cfg()
db.process.process_start()
end
local save = HazardButton{parent=auto_controls,x=2,y=2,text="SAVE",accent=colors.purple,dis_colors=dis_colors,callback=_save_cfg,fg_bg=hzd_fg_bg}
local start = HazardButton{parent=auto_controls,x=13,y=2,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=_start_auto,fg_bg=hzd_fg_bg}
local stop = HazardButton{parent=auto_controls,x=23,y=2,text="STOP",accent=colors.red,dis_colors=dis_colors,callback=db.process.process_stop,fg_bg=hzd_fg_bg}
db.process.fac_ack.on_start = start.on_response
db.process.fac_ack.on_stop = stop.on_response
function facility.save_cfg_ack(ack)
tcd.dispatch(0.2, function () save.on_response(ack) end)
end
start.register(facility.ps, "auto_ready", function (ready)
if ready and (not facility.auto_active) then start.enable() else start.disable() end
end)
-- REGISTER_NOTE: for optimization/brevity, due to not deleting anything but the whole element tree when it comes
-- to the process control display and coordinator GUI as a whole, child elements will not directly be registered here
-- (preventing garbage collection until the parent 'proc' is deleted)
proc.register(facility.ps, "auto_active", function (active)
if active then
b_target.disable()
c_target.disable()
g_target.disable()
mode.disable()
start.disable()
for i = 1, #rate_limits do rate_limits[i].disable() end
else
b_target.enable()
c_target.enable()
g_target.enable()
mode.enable()
if facility.auto_ready then start.enable() end
for i = 1, #rate_limits do rate_limits[i].enable() end
end
end)
------------------------------
-- waste production control --
------------------------------
local waste_status = Div{parent=proc,width=24,height=4,x=57,y=1,}
for i = 1, facility.num_units do
local unit = units[i]
TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8}
local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=ind_wht}
local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.get_waste().states_abbrv,value=1,min_width=6}
a_waste.register(unit.unit_ps, "U_AutoWaste", a_waste.update)
waste_m.register(unit.unit_ps, "U_WasteProduct", waste_m.update)
end
local waste_sel = Div{parent=proc,width=21,height=24,x=81,y=1}
local cutout_fg_bg = cpair(style.theme.bg, colors.brown)
TextBox{parent=waste_sel,text=" ",width=21,x=1,y=1,fg_bg=cutout_fg_bg}
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,x=1,y=2,fg_bg=cutout_fg_bg}
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=2,y=1,states=style.get_waste().states,value=1,min_width=17}
status.register(facility.ps, "current_waste_product", status.update)
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.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)
local fb_active = IndicatorLight{parent=rect,x=2,y=7,label="Fallback Active",colors=ind_wht}
local sps_disabled = IndicatorLight{parent=rect,x=2,y=8,label="SPS Disabled LC",colors=ind_yel}
fb_active.register(facility.ps, "pu_fallback_active", fb_active.update)
sps_disabled.register(facility.ps, "sps_disabled_low_power", sps_disabled.update)
local pu_fallback = Checkbox{parent=rect,x=2,y=10,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.brown,style.theme.checkbox_bg)}
TextBox{parent=rect,x=2,y=12,height=3,text="Switch to Pu when SNAs cannot keep up with waste.",fg_bg=style.label}
local lc_sps = Checkbox{parent=rect,x=2,y=16,label="Low Charge SPS",callback=process.set_sps_low_power,box_fg_bg=cpair(colors.brown,style.theme.checkbox_bg)}
TextBox{parent=rect,x=2,y=18,height=3,text="Use SPS at low charge, otherwise switches to Po.",fg_bg=style.label}
pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value)
lc_sps.register(facility.ps, "process_sps_low_power", lc_sps.set_value)
end
return new_view

View File

@ -0,0 +1,76 @@
local types = require("scada-common.types")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local HorizontalBar = require("graphics.elements.indicators.HorizontalBar")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local cpair = core.cpair
local border = core.border
-- create new reactor view
---@param root Container parent
---@param x integer top left x
---@param y integer top left y
---@param ps psil ps interface
local function new_view(root, x, y, ps)
local text_fg = style.theme.text_fg
local lu_col = style.lu_colors
local db = iocontrol.get_db()
local reactor = Rectangle{parent=root,border=border(1,colors.gray,true),width=30,height=7,x=x,y=y}
local status = StateIndicator{parent=reactor,x=6,y=1,states=style.reactor.states,value=1,min_width=16}
local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.2f",value=0,width=26,fg_bg=text_fg}
local heating_r = DataIndicator{parent=reactor,x=2,y=5,lu_colors=lu_col,label="Heating:",unit="mB/t",format="%12.0f",value=0,commas=true,width=26,fg_bg=text_fg}
status.register(ps, "computed_status", status.update)
core_temp.register(ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end)
burn_r.register(ps, "act_burn_rate", burn_r.update)
heating_r.register(ps, "heating_rate", heating_r.update)
local reactor_fills = Rectangle{parent=root,border=border(1, colors.gray, true),width=24,height=7,x=(x + 29),y=y}
TextBox{parent=reactor_fills,text="FUEL",x=2,y=1,fg_bg=text_fg}
TextBox{parent=reactor_fills,text="COOL",x=2,y=2,fg_bg=text_fg}
TextBox{parent=reactor_fills,text="HCOOL",x=2,y=4,fg_bg=text_fg}
TextBox{parent=reactor_fills,text="WASTE",x=2,y=5,fg_bg=text_fg}
local fuel = HorizontalBar{parent=reactor_fills,x=8,y=1,show_percent=true,bar_fg_bg=cpair(style.theme.fuel_color,colors.gray),height=1,width=14}
local ccool = HorizontalBar{parent=reactor_fills,x=8,y=2,show_percent=true,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=14}
local hcool = HorizontalBar{parent=reactor_fills,x=8,y=4,show_percent=true,bar_fg_bg=cpair(colors.white,colors.gray),height=1,width=14}
local waste = HorizontalBar{parent=reactor_fills,x=8,y=5,show_percent=true,bar_fg_bg=cpair(colors.brown,colors.gray),height=1,width=14}
ccool.register(ps, "ccool_type", function (type)
if type == types.FLUID.SODIUM then
ccool.recolor(cpair(colors.lightBlue, colors.gray))
else
ccool.recolor(cpair(colors.blue, colors.gray))
end
end)
hcool.register(ps, "hcool_type", function (type)
if type == types.FLUID.SUPERHEATED_SODIUM then
hcool.recolor(cpair(colors.orange, colors.gray))
else
hcool.recolor(cpair(colors.white, colors.gray))
end
end)
fuel.register(ps, "fuel_fill", fuel.update)
ccool.register(ps, "ccool_fill", ccool.update)
hcool.register(ps, "hcool_fill", hcool.update)
waste.register(ps, "waste_fill", waste.update)
end
return new_view

View File

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

View File

@ -0,0 +1,538 @@
--
-- Reactor Unit SCADA Coordinator GUI
--
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local AlarmLight = require("graphics.elements.indicators.AlarmLight")
local CoreMap = require("graphics.elements.indicators.CoreMap")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local RadIndicator = require("graphics.elements.indicators.RadIndicator")
local TriIndicatorLight = require("graphics.elements.indicators.TriIndicatorLight")
local VerticalBar = require("graphics.elements.indicators.VerticalBar")
local HazardButton = require("graphics.elements.controls.HazardButton")
local MultiButton = require("graphics.elements.controls.MultiButton")
local NumericSpinbox = require("graphics.elements.controls.NumericSpinbox")
local PushButton = require("graphics.elements.controls.PushButton")
local RadioButton = require("graphics.elements.controls.RadioButton")
local AUTO_GROUP = types.AUTO_GROUP
local ALIGN = core.ALIGN
local cpair = core.cpair
local border = core.border
local bw_fg_bg = style.bw_fg_bg
local gry_wht = style.gray_white
local period = core.flasher.PERIOD
-- create a unit view
---@param parent Container parent
---@param id integer
local function init(parent, id)
local s_hi_box = style.theme.highlight_box
local s_hi_bright = style.theme.highlight_box_bright
local s_field = style.theme.field_box
local hc_text = style.hc_text
local lu_cpair = style.lu_colors
local hzd_fg_bg = style.hzd_fg_bg
local dis_colors = style.dis_colors
local arrow_fg_bg = cpair(style.theme.label, s_hi_box.bkg)
local ind_bkg = style.ind_bkg
local ind_grn = style.ind_grn
local ind_yel = style.ind_yel
local ind_red = style.ind_red
local ind_wht = style.ind_wht
local db = iocontrol.get_db()
local unit = db.units[id]
local f_ps = db.facility.ps
local main = Div{parent=parent,x=1,y=1}
if unit == nil then return main end
local u_ps = unit.unit_ps
local b_ps = unit.boiler_ps_tbl
local t_ps = unit.turbine_ps_tbl
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,fg_bg=style.theme.header}
-----------------------------
-- main stats and core map --
-----------------------------
local core_map = CoreMap{parent=main,x=2,y=3,reactor_l=18,reactor_w=18}
core_map.register(u_ps, "temp", core_map.update)
core_map.register(u_ps, "size", function (s) core_map.resize(s[1], s[2]) end)
TextBox{parent=main,x=12,y=22,text="Heating Rate",width=12,fg_bg=style.label}
local heating_r = DataIndicator{parent=main,x=12,label="",format="%14.0f",value=0,unit="mB/t",commas=true,lu_colors=lu_cpair,width=19,fg_bg=s_field}
heating_r.register(u_ps, "heating_rate", heating_r.update)
TextBox{parent=main,x=12,y=25,text="Commanded Burn Rate",width=19,fg_bg=style.label}
local burn_r = DataIndicator{parent=main,x=12,label="",format="%14.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=19,fg_bg=s_field}
burn_r.register(u_ps, "burn_rate", burn_r.update)
TextBox{parent=main,text="F",x=2,y=22,width=1,fg_bg=style.label}
TextBox{parent=main,text="C",x=4,y=22,width=1,fg_bg=style.label}
TextBox{parent=main,text="\x1a",x=6,y=24,width=1,fg_bg=style.label}
TextBox{parent=main,text="\x1a",x=6,y=25,width=1,fg_bg=style.label}
TextBox{parent=main,text="H",x=8,y=22,width=1,fg_bg=style.label}
TextBox{parent=main,text="W",x=10,y=22,width=1,fg_bg=style.label}
local fuel = VerticalBar{parent=main,x=2,y=23,fg_bg=cpair(style.theme.fuel_color,colors.gray),height=4,width=1}
local ccool = VerticalBar{parent=main,x=4,y=23,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1}
local hcool = VerticalBar{parent=main,x=8,y=23,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
local waste = VerticalBar{parent=main,x=10,y=23,fg_bg=cpair(colors.brown,colors.gray),height=4,width=1}
fuel.register(u_ps, "fuel_fill", fuel.update)
ccool.register(u_ps, "ccool_fill", ccool.update)
hcool.register(u_ps, "hcool_fill", hcool.update)
waste.register(u_ps, "waste_fill", waste.update)
ccool.register(u_ps, "ccool_type", function (type)
if type == types.FLUID.SODIUM then
ccool.recolor(cpair(colors.lightBlue, colors.gray))
else
ccool.recolor(cpair(colors.blue, colors.gray))
end
end)
hcool.register(u_ps, "hcool_type", function (type)
if type == types.FLUID.SUPERHEATED_SODIUM then
hcool.recolor(cpair(colors.orange, colors.gray))
else
hcool.recolor(cpair(colors.white, colors.gray))
end
end)
TextBox{parent=main,x=32,y=22,text="Core Temp",width=9,fg_bg=style.label}
local fmt = util.trinary(string.len(db.temp_label) == 2, "%10.2f", "%11.2f")
local core_temp = DataIndicator{parent=main,x=32,label="",format=fmt,value=0,commas=true,unit=db.temp_label,lu_colors=lu_cpair,width=13,fg_bg=s_field}
core_temp.register(u_ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end)
TextBox{parent=main,x=32,y=25,text="Burn Rate",width=9,fg_bg=style.label}
local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=s_field}
act_burn_r.register(u_ps, "act_burn_rate", act_burn_r.update)
TextBox{parent=main,x=32,y=28,text="Damage",width=6,fg_bg=style.label}
local damage_p = DataIndicator{parent=main,x=32,label="",format="%11.0f",value=0,unit="%",lu_colors=lu_cpair,width=13,fg_bg=s_field}
damage_p.register(u_ps, "damage", damage_p.update)
TextBox{parent=main,x=32,y=31,text="Radiation",width=21,fg_bg=style.label}
local radiation = RadIndicator{parent=main,x=32,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=s_field}
radiation.register(u_ps, "radiation", radiation.update)
-------------------
-- system status --
-------------------
local u_stat = Rectangle{parent=main,border=border(1,colors.gray,true),thin=true,width=33,height=4,x=46,y=3,fg_bg=bw_fg_bg}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,alignment=ALIGN.CENTER,fg_bg=gry_wht}
stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value)
stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value)
-----------------
-- annunciator --
-----------------
-- annunciator colors (generally) per IAEA-TECDOC-812 recommendations
local annunciator = Div{parent=main,width=23,height=18,x=22,y=3}
-- connectivity
local plc_online = IndicatorLight{parent=annunciator,label="PLC Online",colors=cpair(ind_grn.fgd,ind_red.fgd)}
local plc_hbeat = IndicatorLight{parent=annunciator,label="PLC Heartbeat",colors=ind_wht}
local rad_mon = TriIndicatorLight{parent=annunciator,label="Radiation Monitor",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd}
plc_online.register(u_ps, "PLCOnline", plc_online.update)
plc_hbeat.register(u_ps, "PLCHeartbeat", plc_hbeat.update)
rad_mon.register(u_ps, "RadiationMonitor", rad_mon.update)
annunciator.line_break()
-- operating state
local r_active = IndicatorLight{parent=annunciator,label="Active",colors=ind_grn}
local r_auto = IndicatorLight{parent=annunciator,label="Automatic Control",colors=ind_wht}
r_active.register(u_ps, "status", r_active.update)
r_auto.register(u_ps, "AutoControl", r_auto.update)
-- main unit transient/warning annunciator panel
local r_scram = IndicatorLight{parent=annunciator,label="Reactor SCRAM",colors=ind_red}
local r_mscrm = IndicatorLight{parent=annunciator,label="Manual Reactor SCRAM",colors=ind_red}
local r_ascrm = IndicatorLight{parent=annunciator,label="Auto Reactor SCRAM",colors=ind_red}
local rad_wrn = IndicatorLight{parent=annunciator,label="Radiation Warning",colors=ind_yel}
local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=ind_red}
local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=ind_yel}
local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=ind_yel}
local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=ind_red}
local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=ind_yel}
local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=ind_yel}
local r_wloc = IndicatorLight{parent=annunciator,label="Waste Line Occlusion",colors=ind_yel}
local r_hsrt = IndicatorLight{parent=annunciator,label="Startup Rate High",colors=ind_yel}
r_scram.register(u_ps, "ReactorSCRAM", r_scram.update)
r_mscrm.register(u_ps, "ManualReactorSCRAM", r_mscrm.update)
r_ascrm.register(u_ps, "AutoReactorSCRAM", r_ascrm.update)
rad_wrn.register(u_ps, "RadiationWarning", rad_wrn.update)
r_rtrip.register(u_ps, "RCPTrip", r_rtrip.update)
r_cflow.register(u_ps, "RCSFlowLow", r_cflow.update)
r_clow.register(u_ps, "CoolantLevelLow", r_clow.update)
r_temp.register(u_ps, "ReactorTempHigh", r_temp.update)
r_rhdt.register(u_ps, "ReactorHighDeltaT", r_rhdt.update)
r_firl.register(u_ps, "FuelInputRateLow", r_firl.update)
r_wloc.register(u_ps, "WasteLineOcclusion", r_wloc.update)
r_hsrt.register(u_ps, "HighStartupRate", r_hsrt.update)
-- RPS annunciator panel
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,x=46,y=8}
local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9}
local rps_annunc = Div{parent=rps,width=31,height=10,x=2,y=1}
local rps_trp = IndicatorLight{parent=rps_annunc,label="RPS Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local rps_dmg = IndicatorLight{parent=rps_annunc,label="Damage Level High",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local rps_exh = IndicatorLight{parent=rps_annunc,label="Excess Heated Coolant",colors=ind_yel}
local rps_exw = IndicatorLight{parent=rps_annunc,label="Excess Waste",colors=ind_yel}
local rps_tmp = IndicatorLight{parent=rps_annunc,label="Core Temperature High",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local rps_nof = IndicatorLight{parent=rps_annunc,label="No Fuel",colors=ind_yel}
local rps_loc = IndicatorLight{parent=rps_annunc,label="Coolant Level Low Low",colors=ind_yel}
local rps_flt = IndicatorLight{parent=rps_annunc,label="PPM Fault",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
local rps_tmo = IndicatorLight{parent=rps_annunc,label="Connection Timeout",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
local rps_sfl = IndicatorLight{parent=rps_annunc,label="System Failure",colors=ind_red,flash=true,period=period.BLINK_500_MS}
rps_trp.register(u_ps, "rps_tripped", rps_trp.update)
rps_dmg.register(u_ps, "high_dmg", rps_dmg.update)
rps_exh.register(u_ps, "ex_hcool", rps_exh.update)
rps_exw.register(u_ps, "ex_waste", rps_exw.update)
rps_tmp.register(u_ps, "high_temp", rps_tmp.update)
rps_nof.register(u_ps, "no_fuel", rps_nof.update)
rps_loc.register(u_ps, "low_cool", rps_loc.update)
rps_flt.register(u_ps, "fault", rps_flt.update)
rps_tmo.register(u_ps, "timeout", rps_tmo.update)
rps_sfl.register(u_ps, "sys_fail", rps_sfl.update)
-- cooling annunciator panel
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,x=46,y=22}
local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23}
local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1}
local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7}
local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=ind_yel}
local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=ind_bkg,c2=ind_wht.fgd,c3=ind_grn.fgd}
local c_cfm = IndicatorLight{parent=rcs_annunc,label="Coolant Feed Mismatch",colors=ind_yel}
local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=ind_yel}
local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=ind_yel}
local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=ind_yel}
c_flt.register(u_ps, "RCSFault", c_flt.update)
c_emg.register(u_ps, "EmergencyCoolant", c_emg.update)
c_cfm.register(u_ps, "CoolantFeedMismatch", c_cfm.update)
c_brm.register(u_ps, "BoilRateMismatch", c_brm.update)
c_sfm.register(u_ps, "SteamFeedMismatch", c_sfm.update)
c_mwrf.register(u_ps, "MaxWaterReturnFeed", c_mwrf.update)
local available_space = 16 - (unit.num_boilers * 2 + unit.num_turbines * 4)
local function _add_space()
-- if we have some extra space, add padding
rcs_tags.line_break()
rcs_annunc.line_break()
end
-- boiler annunciator panel(s)
if unit.num_boilers > 0 then
if available_space > 0 then _add_space() end
TextBox{parent=rcs_tags,x=1,text="B1",width=2,fg_bg=hc_text}
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
b1_wll.register(b_ps[1], "WaterLevelLow", b1_wll.update)
TextBox{parent=rcs_tags,text="B1",width=2,fg_bg=hc_text}
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
b1_hr.register(b_ps[1], "HeatingRateLow", b1_hr.update)
end
if unit.num_boilers > 1 then
-- note, can't (shouldn't for sure...) have 0 turbines
if (available_space > 2 and unit.num_turbines == 1) or
(available_space > 3 and unit.num_turbines == 2) or
(available_space > 4) then
_add_space()
end
TextBox{parent=rcs_tags,text="B2",width=2,fg_bg=hc_text}
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
b2_wll.register(b_ps[2], "WaterLevelLow", b2_wll.update)
TextBox{parent=rcs_tags,text="B2",width=2,fg_bg=hc_text}
local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
b2_hr.register(b_ps[2], "HeatingRateLow", b2_hr.update)
end
-- turbine annunciator panels
if available_space > 1 then _add_space() end
TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
t1_sdo.register(t_ps[1], "SteamDumpOpen", t1_sdo.update)
TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
t1_tos.register(t_ps[1], "TurbineOverSpeed", t1_tos.update)
TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
t1_gtrp.register(t_ps[1], "GeneratorTrip", t1_gtrp.update)
TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
t1_trp.register(t_ps[1], "TurbineTrip", t1_trp.update)
if unit.num_turbines > 1 then
if (available_space > 2 and unit.num_turbines == 2) or available_space > 3 then
_add_space()
end
TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
t2_sdo.register(t_ps[2], "SteamDumpOpen", t2_sdo.update)
TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
t2_tos.register(t_ps[2], "TurbineOverSpeed", t2_tos.update)
TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
t2_gtrp.register(t_ps[2], "GeneratorTrip", t2_gtrp.update)
TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
t2_trp.register(t_ps[2], "TurbineTrip", t2_trp.update)
end
if unit.num_turbines > 2 then
if available_space > 3 then _add_space() end
TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
t3_sdo.register(t_ps[3], "SteamDumpOpen", t3_sdo.update)
TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
t3_tos.register(t_ps[3], "TurbineOverSpeed", t3_tos.update)
TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
t3_gtrp.register(t_ps[3], "GeneratorTrip", t3_gtrp.update)
TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update)
end
util.nop()
----------------------
-- reactor controls --
----------------------
local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=s_hi_box}
local burn_rate = NumericSpinbox{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=burn_control,x=9,y=2,text="mB/t",fg_bg=style.theme.label_fg}
local set_burn = function () unit.set_burn(burn_rate.get_value()) end
local set_burn_btn = PushButton{parent=burn_control,x=14,y=2,text="SET",min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=style.wh_gray,dis_fg_bg=dis_colors,callback=set_burn}
burn_rate.register(u_ps, "burn_rate", burn_rate.set_value)
burn_rate.register(u_ps, "max_burn", burn_rate.set_max)
local start = HazardButton{parent=main,x=2,y=28,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=unit.start,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=main,x=12,y=32,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=unit.ack_alarms,fg_bg=hzd_fg_bg}
local scram = HazardButton{parent=main,x=2,y=32,text="SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=unit.scram,fg_bg=hzd_fg_bg}
local reset = HazardButton{parent=main,x=22,y=32,text="RESET",accent=colors.red,dis_colors=dis_colors,callback=unit.reset_rps,fg_bg=hzd_fg_bg}
db.process.unit_ack[id].on_start = start.on_response
db.process.unit_ack[id].on_scram = scram.on_response
db.process.unit_ack[id].on_rps_reset = reset.on_response
db.process.unit_ack[id].on_ack_alarms = ack_a.on_response
local function start_button_en_check()
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)
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=ALIGN.CENTER,width=33,x=46,y=48}
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1}
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.get_waste().unit_opts,callback=unit.set_waste,min_width=6}
waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)
----------------------
-- alarm management --
----------------------
local alarm_panel = Div{parent=main,x=2,y=36,width=29,height=16,fg_bg=s_hi_bright}
local a_brc = AlarmLight{parent=alarm_panel,x=6,y=2,label="Containment Breach",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
local a_rad = AlarmLight{parent=alarm_panel,x=6,label="Containment Radiation",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
local a_dmg = AlarmLight{parent=alarm_panel,x=6,label="Critical Damage",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
alarm_panel.line_break()
local a_rcl = AlarmLight{parent=alarm_panel,x=6,label="Reactor Lost",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
local a_rcd = AlarmLight{parent=alarm_panel,x=6,label="Reactor Damage",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
local a_rot = AlarmLight{parent=alarm_panel,x=6,label="Reactor Over Temp",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
local a_rht = AlarmLight{parent=alarm_panel,x=6,label="Reactor High Temp",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_500_MS}
local a_rwl = AlarmLight{parent=alarm_panel,x=6,label="Reactor Waste Leak",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
local a_rwh = AlarmLight{parent=alarm_panel,x=6,label="Reactor Waste High",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_500_MS}
alarm_panel.line_break()
local a_rps = AlarmLight{parent=alarm_panel,x=6,label="RPS Transient",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_500_MS}
local a_clt = AlarmLight{parent=alarm_panel,x=6,label="RCS Transient",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_500_MS}
local a_tbt = AlarmLight{parent=alarm_panel,x=6,label="Turbine Trip",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
a_brc.register(u_ps, "Alarm_1", a_brc.update)
a_rad.register(u_ps, "Alarm_2", a_rad.update)
a_dmg.register(u_ps, "Alarm_4", a_dmg.update)
a_rcl.register(u_ps, "Alarm_3", a_rcl.update)
a_rcd.register(u_ps, "Alarm_5", a_rcd.update)
a_rot.register(u_ps, "Alarm_6", a_rot.update)
a_rht.register(u_ps, "Alarm_7", a_rht.update)
a_rwl.register(u_ps, "Alarm_8", a_rwl.update)
a_rwh.register(u_ps, "Alarm_9", a_rwh.update)
a_rps.register(u_ps, "Alarm_10", a_rps.update)
a_clt.register(u_ps, "Alarm_11", a_clt.update)
a_tbt.register(u_ps, "Alarm_12", a_tbt.update)
-- ack's and resets
local c = unit.alarm_callbacks
local ack_fg_bg = cpair(colors.black, colors.orange)
local rst_fg_bg = cpair(colors.black, colors.lime)
local active_fg_bg = cpair(colors.white, colors.gray)
PushButton{parent=alarm_panel,x=2,y=2,text="\x13",callback=c.c_breach.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=4,y=2,text="R",callback=c.c_breach.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=2,y=3,text="\x13",callback=c.radiation.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=4,y=3,text="R",callback=c.radiation.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=2,y=4,text="\x13",callback=c.dmg_crit.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=4,y=4,text="R",callback=c.dmg_crit.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=2,y=6,text="\x13",callback=c.r_lost.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=4,y=6,text="R",callback=c.r_lost.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=2,y=7,text="\x13",callback=c.damage.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=4,y=7,text="R",callback=c.damage.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=2,y=8,text="\x13",callback=c.over_temp.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=4,y=8,text="R",callback=c.over_temp.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=2,y=9,text="\x13",callback=c.high_temp.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=4,y=9,text="R",callback=c.high_temp.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=2,y=10,text="\x13",callback=c.waste_leak.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=4,y=10,text="R",callback=c.waste_leak.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=2,y=11,text="\x13",callback=c.waste_high.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=4,y=11,text="R",callback=c.waste_high.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=2,y=13,text="\x13",callback=c.rps_trans.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=4,y=13,text="R",callback=c.rps_trans.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=2,y=14,text="\x13",callback=c.rcs_trans.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=4,y=14,text="R",callback=c.rcs_trans.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=2,y=15,text="\x13",callback=c.t_trip.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg}
PushButton{parent=alarm_panel,x=4,y=15,text="R",callback=c.t_trip.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg}
-- color tags
TextBox{parent=alarm_panel,x=5,y=13,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.cyan)}
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)}
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)}
--------------------------------
-- automatic control settings --
--------------------------------
TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,x=32,y=36}
local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37}
local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1}
local group = RadioButton{parent=auto_div,options=types.AUTO_GROUP_NAMES,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end)
auto_div.line_break()
local function set_group() unit.set_group(group.get_value() - 1) end
local set_grp_btn = PushButton{parent=auto_div,text="SET",x=4,min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=style.wh_gray,dis_fg_bg=gry_wht,callback=set_group}
auto_div.line_break()
TextBox{parent=auto_div,text="Prio. Group",width=11,fg_bg=style.label}
local auto_grp = TextBox{parent=auto_div,text="Manual",width=11,fg_bg=s_field}
auto_grp.register(u_ps, "auto_group", auto_grp.set_value)
auto_div.line_break()
local a_rdy = IndicatorLight{parent=auto_div,label="Ready",x=2,colors=ind_grn}
local a_stb = IndicatorLight{parent=auto_div,label="Standby",x=2,colors=ind_wht,flash=true,period=period.BLINK_1000_MS}
a_rdy.register(u_ps, "U_AutoReady", a_rdy.update)
-- update standby indicator
a_stb.register(u_ps, "status", function (active)
a_stb.update(unit.annunciator.AutoControl and (not active))
end)
a_stb.register(u_ps, "AutoControl", function (auto_active)
if auto_active then
a_stb.update(unit.reactor_data.mek_status.status == false)
else a_stb.update(false) end
end)
-- enable/disable controls based on group assignment (start button is separate)
burn_rate.register(u_ps, "auto_group_id", function (gid)
if gid == AUTO_GROUP.MANUAL then burn_rate.enable() else burn_rate.disable() end
end)
set_burn_btn.register(u_ps, "auto_group_id", function (gid)
if gid == AUTO_GROUP.MANUAL then set_burn_btn.enable() else set_burn_btn.disable() end
end)
-- can't change group if auto is engaged regardless of if this unit is part of auto control
set_grp_btn.register(f_ps, "auto_active", function (auto_active)
if auto_active then set_grp_btn.disable() else set_grp_btn.enable() end
end)
return main
end
return init

View File

@ -0,0 +1,255 @@
--
-- Basic Unit Flow Overview
--
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local PipeNetwork = require("graphics.elements.PipeNetwork")
local TextBox = require("graphics.elements.TextBox")
local Rectangle = require("graphics.elements.Rectangle")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local TriIndicatorLight = require("graphics.elements.indicators.TriIndicatorLight")
local COOLANT_TYPE = types.COOLANT_TYPE
local ALIGN = core.ALIGN
local sprintf = util.sprintf
local border = core.border
local cpair = core.cpair
local pipe = core.pipe
local wh_gray = style.wh_gray
local lg_gray = style.lg_gray
-- make a new unit flow window
---@param parent Container parent
---@param x integer top left x
---@param y integer top left y
---@param wide boolean whether to render wide version
---@param unit_id integer unit index
local function make(parent, x, y, wide, unit_id)
local s_field = style.theme.field_box
local text_c = style.text_colors
local lu_c = style.lu_colors
local lu_c_d = style.lu_colors_dark
local ind_grn = style.ind_grn
local ind_wht = style.ind_wht
local height = 16
local facility = iocontrol.get_db().facility
local unit = iocontrol.get_db().units[unit_id]
local tank_conns = facility.tank_conns
local tank_types = facility.tank_fluid_types
local v_start = 1 + ((unit.unit_id - 1) * 6)
local prv_start = 1 + ((unit.unit_id - 1) * 3)
local v_fields = { "pu", "po", "pl", "am" }
local v_names = {
sprintf("PV%02d-PU", v_start),
sprintf("PV%02d-PO", v_start + 1),
sprintf("PV%02d-PL", v_start + 2),
sprintf("PV%02d-AM", v_start + 3),
sprintf("PRV%02d", prv_start),
sprintf("PRV%02d", prv_start + 1),
sprintf("PRV%02d", prv_start + 2)
}
assert(parent.get_height() >= (y + height), "flow display not of sufficient vertical resolution (add an additional row of monitors) " .. y .. "," .. parent.get_height())
local function _wide(a, b) return util.trinary(wide, a, b) end
-- bounding box div
local root = Div{parent=parent,x=x,y=y,width=_wide(136, 114),height=height}
------------------
-- COOLING LOOP --
------------------
local reactor = Rectangle{parent=root,x=1,y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=reactor,y=1,text="FISSION REACTOR",alignment=ALIGN.CENTER}
TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=ALIGN.CENTER}
TextBox{parent=root,x=19,y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
TextBox{parent=root,x=3,y=5,text="\x19",width=1,fg_bg=lg_gray}
local rc_pipes = {}
if unit.num_boilers > 0 then
table.insert(rc_pipes, pipe(0, 1, _wide(28, 19), 1, colors.lightBlue, true))
table.insert(rc_pipes, pipe(0, 3, _wide(28, 19), 3, colors.orange, true))
table.insert(rc_pipes, pipe(_wide(46, 39), 1, _wide(72, 58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(_wide(46, 39), 3, _wide(72, 58), 3, colors.white, true))
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
table.insert(rc_pipes, pipe(0, 1, _wide(72, 58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(0, 3, _wide(72, 58), 3, colors.white, true))
if unit.aux_coolant then
table.insert(rc_pipes, pipe(8, 0, 8, 0, colors.blue, true))
end
end
if unit.has_tank then
local is_water = tank_types[tank_conns[unit_id]] == COOLANT_TYPE.WATER
-- emergency coolant connection x point
local emc_x = util.trinary(is_water and (unit.num_boilers > 0), 42, 3)
table.insert(rc_pipes, pipe(emc_x, 1, emc_x, 0, util.trinary(is_water, colors.blue, colors.lightBlue), true, true))
end
local prv_yo = math.max(3 - unit.num_turbines, 0)
for i = 1, unit.num_turbines do
local py = 2 * (i - 1) + prv_yo
table.insert(rc_pipes, pipe(_wide(92, 78), py, _wide(104, 83), py, colors.white, true))
end
PipeNetwork{parent=root,x=20,y=1,pipes=rc_pipes,bg=style.theme.bg}
if unit.num_boilers > 0 then
local cc_rate = DataIndicator{parent=root,x=_wide(25,22),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
local hc_rate = DataIndicator{parent=root,x=_wide(25,22),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
cc_rate.register(unit.unit_ps, "boiler_boil_sum", function (sum) cc_rate.update(sum * 10) end)
hc_rate.register(unit.unit_ps, "heating_rate", hc_rate.update)
local boiler = Rectangle{parent=root,x=_wide(47,40),y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=boiler,y=1,text="THERMO-ELECTRIC",alignment=ALIGN.CENTER}
TextBox{parent=boiler,y=3,text=util.trinary(unit.num_boilers>1,"BOILERS","BOILER"),alignment=ALIGN.CENTER}
TextBox{parent=root,x=_wide(47,40),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
TextBox{parent=root,x=_wide(65,58),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
local wt_rate = DataIndicator{parent=root,x=_wide(71,61),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
local st_rate = DataIndicator{parent=root,x=_wide(71,61),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
wt_rate.register(unit.unit_ps, "turbine_flow_sum", wt_rate.update)
st_rate.register(unit.unit_ps, "boiler_boil_sum", st_rate.update)
else
local wt_rate = DataIndicator{parent=root,x=28,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
local st_rate = DataIndicator{parent=root,x=28,y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
wt_rate.register(unit.unit_ps, "turbine_flow_sum", wt_rate.update)
st_rate.register(unit.unit_ps, "heating_rate", st_rate.update)
end
local turbine = Rectangle{parent=root,x=_wide(93,79),y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=turbine,y=1,text="STEAM TURBINE",alignment=ALIGN.CENTER}
TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=ALIGN.CENTER}
TextBox{parent=root,x=_wide(93,79),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
for i = 1, unit.num_turbines do
local ry = 1 + (2 * (i - 1)) + prv_yo
TextBox{parent=root,x=_wide(125,103),y=ry,text="\x10\x11\x7f",fg_bg=text_c,width=3}
local state = TriIndicatorLight{parent=root,x=_wide(129,107),y=ry,label=v_names[i+4],c1=style.ind_bkg,c2=style.ind_yel.fgd,c3=style.ind_red.fgd}
state.register(unit.turbine_ps_tbl[i], "SteamDumpOpen", state.update)
end
----------------------
-- WASTE PROCESSING --
----------------------
local waste = Div{parent=root,x=3,y=6}
local waste_c = style.theme.fuel_color
local waste_pipes = {
pipe(0, 0, _wide(19, 16), 1, colors.brown, true),
pipe(_wide(14, 13), 1, _wide(19, 17), 5, colors.brown, true),
pipe(_wide(22, 19), 1, _wide(49, 45), 1, colors.brown, true),
pipe(_wide(22, 19), 5, _wide(28, 24), 5, colors.brown, true),
pipe(_wide(64, 53), 1, _wide(95, 81), 1, colors.cyan, true),
pipe(_wide(48, 43), 4, _wide(71, 61), 4, colors.green, true),
pipe(_wide(66, 57), 4, _wide(71, 61), 8, colors.green, true),
pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.green, true),
pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.green, true),
pipe(_wide(108, 94), 1, _wide(132, 110), 6, waste_c, true, true),
pipe(_wide(108, 94), 4, _wide(111, 95), 1, waste_c, true, true),
pipe(_wide(132, 110), 6, _wide(130, 108), 6, waste_c, true, true)
}
PipeNetwork{parent=waste,x=1,y=1,pipes=waste_pipes,bg=style.theme.bg}
local function _valve(vx, vy, n)
TextBox{parent=waste,x=vx,y=vy,text="\x10\x11",fg_bg=text_c,width=2}
local conn = IndicatorLight{parent=waste,x=vx-3,y=vy+1,label=v_names[n],colors=ind_grn}
local open = IndicatorLight{parent=waste,x=vx-3,y=vy+2,label="OPEN",colors=ind_wht}
conn.register(unit.unit_ps, util.c("V_", v_fields[n], "_conn"), conn.update)
open.register(unit.unit_ps, util.c("V_", v_fields[n], "_state"), open.update)
end
local function _machine(mx, my, name)
local l = string.len(name) + 2
TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=ALIGN.CENTER,fg_bg=cpair(style.theme.bg,style.theme.header.bkg),width=l}
TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=ALIGN.CENTER,fg_bg=style.theme.header,width=l}
end
local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
local pu_rate = DataIndicator{parent=waste,x=_wide(82,70),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=s_field}
local po_rate = DataIndicator{parent=waste,x=_wide(52,45),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
local popl_rate = DataIndicator{parent=waste,x=_wide(82,70),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
local poam_rate = DataIndicator{parent=waste,x=_wide(82,70),y=10,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
local spent_rate = DataIndicator{parent=waste,x=_wide(117,98),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%8.3f",value=0,width=13,fg_bg=s_field}
waste_rate.register(unit.unit_ps, "act_burn_rate", waste_rate.update)
pu_rate.register(unit.unit_ps, "pu_rate", pu_rate.update)
po_rate.register(unit.unit_ps, "po_rate", po_rate.update)
popl_rate.register(unit.unit_ps, "po_pl_rate", popl_rate.update)
poam_rate.register(unit.unit_ps, "po_am_rate", poam_rate.update)
spent_rate.register(unit.unit_ps, "ws_rate", spent_rate.update)
_valve(_wide(21, 18), 2, 1)
_valve(_wide(21, 18), 6, 2)
_valve(_wide(73, 62), 5, 3)
_valve(_wide(73, 62), 9, 4)
_machine(_wide(51, 45), 1, "CENTRIFUGE \x1a");
_machine(_wide(97, 83), 1, "PRC [Pu] \x1a");
_machine(_wide(97, 83), 4, "PRC [Po] \x1a");
_machine(_wide(116, 94), 6, "SPENT WASTE \x1b")
TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=8,thin=true,fg_bg=style.theme.highlight_box_bright}
local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn}
local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c_d,label="CNT",unit="",format="%2d",value=0,width=7}
TextBox{parent=sna_po,y=3,text="PEAK\x1a",width=5,fg_bg=cpair(style.theme.label_dark,colors._INHERIT)}
TextBox{parent=sna_po,text="MAX \x1a",width=5,fg_bg=cpair(style.theme.label_dark,colors._INHERIT)}
local sna_pk = DataIndicator{parent=sna_po,x=6,y=3,lu_colors=lu_c_d,label="",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max_o = DataIndicator{parent=sna_po,x=6,lu_colors=lu_c_d,label="",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max_i = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="\x1aMAX",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="\x1aIN",unit="mB/t",format="%8.2f",value=0,width=17}
sna_act.register(unit.unit_ps, "po_rate", function (r) sna_act.update(r > 0) end)
sna_cnt.register(unit.unit_ps, "sna_count", sna_cnt.update)
sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.update)
sna_max_o.register(unit.unit_ps, "sna_max_rate", sna_max_o.update)
sna_max_i.register(unit.unit_ps, "sna_max_rate", function (r) sna_max_i.update(r * 10) end)
sna_in.register(unit.unit_ps, "sna_in", sna_in.update)
return root
end
return make

View File

@ -0,0 +1,175 @@
--
-- Basic Unit Overview
--
local core = require("graphics.core")
local style = require("coordinator.ui.style")
local reactor_view = require("coordinator.ui.components.reactor")
local boiler_view = require("coordinator.ui.components.boiler")
local turbine_view = require("coordinator.ui.components.turbine")
local Div = require("graphics.elements.Div")
local PipeNetwork = require("graphics.elements.PipeNetwork")
local TextBox = require("graphics.elements.TextBox")
local ALIGN = core.ALIGN
local pipe = core.pipe
-- make a new unit overview window
---@param parent Container parent
---@param x integer top left x
---@param y integer top left y
---@param unit ioctl_unit unit database entry
local function make(parent, x, y, unit)
local num_boilers = #unit.boiler_data_tbl
local num_turbines = #unit.turbine_data_tbl
assert(num_boilers >= 0 and num_boilers <= 2, "minimum 0 boilers, maximum 2 boilers")
assert(num_turbines >= 1 and num_turbines <= 3, "minimum 1 turbine, maximum 3 turbines")
local height = 25
if num_boilers == 0 and num_turbines == 1 then
height = 9
elseif num_boilers <= 1 and num_turbines <= 2 then
height = 17
end
assert(parent.get_height() >= (y + height), "main display not of sufficient vertical resolution (add an additional row of monitors)")
-- bounding box div
local root = Div{parent=parent,x=x,y=y,width=80,height=height}
-- unit header message
TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,fg_bg=style.theme.header}
-------------
-- REACTOR --
-------------
reactor_view(root, 1, 3, unit.unit_ps)
if num_boilers > 0 then
local coolant_pipes = {}
if num_boilers >= 2 then
table.insert(coolant_pipes, pipe(0, 0, 11, 12, colors.lightBlue))
end
table.insert(coolant_pipes, pipe(0, 0, 11, 3, colors.lightBlue))
table.insert(coolant_pipes, pipe(2, 0, 11, 2, colors.orange))
if num_boilers >= 2 then
table.insert(coolant_pipes, pipe(2, 0, 11, 11, colors.orange))
end
PipeNetwork{parent=root,x=4,y=10,pipes=coolant_pipes,bg=style.theme.bg}
end
-------------
-- BOILERS --
-------------
if num_boilers >= 1 then boiler_view(root, 16, 11, unit.boiler_ps_tbl[1]) end
if num_boilers >= 2 then boiler_view(root, 16, 19, unit.boiler_ps_tbl[2]) end
--------------
-- TURBINES --
--------------
local t_idx = 1
local no_boilers = num_boilers == 0
if (num_turbines >= 3) or no_boilers or (num_boilers == 1 and num_turbines >= 2) then
turbine_view(root, 58, 3, unit.turbine_ps_tbl[t_idx])
t_idx = t_idx + 1
end
if (num_turbines >= 1 and not no_boilers) or num_turbines >= 2 then
turbine_view(root, 58, 11, unit.turbine_ps_tbl[t_idx])
t_idx = t_idx + 1
end
if (num_turbines >= 2 and num_boilers >= 2) or num_turbines >= 3 then
turbine_view(root, 58, 19, unit.turbine_ps_tbl[t_idx])
end
local steam_pipes_b = {}
if no_boilers then
table.insert(steam_pipes_b, pipe(0, 1, 3, 1, colors.white)) -- steam to turbine 1
table.insert(steam_pipes_b, pipe(0, 2, 3, 2, colors.blue)) -- water to turbine 1
if num_turbines >= 2 then
table.insert(steam_pipes_b, pipe(1, 2, 3, 9, colors.white)) -- steam to turbine 2
table.insert(steam_pipes_b, pipe(2, 3, 3, 10, colors.blue)) -- water to turbine 2
end
if num_turbines >= 3 then
table.insert(steam_pipes_b, pipe(1, 9, 3, 17, colors.white)) -- steam boiler 1 to turbine 1 junction end
table.insert(steam_pipes_b, pipe(2, 10, 3, 18, colors.blue)) -- water boiler 1 to turbine 1 junction start
end
else
-- boiler side pipes
local steam_pipes_a = {
-- boiler 1 steam/water pipes
pipe(0, 1, 6, 1, colors.white, false, true), -- steam boiler 1 to turbine junction
pipe(0, 2, 6, 2, colors.blue, false, true) -- water boiler 1 to turbine junction
}
if num_boilers >= 2 then
-- boiler 2 steam/water pipes
table.insert(steam_pipes_a, pipe(0, 9, 6, 9, colors.white, false, true)) -- steam boiler 2 to turbine junction
table.insert(steam_pipes_a, pipe(0, 10, 6, 10, colors.blue, false, true)) -- water boiler 2 to turbine junction
end
-- turbine side pipes
if num_turbines >= 3 or (num_boilers == 1 and num_turbines == 2) then
table.insert(steam_pipes_b, pipe(0, 9, 1, 2, colors.white, false, true)) -- steam boiler 1 to turbine 1 junction start
table.insert(steam_pipes_b, pipe(1, 1, 3, 1, colors.white, false, false)) -- steam boiler 1 to turbine 1 junction end
end
table.insert(steam_pipes_b, pipe(0, 9, 3, 9, colors.white, false, true)) -- steam boiler 1 to turbine 2
if num_turbines >= 3 or (num_boilers == 1 and num_turbines == 2) then
table.insert(steam_pipes_b, pipe(0, 10, 2, 3, colors.blue, false, true)) -- water boiler 1 to turbine 1 junction start
table.insert(steam_pipes_b, pipe(2, 2, 3, 2, colors.blue, false, false)) -- water boiler 1 to turbine 1 junction end
end
table.insert(steam_pipes_b, pipe(0, 10, 3, 10, colors.blue, false, true)) -- water boiler 1 to turbine 2
if num_turbines >= 3 or (num_turbines >= 2 and num_boilers >= 2) then
if num_boilers >= 2 then
table.insert(steam_pipes_b, pipe(0, 17, 1, 9, colors.white, false, true)) -- steam boiler 2 to turbine 2 junction
table.insert(steam_pipes_b, pipe(0, 17, 3, 17, colors.white, false, true)) -- steam boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(0, 18, 2, 10, colors.blue, false, true)) -- water boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(0, 18, 3, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
else
table.insert(steam_pipes_b, pipe(1, 17, 1, 9, colors.white, false, true)) -- steam boiler 2 to turbine 2 junction
table.insert(steam_pipes_b, pipe(1, 17, 3, 17, colors.white, false, true)) -- steam boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(2, 18, 2, 10, colors.blue, false, true)) -- water boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(2, 18, 3, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
end
elseif num_turbines == 1 and num_boilers >= 2 then
table.insert(steam_pipes_b, pipe(0, 17, 1, 9, colors.white, false, true)) -- steam boiler 2 to turbine 2 junction
table.insert(steam_pipes_b, pipe(0, 17, 1, 17, colors.white, false, true)) -- steam boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(0, 18, 2, 10, colors.blue, false, true)) -- water boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(0, 18, 2, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
end
PipeNetwork{parent=root,x=47,y=11,pipes=steam_pipes_a,bg=style.theme.bg}
end
PipeNetwork{parent=root,x=54,y=3,pipes=steam_pipes_b,bg=style.theme.bg}
return root
end
return make

View File

@ -0,0 +1,443 @@
--
-- Flow Monitor GUI
--
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local unit_flow = require("coordinator.ui.components.unit_flow")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local PipeNetwork = require("graphics.elements.PipeNetwork")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local HorizontalBar = require("graphics.elements.indicators.HorizontalBar")
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local CONTAINER_MODE = types.CONTAINER_MODE
local COOLANT_TYPE = types.COOLANT_TYPE
local ALIGN = core.ALIGN
local cpair = core.cpair
local border = core.border
local pipe = core.pipe
local wh_gray = style.wh_gray
-- create new flow view
---@param main DisplayBox main displaybox
local function init(main)
local s_hi_bright = style.theme.highlight_box_bright
local s_field = style.theme.field_box
local text_col = style.text_colors
local lu_col = style.lu_colors
local lu_c_d = style.lu_colors_dark
local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units
local tank_defs = facility.tank_defs
local tank_conns = facility.tank_conns
local tank_list = facility.tank_list
local tank_types = facility.tank_fluid_types
-- window header message
local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
-- max length example: "01:23:45 AM - Wednesday, September 28 2022"
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,fg_bg=style.theme.header}
datetime.register(facility.ps, "date_time", datetime.set_value)
local po_pipes = {}
local emcool_pipes = {}
-- get the y offset for this unit index
---@param idx integer unit index
local function y_ofs(idx) return ((idx - 1) * 20) end
-- get the coolant color
---@param idx integer tank index
local function c_clr(idx) return util.trinary(tank_types[tank_conns[idx]] == COOLANT_TYPE.WATER, colors.blue, colors.lightBlue) end
-- determinte facility tank start/end from the definitions list
---@param start_idx integer start index of table iteration
---@param end_idx integer end index of table iteration
local function find_fdef(start_idx, end_idx)
local first, last = 4, 0
for i = start_idx, end_idx do
if tank_defs[i] == 2 then
last = i
if i < first then first = i end
end
end
return first, last
end
if facility.tank_mode == 0 or facility.tank_mode == 8 then
-- (0) tanks belong to reactor units OR (8) 4 total facility tanks (A B C D)
for i = 1, facility.num_units do
if units[i].has_tank then
local y = y_ofs(i)
local color = c_clr(i)
table.insert(emcool_pipes, pipe(2, y, 2, y + 3, color, true))
table.insert(emcool_pipes, pipe(2, y, 21, y, color, true))
local x = util.trinary((tank_types[tank_conns[i]] == COOLANT_TYPE.SODIUM) or (units[i].num_boilers == 0), 45, 84)
table.insert(emcool_pipes, pipe(21, y, x, y + 2, color, true, true))
end
end
else
-- setup connections for units with emergency coolant, always the same
for i = 1, #tank_defs do
if tank_defs[i] > 0 then
local y = y_ofs(i)
local color = c_clr(i)
if tank_defs[i] == 2 then
table.insert(emcool_pipes, pipe(1, y, 21, y, color, true))
else
table.insert(emcool_pipes, pipe(2, y, 2, y + 3, color, true))
table.insert(emcool_pipes, pipe(2, y, 21, y, color, true))
end
local 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
if facility.tank_mode == 1 then
-- (1) 1 total facility tank (A A A A)
local first_fdef, last_fdef = find_fdef(1, #tank_defs)
for i = 1, #tank_defs do
local y = y_ofs(i)
if i == first_fdef then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, c_clr(first_fdef), true))
elseif i < last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, c_clr(first_fdef), true))
end
end
end
elseif facility.tank_mode == 2 then
-- (2) 2 total facility tanks (A A A B)
local first_fdef, last_fdef = find_fdef(1, math.min(3, #tank_defs))
for i = 1, #tank_defs do
local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 4 then
if tank_defs[i] == 2 then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
end
elseif i == first_fdef then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
elseif i < last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
end
end
end
elseif facility.tank_mode == 3 then
-- (3) 2 total facility tanks (A A B B)
for _, a in pairs({ 1, 3 }) do
local b = a + 1
if tank_defs[a] == 2 then
table.insert(emcool_pipes, pipe(0, y_ofs(a), 1, y_ofs(a) + 6, c_clr(a), true))
if tank_defs[b] == 2 then
table.insert(emcool_pipes, pipe(0, y_ofs(b) - 13, 1, y_ofs(b), c_clr(a), true))
end
elseif tank_defs[b] == 2 then
table.insert(emcool_pipes, pipe(0, y_ofs(b), 1, y_ofs(b) + 6, c_clr(b), true))
end
end
elseif facility.tank_mode == 4 then
-- (4) 2 total facility tanks (A B B B)
local first_fdef, last_fdef = find_fdef(2, #tank_defs)
for i = 1, #tank_defs do
local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 1 then
if tank_defs[i] == 2 then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
end
elseif i == first_fdef then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
elseif i < last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
end
end
end
elseif facility.tank_mode == 5 then
-- (5) 3 total facility tanks (A A B C)
local first_fdef, last_fdef = find_fdef(1, math.min(2, #tank_defs))
for i = 1, #tank_defs do
local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 3 or i == 4 then
if tank_defs[i] == 2 then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
end
elseif i == first_fdef then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
elseif i < last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
end
end
end
elseif facility.tank_mode == 6 then
-- (6) 3 total facility tanks (A B B C)
local first_fdef, last_fdef = find_fdef(2, math.min(3, #tank_defs))
for i = 1, #tank_defs do
local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 1 or i == 4 then
if tank_defs[i] == 2 then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
end
elseif i == first_fdef then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
elseif i < last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
end
end
end
elseif facility.tank_mode == 7 then
-- (7) 3 total facility tanks (A B C C)
local first_fdef, last_fdef = find_fdef(3, #tank_defs)
for i = 1, #tank_defs do
local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 1 or i == 2 then
if tank_defs[i] == 2 then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
end
elseif i == first_fdef then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
elseif i < last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
end
end
end
end
end
local flow_x = 3
if #emcool_pipes > 0 then
flow_x = 25
PipeNetwork{parent=main,x=2,y=3,pipes=emcool_pipes,bg=style.theme.bg}
end
for i = 1, facility.num_units do
local y_offset = y_ofs(i)
unit_flow(main, flow_x, 5 + y_offset, #emcool_pipes == 0, i)
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.green, true, true))
util.nop()
end
PipeNetwork{parent=main,x=139,y=15,pipes=po_pipes,bg=style.theme.bg}
-----------------
-- tank valves --
-----------------
local next_f_id = 1
for i = 1, #tank_defs do
if tank_defs[i] > 0 then
local vy = 3 + y_ofs(i)
TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=text_col,width=2}
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", (i * 6) - 1),colors=style.ind_grn}
local open = IndicatorLight{parent=main,x=9,y=vy+2,label="OPEN",colors=style.ind_wht}
conn.register(units[i].unit_ps, "V_emc_conn", conn.update)
open.register(units[i].unit_ps, "V_emc_state", open.update)
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 --
-------------------
for i = 1, #tank_list do
if tank_list[i] > 0 then
local id = "U-" .. i
local f_id = next_f_id
if tank_list[i] == 2 then
id = "F-" .. next_f_id
next_f_id = next_f_id + 1
end
local y_offset = y_ofs(i)
local tank = Div{parent=main,x=3,y=7+y_offset,width=20,height=14}
TextBox{parent=tank,text=" ",x=1,y=1,fg_bg=style.lg_gray}
TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=ALIGN.CENTER,fg_bg=style.wh_gray}
local tank_box = Rectangle{parent=tank,border=border(1,colors.gray,true),width=20,height=12}
local status = StateIndicator{parent=tank_box,x=3,y=1,states=style.dtank.states,value=1,min_width=14}
TextBox{parent=tank_box,x=2,y=3,text="Fill",width=10,fg_bg=style.label}
local tank_pcnt = DataIndicator{parent=tank_box,x=10,y=3,label="",format="%5.2f",value=100,unit="%",lu_colors=lu_col,width=8,fg_bg=text_col}
local tank_amnt = DataIndicator{parent=tank_box,x=2,label="",format="%13d",value=0,commas=true,unit="mB",lu_colors=lu_col,width=16,fg_bg=s_field}
local is_water = tank_types[i] == COOLANT_TYPE.WATER
TextBox{parent=tank_box,x=2,y=6,text=util.trinary(is_water,"Water","Sodium").." Level",width=12,fg_bg=style.label}
local level = HorizontalBar{parent=tank_box,x=2,y=7,bar_fg_bg=cpair(util.trinary(is_water,colors.blue,colors.lightBlue),colors.gray),height=1,width=16}
TextBox{parent=tank_box,x=2,y=9,text="In/Out Mode",width=11,fg_bg=style.label}
local can_fill = IndicatorLight{parent=tank_box,x=2,y=10,label="FILL",colors=style.ind_wht}
local can_empty = IndicatorLight{parent=tank_box,x=10,y=10,label="EMPTY",colors=style.ind_wht}
local function _can_fill(mode)
can_fill.update((mode == CONTAINER_MODE.BOTH) or (mode == CONTAINER_MODE.FILL))
end
local function _can_empty(mode)
can_empty.update((mode == CONTAINER_MODE.BOTH) or (mode == CONTAINER_MODE.EMPTY))
end
if tank_list[i] == 1 then
status.register(units[i].tank_ps_tbl[1], "computed_status", status.update)
tank_pcnt.register(units[i].tank_ps_tbl[1], "fill", function (f) tank_pcnt.update(f * 100) end)
tank_amnt.register(units[i].tank_ps_tbl[1], "stored", function (sto) tank_amnt.update(sto.amount) end)
level.register(units[i].tank_ps_tbl[1], "fill", level.update)
can_fill.register(units[i].tank_ps_tbl[1], "container_mode", _can_fill)
can_empty.register(units[i].tank_ps_tbl[1], "container_mode", _can_empty)
else
status.register(facility.tank_ps_tbl[f_id], "computed_status", status.update)
tank_pcnt.register(facility.tank_ps_tbl[f_id], "fill", function (f) tank_pcnt.update(f * 100) end)
tank_amnt.register(facility.tank_ps_tbl[f_id], "stored", function (sto) tank_amnt.update(sto.amount) end)
level.register(facility.tank_ps_tbl[f_id], "fill", level.update)
can_fill.register(facility.tank_ps_tbl[f_id], "container_mode", _can_fill)
can_empty.register(facility.tank_ps_tbl[f_id], "container_mode", _can_empty)
end
end
end
util.nop()
---------
-- SPS --
---------
local sps = Div{parent=main,x=140,y=3,height=12}
TextBox{parent=sps,text=" ",width=24,x=1,y=1,fg_bg=style.lg_gray}
TextBox{parent=sps,text="SPS",alignment=ALIGN.CENTER,width=24,fg_bg=wh_gray}
local sps_box = Rectangle{parent=sps,border=border(1,colors.gray,true),width=24,height=10}
local status = StateIndicator{parent=sps_box,x=5,y=1,states=style.sps.states,value=1,min_width=14}
status.register(facility.sps_ps_tbl[1], "computed_status", status.update)
TextBox{parent=sps_box,x=2,y=3,text="Input Rate",width=10,fg_bg=style.label}
local sps_in = DataIndicator{parent=sps_box,x=2,label="",format="%15.2f",value=0,unit="mB/t",lu_colors=lu_col,width=20,fg_bg=s_field}
sps_in.register(facility.ps, "po_am_rate", sps_in.update)
TextBox{parent=sps_box,x=2,y=6,text="Production Rate",width=15,fg_bg=style.label}
local sps_rate = DataIndicator{parent=sps_box,x=2,label="",format="%15d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=20,fg_bg=s_field}
sps_rate.register(facility.sps_ps_tbl[1], "process_rate", function (r) sps_rate.update(r * 1000) end)
----------------
-- statistics --
----------------
TextBox{parent=main,x=145,y=16,text="RAW WASTE",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
local raw_waste = Rectangle{parent=main,x=145,y=17,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=s_hi_bright}
local sum_raw_waste = DataIndicator{parent=raw_waste,lu_colors=lu_c_d,label="SUM",unit="mB/t",format="%8.2f",value=0,width=17}
sum_raw_waste.register(facility.ps, "burn_sum", sum_raw_waste.update)
TextBox{parent=main,x=145,y=21,text="PROC. WASTE",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=s_hi_bright}
local pu = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17}
local po = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="Po",unit="mB/t",format="%9.2f",value=0,width=17}
local popl = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="PoPl",unit="mB/t",format="%7.2f",value=0,width=17}
pu.register(facility.ps, "pu_rate", pu.update)
po.register(facility.ps, "po_rate", po.update)
popl.register(facility.ps, "po_pl_rate", popl.update)
TextBox{parent=main,x=145,y=28,text="SPENT WASTE",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
local sp_waste = Rectangle{parent=main,x=145,y=29,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=s_hi_bright}
local sum_sp_waste = DataIndicator{parent=sp_waste,lu_colors=lu_c_d,label="SUM",unit="mB/t",format="%8.3f",value=0,width=17}
sum_sp_waste.register(facility.ps, "spent_waste_rate", sum_sp_waste.update)
end
return init

View File

@ -0,0 +1,170 @@
--
-- Coordinator Front Panel GUI
--
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local pgi = require("coordinator.ui.pgi")
local style = require("coordinator.ui.style")
local pkt_entry = require("coordinator.ui.components.pkt_entry")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local TabBar = require("graphics.elements.controls.TabBar")
local LED = require("graphics.elements.indicators.LED")
local LEDPair = require("graphics.elements.indicators.LEDPair")
local RGBLED = require("graphics.elements.indicators.RGBLED")
local LINK_STATE = types.PANEL_LINK_STATE
local ALIGN = core.ALIGN
local cpair = core.cpair
local led_grn = style.led_grn
-- create new front panel view
---@param panel DisplayBox main displaybox
---@param num_units integer number of units (number of unit monitors)
local function init(panel, num_units)
local ps = iocontrol.get_db().fp.ps
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}
--
-- system indicators
--
local main_page = Div{parent=page_div,x=1,y=1}
local system = Div{parent=main_page,width=14,height=17,x=2,y=2}
local status = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=led_grn}
status.update(true)
system.line_break()
heartbeat.register(ps, "heartbeat", heartbeat.update)
local modem = LED{parent=system,label="MODEM",colors=led_grn}
if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.fp_ind_bkg}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(ps, "link_state", network.update)
else
local nt_lnk = LEDPair{parent=system,label="NT LINKED",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
local nt_ver = LEDPair{parent=system,label="NT VERSION",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
nt_lnk.register(ps, "link_state", function (state)
local value = 2
if state == LINK_STATE.DISCONNECTED then
value = 1
elseif state == LINK_STATE.LINKED then
value = 3
end
nt_lnk.update(value)
end)
nt_ver.register(ps, "link_state", function (state)
local value = 3
if state == LINK_STATE.BAD_VERSION then
value = 2
elseif state == LINK_STATE.DISCONNECTED then
value = 1
end
nt_ver.update(value)
end)
end
system.line_break()
modem.register(ps, "has_modem", modem.update)
local speaker = LED{parent=system,label="SPEAKER",colors=led_grn}
speaker.register(ps, "has_speaker", speaker.update)
system.line_break()
local rt_main = LED{parent=system,label="RT MAIN",colors=led_grn}
local rt_render = LED{parent=system,label="RT RENDER",colors=led_grn}
rt_main.register(ps, "routine__main", rt_main.update)
rt_render.register(ps, "routine__render", rt_render.update)
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=style.fp.disabled_fg}
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=led_grn}
main_monitor.register(ps, "main_monitor", main_monitor.update)
local flow_monitor = LED{parent=monitors,label="FLOW MONITOR",colors=led_grn}
flow_monitor.register(ps, "flow_monitor", flow_monitor.update)
monitors.line_break()
for i = 1, num_units do
local unit_monitor = LED{parent=monitors,label="UNIT "..i.." MONITOR",colors=led_grn}
unit_monitor.register(ps, "unit_monitor_" .. i, unit_monitor.update)
end
--
-- about footer
--
local about = Div{parent=main_page,width=15,height=2,y=term_h-3,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
--
-- page handling
--
-- API page
local api_page = Div{parent=page_div,x=1,y=1,hidden=true}
local api_list = ListBox{parent=api_page,y=1,height=term_h-2,width=term_w,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=api_list,height=1} -- padding
-- assemble page panes
local panes = { main_page, api_page }
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
local tabs = {
{ name = "CRD", color = style.fp.text },
{ name = "API", color = style.fp.text },
}
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=style.fp_theme.highlight_box_bright}
-- link pocket API list management to PGI
pgi.link_elements(api_list, pkt_entry)
end
return init

View File

@ -0,0 +1,94 @@
--
-- Main SCADA Coordinator GUI
--
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local imatrix = require("coordinator.ui.components.imatrix")
local process_ctl = require("coordinator.ui.components.process_ctl")
local unit_overview = require("coordinator.ui.components.unit_overview")
local core = require("graphics.core")
local TextBox = require("graphics.elements.TextBox")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local ALIGN = core.ALIGN
-- create new main view
---@param main DisplayBox main displaybox
local function init(main)
local s_header = style.theme.header
local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units
-- window header message
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=ALIGN.CENTER,fg_bg=s_header}
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=style.lg_white,width=12,fg_bg=s_header}
-- max length example: "01:23:45 AM - Wednesday, September 28 2022"
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,fg_bg=s_header}
ping.register(facility.ps, "sv_ping", ping.update)
datetime.register(facility.ps, "date_time", datetime.set_value)
---@type Div, Div, Div, Div
local uo_1, uo_2, uo_3, uo_4
local cnc_y_start = 3
local row_1_height = 0
-- unit overviews
if facility.num_units >= 1 then
uo_1 = unit_overview(main, 2, 3, units[1])
row_1_height = uo_1.get_height()
end
if facility.num_units >= 2 then
uo_2 = unit_overview(main, 84, 3, units[2])
row_1_height = math.max(row_1_height, uo_2.get_height())
end
cnc_y_start = cnc_y_start + row_1_height + 1
util.nop()
if facility.num_units >= 3 then
-- base offset 3, spacing 1, max height of units 1 and 2
local row_2_offset = cnc_y_start
uo_3 = unit_overview(main, 2, row_2_offset, units[3])
cnc_y_start = row_2_offset + uo_3.get_height() + 1
if facility.num_units == 4 then
uo_4 = unit_overview(main, 84, row_2_offset, units[4])
cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.get_height() + 1)
end
util.nop()
end
-- command & control
-- induction matrix and process control interfaces are 24 tall + space needed for divider
local cnc_bottom_align_start = main.get_height() - 26
assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)")
TextBox{parent=main,y=cnc_bottom_align_start,text=string.rep("\x8c", header.get_width()),alignment=ALIGN.CENTER,fg_bg=style.lg_gray}
cnc_bottom_align_start = cnc_bottom_align_start + 2
process_ctl(main, 2, cnc_bottom_align_start)
util.nop()
imatrix(main, 131, cnc_bottom_align_start, facility.induction_ps_tbl[1])
end
return init

View File

@ -0,0 +1,14 @@
--
-- Reactor Unit SCADA Coordinator GUI
--
local unit_detail = require("coordinator.ui.components.unit_detail")
-- create a unit view
---@param main DisplayBox main displaybox
---@param id integer
local function init(main, id)
unit_detail(main, id)
end
return init

62
coordinator/ui/pgi.lua Normal file
View File

@ -0,0 +1,62 @@
--
-- Protected Graphics Interface
--
local log = require("scada-common.log")
local util = require("scada-common.util")
local pgi = {}
local data = {
pkt_list = nil, ---@type ListBox|nil
pkt_entry = nil, ---@type function
-- session entries
s_entries = {
pkt = {} ---@type Div[]
}
}
-- link list boxes
---@param pkt_list ListBox pocket list element
---@param pkt_entry fun(parent: ListBox, id: integer) : Div pocket entry constructor
function pgi.link_elements(pkt_list, pkt_entry)
data.pkt_list = pkt_list
data.pkt_entry = pkt_entry
end
-- unlink all fields, disabling the PGI
function pgi.unlink()
data.pkt_list = nil
data.pkt_entry = nil
end
-- add a PKT entry to the PKT list
---@param session_id integer pocket session
function pgi.create_pkt_entry(session_id)
if data.pkt_list ~= nil and data.pkt_entry ~= nil then
local success, result = pcall(data.pkt_entry, data.pkt_list, session_id)
if success then
data.s_entries.pkt[session_id] = result
else
log.error(util.c("PGI: failed to create PKT entry (", result, ")"), true)
end
end
end
-- delete a PKT entry from the PKT list
---@param session_id integer pocket session
function pgi.delete_pkt_entry(session_id)
if data.s_entries.pkt[session_id] ~= nil then
local success, result = pcall(data.s_entries.pkt[session_id].delete)
data.s_entries.pkt[session_id] = nil
if not success then
log.error(util.c("PGI: failed to delete PKT entry (", result, ")"), true)
end
else
log.debug(util.c("PGI: tried to delete unknown PKT entry ", session_id))
end
end
return pgi

260
coordinator/ui/style.lua Normal file
View File

@ -0,0 +1,260 @@
--
-- Graphics Style Options
--
local util = require("scada-common.util")
local core = require("graphics.core")
local themes = require("graphics.themes")
local coordinator = require("coordinator.coordinator")
---@class crd_style
local style = {}
local cpair = core.cpair
local config = coordinator.config
-- front panel styling
style.fp_theme = themes.sandstone
style.fp = themes.get_fp_style(style.fp_theme)
style.led_grn = cpair(colors.green, colors.green_off)
-- main GUI styling
---@class theme
local smooth_stone = {
text = colors.black,
text_inv = colors.white,
label = colors.gray,
label_dark = colors.gray,
disabled = colors.lightGray,
bg = colors.lightGray,
checkbox_bg = colors.black,
accent_light = colors.white,
accent_dark = colors.gray,
fuel_color = colors.black,
header = cpair(colors.white, colors.gray),
text_fg = cpair(colors.black, colors._INHERIT),
label_fg = cpair(colors.gray, colors._INHERIT),
disabled_fg = cpair(colors.lightGray, colors._INHERIT),
highlight_box = cpair(colors.black, colors.white),
highlight_box_bright = cpair(colors.black, colors.white),
field_box = cpair(colors.black, colors.white),
colors = themes.smooth_stone.colors,
-- color re-mappings for assistive modes
color_modes = themes.smooth_stone.color_modes
}
---@type theme
local deepslate = {
text = colors.white,
text_inv = colors.black,
label = colors.lightGray,
label_dark = colors.gray,
disabled = colors.gray,
bg = colors.black,
checkbox_bg = colors.gray,
accent_light = colors.gray,
accent_dark = colors.lightGray,
fuel_color = colors.lightGray,
header = cpair(colors.white, colors.gray),
text_fg = cpair(colors.white, colors._INHERIT),
label_fg = cpair(colors.lightGray, colors._INHERIT),
disabled_fg = cpair(colors.gray, colors._INHERIT),
highlight_box = cpair(colors.white, colors.gray),
highlight_box_bright = cpair(colors.black, colors.lightGray),
field_box = cpair(colors.white, colors.gray),
colors = themes.deepslate.colors,
-- color re-mappings for assistive modes
color_modes = themes.deepslate.color_modes
}
style.theme = smooth_stone
-- set themes per configurations
---@param main UI_THEME main UI theme
---@param fp FP_THEME front panel theme
---@param color_mode COLOR_MODE the color mode to use
function style.set_themes(main, fp, color_mode)
local colorblind = color_mode ~= themes.COLOR_MODE.STANDARD and color_mode ~= themes.COLOR_MODE.STD_ON_BLACK
local gray_ind_off = color_mode == themes.COLOR_MODE.STANDARD or color_mode == themes.COLOR_MODE.BLUE_IND
style.ind_bkg = colors.gray
style.fp_ind_bkg = util.trinary(gray_ind_off, colors.gray, colors.black)
style.ind_hi_box_bg = util.trinary(gray_ind_off, colors.gray, colors.black)
if main == themes.UI_THEME.SMOOTH_STONE then
style.theme = smooth_stone
style.ind_bkg = util.trinary(gray_ind_off, colors.gray, colors.black)
elseif main == themes.UI_THEME.DEEPSLATE then
style.theme = deepslate
style.ind_hi_box_bg = util.trinary(gray_ind_off, colors.lightGray, colors.black)
end
style.colorblind = colorblind
style.root = cpair(style.theme.text, style.theme.bg)
style.label = cpair(style.theme.label, style.theme.bg)
-- high contrast text (also tags)
style.hc_text = cpair(style.theme.text, style.theme.text_inv)
-- text on default background
style.text_colors = cpair(style.theme.text, style.theme.bg)
-- label & unit colors
style.lu_colors = cpair(style.theme.label, style.theme.label)
-- label & unit colors (darker if set)
style.lu_colors_dark = cpair(style.theme.label_dark, style.theme.label_dark)
style.ind_grn = cpair(util.trinary(colorblind, colors.blue, colors.green), style.ind_bkg)
style.ind_yel = cpair(colors.yellow, style.ind_bkg)
style.ind_red = cpair(colors.red, style.ind_bkg)
style.ind_wht = cpair(colors.white, style.ind_bkg)
if fp == themes.FP_THEME.SANDSTONE then
style.fp_theme = themes.sandstone
elseif fp == themes.FP_THEME.BASALT then
style.fp_theme = themes.basalt
end
style.fp = themes.get_fp_style(style.fp_theme)
end
-- COMMON COLOR PAIRS --
style.wh_gray = cpair(colors.white, colors.gray)
style.bw_fg_bg = cpair(colors.black, colors.white)
style.hzd_fg_bg = style.wh_gray
style.dis_colors = cpair(colors.white, colors.lightGray)
style.lg_gray = cpair(colors.lightGray, colors.gray)
style.lg_white = cpair(colors.lightGray, colors.white)
style.gray_white = cpair(colors.gray, colors.white)
-- UI COMPONENTS --
style.reactor = {
-- reactor states<br>
---@see REACTOR_STATE
states = {
{ color = cpair(colors.black, colors.yellow), text = "PLC OFF-LINE" },
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
{ color = cpair(colors.black, colors.orange), text = "PLC FAULT" },
{ color = cpair(colors.white, colors.gray), text = "DISABLED" },
{ color = cpair(colors.black, colors.green), text = "ACTIVE" },
{ color = cpair(colors.black, colors.red), text = "SCRAMMED" },
{ color = cpair(colors.black, colors.red), text = "FORCE DISABLED" }
}
}
style.boiler = {
-- boiler states<br>
---@see BOILER_STATE
states = {
{ color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
{ color = cpair(colors.black, colors.orange), text = "RTU FAULT" },
{ color = cpair(colors.white, colors.gray), text = "IDLE" },
{ color = cpair(colors.black, colors.green), text = "ACTIVE" }
}
}
style.turbine = {
-- turbine states<br>
---@see TURBINE_STATE
states = {
{ color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
{ color = cpair(colors.black, colors.orange), text = "RTU FAULT" },
{ color = cpair(colors.white, colors.gray), text = "IDLE" },
{ color = cpair(colors.black, colors.green), text = "ACTIVE" },
{ color = cpair(colors.black, colors.red), text = "TRIP" }
}
}
style.dtank = {
-- dynamic tank states<br>
---@see TANK_STATE
states = {
{ color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
{ color = cpair(colors.black, colors.orange), text = "RTU FAULT" },
{ color = cpair(colors.black, colors.green), text = "ONLINE" },
{ color = cpair(colors.black, colors.yellow), text = "LOW FILL" },
{ color = cpair(colors.black, colors.green), text = "FILLED" }
}
}
style.imatrix = {
-- induction matrix states<br>
---@see IMATRIX_STATE
states = {
{ color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
{ color = cpair(colors.black, colors.orange), text = "RTU FAULT" },
{ color = cpair(colors.black, colors.green), text = "ONLINE" },
{ color = cpair(colors.black, colors.yellow), text = "LOW CHARGE" },
{ color = cpair(colors.black, colors.yellow), text = "HIGH CHARGE" }
}
}
style.sps = {
-- SPS states<br>
---@see SPS_STATE
states = {
{ color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
{ color = cpair(colors.black, colors.orange), text = "RTU FAULT" },
{ color = cpair(colors.white, colors.gray), text = "IDLE" },
{ color = cpair(colors.black, colors.green), text = "ACTIVE" }
}
}
-- get waste styling, which depends on the configuration
---@return { states: { color: color, text: string }, states_abbrv: { color: color, text: string }, options: string[], unit_opts: { text: string, fg_bg: cpair, active_fg_bg:cpair } }
function style.get_waste()
local pu_color = util.trinary(config.GreenPuPellet, colors.green, colors.cyan)
local po_color = util.trinary(config.GreenPuPellet, colors.cyan, colors.green)
return {
-- auto waste processing states
states = {
{ color = cpair(colors.black, pu_color), text = "PLUTONIUM" },
{ color = cpair(colors.black, po_color), text = "POLONIUM" },
{ color = cpair(colors.black, colors.purple), text = "ANTI MATTER" }
},
states_abbrv = {
{ color = cpair(colors.black, pu_color), text = "Pu" },
{ color = cpair(colors.black, po_color), text = "Po" },
{ color = cpair(colors.black, colors.purple), text = "AM" }
},
-- process radio button options
options = { "Plutonium", "Polonium", "Antimatter" },
-- unit waste selection
unit_opts = {
{ text = "Auto", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.white, colors.gray) },
{ text = "Pu", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, pu_color) },
{ text = "Po", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, po_color) },
{ text = "AM", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.purple) }
}
}
end
return style

349
graphics/core.lua Normal file
View File

@ -0,0 +1,349 @@
--
-- Graphics Core Types, Checks, and Constructors
--
local events = require("graphics.events")
local flasher = require("graphics.flasher")
local core = {}
core.version = "2.4.8"
core.flasher = flasher
core.events = events
-- Core Types
---@enum ALIGN
core.ALIGN = { LEFT = 1, CENTER = 2, RIGHT = 3 }
---@alias Container DisplayBox|Div|ListBox|MultiPane|AppMultiPane|Rectangle
---@class graphics_border
---@field width integer
---@field color color
---@field even boolean
---@alias element_id string|integer
-- create a new border definition
---@nodiscard
---@param width integer border width
---@param color color border color
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
---@return graphics_border
function core.border(width, color, even)
return { width = width, color = color, even = even or false }
end
---@class graphics_frame
---@field x integer
---@field y integer
---@field w integer
---@field h integer
-- create a new graphics frame definition
---@nodiscard
---@param x integer
---@param y integer
---@param w integer
---@param h integer
---@return graphics_frame
function core.gframe(x, y, w, h)
return { x = x, y = y, w = w, h = h }
end
---@class cpair
---@field color_a color
---@field color_b color
---@field blit_a string
---@field blit_b string
---@field fgd color
---@field bkg color
---@field blit_fgd string
---@field blit_bkg string
-- add inherited flag, 3 isn't a pure color so it wouldn't be used
colors._INHERIT = 3
-- create a new color pair definition
---@nodiscard
---@param a color
---@param b color
---@return cpair
function core.cpair(a, b)
return {
-- color pairs
color_a = a, color_b = b, blit_a = colors.toBlit(a), blit_b = colors.toBlit(b),
-- aliases
fgd = a, bkg = b, blit_fgd = colors.toBlit(a), blit_bkg = colors.toBlit(b)
}
end
---@class pipe
---@field x1 integer starting x, origin is 0
---@field y1 integer starting y, origin is 0
---@field x2 integer ending x, origin is 0
---@field y2 integer ending y, origin is 0
---@field w integer width
---@field h integer height
---@field color color pipe color
---@field thin boolean true for 1 subpixel, false (default) for 2
---@field align_tr boolean false to align bottom left (default), true to align top right
-- create a new pipe<br>
-- note: pipe coordinate origin is (0, 0)
---@nodiscard
---@param x1 integer starting x, origin is 0
---@param y1 integer starting y, origin is 0
---@param x2 integer ending x, origin is 0
---@param y2 integer ending y, origin is 0
---@param color color pipe color
---@param thin? boolean true for 1 subpixel, false (default) for 2
---@param align_tr? boolean false to align bottom left (default), true to align top right
---@return pipe
function core.pipe(x1, y1, x2, y2, color, thin, align_tr)
return {
x1 = x1,
y1 = y1,
x2 = x2,
y2 = y2,
w = math.abs(x2 - x1) + 1,
h = math.abs(y2 - y1) + 1,
color = color,
thin = thin or false,
align_tr = align_tr or false
}
end
-- Assertion Handling
-- extract the custom element assert message, dropping the path to the element file
function core.extract_assert_msg(msg)
return string.sub(msg, (string.find(msg, "@") or 0) + 1)
end
-- Interactive Field Manager
---@param e graphics_base element
---@param max_len integer max value length
---@param fg_bg cpair enabled fg/bg
---@param dis_fg_bg? cpair disabled fg/bg
---@param align_right? boolean true to align content right while unfocused
function core.new_ifield(e, max_len, fg_bg, dis_fg_bg, align_right)
local self = {
frame_start = 1,
visible_text = e.value,
cursor_pos = string.len(e.value) + 1,
align_offset = 0,
selected_all = false
}
-- update visible text
local function _update_visible()
self.visible_text = string.sub(e.value, self.frame_start, self.frame_start + math.min(string.len(e.value), e.frame.w) - 1)
end
-- try shifting frame left
local function _try_lshift()
if self.frame_start > 1 then
self.frame_start = self.frame_start - 1
return true
end
end
-- try shifting frame right
local function _try_rshift()
if (self.frame_start + e.frame.w - 1) <= string.len(e.value) then
self.frame_start = self.frame_start + 1
return true
end
end
---@class ifield
local public = {}
-- censor the display (for private info, for example) with the provided character<br>
-- disable by passing no argument
---@param censor string? character to hide data with
function public.censor(censor)
if type(censor) == "string" and string.len(censor) == 1 then
self.censor = censor
else self.censor = nil end
public.show()
end
-- show the field
function public.show()
_update_visible()
if e.enabled then
e.w_set_bkg(fg_bg.bkg)
e.w_set_fgd(fg_bg.fgd)
elseif dis_fg_bg ~= nil then
e.w_set_bkg(dis_fg_bg.bkg)
e.w_set_fgd(dis_fg_bg.fgd)
end
-- clear and print
e.w_set_cur(1, 1)
e.w_write(string.rep(" ", e.frame.w))
e.w_set_cur(1, 1)
local function _write(align_r)
if align_r and string.len(self.visible_text) <=e.frame.w then
self.align_offset = (e.frame.w - string.len(self.visible_text))
e.w_set_cur((e.frame.w - string.len(self.visible_text)) + 1, 1)
end
if self.censor then
e.w_write(string.rep(self.censor, string.len(self.visible_text)))
else
e.w_write(self.visible_text)
end
end
if e.is_focused() and e.enabled then
-- write text with cursor
if self.selected_all then
e.w_set_bkg(fg_bg.fgd)
e.w_set_fgd(fg_bg.bkg)
_write()
elseif self.cursor_pos >= (string.len(self.visible_text) + 1) then
-- write text with cursor at the end, no need to blit
_write()
e.w_set_fgd(colors.lightGray)
e.w_write("_")
else
local a, b = "", ""
if self.cursor_pos <= string.len(self.visible_text) then
a = fg_bg.blit_bkg
b = fg_bg.blit_fgd
end
local b_fgd = string.rep(fg_bg.blit_fgd, self.cursor_pos - 1) .. a .. string.rep(fg_bg.blit_fgd, string.len(self.visible_text) - self.cursor_pos)
local b_bkg = string.rep(fg_bg.blit_bkg, self.cursor_pos - 1) .. b .. string.rep(fg_bg.blit_bkg, string.len(self.visible_text) - self.cursor_pos)
if self.censor then
e.w_blit(string.rep(self.censor, string.len(self.visible_text)), b_fgd, b_bkg)
else
e.w_blit(self.visible_text, b_fgd, b_bkg)
end
end
else
self.selected_all = false
-- write text without cursor
_write(align_right)
end
end
-- get an x value to pass to move_cursor taking into account right alignment offset present when unfocused
---@param x integer
function public.get_cursor_align_shift(x)
return math.max(0, x - self.align_offset)
end
-- move cursor to x
---@param x integer x position or 0 to jump to the end
function public.move_cursor(x)
self.selected_all = false
if x <= 0 then
self.cursor_pos = string.len(self.visible_text) + 1
else
self.cursor_pos = math.min(x, string.len(self.visible_text) + 1)
end
public.show()
end
-- select all text
function public.select_all()
self.selected_all = true
public.show()
end
-- set field value
---@param val string
function public.set_value(val)
e.value = string.sub(val, 1, math.min(max_len, string.len(val)))
public.nav_end()
end
-- try to insert a character if there is space
---@param char string
function public.try_insert_char(char)
-- limit length
if string.len(e.value) >= max_len then return end
-- replace if selected all, insert otherwise
if self.selected_all then
self.selected_all = false
self.cursor_pos = 2
self.frame_start = 1
e.value = char
public.show()
else
e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 2) .. char .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value))
_update_visible()
public.nav_right()
end
end
-- remove charcter before cursor if there is anything to remove, or delete all if selected all
function public.backspace()
if self.selected_all then
self.selected_all = false
e.value = ""
self.cursor_pos = 1
self.frame_start = 1
public.show()
else
if self.frame_start + self.cursor_pos > 2 then
e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 3) .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value))
if self.cursor_pos > 1 then
self.cursor_pos = self.cursor_pos - 1
public.show()
elseif _try_lshift() then public.show() end
end
end
end
-- move cursor left by one
function public.nav_left()
if self.cursor_pos > 1 then
self.cursor_pos = self.cursor_pos - 1
public.show()
elseif _try_lshift() then public.show() end
end
-- move cursor right by one
function public.nav_right()
if self.cursor_pos < math.min(string.len(self.visible_text) + 1, e.frame.w) then
self.cursor_pos = self.cursor_pos + 1
public.show()
elseif _try_rshift() then public.show() end
end
-- move cursor to the start
function public.nav_start()
self.cursor_pos = 1
self.frame_start = 1
public.show()
end
-- move cursor to the end
function public.nav_end()
self.frame_start = math.max(1, string.len(e.value) - e.frame.w + 2)
_update_visible()
self.cursor_pos = string.len(self.visible_text) + 1
public.show()
end
return public
end
return core

891
graphics/element.lua Normal file
View File

@ -0,0 +1,891 @@
--
-- Generic Graphics Element
--
local util = require("scada-common.util")
local core = require("graphics.core")
local events = core.events
local element = {}
---@class graphics_args
---@field window? Window base window to use, only root elements should use this
---@field parent? graphics_element parent element, if not a root element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer next line if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
---@field can_focus? boolean true if this element can be focused, false by default
---@class element_subscription
---@field ps psil ps used
---@field key string data key
---@field func function callback
-- more detailed assert message for element verification
---@param condition any assert condition
---@param msg string assert message
---@param callstack_offset? integer shift value to change targets of debug.getinfo()
function element.assert(condition, msg, callstack_offset)
callstack_offset = callstack_offset or 0
local caller = debug.getinfo(3 + callstack_offset)
assert(condition, util.c(caller.source, ":", caller.currentline, "{", debug.getinfo(2 + callstack_offset).name, "}: ", msg))
end
-- a base graphics element, should not be created on its own
---@nodiscard
---@param args graphics_args arguments
---@param constraint? function apply a dimensional constraint based on proposed dimensions function(frame) -> width, height
---@param child_offset_x? integer mouse event offset x
---@param child_offset_y? integer mouse event offset y
function element.new(args, constraint, child_offset_x, child_offset_y)
local self = {
id = nil, ---@type element_id|nil
is_root = args.parent == nil,
elem_type = debug.getinfo(2).name,
define_completed = false,
p_window = nil, ---@type Window
position = events.new_coord_2d(1, 1),
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
offset_x = 0,
offset_y = 0,
next_y = 1, -- next child y coordinate
next_id = 1, -- next child ID
subscriptions = {}, ---@type { ps: psil, key: string, func: function }[]
button_down = { events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1) },
focused = false,
mt = {}
}
---@class graphics_base
local protected = {
enabled = true,
value = nil, ---@type any
window = nil, ---@type Window
content_window = nil, ---@type Window|nil
mouse_window_shift = { x = 0, y = 0 },
fg_bg = core.cpair(colors.white, colors.black),
frame = core.gframe(1, 1, 1, 1),
children = {}, ---@type graphics_base[]
child_id_map = {} ---@type { [element_id]: integer }
}
-- element as string
function self.mt.__tostring()
return util.c("graphics.element{", self.elem_type, "} @ ", self)
end
---@class graphics_element
local public = {}
setmetatable(public, self.mt)
------------------------------
--#region PRIVATE FUNCTIONS --
------------------------------
-- use tab to jump to the next focusable field
---@param reverse boolean
local function _tab_focusable(reverse)
local first_f = nil ---@type graphics_element|nil
local prev_f = nil ---@type graphics_element|nil
local cur_f = nil ---@type graphics_element|nil
local done = false
---@param elem graphics_element
local function handle_element(elem)
if elem.is_visible() and elem.is_focusable() and elem.is_enabled() then
if first_f == nil then first_f = elem end
if cur_f == nil then
if elem.is_focused() then
cur_f = elem
if (not done) and (reverse and prev_f ~= nil) then
cur_f.unfocus()
prev_f.focus()
done = true
end
end
else
if elem.is_focused() then
elem.unfocus()
elseif not (reverse or done) then
cur_f.unfocus()
elem.focus()
done = true
end
end
prev_f = elem
end
end
---@param children graphics_base[]
local function traverse(children)
for i = 1, #children do
local child = children[i]
handle_element(child.get())
if child.get().is_visible() then traverse(child.children) end
end
end
traverse(protected.children)
-- if no element was focused, wrap focus
if first_f ~= nil and not done then
if reverse then
if cur_f ~= nil then cur_f.unfocus() end
if prev_f ~= nil then prev_f.focus() end
else
if cur_f ~= nil then cur_f.unfocus() end
first_f.focus()
end
end
end
--#endregion
--------------------------------
--#region PROTECTED FUNCTIONS --
--------------------------------
-- prepare the template
---@param offset_x integer x offset for mouse events
---@param offset_y integer y offset for mouse events
---@param next_y integer next line if no y was provided
function protected.prepare_template(offset_x, offset_y, next_y)
-- don't auto incrememnt y if inheriting height, that would cause an assertion
next_y = util.trinary(args.height == nil and constraint == nil, 1, next_y)
-- record offsets in case there is a reposition
self.offset_x = offset_x
self.offset_y = offset_y
-- get frame coordinates/size
if args.gframe ~= nil then
protected.frame.x = args.gframe.x
protected.frame.y = args.gframe.y
protected.frame.w = args.gframe.w
protected.frame.h = args.gframe.h
else
local w, h = self.p_window.getSize()
protected.frame.x = args.x or 1
protected.frame.y = args.y or next_y
protected.frame.w = args.width or w
protected.frame.h = args.height or h
end
-- adjust window frame if applicable
local f = protected.frame
if args.parent ~= nil then
-- constrain to parent inner width/height
local w, h = self.p_window.getSize()
f.w = math.min(f.w, w - (f.x - 1))
f.h = math.min(f.h, h - (f.y - 1))
if type(constraint) == "function" then
-- constrain per provided constraint function (can only get smaller than available space)
w, h = constraint(f)
f.w = math.min(f.w, w)
f.h = math.min(f.h, h)
end
end
-- check frame
element.assert(f.x >= 1, "frame x not >= 1", 3)
element.assert(f.y >= 1, "frame y not >= 1", 3)
element.assert(f.w >= 1, "frame width not >= 1", 3)
element.assert(f.h >= 1, "frame height not >= 1", 3)
-- create window
protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, args.hidden ~= true)
-- init colors
if args.fg_bg ~= nil then
protected.fg_bg = core.cpair(args.fg_bg.fgd, args.fg_bg.bkg)
end
if args.parent ~= nil then
local p_fg_bg = args.parent.get_fg_bg()
if args.fg_bg == nil then
protected.fg_bg = core.cpair(p_fg_bg.fgd, p_fg_bg.bkg)
else
if protected.fg_bg.fgd == colors._INHERIT then protected.fg_bg = core.cpair(p_fg_bg.fgd, protected.fg_bg.bkg) end
if protected.fg_bg.bkg == colors._INHERIT then protected.fg_bg = core.cpair(protected.fg_bg.fgd, p_fg_bg.bkg) end
end
end
-- check colors
element.assert(protected.fg_bg.fgd ~= colors._INHERIT, "could not determine foreground color to inherit")
element.assert(protected.fg_bg.bkg ~= colors._INHERIT, "could not determine background color to inherit")
-- set colors
protected.window.setBackgroundColor(protected.fg_bg.bkg)
protected.window.setTextColor(protected.fg_bg.fgd)
protected.window.clear()
-- record position
self.position.x, self.position.y = protected.window.getPosition()
-- shift per parent child offset
self.position.x = self.position.x + offset_x
self.position.y = self.position.y + offset_y
-- calculate mouse event bounds
self.bounds.x1 = self.position.x
self.bounds.x2 = self.position.x + f.w - 1
self.bounds.y1 = self.position.y
self.bounds.y2 = self.position.y + f.h - 1
-- alias functions
-- window set cursor position<br>
---@see Window.setCursorPos
---@param x integer
---@param y integer
function protected.w_set_cur(x, y) protected.window.setCursorPos(x, y) end
-- set background color<br>
---@see Window.setBackgroundColor
---@param c color
function protected.w_set_bkg(c) protected.window.setBackgroundColor(c) end
-- set foreground (text) color<br>
---@see Window.setTextColor
---@param c color
function protected.w_set_fgd(c) protected.window.setTextColor(c) end
-- write text<br>
---@see Window.write
---@param str string
function protected.w_write(str) protected.window.write(str) end
-- blit text<br>
---@see Window.blit
---@param str string
---@param fg string
---@param bg string
function protected.w_blit(str, fg, bg) protected.window.blit(str, fg, bg) end
end
-- check if a coordinate relative to the parent is within the bounds of this element
---@param x integer
---@param y integer
function protected.in_window_bounds(x, y)
local in_x = x >= self.bounds.x1 and x <= self.bounds.x2
local in_y = y >= self.bounds.y1 and y <= self.bounds.y2
return in_x and in_y
end
-- check if a coordinate relative to this window is within the bounds of this element
---@param x integer
---@param y integer
function protected.in_frame_bounds(x, y)
local in_x = x >= 1 and x <= protected.frame.w
local in_y = y >= 1 and y <= protected.frame.h
return in_x and in_y
end
-- get public interface
---@nodiscard
---@return graphics_element element, element_id id
function protected.get() return public, self.id end
-- report completion of element instantiation and get the public interface
---@nodiscard
---@param redraw? boolean true to call redraw as part of completing this element
---@return graphics_element element, element_id id
function protected.complete(redraw)
if redraw then protected.redraw() end
if args.parent ~= nil then args.parent.__child_ready(self.id, public) end
return public, self.id
end
-- protected version of public is_focused()
---@nodiscard
---@return boolean is_focused
function protected.is_focused() return self.focused end
-- defocus this element
function protected.defocus() public.unfocus_all() end
-- focus this element and take away focus from all other elements
function protected.take_focus() args.parent.__focus_child(public) end
--#region Action Handlers
-- luacheck: push ignore
---@diagnostic disable: unused-local, unused-vararg
-- handle a child element having been added
---@param id element_id element identifier
---@param child graphics_element child element
function protected.on_added(id, child) end
-- handle a child element having been removed
---@param id element_id element identifier
function protected.on_removed(id) end
-- handle enabled
function protected.on_enabled() end
-- handle disabled
function protected.on_disabled() end
-- handle this element having been focused
function protected.on_focused() end
-- handle this element having been unfocused
function protected.on_unfocused() end
-- handle this element having had a child focused
---@param child graphics_element
function protected.on_child_focused(child) end
-- handle this element having been shown
function protected.on_shown() end
-- handle this element having been hidden
function protected.on_hidden() end
-- handle a mouse event
---@param event mouse_interaction mouse interaction event
function protected.handle_mouse(event) end
-- handle a keyboard event
---@param event key_interaction key interaction event
function protected.handle_key(event) end
-- handle a paste event
---@param text string pasted text
function protected.handle_paste(text) end
-- handle data value changes
---@param ... any value(s)
function protected.on_update(...) end
--#endregion
--#region Accessors and Control
-- get value
---@nodiscard
function protected.get_value() return protected.value end
-- set value
---@param value any value to set
function protected.set_value(value) end
-- set minimum input value
---@param min integer minimum allowed value
function protected.set_min(min) end
-- set maximum input value
---@param max integer maximum allowed value
function protected.set_max(max) end
-- custom recolor command, varies by element if implemented
---@param ... cpair|color color(s)
function protected.recolor(...) end
-- custom resize command, varies by element if implemented
---@param ... integer sizing
function protected.resize(...) end
-- luacheck: pop
---@diagnostic enable: unused-local, unused-vararg
-- re-draw this element
function protected.redraw() end
-- start animations
function protected.start_anim() end
-- stop animations
function protected.stop_anim() end
--#endregion
--#endregion
------------------
--#region SETUP --
------------------
-- get the parent window
self.p_window = args.window
if self.p_window == nil and args.parent ~= nil then
self.p_window = args.parent.window()
end
-- check window
element.assert(self.p_window, "no parent window provided", 1)
-- prepare the template
if args.parent == nil then
self.id = args.id or "__ROOT__"
protected.prepare_template(0, 0, 1)
else
self.id = args.parent.__add_child(args.id, protected)
end
--#endregion
-----------------------------
--#region PUBLIC FUNCTIONS --
-----------------------------
-- get the window object
---@nodiscard
function public.window() return protected.content_window or protected.window end
-- delete this element (hide and unsubscribe from PSIL)
function public.delete()
local fg_bg = protected.fg_bg
if args.parent ~= nil then
-- grab parent fg/bg so we can clear cleanly as a child element
fg_bg = args.parent.get_fg_bg()
end
-- clear, hide, and stop animations
protected.window.setBackgroundColor(fg_bg.bkg)
protected.window.setTextColor(fg_bg.fgd)
protected.window.clear()
public.hide()
-- unsubscribe from PSIL
for i = 1, #self.subscriptions do
local s = self.subscriptions[i] ---@type element_subscription
s.ps.unsubscribe(s.key, s.func)
end
-- delete all children
for k, v in pairs(protected.children) do
v.get().delete()
protected.children[k] = nil
end
if args.parent ~= nil then
-- remove self from parent
args.parent.__remove_child(self.id)
end
end
--#region ELEMENT TREE
-- add a child element
---@package
---@nodiscard
---@param key string|nil id
---@param child graphics_base
---@return integer|string key
function public.__add_child(key, child)
child.prepare_template(child_offset_x or 0, child_offset_y or 0, self.next_y)
self.next_y = child.frame.y + child.frame.h
local id = key ---@type element_id|nil
if id == nil then
id = self.next_id
self.next_id = self.next_id + 1
end
-- 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
table.insert(protected.children, child)
return id
end
-- remove a child element
---@package
---@param id element_id id
function public.__remove_child(id)
local index = protected.child_id_map[id]
if protected.children[index] ~= nil then
protected.on_removed(id)
protected.children[index] = nil
protected.child_id_map[id] = nil
end
end
-- actions to take upon a child element becoming ready (initial draw/construction completed)
---@package
---@param key element_id id
---@param child graphics_element
function public.__child_ready(key, child) protected.on_added(key, child) end
-- focus solely on this child
---@package
---@param child graphics_element
function public.__focus_child(child)
if self.is_root then
public.unfocus_all()
child.focus()
else args.parent.__focus_child(child) end
end
-- a child was focused, used to make sure it is actually visible to the user in the content frame
---@package
---@param child graphics_element
function public.__child_focused(child)
protected.on_child_focused(child)
if not self.is_root then args.parent.__child_focused(public) end
end
-- get a child element
---@nodiscard
---@param id element_id
---@return graphics_element element
function public.get_child(id) return ({ protected.children[protected.child_id_map[id]].get() })[1] end
-- get all children
---@nodiscard
---@return table children table of graphics_element objects
function public.get_children()
local list = {}
for k, v in pairs(protected.children) do list[k] = v.get() end
return list
end
-- remove a child element
---@param id element_id
function public.remove(id)
local index = protected.child_id_map[id]
if protected.children[index] ~= nil then
protected.children[index].get().delete()
protected.on_removed(id)
protected.children[index] = nil
protected.child_id_map[id] = nil
end
end
-- remove all child elements and reset next y
function public.remove_all()
for i = 1, #protected.children do
local child = protected.children[i].get() ---@type graphics_element
child.delete()
protected.on_removed(child.get_id())
end
self.next_y = 1
protected.children = {}
protected.child_id_map = {}
end
-- attempt to get a child element by ID (does not include this element itself)
---@nodiscard
---@param id element_id
---@return graphics_element|nil element
function public.get_element_by_id(id)
local index = protected.child_id_map[id]
if protected.children[index] == nil then
for _, child in pairs(protected.children) do
local elem = child.get().get_element_by_id(id)
if elem ~= nil then return elem end
end
else return ({ protected.children[index].get() })[1] end
end
--#endregion
--#region AUTO-PLACEMENT
-- skip a line for automatically placed elements
function public.line_break()
self.next_y = self.next_y + 1
end
--#endregion
--#region PROPERTIES
-- get element ID
---@nodiscard
---@return element_id
function public.get_id() return self.id end
-- get element relative x position
---@nodiscard
---@return integer x
function public.get_x() return protected.frame.x end
-- get element relative y position
---@nodiscard
---@return integer y
function public.get_y() return protected.frame.y end
-- get element width
---@nodiscard
---@return integer width
function public.get_width() return protected.frame.w end
-- get element height
---@nodiscard
---@return integer height
function public.get_height() return protected.frame.h end
-- get the foreground/background colors
---@nodiscard
---@return cpair fg_bg
function public.get_fg_bg() return protected.fg_bg end
-- get the element's value
---@nodiscard
---@return any value
function public.get_value() return protected.get_value() end
-- set the element's value
---@param value any new value
function public.set_value(value) protected.set_value(value) end
-- set minimum input value
---@param min integer minimum allowed value
function public.set_min(min) protected.set_min(min) end
-- set maximum input value
---@param max integer maximum allowed value
function public.set_max(max) protected.set_max(max) end
-- check if this element is enabled
function public.is_enabled() return protected.enabled end
-- enable the element
function public.enable()
if not protected.enabled then
protected.enabled = true
protected.on_enabled()
end
end
-- disable the element
function public.disable()
if protected.enabled then
protected.enabled = false
protected.on_disabled()
public.unfocus_all()
end
end
-- can this element be focused
function public.is_focusable() return args.can_focus end
-- is this element focused
function public.is_focused() return self.focused end
-- focus the element
function public.focus()
if args.can_focus and protected.enabled and not self.focused then
self.focused = true
protected.on_focused()
if not self.is_root then args.parent.__child_focused(public) end
end
end
-- unfocus this element
function public.unfocus()
if args.can_focus and self.focused then
self.focused = false
protected.on_unfocused()
end
end
-- unfocus this element and all its children
function public.unfocus_all()
public.unfocus()
for _, child in pairs(protected.children) do child.get().unfocus_all() end
end
-- custom recolor command, varies by element if implemented
---@param ... cpair|color color(s)
function public.recolor(...) protected.recolor(...) end
-- resize attributes of the element value if supported
---@param ... number dimensions (element specific)
function public.resize(...) protected.resize(...) end
-- reposition the element window<br>
-- offsets relative to parent frame are where (1, 1) would be on top of the parent's top left corner
---@param x integer x position relative to parent frame
---@param y integer y position relative to parent frame
function public.reposition(x, y)
protected.window.reposition(x, y)
-- record position
self.position.x, self.position.y = protected.window.getPosition()
-- shift per parent child offset
self.position.x = self.position.x + self.offset_x
self.position.y = self.position.y + self.offset_y
-- calculate mouse event bounds
self.bounds.x1 = self.position.x
self.bounds.x2 = self.position.x + protected.frame.w - 1
self.bounds.y1 = self.position.y
self.bounds.y2 = self.position.y + protected.frame.h - 1
end
--#endregion
--#region FUNCTION CALLBACKS
-- handle a monitor touch or mouse click if this element is visible
---@param event mouse_interaction mouse interaction event
function public.handle_mouse(event)
if protected.window.isVisible() then
local x_ini, y_ini = event.initial.x, event.initial.y
local ini_in = protected.in_window_bounds(x_ini, y_ini)
if ini_in then
if event.type == events.MOUSE_CLICK.UP or event.type == events.MOUSE_CLICK.DRAG then
-- make sure we don't handle mouse events that started before this element was made visible
if (event.initial.x ~= self.button_down[event.button].x) or (event.initial.y ~= self.button_down[event.button].y) then
return
end
elseif event.type == events.MOUSE_CLICK.DOWN then
self.button_down[event.button] = event.initial
end
local event_T = events.mouse_transposed(event, self.position.x, self.position.y)
protected.handle_mouse(event_T)
-- shift child event if the content window has moved then pass to children
local c_event_T = events.mouse_transposed(event_T, protected.mouse_window_shift.x + 1, protected.mouse_window_shift.y + 1)
for _, child in pairs(protected.children) do child.get().handle_mouse(c_event_T) end
elseif event.type == events.MOUSE_CLICK.DOWN or event.type == events.MOUSE_CLICK.TAP then
-- clicked out, unfocus this element and children
public.unfocus_all()
end
else
-- don't track clicks while hidden
self.button_down[event.button] = events.new_coord_2d(-1, -1)
end
end
-- handle a keyboard click if this element is visible and focused
---@param event key_interaction keyboard interaction event
function public.handle_key(event)
if protected.window.isVisible() then
if self.is_root and (event.type == events.KEY_CLICK.DOWN) and (event.key == keys.tab) then
-- try to jump to the next/previous focusable field
_tab_focusable(event.shift)
else
-- handle the key event then pass to children
if self.focused then protected.handle_key(event) end
for _, child in pairs(protected.children) do child.get().handle_key(event) end
end
end
end
-- handle text paste
---@param text string pasted text
function public.handle_paste(text)
if protected.window.isVisible() then
-- handle the paste event then pass to children
if self.focused then protected.handle_paste(text) end
for _, child in pairs(protected.children) do child.get().handle_paste(text) end
end
end
-- draw the element given new data
---@param ... any new data
function public.update(...) protected.on_update(...) end
-- register a callback with a PSIL, allowing for automatic unregister on delete<br>
-- do not use graphics elements directly with PSIL subscribe()
---@param ps psil PSIL to subscribe to
---@param key string key to subscribe to
---@param func function function to link
function public.register(ps, key, func)
table.insert(self.subscriptions, { ps = ps, key = key, func = func })
ps.subscribe(key, func)
end
--#endregion
--#region VISIBILITY & ANIMATIONS
-- check if this element is visible
function public.is_visible() return protected.window.isVisible() end
-- show the element and enables animations by default
---@param animate? boolean true (default) to automatically resume animations
function public.show(animate)
protected.window.setVisible(true)
if animate ~= false then public.animate_all() end
end
-- hide the element and disables animations<br>
-- this alone does not cause an element to be fully hidden, it only prevents updates from being shown<br>
---@see Window.redraw
---@see graphics_element.redraw
---@see graphics_element.content_redraw
---@param clear? boolean true to visibly hide this element (redraws the parent)
function public.hide(clear)
public.freeze_all() -- stop animations for efficiency/performance
public.unfocus_all()
protected.window.setVisible(false)
if clear and args.parent then args.parent.redraw() end
end
-- start/resume animation(s)
function public.animate() protected.start_anim() end
-- start/resume animation(s) for this element and all its children<br>
-- only animates if a window is visible
function public.animate_all()
if protected.window.isVisible() then
public.animate()
for _, child in pairs(protected.children) do child.get().animate_all() end
end
end
-- freeze animation(s)
function public.freeze() protected.stop_anim() end
-- freeze animation(s) for this element and all its children
function public.freeze_all()
public.freeze()
for _, child in pairs(protected.children) do child.get().freeze_all() end
end
-- re-draw this element and all its children
function public.redraw()
local bg, fg = protected.window.getBackgroundColor(), protected.window.getTextColor()
protected.window.setBackgroundColor(protected.fg_bg.bkg)
protected.window.setTextColor(protected.fg_bg.fgd)
protected.window.clear()
protected.window.setBackgroundColor(bg)
protected.window.setTextColor(fg)
protected.redraw()
for _, child in pairs(protected.children) do child.get().redraw() end
end
-- if a content window is set, clears it then re-draws all children
function public.content_redraw()
if protected.content_window ~= nil then
protected.content_window.clear()
for _, child in pairs(protected.children) do child.get().redraw() end
end
end
--#endregion
--#endregion
return protected
end
return element

View File

@ -0,0 +1,107 @@
-- App Page Multi-Pane Display Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
local events = require("graphics.events")
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class app_multipane_args
---@field panes table panes to swap between
---@field nav_colors cpair on/off colors (a/b respectively) for page navigator
---@field scroll_nav boolean? true to allow scrolling to change the active pane
---@field drag_nav boolean? true to allow mouse dragging to change the active pane (on mouse up)
---@field callback function? function to call when pane is changed by mouse interaction
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new app multipane container element.
---@nodiscard
---@param args app_multipane_args
---@return AppMultiPane element, element_id id
return function (args)
element.assert(type(args.panes) == "table", "panes is a required field")
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = 1
local nav_x_start = math.floor((e.frame.w / 2) - (#args.panes / 2)) + 1
local nav_x_end = math.floor((e.frame.w / 2) - (#args.panes / 2)) + #args.panes
-- show the selected pane
function e.redraw()
for i = 1, #args.panes do args.panes[i].hide() end
args.panes[e.value].show()
-- draw page indicator dots
for i = 1, #args.panes do
e.w_set_cur(nav_x_start + (i - 1), e.frame.h)
e.w_set_fgd(util.trinary(i == e.value, args.nav_colors.color_a, args.nav_colors.color_b))
e.w_write("\x07")
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
local initial = e.value
if e.enabled then
if event.current.y == e.frame.h and event.current.x >= nav_x_start and event.current.x <= nav_x_end then
local id = event.current.x - nav_x_start + 1
if event.type == MOUSE_CLICK.TAP then
e.set_value(id)
elseif event.type == MOUSE_CLICK.UP then
e.set_value(id)
end
end
end
if args.scroll_nav then
if event.type == events.MOUSE_CLICK.SCROLL_DOWN then
e.set_value(e.value + 1)
elseif event.type == events.MOUSE_CLICK.SCROLL_UP then
e.set_value(e.value - 1)
end
end
if args.drag_nav then
local x1, x2 = event.initial.x, event.current.x
if event.type == events.MOUSE_CLICK.UP and e.in_frame_bounds(x1, event.initial.y) and e.in_frame_bounds(x1, event.current.y) then
if x2 > x1 then
e.set_value(e.value - 1)
elseif x2 < x1 then
e.set_value(e.value + 1)
end
end
end
if e.value ~= initial and type(args.callback) == "function" then args.callback(e.value) end
end
-- select which pane is shown
---@param value integer pane to show
function e.set_value(value)
if (e.value ~= value) and (value > 0) and (value <= #args.panes) then
e.value = value
e.redraw()
end
end
---@class AppMultiPane:graphics_element
local AppMultiPane, id = e.complete(true)
return AppMultiPane, id
end

View File

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

View File

@ -0,0 +1,28 @@
-- Root Display Box Graphics Element
local element = require("graphics.element")
---@class displaybox_args
---@field window table
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a root display box.
---@nodiscard
---@param args displaybox_args
---@return DisplayBox element, element_id id
return function (args)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
---@class DisplayBox:graphics_element
local DisplayBox, id = e.complete()
return DisplayBox, id
end

28
graphics/elements/Div.lua Normal file
View File

@ -0,0 +1,28 @@
-- Div (Division, like in HTML) Graphics Element
local element = require("graphics.element")
---@class div_args
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new div container element.
---@nodiscard
---@param args div_args
---@return Div element, element_id id
return function (args)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
---@class Div:graphics_element
local Div, id = e.complete()
return Div, id
end

View File

@ -0,0 +1,341 @@
-- Scroll-able List Box Display Graphics Element
local tcd = require("scada-common.tcd")
local core = require("graphics.core")
local element = require("graphics.element")
local KEY_CLICK = core.events.KEY_CLICK
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class listbox_args
---@field scroll_height integer height of internal scrolling container (must fit all elements vertically tiled)
---@field item_pad? integer spacing (lines) between items in the list (default 0)
---@field nav_fg_bg? cpair foreground/background colors for scroll arrows and bar area
---@field nav_active? cpair active colors for bar held down or arrow held down
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
---@class listbox_item
---@field id string|integer element ID
---@field e graphics_element element
---@field y integer y position
---@field h integer element height
-- Create a new scrollable listbox container element.
---@nodiscard
---@param args listbox_args
---@return ListBox element, element_id id
return function (args)
args.can_focus = true
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
-- create content window for child elements
local scroll_frame = window.create(e.window, 1, 1, e.frame.w - 1, args.scroll_height, false)
e.content_window = scroll_frame
-- item list and scroll management
local list = {}
local item_pad = args.item_pad or 0
local scroll_offset = 0
local content_height = 0
local max_down_scroll = 0
-- bar control/tracking variables
local max_bar_height = e.frame.h - 2
local bar_height = 0 -- full height of bar
local bar_bounds = { 0, 0 } -- top and bottom of bar
local bar_is_scaled = false -- if the scrollbar doesn't have a 1:1 ratio with lines
local holding_bar = false -- bar is being held by mouse
local bar_grip_pos = 0 -- where the bar was gripped by mouse down
local mouse_last_y = 0 -- last reported y coordinate of drag
-- draw scroll bar arrows, optionally showing one of them as pressed
---@param pressed_arrow? 1|0|-1 arrow to show as pressed (1 = scroll up, 0 = neither, -1 = scroll down)
local function draw_arrows(pressed_arrow)
local nav_fg_bg = args.nav_fg_bg or e.fg_bg
local active_fg_bg = args.nav_active or nav_fg_bg
-- draw up/down arrows
if pressed_arrow == 1 then
e.w_set_fgd(active_fg_bg.fgd)
e.w_set_bkg(active_fg_bg.bkg)
e.w_set_cur(e.frame.w, 1)
e.w_write("\x1e")
e.w_set_fgd(nav_fg_bg.fgd)
e.w_set_bkg(nav_fg_bg.bkg)
e.w_set_cur(e.frame.w, e.frame.h)
e.w_write("\x1f")
elseif pressed_arrow == -1 then
e.w_set_fgd(nav_fg_bg.fgd)
e.w_set_bkg(nav_fg_bg.bkg)
e.w_set_cur(e.frame.w, 1)
e.w_write("\x1e")
e.w_set_fgd(active_fg_bg.fgd)
e.w_set_bkg(active_fg_bg.bkg)
e.w_set_cur(e.frame.w, e.frame.h)
e.w_write("\x1f")
else
e.w_set_fgd(nav_fg_bg.fgd)
e.w_set_bkg(nav_fg_bg.bkg)
e.w_set_cur(e.frame.w, 1)
e.w_write("\x1e")
e.w_set_cur(e.frame.w, e.frame.h)
e.w_write("\x1f")
end
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
end
-- render the scroll bar and re-cacluate height & bounds
local function draw_bar()
local offset = 2 + math.abs(scroll_offset)
bar_height = math.min(max_bar_height + max_down_scroll, max_bar_height)
if bar_height < 1 then
bar_is_scaled = true
-- can't do a 1:1 ratio
-- use minimum size bar with scaled offset
local scroll_progress = scroll_offset / max_down_scroll
offset = 2 + math.floor(scroll_progress * (max_bar_height - 1))
bar_height = 1
else
bar_is_scaled = false
end
bar_bounds = { offset, (bar_height + offset) - 1 }
for i = 2, e.frame.h - 1 do
if (i >= offset and i < (bar_height + offset)) and (bar_height ~= max_bar_height) then
if args.nav_fg_bg ~= nil then
e.w_set_bkg(args.nav_fg_bg.fgd)
else
e.w_set_bkg(e.fg_bg.fgd)
end
else
if args.nav_fg_bg ~= nil then
e.w_set_bkg(args.nav_fg_bg.bkg)
else
e.w_set_bkg(e.fg_bg.bkg)
end
end
e.w_set_cur(e.frame.w, i)
if e.is_focused() then e.w_write("\x7f") else e.w_write(" ") end
end
e.w_set_bkg(e.fg_bg.bkg)
end
-- update item y positions and move elements
local function update_positions()
local next_y = 1
scroll_frame.setVisible(false)
scroll_frame.setBackgroundColor(e.fg_bg.bkg)
scroll_frame.setTextColor(e.fg_bg.fgd)
scroll_frame.clear()
for i = 1, #list do
local item = list[i] ---@type listbox_item
item.y = next_y
next_y = next_y + item.h + item_pad
item.e.reposition(1, item.y)
item.e.show()
end
content_height = next_y
max_down_scroll = math.min(-1 * (content_height - (e.frame.h + 1 + item_pad)), 0)
if scroll_offset < max_down_scroll then scroll_offset = max_down_scroll end
scroll_frame.reposition(1, 1 + scroll_offset)
scroll_frame.setVisible(true)
-- shift mouse events
e.mouse_window_shift.y = scroll_offset
draw_bar()
end
-- determine where to scroll to based on a scrollbar being dragged without a 1:1 relationship
---@param direction -1|1 negative 1 to scroll up by one, positive 1 to scroll down by one
local function scaled_bar_scroll(direction)
local scroll_progress = scroll_offset / max_down_scroll
local bar_position = math.floor(scroll_progress * (max_bar_height - 1))
-- check what moving the scroll bar up or down would mean for the scroll progress
scroll_progress = (bar_position + direction) / (max_bar_height - 1)
return math.max(math.floor(scroll_progress * max_down_scroll), max_down_scroll)
end
-- scroll down the list
local function scroll_down(scaled)
if scroll_offset > max_down_scroll then
if scaled then
scroll_offset = scaled_bar_scroll(1)
else
scroll_offset = scroll_offset - 1
end
update_positions()
end
end
-- scroll up the list
local function scroll_up(scaled)
if scroll_offset < 0 then
if scaled then
scroll_offset = scaled_bar_scroll(-1)
else
scroll_offset = scroll_offset + 1
end
update_positions()
end
end
-- handle a child element having been added to the list
---@param id element_id element identifier
---@param child graphics_element child element
function e.on_added(id, child)
table.insert(list, { id = id, e = child, y = 0, h = child.get_height() })
update_positions()
end
-- handle a child element having been removed from the list
---@param id element_id element identifier
function e.on_removed(id)
for idx, elem in ipairs(list) do
if elem.id == id then
table.remove(list, idx)
update_positions()
return
end
end
end
-- handle focus
e.on_focused = draw_bar
e.on_unfocused = draw_bar
-- handle a child in the list being focused, make sure it is visible
function e.on_child_focused(child)
for i = 1, #list do
local item = list[i] ---@type listbox_item
if item.e == child then
if (item.y + scroll_offset) <= 0 then
scroll_offset = 1 - item.y
update_positions()
draw_bar()
elseif (item.y + scroll_offset) == 1 then
-- do nothing, it's right at the top (if the bottom doesn't fit we can't easily fix that)
elseif ((item.h + item.y - 1) + scroll_offset) > e.frame.h then
scroll_offset = 1 - ((item.h + item.y) - e.frame.h)
update_positions()
draw_bar()
end
return
end
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled then
if event.type == MOUSE_CLICK.TAP then
if event.current.x == e.frame.w then
if event.current.y == 1 or event.current.y < bar_bounds[1] then
scroll_up()
if event.current.y == 1 then
draw_arrows(1)
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
end
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
scroll_down()
if event.current.y == e.frame.h then
draw_arrows(-1)
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
end
end
end
elseif event.type == MOUSE_CLICK.DOWN then
if event.current.x == e.frame.w then
if event.current.y == 1 or event.current.y < bar_bounds[1] then
scroll_up()
if event.current.y == 1 then draw_arrows(1) end
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
scroll_down()
if event.current.y == e.frame.h then draw_arrows(-1) end
else
-- clicked on bar
holding_bar = true
bar_grip_pos = event.current.y - bar_bounds[1]
mouse_last_y = event.current.y
end
end
elseif event.type == MOUSE_CLICK.UP then
holding_bar = false
draw_arrows(0)
elseif event.type == MOUSE_CLICK.DRAG then
if holding_bar then
-- if mouse is within vertical frame, including the grip point
if event.current.y > (1 + bar_grip_pos) and event.current.y <= ((e.frame.h - bar_height) + bar_grip_pos) then
if event.current.y < mouse_last_y then
scroll_up(bar_is_scaled)
elseif event.current.y > mouse_last_y then
scroll_down(bar_is_scaled)
end
mouse_last_y = event.current.y
end
end
elseif event.type == MOUSE_CLICK.SCROLL_DOWN then
scroll_down()
elseif event.type == MOUSE_CLICK.SCROLL_UP then
scroll_up()
end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
if event.key == keys.up then
scroll_up()
elseif event.key == keys.down then
scroll_down()
elseif event.key == keys.home then
scroll_offset = 0
update_positions()
elseif event.key == keys["end"] then
scroll_offset = max_down_scroll
update_positions()
end
end
end
-- element redraw
function e.redraw()
draw_arrows(0)
draw_bar()
end
---@class ListBox:graphics_element
local ListBox, id = e.complete(true)
return ListBox, id
end

View File

@ -0,0 +1,48 @@
-- Multi-Pane Display Graphics Element
local element = require("graphics.element")
---@class multipane_args
---@field panes table panes to swap between
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new multipane container element.
---@nodiscard
---@param args multipane_args
---@return MultiPane element, element_id id
return function (args)
element.assert(type(args.panes) == "table", "panes is a required field")
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = 1
-- show the selected pane
function e.redraw()
for i = 1, #args.panes do args.panes[i].hide() end
args.panes[e.value].show()
end
-- select which pane is shown
---@param value integer pane to show
function e.set_value(value)
if (e.value ~= value) and (value > 0) and (value <= #args.panes) then
e.value = value
e.redraw()
end
end
---@class MultiPane:graphics_element
local MultiPane, id = e.complete(true)
return MultiPane, id
end

View File

@ -0,0 +1,329 @@
-- Pipe Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
---@class pipenet_args
---@field pipes table pipe list
---@field bg? color background color
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field hidden? boolean true to hide on initial draw
---@class _pipe_map_entry
---@field atr boolean align top right (or bottom left for false)
---@field thin boolean thin pipe or not
---@field fg string foreground blit
---@field bg string background blit
-- Create a pipe network diagram.
---@param args pipenet_args
---@return PipeNetwork element, element_id id
return function (args)
element.assert(type(args.pipes) == "table", "pipes is a required field")
args.width = 0
args.height = 0
for i = 1, #args.pipes do
local pipe = args.pipes[i] ---@type pipe
local true_w = pipe.w + math.min(pipe.x1, pipe.x2)
local true_h = pipe.h + math.min(pipe.y1, pipe.y2)
if true_w > args.width then args.width = true_w end
if true_h > args.height then args.height = true_h end
end
args.x = args.x or 1
args.y = args.y or 1
if args.bg ~= nil then
args.fg_bg = core.cpair(args.bg, args.bg)
end
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
-- determine if there are any thin pipes involved
local any_thin = false
for p = 1, #args.pipes do
any_thin = args.pipes[p].thin
if any_thin then break end
end
-- draw all pipes by drawing out lines
local function vector_draw()
for p = 1, #args.pipes do
local pipe = args.pipes[p] ---@type pipe
local x = 1 + pipe.x1
local y = 1 + pipe.y1
local x_step = util.trinary(pipe.x1 >= pipe.x2, -1, 1)
local y_step = util.trinary(pipe.y1 >= pipe.y2, -1, 1)
if pipe.thin then
x_step = util.trinary(pipe.x1 == pipe.x2, 0, x_step)
y_step = util.trinary(pipe.y1 == pipe.y2, 0, y_step)
end
e.w_set_cur(x, y)
local c = core.cpair(pipe.color, e.fg_bg.bkg)
if pipe.align_tr then
-- cross width then height
for i = 1, pipe.w do
if pipe.thin then
if i == pipe.w then
-- corner
if y_step > 0 then
e.w_blit("\x93", c.blit_bkg, c.blit_fgd)
else
e.w_blit("\x8e", c.blit_fgd, c.blit_bkg)
end
else
e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
end
else
if i == pipe.w and y_step > 0 then
-- corner
e.w_blit(" ", c.blit_bkg, c.blit_fgd)
else
e.w_blit("\x8f", c.blit_fgd, c.blit_bkg)
end
end
x = x + x_step
e.w_set_cur(x, y)
end
-- back up one
x = x - x_step
for _ = 1, pipe.h - 1 do
y = y + y_step
e.w_set_cur(x, y)
if pipe.thin then
e.w_blit("\x95", c.blit_bkg, c.blit_fgd)
else
e.w_blit(" ", c.blit_bkg, c.blit_fgd)
end
end
else
-- cross height then width
for i = 1, pipe.h do
if pipe.thin then
if i == pipe.h then
-- corner
if y_step < 0 then
e.w_blit("\x97", c.blit_bkg, c.blit_fgd)
elseif y_step > 0 then
e.w_blit("\x8d", c.blit_fgd, c.blit_bkg)
else
e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
end
else
e.w_blit("\x95", c.blit_fgd, c.blit_bkg)
end
else
if i == pipe.h and y_step < 0 then
-- corner
e.w_blit("\x83", c.blit_bkg, c.blit_fgd)
else
e.w_blit(" ", c.blit_bkg, c.blit_fgd)
end
end
y = y + y_step
e.w_set_cur(x, y)
end
-- back up one
y = y - y_step
for _ = 1, pipe.w - 1 do
x = x + x_step
e.w_set_cur(x, y)
if pipe.thin then
e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
else
e.w_blit("\x83", c.blit_bkg, c.blit_fgd)
end
end
end
end
end
-- draw a particular map cell
---@param map table 2D cell map
---@param x integer x coord
---@param y integer y coord
local function draw_map_cell(map, x, y)
local entry = map[x][y] ---@type _pipe_map_entry already confirmed not false
local char
local invert = false
local function check(cx, cy)
return (map[cx] ~= nil) and (map[cx][cy] ~= nil) and (map[cx][cy] ~= false) and (map[cx][cy].fg == entry.fg)
end
if entry.thin then
if check(x - 1, y) then -- if left
if check(x, y - 1) then -- if above
if check(x + 1, y) then -- if right
if check(x, y + 1) then -- if below
char = util.trinary(entry.atr, "\x91", "\x9d")
invert = entry.atr
else -- not below
char = util.trinary(entry.atr, "\x8e", "\x8d")
end
else -- not right
if check(x, y + 1) then -- if below
char = util.trinary(entry.atr, "\x91", "\x95")
invert = entry.atr
else -- not below
char = util.trinary(entry.atr, "\x8e", "\x85")
end
end
elseif check(x, y + 1) then-- not above, if below
if check(x + 1, y) then -- if right
char = util.trinary(entry.atr, "\x93", "\x9c")
invert = entry.atr
else -- not right
char = util.trinary(entry.atr, "\x93", "\x94")
invert = entry.atr
end
else -- not above, not below
char = "\x8c"
end
elseif check(x + 1, y) then -- not left, if right
if check(x, y - 1) then -- if above
if check(x, y + 1) then -- if below
char = util.trinary(entry.atr, "\x95", "\x9d")
invert = entry.atr
else -- not below
char = util.trinary(entry.atr, "\x8a", "\x8d")
end
else -- not above
if check(x, y + 1) then -- if below
char = util.trinary(entry.atr, "\x97", "\x9c")
invert = entry.atr
else -- not below
char = "\x8c"
end
end
else -- not left, not right
char = "\x95"
invert = entry.atr
end
else
if check(x, y - 1) then -- above
-- not below and (if left or right)
if (not check(x, y + 1)) and (check(x - 1, y) or check(x + 1, y)) then
char = util.trinary(entry.atr, "\x8f", " ")
invert = not entry.atr
else -- not below w/ sides only
char = " "
invert = true
end
elseif check(x, y + 1) then -- not above, if below
-- if left or right
if (check(x - 1, y) or check(x + 1, y)) then
char = "\x83"
invert = true
else -- not left or right
char = " "
invert = true
end
else -- not above, not below
char = util.trinary(entry.atr, "\x8f", "\x83")
invert = not entry.atr
end
end
e.w_set_cur(x, y)
if invert then
e.w_blit(char, entry.bg, entry.fg)
else
e.w_blit(char, entry.fg, entry.bg)
end
end
-- draw all pipes by assembling and marking up a 2D map<br>
-- this is an easy way to check adjacent blocks, which is required to properly draw thin pipes
local function map_draw()
local map = {}
for x = 1, args.width do
table.insert(map, {})
for _ = 1, args.height do table.insert(map[x], false) end
end
-- build map
for p = 1, #args.pipes do
local pipe = args.pipes[p] ---@type pipe
local x = 1 + pipe.x1
local y = 1 + pipe.y1
local x_step = util.trinary(pipe.x1 >= pipe.x2, -1, 1)
local y_step = util.trinary(pipe.y1 >= pipe.y2, -1, 1)
local entry = { atr = pipe.align_tr, thin = pipe.thin, fg = colors.toBlit(pipe.color), bg = e.fg_bg.blit_bkg }
if pipe.align_tr then
-- cross width then height
for _ = 1, pipe.w do
map[x][y] = entry
x = x + x_step
end
x = x - x_step -- back up one
for _ = 1, pipe.h do
map[x][y] = entry
y = y + y_step
end
else
-- cross height then width
for _ = 1, pipe.h do
map[x][y] = entry
y = y + y_step
end
y = y - y_step -- back up one
for _ = 1, pipe.w do
map[x][y] = entry
x = x + x_step
end
end
end
-- render
for x = 1, args.width do
for y = 1, args.height do
if map[x][y] ~= false then draw_map_cell(map, x, y) end
end
end
end
-- element redraw
function e.redraw()
if any_thin then map_draw() else vector_draw() end
end
---@class PipeNetwork:graphics_element
local PipeNetwork, id = e.complete(true)
return PipeNetwork, id
end

View File

@ -0,0 +1,198 @@
-- Rectangle Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class rectangle_args
---@field border? graphics_border
---@field thin? boolean true to use extra thin even borders
---@field even_inner? boolean true to make the inner area of a border even
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new rectangle container element.
---@param args rectangle_args
---@return Rectangle element, element_id id
return function (args)
element.assert(args.border ~= nil or args.thin ~= true, "thin requires border to be provided")
-- if thin, then width will always need to be 1
if args.thin == true then
args.border.width = 1
args.border.even = true
end
-- offset children
local offset_x = 0
local offset_y = 0
if args.border ~= nil then
offset_x = args.border.width
offset_y = args.border.width
-- slightly different y offset if the border is set to even
if args.border.even then
local width_x2 = (2 * args.border.width)
offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0)
end
end
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]], nil, offset_x, offset_y)
-- create content window for child elements
e.content_window = window.create(e.window, 1 + offset_x, 1 + offset_y, e.frame.w - (2 * offset_x), e.frame.h - (2 * offset_y))
e.content_window.setBackgroundColor(e.fg_bg.bkg)
e.content_window.setTextColor(e.fg_bg.fgd)
e.content_window.clear()
-- draw bordered box if requested
-- element constructor will have drawn basic colored rectangle regardless
if args.border ~= nil then
e.w_set_cur(1, 1)
local border_width = offset_x
local border_height = offset_y
local border_blit = colors.toBlit(args.border.color)
local width_x2 = border_width * 2
local inner_width = e.frame.w - width_x2
-- check dimensions
element.assert(width_x2 <= e.frame.w, "border too thick for width")
element.assert(width_x2 <= e.frame.h, "border too thick for height")
-- form the basic line strings and top/bottom blit strings
local spaces = util.spaces(e.frame.w)
local blit_fg = string.rep(e.fg_bg.blit_fgd, e.frame.w)
local blit_fg_sides = blit_fg
local blit_bg_sides = ""
local blit_bg_top_bot = string.rep(border_blit, e.frame.w)
-- partial bars
local p_a, p_b, p_s
if args.thin == true then
if args.even_inner == true then
p_a = "\x9c" .. string.rep("\x8c", inner_width) .. "\x93"
p_b = "\x8d" .. string.rep("\x8c", inner_width) .. "\x8e"
else
p_a = "\x97" .. string.rep("\x83", inner_width) .. "\x94"
p_b = "\x8a" .. string.rep("\x8f", inner_width) .. "\x85"
end
p_s = "\x95" .. util.spaces(inner_width) .. "\x95"
else
if args.even_inner == true then
p_a = string.rep("\x83", inner_width + width_x2)
p_b = string.rep("\x8f", inner_width + width_x2)
else
p_a = util.spaces(border_width) .. string.rep("\x8f", inner_width) .. util.spaces(border_width)
p_b = util.spaces(border_width) .. string.rep("\x83", inner_width) .. util.spaces(border_width)
end
p_s = spaces
end
local p_inv_fg = string.rep(border_blit, border_width) .. string.rep(e.fg_bg.blit_bkg, inner_width) ..
string.rep(border_blit, border_width)
local p_inv_bg = string.rep(e.fg_bg.blit_bkg, border_width) .. string.rep(border_blit, inner_width) ..
string.rep(e.fg_bg.blit_bkg, border_width)
if args.thin == true then
p_inv_fg = e.fg_bg.blit_bkg .. string.rep(e.fg_bg.blit_bkg, inner_width) .. string.rep(border_blit, border_width)
p_inv_bg = border_blit .. string.rep(border_blit, inner_width) .. string.rep(e.fg_bg.blit_bkg, border_width)
blit_fg_sides = border_blit .. string.rep(e.fg_bg.blit_bkg, inner_width) .. e.fg_bg.blit_bkg
end
-- form the body blit strings (sides are border, inside is normal)
for x = 1, e.frame.w do
-- edges get border color, center gets normal
if x <= border_width or x > (e.frame.w - border_width) then
if args.thin and x == 1 then
blit_bg_sides = blit_bg_sides .. e.fg_bg.blit_bkg
else
blit_bg_sides = blit_bg_sides .. border_blit
end
else
blit_bg_sides = blit_bg_sides .. e.fg_bg.blit_bkg
end
end
-- draw rectangle with borders
function e.redraw()
for y = 1, e.frame.h do
e.w_set_cur(1, y)
-- top border
if y <= border_height then
-- partial pixel fill
if args.border.even and y == border_height then
if args.thin == true then
e.w_blit(p_a, p_inv_bg, p_inv_fg)
else
local _fg = util.trinary(args.even_inner == true, string.rep(e.fg_bg.blit_bkg, e.frame.w), p_inv_bg)
local _bg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
if width_x2 % 3 == 1 then
e.w_blit(p_b, _fg, _bg)
elseif width_x2 % 3 == 2 then
e.w_blit(p_a, _fg, _bg)
else
-- skip line
e.w_blit(spaces, blit_fg, blit_bg_sides)
end
end
else
e.w_blit(spaces, blit_fg, blit_bg_top_bot)
end
-- bottom border
elseif y > (e.frame.h - border_width) then
-- partial pixel fill
if args.border.even and y == ((e.frame.h - border_width) + 1) then
if args.thin == true then
if args.even_inner == true then
e.w_blit(p_b, blit_bg_top_bot, string.rep(e.fg_bg.blit_bkg, e.frame.w))
else
e.w_blit(p_b, string.rep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
end
else
local _fg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
local _bg = util.trinary(args.even_inner == true, string.rep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
if width_x2 % 3 == 1 then
e.w_blit(p_a, _fg, _bg)
elseif width_x2 % 3 == 2 then
e.w_blit(p_b, _fg, _bg)
else
-- skip line
e.w_blit(spaces, blit_fg, blit_bg_sides)
end
end
else
e.w_blit(spaces, blit_fg, blit_bg_top_bot)
end
else
if args.thin == true then
e.w_blit(p_s, blit_fg_sides, blit_bg_sides)
else
e.w_blit(p_s, blit_fg, blit_bg_sides)
end
end
end
end
-- initial draw of border
e.redraw()
end
---@class Rectangle:graphics_element
local Rectangle, id = e.complete()
return Rectangle, id
end

View File

@ -0,0 +1,100 @@
-- Text Box Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
local ALIGN = core.ALIGN
---@class textbox_args
---@field text string text to show
---@field alignment? ALIGN text alignment, left by default
---@field trim_whitespace? boolean true to trim whitespace before/after lines of text
---@field anchor? boolean true to use this as an anchor, making it focusable
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field height? integer minimum necessary height for wrapped text if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new text box element.
---@param args textbox_args
---@return TextBox element, element_id id
return function (args)
element.assert(type(args.text) == "string", "text is a required field")
if args.anchor == true then args.can_focus = true end
-- provide a constraint condition to element creation to prevent an pointlessly tall text box
---@param frame graphics_frame
local function constrain(frame)
local new_height = math.max(1, #util.strwrap(args.text, frame.w))
if args.height then
new_height = math.max(frame.h, new_height)
end
return frame.w, new_height
end
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]], constrain)
e.value = args.text
local alignment = args.alignment or ALIGN.LEFT
-- draw textbox
function e.redraw()
e.window.clear()
local lines = util.strwrap(e.value, e.frame.w)
for i = 1, #lines do
if i > e.frame.h then break end
-- trim leading/trailing whitespace, except on the first line
-- leading whitespace on the first line is usually intentional
if args.trim_whitespace == true then
lines[i] = util.trim(lines[i])
end
local len = string.len(lines[i])
-- use cursor position to align this line
if alignment == ALIGN.CENTER then
e.w_set_cur(math.floor((e.frame.w - len) / 2) + 1, i)
elseif alignment == ALIGN.RIGHT then
e.w_set_cur((e.frame.w - len) + 1, i)
else
e.w_set_cur(1, i)
end
e.w_write(lines[i])
end
end
-- set the string value and re-draw the text
---@param val string value
function e.set_value(val)
e.value = val
e.redraw()
end
-- change the foreground color of the text
---@param c color
function e.recolor(c)
e.w_set_fgd(c)
e.redraw()
end
---@class TextBox:graphics_element
local TextBox, id = e.complete(true)
return TextBox, id
end

View File

@ -0,0 +1,93 @@
-- "Basketweave" Tiling Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class tiling_args
---@field fill_c cpair colors to fill with
---@field even? boolean whether to account for rectangular pixels
---@field border_c? color optional frame color
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new tiling box element.
---@param args tiling_args
---@return Tiling element, element_id id
return function (args)
element.assert(type(args.fill_c) == "table", "fill_c is a required field")
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local fill_a = args.fill_c.blit_a
local fill_b = args.fill_c.blit_b
local even = args.even == true
local start_x = 1
local start_y = 1
local inner_width = math.floor(e.frame.w / util.trinary(even, 2, 1))
local inner_height = e.frame.h
-- border
if args.border_c ~= nil then
start_x = 1 + util.trinary(even, 2, 1)
start_y = 2
inner_width = math.floor((e.frame.w - 2 * util.trinary(even, 2, 1)) / util.trinary(even, 2, 1))
inner_height = e.frame.h - 2
end
-- check dimensions
element.assert(inner_width > 0, "inner_width <= 0")
element.assert(inner_height > 0, "inner_height <= 0")
element.assert(start_x <= inner_width, "start_x > inner_width")
element.assert(start_y <= inner_height, "start_y > inner_height")
-- draw the tiling box
function e.redraw()
local alternator = true
if args.border_c ~= nil then
e.w_set_bkg(args.border_c)
e.window.clear()
end
-- draw pattern
for y = start_y, inner_height + (start_y - 1) do
e.w_set_cur(start_x, y)
for _ = 1, inner_width do
if alternator then
if even then
e.w_blit(" ", "00", fill_a .. fill_a)
else
e.w_blit(" ", "0", fill_a)
end
else
if even then
e.w_blit(" ", "00", fill_b .. fill_b)
else
e.w_blit(" ", "0", fill_b)
end
end
alternator = not alternator
end
if inner_width % 2 == 0 then alternator = not alternator end
end
end
---@class Tiling:graphics_element
local Tiling, id = e.complete(true)
return Tiling, id
end

View File

@ -0,0 +1,110 @@
-- Loading/Waiting Animation Graphics Element
local tcd = require("scada-common.tcd")
local element = require("graphics.element")
---@class waiting_args
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new waiting animation element.
---@param args waiting_args
---@return Waiting element, element_id id
return function (args)
local state = 0
local run_animation = false
args.width = 4
args.height = 3
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local blit_fg = e.fg_bg.blit_fgd
local blit_bg = e.fg_bg.blit_bkg
local blit_fg_2x = e.fg_bg.blit_fgd .. e.fg_bg.blit_fgd
local blit_bg_2x = e.fg_bg.blit_bkg .. e.fg_bg.blit_bkg
-- tick the animation
local function animate()
e.window.clear()
if state >= 0 and state < 7 then
-- top
e.w_set_cur(1 + math.floor(state / 2), 1)
if state % 2 == 0 then
e.w_blit("\x8f", blit_fg, blit_bg)
else
e.w_blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
end
-- bottom
e.w_set_cur(4 - math.ceil(state / 2), 3)
if state % 2 == 0 then
e.w_blit("\x8f", blit_fg, blit_bg)
else
e.w_blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
end
else
local st = state - 7
-- right
if st % 3 == 0 then
e.w_set_cur(4, 1 + math.floor(st / 3))
e.w_blit("\x83", blit_bg, blit_fg)
elseif st % 3 == 1 then
e.w_set_cur(4, 1 + math.floor(st / 3))
e.w_blit("\x8f", blit_bg, blit_fg)
e.w_set_cur(4, 2 + math.floor(st / 3))
e.w_blit("\x83", blit_fg, blit_bg)
else
e.w_set_cur(4, 2 + math.floor(st / 3))
e.w_blit("\x8f", blit_fg, blit_bg)
end
-- left
if st % 3 == 0 then
e.w_set_cur(1, 3 - math.floor(st / 3))
e.w_blit("\x83", blit_fg, blit_bg)
e.w_set_cur(1, 2 - math.floor(st / 3))
e.w_blit("\x8f", blit_bg, blit_fg)
elseif st % 3 == 1 then
e.w_set_cur(1, 2 - math.floor(st / 3))
e.w_blit("\x83", blit_bg, blit_fg)
else
e.w_set_cur(1, 2 - math.floor(st / 3))
e.w_blit("\x8f", blit_fg, blit_bg)
end
end
state = state + 1
if state >= 12 then state = 0 end
if run_animation then
tcd.dispatch_unique(0.15, animate)
end
end
-- start the animation
function e.start_anim()
run_animation = true
animate()
end
-- stop the animation
function e.stop_anim()
run_animation = false
end
e.start_anim()
---@class Waiting:graphics_element
local Waiting, id = e.complete()
return Waiting, id
end

View File

@ -0,0 +1,130 @@
-- App Button Graphics Element
local tcd = require("scada-common.tcd")
local core = require("graphics.core")
local element = require("graphics.element")
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class app_button_args
---@field text string app icon text
---@field title string app title text
---@field callback function function to call on touch
---@field app_fg_bg cpair app icon foreground/background colors
---@field active_fg_bg? cpair foreground/background colors when pressed
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new app icon style button control element, like on a mobile device.
---@param args app_button_args
---@return App element, element_id id
return function (args)
element.assert(type(args.text) == "string", "text is a required field")
element.assert(type(args.title) == "string", "title is a required field")
element.assert(type(args.callback) == "function", "callback is a required field")
element.assert(type(args.app_fg_bg) == "table", "app_fg_bg is a required field")
args.height = 4
args.width = 7
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
-- draw the app button
local function draw()
local fgd = args.app_fg_bg.fgd
local bkg = args.app_fg_bg.bkg
if e.value then
fgd = args.active_fg_bg.fgd
bkg = args.active_fg_bg.bkg
end
-- draw icon
e.w_set_cur(2, 1)
e.w_set_fgd(fgd)
e.w_set_bkg(bkg)
e.w_write("\x9f\x83\x83\x83")
e.w_set_fgd(bkg)
e.w_set_bkg(fgd)
e.w_write("\x90")
e.w_set_fgd(fgd)
e.w_set_bkg(bkg)
e.w_set_cur(2, 2)
e.w_write("\x95 ")
e.w_set_fgd(bkg)
e.w_set_bkg(fgd)
e.w_write("\x95")
e.w_set_cur(2, 3)
e.w_write("\x82\x8f\x8f\x8f\x81")
-- write the icon text
e.w_set_cur(4, 2)
e.w_set_fgd(fgd)
e.w_set_bkg(bkg)
e.w_write(args.text)
end
-- draw the app button as pressed (if active_fg_bg set)
local function show_pressed()
if e.enabled and args.active_fg_bg ~= nil then
e.value = true
e.w_set_fgd(args.active_fg_bg.fgd)
e.w_set_bkg(args.active_fg_bg.bkg)
draw()
end
end
-- draw the app button as unpressed (if active_fg_bg set)
local function show_unpressed()
if e.enabled and args.active_fg_bg ~= nil then
e.value = false
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
draw()
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled then
if event.type == MOUSE_CLICK.TAP then
show_pressed()
-- show as unpressed in 0.25 seconds
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
args.callback()
elseif event.type == MOUSE_CLICK.DOWN then
show_pressed()
elseif event.type == MOUSE_CLICK.UP then
show_unpressed()
if e.in_frame_bounds(event.current.x, event.current.y) then
args.callback()
end
end
end
end
-- set the value (true simulates pressing the app button)
---@param val boolean new value
function e.set_value(val)
if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
end
-- element redraw
function e.redraw()
e.w_set_cur(math.floor((e.frame.w - string.len(args.title)) / 2) + 1, 4)
e.w_write(args.title)
draw()
end
---@class App:graphics_element
local App, id = e.complete(true)
return App, id
end

View File

@ -0,0 +1,129 @@
-- Checkbox Graphics Element
local core = require("graphics.core")
local element = require("graphics.element")
---@class checkbox_args
---@field label string checkbox text
---@field box_fg_bg cpair colors for checkbox
---@field disable_fg_bg? cpair text colors when disabled
---@field default? boolean default value
---@field callback? function function to call on press
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new checkbox control element.
---@param args checkbox_args
---@return Checkbox element, element_id id
return function (args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.box_fg_bg) == "table", "box_fg_bg is a required field")
args.can_focus = true
args.height = 1
args.width = 2 + string.len(args.label)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = args.default == true
-- show the button state
local function draw()
e.w_set_cur(1, 1)
local fgd, bkg = args.box_fg_bg.fgd, args.box_fg_bg.bkg
if (not e.enabled) and type(args.disable_fg_bg) == "table" then
fgd = args.disable_fg_bg.bkg
bkg = args.disable_fg_bg.fgd
end
if e.value then
-- show as selected
e.w_set_fgd(bkg)
e.w_set_bkg(fgd)
e.w_write("\x88")
e.w_set_fgd(fgd)
e.w_set_bkg(e.fg_bg.bkg)
e.w_write("\x95")
else
-- show as unselected
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(bkg)
e.w_write("\x88")
e.w_set_fgd(bkg)
e.w_set_bkg(e.fg_bg.bkg)
e.w_write("\x95")
end
end
-- write label text
local function draw_label()
if e.enabled and e.is_focused() then
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(e.fg_bg.fgd)
elseif (not e.enabled) and type(args.disable_fg_bg) == "table" then
e.w_set_fgd(args.disable_fg_bg.fgd)
e.w_set_bkg(args.disable_fg_bg.bkg)
else
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
end
e.w_set_cur(3, 1)
e.w_write(args.label)
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then
e.value = not e.value
draw()
if type(args.callback) == "function" then args.callback(e.value) end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == core.events.KEY_CLICK.DOWN then
if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then
e.value = not e.value
draw()
if type(args.callback) == "function" then args.callback(e.value) end
end
end
end
-- set the value
---@param val integer new value
function e.set_value(val)
e.value = val
draw()
end
-- element redraw
function e.redraw()
draw()
draw_label()
end
-- handle focus
e.on_focused = draw_label
e.on_unfocused = draw_label
-- handle enable
e.on_enabled = e.redraw
e.on_disabled = e.redraw
---@class Checkbox:graphics_element
local Checkbox, id = e.complete(true)
return Checkbox, id
end

View File

@ -0,0 +1,205 @@
-- Hazard-bordered Button Graphics Element
local tcd = require("scada-common.tcd")
local core = require("graphics.core")
local element = require("graphics.element")
---@class hazard_button_args
---@field text string text to show on button
---@field accent color accent color for hazard border
---@field dis_colors? cpair text color and border color when disabled
---@field callback function function to call on touch
---@field timeout? integer override for the default 1.5 second timeout, in seconds
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new hazard button control element.
---@param args hazard_button_args
---@return HazardButton element, element_id id
return function (args)
element.assert(type(args.text) == "string", "text is a required field")
element.assert(type(args.accent) == "number", "accent is a required field")
element.assert(type(args.callback) == "function", "callback is a required field")
args.height = 3
args.width = string.len(args.text) + 4
local timeout = args.timeout or 1.5
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
-- draw border
---@param accent color accent color
local function draw_border(accent)
-- top
e.w_set_fgd(accent)
e.w_set_bkg(args.fg_bg.bkg)
e.w_set_cur(1, 1)
e.w_write("\x99" .. string.rep("\x89", args.width - 2) .. "\x99")
-- center left
e.w_set_cur(1, 2)
e.w_set_fgd(args.fg_bg.bkg)
e.w_set_bkg(accent)
e.w_write("\x99")
-- center right
e.w_set_fgd(args.fg_bg.bkg)
e.w_set_bkg(accent)
e.w_set_cur(args.width, 2)
e.w_write("\x99")
-- bottom
e.w_set_fgd(accent)
e.w_set_bkg(args.fg_bg.bkg)
e.w_set_cur(1, 3)
e.w_write("\x99" .. string.rep("\x98", args.width - 2) .. "\x99")
end
-- on request timeout: recursively calls itself to double flash button text
---@param n integer call count
local function on_timeout(n)
-- start at 0
if n == nil then n = 0 end
if n == 0 then
-- go back off
e.w_set_fgd(args.fg_bg.fgd)
e.w_set_cur(3, 2)
e.w_write(args.text)
end
if n >= 4 then
-- done
elseif n % 2 == 0 then
-- toggle text color on after 0.25 seconds
tcd.dispatch(0.25, function ()
e.w_set_fgd(args.accent)
e.w_set_cur(3, 2)
e.w_write(args.text)
on_timeout(n + 1)
on_timeout(n + 1)
end)
elseif n % 1 then
-- toggle text color off after 0.25 seconds
tcd.dispatch(0.25, function ()
e.w_set_fgd(args.fg_bg.fgd)
e.w_set_cur(3, 2)
e.w_write(args.text)
on_timeout(n + 1)
end)
end
end
-- blink routine for success indication
local function on_success()
e.w_set_fgd(args.fg_bg.fgd)
e.w_set_cur(3, 2)
e.w_write(args.text)
end
-- blink routine for failure indication
---@param n integer call count
local function on_failure(n)
-- start at 0
if n == nil then n = 0 end
if n == 0 then
-- go back off
e.w_set_fgd(args.fg_bg.fgd)
e.w_set_cur(3, 2)
e.w_write(args.text)
end
if n >= 2 then
-- done
elseif n % 2 == 0 then
-- toggle text color on after 0.5 seconds
tcd.dispatch(0.5, function ()
e.w_set_fgd(args.accent)
e.w_set_cur(3, 2)
e.w_write(args.text)
on_failure(n + 1)
end)
elseif n % 1 then
-- toggle text color off after 0.25 seconds
tcd.dispatch(0.25, function ()
e.w_set_fgd(args.fg_bg.fgd)
e.w_set_cur(3, 2)
e.w_write(args.text)
on_failure(n + 1)
end)
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then
-- change text color to indicate clicked
e.w_set_fgd(args.accent)
e.w_set_cur(3, 2)
e.w_write(args.text)
-- abort any other callbacks
tcd.abort(on_timeout)
tcd.abort(on_success)
tcd.abort(on_failure)
-- operation timeout animation
tcd.dispatch(timeout, on_timeout)
args.callback()
end
end
-- set the value (true simulates pressing the button)
---@param val boolean new value
function e.set_value(val)
if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
end
-- show the button as disabled
function e.on_disabled()
if args.dis_colors then
draw_border(args.dis_colors.color_a)
e.w_set_fgd(args.dis_colors.color_b)
e.w_set_cur(3, 2)
e.w_write(args.text)
end
end
-- show the button as enabled
function e.on_enabled()
draw_border(args.accent)
e.w_set_fgd(args.fg_bg.fgd)
e.w_set_cur(3, 2)
e.w_write(args.text)
end
-- element redraw
function e.redraw()
-- write the button text and draw border
e.w_set_cur(3, 2)
e.w_write(args.text)
draw_border(args.accent)
end
---@class HazardButton:graphics_element
local HazardButton, id = e.complete(true)
-- callback for request response
---@param success boolean
function HazardButton.on_response(success)
tcd.abort(on_timeout)
if success then on_success() else on_failure(0) end
end
return HazardButton, id
end

View File

@ -0,0 +1,133 @@
-- Multi Button Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
---@class button_option
---@field text string
---@field fg_bg cpair
---@field active_fg_bg cpair
---@field _start_x integer starting touch x range (inclusive)
---@field _end_x integer ending touch x range (inclusive)
---@class multi_button_args
---@field options table button options
---@field callback function function to call on touch
---@field default? integer default state, defaults to options[1]
---@field min_width? integer text length + 2 if omitted
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new multi button control element (latch selection, exclusively one button at a time).
---@param args multi_button_args
---@return MultiButton element, element_id id
return function (args)
element.assert(type(args.options) == "table", "options is a required field")
element.assert(#args.options > 0, "at least one option is required")
element.assert(type(args.callback) == "function", "callback is a required field")
element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
-- single line
args.height = 1
-- determine widths
local max_width = 1
for i = 1, #args.options do
local opt = args.options[i] ---@type button_option
if string.len(opt.text) > max_width then
max_width = string.len(opt.text)
end
end
local button_width = math.max(max_width, args.min_width or 0)
args.width = (button_width * #args.options) + #args.options + 1
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
-- button state (convert nil to 1 if missing)
e.value = args.default or 1
-- calculate required button information
local next_x = 2
for i = 1, #args.options do
local opt = args.options[i] ---@type button_option
opt._start_x = next_x
opt._end_x = next_x + button_width - 1
next_x = next_x + (button_width + 1)
end
-- show the button state
function e.redraw()
for i = 1, #args.options do
local opt = args.options[i] ---@type button_option
e.w_set_cur(opt._start_x, 1)
if e.value == i then
-- show as pressed
e.w_set_fgd(opt.active_fg_bg.fgd)
e.w_set_bkg(opt.active_fg_bg.bkg)
else
-- show as unpressed
e.w_set_fgd(opt.fg_bg.fgd)
e.w_set_bkg(opt.fg_bg.bkg)
end
e.w_write(util.pad(opt.text, button_width))
end
end
-- check which button a given x is within
---@return integer|nil button index or nil if not within a button
local function which_button(x)
for i = 1, #args.options do
local opt = args.options[i] ---@type button_option
if x >= opt._start_x and x <= opt._end_x then return i end
end
return nil
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
-- if enabled and the button row was pressed...
if e.enabled and core.events.was_clicked(event.type) then
-- a button may have been pressed, which one was it?
local button_ini = which_button(event.initial.x)
local button_cur = which_button(event.current.x)
-- mouse up must always have started with a mouse down on the same button to count as a click
-- tap always has identical coordinates, so this always passes for taps
if button_ini == button_cur and button_cur ~= nil then
e.value = button_cur
e.redraw()
args.callback(e.value)
end
end
end
-- set the value
---@param val integer new value
function e.set_value(val)
e.value = val
e.redraw()
end
---@class MultiButton:graphics_element
local MultiButton, id = e.complete(true)
return MultiButton, id
end

View File

@ -0,0 +1,186 @@
-- Spinbox Numeric Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
---@class spinbox_args
---@field default? number default value, defaults to 0.0
---@field min? number default 0, currently must be 0 or greater
---@field max? number default max number that can be displayed with the digits configuration
---@field whole_num_precision integer number of whole number digits
---@field fractional_precision integer number of fractional digits
---@field arrow_fg_bg cpair arrow foreground/background colors
---@field arrow_disable? color color when disabled (default light gray)
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new spinbox control element (minimum value is 0).
---@param args spinbox_args
---@return NumericSpinbox element, element_id id
return function (args)
-- properties
local digits = {}
local wn_prec = args.whole_num_precision
local fr_prec = args.fractional_precision
element.assert(util.is_int(wn_prec), "whole number precision must be an integer")
element.assert(util.is_int(fr_prec), "fractional precision must be an integer")
local fmt, fmt_init ---@type string, string
if fr_prec > 0 then
fmt = "%" .. (wn_prec + fr_prec + 1) .. "." .. fr_prec .. "f"
fmt_init = "%0" .. (wn_prec + fr_prec + 1) .. "." .. fr_prec .. "f"
else
fmt = "%" .. wn_prec .. "d"
fmt_init = "%0" .. wn_prec .. "d"
end
local dec_point_x = args.whole_num_precision + 1
element.assert(type(args.arrow_fg_bg) == "table", "arrow_fg_bg is a required field")
-- determine widths
args.width = wn_prec + fr_prec + util.trinary(fr_prec > 0, 1, 0)
args.height = 3
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
-- set initial value
e.value = args.default or 0
-- draw the arrows
local function draw_arrows(color)
e.w_set_bkg(args.arrow_fg_bg.bkg)
e.w_set_fgd(color)
e.w_set_cur(1, 1)
e.w_write(string.rep("\x1e", wn_prec))
e.w_set_cur(1, 3)
e.w_write(string.rep("\x1f", wn_prec))
if fr_prec > 0 then
e.w_set_cur(1 + wn_prec, 1)
e.w_write(" " .. string.rep("\x1e", fr_prec))
e.w_set_cur(1 + wn_prec, 3)
e.w_write(" " .. string.rep("\x1f", fr_prec))
end
end
-- populate digits from current value
local function set_digits()
local initial_str = util.sprintf(fmt_init, e.value)
digits = {}
---@diagnostic disable-next-line: discard-returns
initial_str:gsub("%d", function (char) table.insert(digits, char) end)
end
-- update the value per digits table
local function update_value()
e.value = 0
for i = 1, #digits do
local pow = math.abs(wn_prec - i)
if i <= wn_prec then
e.value = e.value + (digits[i] * (10 ^ pow))
else
e.value = e.value + (digits[i] * (10 ^ -pow))
end
end
end
-- print out the current value
local function show_num()
-- enforce limits
if (type(args.min) == "number") and (e.value < args.min) then
e.value = args.min
set_digits()
elseif e.value < 0 then
e.value = 0
set_digits()
else
if string.len(util.sprintf(fmt, e.value)) > args.width then
-- max printable exceeded, so max out to all 9s
for i = 1, #digits do digits[i] = 9 end
update_value()
elseif (type(args.max) == "number") and (e.value > args.max) then
e.value = args.max
set_digits()
else
set_digits()
end
end
-- draw
e.w_set_bkg(e.fg_bg.bkg)
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_cur(1, 2)
e.w_write(util.sprintf(fmt, e.value))
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
-- only handle if on an increment or decrement arrow
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) and
(event.current.x ~= dec_point_x) and (event.current.y ~= 2) and
(event.current.x == event.initial.x) and (event.current.y == event.initial.y) then
local idx = util.trinary(event.current.x > dec_point_x, event.current.x - 1, event.current.x)
if digits[idx] ~= nil then
if event.current.y == 1 then
digits[idx] = digits[idx] + 1
elseif event.current.y == 3 then
digits[idx] = digits[idx] - 1
end
update_value()
show_num()
end
end
end
-- set the value
---@param val number number to show
function e.set_value(val)
e.value = val
show_num()
end
-- set minimum input value
---@param min integer minimum allowed value
function e.set_min(min)
if min >= 0 then
args.min = min
show_num()
end
end
-- set maximum input value
---@param max integer maximum allowed value
function e.set_max(max)
args.max = max
show_num()
end
-- enable this input
function e.on_enabled() draw_arrows(args.arrow_fg_bg.fgd) end
-- disable this input
function e.on_disabled() draw_arrows(args.arrow_disable or colors.lightGray) end
-- element redraw
function e.redraw()
show_num()
draw_arrows(util.trinary(e.enabled, args.arrow_fg_bg.fgd, args.arrow_disable or colors.lightGray))
end
---@class NumericSpinbox:graphics_element
local NumericSpinbox, id = e.complete(true)
return NumericSpinbox, id
end

View File

@ -0,0 +1,164 @@
-- Button Graphics Element
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
local ALIGN = core.ALIGN
local MOUSE_CLICK = core.events.MOUSE_CLICK
local KEY_CLICK = core.events.KEY_CLICK
---@class push_button_args
---@field text string button text
---@field callback function function to call on touch
---@field min_width? integer text length if omitted
---@field alignment? ALIGN text align if min width > length
---@field active_fg_bg? cpair foreground/background colors when pressed
---@field dis_fg_bg? cpair foreground/background colors when disabled
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new push button control element.
---@param args push_button_args
---@return PushButton element, element_id id
return function (args)
element.assert(type(args.text) == "string", "text is a required field")
element.assert(type(args.callback) == "function", "callback is a required field")
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
local text_width = string.len(args.text)
local alignment = args.alignment or ALIGN.CENTER
-- set automatic settings
args.can_focus = true
args.min_width = args.min_width or 0
args.width = math.max(text_width, args.min_width)
-- provide a constraint condition to element creation to prefer a single line button
---@param frame graphics_frame
local function constrain(frame)
return frame.w, math.max(1, #util.strwrap(args.text, frame.w))
end
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]], constrain)
local text_lines = util.strwrap(args.text, e.frame.w)
-- draw the button
function e.redraw()
e.window.clear()
for i = 1, #text_lines do
if i > e.frame.h then break end
local len = string.len(text_lines[i])
-- use cursor position to align this line
if alignment == ALIGN.CENTER then
e.w_set_cur(math.floor((e.frame.w - len) / 2) + 1, i)
elseif alignment == ALIGN.RIGHT then
e.w_set_cur((e.frame.w - len) + 1, i)
else
e.w_set_cur(1, i)
end
e.w_write(text_lines[i])
end
end
-- draw the button as pressed (if active_fg_bg set)
local function show_pressed()
if e.enabled and args.active_fg_bg ~= nil then
e.value = true
e.w_set_fgd(args.active_fg_bg.fgd)
e.w_set_bkg(args.active_fg_bg.bkg)
e.redraw()
end
end
-- draw the button as unpressed (if active_fg_bg set)
local function show_unpressed()
if e.enabled and args.active_fg_bg ~= nil then
e.value = false
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
e.redraw()
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled then
if event.type == MOUSE_CLICK.TAP then
show_pressed()
-- show as unpressed in 0.25 seconds
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
args.callback()
elseif event.type == MOUSE_CLICK.DOWN then
show_pressed()
elseif event.type == MOUSE_CLICK.UP then
show_unpressed()
if e.in_frame_bounds(event.current.x, event.current.y) then
args.callback()
end
end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == KEY_CLICK.DOWN then
if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then
args.callback()
-- visualize click without unfocusing
show_unpressed()
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_pressed) end
end
end
end
-- set the value (true simulates pressing the button)
---@param val boolean new value
function e.set_value(val)
if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
end
-- show butten as enabled
function e.on_enabled()
if args.dis_fg_bg ~= nil then
e.value = false
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
e.redraw()
end
end
-- show button as disabled
function e.on_disabled()
if args.dis_fg_bg ~= nil then
e.value = false
e.w_set_fgd(args.dis_fg_bg.fgd)
e.w_set_bkg(args.dis_fg_bg.bkg)
e.redraw()
end
end
-- handle focus
e.on_focused = show_pressed
e.on_unfocused = show_unpressed
---@class PushButton:graphics_element
local PushButton, id = e.complete(true)
return PushButton, id
end

View File

@ -0,0 +1,201 @@
-- 2D Radio Button Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
---@class radio_2d_args
---@field rows integer
---@field columns integer
---@field options table
---@field radio_colors cpair radio button colors (inner & outer)
---@field select_color? color color for radio button when selected
---@field color_map? table colors for each radio button when selected
---@field disable_color? color color for radio button when disabled
---@field disable_fg_bg? cpair text colors when disabled
---@field default? integer default state, defaults to options[1]
---@field callback? function function to call on touch
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new 2-dimensional (rows and columns of options) radio button list control element (latch selection, exclusively one color at a time).
---@param args radio_2d_args
---@return Radio2D element, element_id id
return function (args)
element.assert(type(args.options) == "table" and #args.options > 0, "options should be a table with length >= 1")
element.assert(util.is_int(args.rows) and util.is_int(args.columns), "rows/columns must be integers")
element.assert((args.rows * args.columns) >= #args.options, "rows x columns size insufficient for provided number of options")
element.assert(type(args.radio_colors) == "table", "radio_colors is a required field")
element.assert(type(args.select_color) == "number" or type(args.color_map) == "table", "select_color or color_map is required")
element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
local array = {}
local col_widths = {}
local next_idx = 1
local total_width = 0
local max_rows = 1
local focused_opt = 1
local focus_x, focus_y = 1, 1
-- build table to display
for col = 1, args.columns do
local max_width = 0
array[col] = {}
for row = 1, args.rows do
local len = string.len(args.options[next_idx])
if len > max_width then max_width = len end
if row > max_rows then max_rows = row end
table.insert(array[col], { text = args.options[next_idx], id = next_idx, x_1 = 1 + total_width, x_2 = 2 + total_width + len })
next_idx = next_idx + 1
if next_idx > #args.options then break end
end
table.insert(col_widths, max_width + 3)
total_width = total_width + max_width + 3
if next_idx > #args.options then break end
end
args.can_focus = true
args.width = total_width
args.height = max_rows
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
-- selected option (convert nil to 1 if missing)
e.value = args.default or 1
-- draw the element
function e.redraw()
local col_x = 1
local radio_color_b = util.trinary(type(args.disable_color) == "number" and not e.enabled, args.disable_color, args.radio_colors.color_b)
for col = 1, #array do
for row = 1, #array[col] do
local opt = array[col][row]
local select_color = args.select_color
if type(args.color_map) == "table" and args.color_map[opt.id] then
select_color = args.color_map[opt.id]
end
local inner_color = util.trinary((e.value == opt.id) and e.enabled, radio_color_b, args.radio_colors.color_a)
local outer_color = util.trinary((e.value == opt.id) and e.enabled, select_color, radio_color_b)
e.w_set_cur(col_x, row)
e.w_set_fgd(inner_color)
e.w_set_bkg(outer_color)
e.w_write("\x88")
e.w_set_fgd(outer_color)
e.w_set_bkg(e.fg_bg.bkg)
e.w_write("\x95")
if opt.id == focused_opt then
focus_x, focus_y = row, col
end
-- write button text
if opt.id == focused_opt and e.is_focused() and e.enabled then
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(e.fg_bg.fgd)
elseif type(args.disable_fg_bg) == "table" and not e.enabled then
e.w_set_fgd(args.disable_fg_bg.fgd)
e.w_set_bkg(args.disable_fg_bg.bkg)
else
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
end
e.w_write(opt.text)
end
col_x = col_x + col_widths[col]
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled and core.events.was_clicked(event.type) and (event.initial.y == event.current.y) then
-- determine what was pressed
for _, row in ipairs(array) do
local elem = row[event.current.y]
if elem ~= nil and event.initial.x >= elem.x_1 and event.initial.x <= elem.x_2 and event.current.x >= elem.x_1 and event.current.x <= elem.x_2 then
e.value = elem.id
focused_opt = elem.id
e.redraw()
if type(args.callback) == "function" then args.callback(e.value) end
break
end
end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == core.events.KEY_CLICK.DOWN or event.type == core.events.KEY_CLICK.HELD then
if event.type == core.events.KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then
e.value = focused_opt
e.redraw()
if type(args.callback) == "function" then args.callback(e.value) end
elseif event.key == keys.down then
if focused_opt < #args.options then
focused_opt = focused_opt + 1
e.redraw()
end
elseif event.key == keys.up then
if focused_opt > 1 then
focused_opt = focused_opt - 1
e.redraw()
end
elseif event.key == keys.right then
if array[focus_y + 1] and array[focus_y + 1][focus_x] then
focused_opt = array[focus_y + 1][focus_x].id
else focused_opt = array[1][focus_x].id end
e.redraw()
elseif event.key == keys.left then
if array[focus_y - 1] and array[focus_y - 1][focus_x] then
focused_opt = array[focus_y - 1][focus_x].id
e.redraw()
elseif array[#array][focus_x] then
focused_opt = array[#array][focus_x].id
e.redraw()
end
end
end
end
-- set the value
---@param val integer new value
function e.set_value(val)
if type(val) == "number" and val > 0 and val <= #args.options then
e.value = val
e.redraw()
end
end
-- handle focus & enable
e.on_focused = e.redraw
e.on_unfocused = e.redraw
e.on_enabled = e.redraw
e.on_disabled = e.redraw
---@class Radio2D:graphics_element
local Radio2D, id = e.complete(true)
return Radio2D, id
end

View File

@ -0,0 +1,156 @@
-- Radio Button Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
local KEY_CLICK = core.events.KEY_CLICK
---@class radio_button_args
---@field options table button options
---@field radio_colors cpair radio button colors (inner & outer)
---@field select_color color color for radio button border when selected
---@field dis_fg_bg? cpair foreground/background colors when disabled
---@field default? integer default state, defaults to options[1]
---@field min_width? integer text length + 2 if omitted
---@field callback? function function to call on touch
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new radio button list control element (latch selection, exclusively one button at a time).
---@param args radio_button_args
---@return RadioButton element, element_id id
return function (args)
element.assert(type(args.options) == "table", "options is a required field")
element.assert(#args.options > 0, "at least one option is required")
element.assert(type(args.radio_colors) == "table", "radio_colors is a required field")
element.assert(type(args.select_color) == "number", "select_color is a required field")
element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
-- determine widths
local max_width = 1
for i = 1, #args.options do
local opt = args.options[i] ---@type string
if string.len(opt) > max_width then
max_width = string.len(opt)
end
end
local button_text_width = math.max(max_width, args.min_width or 0)
-- set automatic args
args.can_focus = true
args.width = button_text_width + 2
args.height = #args.options -- one line per option
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local focused_opt = 1
-- button state (convert nil to 1 if missing)
e.value = args.default or 1
-- show the button state
function e.redraw()
for i = 1, #args.options do
local opt = args.options[i] ---@type string
local inner_color = util.trinary(e.value == i, args.radio_colors.color_b, args.radio_colors.color_a)
local outer_color = util.trinary(e.value == i, args.select_color, args.radio_colors.color_b)
if e.value == i and args.dis_fg_bg and not e.enabled then
outer_color = args.radio_colors.color_a
end
e.w_set_cur(1, i)
e.w_set_fgd(inner_color)
e.w_set_bkg(outer_color)
e.w_write("\x88")
e.w_set_fgd(outer_color)
e.w_set_bkg(e.fg_bg.bkg)
e.w_write("\x95")
-- write button text
if args.dis_fg_bg and not e.enabled then
e.w_set_fgd(args.dis_fg_bg.fgd)
e.w_set_bkg(args.dis_fg_bg.bkg)
elseif i == focused_opt and e.is_focused() then
if e.enabled then
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(e.fg_bg.fgd)
end
else
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
end
e.w_write(opt)
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled and core.events.was_clicked(event.type) and
(event.initial.y == event.current.y) and e.in_frame_bounds(event.current.x, event.current.y) then
-- determine what was pressed
if args.options[event.current.y] ~= nil then
e.value = event.current.y
focused_opt = e.value
e.redraw()
if type(args.callback) == "function" then args.callback(e.value) end
end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
if event.type == KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then
e.value = focused_opt
e.redraw()
if type(args.callback) == "function" then args.callback(e.value) end
elseif event.key == keys.down then
if focused_opt < #args.options then
focused_opt = focused_opt + 1
e.redraw()
end
elseif event.key == keys.up then
if focused_opt > 1 then
focused_opt = focused_opt - 1
e.redraw()
end
end
end
end
-- set the value
---@param val integer new value
function e.set_value(val)
if type(val) == "number" and val > 0 and val <= #args.options then
e.value = val
e.redraw()
end
end
-- handle focus & enable
e.on_focused = e.redraw
e.on_unfocused = e.redraw
e.on_enabled = e.redraw
e.on_disabled = e.redraw
---@class RadioButton:graphics_element
local RadioButton, id = e.complete(true)
return RadioButton, id
end

View File

@ -0,0 +1,173 @@
-- Sidebar Graphics Element
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class sidebar_args
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new sidebar tab selector control element.
---@param args sidebar_args
---@return Sidebar element, element_id id
return function (args)
args.width = 3
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
-- default to 1st tab
e.value = 1
local was_pressed = false
local tabs = {}
-- show the button state
---@param pressed? boolean if the currently selected tab should appear as actively pressed
---@param pressed_idx? integer optional index to show as held (that is not yet selected)
local function draw(pressed, pressed_idx)
pressed = util.trinary(pressed == nil, was_pressed, pressed)
was_pressed = pressed
pressed_idx = pressed_idx or e.value
-- clear
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
for y = 1, e.frame.h do
e.w_set_cur(1, y)
e.w_write(" ")
end
-- draw tabs
for i = 1, #tabs do
local tab = tabs[i] ---@type sidebar_tab
local y = tab.y_start
e.w_set_cur(1, y)
if pressed and i == pressed_idx then
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
else
e.w_set_fgd(tab.color.fgd)
e.w_set_bkg(tab.color.bkg)
end
if tab.tall then
e.w_write(" ")
e.w_set_cur(1, y + 1)
end
e.w_write(tab.label)
if tab.tall then
e.w_set_cur(1, y + 2)
e.w_write(" ")
end
end
end
-- determine which tab was pressed
---@param y integer y coordinate
local function find_tab(y)
for i = 1, #tabs do
local tab = tabs[i] ---@type sidebar_tab
if y >= tab.y_start and y <= tab.y_end then
return i
end
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
-- determine what was pressed
if e.enabled then
local cur_idx = find_tab(event.current.y)
local ini_idx = find_tab(event.initial.y)
local tab = tabs[cur_idx]
-- handle press if a callback was provided
if tab ~= nil and type(tab.callback) == "function" then
if event.type == MOUSE_CLICK.TAP then
e.value = cur_idx
draw(true)
-- show as unpressed in 0.25 seconds
tcd.dispatch(0.25, function () draw(false) end)
tab.callback()
elseif event.type == MOUSE_CLICK.DOWN then
draw(true, cur_idx)
elseif event.type == MOUSE_CLICK.UP then
if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then
e.value = cur_idx
draw(false)
tab.callback()
else draw(false) end
end
elseif event.type == MOUSE_CLICK.UP then
draw(false)
end
end
end
-- set the value
---@param val integer new value
function e.set_value(val)
e.value = val
draw(false)
end
-- update the sidebar navigation options
---@param items sidebar_entry[] sidebar entries
function e.on_update(items)
---@class sidebar_entry
---@field label string
---@field tall boolean
---@field color cpair
---@field callback function|nil
local next_y = 1
tabs = {}
for i = 1, #items do
local item = items[i]
local height = util.trinary(item.tall, 3, 1)
---@class sidebar_tab
local entry = {
y_start = next_y, ---@type integer
y_end = next_y + height - 1, ---@type integer
tall = item.tall, ---@type boolean
label = item.label, ---@type string
color = item.color, ---@type cpair
callback = item.callback ---@type function|nil
}
next_y = next_y + height
tabs[i] = entry
end
draw()
end
-- element redraw
e.redraw = draw
---@class Sidebar:graphics_element
local Sidebar, id = e.complete(true)
return Sidebar, id
end

View File

@ -0,0 +1,79 @@
-- Button Graphics Element
local core = require("graphics.core")
local element = require("graphics.element")
---@class switch_button_args
---@field text string button text
---@field callback function function to call on touch
---@field default? boolean default state, defaults to off (false)
---@field min_width? integer text length + 2 if omitted
---@field active_fg_bg cpair foreground/background colors when pressed
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new latching switch button control element.
---@param args switch_button_args
---@return SwitchButton element, element_id id
return function (args)
element.assert(type(args.text) == "string", "text is a required field")
element.assert(type(args.callback) == "function", "callback is a required field")
element.assert(type(args.active_fg_bg) == "table", "active_fg_bg is a required field")
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
local text_width = string.len(args.text)
args.height = 1
args.min_width = args.min_width or 0
args.width = math.max(text_width, args.min_width)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = args.default or false
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1
local v_pad = math.floor(e.frame.h / 2) + 1
-- show the button state
function e.redraw()
if e.value then
e.w_set_fgd(args.active_fg_bg.fgd)
e.w_set_bkg(args.active_fg_bg.bkg)
else
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
end
e.window.clear()
e.w_set_cur(h_pad, v_pad)
e.w_write(args.text)
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then
e.value = not e.value
e.redraw()
args.callback(e.value)
end
end
-- set the value (does not call the callback)
---@param val boolean new value
function e.set_value(val)
e.value = val
e.redraw()
end
---@class SwitchButton:graphics_element
local SwitchButton, id = e.complete(true)
return SwitchButton, id
end

View File

@ -0,0 +1,127 @@
-- Tab Bar Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
---@class tabbar_tab
---@field name string tab name
---@field color cpair tab colors (fg/bg)
---@field _start_x integer starting touch x range (inclusive)
---@field _end_x integer ending touch x range (inclusive)
---@class tabbar_args
---@field tabs table tab options
---@field callback function function to call on tab change
---@field min_width? integer text length + 2 if omitted
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new tab selector control element.
---@param args tabbar_args
---@return TabBar element, element_id id
return function (args)
element.assert(type(args.tabs) == "table", "tabs is a required field")
element.assert(#args.tabs > 0, "at least one tab is required")
element.assert(type(args.callback) == "function", "callback is a required field")
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
args.height = 1
-- determine widths
local max_width = 1
for i = 1, #args.tabs do
local opt = args.tabs[i] ---@type tabbar_tab
if string.len(opt.name) > max_width then
max_width = string.len(opt.name)
end
end
local button_width = math.max(max_width, args.min_width or 0)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
element.assert(e.frame.w >= (button_width * #args.tabs), "width insufficent to display all tabs")
-- default to 1st tab
e.value = 1
-- calculate required tab dimension information
local next_x = 1
for i = 1, #args.tabs do
local tab = args.tabs[i] ---@type tabbar_tab
tab._start_x = next_x
tab._end_x = next_x + button_width - 1
next_x = next_x + button_width
end
-- show the tab state
function e.redraw()
for i = 1, #args.tabs do
local tab = args.tabs[i] ---@type tabbar_tab
e.w_set_cur(tab._start_x, 1)
if e.value == i then
e.w_set_fgd(tab.color.fgd)
e.w_set_bkg(tab.color.bkg)
else
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
end
e.w_write(util.pad(tab.name, button_width))
end
end
-- check which tab a given x is within
---@return integer|nil button index or nil if not within a tab
local function which_tab(x)
for i = 1, #args.tabs do
local tab = args.tabs[i] ---@type tabbar_tab
if x >= tab._start_x and x <= tab._end_x then return i end
end
return nil
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
-- determine what was pressed
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then
-- a button may have been pressed, which one was it?
local tab_ini = which_tab(event.initial.x)
local tab_cur = which_tab(event.current.x)
-- mouse up must always have started with a mouse down on the same tab to count as a click
-- tap always has identical coordinates, so this always passes for taps
if tab_ini == tab_cur and tab_cur ~= nil then
e.value = tab_cur
e.redraw()
args.callback(e.value)
end
end
end
-- set the value
---@param val integer new value
function e.set_value(val)
e.value = val
e.redraw()
end
---@class TabBar:graphics_element
local TabBar, id = e.complete(true)
return TabBar, id
end

View File

@ -0,0 +1,255 @@
-- Numeric Value Entry Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
local KEY_CLICK = core.events.KEY_CLICK
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class number_field_args
---@field default? number default value, defaults to 0
---@field min? number minimum, enforced on unfocus
---@field max? number maximum, enforced on unfocus
---@field max_chars? integer maximum number of characters, defaults to width
---@field max_int_digits? integer maximum number of integer digits, enforced on unfocus
---@field max_frac_digits? integer maximum number of fractional digits, enforced on unfocus
---@field allow_decimal? boolean true to allow decimals
---@field allow_negative? boolean true to allow negative numbers
---@field align_right? boolean true to align right while unfocused
---@field dis_fg_bg? cpair foreground/background colors when disabled
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new numeric entry field.
---@param args number_field_args
---@return NumberField element, element_id id
return function (args)
element.assert(args.max_int_digits == nil or (util.is_int(args.max_int_digits) and args.max_int_digits > 0), "max_int_digits must be an integer greater than zero if supplied")
element.assert(args.max_frac_digits == nil or (util.is_int(args.max_frac_digits) and args.max_frac_digits > 0), "max_frac_digits must be an integer greater than zero if supplied")
args.height = 1
args.can_focus = true
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local has_decimal = false
args.max_chars = args.max_chars or e.frame.w
-- determine the format to convert the number to a string
local format = "%d"
if args.allow_decimal then
if args.max_frac_digits then
format = "%."..args.max_frac_digits.."f"
else format = "%f" end
end
-- set the value to a formatted numeric string<br>
-- trims trailing zeros from floating point numbers
---@param num number
local function _set_value(num)
local str = util.sprintf(format, num)
if args.allow_decimal then
local found_nonzero = false
local str_table = {}
for i = #str, 1, -1 do
local c = string.sub(str, i, i)
if found_nonzero then
str_table[i] = c
else
if c == "." then
found_nonzero = true
elseif c ~= "0" then
str_table[i] = c
found_nonzero = true
end
end
end
e.value = table.concat(str_table)
else
e.value = str
end
end
-- set initial value
_set_value(args.default or 0)
-- make an interactive field manager
local ifield = core.new_ifield(e, args.max_chars, args.fg_bg, args.dis_fg_bg, args.align_right)
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
-- only handle if on an increment or decrement arrow
if e.enabled and e.in_frame_bounds(event.current.x, event.current.y) then
if core.events.was_clicked(event.type) then
local x = event.current.x
if not e.is_focused() then
x = ifield.get_cursor_align_shift(x)
end
e.take_focus()
if event.type == MOUSE_CLICK.UP then
ifield.move_cursor(x)
end
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
ifield.select_all()
end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == KEY_CLICK.CHAR and string.len(e.value) < args.max_chars then
if tonumber(event.name) then
if e.value == 0 then e.value = "" end
ifield.try_insert_char(event.name)
end
elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
if (event.key == keys.backspace or event.key == keys.delete) and (string.len(e.value) > 0) then
ifield.backspace()
has_decimal = string.find(e.value, "%.") ~= nil
elseif (event.key == keys.period or event.key == keys.numPadDecimal) and (not has_decimal) and args.allow_decimal then
has_decimal = true
ifield.try_insert_char(".")
elseif (event.key == keys.minus or event.key == keys.numPadSubtract) and (string.len(e.value) == 0) and args.allow_negative then
ifield.set_value("-")
elseif event.key == keys.left then
ifield.nav_left()
elseif event.key == keys.right then
ifield.nav_right()
elseif event.key == keys.a and event.ctrl then
ifield.select_all()
elseif event.key == keys.home or event.key == keys.up then
ifield.nav_start()
elseif event.key == keys["end"] or event.key == keys.down then
ifield.nav_end()
end
end
end
-- set the value (must be a number)
---@param val number number to show
function e.set_value(val)
local num, max, min = tonumber(val), tonumber(args.max), tonumber(args.min)
if max and num > max then
_set_value(max)
elseif min and num < min then
_set_value(min)
elseif num then
_set_value(num)
end
ifield.set_value(e.value)
end
-- set minimum input value
---@param min integer minimum allowed value
function e.set_min(min)
args.min = min
e.on_unfocused()
end
-- set maximum input value
---@param max integer maximum allowed value
function e.set_max(max)
args.max = max
e.on_unfocused()
end
-- replace text with pasted text if its a number
---@param text string string pasted
function e.handle_paste(text)
if tonumber(text) then
ifield.set_value("" .. tonumber(text))
else
ifield.set_value("0")
end
end
-- handle unfocused
function e.on_unfocused()
local val, max, min = tonumber(e.value), tonumber(args.max), tonumber(args.min)
if val then
if args.max_int_digits or args.max_frac_digits then
local str = e.value
local ceil = false
if string.find(str, "-") then str = string.sub(e.value, 2) end
local parts = util.strtok(str, ".")
if parts[1] and args.max_int_digits then
if string.len(parts[1]) > args.max_int_digits then
parts[1] = string.rep("9", args.max_int_digits)
ceil = true
end
end
if args.allow_decimal and args.max_frac_digits then
if ceil then
parts[2] = string.rep("9", args.max_frac_digits)
elseif parts[2] and (string.len(parts[2]) > args.max_frac_digits) then
-- add a half of the highest precision fractional value in order to round using floor
local scaled = math.fmod(val, 1) * (10 ^ (args.max_frac_digits))
local value = math.floor(scaled + 0.5)
local unscaled = value * (10 ^ (-args.max_frac_digits))
parts[2] = string.sub(tostring(unscaled), 3) -- remove starting "0."
end
end
if parts[2] then parts[2] = "." .. parts[2] else parts[2] = "" end
val = tonumber((parts[1] or "") .. parts[2]) or 0
end
if max and val > max then
_set_value(max)
ifield.nav_start()
elseif min and val < min then
_set_value(min)
ifield.nav_start()
else
_set_value(val)
ifield.nav_end()
end
else
e.value = ""
end
ifield.show()
end
-- handle focus (not unfocus), enable, and redraw with show()
e.on_focused = ifield.show
e.on_enabled = ifield.show
e.on_disabled = ifield.show
e.redraw = ifield.show
---@class NumberField:graphics_element
local NumberField, id = e.complete(true)
-- get the numeric value of this field
---@return number value the value, or 0 if not a valid number
function NumberField.get_numeric()
return tonumber(e.value) or 0
end
return NumberField, id
end

View File

@ -0,0 +1,104 @@
-- Text Value Entry Graphics Element
local core = require("graphics.core")
local element = require("graphics.element")
local KEY_CLICK = core.events.KEY_CLICK
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class text_field_args
---@field value? string initial value
---@field max_len? integer maximum string length
---@field censor? string character to replace text with when printing to screen
---@field dis_fg_bg? cpair foreground/background colors when disabled
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new text entry field.
---@param args text_field_args
---@return TextField element, element_id id
return function (args)
args.height = 1
args.can_focus = true
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
-- set initial value
e.value = args.value or ""
-- make an interactive field manager
local ifield = core.new_ifield(e, args.max_len or e.frame.w, args.fg_bg, args.dis_fg_bg)
ifield.censor(args.censor)
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
-- only handle if on an increment or decrement arrow
if e.enabled and e.in_frame_bounds(event.current.x, event.current.y) then
if core.events.was_clicked(event.type) then
e.take_focus()
if event.type == MOUSE_CLICK.UP then
ifield.move_cursor(event.current.x)
end
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
ifield.select_all()
end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == KEY_CLICK.CHAR then
ifield.try_insert_char(event.name)
elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
if (event.key == keys.backspace or event.key == keys.delete) then
ifield.backspace()
elseif event.key == keys.left then
ifield.nav_left()
elseif event.key == keys.right then
ifield.nav_right()
elseif event.key == keys.a and event.ctrl then
ifield.select_all()
elseif event.key == keys.home or event.key == keys.up then
ifield.nav_start()
elseif event.key == keys["end"] or event.key == keys.down then
ifield.nav_end()
end
end
end
-- set the value
---@param val string string to set
function e.set_value(val)
ifield.set_value(val)
end
-- replace text with pasted text
---@param text string string to set
function e.handle_paste(text)
ifield.set_value(text)
end
-- handle focus, enable, and redraw with show()
e.on_focused = ifield.show
e.on_unfocused = ifield.show
e.on_enabled = ifield.show
e.on_disabled = ifield.show
e.redraw = ifield.show
---@class TextField:graphics_element
local TextField, id = e.complete(true)
TextField.censor = ifield.censor
return TextField, id
end

View File

@ -0,0 +1,120 @@
-- Tri-State Alarm Indicator Light Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
local flasher = require("graphics.flasher")
---@class alarm_indicator_light
---@field label string indicator label
---@field c1 color color for off state
---@field c2 color color for alarm state
---@field c3 color color for ring-back state
---@field min_label_width? integer label length if omitted
---@field flash? boolean whether to flash on alarm state rather than stay on
---@field period? PERIOD flash period
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new alarm indicator light element.
---@nodiscard
---@param args alarm_indicator_light
---@return AlarmLight element, element_id id
return function (args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.c1) == "number", "c1 is a required field")
element.assert(type(args.c2) == "number", "c2 is a required field")
element.assert(type(args.c3) == "number", "c3 is a required field")
if args.flash then
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
end
-- single line
args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
-- flasher state
local flash_on = true
-- blit translations
local c1 = colors.toBlit(args.c1)
local c2 = colors.toBlit(args.c2)
local c3 = colors.toBlit(args.c3)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = 1
-- called by flasher when enabled
local function flash_callback()
e.w_set_cur(1, 1)
if flash_on then
if e.value == 2 then
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
end
else
if e.value == 3 then
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
else
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end
end
flash_on = not flash_on
end
-- on state change
---@param new_state integer indicator state
function e.on_update(new_state)
local was_off = e.value ~= 2
e.value = new_state
e.w_set_cur(1, 1)
if args.flash then
if was_off and (new_state == 2) then
flash_on = true
flasher.start(flash_callback, args.period)
elseif new_state ~= 2 then
flash_on = false
flasher.stop(flash_callback)
if new_state == 3 then
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
else
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end
end
elseif new_state == 2 then
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
elseif new_state == 3 then
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
else
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end
end
-- set indicator state
---@param val integer indicator state
function e.set_value(val) e.on_update(val) end
-- draw label and indicator light
function e.redraw()
e.on_update(e.value)
e.w_write(args.label)
end
---@class AlarmLight:graphics_element
local AlarmLight, id = e.complete(true)
return AlarmLight, id
end

View File

@ -0,0 +1,172 @@
-- Reactor Core View Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
---@class core_map_args
---@field reactor_l integer reactor length
---@field reactor_w integer reactor width
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
-- Create a new core map diagram indicator element.
---@nodiscard
---@param args core_map_args
---@return CoreMap element, element_id id
return function (args)
element.assert(util.is_int(args.reactor_l), "reactor_l is a required field")
element.assert(util.is_int(args.reactor_w), "reactor_w is a required field")
-- require max dimensions
args.width = 18
args.height = 18
-- inherit only foreground color
args.fg_bg = core.cpair(args.parent.get_fg_bg().fgd, colors.gray)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = 0
local alternator = true
local core_l = args.reactor_l - 2
local core_w = args.reactor_w - 2
local shift_x = 8 - math.floor(core_l / 2)
local shift_y = 8 - math.floor(core_w / 2)
local start_x = 2 + shift_x
local start_y = 2 + shift_y
local inner_width = core_l
local inner_height = core_w
-- create coordinate grid and frame
local function draw_frame()
e.w_set_fgd(colors.white)
for x = 0, (inner_width - 1) do
e.w_set_cur(x + start_x, 1)
e.w_write(util.sprintf("%X", x))
end
for y = 0, (inner_height - 1) do
e.w_set_cur(1, y + start_y)
e.w_write(util.sprintf("%X", y))
end
-- even out bottom edge
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(args.parent.get_fg_bg().bkg)
e.w_set_cur(1, e.frame.h)
e.w_write(string.rep("\x8f", e.frame.w))
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
end
-- draw the core
---@param t number temperature in K
local function draw_core(t)
local i = 1
local back_c = "F"
local text_c ---@type string
-- determine fuel assembly coloring
if t <= 300 then
-- gray
text_c = "8"
elseif t <= 350 then
-- blue
text_c = "3"
elseif t < 600 then
-- green
text_c = "D"
elseif t < 1000 then
-- yellow
text_c = "4"
-- back_c = "8"
elseif t < 1200 then
-- orange
text_c = "1"
elseif t < 1300 then
-- red
text_c = "E"
else
-- pink
text_c = "2"
end
-- draw pattern
for y = start_y, inner_height + (start_y - 1) do
e.w_set_cur(start_x, y)
for _ = 1, inner_width do
if alternator then
i = i + 1
e.w_blit("\x07", text_c, back_c)
else
e.w_blit("\x07", "7", "8")
end
alternator = not alternator
end
if inner_width % 2 == 0 then alternator = not alternator end
end
-- reset alternator
alternator = true
end
-- on state change
---@param temperature number temperature in Kelvin
function e.on_update(temperature)
e.value = temperature
draw_core(e.value)
end
-- set temperature to display
---@param val number degrees K
function e.set_value(val) e.on_update(val) end
-- resize reactor dimensions
---@param reactor_l integer reactor length (rendered in 2D top-down as width)
---@param reactor_w integer reactor width (rendered in 2D top-down as height)
function e.resize(reactor_l, reactor_w)
-- enforce possible dimensions
if reactor_l > 18 then reactor_l = 18 elseif reactor_l < 3 then reactor_l = 3 end
if reactor_w > 18 then reactor_w = 18 elseif reactor_w < 3 then reactor_w = 3 end
-- update dimensions
core_l = reactor_l - 2
core_w = reactor_w - 2
shift_x = 8 - math.floor(core_l / 2)
shift_y = 8 - math.floor(core_w / 2)
start_x = 2 + shift_x
start_y = 2 + shift_y
inner_width = core_l
inner_height = core_w
e.window.clear()
-- re-draw
draw_frame()
e.on_update(e.value)
end
-- redraw both frame and core
function e.redraw()
draw_frame()
draw_core(e.value)
end
---@class CoreMap:graphics_element
local CoreMap, id = e.complete(true)
return CoreMap, id
end

View File

@ -0,0 +1,101 @@
-- Data Indicator Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class data_indicator_args
---@field label string indicator label
---@field unit? string indicator unit
---@field format string data format (lua string format)
---@field commas? boolean whether to use commas if a number is given (default to false)
---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
---@field value any default value
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width integer length
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create new data indicator element.
---@nodiscard
---@param args data_indicator_args
---@return DataIndicator element, element_id id
return function (args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.format) == "string", "format is a required field")
element.assert(args.value ~= nil, "value is a required field")
element.assert(util.is_int(args.width), "width is a required field")
args.height = 1
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = args.value
local value_color = e.fg_bg.fgd
local label_len = string.len(args.label)
local data_start = 1
local clear_width = args.width
if label_len > 0 then
data_start = data_start + (label_len + 1)
clear_width = args.width - (label_len + 1)
end
-- on state change
---@param value any new value
function e.on_update(value)
e.value = value
-- clear old data and label
e.w_set_cur(data_start, 1)
e.w_write(util.spaces(clear_width))
-- write data
local data_str = util.sprintf(args.format, value)
e.w_set_cur(data_start, 1)
e.w_set_fgd(value_color)
if args.commas then
e.w_write(util.comma_format(data_str))
else
e.w_write(data_str)
end
-- write label
if args.unit ~= nil then
if args.lu_colors ~= nil then
e.w_set_fgd(args.lu_colors.color_b)
end
e.w_write(" " .. args.unit)
end
end
-- set the value
---@param val any new value
function e.set_value(val) e.on_update(val) end
-- change the foreground color of the value, or all text if no label/unit colors provided
---@param c color
function e.recolor(c)
value_color = c
e.on_update(e.value)
end
-- element redraw
function e.redraw()
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
e.w_set_cur(1, 1)
e.w_write(args.label)
e.on_update(e.value)
end
---@class DataIndicator:graphics_element
local DataIndicator, id = e.complete(true)
return DataIndicator, id
end

View File

@ -0,0 +1,126 @@
-- Horizontal Bar Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class hbar_args
---@field show_percent? boolean whether or not to show the percent
---@field bar_fg_bg? cpair bar foreground/background colors if showing percent
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new horizontal fill bar indicator element.
---@nodiscard
---@param args hbar_args
---@return graphics_element element, element_id id
return function (args)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = 0.0
-- bar width is width - 5 characters for " 100%" if showing percent
local bar_width = util.trinary(args.show_percent, e.frame.w - 5, e.frame.w)
element.assert(bar_width > 0, "too small for bar")
local last_num_bars = -1
-- determine bar colors
local bar_bkg = e.fg_bg.blit_bkg
local bar_fgd = e.fg_bg.blit_fgd
if args.bar_fg_bg ~= nil then
bar_bkg = args.bar_fg_bg.blit_bkg
bar_fgd = args.bar_fg_bg.blit_fgd
end
-- handle data changes
---@param fraction number 0.0 to 1.0
function e.on_update(fraction)
e.value = fraction
-- enforce minimum and maximum
if fraction < 0 then
fraction = 0.0
elseif fraction > 1 then
fraction = 1.0
end
-- compute number of bars
local num_bars = util.round(fraction * (bar_width * 2))
-- redraw bar if changed
if num_bars ~= last_num_bars then
last_num_bars = num_bars
local fgd = ""
local bkg = ""
local spaces = ""
-- fill percentage
for _ = 1, num_bars / 2 do
spaces = spaces .. " "
fgd = fgd .. bar_fgd
bkg = bkg .. bar_bkg
end
-- add fractional bar if needed
if num_bars % 2 == 1 then
spaces = spaces .. "\x95"
fgd = fgd .. bar_bkg
bkg = bkg .. bar_fgd
end
-- pad background
for _ = 1, ((bar_width * 2) - num_bars) / 2 do
spaces = spaces .. " "
fgd = fgd .. bar_bkg
bkg = bkg .. bar_bkg
end
-- draw bar
for y = 1, e.frame.h do
e.w_set_cur(1, y)
-- intentionally swapped fgd/bkg since we use spaces as fill, but they are the opposite
e.w_blit(spaces, bkg, fgd)
end
end
-- update percentage
if args.show_percent then
e.w_set_cur(bar_width + 2, math.max(1, math.ceil(e.frame.h / 2)))
e.w_write(util.sprintf("%3.0f%%", fraction * 100))
end
end
-- change bar color
---@param bar_fg_bg cpair new bar colors
function e.recolor(bar_fg_bg)
bar_bkg = bar_fg_bg.blit_bkg
bar_fgd = bar_fg_bg.blit_fgd
e.redraw()
end
-- set the percentage value
---@param val number 0.0 to 1.0
function e.set_value(val) e.on_update(val) end
-- element redraw
function e.redraw()
last_num_bars = -1
e.on_update(e.value)
end
---@class HorizontalBar:graphics_element
local HorizontalBar, id = e.complete(true)
return HorizontalBar, id
end

View File

@ -0,0 +1,78 @@
-- Icon Indicator Graphics Element
local element = require("graphics.element")
---@class icon_sym_color
---@field color cpair
---@field symbol string
---@class icon_indicator_args
---@field label string indicator label
---@field states table state color and symbol table
---@field value? integer|boolean default state, defaults to 1 (true = 2, false = 1)
---@field min_label_width? integer label length if omitted
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new icon indicator element.
---@nodiscard
---@param args icon_indicator_args
---@return IconIndicator element, element_id id
return function (args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.states) == "table", "states is a required field")
args.height = 1
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = args.value or 1
if e.value == true then e.value = 2 end
-- state blit strings
local state_blit_cmds = {}
for i = 1, #args.states do
local sym_color = args.states[i] ---@type icon_sym_color
table.insert(state_blit_cmds, {
text = " " .. sym_color.symbol .. " ",
fgd = string.rep(sym_color.color.blit_fgd, 3),
bkg = string.rep(sym_color.color.blit_bkg, 3)
})
end
-- on state change
---@param new_state integer|boolean indicator state
function e.on_update(new_state)
new_state = new_state or 1
if new_state == true then new_state = 2 end
local blit_cmd = state_blit_cmds[new_state]
e.value = new_state
e.w_set_cur(1, 1)
e.w_blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
end
-- set indicator state
---@param val integer|boolean indicator state
function e.set_value(val) e.on_update(val) end
-- element redraw
function e.redraw()
e.w_set_cur(5, 1)
e.w_write(args.label)
e.on_update(e.value)
end
---@class IconIndicator:graphics_element
local IconIndicator, id = e.complete(true)
return IconIndicator, id
end

View File

@ -0,0 +1,100 @@
-- Indicator Light Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
local flasher = require("graphics.flasher")
---@class indicator_light_args
---@field label string indicator label
---@field colors cpair on/off colors (a/b respectively)
---@field min_label_width? integer label length if omitted
---@field flash? boolean whether to flash on true rather than stay on
---@field period? PERIOD flash period
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new indicator light element.
---@nodiscard
---@param args indicator_light_args
---@return IndicatorLight element, element_id id
return function (args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.colors) == "table", "colors is a required field")
if args.flash then
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
end
args.height = 1
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
local flash_on = true
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = false
-- called by flasher when enabled
local function flash_callback()
e.w_set_cur(1, 1)
if flash_on then
e.w_blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
else
e.w_blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
end
flash_on = not flash_on
end
-- enable light or start flashing
local function enable()
if args.flash then
flash_on = true
flasher.start(flash_callback, args.period)
else
e.w_set_cur(1, 1)
e.w_blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
end
end
-- disable light or stop flashing
local function disable()
if args.flash then
flash_on = false
flasher.stop(flash_callback)
end
e.w_set_cur(1, 1)
e.w_blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
end
-- on state change
---@param new_state boolean indicator state
function e.on_update(new_state)
e.value = new_state
if new_state then enable() else disable() end
end
-- set indicator state
---@param val boolean indicator state
function e.set_value(val) e.on_update(val) end
-- draw label and indicator light
function e.redraw()
e.on_update(false)
e.w_set_cur(3, 1)
e.w_write(args.label)
end
---@class IndicatorLight:graphics_element
local IndicatorLight, id = e.complete(true)
return IndicatorLight, id
end

View File

@ -0,0 +1,102 @@
-- Indicator "LED" Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
local flasher = require("graphics.flasher")
---@class indicator_led_args
---@field label string indicator label
---@field colors cpair on/off colors (a/b respectively)
---@field min_label_width? integer label length if omitted
---@field flash? boolean whether to flash on true rather than stay on
---@field period? PERIOD flash period
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new indicator LED element.
---@nodiscard
---@param args indicator_led_args
---@return LED element, element_id id
return function (args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.colors) == "table", "colors is a required field")
if args.flash then
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
end
args.height = 1
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
local flash_on = true
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = false
-- called by flasher when enabled
local function flash_callback()
e.w_set_cur(1, 1)
if flash_on then
e.w_blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
else
e.w_blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
end
flash_on = not flash_on
end
-- enable light or start flashing
local function enable()
if args.flash then
flash_on = true
flasher.start(flash_callback, args.period)
else
e.w_set_cur(1, 1)
e.w_blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
end
end
-- disable light or stop flashing
local function disable()
if args.flash then
flash_on = false
flasher.stop(flash_callback)
end
e.w_set_cur(1, 1)
e.w_blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
end
-- on state change
---@param new_state boolean indicator state
function e.on_update(new_state)
e.value = new_state
if new_state then enable() else disable() end
end
-- set indicator state
---@param val boolean indicator state
function e.set_value(val) e.on_update(val) end
-- draw label and indicator light
function e.redraw()
e.on_update(e.value)
if string.len(args.label) > 0 then
e.w_set_cur(3, 1)
e.w_write(args.label)
end
end
---@class LED:graphics_element
local LED, id = e.complete(true)
return LED, id
end

View File

@ -0,0 +1,112 @@
-- Indicator LED Pair Graphics Element (two LEDs provide: off, color_a, color_b)
local util = require("scada-common.util")
local element = require("graphics.element")
local flasher = require("graphics.flasher")
---@class indicator_led_pair_args
---@field label string indicator label
---@field off color color for off
---@field c1 color color for #1 on
---@field c2 color color for #2 on
---@field min_label_width? integer label length if omitted
---@field flash? boolean whether to flash when on rather than stay on
---@field period? PERIOD flash period
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new three-state LED indicator light. Two "active" states (colors c1 and c2) and an inactive state (off).<br>
-- Values: 1 = off, 2 = c1, 3 = c2
---@nodiscard
---@param args indicator_led_pair_args
---@return LEDPair element, element_id id
return function (args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.off) == "number", "off is a required field")
element.assert(type(args.c1) == "number", "c1 is a required field")
element.assert(type(args.c2) == "number", "c2 is a required field")
if args.flash then
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
end
args.height = 1
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
local flash_on = true
local co = colors.toBlit(args.off)
local c1 = colors.toBlit(args.c1)
local c2 = colors.toBlit(args.c2)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = 1
-- called by flasher when enabled
local function flash_callback()
e.w_set_cur(1, 1)
if flash_on then
if e.value == 2 then
e.w_blit("\x8c", c1, e.fg_bg.blit_bkg)
elseif e.value == 3 then
e.w_blit("\x8c", c2, e.fg_bg.blit_bkg)
end
else
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
end
flash_on = not flash_on
end
-- on state change
---@param new_state integer indicator state
function e.on_update(new_state)
local was_off = e.value <= 1
e.value = new_state
e.w_set_cur(1, 1)
if args.flash then
if was_off and (new_state > 1) then
flash_on = true
flasher.start(flash_callback, args.period)
elseif new_state <= 1 then
flash_on = false
flasher.stop(flash_callback)
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
end
elseif new_state == 2 then
e.w_blit("\x8c", c1, e.fg_bg.blit_bkg)
elseif new_state == 3 then
e.w_blit("\x8c", c2, e.fg_bg.blit_bkg)
else
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
end
end
-- set indicator state
---@param val integer indicator state
function e.set_value(val) e.on_update(val) end
-- draw label and indicator light
function e.redraw()
e.on_update(e.value)
if string.len(args.label) > 0 then
e.w_set_cur(3, 1)
e.w_write(args.label)
end
end
---@class LEDPair:graphics_element
local LEDPair, id = e.complete(true)
return LEDPair, id
end

View File

@ -0,0 +1,89 @@
-- Power Indicator Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class power_indicator_args
---@field label string indicator label
---@field unit string energy unit
---@field format string power format override (lua string format)
---@field rate boolean? whether to append /t to the end (power per tick)
---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
---@field value number default value
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width integer length
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new power indicator. Variant of a data indicator with dynamic energy units.
---@nodiscard
---@param args power_indicator_args
---@return PowerIndicator element, element_id id
return function (args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.unit) == "string", "unit is a required field")
element.assert(type(args.value) == "number", "value is a required field")
element.assert(util.is_int(args.width), "width is a required field")
args.height = 1
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = args.value
local data_start = 0
-- on state change
---@param value any new value
function e.on_update(value)
e.value = value
local data_str, unit = util.power_format(value, args.unit, false, args.format)
-- write data
e.w_set_cur(data_start, 1)
e.w_set_fgd(e.fg_bg.fgd)
e.w_write(util.comma_format(data_str))
-- write unit
if args.lu_colors ~= nil then
e.w_set_fgd(args.lu_colors.color_b)
end
-- append per tick if rate is set
if args.rate == true then
unit = unit .. "/t"
end
-- add space to unit so we don't end up with something like FEE after having kFE
unit = util.strminw(unit, 5)
e.w_write(" " .. unit)
end
-- set the value
---@param val any new value
function e.set_value(val) e.on_update(val) end
-- element redraw
function e.redraw()
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
e.w_set_cur(1, 1)
e.w_write(args.label)
data_start = string.len(args.label) + 2
if string.len(args.label) == 0 then data_start = 1 end
e.on_update(e.value)
end
---@class PowerIndicator:graphics_element
local PowerIndicator, id = e.complete(true)
return PowerIndicator, id
end

View File

@ -0,0 +1,59 @@
-- Indicator RGB LED Graphics Element
local element = require("graphics.element")
---@class indicator_led_rgb_args
---@field label string indicator label
---@field colors table colors to use
---@field min_label_width? integer label length if omitted
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new RGB LED indicator light element.
---@nodiscard
---@param args indicator_led_rgb_args
---@return RGBLED element, element_id id
return function (args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.colors) == "table", "colors is a required field")
args.height = 1
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = 1
-- on state change
---@param new_state integer indicator state
function e.on_update(new_state)
e.value = new_state
e.w_set_cur(1, 1)
if type(args.colors[new_state]) == "number" then
e.w_blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg)
end
end
-- set indicator state
---@param val integer indicator state
function e.set_value(val) e.on_update(val) end
-- draw label and indicator light
function e.redraw()
e.on_update(e.value)
if string.len(args.label) > 0 then
e.w_set_cur(3, 1)
e.w_write(args.label)
end
end
---@class RGBLED:graphics_element
local RGBLED, id = e.complete(true)
return RGBLED, id
end

View File

@ -0,0 +1,90 @@
-- Radiation Indicator Graphics Element
local types = require("scada-common.types")
local util = require("scada-common.util")
local element = require("graphics.element")
---@class rad_indicator_args
---@field label string indicator label
---@field format string data format (lua string format)
---@field commas? boolean whether to use commas if a number is given (default to false)
---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
---@field value? radiation_reading default value
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width integer length
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new radiation indicator element. Variant of a data indicator using dynamic Sievert unit precision.
---@nodiscard
---@param args rad_indicator_args
---@return RadIndicator element, element_id id
return function (args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.format) == "string", "format is a required field")
element.assert(util.is_int(args.width), "width is a required field")
args.height = 1
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = args.value or types.new_zero_radiation_reading()
local label_len = string.len(args.label)
local data_start = 1
local clear_width = args.width
if label_len > 0 then
data_start = data_start + (label_len + 1)
clear_width = args.width - (label_len + 1)
end
-- on state change
---@param value any new value
function e.on_update(value)
e.value = value.radiation
-- clear old data and label
e.w_set_cur(data_start, 1)
e.w_write(util.spaces(clear_width))
-- write data
local data_str = util.sprintf(args.format, e.value)
e.w_set_cur(data_start, 1)
e.w_set_fgd(e.fg_bg.fgd)
if args.commas then
e.w_write(util.comma_format(data_str))
else
e.w_write(data_str)
end
-- write unit
if args.lu_colors ~= nil then
e.w_set_fgd(args.lu_colors.color_b)
end
e.w_write(" " .. value.unit)
end
-- set the value
---@param val any new value
function e.set_value(val) e.on_update(val) end
-- element redraw
function e.redraw()
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
e.w_set_cur(1, 1)
e.w_write(args.label)
e.on_update(e.value)
end
---@class RadIndicator:graphics_element
local RadIndicator, id = e.complete(true)
return RadIndicator, id
end

View File

@ -0,0 +1,83 @@
-- Signal Bars Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class signal_bar_args
---@field compact? boolean true to use a single character (works better against edges that extend out colors)
---@field colors_low_med? cpair color a for low signal quality, color b for medium signal quality
---@field disconnect_color? color color for the 'x' on disconnect
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors (foreground is used for high signal quality)
---@field hidden? boolean true to hide on initial draw
-- Create a new signal bar indicator element.
---@nodiscard
---@param args signal_bar_args
---@return SignalBar element, element_id id
return function (args)
args.height = 1
args.width = util.trinary(args.compact, 1, 2)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = 0
local blit_bkg = args.fg_bg.blit_bkg
local blit_0, blit_1, blit_2, blit_3 = args.fg_bg.blit_fgd, args.fg_bg.blit_fgd, args.fg_bg.blit_fgd, args.fg_bg.blit_fgd
if type(args.colors_low_med) == "table" then
blit_1 = args.colors_low_med.blit_a or blit_1
blit_2 = args.colors_low_med.blit_b or blit_2
end
if util.is_int(args.disconnect_color) then blit_0 = colors.toBlit(args.disconnect_color) end
-- on state change (0 = offline, 1 through 3 = low to high signal)
---@param new_state integer signal state
function e.on_update(new_state)
e.value = new_state
e.redraw()
end
-- set signal state (0 = offline, 1 through 3 = low to high signal)
---@param val integer signal state
function e.set_value(val) e.on_update(val) end
-- draw label and signal bar
function e.redraw()
e.w_set_cur(1, 1)
if args.compact then
if e.value == 1 then
e.w_blit("\x90", blit_1, blit_bkg)
elseif e.value == 2 then
e.w_blit("\x94", blit_2, blit_bkg)
elseif e.value == 3 then
e.w_blit("\x95", blit_3, blit_bkg)
else
e.w_blit("x", blit_0, blit_bkg)
end
else
if e.value == 1 then
e.w_blit("\x9f ", blit_bkg .. blit_bkg, blit_1 .. blit_bkg)
elseif e.value == 2 then
e.w_blit("\x9f\x94", blit_bkg .. blit_2, blit_2 .. blit_bkg)
elseif e.value == 3 then
e.w_blit("\x9f\x81", blit_bkg .. blit_bkg, blit_3 .. blit_3)
else
e.w_blit(" x", blit_0 .. blit_0, blit_bkg .. blit_bkg)
end
end
end
---@class SignalBar:graphics_element
local SignalBar, id = e.complete(true)
return SignalBar, id
end

View File

@ -0,0 +1,81 @@
-- State (Text) Indicator Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class state_text_color
---@field color cpair
---@field text string
---@class state_indicator_args
---@field states table state color and text table
---@field value? integer default state, defaults to 1
---@field min_width? integer max state text length if omitted
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field height? integer 1 if omitted, must be an odd number
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new state indicator element.
---@nodiscard
---@param args state_indicator_args
---@return StateIndicator element, element_id id
return function (args)
element.assert(type(args.states) == "table", "states is a required field")
if util.is_int(args.height) then
element.assert(args.height % 2 == 1, "height should be an odd number")
else args.height = 1 end
args.width = args.min_width or 1
-- state blit strings
local state_blit_cmds = {}
for i = 1, #args.states do
local state_def = args.states[i] ---@type state_text_color
if string.len(state_def.text) > args.width then
args.width = string.len(state_def.text)
end
local text = util.pad(state_def.text, args.width)
table.insert(state_blit_cmds, {
text = text,
fgd = string.rep(state_def.color.blit_fgd, string.len(text)),
bkg = string.rep(state_def.color.blit_bkg, string.len(text))
})
end
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = args.value or 1
-- element redraw
function e.redraw()
local blit_cmd = state_blit_cmds[e.value]
e.w_set_cur(1, 1)
e.w_blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
end
-- on state change
---@param new_state integer indicator state
function e.on_update(new_state)
e.value = new_state
e.redraw()
end
-- set indicator state
---@param val integer indicator state
function e.set_value(val) e.on_update(val) end
---@class StateIndicator:graphics_element
local StateIndicator, id = e.complete(true)
return StateIndicator, id
end

View File

@ -0,0 +1,109 @@
-- Tri-State Indicator Light Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
local flasher = require("graphics.flasher")
---@class tristate_indicator_light_args
---@field label string indicator label
---@field c1 color color for state 1
---@field c2 color color for state 2
---@field c3 color color for state 3
---@field min_label_width? integer label length if omitted
---@field flash? boolean whether to flash on state 2 or 3 rather than stay on
---@field period? PERIOD flash period
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new tri-state indicator light element.
---@nodiscard
---@param args tristate_indicator_light_args
---@return TriIndicatorLight element, element_id id
return function (args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.c1) == "number", "c1 is a required field")
element.assert(type(args.c2) == "number", "c2 is a required field")
element.assert(type(args.c3) == "number", "c3 is a required field")
if args.flash then
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
end
args.height = 1
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = 1
local flash_on = true
local c1 = colors.toBlit(args.c1)
local c2 = colors.toBlit(args.c2)
local c3 = colors.toBlit(args.c3)
-- called by flasher when enabled
local function flash_callback()
e.w_set_cur(1, 1)
if flash_on then
if e.value == 2 then
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
elseif e.value == 3 then
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
end
else
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end
flash_on = not flash_on
end
-- on state change
---@param new_state integer indicator state
function e.on_update(new_state)
local was_off = e.value <= 1
e.value = new_state
e.w_set_cur(1, 1)
if args.flash then
if was_off and (new_state > 1) then
flash_on = true
flasher.start(flash_callback, args.period)
elseif new_state <= 1 then
flash_on = false
flasher.stop(flash_callback)
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end
elseif new_state == 2 then
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
elseif new_state == 3 then
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
else
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end
end
-- set indicator state
---@param val integer indicator state
function e.set_value(val) e.on_update(val) end
-- draw light and label
function e.redraw()
e.on_update(1)
e.w_write(args.label)
end
---@class TriIndicatorLight:graphics_element
local TriIndicatorLight, id = e.complete(true)
return TriIndicatorLight, id
end

View File

@ -0,0 +1,105 @@
-- Vertical Bar Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class vbar_args
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new vertical fill bar indicator element.
---@nodiscard
---@param args vbar_args
---@return VerticalBar element, element_id id
return function (args)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
e.value = 0.0
local last_num_bars = -1
local fgd = string.rep(e.fg_bg.blit_fgd, e.frame.w)
local bkg = string.rep(e.fg_bg.blit_bkg, e.frame.w)
local spaces = util.spaces(e.frame.w)
local one_third = string.rep("\x8f", e.frame.w)
local two_thirds = string.rep("\x83", e.frame.w)
-- handle data changes
---@param fraction number 0.0 to 1.0
function e.on_update(fraction)
e.value = fraction
-- enforce minimum and maximum
if fraction < 0 then
fraction = 0.0
elseif fraction > 1 then
fraction = 1.0
end
-- compute number of bars
local num_bars = util.round(fraction * (e.frame.h * 3))
-- redraw only if number of bars has changed
if num_bars ~= last_num_bars then
last_num_bars = num_bars
local y = e.frame.h
e.w_set_cur(1, y)
-- fill percentage
for _ = 1, num_bars / 3 do
e.w_blit(spaces, bkg, fgd)
y = y - 1
e.w_set_cur(1, y)
end
-- add fractional bar if needed
if num_bars % 3 == 1 then
e.w_blit(one_third, bkg, fgd)
y = y - 1
elseif num_bars % 3 == 2 then
e.w_blit(two_thirds, bkg, fgd)
y = y - 1
end
-- fill the rest blank
while y > 0 do
e.w_set_cur(1, y)
e.w_blit(spaces, fgd, bkg)
y = y - 1
end
end
end
-- set the percentage value
---@param val number 0.0 to 1.0
function e.set_value(val) e.on_update(val) end
-- element redraw
function e.redraw()
last_num_bars = -1
e.on_update(e.value)
end
-- change bar color
---@param fg_bg cpair new bar colors
function e.recolor(fg_bg)
fgd = string.rep(fg_bg.blit_fgd, e.frame.w)
bkg = string.rep(fg_bg.blit_bkg, e.frame.w)
e.redraw()
end
---@class VerticalBar:graphics_element
local VerticalBar, id = e.complete(true)
return VerticalBar, id
end

254
graphics/events.lua Normal file
View File

@ -0,0 +1,254 @@
--
-- Graphics Events and Event Handlers
--
local util = require("scada-common.util")
local DOUBLE_CLICK_MS = 500
local events = {}
---@enum CLICK_BUTTON
local CLICK_BUTTON = {
GENERIC = 0,
LEFT_BUTTON = 1,
RIGHT_BUTTON = 2,
MID_BUTTON = 3
}
events.CLICK_BUTTON = CLICK_BUTTON
---@enum MOUSE_CLICK
local MOUSE_CLICK = {
TAP = 1, -- screen tap (complete click)
DOWN = 2, -- button down
UP = 3, -- button up (completed a click)
DRAG = 4, -- mouse dragged
SCROLL_DOWN = 5, -- scroll down
SCROLL_UP = 6, -- scroll up
DOUBLE_CLICK = 7 -- double left click
}
events.MOUSE_CLICK = MOUSE_CLICK
---@enum KEY_CLICK
local KEY_CLICK = {
DOWN = 1,
HELD = 2,
UP = 3,
CHAR = 4
}
events.KEY_CLICK = KEY_CLICK
-- create a new 2D coordinate
---@param x integer
---@param y integer
---@return coordinate_2d
local function _coord2d(x, y) return { x = x, y = y } end
events.new_coord_2d = _coord2d
---@class mouse_interaction
---@field monitor string
---@field button CLICK_BUTTON
---@field type MOUSE_CLICK
---@field initial coordinate_2d
---@field current coordinate_2d
---@class key_interaction
---@field type KEY_CLICK
---@field key number key code
---@field name string key character name
---@field shift boolean shift held
---@field ctrl boolean ctrl held
---@field alt boolean alt held
local handler = {
-- left, right, middle button down tracking
button_down = { _coord2d(0, 0), _coord2d(0, 0), _coord2d(0, 0) },
-- keyboard modifiers
shift = false,
alt = false,
ctrl = false,
-- double click tracking
dc_start = 0,
dc_step = 1,
dc_coord = _coord2d(0, 0)
}
-- create a new monitor touch mouse interaction event
---@nodiscard
---@param monitor string
---@param x integer
---@param y integer
---@return mouse_interaction
local function _monitor_touch(monitor, x, y)
return {
monitor = monitor,
button = CLICK_BUTTON.GENERIC,
type = MOUSE_CLICK.TAP,
initial = _coord2d(x, y),
current = _coord2d(x, y)
}
end
-- create a new mouse button mouse interaction event
---@nodiscard
---@param button CLICK_BUTTON mouse button
---@param type MOUSE_CLICK click type
---@param x1 integer initial x
---@param y1 integer initial y
---@param x2 integer current x
---@param y2 integer current y
---@return mouse_interaction
local function _mouse_event(button, type, x1, y1, x2, y2)
return {
monitor = "terminal",
button = button,
type = type,
initial = _coord2d(x1, y1),
current = _coord2d(x2, y2)
}
end
-- create a new generic mouse interaction event
---@nodiscard
---@param type MOUSE_CLICK
---@param x integer
---@param y integer
---@return mouse_interaction
function events.mouse_generic(type, x, y)
return {
monitor = "",
button = CLICK_BUTTON.GENERIC,
type = type,
initial = _coord2d(x, y),
current = _coord2d(x, y)
}
end
-- create a new transposed mouse interaction event using the event's monitor/button fields
---@nodiscard
---@param event mouse_interaction
---@param elem_pos_x integer element's x position: new x = (event x - element x) + 1
---@param elem_pos_y integer element's y position: new y = (event y - element y) + 1
---@return mouse_interaction
function events.mouse_transposed(event, elem_pos_x, elem_pos_y)
return {
monitor = event.monitor,
button = event.button,
type = event.type,
initial = _coord2d((event.initial.x - elem_pos_x) + 1, (event.initial.y - elem_pos_y) + 1),
current = _coord2d((event.current.x - elem_pos_x) + 1, (event.current.y - elem_pos_y) + 1)
}
end
-- check if an event qualifies as a click (tap or up)
---@nodiscard
---@param t MOUSE_CLICK
function events.was_clicked(t) return t == MOUSE_CLICK.TAP or t == MOUSE_CLICK.UP end
-- create a new mouse event to pass onto graphics renderer<br>
-- supports: mouse_click, mouse_up, mouse_drag, mouse_scroll, and monitor_touch
---@param event_type os_event OS event to handle
---@param opt integer|string button, scroll direction, or monitor for monitor touch
---@param x integer x coordinate
---@param y integer y coordinate
---@return mouse_interaction|nil
function events.new_mouse_event(event_type, opt, x, y)
local h = handler
if event_type == "mouse_click" then
---@cast opt 1|2|3
local init = true
if opt == 1 and (h.dc_step % 2) == 1 then
if h.dc_step ~= 1 and h.dc_coord.x == x and h.dc_coord.y == y and (util.time_ms() - h.dc_start) < DOUBLE_CLICK_MS then
init = false
h.dc_step = h.dc_step + 1
end
end
if init then
h.dc_start = util.time_ms()
h.dc_coord = _coord2d(x, y)
h.dc_step = 2
end
h.button_down[opt] = _coord2d(x, y)
return _mouse_event(opt, MOUSE_CLICK.DOWN, x, y, x, y)
elseif event_type == "mouse_up" then
---@cast opt 1|2|3
if opt == 1 and (h.dc_step % 2) == 0 and h.dc_coord.x == x and h.dc_coord.y == y and
(util.time_ms() - h.dc_start) < DOUBLE_CLICK_MS then
if h.dc_step == 4 then
util.push_event("double_click", 1, x, y)
h.dc_step = 1
else h.dc_step = h.dc_step + 1 end
else h.dc_step = 1 end
local initial = h.button_down[opt] ---@type coordinate_2d
return _mouse_event(opt, MOUSE_CLICK.UP, initial.x, initial.y, x, y)
elseif event_type == "monitor_touch" then
---@cast opt string
return _monitor_touch(opt, x, y)
elseif event_type == "mouse_drag" then
---@cast opt 1|2|3
local initial = h.button_down[opt] ---@type coordinate_2d
return _mouse_event(opt, MOUSE_CLICK.DRAG, initial.x, initial.y, x, y)
elseif event_type == "mouse_scroll" then
---@cast opt 1|-1
local scroll_direction = util.trinary(opt == 1, MOUSE_CLICK.SCROLL_DOWN, MOUSE_CLICK.SCROLL_UP)
return _mouse_event(CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y)
elseif event_type == "double_click" then
return _mouse_event(CLICK_BUTTON.LEFT_BUTTON, MOUSE_CLICK.DOUBLE_CLICK, x, y, x, y)
end
end
-- create a new keyboard interaction event
---@nodiscard
---@param click_type KEY_CLICK key click type
---@param key integer|string keyboard key code or character for 'char' event
---@return key_interaction
local function _key_event(click_type, key)
local name = key
if type(key) == "number" then name = keys.getName(key) end
return { type = click_type, key = key, name = name, shift = handler.shift, ctrl = handler.ctrl, alt = handler.alt }
end
-- create a new keyboard event to pass onto graphics renderer<br>
-- supports: char, key, and key_up
---@param event_type os_event OS event to handle
---@param key integer keyboard key code
---@param held boolean? if the key is being held (for 'key' event)
---@return key_interaction|nil
function events.new_key_event(event_type, key, held)
if event_type == "char" then
return _key_event(KEY_CLICK.CHAR, key)
elseif event_type == "key" then
if key == keys.leftShift or key == keys.rightShift then
handler.shift = true
elseif key == keys.leftCtrl or key == keys.rightCtrl then
handler.ctrl = true
elseif key == keys.leftAlt or key == keys.rightAlt then
handler.alt = true
else
return _key_event(util.trinary(held, KEY_CLICK.HELD, KEY_CLICK.DOWN), key)
end
elseif event_type == "key_up" then
if key == keys.leftShift or key == keys.rightShift then
handler.shift = false
elseif key == keys.leftCtrl or key == keys.rightCtrl then
handler.ctrl = false
elseif key == keys.leftAlt or key == keys.rightAlt then
handler.alt = false
else
return _key_event(KEY_CLICK.UP, key)
end
end
end
return events

82
graphics/flasher.lua Normal file
View File

@ -0,0 +1,82 @@
--
-- Indicator Light Flasher
--
local tcd = require("scada-common.tcd")
local flasher = {}
-- note: no additional call needs to be made in a main loop as this class automatically uses the TCD to operate
---@alias PERIOD integer
local PERIOD = {
BLINK_250_MS = 1,
BLINK_500_MS = 2,
BLINK_1000_MS = 3
}
flasher.PERIOD = PERIOD
local active = false
local registry = { {}, {}, {} } ---@type [ function[], function[], function [] ] one registry table per period
local callback_counter = 0
-- blink registered indicators<br>
-- this assumes it is called every 250ms, it does no checking of time on its own
local function callback_250ms()
if active then
for _, f in ipairs(registry[PERIOD.BLINK_250_MS]) do f() end
if callback_counter % 2 == 0 then
for _, f in ipairs(registry[PERIOD.BLINK_500_MS]) do f() end
end
if callback_counter % 4 == 0 then
for _, f in ipairs(registry[PERIOD.BLINK_1000_MS]) do f() end
end
callback_counter = callback_counter + 1
tcd.dispatch_unique(0.25, callback_250ms)
end
end
-- start/resume the flasher periodic
function flasher.run()
if not active then
active = true
callback_250ms()
end
end
-- clear all blinking indicators and stop the flasher periodic
function flasher.clear()
active = false
callback_counter = 0
registry = { {}, {}, {} }
end
-- register a function to be called on the selected blink period<br>
-- times are not strictly enforced, but all with a given period will be set at the same time
---@param f function function to call each period
---@param period PERIOD time period option (1, 2, or 3)
function flasher.start(f, period)
if type(registry[period]) == "table" then
table.insert(registry[period], f)
end
end
-- stop a function from being called at the blink period
---@param f function function callback registered
function flasher.stop(f)
for i = 1, #registry do
for key, val in ipairs(registry[i]) do
if val == f then
table.remove(registry[i], key)
return
end
end
end
end
return flasher

418
graphics/themes.lua Normal file
View File

@ -0,0 +1,418 @@
--
-- Graphics Themes
--
local core = require("graphics.core")
local cpair = core.cpair
---@class graphics_themes
local themes = {}
-- add color mappings for front panels
colors.ivory = colors.pink
colors.green_hc = colors.cyan
colors.yellow_hc = colors.purple
colors.red_off = colors.brown
colors.yellow_off = colors.magenta
colors.green_off = colors.lime
--#region Types
---@enum UI_THEME
themes.UI_THEME = { SMOOTH_STONE = 1, DEEPSLATE = 2 }
themes.UI_THEME_NAMES = { "Smooth Stone", "Deepslate" }
-- attempts to get the string name of a main ui theme
---@nodiscard
---@param id any
---@return string|nil
function themes.ui_theme_name(id)
if id == themes.UI_THEME.SMOOTH_STONE or
id == themes.UI_THEME.DEEPSLATE then
return themes.UI_THEME_NAMES[id]
else return nil end
end
---@enum FP_THEME
themes.FP_THEME = { SANDSTONE = 1, BASALT = 2 }
themes.FP_THEME_NAMES = { "Sandstone", "Basalt" }
-- attempts to get the string name of a front panel theme
---@nodiscard
---@param id any
---@return string|nil
function themes.fp_theme_name(id)
if id == themes.FP_THEME.SANDSTONE or
id == themes.FP_THEME.BASALT then
return themes.FP_THEME_NAMES[id]
else return nil end
end
---@enum COLOR_MODE
themes.COLOR_MODE = {
STANDARD = 1,
DEUTERANOPIA = 2,
PROTANOPIA = 3,
TRITANOPIA = 4,
BLUE_IND = 5,
STD_ON_BLACK = 6,
BLUE_ON_BLACK = 7,
NUM_MODES = 8
}
themes.COLOR_MODE_NAMES = {
"Standard",
"Deuteranopia",
"Protanopia",
"Tritanopia",
"Blue for 'Good'",
"Standard + Black",
"Blue + Black"
}
-- attempts to get the string name of a color mode
---@nodiscard
---@param id any
---@return string|nil
function themes.color_mode_name(id)
if id == themes.COLOR_MODE.STANDARD or
id == themes.COLOR_MODE.DEUTERANOPIA or
id == themes.COLOR_MODE.PROTANOPIA or
id == themes.COLOR_MODE.TRITANOPIA or
id == themes.COLOR_MODE.BLUE_IND or
id == themes.COLOR_MODE.STD_ON_BLACK or
id == themes.COLOR_MODE.BLUE_ON_BLACK then
return themes.COLOR_MODE_NAMES[id]
else return nil end
end
--#endregion
--#region Front Panel Themes
---@class fp_theme
themes.sandstone = {
text = colors.black,
label = colors.lightGray,
label_dark = colors.gray,
disabled = colors.lightGray,
bg = colors.ivory,
header = cpair(colors.black, colors.lightGray),
highlight_box = cpair(colors.black, colors.lightGray),
highlight_box_bright = cpair(colors.black, colors.white),
field_box = cpair(colors.gray, colors.white),
colors = {
{ c = colors.red, hex = 0xdf4949 },
{ c = colors.orange, hex = 0xffb659 },
{ c = colors.yellow, hex = 0xf9fb53 },
{ c = colors.green_off, hex = 0x16665a },
{ c = colors.green, hex = 0x6be551 },
{ c = colors.green_hc, hex = 0x6be551 },
{ c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff },
{ c = colors.yellow_hc, hex = 0xe3bc2a },
{ c = colors.ivory, hex = 0xdcd9ca },
{ c = colors.yellow_off, hex = 0x85862c },
{ c = colors.white, hex = 0xf0f0f0 },
{ c = colors.lightGray, hex = 0xb1b8b3 },
{ c = colors.gray, hex = 0x575757 },
{ c = colors.black, hex = 0x191919 },
{ c = colors.red_off, hex = 0x672223 }
},
-- color re-mappings for assistive modes
color_modes = {
-- standard
{},
-- deuteranopia
{
{ c = colors.green, hex = 0x1081ff },
{ c = colors.green_hc, hex = 0x1081ff },
{ c = colors.green_off, hex = 0x141414 },
{ c = colors.yellow, hex = 0xf7c311 },
{ c = colors.yellow_off, hex = 0x141414 },
{ c = colors.red, hex = 0xfb5615 },
{ c = colors.red_off, hex = 0x141414 }
},
-- protanopia
{
{ c = colors.green, hex = 0x1081ff },
{ c = colors.green_hc, hex = 0x1081ff },
{ c = colors.green_off, hex = 0x141414 },
{ c = colors.yellow, hex = 0xf5e633 },
{ c = colors.yellow_off, hex = 0x141414 },
{ c = colors.red, hex = 0xff521a },
{ c = colors.red_off, hex = 0x141414 }
},
-- tritanopia
{
{ c = colors.green, hex = 0x40cbd7 },
{ c = colors.green_hc, hex = 0x40cbd7 },
{ c = colors.green_off, hex = 0x141414 },
{ c = colors.yellow, hex = 0xffbc00 },
{ c = colors.yellow_off, hex = 0x141414 },
{ c = colors.red, hex = 0xff0000 },
{ c = colors.red_off, hex = 0x141414 }
},
-- blue indicators
{
{ c = colors.green, hex = 0x1081ff },
{ c = colors.green_hc, hex = 0x1081ff },
{ c = colors.green_off, hex = 0x053466 },
},
-- standard, black backgrounds
{
{ c = colors.green_off, hex = 0x141414 },
{ c = colors.yellow_off, hex = 0x141414 },
{ c = colors.red_off, hex = 0x141414 }
},
-- blue indicators, black backgrounds
{
{ c = colors.green, hex = 0x1081ff },
{ c = colors.green_hc, hex = 0x1081ff },
{ c = colors.green_off, hex = 0x141414 },
{ c = colors.yellow_off, hex = 0x141414 },
{ c = colors.red_off, hex = 0x141414 }
}
}
}
---@type fp_theme
themes.basalt = {
text = colors.white,
label = colors.gray,
label_dark = colors.ivory,
disabled = colors.lightGray,
bg = colors.ivory,
header = cpair(colors.white, colors.gray),
highlight_box = cpair(colors.white, colors.gray),
highlight_box_bright = cpair(colors.black, colors.lightGray),
field_box = cpair(colors.white, colors.gray),
colors = {
{ c = colors.red, hex = 0xf18486 },
{ c = colors.orange, hex = 0xffb659 },
{ c = colors.yellow, hex = 0xefe37c },
{ c = colors.green_off, hex = 0x436b41 },
{ c = colors.green, hex = 0x7ae175 },
{ c = colors.green_hc, hex = 0x7ae175 },
{ c = colors.lightBlue, hex = 0x7dc6f2 },
{ c = colors.blue, hex = 0x56aae6 },
{ c = colors.yellow_hc, hex = 0xe9cd68 },
{ c = colors.ivory, hex = 0x4d4e52 },
{ c = colors.yellow_off, hex = 0x757040 },
{ c = colors.white, hex = 0xbfbfbf },
{ c = colors.lightGray, hex = 0x848794 },
{ c = colors.gray, hex = 0x5c5f68 },
{ c = colors.black, hex = 0x333333 },
{ c = colors.red_off, hex = 0x512d2d }
},
color_modes = {
-- standard
{},
-- deuteranopia
{
{ c = colors.green, hex = 0x65aeff },
{ c = colors.green_hc, hex = 0x99c9ff },
{ c = colors.green_off, hex = 0x333333 },
{ c = colors.yellow, hex = 0xf7c311 },
{ c = colors.yellow_off, hex = 0x333333 },
{ c = colors.red, hex = 0xf18486 },
{ c = colors.red_off, hex = 0x333333 }
},
-- protanopia
{
{ c = colors.green, hex = 0x65aeff },
{ c = colors.green_hc, hex = 0x99c9ff },
{ c = colors.green_off, hex = 0x333333 },
{ c = colors.yellow, hex = 0xf5e633 },
{ c = colors.yellow_off, hex = 0x333333 },
{ c = colors.red, hex = 0xff8058 },
{ c = colors.red_off, hex = 0x333333 }
},
-- tritanopia
{
{ c = colors.green, hex = 0x00ecff },
{ c = colors.green_hc, hex = 0x00ecff },
{ c = colors.green_off, hex = 0x333333 },
{ c = colors.yellow, hex = 0xffbc00 },
{ c = colors.yellow_off, hex = 0x333333 },
{ c = colors.red, hex = 0xdf4949 },
{ c = colors.red_off, hex = 0x333333 }
},
-- blue indicators
{
{ c = colors.green, hex = 0x65aeff },
{ c = colors.green_hc, hex = 0x99c9ff },
{ c = colors.green_off, hex = 0x365e8a },
},
-- standard, black backgrounds
{
{ c = colors.green_off, hex = 0x333333 },
{ c = colors.yellow_off, hex = 0x333333 },
{ c = colors.red_off, hex = 0x333333 }
},
-- blue indicators, black backgrounds
{
{ c = colors.green, hex = 0x65aeff },
{ c = colors.green_hc, hex = 0x99c9ff },
{ c = colors.green_off, hex = 0x333333 },
{ c = colors.yellow_off, hex = 0x333333 },
{ c = colors.red_off, hex = 0x333333 }
}
}
}
-- get style fields for a front panel based on the provided theme
---@param theme fp_theme
function themes.get_fp_style(theme)
---@class fp_style
local style = {
root = cpair(theme.text, theme.bg),
text = cpair(theme.text, theme.bg),
text_fg = cpair(theme.text, colors._INHERIT),
label_fg = cpair(theme.label, colors._INHERIT),
label_d_fg = cpair(theme.label_dark, colors._INHERIT),
disabled_fg = cpair(theme.disabled, colors._INHERIT)
}
return style
end
--#endregion
--#region Main UI Color Palettes
---@class ui_palette
themes.smooth_stone = {
colors = {
{ c = colors.red, hex = 0xdf4949 },
{ c = colors.orange, hex = 0xffb659 },
{ c = colors.yellow, hex = 0xfffc79 },
{ c = colors.lime, hex = 0x80ff80 },
{ c = colors.green, hex = 0x4aee8a },
{ c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff },
{ c = colors.purple, hex = 0xb156ee },
{ c = colors.pink, hex = 0xf26ba2 },
{ c = colors.magenta, hex = 0xf9488a },
{ c = colors.white, hex = 0xf0f0f0 },
{ c = colors.lightGray, hex = 0xcacaca },
{ c = colors.gray, hex = 0x575757 },
{ c = colors.black, hex = 0x191919 },
{ c = colors.brown, hex = 0x7f664c }
},
-- color re-mappings for assistive modes
color_modes = {
-- standard
{},
-- deuteranopia
{
{ c = colors.blue, hex = 0x1081ff },
{ c = colors.yellow, hex = 0xf7c311 },
{ c = colors.red, hex = 0xfb5615 }
},
-- protanopia
{
{ c = colors.blue, hex = 0x1081ff },
{ c = colors.yellow, hex = 0xf5e633 },
{ c = colors.red, hex = 0xff521a }
},
-- tritanopia
{
{ c = colors.blue, hex = 0x40cbd7 },
{ c = colors.yellow, hex = 0xffbc00 },
{ c = colors.red, hex = 0xff0000 }
},
-- blue indicators
{
{ c = colors.blue, hex = 0x1081ff },
{ c = colors.yellow, hex = 0xfffc79 },
{ c = colors.red, hex = 0xdf4949 }
},
-- standard, black backgrounds
{},
-- blue indicators, black backgrounds
{
{ c = colors.blue, hex = 0x1081ff },
{ c = colors.yellow, hex = 0xfffc79 },
{ c = colors.red, hex = 0xdf4949 }
}
}
}
---@type ui_palette
themes.deepslate = {
colors = {
{ c = colors.red, hex = 0xeb6a6c },
{ c = colors.orange, hex = 0xf2b86c },
{ c = colors.yellow, hex = 0xd9cf81 },
{ c = colors.lime, hex = 0x80ff80 },
{ c = colors.green, hex = 0x70e19b },
{ c = colors.cyan, hex = 0x7ccdd0 },
{ c = colors.lightBlue, hex = 0x99ceef },
{ c = colors.blue, hex = 0x60bcff },
{ c = colors.purple, hex = 0xc38aea },
{ c = colors.pink, hex = 0xff7fb8 },
{ c = colors.magenta, hex = 0xf980dd },
{ c = colors.white, hex = 0xd9d9d9 },
{ c = colors.lightGray, hex = 0x949494 },
{ c = colors.gray, hex = 0x575757 },
{ c = colors.black, hex = 0x262626 },
{ c = colors.brown, hex = 0xb18f6a }
},
-- color re-mappings for assistive modes
color_modes = {
-- standard
{},
-- deuteranopia
{
{ c = colors.blue, hex = 0x65aeff },
{ c = colors.yellow, hex = 0xf7c311 },
{ c = colors.red, hex = 0xfb5615 }
},
-- protanopia
{
{ c = colors.blue, hex = 0x65aeff },
{ c = colors.yellow, hex = 0xf5e633 },
{ c = colors.red, hex = 0xff8058 }
},
-- tritanopia
{
{ c = colors.blue, hex = 0x00ecff },
{ c = colors.yellow, hex = 0xffbc00 },
{ c = colors.red, hex = 0xdf4949 }
},
-- blue indicators
{
{ c = colors.blue, hex = 0x65aeff },
{ c = colors.yellow, hex = 0xd9cf81 },
{ c = colors.red, hex = 0xeb6a6c }
},
-- standard, black backgrounds
{},
-- blue indicators, black backgrounds
{
{ c = colors.blue, hex = 0x65aeff },
{ c = colors.yellow, hex = 0xd9cf81 },
{ c = colors.red, hex = 0xeb6a6c }
}
}
}
--#endregion
return themes

View File

@ -1,18 +1,8 @@
--
-- Initialize the Post-Boot Module Environment
--
return {
-- initialize booted environment
local init_env = function ()
local _require = require("cc.require")
local _env = setmetatable({}, { __index = _ENV })
-- overwrite require/package globals
init_env = function ()
local _require, _env = require("cc.require"), setmetatable({}, { __index = _ENV })
require, package = _require.make(_env, "/")
-- reset terminal
term.clear()
term.setCursorPos(1, 1)
term.clear(); term.setCursorPos(1, 1)
end
return { init_env = init_env }
}

22
lockbox/LICENSE Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 James L.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

199
lockbox/digest/md5.lua Normal file
View File

@ -0,0 +1,199 @@
local Bit = require("lockbox.util.bit");
local String = require("string");
local Math = require("math");
local Queue = require("lockbox.util.queue");
local SHIFT = {
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21};
local CONSTANTS = {
0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391};
local AND = Bit.band;
local OR = Bit.bor;
local NOT = Bit.bnot;
local XOR = Bit.bxor;
local LROT = Bit.lrotate;
local LSHIFT = Bit.lshift;
local RSHIFT = Bit.rshift;
--MD5 is little-endian
local bytes2word = function(b0, b1, b2, b3)
local i = b3; i = LSHIFT(i, 8);
i = OR(i, b2); i = LSHIFT(i, 8);
i = OR(i, b1); i = LSHIFT(i, 8);
i = OR(i, b0);
return i;
end
local word2bytes = function(word)
local b0, b1, b2, b3;
b0 = AND(word, 0xFF); word = RSHIFT(word, 8);
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
b3 = AND(word, 0xFF);
return b0, b1, b2, b3;
end
local dword2bytes = function(i)
local b4, b5, b6, b7 = word2bytes(Math.floor(i / 0x100000000));
local b0, b1, b2, b3 = word2bytes(i);
return b0, b1, b2, b3, b4, b5, b6, b7;
end
local F = function(x, y, z) return OR(AND(x, y), AND(NOT(x), z)); end
local G = function(x, y, z) return OR(AND(x, z), AND(y, NOT(z))); end
local H = function(x, y, z) return XOR(x, XOR(y, z)); end
local I = function(x, y, z) return XOR(y, OR(x, NOT(z))); end
local MD5 = function()
local queue = Queue();
local A = 0x67452301;
local B = 0xefcdab89;
local C = 0x98badcfe;
local D = 0x10325476;
local public = {};
local processBlock = function()
local a = A;
local b = B;
local c = C;
local d = D;
local X = {};
for i = 1, 16 do
X[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
end
for i = 0, 63 do
local f, g, temp;
if (0 <= i) and (i <= 15) then
f = F(b, c, d);
g = i;
elseif (16 <= i) and (i <= 31) then
f = G(b, c, d);
g = (5 * i + 1) % 16;
elseif (32 <= i) and (i <= 47) then
f = H(b, c, d);
g = (3 * i + 5) % 16;
elseif (48 <= i) and (i <= 63) then
f = I(b, c, d);
g = (7 * i) % 16;
end
temp = d;
d = c;
c = b;
b = b + LROT((a + f + CONSTANTS[i + 1] + X[g + 1]), SHIFT[i + 1]);
a = temp;
end
A = AND(A + a, 0xFFFFFFFF);
B = AND(B + b, 0xFFFFFFFF);
C = AND(C + c, 0xFFFFFFFF);
D = AND(D + d, 0xFFFFFFFF);
end
public.init = function()
queue.reset();
A = 0x67452301;
B = 0xefcdab89;
C = 0x98badcfe;
D = 0x10325476;
return public;
end
public.update = function(bytes)
for b in bytes do
queue.push(b);
if(queue.size() >= 64) then processBlock(); end
end
return public;
end
public.finish = function()
local bits = queue.getHead() * 8;
queue.push(0x80);
while ((queue.size() + 7) % 64) < 63 do
queue.push(0x00);
end
local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits);
queue.push(b0);
queue.push(b1);
queue.push(b2);
queue.push(b3);
queue.push(b4);
queue.push(b5);
queue.push(b6);
queue.push(b7);
while queue.size() > 0 do
processBlock();
end
return public;
end
public.asBytes = function()
local b0, b1, b2, b3 = word2bytes(A);
local b4, b5, b6, b7 = word2bytes(B);
local b8, b9, b10, b11 = word2bytes(C);
local b12, b13, b14, b15 = word2bytes(D);
return {b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15};
end
public.asHex = function()
local b0, b1, b2, b3 = word2bytes(A);
local b4, b5, b6, b7 = word2bytes(B);
local b8, b9, b10, b11 = word2bytes(C);
local b12, b13, b14, b15 = word2bytes(D);
return String.format("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15);
end
public.asString = function()
local b0, b1, b2, b3 = word2bytes(A);
local b4, b5, b6, b7 = word2bytes(B);
local b8, b9, b10, b11 = word2bytes(C);
local b12, b13, b14, b15 = word2bytes(D);
return string.pack(string.rep('B', 16),
b0, b1, b2, b3, b4, b5, b6, b7, b8,
b9, b10, b11, b12, b13, b14, b15
)
end
return public;
end
return MD5;

171
lockbox/digest/sha1.lua Normal file
View File

@ -0,0 +1,171 @@
local Bit = require("lockbox.util.bit");
local String = require("string");
local Math = require("math");
local Queue = require("lockbox.util.queue");
local AND = Bit.band;
local OR = Bit.bor;
local XOR = Bit.bxor;
local LROT = Bit.lrotate;
local LSHIFT = Bit.lshift;
local RSHIFT = Bit.rshift;
--SHA1 is big-endian
local bytes2word = function(b0, b1, b2, b3)
local i = b0; i = LSHIFT(i, 8);
i = OR(i, b1); i = LSHIFT(i, 8);
i = OR(i, b2); i = LSHIFT(i, 8);
i = OR(i, b3);
return i;
end
local word2bytes = function(word)
local b0, b1, b2, b3;
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
b0 = AND(word, 0xFF);
return b0, b1, b2, b3;
end
local dword2bytes = function(i)
local b4, b5, b6, b7 = word2bytes(i);
local b0, b1, b2, b3 = word2bytes(Math.floor(i / 0x100000000));
return b0, b1, b2, b3, b4, b5, b6, b7;
end
local F = function(x, y, z) return XOR(z, AND(x, XOR(y, z))); end
local G = function(x, y, z) return XOR(x, XOR(y, z)); end
local H = function(x, y, z) return OR(AND(x, OR(y, z)), AND(y, z)); end
local I = function(x, y, z) return XOR(x, XOR(y, z)); end
local SHA1 = function()
local queue = Queue();
local h0 = 0x67452301;
local h1 = 0xEFCDAB89;
local h2 = 0x98BADCFE;
local h3 = 0x10325476;
local h4 = 0xC3D2E1F0;
local public = {};
local processBlock = function()
local a = h0;
local b = h1;
local c = h2;
local d = h3;
local e = h4;
local temp;
local k;
local w = {};
for i = 0, 15 do
w[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
end
for i = 16, 79 do
w[i] = LROT((XOR(XOR(w[i - 3], w[i - 8]), XOR(w[i - 14], w[i - 16]))), 1);
end
for i = 0, 79 do
if (i <= 19) then
temp = F(b, c, d);
k = 0x5A827999;
elseif (i <= 39) then
temp = G(b, c, d);
k = 0x6ED9EBA1;
elseif (i <= 59) then
temp = H(b, c, d);
k = 0x8F1BBCDC;
else
temp = I(b, c, d);
k = 0xCA62C1D6;
end
temp = LROT(a, 5) + temp + e + k + w[i];
e = d;
d = c;
c = LROT(b, 30);
b = a;
a = temp;
end
h0 = AND(h0 + a, 0xFFFFFFFF);
h1 = AND(h1 + b, 0xFFFFFFFF);
h2 = AND(h2 + c, 0xFFFFFFFF);
h3 = AND(h3 + d, 0xFFFFFFFF);
h4 = AND(h4 + e, 0xFFFFFFFF);
end
public.init = function()
queue.reset();
h0 = 0x67452301;
h1 = 0xEFCDAB89;
h2 = 0x98BADCFE;
h3 = 0x10325476;
h4 = 0xC3D2E1F0;
return public;
end
public.update = function(bytes)
for b in bytes do
queue.push(b);
if queue.size() >= 64 then processBlock(); end
end
return public;
end
public.finish = function()
local bits = queue.getHead() * 8;
queue.push(0x80);
while ((queue.size() + 7) % 64) < 63 do
queue.push(0x00);
end
local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits);
queue.push(b0);
queue.push(b1);
queue.push(b2);
queue.push(b3);
queue.push(b4);
queue.push(b5);
queue.push(b6);
queue.push(b7);
while queue.size() > 0 do
processBlock();
end
return public;
end
public.asBytes = function()
local b0, b1, b2, b3 = word2bytes(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
return {b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19};
end
public.asHex = function()
local b0, b1, b2, b3 = word2bytes(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
return String.format("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19);
end
return public;
end
return SHA1;

200
lockbox/digest/sha2_224.lua Normal file
View File

@ -0,0 +1,200 @@
local Bit = require("lockbox.util.bit");
local String = require("string");
local Math = require("math");
local Queue = require("lockbox.util.queue");
local CONSTANTS = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 };
local fmt = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" ..
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
local AND = Bit.band;
local OR = Bit.bor;
local NOT = Bit.bnot;
local XOR = Bit.bxor;
local RROT = Bit.rrotate;
local LSHIFT = Bit.lshift;
local RSHIFT = Bit.rshift;
--SHA2 is big-endian
local bytes2word = function(b0, b1, b2, b3)
local i = b0; i = LSHIFT(i, 8);
i = OR(i, b1); i = LSHIFT(i, 8);
i = OR(i, b2); i = LSHIFT(i, 8);
i = OR(i, b3);
return i;
end
local word2bytes = function(word)
local b0, b1, b2, b3;
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
b0 = AND(word, 0xFF);
return b0, b1, b2, b3;
end
local dword2bytes = function(i)
local b4, b5, b6, b7 = word2bytes(i);
local b0, b1, b2, b3 = word2bytes(Math.floor(i / 0x100000000));
return b0, b1, b2, b3, b4, b5, b6, b7;
end
local SHA2_224 = function()
local queue = Queue();
local h0 = 0xc1059ed8;
local h1 = 0x367cd507;
local h2 = 0x3070dd17;
local h3 = 0xf70e5939;
local h4 = 0xffc00b31;
local h5 = 0x68581511;
local h6 = 0x64f98fa7;
local h7 = 0xbefa4fa4;
local public = {};
local processBlock = function()
local a = h0;
local b = h1;
local c = h2;
local d = h3;
local e = h4;
local f = h5;
local g = h6;
local h = h7;
local w = {};
for i = 0, 15 do
w[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
end
for i = 16, 63 do
local s0 = XOR(RROT(w[i - 15], 7), XOR(RROT(w[i - 15], 18), RSHIFT(w[i - 15], 3)));
local s1 = XOR(RROT(w[i - 2], 17), XOR(RROT(w[i - 2], 19), RSHIFT(w[i - 2], 10)));
w[i] = AND(w[i - 16] + s0 + w[i - 7] + s1, 0xFFFFFFFF);
end
for i = 0, 63 do
local s1 = XOR(RROT(e, 6), XOR(RROT(e, 11), RROT(e, 25)));
local ch = XOR(AND(e, f), AND(NOT(e), g));
local temp1 = h + s1 + ch + CONSTANTS[i + 1] + w[i];
local s0 = XOR(RROT(a, 2), XOR(RROT(a, 13), RROT(a, 22)));
local maj = XOR(AND(a, b), XOR(AND(a, c), AND(b, c)));
local temp2 = s0 + maj;
h = g;
g = f;
f = e;
e = d + temp1;
d = c;
c = b;
b = a;
a = temp1 + temp2;
end
h0 = AND(h0 + a, 0xFFFFFFFF);
h1 = AND(h1 + b, 0xFFFFFFFF);
h2 = AND(h2 + c, 0xFFFFFFFF);
h3 = AND(h3 + d, 0xFFFFFFFF);
h4 = AND(h4 + e, 0xFFFFFFFF);
h5 = AND(h5 + f, 0xFFFFFFFF);
h6 = AND(h6 + g, 0xFFFFFFFF);
h7 = AND(h7 + h, 0xFFFFFFFF);
end
public.init = function()
queue.reset();
h0 = 0xc1059ed8;
h1 = 0x367cd507;
h2 = 0x3070dd17;
h3 = 0xf70e5939;
h4 = 0xffc00b31;
h5 = 0x68581511;
h6 = 0x64f98fa7;
h7 = 0xbefa4fa4;
return public;
end
public.update = function(bytes)
for b in bytes do
queue.push(b);
if queue.size() >= 64 then processBlock(); end
end
return public;
end
public.finish = function()
local bits = queue.getHead() * 8;
queue.push(0x80);
while ((queue.size() + 7) % 64) < 63 do
queue.push(0x00);
end
local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits);
queue.push(b0);
queue.push(b1);
queue.push(b2);
queue.push(b3);
queue.push(b4);
queue.push(b5);
queue.push(b6);
queue.push(b7);
while queue.size() > 0 do
processBlock();
end
return public;
end
public.asBytes = function()
local b0, b1, b2, b3 = word2bytes(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
local b20, b21, b22, b23 = word2bytes(h5);
local b24, b25, b26, b27 = word2bytes(h6);
return { b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27};
end
public.asHex = function()
local b0, b1, b2, b3 = word2bytes(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
local b20, b21, b22, b23 = word2bytes(h5);
local b24, b25, b26, b27 = word2bytes(h6);
return String.format(fmt, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27);
end
return public;
end
return SHA2_224;

203
lockbox/digest/sha2_256.lua Normal file
View File

@ -0,0 +1,203 @@
local Bit = require("lockbox.util.bit");
local String = require("string");
local Math = require("math");
local Queue = require("lockbox.util.queue");
local CONSTANTS = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 };
local fmt = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" ..
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
local AND = Bit.band;
local OR = Bit.bor;
local NOT = Bit.bnot;
local XOR = Bit.bxor;
local RROT = Bit.rrotate;
local LSHIFT = Bit.lshift;
local RSHIFT = Bit.rshift;
--SHA2 is big-endian
local bytes2word = function(b0, b1, b2, b3)
local i = b0; i = LSHIFT(i, 8);
i = OR(i, b1); i = LSHIFT(i, 8);
i = OR(i, b2); i = LSHIFT(i, 8);
i = OR(i, b3);
return i;
end
local word2bytes = function(word)
local b0, b1, b2, b3;
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
b0 = AND(word, 0xFF);
return b0, b1, b2, b3;
end
local dword2bytes = function(i)
local b4, b5, b6, b7 = word2bytes(i);
local b0, b1, b2, b3 = word2bytes(Math.floor(i / 0x100000000));
return b0, b1, b2, b3, b4, b5, b6, b7;
end
local SHA2_256 = function()
local queue = Queue();
local h0 = 0x6a09e667;
local h1 = 0xbb67ae85;
local h2 = 0x3c6ef372;
local h3 = 0xa54ff53a;
local h4 = 0x510e527f;
local h5 = 0x9b05688c;
local h6 = 0x1f83d9ab;
local h7 = 0x5be0cd19;
local public = {};
local processBlock = function()
local a = h0;
local b = h1;
local c = h2;
local d = h3;
local e = h4;
local f = h5;
local g = h6;
local h = h7;
local w = {};
for i = 0, 15 do
w[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
end
for i = 16, 63 do
local s0 = XOR(RROT(w[i - 15], 7), XOR(RROT(w[i - 15], 18), RSHIFT(w[i - 15], 3)));
local s1 = XOR(RROT(w[i - 2], 17), XOR(RROT(w[i - 2], 19), RSHIFT(w[i - 2], 10)));
w[i] = AND(w[i - 16] + s0 + w[i - 7] + s1, 0xFFFFFFFF);
end
for i = 0, 63 do
local s1 = XOR(RROT(e, 6), XOR(RROT(e, 11), RROT(e, 25)));
local ch = XOR(AND(e, f), AND(NOT(e), g));
local temp1 = h + s1 + ch + CONSTANTS[i + 1] + w[i];
local s0 = XOR(RROT(a, 2), XOR(RROT(a, 13), RROT(a, 22)));
local maj = XOR(AND(a, b), XOR(AND(a, c), AND(b, c)));
local temp2 = s0 + maj;
h = g;
g = f;
f = e;
e = d + temp1;
d = c;
c = b;
b = a;
a = temp1 + temp2;
end
h0 = AND(h0 + a, 0xFFFFFFFF);
h1 = AND(h1 + b, 0xFFFFFFFF);
h2 = AND(h2 + c, 0xFFFFFFFF);
h3 = AND(h3 + d, 0xFFFFFFFF);
h4 = AND(h4 + e, 0xFFFFFFFF);
h5 = AND(h5 + f, 0xFFFFFFFF);
h6 = AND(h6 + g, 0xFFFFFFFF);
h7 = AND(h7 + h, 0xFFFFFFFF);
end
public.init = function()
queue.reset();
h0 = 0x6a09e667;
h1 = 0xbb67ae85;
h2 = 0x3c6ef372;
h3 = 0xa54ff53a;
h4 = 0x510e527f;
h5 = 0x9b05688c;
h6 = 0x1f83d9ab;
h7 = 0x5be0cd19;
return public;
end
public.update = function(bytes)
for b in bytes do
queue.push(b);
if queue.size() >= 64 then processBlock(); end
end
return public;
end
public.finish = function()
local bits = queue.getHead() * 8;
queue.push(0x80);
while ((queue.size() + 7) % 64) < 63 do
queue.push(0x00);
end
local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits);
queue.push(b0);
queue.push(b1);
queue.push(b2);
queue.push(b3);
queue.push(b4);
queue.push(b5);
queue.push(b6);
queue.push(b7);
while queue.size() > 0 do
processBlock();
end
return public;
end
public.asBytes = function()
local b0, b1, b2, b3 = word2bytes(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
local b20, b21, b22, b23 = word2bytes(h5);
local b24, b25, b26, b27 = word2bytes(h6);
local b28, b29, b30, b31 = word2bytes(h7);
return { b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29, b30, b31};
end
public.asHex = function()
local b0, b1, b2, b3 = word2bytes(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
local b20, b21, b22, b23 = word2bytes(h5);
local b24, b25, b26, b27 = word2bytes(h6);
local b28, b29, b30, b31 = word2bytes(h7);
return String.format(fmt, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29, b30, b31);
end
return public;
end
return SHA2_256;

6
lockbox/init.lua Normal file
View File

@ -0,0 +1,6 @@
local Lockbox = {}
-- cc-mek-scada lockbox version
Lockbox.version = "1.1"
return Lockbox

114
lockbox/kdf/pbkdf2.lua Normal file
View File

@ -0,0 +1,114 @@
local Bit = require("lockbox.util.bit");
local Array = require("lockbox.util.array");
local Stream = require("lockbox.util.stream");
local Math = require("math");
local AND = Bit.band;
local RSHIFT = Bit.rshift;
local word2bytes = function(word)
local b0, b1, b2, b3;
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
b0 = AND(word, 0xFF);
return b0, b1, b2, b3;
end
local PBKDF2 = function()
local public = {};
local blockLen = 16;
local dKeyLen = 256;
local iterations = 4096;
local salt;
local password;
local PRF;
local dKey;
public.setBlockLen = function(len)
blockLen = len;
return public;
end
public.setDKeyLen = function(len)
dKeyLen = len
return public;
end
public.setIterations = function(iter)
iterations = iter;
return public;
end
public.setSalt = function(saltBytes)
salt = saltBytes;
return public;
end
public.setPassword = function(passwordBytes)
password = passwordBytes;
return public;
end
public.setPRF = function(prf)
PRF = prf;
return public;
end
local buildBlock = function(i)
local b0, b1, b2, b3 = word2bytes(i);
local ii = {b0, b1, b2, b3};
local s = Array.concat(salt, ii);
local out = {};
PRF.setKey(password);
for c = 1, iterations do
PRF.init()
.update(Stream.fromArray(s));
s = PRF.finish().asBytes();
if(c > 1) then
out = Array.XOR(out, s);
else
out = s;
end
end
return out;
end
public.finish = function()
local blocks = Math.ceil(dKeyLen / blockLen);
dKey = {};
for b = 1, blocks do
local block = buildBlock(b);
dKey = Array.concat(dKey, block);
end
if(Array.size(dKey) > dKeyLen) then dKey = Array.truncate(dKey, dKeyLen); end
return public;
end
public.asBytes = function()
return dKey;
end
public.asHex = function()
return Array.toHex(dKey);
end
return public;
end
return PBKDF2;

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