Compare commits

...

184 Commits

Author SHA1 Message Date
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
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 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 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
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 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 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
8ddc233da0 #399 pocket waste control comms commands 2024-10-18 02:35:48 +00:00
101 changed files with 5616 additions and 2489 deletions

View File

@@ -28,6 +28,9 @@ def minify(path: str):
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

175
ccmsi.lua
View File

@@ -15,7 +15,7 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]--
local CCMSI_VERSION = "v1.19"
local CCMSI_VERSION = "v1.21"
local install_dir = "/.install-cache"
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
@@ -149,16 +149,16 @@ local function get_remote_manifest()
end
-- record the local installation manifest
local function write_install_manifest(manifest, dependencies)
local function write_install_manifest(manifest, deps)
local versions = {}
for key, value in pairs(manifest.versions) do
local is_dependency = false
for _, dependency in pairs(dependencies) do
if (key == "bootloader" and dependency == "system") or key == dependency then
is_dependency = true;break
local is_dep = false
for _, dep in pairs(deps) do
if (key == "bootloader" and dep == "system") or key == dep then
is_dep = true;break
end
end
if key == app or key == "comms" or is_dependency then versions[key] = value end
if key == app or key == "comms" or is_dep then versions[key] = value end
end
manifest.versions = versions
@@ -321,23 +321,38 @@ if #opts == 0 or opts[1] == "help" then
end
return
else
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
if mode == nil then
red();println("Unrecognized mode.");white()
return
end
app = get_opt(opts[2], { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket", "installer" })
local next_opt = 3
local apps = { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket", "installer" }
app = get_opt(opts[2], apps)
if app == nil then
for _, a in pairs(apps) do
if fs.exists(a) and fs.isDir(a) then
app = a
next_opt = 2
break
end
end
end
if app == nil and mode ~= "check" then
red();println("Unrecognized application.");white()
return
elseif mode == "check" then
next_opt = 2
elseif app == "installer" and mode ~= "update" then
red();println("Installer app only supports 'update' option.");white()
return
end
-- determine target
if mode == "check" then target = opts[2] else target = opts[3] end
target = opts[next_opt]
if (target ~= "main") and (target ~= "devel") then
if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end
target = "main"
@@ -383,8 +398,10 @@ if mode == "check" then
yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white()
end
elseif mode == "install" or mode == "update" then
local ok, r_manifest, l_manifest
local update_installer = app == "installer"
local ok, manifest = get_remote_manifest()
ok, r_manifest = get_remote_manifest()
if not ok then return end
local ver = {
@@ -397,27 +414,27 @@ elseif mode == "install" or mode == "update" then
}
-- try to find local versions
local local_ok, lmnf = read_local_manifest()
if not local_ok then
if mode == "update" then
ok, l_manifest = read_local_manifest()
if mode == "update" and not update_installer then
if not ok then
red();println("Failed to load local installation information, cannot update.");white()
return
end
elseif not update_installer then
ver.boot.v_local = lmnf.versions.bootloader
ver.app.v_local = lmnf.versions[app]
ver.comms.v_local = lmnf.versions.comms
ver.common.v_local = lmnf.versions.common
ver.graphics.v_local = lmnf.versions.graphics
ver.lockbox.v_local = lmnf.versions.lockbox
else
ver.boot.v_local = l_manifest.versions.bootloader
ver.app.v_local = l_manifest.versions[app]
ver.comms.v_local = l_manifest.versions.comms
ver.common.v_local = l_manifest.versions.common
ver.graphics.v_local = l_manifest.versions.graphics
ver.lockbox.v_local = l_manifest.versions.lockbox
if lmnf.versions[app] == nil then
red();println("Another application is already installed, please uninstall it before installing a new application.");white()
return
if l_manifest.versions[app] == nil then
red();println("Another application is already installed, please uninstall it before installing a new application.");white()
return
end
end
end
if manifest.versions.installer ~= CCMSI_VERSION then
if r_manifest.versions.installer ~= CCMSI_VERSION then
if not update_installer then yellow();println("A different version of the installer is available, it is recommended to update to it.");white() end
if update_installer or ask_y_n("Would you like to update now", true) then
lgray();println("GET ccmsi.lua")
@@ -440,12 +457,12 @@ elseif mode == "install" or mode == "update" then
return
end
ver.boot.v_remote = manifest.versions.bootloader
ver.app.v_remote = manifest.versions[app]
ver.comms.v_remote = manifest.versions.comms
ver.common.v_remote = manifest.versions.common
ver.graphics.v_remote = manifest.versions.graphics
ver.lockbox.v_remote = manifest.versions.lockbox
ver.boot.v_remote = r_manifest.versions.bootloader
ver.app.v_remote = r_manifest.versions[app]
ver.comms.v_remote = r_manifest.versions.comms
ver.common.v_remote = r_manifest.versions.common
ver.graphics.v_remote = r_manifest.versions.graphics
ver.lockbox.v_remote = r_manifest.versions.lockbox
green()
if mode == "install" then print("Installing ") else print("Updating ") end
@@ -461,36 +478,33 @@ elseif mode == "install" or mode == "update" then
ver.graphics.changed = show_pkg_change("graphics", ver.graphics)
ver.lockbox.changed = show_pkg_change("lockbox", ver.lockbox)
--------------------------
-- START INSTALL/UPDATE --
--------------------------
-- start install/update
local space_required = manifest.sizes.manifest
local space_available = fs.getFreeSpace("/")
local space_req = r_manifest.sizes.manifest
local space_avail = fs.getFreeSpace("/")
local single_file_mode = false
local file_list = manifest.files
local size_list = manifest.sizes
local dependencies = manifest.depends[app]
local file_list = r_manifest.files
local size_list = r_manifest.sizes
local deps = r_manifest.depends[app]
table.insert(dependencies, app)
table.insert(deps, app)
-- helper function to check if a dependency is unchanged
local function unchanged(dependency)
if dependency == "system" then return not ver.boot.changed
elseif dependency == "graphics" then return not ver.graphics.changed
elseif dependency == "lockbox" then return not ver.lockbox.changed
elseif dependency == "common" then return not (ver.common.changed or ver.comms.changed)
elseif dependency == app then return not ver.app.changed
local function unchanged(dep)
if dep == "system" then return not ver.boot.changed
elseif dep == "graphics" then return not ver.graphics.changed
elseif dep == "lockbox" then return not ver.lockbox.changed
elseif dep == "common" then return not (ver.common.changed or ver.comms.changed)
elseif dep == app then return not ver.app.changed
else return true end
end
local any_change = false
for _, dependency in pairs(dependencies) do
local size = size_list[dependency]
space_required = space_required + size
any_change = any_change or not unchanged(dependency)
for _, dep in pairs(deps) do
local size = size_list[dep]
space_req = space_req + size
any_change = any_change or not unchanged(dep)
end
if mode == "update" and not any_change then
@@ -501,10 +515,7 @@ elseif mode == "install" or mode == "update" then
-- ask for confirmation
if not ask_y_n("Continue", false) then return end
-- check space constraints
if space_available < space_required then
single_file_mode = true
end
local single_file_mode = space_avail < space_req
local success = true
@@ -548,7 +559,7 @@ elseif mode == "install" or mode == "update" then
success = false
return
end
clean(manifest)
clean(r_manifest)
sf_install(3)
elseif attempt == 3 then
yellow()
@@ -574,30 +585,30 @@ elseif mode == "install" or mode == "update" then
local abort_attempt = false
success = true
for _, dependency in pairs(dependencies) do
if mode == "update" and unchanged(dependency) then
pkg_message("skipping install of unchanged package", dependency)
for _, dep in pairs(deps) do
if mode == "update" and unchanged(dep) then
pkg_message("skipping install of unchanged package", dep)
else
pkg_message("installing package", dependency)
pkg_message("installing package", dep)
lgray()
-- beginning on the second try, delete the directory before starting
if attempt >= 2 then
if dependency == "system" then
elseif dependency == "common" 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("/"..dependency) then
fs.delete("/"..dependency)
println("deleted /"..dependency)
if fs.exists("/"..dep) then
fs.delete("/"..dep)
println("deleted /"..dep)
end
end
end
local files = file_list[dependency]
local files = file_list[dep]
for _, file in pairs(files) do
println("GET "..file)
mitigate_case(file)
@@ -620,14 +631,14 @@ elseif mode == "install" or mode == "update" then
if fs.exists(install_dir) then fs.delete(install_dir);fs.makeDir(install_dir) end
-- download all dependencies
for _, dependency in pairs(dependencies) do
if mode == "update" and unchanged(dependency) then
pkg_message("skipping download of unchanged package", dependency)
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", dependency)
pkg_message("downloading package", dep)
lgray()
local files = file_list[dependency]
local files = file_list[dep]
for _, file in pairs(files) do
println("GET "..file)
local dl_stat = http_get_file(file, install_dir.."/")
@@ -650,14 +661,14 @@ elseif mode == "install" or mode == "update" then
-- copy in downloaded files (installation)
if success then
for _, dependency in pairs(dependencies) do
if mode == "update" and unchanged(dependency) then
pkg_message("skipping install of unchanged package", dependency)
for _, dep in pairs(deps) do
if mode == "update" and unchanged(dep) then
pkg_message("skipping install of unchanged package", dep)
else
pkg_message("installing package", dependency)
pkg_message("installing package", dep)
lgray()
local files = file_list[dependency]
local files = file_list[dep]
for _, file in pairs(files) do
local temp_file = install_dir.."/"..file
if fs.exists(file) then fs.delete(file) end
@@ -671,13 +682,13 @@ elseif mode == "install" or mode == "update" then
end
if success then
write_install_manifest(manifest, dependencies)
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(manifest)
any_key();clean(r_manifest)
white();println("Done.")
else
red()
@@ -712,14 +723,14 @@ elseif mode == "uninstall" then
clean(manifest)
local file_list = manifest.files
local dependencies = manifest.depends[app]
local deps = manifest.depends[app]
table.insert(dependencies, app)
table.insert(deps, app)
-- delete all installed files
lgray()
for _, dependency in pairs(dependencies) do
local files = file_list[dependency]
for _, dep in pairs(deps) do
local files = file_list[dep]
for _, file in pairs(files) do
if fs.exists(file) then fs.delete(file);println("deleted "..file) end
end

View File

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

View File

@@ -231,7 +231,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
TextBox{parent=fac_c_2,x=1,y=1,height=3,text="Please enter the number of reactors you have, also referred to as reactor units or 'units' for short. A maximum of 4 is currently supported."}
tool_ctl.num_units = NumberField{parent=fac_c_2,x=1,y=5,width=5,max_chars=2,default=ini_cfg.UnitCount,min=1,max=4,fg_bg=bw_fg_bg}
TextBox{parent=fac_c_2,x=7,y=5,text="reactors"}
TextBox{parent=fac_c_2,x=1,y=7,height=3,text="This will decide how many monitors you need. If this does not match the supervisor's number of reactor units, the coordinator will not connect.",fg_bg=g_lg_fg_bg}
TextBox{parent=fac_c_2,x=1,y=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}

View File

@@ -234,19 +234,24 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
TextBox{parent=crd_cfg,x=1,y=2,text=" Coordinator UI Configuration",fg_bg=cpair(colors.black,colors.lime)}
TextBox{parent=crd_c_1,x=1,y=1,height=3,text="Configure the UI interface options below if you wish to customize formats."}
TextBox{parent=crd_c_1,x=1,y=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=24,y=8,text="Energy Scale"}
tool_ctl.energy_scale = RadioButton{parent=crd_c_1,x=24,y=9,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
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)

View File

@@ -380,6 +380,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
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)
@@ -528,6 +529,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
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
@@ -550,7 +553,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if string.len(val) > val_max_w then
if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end

View File

@@ -35,7 +35,8 @@ 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.5.1", { "Added energy scale options" } },
{ "v1.6.13", { "Added option for Po/Pu pellet green/cyan pairing" } }
}
---@class crd_configurator
@@ -58,6 +59,7 @@ style.btn_dis_fg_bg = cpair(colors.lightGray,colors.white)
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,
@@ -76,6 +78,7 @@ local tool_ctl = {
-- 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
@@ -94,6 +97,7 @@ local tmp_cfg = {
UnitCount = 1,
SpeakerVolume = 1.0,
Time24Hour = true,
GreenPuPellet = false,
TempScale = 1, ---@type TEMP_SCALE
EnergyScale = 1, ---@type ENERGY_SCALE
DisableFlowView = false,
@@ -128,6 +132,7 @@ local fields = {
{ "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 },
@@ -210,7 +215,7 @@ local function config_view(display)
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text=msg,fg_bg=cpair(colors.red,colors.lightGray)}
y_start = y_start + 5
elseif tool_ctl.start_fail > 0 then
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device had no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
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
@@ -236,9 +241,17 @@ local function config_view(display)
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}
tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,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=17,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}
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()
@@ -372,7 +385,7 @@ function configurator.configure(start_code, message)
println("configurator error: " .. error)
end
return status, error
return status, error, tool_ctl.launch_startup
end
return configurator

View File

@@ -24,6 +24,7 @@ local LINK_TIMEOUT = 60.0
local coordinator = {}
---@type crd_config
---@diagnostic disable-next-line: missing-fields
local config = {}
coordinator.config = config
@@ -37,6 +38,7 @@ function coordinator.load_config()
config.UnitCount = settings.get("UnitCount")
config.SpeakerVolume = settings.get("SpeakerVolume")
config.Time24Hour = settings.get("Time24Hour")
config.GreenPuPellet = settings.get("GreenPuPellet")
config.TempScale = settings.get("TempScale")
config.EnergyScale = settings.get("EnergyScale")
@@ -66,6 +68,7 @@ function coordinator.load_config()
cfv.assert_type_int(config.UnitCount)
cfv.assert_range(config.UnitCount, 1, 4)
cfv.assert_type_bool(config.Time24Hour)
cfv.assert_type_bool(config.GreenPuPellet)
cfv.assert_type_int(config.TempScale)
cfv.assert_range(config.TempScale, 1, 4)
cfv.assert_type_int(config.EnergyScale)
@@ -379,6 +382,18 @@ function coordinator.comms(version, nic, sv_watchdog)
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {})
end
-- send the resume ready state to the supervisor
---@param mode PROCESS process control mode
---@param burn_target number burn rate target
---@param charge_target number charge level target
---@param gen_target number generation rate target
---@param limits number[] unit burn rate limits
function public.send_ready(mode, burn_target, charge_target, gen_target, limits)
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.PROCESS_READY, {
mode, burn_target, charge_target, gen_target, limits
})
end
-- send a facility command
---@param cmd FAC_COMMAND command
---@param option any? optional option options for the optional options (like waste mode)

View File

@@ -20,6 +20,13 @@ local ENERGY_UNITS = types.ENERGY_SCALE_UNITS
local TEMP_SCALE = types.TEMP_SCALE
local TEMP_UNITS = types.TEMP_SCALE_UNITS
local RCT_STATE = types.REACTOR_STATE
local BLR_STATE = types.BOILER_STATE
local TRB_STATE = types.TURBINE_STATE
local TNK_STATE = types.TANK_STATE
local MTX_STATE = types.IMATRIX_STATE
local SPS_STATE = types.SPS_STATE
-- nominal RTT is ping (0ms to 10ms usually) + 500ms for CRD main loop tick
local WARN_RTT = 1000 -- 2x as long as expected w/ 0 ping
local HIGH_RTT = 1500 -- 3.33x as long as expected w/ 0 ping
@@ -81,6 +88,8 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
tank_mode = conf.cooling.fac_tank_mode,
tank_defs = conf.cooling.fac_tank_defs,
tank_list = conf.cooling.fac_tank_list,
tank_conns = conf.cooling.fac_tank_conns,
tank_fluid_types = conf.cooling.tank_fluid_types,
all_sys_ok = false,
rtu_count = 0,
@@ -94,7 +103,7 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
auto_scram = false,
---@type ascram_status
ascram_status = {
matrix_dc = false,
matrix_fault = false,
matrix_fill = false,
crit_alarm = false,
radiation = false,
@@ -105,10 +114,11 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
auto_pu_fallback_active = false,
auto_sps_disabled = false,
waste_stats = { 0, 0, 0, 0, 0, 0 }, -- waste in, pu, po, po pellets, am, spent waste
radiation = types.new_zero_radiation_reading(),
save_cfg_ack = nil, ---@type fun(success: boolean)
save_cfg_ack = nil, ---@type fun(success: boolean)
---@type { [TONE]: boolean }
alarm_tones = { false, false, false, false, false, false, false, false },
@@ -149,15 +159,12 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
local entry = {
unit_id = i,
connected = false,
rtu_hw = {
boilers = {}, ---@type { connected: boolean, faulted: boolean }[]
turbines = {} ---@type { connected: boolean, faulted: boolean }[]
},
num_boilers = 0,
num_turbines = 0,
num_snas = 0,
has_tank = conf.cooling.r_cool[i].TankConnection,
aux_coolant = conf.cooling.aux_coolant[i],
status_lines = { "", "" },
@@ -174,6 +181,7 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
waste_product = types.WASTE_PRODUCT.PLUTONIUM,
waste_stats = { 0, 0, 0 }, -- plutonium, polonium, po pellets
last_rate_change_ms = 0,
turbine_flow_stable = false,
@@ -221,6 +229,7 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
ALARM_STATE.INACTIVE -- turbine trip
},
---@diagnostic disable-next-line: missing-fields
annunciator = {}, ---@type annunciator
unit_ps = psil.create(),
@@ -245,14 +254,12 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
for _ = 1, conf.cooling.r_cool[i].BoilerCount do
table.insert(entry.boiler_ps_tbl, psil.create())
table.insert(entry.boiler_data_tbl, {})
table.insert(entry.rtu_hw.boilers, { connected = false, faulted = false })
end
-- create turbine tables
for _ = 1, conf.cooling.r_cool[i].TurbineCount do
table.insert(entry.turbine_ps_tbl, psil.create())
table.insert(entry.turbine_data_tbl, {})
table.insert(entry.rtu_hw.turbines, { connected = false, faulted = false })
end
-- create tank tables
@@ -363,6 +370,7 @@ local function _record_multiblock_build(id, entry, data_tbl, ps_tbl, create)
if exists or create then
if not exists then
ps_tbl[id] = psil.create()
---@diagnostic disable-next-line: missing-fields
data_tbl[id] = {}
end
@@ -488,6 +496,49 @@ end
--#region Statuses
-- generate the text string for the induction matrix charge/discharge ETA
---@param eta_ms number eta in milliseconds
local function gen_eta_text(eta_ms)
local str, pre = "", util.trinary(eta_ms >= 0, "Full in ", "Empty in ")
local seconds = math.abs(eta_ms) / 1000
local minutes = seconds / 60
local hours = minutes / 60
local days = hours / 24
if math.abs(eta_ms) < 1000 or (eta_ms ~= eta_ms) then
-- really small or NaN
str = "No ETA"
elseif days < 1000 then
days = math.floor(days)
hours = math.floor(hours % 24)
minutes = math.floor(minutes % 60)
seconds = math.floor(seconds % 60)
if days > 0 then
str = days .. "d"
elseif hours > 0 then
str = hours .. "h " .. minutes .. "m"
elseif minutes > 0 then
str = minutes .. "m " .. seconds .. "s"
elseif seconds > 0 then
str = seconds .. "s"
end
str = pre .. str
else
local years = math.floor(days / 365.25)
if years <= 99999999 then
str = pre .. years .. "y"
else
str = pre .. "eras"
end
end
return str
end
-- record and publish multiblock status data
---@param entry any
---@param data imatrix_session_db|sps_session_db|dynamicv_session_db|turbinev_session_db|boilerv_session_db
@@ -540,7 +591,7 @@ function iocontrol.update_facility_status(status)
fac.auto_saturated = ctl_status[5]
fac.auto_scram = ctl_status[6]
fac.ascram_status.matrix_dc = ctl_status[7]
fac.ascram_status.matrix_fault = ctl_status[7]
fac.ascram_status.matrix_fill = ctl_status[8]
fac.ascram_status.crit_alarm = ctl_status[9]
fac.ascram_status.radiation = ctl_status[10]
@@ -555,7 +606,7 @@ function iocontrol.update_facility_status(status)
fac.ps.publish("auto_ramping", fac.auto_ramping)
fac.ps.publish("auto_saturated", fac.auto_saturated)
fac.ps.publish("auto_scram", fac.auto_scram)
fac.ps.publish("as_matrix_dc", fac.ascram_status.matrix_dc)
fac.ps.publish("as_matrix_fault", fac.ascram_status.matrix_fault)
fac.ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill)
fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm)
fac.ps.publish("as_radiation", fac.ascram_status.radiation)
@@ -609,6 +660,7 @@ function iocontrol.update_facility_status(status)
ps.publish("avg_inflow", in_f)
ps.publish("avg_outflow", out_f)
ps.publish("eta_ms", eta)
ps.publish("eta_string", gen_eta_text(eta or 0))
ps.publish("is_charging", in_f > out_f)
ps.publish("is_discharging", out_f > in_f)
@@ -624,10 +676,12 @@ function iocontrol.update_facility_status(status)
-- induction matricies statuses
if type(rtu_statuses.induction) == "table" then
local matrix_status = MTX_STATE.OFFLINE
for id = 1, #fac.induction_ps_tbl do
if rtu_statuses.induction[id] == nil then
-- disconnected
fac.induction_ps_tbl[id].publish("computed_status", 1)
fac.induction_ps_tbl[id].publish("computed_status", matrix_status)
end
end
@@ -639,18 +693,20 @@ function iocontrol.update_facility_status(status)
local rtu_faulted = _record_multiblock_status(matrix, data, ps)
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
matrix_status = MTX_STATE.FAULT
elseif data.formed then
if data.tanks.energy_fill >= 0.99 then
ps.publish("computed_status", 6) -- full
matrix_status = MTX_STATE.HIGH_CHARGE
elseif data.tanks.energy_fill <= 0.01 then
ps.publish("computed_status", 5) -- empty
matrix_status = MTX_STATE.LOW_CHARGE
else
ps.publish("computed_status", 4) -- on-line
matrix_status = MTX_STATE.ONLINE
end
else
ps.publish("computed_status", 2) -- not formed
matrix_status = MTX_STATE.UNFORMED
end
ps.publish("computed_status", matrix_status)
else
log.debug(util.c(log_header, "invalid induction matrix id ", id))
end
@@ -662,10 +718,12 @@ function iocontrol.update_facility_status(status)
-- SPS statuses
if type(rtu_statuses.sps) == "table" then
local sps_status = SPS_STATE.OFFLINE
for id = 1, #fac.sps_ps_tbl do
if rtu_statuses.sps[id] == nil then
-- disconnected
fac.sps_ps_tbl[id].publish("computed_status", 1)
fac.sps_ps_tbl[id].publish("computed_status", sps_status)
end
end
@@ -677,16 +735,13 @@ function iocontrol.update_facility_status(status)
local rtu_faulted = _record_multiblock_status(sps, data, ps)
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
sps_status = SPS_STATE.FAULT
elseif data.formed then
if data.state.process_rate > 0 then
ps.publish("computed_status", 5) -- active
else
ps.publish("computed_status", 4) -- idle
end
else
ps.publish("computed_status", 2) -- not formed
end
-- active / idle
sps_status = util.trinary(data.state.process_rate > 0, SPS_STATE.ACTIVE, SPS_STATE.IDLE)
else sps_status = SPS_STATE.UNFORMED end
ps.publish("computed_status", sps_status)
io.facility.ps.publish("am_rate", data.state.process_rate * 1000)
else
@@ -700,10 +755,12 @@ function iocontrol.update_facility_status(status)
-- dynamic tank statuses
if type(rtu_statuses.tanks) == "table" then
local tank_status = TNK_STATE.OFFLINE
for id = 1, #fac.tank_ps_tbl do
if rtu_statuses.tanks[id] == nil then
-- disconnected
fac.tank_ps_tbl[id].publish("computed_status", 1)
fac.tank_ps_tbl[id].publish("computed_status", tank_status)
end
end
@@ -715,18 +772,18 @@ function iocontrol.update_facility_status(status)
local rtu_faulted = _record_multiblock_status(tank, data, ps)
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
tank_status = TNK_STATE.FAULT
elseif data.formed then
if data.tanks.fill >= 0.99 then
ps.publish("computed_status", 6) -- full
tank_status = TNK_STATE.HIGH_FILL
elseif data.tanks.fill < 0.20 then
ps.publish("computed_status", 5) -- low
tank_status = TNK_STATE.LOW_FILL
else
ps.publish("computed_status", 4) -- on-line
tank_status = TNK_STATE.ONLINE
end
else
ps.publish("computed_status", 2) -- not formed
end
else tank_status = TNK_STATE.UNFORMED end
ps.publish("computed_status", tank_status)
else
log.debug(util.c(log_header, "invalid dynamic tank id ", id))
end
@@ -826,9 +883,11 @@ function iocontrol.update_unit_statuses(statuses)
log.debug(log_header .. "reactor status not a table")
end
local computed_status = RCT_STATE.OFFLINE
if #reactor_status == 0 then
unit.connected = false
unit.unit_ps.publish("computed_status", 1) -- disconnected
unit.unit_ps.publish("computed_status", computed_status)
elseif #reactor_status == 3 then
local mek_status = reactor_status[1]
local rps_status = reactor_status[2]
@@ -867,22 +926,23 @@ function iocontrol.update_unit_statuses(statuses)
burn_rate_sum = burn_rate_sum + burn_rate
if unit.reactor_data.mek_status.status then
unit.unit_ps.publish("computed_status", 5) -- running
computed_status = RCT_STATE.ACTIVE
else
if unit.reactor_data.no_reactor then
unit.unit_ps.publish("computed_status", 3) -- faulted
computed_status = RCT_STATE.FAULT
elseif not unit.reactor_data.formed then
unit.unit_ps.publish("computed_status", 2) -- multiblock not formed
computed_status = RCT_STATE.UNFORMED
elseif unit.reactor_data.rps_status.force_dis then
unit.unit_ps.publish("computed_status", 7) -- reactor force disabled
computed_status = RCT_STATE.FORCE_DISABLED
elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then
unit.unit_ps.publish("computed_status", 6) -- SCRAM
computed_status = RCT_STATE.SCRAMMED
else
unit.unit_ps.publish("computed_status", 4) -- disabled
computed_status = RCT_STATE.DISABLED
end
end
unit.connected = true
unit.unit_ps.publish("computed_status", computed_status)
else
log.debug(log_header .. "reactor status length mismatch")
valid = false
@@ -896,13 +956,11 @@ function iocontrol.update_unit_statuses(statuses)
if type(rtu_statuses.boilers) == "table" then
local boil_sum = 0
for id = 1, #unit.boiler_ps_tbl do
local connected = rtu_statuses.boilers[id] ~= nil
unit.rtu_hw.boilers[id].connected = connected
computed_status = BLR_STATE.OFFLINE
if not connected then
-- disconnected
unit.boiler_ps_tbl[id].publish("computed_status", 1)
for id = 1, #unit.boiler_ps_tbl do
if rtu_statuses.boilers[id] == nil then
unit.boiler_ps_tbl[id].publish("computed_status", computed_status)
end
end
@@ -912,21 +970,15 @@ function iocontrol.update_unit_statuses(statuses)
local ps = unit.boiler_ps_tbl[id]
local rtu_faulted = _record_multiblock_status(boiler, data, ps)
unit.rtu_hw.boilers[id].faulted = rtu_faulted
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
computed_status = BLR_STATE.FAULT
elseif data.formed then
boil_sum = boil_sum + data.state.boil_rate
computed_status = util.trinary(data.state.boil_rate > 0, BLR_STATE.ACTIVE, BLR_STATE.IDLE)
else computed_status = BLR_STATE.UNFORMED end
if data.state.boil_rate > 0 then
ps.publish("computed_status", 5) -- active
else
ps.publish("computed_status", 4) -- idle
end
else
ps.publish("computed_status", 2) -- not formed
end
unit.boiler_ps_tbl[id].publish("computed_status", computed_status)
else
log.debug(util.c(log_header, "invalid boiler id ", id))
valid = false
@@ -943,13 +995,11 @@ function iocontrol.update_unit_statuses(statuses)
if type(rtu_statuses.turbines) == "table" then
local flow_sum = 0
for id = 1, #unit.turbine_ps_tbl do
local connected = rtu_statuses.turbines[id] ~= nil
unit.rtu_hw.turbines[id].connected = connected
computed_status = TRB_STATE.OFFLINE
if not connected then
-- disconnected
unit.turbine_ps_tbl[id].publish("computed_status", 1)
for id = 1, #unit.turbine_ps_tbl do
if rtu_statuses.turbines[id] == nil then
unit.turbine_ps_tbl[id].publish("computed_status", computed_status)
end
end
@@ -959,23 +1009,22 @@ function iocontrol.update_unit_statuses(statuses)
local ps = unit.turbine_ps_tbl[id]
local rtu_faulted = _record_multiblock_status(turbine, data, ps)
unit.rtu_hw.turbines[id].faulted = rtu_faulted
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
computed_status = TRB_STATE.FAULT
elseif data.formed then
flow_sum = flow_sum + data.state.flow_rate
if data.tanks.energy_fill >= 0.99 then
ps.publish("computed_status", 6) -- trip
computed_status = TRB_STATE.TRIPPED
elseif data.state.flow_rate < 100 then
ps.publish("computed_status", 4) -- idle
computed_status = TRB_STATE.IDLE
else
ps.publish("computed_status", 5) -- active
computed_status = TRB_STATE.ACTIVE
end
else
ps.publish("computed_status", 2) -- not formed
end
else computed_status = TRB_STATE.UNFORMED end
unit.turbine_ps_tbl[id].publish("computed_status", computed_status)
else
log.debug(util.c(log_header, "invalid turbine id ", id))
valid = false
@@ -990,10 +1039,11 @@ function iocontrol.update_unit_statuses(statuses)
-- dynamic tank statuses
if type(rtu_statuses.tanks) == "table" then
computed_status = TNK_STATE.OFFLINE
for id = 1, #unit.tank_ps_tbl do
if rtu_statuses.tanks[id] == nil then
-- disconnected
unit.tank_ps_tbl[id].publish("computed_status", 1)
unit.tank_ps_tbl[id].publish("computed_status", computed_status)
end
end
@@ -1005,18 +1055,18 @@ function iocontrol.update_unit_statuses(statuses)
local rtu_faulted = _record_multiblock_status(tank, data, ps)
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
computed_status = TNK_STATE.FAULT
elseif data.formed then
if data.tanks.fill >= 0.99 then
ps.publish("computed_status", 6) -- full
computed_status = TNK_STATE.HIGH_FILL
elseif data.tanks.fill < 0.20 then
ps.publish("computed_status", 5) -- low
computed_status = TNK_STATE.LOW_FILL
else
ps.publish("computed_status", 4) -- on-line
computed_status = TNK_STATE.ONLINE
end
else
ps.publish("computed_status", 2) -- not formed
end
else computed_status = TNK_STATE.UNFORMED end
unit.tank_ps_tbl[id].publish("computed_status", computed_status)
else
log.debug(util.c(log_header, "invalid dynamic tank id ", id))
valid = false
@@ -1081,6 +1131,7 @@ function iocontrol.update_unit_statuses(statuses)
unit.annunciator = status[3]
if type(unit.annunciator) ~= "table" then
---@diagnostic disable-next-line: missing-fields
unit.annunciator = {}
log.debug(log_header .. "annunciator state not a table")
valid = false
@@ -1164,7 +1215,7 @@ function iocontrol.update_unit_statuses(statuses)
local valve_states = status[6]
if type(valve_states) == "table" then
if #valve_states == 5 then
if #valve_states == 6 then
unit.unit_ps.publish("V_pu_conn", valve_states[1] > 0)
unit.unit_ps.publish("V_pu_state", valve_states[1] == 2)
unit.unit_ps.publish("V_po_conn", valve_states[2] > 0)
@@ -1175,6 +1226,8 @@ function iocontrol.update_unit_statuses(statuses)
unit.unit_ps.publish("V_am_state", valve_states[4] == 2)
unit.unit_ps.publish("V_emc_conn", valve_states[5] > 0)
unit.unit_ps.publish("V_emc_state", valve_states[5] == 2)
unit.unit_ps.publish("V_aux_conn", valve_states[6] > 0)
unit.unit_ps.publish("V_aux_state", valve_states[6] == 2)
else
log.debug(log_header .. "valve states length mismatch")
valid = false
@@ -1192,6 +1245,7 @@ function iocontrol.update_unit_statuses(statuses)
local u_spent_rate = waste_rate
local u_pu_rate = util.trinary(is_pu, waste_rate, 0.0)
local u_po_rate = unit.sna_out_rate
local u_po_pl_rate = 0
unit.unit_ps.publish("pu_rate", u_pu_rate)
unit.unit_ps.publish("po_rate", u_po_rate)
@@ -1202,6 +1256,7 @@ function iocontrol.update_unit_statuses(statuses)
u_spent_rate = u_po_rate
unit.unit_ps.publish("po_pl_rate", u_po_rate)
unit.unit_ps.publish("po_am_rate", 0)
u_po_pl_rate = u_po_rate
po_pl_rate = po_pl_rate + u_po_rate
elseif unit.waste_product == types.WASTE_PRODUCT.ANTI_MATTER then
u_spent_rate = 0
@@ -1213,6 +1268,8 @@ function iocontrol.update_unit_statuses(statuses)
unit.unit_ps.publish("po_am_rate", 0)
end
unit.waste_stats = { u_pu_rate, u_po_rate, u_po_pl_rate }
unit.unit_ps.publish("ws_rate", u_spent_rate)
pu_rate = pu_rate + u_pu_rate
@@ -1221,6 +1278,8 @@ function iocontrol.update_unit_statuses(statuses)
end
end
io.facility.waste_stats = { burn_rate_sum, pu_rate, po_rate, po_pl_rate, po_am_rate, spent_rate }
io.facility.ps.publish("burn_sum", burn_rate_sum)
io.facility.ps.publish("sna_count", sna_count_sum)
io.facility.ps.publish("pu_rate", pu_rate)

View File

@@ -139,6 +139,11 @@ function process.init(iocontrol, coord_comms)
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
@@ -286,6 +291,7 @@ function process.create_handle()
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
@@ -445,36 +451,21 @@ end
---@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))
-- update config table and save
pctl.control_states.process.waste_product = product
_write_auto_config()
end
-- set automatic process control plutonium fallback
---@param enabled boolean whether to enable plutonium fallback
function process.set_pu_fallback(enabled)
pctl.comms.send_fac_command(F_CMD.SET_PU_FB, enabled)
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
-- update config table and save
pctl.control_states.process.pu_fallback = enabled
_write_auto_config()
end
-- set automatic process control SPS usage at low power
---@param enabled boolean whether to enable SPS usage at low power
function process.set_sps_low_power(enabled)
pctl.comms.send_fac_command(F_CMD.SET_SPS_LP, enabled)
log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled))
-- update config table and save
pctl.control_states.process.sps_low_power = enabled
_write_auto_config()
end
-- save process control settings
@@ -527,21 +518,30 @@ 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

View File

@@ -137,7 +137,7 @@ function renderer.try_start_fp()
if not engine.fp_ready then
-- show front panel view on terminal
status, msg = pcall(function ()
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root}
engine.ui.front_panel = DisplayBox{window=term.current(),fg_bg=style.fp.root}
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
end)

View File

@@ -1,6 +1,7 @@
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")
@@ -14,6 +15,9 @@ 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
@@ -166,8 +170,26 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
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
@@ -192,20 +214,28 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
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 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 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
@@ -230,20 +260,67 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
{ fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated },
{ fac.auto_current_waste_product, fac.auto_pu_fallback_active },
util.table_len(fac.tank_data_tbl),
fac.induction_data_tbl[1] ~= nil,
fac.sps_data_tbl[1] ~= nil,
fac.induction_data_tbl[1] ~= nil, ---@fixme this means nothing
fac.sps_data_tbl[1] ~= nil ---@fixme this means nothing
}
_send(CRDN_TYPE.API_GET_FAC, data)
elseif pkt.type == CRDN_TYPE.API_GET_FAC_DTL then
local fac = db.facility
local mtx_sps = fac.induction_ps_tbl[1]
local units = {}
local tank_statuses = {}
for i = 1, #db.units do
local u = db.units[i]
units[i] = { u.connected, u.annunciator, u.reactor_data, u.tank_data_tbl }
for t = 1, #u.tank_ps_tbl do table.insert(tank_statuses, u.tank_ps_tbl[t].get("computed_status")) end
end
for i = 1, #fac.tank_ps_tbl do table.insert(tank_statuses, fac.tank_ps_tbl[i].get("computed_status")) end
local matrix_data = {
mtx_sps.get("eta_string"),
mtx_sps.get("avg_charge"),
mtx_sps.get("avg_inflow"),
mtx_sps.get("avg_outflow"),
mtx_sps.get("is_charging"),
mtx_sps.get("is_discharging"),
mtx_sps.get("at_max_io")
}
local data = {
fac.all_sys_ok,
fac.rtu_count,
fac.auto_scram,
fac.ascram_status,
tank_statuses,
fac.tank_data_tbl,
fac.induction_ps_tbl[1].get("computed_status") or types.IMATRIX_STATE.OFFLINE,
fac.induction_data_tbl[1],
matrix_data,
fac.sps_ps_tbl[1].get("computed_status") or types.SPS_STATE.OFFLINE,
fac.sps_data_tbl[1],
units
}
_send(CRDN_TYPE.API_GET_FAC_DTL, data)
elseif pkt.type == CRDN_TYPE.API_GET_UNIT then
if pkt.length == 1 and type(pkt.data[1]) == "number" then
local u = db.units[pkt.data[1]]
local statuses = { u.unit_ps.get("computed_status") }
for i = 1, #u.boiler_ps_tbl do table.insert(statuses, u.boiler_ps_tbl[i].get("computed_status")) end
for i = 1, #u.turbine_ps_tbl do table.insert(statuses, u.turbine_ps_tbl[i].get("computed_status")) end
for i = 1, #u.tank_ps_tbl do table.insert(statuses, u.tank_ps_tbl[i].get("computed_status")) end
if u then
local data = {
u.unit_id,
u.connected,
u.rtu_hw,
statuses,
u.a_group,
u.alarms,
u.annunciator,
@@ -275,7 +352,6 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
u.annunciator.AutoControl,
u.a_group
}
end
_send(CRDN_TYPE.API_GET_CTRL, data)
@@ -310,6 +386,47 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
}
_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)
else
log.debug(log_tag .. "handler received unsupported CRDN packet type " .. pkt.type)
end

View File

@@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder")
local threads = require("coordinator.threads")
local COORDINATOR_VERSION = "v1.5.13"
local COORDINATOR_VERSION = "v1.6.15"
local CHUNK_LOAD_DELAY_S = 30.0

View File

@@ -24,7 +24,8 @@ local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
local MQ__RENDER_CMD = {
START_MAIN_UI = 1
START_MAIN_UI = 1,
CLOSE_MAIN_UI = 2
}
local MQ__RENDER_DATA = {
@@ -81,7 +82,7 @@ function threads.thread__main(smem)
nic.connect(other_modem)
else
-- close out main UI
renderer.close_ui()
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
-- alert user to status
log_sys("awaiting comms modem reconnect...")
@@ -167,9 +168,9 @@ function threads.thread__main(smem)
-- supervisor watchdog timeout
log_comms("supervisor server timeout")
-- close connection, main UI, and stop sounder
-- close main UI, connection, and stop sounder
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
coord_comms.close()
renderer.close_ui()
sounder.stop()
else
-- a non-clock/main watchdog timer event
@@ -188,9 +189,9 @@ function threads.thread__main(smem)
if coord_comms.handle_packet(packet) then
log_comms("supervisor closed connection")
-- close connection, main UI, and stop sounder
-- close main UI, connection, and stop sounder
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
coord_comms.close()
renderer.close_ui()
sounder.stop()
end
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
@@ -302,6 +303,13 @@ function threads.thread__render(smem)
else
log_render("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
end
elseif msg.message == MQ__RENDER_CMD.CLOSE_MAIN_UI then
-- close the main UI if it has been drawn
if renderer.ui_ready() then
log_render("closing main UI...")
renderer.close_ui()
log_render("main UI closed")
end
end
elseif msg.qtype == mqueue.TYPE.DATA then
-- received data

View File

@@ -25,10 +25,9 @@ local ALIGN = core.ALIGN
---@param root Container parent
---@param x integer top left x
---@param y integer top left y
---@param data imatrix_session_db matrix data
---@param ps psil ps interface
---@param id number? matrix ID
local function new_view(root, x, y, data, ps, id)
local function new_view(root, x, y, ps, id)
local label_fg = style.theme.label_fg
local text_fg = style.theme.text_fg
local lu_col = style.lu_colors
@@ -94,6 +93,7 @@ local function new_view(root, x, y, data, ps, id)
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
@@ -105,46 +105,7 @@ local function new_view(root, x, y, data, ps, id)
local eta = TextBox{parent=rect,x=11,y=20,width=20,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=style.theme.field_box}
eta.register(ps, "eta_ms", function (eta_ms)
local str, pre = "", util.trinary(eta_ms >= 0, "Full in ", "Empty in ")
local seconds = math.abs(eta_ms) / 1000
local minutes = seconds / 60
local hours = minutes / 60
local days = hours / 24
if math.abs(eta_ms) < 1000 or (eta_ms ~= eta_ms) then
-- really small or NaN
str = "No ETA"
elseif days < 1000 then
days = math.floor(days)
hours = math.floor(hours % 24)
minutes = math.floor(minutes % 60)
seconds = math.floor(seconds % 60)
if days > 0 then
str = days .. "d"
elseif hours > 0 then
str = hours .. "h " .. minutes .. "m"
elseif minutes > 0 then
str = minutes .. "m " .. seconds .. "s"
elseif seconds > 0 then
str = seconds .. "s"
end
str = pre .. str
else
local years = math.floor(days / 365.25)
if years <= 99999999 then
str = pre .. years .. "y"
else
str = pre .. "eras"
end
end
eta.set_value(str)
end)
eta.register(ps, "eta_string", eta.set_value)
end
return new_view

View File

@@ -28,6 +28,8 @@ local function init(parent, id)
local ps = iocontrol.get_db().fp.ps
local term_w, _ = term.getSize()
-- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=s_hi_bright}
@@ -43,9 +45,9 @@ local function init(parent, id)
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,fg_bg=label_fg}
pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value)
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4}
local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,fg_bg=label_fg}
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)

View File

@@ -94,14 +94,14 @@ local function new_view(root, x, y)
main.line_break()
local auto_scram = IndicatorLight{parent=main,label="Automatic SCRAM",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local matrix_dc = IndicatorLight{parent=main,label="Matrix Disconnected",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
local matrix_flt = IndicatorLight{parent=main,label="Induction Matrix Fault",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
local matrix_fill = IndicatorLight{parent=main,label="Matrix Charge High",colors=ind_red,flash=true,period=period.BLINK_500_MS}
local unit_crit = IndicatorLight{parent=main,label="Unit Critical Alarm",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local fac_rad_h = IndicatorLight{parent=main,label="Facility Radiation High",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local gen_fault = IndicatorLight{parent=main,label="Gen. Control Fault",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
auto_scram.register(facility.ps, "auto_scram", auto_scram.update)
matrix_dc.register(facility.ps, "as_matrix_dc", matrix_dc.update)
matrix_flt.register(facility.ps, "as_matrix_fault", matrix_flt.update)
matrix_fill.register(facility.ps, "as_matrix_fill", matrix_fill.update)
unit_crit.register(facility.ps, "as_crit_alarm", unit_crit.update)
fac_rad_h.register(facility.ps, "as_radiation", fac_rad_h.update)
@@ -325,7 +325,7 @@ local function new_view(root, x, y)
TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8}
local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=ind_wht}
local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.waste.states_abbrv,value=1,min_width=6}
local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.get_waste().states_abbrv,value=1,min_width=6}
a_waste.register(unit.unit_ps, "U_AutoWaste", a_waste.update)
waste_m.register(unit.unit_ps, "U_WasteProduct", waste_m.update)
@@ -339,11 +339,11 @@ local function new_view(root, x, y)
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,x=1,y=2,fg_bg=cutout_fg_bg}
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17}
local status = StateIndicator{parent=rect,x=2,y=1,states=style.get_waste().states,value=1,min_width=17}
status.register(facility.ps, "current_waste_product", status.update)
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.brown}
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.get_waste().options,callback=process.set_process_waste,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.brown}
waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value)

View File

@@ -398,7 +398,7 @@ local function init(parent, id)
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1}
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.waste.unit_opts,callback=unit.set_waste,min_width=6}
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.get_waste().unit_opts,callback=unit.set_waste,min_width=6}
waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)

View File

@@ -2,8 +2,11 @@
-- 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")
@@ -19,6 +22,8 @@ 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
@@ -35,8 +40,8 @@ local lg_gray = style.lg_gray
---@param x integer top left x
---@param y integer top left y
---@param wide boolean whether to render wide version
---@param unit ioctl_unit unit database entry
local function make(parent, x, y, wide, unit)
---@param unit_id integer unit index
local function make(parent, x, y, wide, unit_id)
local s_field = style.theme.field_box
local text_c = style.text_colors
@@ -48,7 +53,13 @@ local function make(parent, x, y, wide, unit)
local height = 16
local v_start = 1 + ((unit.unit_id - 1) * 5)
local facility = iocontrol.get_db().facility
local unit = iocontrol.get_db().units[unit_id]
local tank_conns = facility.tank_conns
local tank_types = facility.tank_fluid_types
local v_start = 1 + ((unit.unit_id - 1) * 6)
local prv_start = 1 + ((unit.unit_id - 1) * 3)
local v_fields = { "pu", "po", "pl", "am" }
local v_names = {
@@ -80,21 +91,32 @@ local function make(parent, x, y, wide, unit)
local rc_pipes = {}
local emc_x = 42 -- emergency coolant connection x point
if unit.num_boilers > 0 then
table.insert(rc_pipes, pipe(0, 1, _wide(28, 19), 1, colors.lightBlue, true))
table.insert(rc_pipes, pipe(0, 3, _wide(28, 19), 3, colors.orange, true))
table.insert(rc_pipes, pipe(_wide(46 ,39), 1, _wide(72,58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(_wide(46,39), 3, _wide(72,58), 3, colors.white, true))
table.insert(rc_pipes, pipe(_wide(46, 39), 1, _wide(72, 58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(_wide(46, 39), 3, _wide(72, 58), 3, colors.white, true))
if unit.aux_coolant then
local em_water = facility.tank_fluid_types[facility.tank_conns[unit_id]] == COOLANT_TYPE.WATER
local offset = util.trinary(unit.has_tank and em_water, 3, 0)
table.insert(rc_pipes, pipe(_wide(51, 41) + offset, 0, _wide(51, 41) + offset, 0, colors.blue, true))
end
else
emc_x = 3
table.insert(rc_pipes, pipe(0, 1, _wide(72,58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(0, 3, _wide(72,58), 3, colors.white, true))
table.insert(rc_pipes, pipe(0, 1, _wide(72, 58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(0, 3, _wide(72, 58), 3, colors.white, true))
if unit.aux_coolant then
table.insert(rc_pipes, pipe(8, 0, 8, 0, colors.blue, true))
end
end
if unit.has_tank then
table.insert(rc_pipes, pipe(emc_x, 1, emc_x, 0, colors.blue, true, true))
local is_water = tank_types[tank_conns[unit_id]] == COOLANT_TYPE.WATER
-- emergency coolant connection x point
local emc_x = util.trinary(is_water and (unit.num_boilers > 0), 42, 3)
table.insert(rc_pipes, pipe(emc_x, 1, emc_x, 0, util.trinary(is_water, colors.blue, colors.lightBlue), true, true))
end
local prv_yo = math.max(3 - unit.num_turbines, 0)
@@ -157,12 +179,12 @@ local function make(parent, x, y, wide, unit)
pipe(_wide(22, 19), 1, _wide(49, 45), 1, colors.brown, true),
pipe(_wide(22, 19), 5, _wide(28, 24), 5, colors.brown, true),
pipe(_wide(64, 53), 1, _wide(95, 81), 1, colors.green, true),
pipe(_wide(64, 53), 1, _wide(95, 81), 1, colors.cyan, true),
pipe(_wide(48, 43), 4, _wide(71, 61), 4, colors.cyan, true),
pipe(_wide(66, 57), 4, _wide(71, 61), 8, colors.cyan, true),
pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.cyan, true),
pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.cyan, true),
pipe(_wide(48, 43), 4, _wide(71, 61), 4, colors.green, true),
pipe(_wide(66, 57), 4, _wide(71, 61), 8, colors.green, true),
pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.green, true),
pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.green, true),
pipe(_wide(108, 94), 1, _wide(132, 110), 6, waste_c, true, true),
pipe(_wide(108, 94), 4, _wide(111, 95), 1, waste_c, true, true),
@@ -210,17 +232,21 @@ local function make(parent, x, y, wide, unit)
_machine(_wide(116, 94), 6, "SPENT WASTE \x1b")
TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=style.theme.highlight_box_bright}
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=8,thin=true,fg_bg=style.theme.highlight_box_bright}
local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn}
local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c_d,label="CNT",unit="",format="%2d",value=0,width=7}
local sna_pk = DataIndicator{parent=sna_po,y=3,lu_colors=lu_c_d,label="PEAK",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="MAX",unit="mB/t",format="%8.2f",value=0,width=17}
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="IN",unit="mB/t",format="%9.2f",value=0,width=17}
TextBox{parent=sna_po,y=3,text="PEAK\x1a",width=5,fg_bg=cpair(style.theme.label_dark,colors._INHERIT)}
TextBox{parent=sna_po,text="MAX \x1a",width=5,fg_bg=cpair(style.theme.label_dark,colors._INHERIT)}
local sna_pk = DataIndicator{parent=sna_po,x=6,y=3,lu_colors=lu_c_d,label="",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max_o = DataIndicator{parent=sna_po,x=6,lu_colors=lu_c_d,label="",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max_i = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="\x1aMAX",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="\x1aIN",unit="mB/t",format="%8.2f",value=0,width=17}
sna_act.register(unit.unit_ps, "po_rate", function (r) sna_act.update(r > 0) end)
sna_cnt.register(unit.unit_ps, "sna_count", sna_cnt.update)
sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.update)
sna_max.register(unit.unit_ps, "sna_max_rate", sna_max.update)
sna_max_o.register(unit.unit_ps, "sna_max_rate", sna_max_o.update)
sna_max_i.register(unit.unit_ps, "sna_max_rate", function (r) sna_max_i.update(r * 10) end)
sna_in.register(unit.unit_ps, "sna_in", sna_in.update)
return root

View File

@@ -24,6 +24,7 @@ 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
@@ -45,8 +46,10 @@ local function init(main)
local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units
local tank_defs = facility.tank_defs
local tank_list = facility.tank_list
local tank_defs = facility.tank_defs
local tank_conns = facility.tank_conns
local tank_list = facility.tank_list
local tank_types = facility.tank_fluid_types
-- window header message
local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
@@ -56,12 +59,16 @@ local function init(main)
datetime.register(facility.ps, "date_time", datetime.set_value)
local po_pipes = {}
local water_pipes = {}
local emcool_pipes = {}
-- get the y offset for this unit index
---@param idx integer unit index
local function y_ofs(idx) return ((idx - 1) * 20) end
-- get the coolant color
---@param idx integer tank index
local function c_clr(idx) return util.trinary(tank_types[tank_conns[idx]] == COOLANT_TYPE.WATER, colors.blue, colors.lightBlue) end
-- determinte facility tank start/end from the definitions list
---@param start_idx integer start index of table iteration
---@param end_idx integer end index of table iteration
@@ -81,11 +88,13 @@ local function init(main)
for i = 1, facility.num_units do
if units[i].has_tank then
local y = y_ofs(i)
table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true))
table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true))
local color = c_clr(i)
local x = util.trinary(units[i].num_boilers == 0, 45, 84)
table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true))
table.insert(emcool_pipes, pipe(2, y, 2, y + 3, color, true))
table.insert(emcool_pipes, pipe(2, y, 21, y, color, true))
local x = util.trinary((tank_types[tank_conns[i]] == COOLANT_TYPE.SODIUM) or (units[i].num_boilers == 0), 45, 84)
table.insert(emcool_pipes, pipe(21, y, x, y + 2, color, true, true))
end
end
else
@@ -93,16 +102,17 @@ local function init(main)
for i = 1, #tank_defs do
if tank_defs[i] > 0 then
local y = y_ofs(i)
local color = c_clr(i)
if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(1, y, 21, y, colors.blue, true))
table.insert(emcool_pipes, pipe(1, y, 21, y, color, true))
else
table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true))
table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true))
table.insert(emcool_pipes, pipe(2, y, 2, y + 3, color, true))
table.insert(emcool_pipes, pipe(2, y, 21, y, color, true))
end
local x = util.trinary(units[i].num_boilers == 0, 45, 84)
table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true))
local x = util.trinary((tank_types[tank_conns[i]] == COOLANT_TYPE.SODIUM) or (units[i].num_boilers == 0), 45, 84)
table.insert(emcool_pipes, pipe(21, y, x, y + 2, color, true, true))
end
end
@@ -112,13 +122,14 @@ local function init(main)
for i = 1, #tank_defs do
local y = y_ofs(i)
if i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, c_clr(first_fdef), true))
elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, c_clr(first_fdef), true))
end
end
end
@@ -128,17 +139,19 @@ local function init(main)
for i = 1, #tank_defs do
local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 4 then
if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
end
elseif i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
end
end
end
@@ -147,12 +160,12 @@ local function init(main)
for _, a in pairs({ 1, 3 }) do
local b = a + 1
if tank_defs[a] == 2 then
table.insert(water_pipes, pipe(0, y_ofs(a), 1, y_ofs(a) + 6, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y_ofs(a), 1, y_ofs(a) + 6, c_clr(a), true))
if tank_defs[b] == 2 then
table.insert(water_pipes, pipe(0, y_ofs(b) - 13, 1, y_ofs(b), colors.blue, true))
table.insert(emcool_pipes, pipe(0, y_ofs(b) - 13, 1, y_ofs(b), c_clr(a), true))
end
elseif tank_defs[b] == 2 then
table.insert(water_pipes, pipe(0, y_ofs(b), 1, y_ofs(b) + 6, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y_ofs(b), 1, y_ofs(b) + 6, c_clr(b), true))
end
end
elseif facility.tank_mode == 4 then
@@ -161,17 +174,19 @@ local function init(main)
for i = 1, #tank_defs do
local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 1 then
if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
end
elseif i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
end
end
end
@@ -181,17 +196,19 @@ local function init(main)
for i = 1, #tank_defs do
local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 3 or i == 4 then
if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
end
elseif i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
end
end
end
@@ -201,17 +218,19 @@ local function init(main)
for i = 1, #tank_defs do
local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 1 or i == 4 then
if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
end
elseif i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
end
end
end
@@ -221,17 +240,19 @@ local function init(main)
for i = 1, #tank_defs do
local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 1 or i == 2 then
if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
end
elseif i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
end
end
end
@@ -239,15 +260,15 @@ local function init(main)
end
local flow_x = 3
if #water_pipes > 0 then
if #emcool_pipes > 0 then
flow_x = 25
PipeNetwork{parent=main,x=2,y=3,pipes=water_pipes,bg=style.theme.bg}
PipeNetwork{parent=main,x=2,y=3,pipes=emcool_pipes,bg=style.theme.bg}
end
for i = 1, facility.num_units do
local y_offset = y_ofs(i)
unit_flow(main, flow_x, 5 + y_offset, #water_pipes == 0, units[i])
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.cyan, true, true))
unit_flow(main, flow_x, 5 + y_offset, #emcool_pipes == 0, i)
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.green, true, true))
util.nop()
end
@@ -265,7 +286,7 @@ local function init(main)
TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=text_col,width=2}
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", i * 5),colors=style.ind_grn}
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", (i * 6) - 1),colors=style.ind_grn}
local open = IndicatorLight{parent=main,x=9,y=vy+2,label="OPEN",colors=style.ind_wht}
conn.register(units[i].unit_ps, "V_emc_conn", conn.update)
@@ -273,6 +294,35 @@ local function init(main)
end
end
------------------------------
-- auxiliary coolant valves --
------------------------------
for i = 1, facility.num_units do
if units[i].aux_coolant then
local vx
local vy = 3 + y_ofs(i)
if #emcool_pipes == 0 then
vx = util.trinary(units[i].num_boilers == 0, 36, 79)
else
local em_water = tank_types[tank_conns[i]] == COOLANT_TYPE.WATER
vx = util.trinary(units[i].num_boilers == 0, 58, util.trinary(units[i].has_tank and em_water, 94, 91))
end
PipeNetwork{parent=main,x=vx-6,y=vy,pipes={pipe(0,1,9,0,colors.blue,true)},bg=style.theme.bg}
TextBox{parent=main,x=vx,y=vy,text="\x10\x11",fg_bg=text_col,width=2}
TextBox{parent=main,x=vx+5,y=vy,text="\x1b",fg_bg=cpair(colors.blue,text_col.bkg),width=1}
local conn = IndicatorLight{parent=main,x=vx-3,y=vy+1,label=util.sprintf("PV%02d-AUX", i * 6),colors=style.ind_grn}
local open = IndicatorLight{parent=main,x=vx-3,y=vy+2,label="OPEN",colors=style.ind_wht}
conn.register(units[i].unit_ps, "V_aux_conn", conn.update)
open.register(units[i].unit_ps, "V_aux_state", open.update)
end
end
-------------------
-- dynamic tanks --
-------------------
@@ -301,8 +351,10 @@ local function init(main)
local tank_pcnt = DataIndicator{parent=tank_box,x=10,y=3,label="",format="%5.2f",value=100,unit="%",lu_colors=lu_col,width=8,fg_bg=text_col}
local tank_amnt = DataIndicator{parent=tank_box,x=2,label="",format="%13d",value=0,commas=true,unit="mB",lu_colors=lu_col,width=16,fg_bg=s_field}
TextBox{parent=tank_box,x=2,y=6,text="Water Level",width=11,fg_bg=style.label}
local level = HorizontalBar{parent=tank_box,x=2,y=7,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=16}
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}

View File

@@ -39,6 +39,8 @@ local led_grn = style.led_grn
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}
@@ -61,7 +63,7 @@ local function init(panel, num_units)
local modem = LED{parent=system,label="MODEM",colors=led_grn}
if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,style.fp_ind_bkg}}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.fp_ind_bkg}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(ps, "link_state", network.update)
else
@@ -131,9 +133,9 @@ local function init(panel, num_units)
-- about footer
--
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
local about = Div{parent=main_page,width=15,height=2,y=term_h-3,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@@ -145,7 +147,7 @@ local function init(panel, num_units)
-- API page
local api_page = Div{parent=page_div,x=1,y=1,hidden=true}
local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local 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

View File

@@ -88,7 +88,7 @@ local function init(main)
util.nop()
imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1])
imatrix(main, 131, cnc_bottom_align_start, facility.induction_ps_tbl[1])
end
return init

View File

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

View File

@@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
local core = {}
core.version = "2.4.5"
core.version = "2.4.7"
core.flasher = flasher
core.events = events

View File

@@ -2,7 +2,6 @@
-- Generic Graphics Element
--
-- local log = require("scada-common.log")
local util = require("scada-common.util")
local core = require("graphics.core")
@@ -476,10 +475,7 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
if args.parent ~= nil then
-- remove self from parent
-- log.debug("removing " .. self.id .. " from parent")
args.parent.__remove_child(self.id)
else
-- log.debug("no parent for " .. self.id .. " on delete attempt")
end
end
@@ -502,9 +498,12 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
self.next_id = self.next_id + 1
end
table.insert(protected.children, child)
-- see #539 on GitHub
-- using #protected.children after inserting may give the wrong index, since if it inserts in a hole that completes the list then
-- the length will jump up to the full length of the list, possibly making two map entries point to the same child
protected.child_id_map[id] = #protected.children + 1
protected.child_id_map[id] = #protected.children
table.insert(protected.children, child)
return id
end

View File

@@ -1,6 +1,5 @@
-- Scroll-able List Box Display Graphics Element
-- local log = require("scada-common.log")
local tcd = require("scada-common.tcd")
local core = require("graphics.core")
@@ -153,7 +152,6 @@ return function (args)
next_y = next_y + item.h + item_pad
item.e.reposition(1, item.y)
item.e.show()
-- log.debug("iterated " .. item.e.get_id())
end
content_height = next_y
@@ -212,7 +210,6 @@ return function (args)
---@param child graphics_element child element
function e.on_added(id, child)
table.insert(list, { id = id, e = child, y = 0, h = child.get_height() })
-- log.debug("added child " .. id .. " into slot " .. #list)
update_positions()
end
@@ -222,12 +219,10 @@ return function (args)
for idx, elem in ipairs(list) do
if elem.id == id then
table.remove(list, idx)
-- log.debug("removed child " .. id .. " from slot " .. idx)
update_positions()
return
end
end
-- log.debug("failed to remove child " .. id)
end
-- handle focus

View File

@@ -6,6 +6,7 @@ local element = require("graphics.element")
---@class checkbox_args
---@field label string checkbox text
---@field box_fg_bg cpair colors for checkbox
---@field disable_fg_bg? cpair text colors when disabled
---@field default? boolean default value
---@field callback? function function to call on press
---@field parent graphics_element
@@ -35,20 +36,27 @@ return function (args)
local function draw()
e.w_set_cur(1, 1)
local fgd, bkg = args.box_fg_bg.fgd, args.box_fg_bg.bkg
if (not e.enabled) and type(args.disable_fg_bg) == "table" then
fgd = args.disable_fg_bg.bkg
bkg = args.disable_fg_bg.fgd
end
if e.value then
-- show as selected
e.w_set_fgd(args.box_fg_bg.bkg)
e.w_set_bkg(args.box_fg_bg.fgd)
e.w_set_fgd(bkg)
e.w_set_bkg(fgd)
e.w_write("\x88")
e.w_set_fgd(args.box_fg_bg.fgd)
e.w_set_fgd(fgd)
e.w_set_bkg(e.fg_bg.bkg)
e.w_write("\x95")
else
-- show as unselected
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(args.box_fg_bg.bkg)
e.w_set_bkg(bkg)
e.w_write("\x88")
e.w_set_fgd(args.box_fg_bg.bkg)
e.w_set_fgd(bkg)
e.w_set_bkg(e.fg_bg.bkg)
e.w_write("\x95")
end
@@ -57,16 +65,18 @@ return function (args)
-- write label text
local function draw_label()
if e.enabled and e.is_focused() then
e.w_set_cur(3, 1)
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(e.fg_bg.fgd)
e.w_write(args.label)
elseif (not e.enabled) and type(args.disable_fg_bg) == "table" then
e.w_set_fgd(args.disable_fg_bg.fgd)
e.w_set_bkg(args.disable_fg_bg.bkg)
else
e.w_set_cur(3, 1)
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
e.w_write(args.label)
end
e.w_set_cur(3, 1)
e.w_write(args.label)
end
-- handle mouse interaction
@@ -98,20 +108,20 @@ return function (args)
draw()
end
-- handle focus
e.on_focused = draw_label
e.on_unfocused = draw_label
-- handle enable
e.on_enabled = draw_label
e.on_disabled = draw_label
-- element redraw
function e.redraw()
draw()
draw_label()
end
-- 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)

View File

@@ -53,25 +53,44 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
--#region Pocket UI
local ui_c_1 = Div{parent=ui_cfg,x=2,y=4,width=24}
local ui_c_2 = Div{parent=ui_cfg,x=2,y=4,width=24}
local ui_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={ui_c_1,ui_c_2}}
TextBox{parent=ui_cfg,x=1,y=2,text=" Pocket UI",fg_bg=cpair(colors.black,colors.lime)}
TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize units below."}
TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize UI options below."}
TextBox{parent=ui_c_1,x=1,y=4,text="Temperature Scale"}
local temp_scale = RadioButton{parent=ui_c_1,x=1,y=5,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=ui_c_1,y=4,text="Po/Pu Pellet Color"}
TextBox{parent=ui_c_1,x=20,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
local pellet_color = RadioButton{parent=ui_c_1,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=ui_c_1,x=1,y=10,text="Energy Scale"}
local energy_scale = RadioButton{parent=ui_c_1,x=1,y=11,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=ui_c_1,y=8,height=4,text="In Mekanism 10.4 and later, pellet colors now match gas colors (Cyan Pu/Green Po).",fg_bg=g_lg_fg_bg}
local function submit_ui_opts()
tmp_cfg.GreenPuPellet = pellet_color.get_value() == 1
ui_pane.set_value(2)
end
PushButton{parent=ui_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=ui_c_1,x=19,y=15,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=ui_c_2,x=1,y=1,height=3,text="You may customize units below."}
TextBox{parent=ui_c_2,x=1,y=4,text="Temperature Scale"}
local temp_scale = RadioButton{parent=ui_c_2,x=1,y=5,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=ui_c_2,x=1,y=10,text="Energy Scale"}
local energy_scale = RadioButton{parent=ui_c_2,x=1,y=11,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
local function submit_ui_units()
tmp_cfg.TempScale = temp_scale.get_value()
tmp_cfg.EnergyScale = energy_scale.get_value()
main_pane.set_value(3)
end
PushButton{parent=ui_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=ui_c_1,x=19,y=15,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=ui_c_2,x=1,y=15,text="\x1b Back",callback=function()ui_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=ui_c_2,x=19,y=15,text="Next \x1a",callback=submit_ui_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
@@ -266,6 +285,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
load_settings(settings_cfg, true)
load_settings(ini_cfg)
try_set(pellet_color, ini_cfg.GreenPuPellet)
try_set(temp_scale, ini_cfg.TempScale)
try_set(energy_scale, ini_cfg.EnergyScale)
try_set(svr_chan, ini_cfg.SVR_Channel)
@@ -374,6 +394,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
val = string.rep("*", string.len(val))
elseif f[1] == "LogMode" then
val = tri(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "GreenPuPellet" then
val = tri(raw, "Green Pu/Cyan Po", "Cyan Pu/Green Po")
elseif f[1] == "TempScale" then
val = util.strval(types.TEMP_SCALE_NAMES[raw])
elseif f[1] == "EnergyScale" then
@@ -385,7 +407,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if string.len(val) > val_max_w then
if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end

View File

@@ -29,7 +29,8 @@ local CENTER = core.ALIGN.CENTER
-- changes to the config data/format to let the user know
local changes = {
{ "v0.9.2", { "Added temperature scale options" } },
{ "v0.11.3", { "Added energy scale options" } }
{ "v0.11.3", { "Added energy scale options" } },
{ "v0.13.2", { "Added option for Po/Pu pellet green/cyan pairing" } }
}
---@class pkt_configurator
@@ -50,6 +51,7 @@ style.btn_dis_fg_bg = cpair(colors.lightGray, colors.white)
---@class _pkt_cfg_tool_ctl
local tool_ctl = {
launch_startup = false,
ask_config = false,
has_config = false,
viewing_config = false,
@@ -63,6 +65,7 @@ local tool_ctl = {
---@class pkt_config
local tmp_cfg = {
GreenPuPellet = false,
TempScale = 1, ---@type TEMP_SCALE
EnergyScale = 1, ---@type ENERGY_SCALE
SVR_Channel = nil, ---@type integer
@@ -83,6 +86,7 @@ local settings_cfg = {}
-- all settings fields, their nice names, and their default values
local fields = {
{ "GreenPuPellet", "Pellet Colors", false },
{ "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN },
{ "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE },
{ "SVR_Channel", "SVR Channel", 16240 },
@@ -162,8 +166,16 @@ local function config_view(display)
if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end
local function startup()
tool_ctl.launch_startup = true
exit()
end
PushButton{parent=main_page,x=2,y=18,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
PushButton{parent=main_page,x=14,y=18,min_width=12,text="Change Log",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local start_btn = PushButton{parent=main_page,x=17,y=18,min_width=9,text="Startup",callback=startup,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=main_page,x=2,y=y_start+4,min_width=12,text="Change Log",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
if tool_ctl.ask_config then start_btn.disable() end
--#endregion
@@ -254,7 +266,7 @@ function configurator.configure(ask_config)
println("configurator error: " .. error)
end
return status, error
return status, error, tool_ctl.launch_startup
end
return configurator

View File

@@ -2,11 +2,11 @@
-- I/O Control for Pocket Integration with Supervisor & Coordinator
--
local const = require("scada-common.constants")
local psil = require("scada-common.psil")
local types = require("scada-common.types")
local util = require("scada-common.util")
local iorx = require("pocket.iorx")
local process = require("pocket.process")
local ALARM = types.ALARM
@@ -50,6 +50,8 @@ function iocontrol.init_core(pkt_comms, nav, cfg)
comms = pkt_comms
config = cfg
iocontrol.rx = iorx(io)
io.nav = nav
---@class pocket_ioctl_diag
@@ -92,9 +94,11 @@ function iocontrol.init_core(pkt_comms, nav, cfg)
-- API access
---@class pocket_ioctl_api
io.api = {
get_fac = function () comms.api__get_facility() end,
get_unit = function (unit) comms.api__get_unit(unit) end,
get_ctrl = function () comms.api__get_control() end,
get_proc = function () comms.api__get_process() end
get_proc = function () comms.api__get_process() end,
get_waste = function () comms.api__get_waste() end
}
end
@@ -135,6 +139,9 @@ function iocontrol.init_fac(conf)
num_units = conf.num_units,
tank_mode = conf.cooling.fac_tank_mode,
tank_defs = conf.cooling.fac_tank_defs,
tank_list = conf.cooling.fac_tank_list,
tank_conns = conf.cooling.fac_tank_conns,
tank_fluid_types = conf.cooling.tank_fluid_types,
all_sys_ok = false,
rtu_count = 0,
@@ -148,7 +155,7 @@ function iocontrol.init_fac(conf)
auto_scram = false,
---@type ascram_status
ascram_status = {
matrix_dc = false,
matrix_fault = false,
matrix_fill = false,
crit_alarm = false,
radiation = false,
@@ -158,6 +165,8 @@ function iocontrol.init_fac(conf)
---@type WASTE_PRODUCT
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
auto_pu_fallback_active = false,
auto_sps_disabled = false,
waste_stats = { 0, 0, 0, 0, 0, 0 }, -- waste in, pu, po, po pellets, am, spent waste
radiation = types.new_zero_radiation_reading(),
@@ -184,6 +193,14 @@ function iocontrol.init_fac(conf)
table.insert(io.facility.sps_ps_tbl, psil.create())
table.insert(io.facility.sps_data_tbl, {})
-- create facility tank tables
for i = 1, #io.facility.tank_list do
if io.facility.tank_list[i] == 2 then
table.insert(io.facility.tank_ps_tbl, psil.create())
table.insert(io.facility.tank_data_tbl, {})
end
end
-- create unit data structures
io.units = {} ---@type pioctl_unit[]
for i = 1, conf.num_units do
@@ -191,8 +208,6 @@ function iocontrol.init_fac(conf)
local entry = {
unit_id = i,
connected = false,
---@type { boilers: { connected: boolean, faulted: boolean }[], turbines: { connected: boolean, faulted: boolean }[] }
rtu_hw = {},
num_boilers = 0,
num_turbines = 0,
@@ -217,6 +232,7 @@ function iocontrol.init_fac(conf)
last_rate_change_ms = 0,
turbine_flow_stable = false,
waste_stats = { 0, 0, 0 }, -- plutonium, polonium, po pellets
-- auto control group
a_group = types.AUTO_GROUP.MANUAL,
@@ -235,6 +251,7 @@ function iocontrol.init_fac(conf)
---@type { [ALARM]: ALARM_STATE }
alarms = { ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE },
---@diagnostic disable-next-line: missing-fields
annunciator = {}, ---@type annunciator
unit_ps = psil.create(),
@@ -343,583 +360,6 @@ function iocontrol.report_crd_tt(trip_time)
io.ps.publish("crd_conn_quality", state)
end
-- populate facility data from API_GET_FAC
---@param data table
---@return boolean valid
function iocontrol.record_facility_data(data)
local valid = true
local fac = io.facility
fac.all_sys_ok = data[1]
fac.rtu_count = data[2]
fac.radiation = data[3]
-- auto control
if type(data[4]) == "table" and #data[4] == 4 then
fac.auto_ready = data[4][1]
fac.auto_active = data[4][2]
fac.auto_ramping = data[4][3]
fac.auto_saturated = data[4][4]
end
-- waste
if type(data[5]) == "table" and #data[5] == 2 then
fac.auto_current_waste_product = data[5][1]
fac.auto_pu_fallback_active = data[5][2]
end
fac.num_tanks = data[6]
fac.has_imatrix = data[7]
fac.has_sps = data[8]
return valid
end
local function tripped(state) return state == ALARM_STATE.TRIPPED or state == ALARM_STATE.ACKED end
local function _record_multiblock_status(faulted, data, ps)
ps.publish("formed", data.formed)
ps.publish("faulted", faulted)
for key, val in pairs(data.state) do ps.publish(key, val) end
for key, val in pairs(data.tanks) do ps.publish(key, val) end
end
-- update unit status data from API_GET_UNIT
---@param data table
function iocontrol.record_unit_data(data)
local unit = io.units[data[1]]
unit.connected = data[2]
unit.rtu_hw = data[3]
unit.a_group = data[4]
unit.alarms = data[5]
unit.unit_ps.publish("auto_group_id", unit.a_group)
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
--#region Annunciator
unit.annunciator = data[6]
local rcs_disconn, rcs_warn, rcs_hazard = false, false, false
for key, val in pairs(unit.annunciator) do
if key == "BoilerOnline" or key == "TurbineOnline" then
local every = true
-- split up online arrays
for id = 1, #val do
every = every and val[id]
if key == "BoilerOnline" then
unit.boiler_ps_tbl[id].publish(key, val[id])
else
unit.turbine_ps_tbl[id].publish(key, val[id])
end
end
if not every then rcs_disconn = true end
unit.unit_ps.publish("U_" .. key, every)
elseif key == "HeatingRateLow" or key == "WaterLevelLow" then
-- split up array for all boilers
local any = false
for id = 1, #val do
any = any or val[id]
unit.boiler_ps_tbl[id].publish(key, val[id])
end
if key == "HeatingRateLow" and any then
rcs_warn = true
elseif key == "WaterLevelLow" and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
elseif key == "SteamDumpOpen" or key == "TurbineOverSpeed" or key == "GeneratorTrip" or key == "TurbineTrip" then
-- split up array for all turbines
local any = false
for id = 1, #val do
any = any or val[id]
unit.turbine_ps_tbl[id].publish(key, val[id])
end
if key == "GeneratorTrip" and any then
rcs_warn = true
elseif (key == "TurbineOverSpeed" or key == "TurbineTrip") and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
else
-- non-table fields
unit.unit_ps.publish(key, val)
end
end
local anc = unit.annunciator
rcs_hazard = rcs_hazard or anc.RCPTrip
rcs_warn = rcs_warn or anc.RCSFlowLow or anc.CoolantLevelLow or anc.RCSFault or anc.MaxWaterReturnFeed or
anc.CoolantFeedMismatch or anc.BoilRateMismatch or anc.SteamFeedMismatch
local rcs_status = 4
if rcs_hazard then
rcs_status = 2
elseif rcs_warn then
rcs_status = 3
elseif rcs_disconn then
rcs_status = 1
end
unit.unit_ps.publish("U_RCS", rcs_status)
--#endregion
--#region Reactor Data
unit.reactor_data = data[7]
local control_status = 1
local reactor_status = 1
local reactor_state = 1
local rps_status = 1
if unit.connected then
-- update RPS status
if unit.reactor_data.rps_tripped then
control_status = 2
if unit.reactor_data.rps_trip_cause == "manual" then
reactor_state = 4 -- disabled
rps_status = 3
else
reactor_state = 6 -- SCRAM
rps_status = 2
end
else
rps_status = 4
reactor_state = 4
end
-- update reactor/control status
if unit.reactor_data.mek_status.status then
reactor_status = 4
reactor_state = 5 -- running
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
else
if unit.reactor_data.no_reactor then
reactor_status = 2
reactor_state = 3 -- faulted
elseif not unit.reactor_data.formed then
reactor_status = 3
reactor_state = 2 -- not formed
elseif unit.reactor_data.rps_status.force_dis then
reactor_status = 3
reactor_state = 7 -- force disabled
else
reactor_status = 4
end
end
for key, val in pairs(unit.reactor_data) do
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
unit.unit_ps.publish(key, val)
end
end
for key, val in pairs(unit.reactor_data.rps_status) do
unit.unit_ps.publish(key, val)
end
for key, val in pairs(unit.reactor_data.mek_struct) do
unit.unit_ps.publish(key, val)
end
for key, val in pairs(unit.reactor_data.mek_status) do
unit.unit_ps.publish(key, val)
end
end
unit.unit_ps.publish("U_ControlStatus", control_status)
unit.unit_ps.publish("U_ReactorStatus", reactor_status)
unit.unit_ps.publish("U_ReactorStateStatus", reactor_state)
unit.unit_ps.publish("U_RPS", rps_status)
--#endregion
--#region RTU Devices
unit.boiler_data_tbl = data[8]
for id = 1, #unit.boiler_data_tbl do
local boiler = unit.boiler_data_tbl[id]
local ps = unit.boiler_ps_tbl[id]
local boiler_status = 1
local computed_status = 1
if unit.rtu_hw.boilers[id].connected then
if unit.rtu_hw.boilers[id].faulted then
boiler_status = 3
computed_status = 3
elseif boiler.formed then
boiler_status = 4
if boiler.state.boil_rate > 0 then
computed_status = 5
else
computed_status = 4
end
else
boiler_status = 2
computed_status = 2
end
_record_multiblock_status(unit.rtu_hw.boilers[id].faulted, boiler, ps)
end
ps.publish("BoilerStatus", boiler_status)
ps.publish("BoilerStateStatus", computed_status)
end
unit.turbine_data_tbl = data[9]
for id = 1, #unit.turbine_data_tbl do
local turbine = unit.turbine_data_tbl[id]
local ps = unit.turbine_ps_tbl[id]
local turbine_status = 1
local computed_status = 1
if unit.rtu_hw.turbines[id].connected then
if unit.rtu_hw.turbines[id].faulted then
turbine_status = 3
computed_status = 3
elseif turbine.formed then
turbine_status = 4
if turbine.tanks.energy_fill >= 0.99 then
computed_status = 6
elseif turbine.state.flow_rate < 100 then
computed_status = 4
else
computed_status = 5
end
else
turbine_status = 2
computed_status = 2
end
_record_multiblock_status(unit.rtu_hw.turbines[id].faulted, turbine, ps)
end
ps.publish("TurbineStatus", turbine_status)
ps.publish("TurbineStateStatus", computed_status)
end
unit.tank_data_tbl = data[10]
unit.last_rate_change_ms = data[11]
unit.turbine_flow_stable = data[12]
--#endregion
--#region Status Information Display
local ecam = {} -- aviation reference :)
-- local function red(text) return { text = text, color = colors.red } end
local function white(text) return { text = text, color = colors.white } end
local function blue(text) return { text = text, color = colors.blue } end
-- if unit.reactor_data.rps_status then
-- for k, v in pairs(unit.alarms) do
-- unit.alarms[k] = ALARM_STATE.TRIPPED
-- end
-- end
if tripped(unit.alarms[ALARM.ContainmentBreach]) then
local items = { white("REACTOR MELTDOWN"), blue("DON HAZMAT SUIT") }
table.insert(ecam, { color = colors.red, text = "CONTAINMENT BREACH", help = "ContainmentBreach", items = items })
end
if tripped(unit.alarms[ALARM.ContainmentRadiation]) then
local items = {
white("RADIATION DETECTED"),
blue("DON HAZMAT SUIT"),
blue("RESOLVE LEAK"),
blue("AWAIT SAFE LEVELS")
}
table.insert(ecam, { color = colors.red, text = "RADIATION LEAK", help = "ContainmentRadiation", items = items })
end
if tripped(unit.alarms[ALARM.CriticalDamage]) then
local items = { white("MELTDOWN IMMINENT"), blue("EVACUATE") }
table.insert(ecam, { color = colors.red, text = "RCT DAMAGE CRITICAL", help = "CriticalDamage", items = items })
end
if tripped(unit.alarms[ALARM.ReactorLost]) then
local items = { white("REACTOR OFF-LINE"), blue("CHECK PLC") }
table.insert(ecam, { color = colors.red, text = "REACTOR CONN LOST", help = "ReactorLost", items = items })
end
if tripped(unit.alarms[ALARM.ReactorDamage]) then
local items = { white("REACTOR DAMAGED"), blue("CHECK RCS"), blue("AWAIT DMG REDUCED") }
table.insert(ecam, { color = colors.red, text = "REACTOR DAMAGE", help = "ReactorDamage", items = items })
end
if tripped(unit.alarms[ALARM.ReactorOverTemp]) then
local items = { white("DAMAGING TEMP"), blue("CHECK RCS"), blue("AWAIT COOLDOWN") }
table.insert(ecam, { color = colors.red, text = "REACTOR OVER TEMP", help = "ReactorOverTemp", items = items })
end
if tripped(unit.alarms[ALARM.ReactorHighTemp]) then
local items = { white("OVER EXPECTED TEMP"), blue("CHECK RCS") }
table.insert(ecam, { color = colors.yellow, text = "REACTOR HIGH TEMP", help = "ReactorHighTemp", items = items})
end
if tripped(unit.alarms[ALARM.ReactorWasteLeak]) then
local items = { white("AT WASTE CAPACITY"), blue("CHECK WASTE OUTPUT"), blue("KEEP RCT DISABLED") }
table.insert(ecam, { color = colors.red, text = "REACTOR WASTE LEAK", help = "ReactorWasteLeak", items = items})
end
if tripped(unit.alarms[ALARM.ReactorHighWaste]) then
local items = { blue("CHECK WASTE OUTPUT") }
table.insert(ecam, { color = colors.yellow, text = "REACTOR WASTE HIGH", help = "ReactorHighWaste", items = items})
end
if tripped(unit.alarms[ALARM.RPSTransient]) then
local items = {}
local stat = unit.reactor_data.rps_status
-- for k, _ in pairs(stat) do stat[k] = true end
local function insert(cond, key, text, color) if cond[key] then table.insert(items, { text = text, help = key, color = color }) end end
table.insert(items, white("REACTOR SCRAMMED"))
insert(stat, "high_dmg", "HIGH DAMAGE", colors.red)
insert(stat, "high_temp", "HIGH TEMPERATURE", colors.red)
insert(stat, "low_cool", "CRIT LOW COOLANT")
insert(stat, "ex_waste", "EXCESS WASTE")
insert(stat, "ex_hcool", "EXCESS HEATED COOL")
insert(stat, "no_fuel", "NO FUEL")
insert(stat, "fault", "HARDWARE FAULT")
insert(stat, "timeout", "SUPERVISOR DISCONN")
insert(stat, "manual", "MANUAL SCRAM", colors.white)
insert(stat, "automatic", "AUTOMATIC SCRAM")
insert(stat, "sys_fail", "NOT FORMED", colors.red)
insert(stat, "force_dis", "FORCE DISABLED", colors.red)
table.insert(items, blue("RESOLVE PROBLEM"))
table.insert(items, blue("RESET RPS"))
table.insert(ecam, { color = colors.yellow, text = "RPS TRANSIENT", help = "RPSTransient", items = items})
end
if tripped(unit.alarms[ALARM.RCSTransient]) then
local items = {}
local annunc = unit.annunciator
-- for k, v in pairs(annunc) do
-- if type(v) == "boolean" then annunc[k] = true end
-- if type(v) == "table" then
-- for a, _ in pairs(v) do
-- v[a] = true
-- end
-- end
-- end
local function insert(cond, key, text, color)
if cond == true or (type(cond) == "table" and cond[key]) then table.insert(items, { text = text, help = key, color = color }) end
end
table.insert(items, white("COOLANT PROBLEM"))
insert(annunc, "RCPTrip", "RCP TRIP", colors.red)
insert(annunc, "CoolantLevelLow", "LOW COOLANT")
if unit.num_boilers == 0 then
if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then
insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH")
end
if unit.turbine_flow_stable then
insert(annunc, "RCSFlowLow", "RCS FLOW LOW")
insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH")
insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH")
end
else
if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then
insert(annunc, "RCSFlowLow", "RCS FLOW LOW")
insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH")
insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH")
end
if unit.turbine_flow_stable then
insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH")
end
end
insert(annunc, "MaxWaterReturnFeed", "MAX WTR RTRN FEED")
for k, v in ipairs(annunc.WaterLevelLow) do insert(v, "WaterLevelLow", "BOILER " .. k .. " WTR LOW", colors.red) end
for k, v in ipairs(annunc.HeatingRateLow) do insert(v, "HeatingRateLow", "BOILER " .. k .. " HEAT RATE") end
for k, v in ipairs(annunc.TurbineOverSpeed) do insert(v, "TurbineOverSpeed", "TURBINE " .. k .. " OVERSPD", colors.red) end
for k, v in ipairs(annunc.GeneratorTrip) do insert(v, "GeneratorTrip", "TURBINE " .. k .. " GEN TRIP") end
table.insert(items, blue("CHECK COOLING SYS"))
table.insert(ecam, { color = colors.yellow, text = "RCS TRANSIENT", help = "RCSTransient", items = items})
end
if tripped(unit.alarms[ALARM.TurbineTrip]) then
local items = {}
for k, v in ipairs(unit.annunciator.TurbineTrip) do
if v then table.insert(items, { text = "TURBINE " .. k .. " TRIP", help = "TurbineTrip" }) end
end
table.insert(items, blue("CHECK ENERGY OUT"))
table.insert(ecam, { color = colors.red, text = "TURBINE TRIP", help = "TurbineTripAlarm", items = items})
end
if not (tripped(unit.alarms[ALARM.ReactorLost]) or unit.connected) then
local items = { blue("CHECK PLC") }
table.insert(ecam, { color = colors.yellow, text = "REACTOR OFF-LINE", items = items })
end
for k, v in ipairs(unit.annunciator.BoilerOnline) do
if not v then
local items = { blue("CHECK RTU") }
table.insert(ecam, { color = colors.yellow, text = "BOILER " .. k .. " OFF-LINE", items = items})
end
end
for k, v in ipairs(unit.annunciator.TurbineOnline) do
if not v then
local items = { blue("CHECK RTU") }
table.insert(ecam, { color = colors.yellow, text = "TURBINE " .. k .. " OFF-LINE", items = items})
end
end
-- if no alarms, put some basic status messages in
if #ecam == 0 then
table.insert(ecam, { color = colors.green, text = "REACTOR " .. util.trinary(unit.reactor_data.mek_status.status, "NOMINAL", "IDLE"), items = {}})
local plural = util.trinary(unit.num_turbines > 1, "S", "")
table.insert(ecam, { color = colors.green, text = "TURBINE" .. plural .. util.trinary(unit.turbine_flow_stable, " STABLE", " STABILIZING"), items = {}})
end
unit.unit_ps.publish("U_ECAM", textutils.serialize(ecam))
--#endregion
end
-- update control app with unit data from API_GET_CTRL
---@param data table
function iocontrol.record_control_data(data)
for u_id = 1, #data do
local unit = io.units[u_id]
local u_data = data[u_id]
unit.connected = u_data[1]
unit.reactor_data.rps_tripped = u_data[2]
unit.unit_ps.publish("rps_tripped", u_data[2])
unit.reactor_data.mek_status.status = u_data[3]
unit.unit_ps.publish("status", u_data[3])
unit.reactor_data.mek_status.temp = u_data[4]
unit.unit_ps.publish("temp", u_data[4])
unit.reactor_data.mek_status.burn_rate = u_data[5]
unit.unit_ps.publish("burn_rate", u_data[5])
unit.reactor_data.mek_status.act_burn_rate = u_data[6]
unit.unit_ps.publish("act_burn_rate", u_data[6])
unit.reactor_data.mek_struct.max_burn = u_data[7]
unit.unit_ps.publish("max_burn", u_data[7])
unit.annunciator.AutoControl = u_data[8]
unit.unit_ps.publish("AutoControl", u_data[8])
unit.a_group = u_data[9]
unit.unit_ps.publish("auto_group_id", unit.a_group)
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
local control_status = 1
if unit.connected then
if unit.reactor_data.rps_tripped then
control_status = 2
end
if unit.reactor_data.mek_status.status then
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
end
end
unit.unit_ps.publish("U_ControlStatus", control_status)
end
end
-- update process app with unit data from API_GET_PROC
---@param data table
function iocontrol.record_process_data(data)
-- get unit data
for u_id = 1, #io.units do
local unit = io.units[u_id]
local u_data = data[u_id]
unit.reactor_data.mek_status.status = u_data[1]
unit.reactor_data.mek_struct.max_burn = u_data[2]
unit.annunciator.AutoControl = u_data[6]
unit.a_group = u_data[7]
unit.unit_ps.publish("status", u_data[1])
unit.unit_ps.publish("max_burn", u_data[2])
unit.unit_ps.publish("burn_limit", u_data[3])
unit.unit_ps.publish("U_AutoReady", u_data[4])
unit.unit_ps.publish("U_AutoDegraded", u_data[5])
unit.unit_ps.publish("AutoControl", u_data[6])
unit.unit_ps.publish("auto_group_id", unit.a_group)
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
end
-- get facility data
local fac = io.facility
local f_data = data[#io.units + 1]
fac.status_lines = f_data[1]
fac.auto_ready = f_data[2][1]
fac.auto_active = f_data[2][2]
fac.auto_ramping = f_data[2][3]
fac.auto_saturated = f_data[2][4]
fac.auto_scram = f_data[3]
fac.ascram_status = f_data[4]
fac.ps.publish("status_line_1", fac.status_lines[1])
fac.ps.publish("status_line_2", fac.status_lines[2])
fac.ps.publish("auto_ready", fac.auto_ready)
fac.ps.publish("auto_active", fac.auto_active)
fac.ps.publish("auto_ramping", fac.auto_ramping)
fac.ps.publish("auto_saturated", fac.auto_saturated)
fac.ps.publish("auto_scram", fac.auto_scram)
fac.ps.publish("as_matrix_dc", fac.ascram_status.matrix_dc)
fac.ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill)
fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm)
fac.ps.publish("as_radiation", fac.ascram_status.radiation)
fac.ps.publish("as_gen_fault", fac.ascram_status.gen_fault)
fac.ps.publish("process_mode", f_data[5][1])
fac.ps.publish("process_burn_target", f_data[5][2])
fac.ps.publish("process_charge_target", f_data[5][3])
fac.ps.publish("process_gen_target", f_data[5][4])
end
-- get the IO controller database
function iocontrol.get_db() return io end

825
pocket/iorx.lua Normal file
View File

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

View File

@@ -29,6 +29,7 @@ pocket.MQ__RENDER_CMD = MQ__RENDER_CMD
pocket.MQ__RENDER_DATA = MQ__RENDER_DATA
---@type pkt_config
---@diagnostic disable-next-line: missing-fields
local config = {}
pocket.config = config
@@ -37,6 +38,7 @@ pocket.config = config
function pocket.load_config()
if not settings.load("/pocket.settings") then return false end
config.GreenPuPellet = settings.get("GreenPuPellet")
config.TempScale = settings.get("TempScale")
config.EnergyScale = settings.get("EnergyScale")
@@ -53,6 +55,7 @@ function pocket.load_config()
local cfv = util.new_validator()
cfv.assert_type_bool(config.GreenPuPellet)
cfv.assert_type_int(config.TempScale)
cfv.assert_range(config.TempScale, 1, 4)
cfv.assert_type_int(config.EnergyScale)
@@ -87,15 +90,17 @@ local APP_ID = {
LOADER = 2,
-- main app pages
UNITS = 3,
CONTROL = 4,
PROCESS = 5,
GUIDE = 6,
ABOUT = 7,
FACILITY = 4,
CONTROL = 5,
PROCESS = 6,
WASTE = 7,
GUIDE = 8,
ABOUT = 9,
-- diagnostic app pages
ALARMS = 8,
ALARMS = 10,
-- other
DUMMY = 9,
NUM_APPS = 9
DUMMY = 11,
NUM_APPS = 11
}
pocket.APP_ID = APP_ID
@@ -264,7 +269,8 @@ function pocket.init_nav(smem)
-- open an app
---@param app_id POCKET_APP_ID
function nav.open_app(app_id)
---@param on_ready? function
function nav.open_app(app_id, on_ready)
-- reset help return on navigating out of an app
if app_id == APP_ID.ROOT then self.help_return = nil end
@@ -277,7 +283,7 @@ function pocket.init_nav(smem)
app = self.apps[app_id]
else self.loader_return = nil end
if not app.loaded then smem.q.mq_render.push_data(MQ__RENDER_DATA.LOAD_APP, app_id) end
if not app.loaded then smem.q.mq_render.push_data(MQ__RENDER_DATA.LOAD_APP, { app_id, on_ready }) end
self.cur_app = app_id
self.pane.set_value(app_id)
@@ -285,6 +291,8 @@ function pocket.init_nav(smem)
if #app.sidebar_items > 0 then
self.sidebar.update(app.sidebar_items)
end
if app.loaded and on_ready then on_ready() end
else
log.debug("tried to open unknown app")
end
@@ -360,10 +368,10 @@ function pocket.init_nav(smem)
function nav.open_help(key)
self.help_return = self.cur_app
nav.open_app(APP_ID.GUIDE)
local load = self.help_map[key]
if load then load() end
nav.open_app(APP_ID.GUIDE, function ()
local show = self.help_map[key]
if show then show() end
end)
end
-- link the help map from the guide app
@@ -550,6 +558,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end
end
-- coordinator get facility app data
function public.api__get_facility()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_FAC_DTL, {}) end
end
-- coordinator get unit data
function public.api__get_unit(unit)
if self.api.linked then _send_api(CRDN_TYPE.API_GET_UNIT, { unit }) end
@@ -565,6 +578,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
if self.api.linked then _send_api(CRDN_TYPE.API_GET_PROC, {}) end
end
-- coordinator get waste app data
function public.api__get_waste()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_WASTE, {}) end
end
-- send a facility command
---@param cmd FAC_COMMAND command
---@param option any? optional option options for the optional options (like waste mode)
@@ -719,19 +737,27 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
end
elseif packet.type == CRDN_TYPE.API_GET_FAC then
if _check_length(packet, 11) then
iocontrol.record_facility_data(packet.data)
iocontrol.rx.record_facility_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_FAC_DTL then
if _check_length(packet, 12) then
iocontrol.rx.record_fac_detail_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_UNIT then
if _check_length(packet, 12) and type(packet.data[1]) == "number" and iocontrol.get_db().units[packet.data[1]] then
iocontrol.record_unit_data(packet.data)
iocontrol.rx.record_unit_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_CTRL then
if _check_length(packet, #iocontrol.get_db().units) then
iocontrol.record_control_data(packet.data)
iocontrol.rx.record_control_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_PROC then
if _check_length(packet, #iocontrol.get_db().units + 1) then
iocontrol.record_process_data(packet.data)
iocontrol.rx.record_process_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_WASTE then
if _check_length(packet, #iocontrol.get_db().units + 1) then
iocontrol.rx.record_waste_data(packet.data)
end
else _fail_type(packet) end
else
@@ -891,7 +917,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
local ready = packet.data[1]
local states = packet.data[2]
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready"))
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not idle"))
for i = 1, #states do
if diag.tone_test.tone_buttons[i] ~= nil then
@@ -910,7 +936,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
local ready = packet.data[1]
local states = packet.data[2]
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready"))
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not idle"))
for i = 1, #states do
if diag.tone_test.alarm_buttons[i] ~= nil then

View File

@@ -85,6 +85,14 @@ function process.set_group(unit_id, group_id)
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
end
-- set waste mode
---@param id integer unit ID
---@param mode integer waste mode
function process.set_unit_waste(id, mode)
self.comms.send_unit_command(U_CMD.SET_WASTE, id, mode)
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
end
-- acknowledge all alarms
---@param id integer unit ID
function process.ack_all_alarms(id)
@@ -131,6 +139,27 @@ function process.process_stop()
log.debug("PROCESS: STOP AUTO CTRL")
end
-- set automatic process control waste mode
---@param product WASTE_PRODUCT waste product for auto control
function process.set_process_waste(product)
self.comms.send_fac_command(F_CMD.SET_WASTE_MODE, product)
log.debug(util.c("PROCESS: SET WASTE ", product))
end
-- set automatic process control plutonium fallback
---@param enabled boolean whether to enable plutonium fallback
function process.set_pu_fallback(enabled)
self.comms.send_fac_command(F_CMD.SET_PU_FB, enabled)
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
end
-- set automatic process control SPS usage at low power
---@param enabled boolean whether to enable SPS usage at low power
function process.set_sps_low_power(enabled)
self.comms.send_fac_command(F_CMD.SET_SPS_LP, enabled)
log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled))
end
-- #endregion
---------------------------------

View File

@@ -2,8 +2,10 @@
-- SCADA System Access on a Pocket Computer
--
---@diagnostic disable-next-line: undefined-global
local _is_pocket_env = pocket or periphemu -- luacheck: ignore pocket
---@diagnostic disable-next-line: lowercase-global
pocket = pocket or periphemu -- luacheck: ignore pocket
local _is_pocket_env = pocket -- luacheck: ignore pocket
require("/initenv").init_env()
@@ -20,7 +22,7 @@ local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer")
local threads = require("pocket.threads")
local POCKET_VERSION = "v0.12.7-alpha"
local POCKET_VERSION = "v0.13.4-beta"
local println = util.println
local println_ts = util.println_ts

View File

@@ -165,15 +165,18 @@ function threads.thread__render(smem)
local cmd = msg.message ---@type queue_data
if cmd.key == MQ__RENDER_DATA.LOAD_APP then
log.debug("RENDER: load app " .. cmd.val)
log.debug("RENDER: load app " .. cmd.val[1])
local draw_start = util.time_ms()
pkt_state.ui_ok, pkt_state.ui_error = pcall(function () nav.load_app(cmd.val) end)
pkt_state.ui_ok, pkt_state.ui_error = pcall(function () nav.load_app(cmd.val[1]) end)
if not pkt_state.ui_ok then
log.fatal(util.c("RENDER: app load failed with error ", pkt_state.ui_error))
else
log.debug("RENDER: app loaded in " .. (util.time_ms() - draw_start) .. "ms")
-- call the on loaded function if provided
if type(cmd.val[2]) == "function" then cmd.val[2]() end
end
end
elseif msg.qtype == mqueue.TYPE.PACKET then

View File

@@ -1,5 +1,5 @@
--
-- Unit Control Page
-- Facility & Unit Control App
--
local types = require("scada-common.types")

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

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

View File

@@ -1,5 +1,5 @@
--
-- Process Control Page
-- Process Control App
--
local types = require("scada-common.types")
@@ -269,7 +269,7 @@ local function new_view(root)
local auto_scram = IconIndicator{parent=a_div,y=3,label="Automatic SCRAM",states=red_ind_s}
TextBox{parent=a_div,y=5,text="Induction Matrix",fg_bg=label_fg_bg}
local matrix_dc = IconIndicator{parent=a_div,label="Disconnected",states=yel_ind_s}
local matrix_flt = IconIndicator{parent=a_div,label="Matrix Fault",states=yel_ind_s}
local matrix_fill = IconIndicator{parent=a_div,label="Charge High",states=red_ind_s}
TextBox{parent=a_div,y=9,text="Assigned Units",fg_bg=label_fg_bg}
@@ -282,7 +282,7 @@ local function new_view(root)
local gen_fault = IconIndicator{parent=a_div,label="Control Fault",states=yel_ind_s}
auto_scram.register(f_ps, "auto_scram", auto_scram.update)
matrix_dc.register(f_ps, "as_matrix_dc", matrix_dc.update)
matrix_flt.register(f_ps, "as_matrix_fault", matrix_flt.update)
matrix_fill.register(f_ps, "as_matrix_fill", matrix_fill.update)
unit_crit.register(f_ps, "as_crit_alarm", unit_crit.update)
fac_rad_h.register(f_ps, "as_radiation", fac_rad_h.update)

View File

@@ -1,5 +1,5 @@
--
-- Unit Overview Page
-- Unit Overview App
--
local util = require("scada-common.util")
@@ -9,6 +9,7 @@ local pocket = require("pocket.pocket")
local style = require("pocket.ui.style")
local dyn_tank = require("pocket.ui.pages.dynamic_tank")
local boiler = require("pocket.ui.pages.unit_boiler")
local reactor = require("pocket.ui.pages.unit_reactor")
local turbine = require("pocket.ui.pages.unit_turbine")
@@ -32,9 +33,8 @@ local cpair = core.cpair
local APP_ID = pocket.APP_ID
-- local label = style.label
local lu_col = style.label_unit_pair
local text_fg = style.text_fg
local lu_col = style.label_unit_pair
local basic_states = style.icon_states.basic_states
local mode_states = style.icon_states.mode_states
local red_ind_s = style.icon_states.red_ind_s
@@ -92,6 +92,10 @@ local function new_view(root)
table.insert(list, { label = "T-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = nav_links[id].turbine[i] })
end
if #unit.tank_data_tbl > 0 then
table.insert(list, { label = "DYN", color = core.cpair(colors.black, colors.lightGray), callback = nav_links[id].d_tank })
end
app.set_sidebar(list)
end
@@ -363,6 +367,15 @@ local function new_view(root)
--#endregion
--#region Dynamic Tank Tab
if #unit.tank_data_tbl > 0 then
local tank_pane = Div{parent=page_div}
nav_links[i].d_tank = dyn_tank(app, u_page, panes, tank_pane, i, unit.tank_ps_tbl[1], update)
end
--#endregion
util.nop()
end

308
pocket/ui/apps/waste.lua Normal file
View File

@@ -0,0 +1,308 @@
--
-- Waste Control App
--
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local process = require("pocket.process")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local Checkbox = require("graphics.elements.controls.Checkbox")
local PushButton = require("graphics.elements.controls.PushButton")
local RadioButton = require("graphics.elements.controls.RadioButton")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
local label_fg_bg = style.label
local text_fg = style.text_fg
local lu_col = style.label_unit_pair
local yel_ind_s = style.icon_states.yel_ind_s
local wht_ind_s = style.icon_states.wht_ind_s
-- new waste control page view
---@param root Container parent
local function new_view(root)
local db = iocontrol.get_db()
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.WASTE, frame, nil, false, true)
local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.brown,colors._INHERIT)}
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
local page_div = nil ---@type Div|nil
-- load the app (create the elements)
local function load()
local f_ps = db.facility.ps
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {} ---@type Div[]
local u_pages = {} ---@type nav_tree_page[]
local last_update = 0
-- refresh data callback, every 500ms it will re-send the query
local function update()
if util.time_ms() - last_update >= 500 then
db.api.get_waste()
last_update = util.time_ms()
end
end
--#region unit waste options/statistics
for i = 1, db.facility.num_units do
local u_pane = Div{parent=page_div}
local u_div = Div{parent=u_pane,x=2,width=main.get_width()-2}
local unit = db.units[i]
local u_ps = unit.unit_ps
table.insert(panes, u_div)
local u_page = app.new_page(nil, #panes)
u_page.tasks = { update }
table.insert(u_pages, u_page)
TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,alignment=ALIGN.CENTER}
local function set_waste(mode) process.set_unit_waste(i, mode) end
local waste_prod = StateIndicator{parent=u_div,x=16,y=3,states=style.get_waste().states_abbrv,value=1,min_width=6}
local waste_mode = RadioButton{parent=u_div,y=3,options=style.get_waste().unit_opts,callback=set_waste,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.white}
waste_prod.register(u_ps, "U_WasteProduct", waste_prod.update)
waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)
TextBox{parent=u_div,y=8,text="Plutonium (Pellets)",fg_bg=label_fg_bg}
local pu = DataIndicator{parent=u_div,label="",format="%16.3f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
TextBox{parent=u_div,y=11,text="Polonium",fg_bg=label_fg_bg}
local po = DataIndicator{parent=u_div,label="",format="%16.3f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
TextBox{parent=u_div,y=14,text="Polonium (Pellets)",fg_bg=label_fg_bg}
local popl = DataIndicator{parent=u_div,label="",format="%16.3f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
pu.register(u_ps, "pu_rate", pu.update)
po.register(u_ps, "po_rate", po.update)
popl.register(u_ps, "po_pl_rate", popl.update)
local sna_div = Div{parent=u_pane,x=2,width=page_div.get_width()-2}
table.insert(panes, sna_div)
local sps_page = app.new_page(u_page, #panes)
sps_page.tasks = { update }
PushButton{parent=u_div,x=6,y=18,text="SNA DATA",min_width=12,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=sps_page.nav_to}
PushButton{parent=sna_div,x=9,y=18,text="BACK",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=u_page.nav_to}
TextBox{parent=sna_div,y=1,text="Unit "..i.." SNAs",alignment=ALIGN.CENTER}
TextBox{parent=sna_div,y=3,text="Connected",fg_bg=label_fg_bg}
local count = DataIndicator{parent=sna_div,x=20,y=3,label="",format="%2d",value=0,unit="",lu_colors=lu_col,width=2,fg_bg=text_fg}
TextBox{parent=sna_div,y=5,text="Peak Possible Rate\n In\n Out",fg_bg=label_fg_bg}
local peak_i = DataIndicator{parent=sna_div,x=6,y=6,label="",format="%11.2f",value=0,unit="mB/t",lu_colors=lu_col,width=17,fg_bg=text_fg}
local peak_o = DataIndicator{parent=sna_div,x=6,label="",format="%11.2f",value=0,unit="mB/t",lu_colors=lu_col,width=17,fg_bg=text_fg}
TextBox{parent=sna_div,y=9,text="Current Maximum Rate\n In\n Out",fg_bg=label_fg_bg}
local max_i = DataIndicator{parent=sna_div,x=6,y=10,label="",format="%11.2f",value=0,unit="mB/t",lu_colors=lu_col,width=17,fg_bg=text_fg}
local max_o = DataIndicator{parent=sna_div,x=6,label="",format="%11.2f",value=0,unit="mB/t",lu_colors=lu_col,width=17,fg_bg=text_fg}
TextBox{parent=sna_div,y=13,text="Current Rate\n In\n Out",fg_bg=label_fg_bg}
local cur_i = DataIndicator{parent=sna_div,x=6,y=14,label="",format="%11.2f",value=0,unit="mB/t",lu_colors=lu_col,width=17,fg_bg=text_fg}
local cur_o = DataIndicator{parent=sna_div,x=6,label="",format="%11.2f",value=0,unit="mB/t",lu_colors=lu_col,width=17,fg_bg=text_fg}
count.register(u_ps, "sna_count", count.update)
peak_i.register(u_ps, "sna_peak_rate", function (x) peak_i.update(x * 10) end)
peak_o.register(u_ps, "sna_peak_rate", peak_o.update)
max_i.register(u_ps, "sna_max_rate", function (x) max_i.update(x * 10) end)
max_o.register(u_ps, "sna_max_rate", max_o.update)
cur_i.register(u_ps, "sna_out_rate", function (x) cur_i.update(x * 10) end)
cur_o.register(u_ps, "sna_out_rate", cur_o.update)
end
--#endregion
--#region waste control page
local c_pane = Div{parent=page_div}
local c_div = Div{parent=c_pane,x=2,width=main.get_width()-2}
table.insert(panes, c_div)
local wst_ctrl = app.new_page(nil, #panes)
wst_ctrl.tasks = { update }
TextBox{parent=c_div,y=1,text="Waste Control",alignment=ALIGN.CENTER}
local status = StateIndicator{parent=c_div,x=3,y=3,states=style.get_waste().states,value=1,min_width=17}
local waste_prod = RadioButton{parent=c_div,y=5,options=style.get_waste().options,callback=process.set_process_waste,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.white}
status.register(f_ps, "current_waste_product", status.update)
waste_prod.register(f_ps, "process_waste_product", waste_prod.set_value)
local fb_active = IconIndicator{parent=c_div,y=9,label="Fallback Active",states=wht_ind_s}
local sps_disabled = IconIndicator{parent=c_div,y=10,label="SPS Disabled LC",states=yel_ind_s}
fb_active.register(f_ps, "pu_fallback_active", fb_active.update)
sps_disabled.register(f_ps, "sps_disabled_low_power", sps_disabled.update)
TextBox{parent=c_div,y=12,text="Nuclear Waste In",fg_bg=label_fg_bg}
local sum_raw_waste = DataIndicator{parent=c_div,label="",format="%16.3f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
sum_raw_waste.register(f_ps, "burn_sum", sum_raw_waste.update)
TextBox{parent=c_div,y=15,text="Spent Waste Out",fg_bg=label_fg_bg}
local sum_sp_waste = DataIndicator{parent=c_div,label="",format="%16.3f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
sum_sp_waste.register(f_ps, "spent_waste_rate", sum_sp_waste.update)
local stats_div = Div{parent=c_pane,x=2,width=page_div.get_width()-2}
table.insert(panes, stats_div)
local stats_page = app.new_page(wst_ctrl, #panes)
stats_page.tasks = { update }
PushButton{parent=c_div,x=6,y=18,text="PROD RATES",min_width=12,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=stats_page.nav_to}
PushButton{parent=stats_div,x=9,y=18,text="BACK",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=wst_ctrl.nav_to}
TextBox{parent=stats_div,y=1,text="Production Rates",alignment=ALIGN.CENTER}
TextBox{parent=stats_div,y=3,text="Plutonium (Pellets)",fg_bg=label_fg_bg}
local pu = DataIndicator{parent=stats_div,label="",format="%16.3f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
TextBox{parent=stats_div,y=6,text="Polonium",fg_bg=label_fg_bg}
local po = DataIndicator{parent=stats_div,label="",format="%16.3f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
TextBox{parent=stats_div,y=9,text="Polonium (Pellets)",fg_bg=label_fg_bg}
local popl = DataIndicator{parent=stats_div,label="",format="%16.3f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
pu.register(f_ps, "pu_rate", pu.update)
po.register(f_ps, "po_rate", po.update)
popl.register(f_ps, "po_pl_rate", popl.update)
TextBox{parent=stats_div,y=12,text="Antimatter",fg_bg=label_fg_bg}
local am = DataIndicator{parent=stats_div,label="",format="%16d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
am.register(f_ps, "sps_process_rate", function (r) am.update(r * 1000) end)
--#endregion
--#region waste options page
local o_pane = Div{parent=page_div}
local o_div = Div{parent=o_pane,x=2,width=main.get_width()-2}
table.insert(panes, o_pane)
local opt_page = app.new_page(nil, #panes)
opt_page.tasks = { update }
TextBox{parent=o_div,y=1,text="Waste Options",alignment=ALIGN.CENTER}
local pu_fallback = Checkbox{parent=o_div,x=2,y=3,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=o_div,x=2,y=5,height=3,text="Switch to Pu when SNAs cannot keep up with waste.",fg_bg=label_fg_bg}
local lc_sps = Checkbox{parent=o_div,x=2,y=9,label="Low Charge SPS",callback=process.set_sps_low_power,box_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=o_div,x=2,y=11,height=3,text="Use SPS at low charge, otherwise switches to Po.",fg_bg=label_fg_bg}
pu_fallback.register(f_ps, "process_pu_fallback", pu_fallback.set_value)
lc_sps.register(f_ps, "process_sps_low_power", lc_sps.set_value)
--#endregion
--#region SPS page
local s_pane = Div{parent=page_div}
local s_div = Div{parent=s_pane,x=2,width=main.get_width()-2}
table.insert(panes, s_pane)
local sps_page = app.new_page(nil, #panes)
sps_page.tasks = { update }
TextBox{parent=s_div,y=1,text="Facility SPS",alignment=ALIGN.CENTER}
local sps_status = StateIndicator{parent=s_div,x=5,y=3,states=style.sps.states,value=1,min_width=12}
sps_status.register(db.facility.sps_ps_tbl[1], "SPSStateStatus", sps_status.update)
TextBox{parent=s_div,y=5,text="Input Rate",width=10,fg_bg=label_fg_bg}
local sps_in = DataIndicator{parent=s_div,label="",format="%16.2f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
sps_in.register(f_ps, "po_am_rate", sps_in.update)
TextBox{parent=s_div,y=8,text="Production Rate",width=15,fg_bg=label_fg_bg}
local sps_rate = DataIndicator{parent=s_div,label="",format="%16d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
sps_rate.register(f_ps, "sps_process_rate", function (r) sps_rate.update(r * 1000) end)
--#endregion
-- setup multipane
local w_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(w_pane)
-- setup sidebar
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = "WST", color = core.cpair(colors.black, colors.brown), callback = wst_ctrl.nav_to },
{ label = "OPT", color = core.cpair(colors.black, colors.white), callback = opt_page.nav_to },
{ label = "SPS", color = core.cpair(colors.black, colors.purple), callback = sps_page.nav_to }
}
for i = 1, db.facility.num_units do
table.insert(list, { label = "U-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = u_pages[i].nav_to })
end
app.set_sidebar(list)
-- done, show the app
wst_ctrl.nav_to()
load_pane.set_value(2)
end
-- delete the elements and switch back to the loading screen
local function unload()
if page_div then
page_div.delete()
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.delete_pages()
-- show loading screen
load_pane.set_value(1)
end
app.set_load(load)
app.set_unload(unload)
return main
end
return new_view

View File

@@ -148,7 +148,7 @@ doc("auto_ramping", "Process Ramping", "Automatic process control is performing
doc("auto_saturated", "Min/Max Burn Rate", "Auto control has either commanded 0 mB/t or the maximum total burn rate available (from assigned units).")
sect("Automatic SCRAM")
doc("auto_scram", "Automatic SCRAM", "Automatic control system SCRAM'ed the assigned reactors due to a safety hazard, shown by the below indicators.")
doc("as_matrix_dc", "Matrix Disconnected", "Automatic SCRAM occurred due to loss of induction matrix connection.")
doc("as_matrix_fault", "Matrix Fault", "Automatic SCRAM occurred due to the loss of the induction matrix connection, or the matrix being unformed or faulted.")
doc("as_matrix_fill", "Matrix Charge High", "Automatic SCRAM occurred due to induction matrix charge exceeding acceptable limit.")
doc("as_crit_alarm", "Unit Critical Alarm", "Automatic SCRAM occurred due to critical level unit alarm(s).")
doc("as_radiation", "Facility Radiation High", "Automatic SCRAM occurred due to high facility radiation levels.")

View File

@@ -10,11 +10,13 @@ local pocket = require("pocket.pocket")
local control_app = require("pocket.ui.apps.control")
local diag_apps = require("pocket.ui.apps.diag_apps")
local dummy_app = require("pocket.ui.apps.dummy_app")
local facil_app = require("pocket.ui.apps.facility")
local guide_app = require("pocket.ui.apps.guide")
local loader_app = require("pocket.ui.apps.loader")
local process_app = require("pocket.ui.apps.process")
local sys_apps = require("pocket.ui.apps.sys_apps")
local unit_app = require("pocket.ui.apps.unit")
local waste_app = require("pocket.ui.apps.waste")
local home_page = require("pocket.ui.pages.home_page")
@@ -44,7 +46,7 @@ local function init(main)
local db = iocontrol.get_db()
-- window header message and connection status
TextBox{parent=main,y=1,text="EARLY ACCESS ALPHA S C ",fg_bg=style.header}
TextBox{parent=main,y=1,text=" S C ",fg_bg=style.header}
local svr_conn = SignalBar{parent=main,y=1,x=22,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
local crd_conn = SignalBar{parent=main,y=1,x=26,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
@@ -64,8 +66,10 @@ local function init(main)
-- create all the apps & pages
home_page(page_div)
unit_app(page_div)
facil_app(page_div)
control_app(page_div)
process_app(page_div)
waste_app(page_div)
guide_app(page_div)
loader_app(page_div)
sys_apps(page_div)

View File

@@ -0,0 +1,97 @@
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local style = require("pocket.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 HorizontalBar = require("graphics.elements.indicators.HorizontalBar")
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
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 label = style.label
local lu_col = style.label_unit_pair
local text_fg = style.text_fg
local mode_ind_s = {
{ color = cpair(colors.black, colors.lightGray), symbol = "-" },
{ color = cpair(colors.black, colors.white), symbol = "+" }
}
-- create a dynamic tank view for the unit or facility app
---@param app pocket_app
---@param page nav_tree_page|nil parent page, if applicable
---@param panes Div[]
---@param tank_pane Div
---@param tank_id integer global facility tank ID (as used for tank list, etc)
---@param ps psil
---@param update function
return function (app, page, panes, tank_pane, tank_id, ps, update)
local fac = iocontrol.get_db().facility
local tank_div = Div{parent=tank_pane,x=2,width=tank_pane.get_width()-2}
table.insert(panes, tank_div)
local tank_page = app.new_page(page, #panes)
tank_page.tasks = { update }
local tank_assign = ""
local f_tank_count = 0
for i = 1, #fac.tank_list do
local is_fac = fac.tank_list[i] == 2
if is_fac then f_tank_count = f_tank_count + 1 end
if i == tank_id then
tank_assign = util.trinary(is_fac, "F-" .. f_tank_count, "U-" .. i)
break
end
end
TextBox{parent=tank_div,y=1,text="Dynamic Tank "..tank_assign,alignment=ALIGN.CENTER}
local status = StateIndicator{parent=tank_div,x=5,y=3,states=style.dtank.states,value=1,min_width=12}
status.register(ps, "DynamicTankStateStatus", status.update)
TextBox{parent=tank_div,y=5,text="Fill",width=10,fg_bg=label}
local tank_pcnt = DataIndicator{parent=tank_div,x=14,y=5,label="",format="%5.2f",value=100,unit="%",lu_colors=lu_col,width=8,fg_bg=text_fg}
local tank_amnt = DataIndicator{parent=tank_div,label="",format="%18d",value=0,commas=true,unit="mB",lu_colors=lu_col,width=21,fg_bg=text_fg}
local is_water = fac.tank_fluid_types[tank_id] == COOLANT_TYPE.WATER
TextBox{parent=tank_div,y=8,text=util.trinary(is_water,"Water","Sodium").." Level",width=12,fg_bg=label}
local level = HorizontalBar{parent=tank_div,y=9,bar_fg_bg=cpair(util.trinary(is_water,colors.blue,colors.lightBlue),colors.gray),height=1,width=21}
TextBox{parent=tank_div,y=11,text="Tank Fill Mode",width=14,fg_bg=label}
local can_fill = IconIndicator{parent=tank_div,y=12,label="Fill",states=mode_ind_s}
local can_empty = IconIndicator{parent=tank_div,y=13,label="Empty",states=mode_ind_s}
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
tank_pcnt.register(ps, "fill", function (f) tank_pcnt.update(f * 100) end)
tank_amnt.register(ps, "stored", function (sto) tank_amnt.update(sto.amount) end)
level.register(ps, "fill", level.update)
can_fill.register(ps, "container_mode", _can_fill)
can_empty.register(ps, "container_mode", _can_empty)
return tank_page.nav_to
end

View File

@@ -0,0 +1,121 @@
local iocontrol = require("pocket.iocontrol")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local HorizontalBar = require("graphics.elements.indicators.HorizontalBar")
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
local PowerIndicator = require("graphics.elements.indicators.PowerIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local ALIGN = core.ALIGN
local cpair = core.cpair
local label = style.label
local lu_col = style.label_unit_pair
local text_fg = style.text_fg
local yel_ind_s = style.icon_states.yel_ind_s
local wht_ind_s = style.icon_states.wht_ind_s
-- create an induction matrix view for the facility app
---@param app pocket_app
---@param panes Div[]
---@param matrix_pane Div
---@param ps psil
---@param update function
return function (app, panes, matrix_pane, ps, update)
local db = iocontrol.get_db()
local fac = db.facility
local mtx_div = Div{parent=matrix_pane,x=2,width=matrix_pane.get_width()-2}
table.insert(panes, mtx_div)
local matrix_page = app.new_page(nil, #panes)
matrix_page.tasks = { update }
TextBox{parent=mtx_div,y=1,text="Induction Matrix",alignment=ALIGN.CENTER}
local status = StateIndicator{parent=mtx_div,x=5,y=3,states=style.imatrix.states,value=1,min_width=12}
status.register(ps, "InductionMatrixStateStatus", status.update)
TextBox{parent=mtx_div,text="Chg",y=5,fg_bg=label}
local chg_bar = HorizontalBar{parent=mtx_div,x=5,y=5,height=1,fg_bg=cpair(colors.green,colors.gray)}
TextBox{parent=mtx_div,text="In",y=7,fg_bg=label}
local in_bar = HorizontalBar{parent=mtx_div,x=5,y=7,height=1,fg_bg=cpair(colors.blue,colors.gray)}
TextBox{parent=mtx_div,text="Out",y=9,fg_bg=label}
local out_bar = HorizontalBar{parent=mtx_div,x=5,y=9,height=1,fg_bg=cpair(colors.red,colors.gray)}
local function calc_saturation(val)
local data = fac.induction_data_tbl[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
chg_bar.register(ps, "energy_fill", chg_bar.update)
in_bar.register(ps, "last_input", function (val) in_bar.update(calc_saturation(val)) end)
out_bar.register(ps, "last_output", function (val) out_bar.update(calc_saturation(val)) end)
local energy = PowerIndicator{parent=mtx_div,y=11,lu_colors=lu_col,label="Chg: ",unit=db.energy_label,format="%8.2f",value=0,width=21,fg_bg=text_fg}
local avg_chg = PowerIndicator{parent=mtx_div,lu_colors=lu_col,label="\xb7Avg: ",unit=db.energy_label,format="%8.2f",value=0,width=21,fg_bg=text_fg}
local input = PowerIndicator{parent=mtx_div,lu_colors=lu_col,label="In: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=21,fg_bg=text_fg}
local avg_in = PowerIndicator{parent=mtx_div,lu_colors=lu_col,label="\xb7Avg: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=21,fg_bg=text_fg}
local output = PowerIndicator{parent=mtx_div,lu_colors=lu_col,label="Out: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=21,fg_bg=text_fg}
local avg_out = PowerIndicator{parent=mtx_div,lu_colors=lu_col,label="\xb7Avg: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=21,fg_bg=text_fg}
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)
local mtx_ext_div = Div{parent=matrix_pane,x=2,width=matrix_pane.get_width()-2}
table.insert(panes, mtx_ext_div)
local mtx_ext_page = app.new_page(matrix_page, #panes)
mtx_ext_page.tasks = { update }
PushButton{parent=mtx_div,x=9,y=18,text="MORE",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=mtx_ext_page.nav_to}
PushButton{parent=mtx_ext_div,x=9,y=18,text="BACK",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=matrix_page.nav_to}
TextBox{parent=mtx_ext_div,y=1,text="More Matrix Info",alignment=ALIGN.CENTER}
local chging = IconIndicator{parent=mtx_ext_div,y=3,label="Charging",states=wht_ind_s}
local dischg = IconIndicator{parent=mtx_ext_div,y=4,label="Discharging",states=wht_ind_s}
TextBox{parent=mtx_ext_div,text="Energy Fill",x=1,y=6,width=13,fg_bg=label}
local fill = DataIndicator{parent=mtx_ext_div,x=14,y=6,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
chging.register(ps, "is_charging", chging.update)
dischg.register(ps, "is_discharging", dischg.update)
fill.register(ps, "energy_fill", function (x) fill.update(x * 100) end)
local max_io = IconIndicator{parent=mtx_ext_div,y=8,label="Max I/O Rate",states=yel_ind_s}
TextBox{parent=mtx_ext_div,text="Input Util.",x=1,y=10,width=13,fg_bg=label}
local in_util = DataIndicator{parent=mtx_ext_div,x=14,y=10,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
TextBox{parent=mtx_ext_div,text="Output Util.",x=1,y=11,width=13,fg_bg=label}
local out_util = DataIndicator{parent=mtx_ext_div,x=14,y=11,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
max_io.register(ps, "at_max_io", max_io.update)
in_util.register(ps, "last_input", function (x) in_util.update(calc_saturation(x) * 100) end)
out_util.register(ps, "last_output", function (x) out_util.update(calc_saturation(x) * 100) end)
TextBox{parent=mtx_ext_div,text="Capacity ("..db.energy_label..")",x=1,y=13,fg_bg=label}
local capacity = DataIndicator{parent=mtx_ext_div,y=14,lu_colors=lu_col,label="",unit="",format="%21d",value=0,width=21,fg_bg=text_fg}
TextBox{parent=mtx_ext_div,text="Max In/Out ("..db.energy_label.."/t)",x=1,y=15,fg_bg=label}
local trans_cap = DataIndicator{parent=mtx_ext_div,y=16,lu_colors=lu_col,label="",unit="",format="%21d",rate=true,value=0,width=21,fg_bg=text_fg}
capacity.register(ps, "max_energy", function (val) capacity.update(db.energy_convert(val)) end)
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(db.energy_convert(val)) end)
return matrix_page.nav_to
end

View File

@@ -0,0 +1,84 @@
local iocontrol = require("pocket.iocontrol")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local HorizontalBar = require("graphics.elements.indicators.HorizontalBar")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local ALIGN = core.ALIGN
local cpair = core.cpair
local label = style.label
local lu_col = style.label_unit_pair
local text_fg = style.text_fg
-- create an SPS view in the facility app
---@param app pocket_app
---@param panes Div[]
---@param sps_pane Div
---@param ps psil
---@param update function
return function (app, panes, sps_pane, ps, update)
local db = iocontrol.get_db()
local sps_div = Div{parent=sps_pane,x=2,width=sps_pane.get_width()-2}
table.insert(panes, sps_div)
local sps_page = app.new_page(nil, #panes)
sps_page.tasks = { update }
TextBox{parent=sps_div,y=1,text="Facility SPS",alignment=ALIGN.CENTER}
local status = StateIndicator{parent=sps_div,x=5,y=3,states=style.sps.states,value=1,min_width=12}
status.register(ps, "SPSStateStatus", status.update)
TextBox{parent=sps_div,text="Po",y=5,fg_bg=label}
local po_bar = HorizontalBar{parent=sps_div,x=4,y=5,fg_bg=cpair(colors.cyan,colors.gray),height=1}
TextBox{parent=sps_div,text="AM",y=7,fg_bg=label}
local am_bar = HorizontalBar{parent=sps_div,x=4,y=7,fg_bg=cpair(colors.purple,colors.gray),height=1}
po_bar.register(ps, "input_fill", po_bar.update)
am_bar.register(ps, "output_fill", am_bar.update)
TextBox{parent=sps_div,y=9,text="Input Rate",width=10,fg_bg=label}
local input_rate = DataIndicator{parent=sps_div,label="",format="%16.2f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
TextBox{parent=sps_div,y=12,text="Production Rate",width=15,fg_bg=label}
local proc_rate = DataIndicator{parent=sps_div,label="",format="%16d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
proc_rate.register(ps, "process_rate", function (r) proc_rate.update(r * 1000) end)
input_rate.register(db.facility.ps, "po_am_rate", input_rate.update)
local sps_ext_div = Div{parent=sps_pane,x=2,width=sps_pane.get_width()-2}
table.insert(panes, sps_ext_div)
local sps_ext_page = app.new_page(sps_page, #panes)
sps_ext_page.tasks = { update }
PushButton{parent=sps_div,x=9,y=18,text="MORE",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=sps_ext_page.nav_to}
PushButton{parent=sps_ext_div,x=9,y=18,text="BACK",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=sps_page.nav_to}
TextBox{parent=sps_ext_div,y=1,text="More SPS Info",alignment=ALIGN.CENTER}
TextBox{parent=sps_ext_div,text="Polonium",x=1,y=3,width=13,fg_bg=label}
local input_p = DataIndicator{parent=sps_ext_div,x=14,y=3,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
local input_amnt = DataIndicator{parent=sps_ext_div,x=1,y=4,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
input_p.register(ps, "input_fill", function (x) input_p.update(x * 100) end)
input_amnt.register(ps, "input", function (x) input_amnt.update(x.amount) end)
TextBox{parent=sps_ext_div,text="Antimatter",x=1,y=6,width=15,fg_bg=label}
local output_p = DataIndicator{parent=sps_ext_div,x=14,y=6,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
local output_amnt = DataIndicator{parent=sps_ext_div,x=1,y=7,lu_colors=lu_col,label="",unit="\xb5B",format="%18.3f",value=0,commas=true,width=21,fg_bg=text_fg}
output_p.register(ps, "output_fill", function (x) output_p.update(x * 100) end)
output_amnt.register(ps, "output", function (x) output_amnt.update(x.amount) end)
return sps_page.nav_to
end

View File

@@ -46,10 +46,10 @@ local function new_view(root)
local active_fg_bg = cpair(colors.white,colors.gray)
App{parent=apps_1,x=2,y=2,text="U",title="Units",callback=function()open(APP_ID.UNITS)end,app_fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=9,y=2,text="F",title="Facil",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=9,y=2,text="F",title="Facil",callback=function()open(APP_ID.FACILITY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=16,y=2,text="\x15",title="Control",callback=function()open(APP_ID.CONTROL)end,app_fg_bg=cpair(colors.black,colors.green),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=2,y=7,text="\x17",title="Process",callback=function()open(APP_ID.PROCESS)end,app_fg_bg=cpair(colors.black,colors.purple),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=9,y=7,text="\x7f",title="Waste",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.brown),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=9,y=7,text="\x7f",title="Waste",callback=function()open(APP_ID.WASTE)end,app_fg_bg=cpair(colors.black,colors.brown),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=16,y=7,text="\x08",title="Devices",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=2,y=12,text="\xb6",title="Guide",callback=function()open(APP_ID.GUIDE)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=9,y=12,text="?",title="About",callback=function()open(APP_ID.ABOUT)end,app_fg_bg=cpair(colors.black,colors.white),active_fg_bg=active_fg_bg}

View File

@@ -2,12 +2,18 @@
-- Graphics Style Options
--
local core = require("graphics.core")
local util = require("scada-common.util")
local core = require("graphics.core")
local pocket = require("pocket.pocket")
local style = {}
local cpair = core.cpair
local config = pocket.config
-- GLOBAL --
style.root = cpair(colors.white, colors.black)
@@ -95,123 +101,105 @@ style.icon_states = states
-- MAIN LAYOUT --
style.reactor = {
-- reactor states
-- reactor states<br>
---@see REACTOR_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 = "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 DSBL"
}
{ 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 = "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 DSBL" }
}
}
style.boiler = {
-- boiler states
-- boiler states<br>
---@see BOILER_STATE
states = {
{
color = cpair(colors.black, colors.yellow),
text = "OFF-LINE"
},
{
color = cpair(colors.black, colors.orange),
text = "NOT FORMED"
},
{
color = cpair(colors.black, colors.orange),
text = "RTU FAULT"
},
{
color = cpair(colors.white, colors.gray),
text = "IDLE"
},
{
color = cpair(colors.black, colors.green),
text = "ACTIVE"
}
{ color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
{ color = cpair(colors.black, colors.orange), text = "RTU FAULT" },
{ color = cpair(colors.white, colors.gray), text = "IDLE" },
{ color = cpair(colors.black, colors.green), text = "ACTIVE" }
}
}
style.turbine = {
-- turbine states
-- turbine states<br>
---@see TURBINE_STATE
states = {
{
color = cpair(colors.black, colors.yellow),
text = "OFF-LINE"
},
{
color = cpair(colors.black, colors.orange),
text = "NOT FORMED"
},
{
color = cpair(colors.black, colors.orange),
text = "RTU FAULT"
},
{
color = cpair(colors.white, colors.gray),
text = "IDLE"
},
{
color = cpair(colors.black, colors.green),
text = "ACTIVE"
},
{
color = cpair(colors.black, colors.red),
text = "TRIP"
}
{ 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
-- 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"
},
{ 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: string[] }
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 = { "Auto", "Plutonium", "Polonium", "Antimatter" }
}
end
return style

View File

@@ -84,7 +84,7 @@ local function handle_packet(packet)
elseif est_ack == ESTABLISH_ACK.COLLISION then
error_msg = "another reactor PLC is connected with this reactor unit ID"
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
error_msg = "reactor PLC comms version does not match supervisor comms version, make sure both devices are up-to-date (ccmsi update ...)"
error_msg = "reactor PLC comms version does not match supervisor comms version, make sure both devices are up-to-date (ccmsi update)"
else
error_msg = "error: invalid reply from supervisor"
end
@@ -120,11 +120,15 @@ local function self_check()
self.self_check_pass = true
local cfg = self.settings
local modem = ppm.get_wireless_modem()
local reactor = ppm.get_fission_reactor()
local valid_cfg = plc.validate_config(self.settings)
local valid_cfg = plc.validate_config(cfg)
if cfg.Networked then
self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the reactor PLC")
end
self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the reactor PLC")
self.self_check_msg("> check fission reactor connected...", reactor ~= nil, "please connect the reactor PLC to the reactor's fission reactor logic adapter")
self.self_check_msg("> check fission reactor formed...")
-- this consumes events, but that is fine here
@@ -132,12 +136,12 @@ local function self_check()
self.self_check_msg("> check configuration...", valid_cfg, "go through Configure System and apply settings to set any missing settings and repair any corrupted ones")
if valid_cfg and modem then
if cfg.Networked and valid_cfg and modem then
self.self_check_msg("> check supervisor connection...")
-- init mac as needed
if self.settings.AuthKey and string.len(self.settings.AuthKey) >= 8 then
network.init_mac(self.settings.AuthKey)
if cfg.AuthKey and string.len(cfg.AuthKey) >= 8 then
network.init_mac(cfg.AuthKey)
else
network.deinit_mac()
end
@@ -145,12 +149,12 @@ local function self_check()
self.nic = network.nic(modem)
self.nic.closeAll()
self.nic.open(self.settings.PLC_Channel)
self.nic.open(cfg.PLC_Channel)
self.sv_addr = comms.BROADCAST
self.net_listen = true
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.PLC, self.settings.UnitID })
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.PLC, cfg.UnitID })
tcd.dispatch_unique(8, handle_timeout)
else

View File

@@ -592,7 +592,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if string.len(val) > val_max_w then
if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end

View File

@@ -53,6 +53,7 @@ style.btn_dis_fg_bg = cpair(colors.lightGray, colors.white)
---@class _plc_cfg_tool_ctl
local tool_ctl = {
launch_startup = false,
ask_config = false,
has_config = false,
viewing_config = false,
@@ -158,7 +159,7 @@ local function config_view(display)
TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the Reactor PLC configurator! Please select one of the following options."}
if tool_ctl.ask_config then
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device had no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
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 reactor PLC. 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
@@ -184,10 +185,18 @@ local function config_view(display)
main_pane.set_value(5)
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}
PushButton{parent=main_page,x=10,y=17,min_width=12,text="Self-Check",callback=function()main_pane.set_value(8)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(7)end,fg_bg=nav_fg_bg,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}
PushButton{parent=main_page,x=39,y=y_start,min_width=12,text="Self-Check",callback=function()main_pane.set_value(8)end,fg_bg=nav_fg_bg,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+2,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=main_page,x=39,y=y_start+4,min_width=12,text="Change Log",callback=function()main_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
if tool_ctl.ask_config then start_btn.disable() end
if not tool_ctl.has_config then
tool_ctl.view_cfg.disable()
@@ -293,7 +302,7 @@ function configurator.configure(ask_config)
println("configurator error: " .. error)
end
return status, error
return status, error, tool_ctl.launch_startup
end
return configurator

View File

@@ -40,6 +40,8 @@ local function init(panel)
local disabled_fg = style.fp.disabled_fg
local term_w, term_h = term.getSize()
local header = TextBox{parent=panel,y=1,text="FISSION REACTOR PLC - UNIT ?",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
header.register(databus.ps, "unit_id", function (id) header.set_value(util.c("FISSION REACTOR PLC - UNIT ", id)) end)
@@ -60,7 +62,7 @@ local function init(panel)
local modem = LED{parent=system,label="MODEM",colors=ind_grn}
if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,style.ind_bkg}}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.ind_bkg}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(databus.ps, "link_state", network.update)
else
@@ -121,7 +123,7 @@ local function init(panel)
-- status & controls
--
local status = Div{parent=panel,width=19,height=18,x=17,y=3}
local status = Div{parent=panel,width=term_w-32,height=18,x=17,y=3}
local active = LED{parent=status,x=2,width=12,label="RCT ACTIVE",colors=ind_grn}
@@ -131,14 +133,15 @@ local function init(panel)
emer_cool.register(databus.ps, "emer_cool", emer_cool.update)
end
local status_trip_rct = Rectangle{parent=status,width=20,height=3,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true}
local status_trip = Div{parent=status_trip_rct,width=18,height=1,fg_bg=s_hi_box}
local status_trip_rct = Rectangle{parent=status,height=3,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true}
local status_trip = Div{parent=status_trip_rct,height=1,fg_bg=s_hi_box}
local scram = LED{parent=status_trip,width=10,label="RPS TRIP",colors=ind_red,flash=true,period=flasher.PERIOD.BLINK_250_MS}
local controls_rct = Rectangle{parent=status,width=17,height=3,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true}
local controls = Div{parent=controls_rct,width=15,height=1,fg_bg=s_hi_box}
PushButton{parent=controls,x=1,y=1,min_width=7,text="SCRAM",callback=databus.rps_scram,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.black,colors.red_off)}
PushButton{parent=controls,x=9,y=1,min_width=7,text="RESET",callback=databus.rps_reset,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.black,colors.yellow_off)}
local controls_rct = Rectangle{parent=status,width=status.get_width()-2,height=3,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true}
local controls = Div{parent=controls_rct,width=controls_rct.get_width()-2,height=1,fg_bg=s_hi_box}
local button_padding = math.floor((controls.get_width() - 14) / 3)
PushButton{parent=controls,x=button_padding+1,y=1,min_width=7,text="SCRAM",callback=databus.rps_scram,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.black,colors.red_off)}
PushButton{parent=controls,x=(2*button_padding)+9,y=1,min_width=7,text="RESET",callback=databus.rps_reset,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.black,colors.yellow_off)}
active.register(databus.ps, "reactor_active", active.update)
scram.register(databus.ps, "rps_scram", scram.update)
@@ -147,9 +150,9 @@ local function init(panel)
-- about footer
--
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
local about = Div{parent=panel,width=15,height=2,y=term_h-1,fg_bg=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(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@@ -158,7 +161,7 @@ local function init(panel)
-- rps list
--
local rps = Rectangle{parent=panel,width=16,height=16,x=36,y=3,border=border(1,s_hi_box.bkg),thin=true,fg_bg=s_hi_box}
local rps = Rectangle{parent=panel,width=16,height=16,x=term_w-15,y=3,border=border(1,s_hi_box.bkg),thin=true,fg_bg=s_hi_box}
local rps_man = LED{parent=rps,label="MANUAL",colors=ind_red}
local rps_auto = LED{parent=rps,label="AUTOMATIC",colors=ind_red}
local rps_tmo = LED{parent=rps,label="TIMEOUT",colors=ind_red}

View File

@@ -23,12 +23,12 @@ local AUTO_ACK = comms.PLC_AUTO_ACK
local RPS_LIMITS = const.RPS_LIMITS
-- I sure hope the devs don't change this error message, not that it would have safety implications
-- I wish they didn't change it to be like this
-- specific errors thrown when scram/start is used that still count as success
local PCALL_SCRAM_MSG = "Scram requires the reactor to be active."
local PCALL_START_MSG = "Reactor is already active."
---@type plc_config
---@diagnostic disable-next-line: missing-fields
local config = {}
plc.config = config

View File

@@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "v1.8.12"
local R_PLC_VERSION = "v1.8.20"
local println = util.println
local println_ts = util.println_ts
@@ -169,12 +169,12 @@ local function main()
-- PLC init<br>
--- EVENT_CONSUMER: this function consumes events
local function init()
-- just booting up, no fission allowed (neutrons stay put thanks)
if (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
-- scram on boot if networked, otherwise leave the reactor be
if __shared_memory.networked and (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
smem_dev.reactor.scram()
end
-- front panel time!
-- setup front panel
if not renderer.ui_ready() then
local message
plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode)

318
rtu/config/check.lua Normal file
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 rsio = require("scada-common.rsio")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local rtu = require("rtu.rtu")
local redstone = require("rtu.config.redstone")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton")
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,
self_check_pass = true,
settings = nil, ---@type rtu_config
run_test_btn = nil, ---@type PushButton
sc_log = nil, ---@type ListBox
self_check_msg = nil ---@type function
}
-- report successful completion of the check
local function check_complete()
TextBox{parent=self.sc_log,text="> all tests passed!",fg_bg=cpair(colors.blue,colors._INHERIT)}
TextBox{parent=self.sc_log,text=""}
local more = Div{parent=self.sc_log,height=3,fg_bg=cpair(colors.gray,colors._INHERIT)}
TextBox{parent=more,text="if you still have a problem:"}
TextBox{parent=more,text="- check the wiki on GitHub"}
TextBox{parent=more,text="- ask for help on GitHub discussions or Discord"}
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.settings.SVR_Channel, self.settings.RTU_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.settings.RTU_Channel then
error_msg = "error: unknown receive channel"
elseif packet.scada_frame.remote_channel() == self.settings.SVR_Channel and packet.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
if packet.type == MGMT_TYPE.ESTABLISH then
if packet.length == 1 then
local est_ack = packet.data[1]
if est_ack== ESTABLISH_ACK.ALLOW then
self.self_check_msg(nil, true, "")
self.sv_addr = packet.scada_frame.src_addr()
send_sv(MGMT_TYPE.CLOSE, {})
if self.self_check_pass then check_complete() end
elseif est_ack == ESTABLISH_ACK.DENY then
error_msg = "error: supervisor connection denied"
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
error_msg = "RTU gateway comms version does not match supervisor comms version, make sure both devices are up-to-date (ccmsi update)"
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
self.run_test_btn.enable()
if error_msg then
self.self_check_msg(nil, false, error_msg)
end
end
-- handle supervisor connection failure
local function handle_timeout()
self.net_listen = false
self.run_test_btn.enable()
self.self_check_msg(nil, false, "make sure your supervisor is running, your channels are correct, trusted ranges are set properly (if enabled), facility keys match (if set), and if you are using wireless modems rather than ender modems, that your devices are close together in the same dimension")
end
-- 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
-- execute the self-check
local function self_check()
self.run_test_btn.disable()
self.sc_log.remove_all()
ppm.mount_all()
self.self_check_pass = true
local cfg = self.settings
local modem = ppm.get_wireless_modem()
local valid_cfg = rtu.validate_config(cfg)
self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the RTU gateway")
self.self_check_msg("> check gateway configuration...", valid_cfg, "go through Configure Gateway and apply settings to set any missing settings and repair any corrupted ones")
-- check redstone configurations
local phys = {} ---@type rtu_rs_definition[][]
local inputs = { [0] = {}, {}, {}, {}, {} }
for i = 1, #cfg.Redstone do
local entry = cfg.Redstone[i]
local name = entry.relay or "local"
if phys[name] == nil then phys[name] = {} end
table.insert(phys[entry.relay or "local"], entry)
end
for name, entries in pairs(phys) do
TextBox{parent=self.sc_log,text="> checking redstone @ "..name.."...",fg_bg=cpair(colors.blue,colors.white)}
local ifaces = {}
local bundled_sides = {}
for i = 1, #entries do
local entry = entries[i]
local ident = entry.side .. tri(entry.color, ":" .. rsio.color_name(entry.color), "")
local sc_dupe = util.table_contains(ifaces, ident)
local mixed = (bundled_sides[entry.side] and (entry.color == nil)) or (bundled_sides[entry.side] == false and (entry.color ~= nil))
local mixed_msg = util.trinary(bundled_sides[entry.side], "bundled entry(s) but this entry is not", "non-bundled entry(s) but this entry is")
self.self_check_msg("> check redstone " .. ident .. " unique...", not sc_dupe, "only one port should be set to a side/color combination")
self.self_check_msg("> check redstone " .. ident .. " bundle...", not mixed, "this side has " .. mixed_msg .. " bundled, which will not work")
self.self_check_msg("> check redstone " .. ident .. " valid...", redstone.validate(entry), "configuration invalid, please re-configure redstone entry")
if rsio.get_io_dir(entry.port) == rsio.IO_DIR.IN then
local in_dupe = util.table_contains(inputs[entry.unit or 0], entry.port)
self.self_check_msg("> check redstone " .. ident .. " input...", not in_dupe, "you cannot have multiple of the same input for a given unit or the facility ("..rsio.to_string(entry.port)..")")
end
bundled_sides[entry.side] = bundled_sides[entry.side] or entry.color ~= nil
table.insert(ifaces, ident)
end
end
-- check peripheral configurations
for i = 1, #cfg.Peripherals do
local entry = cfg.Peripherals[i]
local valid = false
if type(entry.name) == "string" then
self.self_check_msg("> check " .. entry.name .. " connected...", ppm.get_periph(entry.name), "please connect this device via a wired modem or direct contact and ensure the configuration matches what it connects as")
local p_type = ppm.get_type(entry.name)
if p_type == "boilerValve" then
valid = is_int_min_max(entry.index, 1, 2) and is_int_min_max(entry.unit, 1, 4)
elseif p_type == "turbineValve" then
valid = is_int_min_max(entry.index, 1, 3) and is_int_min_max(entry.unit, 1, 4)
elseif p_type == "solarNeutronActivator" then
valid = is_int_min_max(entry.unit, 1, 4)
elseif p_type == "dynamicValve" then
valid = (entry.unit == nil and is_int_min_max(entry.index, 1, 4)) or is_int_min_max(entry.unit, 1, 4)
elseif p_type == "environmentDetector" then
valid = (entry.unit == nil or is_int_min_max(entry.unit, 1, 4)) and util.is_int(entry.index)
else
valid = true
if p_type ~= nil and not (p_type == "inductionPort" or p_type == "spsPort") then
self.self_check_msg("> check " .. entry.name .. " valid...", false, "unrecognized device type")
end
end
end
if not valid then
self.self_check_msg("> check " .. entry.name .. " valid...", false, "configuration invalid, please re-configure peripheral entry")
end
end
if valid_cfg and modem then
self.self_check_msg("> check supervisor connection...")
-- init mac as needed
if cfg.AuthKey and string.len(cfg.AuthKey) >= 8 then
network.init_mac(cfg.AuthKey)
else
network.deinit_mac()
end
self.nic = network.nic(modem)
self.nic.closeAll()
self.nic.open(cfg.RTU_Channel)
self.sv_addr = comms.BROADCAST
self.net_listen = true
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.RTU, {} })
tcd.dispatch_unique(8, handle_timeout)
else
if self.self_check_pass then check_complete() end
self.run_test_btn.enable()
end
end
-- exit self check back home
---@param main_pane MultiPane
local function exit_self_check(main_pane)
tcd.abort(handle_timeout)
self.net_listen = false
self.run_test_btn.enable()
self.sc_log.remove_all()
main_pane.set_value(1)
end
local check = {}
-- create the self-check view
---@param main_pane MultiPane
---@param settings_cfg rtu_config
---@param check_sys Div
---@param style { [string]: cpair }
function check.create(main_pane, settings_cfg, check_sys, style)
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
self.settings = settings_cfg
local sc = Div{parent=check_sys,x=2,y=4,width=49}
TextBox{parent=check_sys,x=1,y=2,text=" RTU Gateway Self-Check",fg_bg=bw_fg_bg}
self.sc_log = ListBox{parent=sc,x=1,y=1,height=12,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local last_check = { nil, nil }
function self.self_check_msg(msg, success, fail_msg)
if type(msg) == "string" then
last_check[1] = Div{parent=self.sc_log,height=1}
local e = TextBox{parent=last_check[1],text=msg,fg_bg=bw_fg_bg}
last_check[2] = e.get_x()+string.len(msg)
end
if type(fail_msg) == "string" then
TextBox{parent=last_check[1],x=last_check[2],y=1,text=tri(success,"PASS","FAIL"),fg_bg=tri(success,cpair(colors.green,colors._INHERIT),cpair(colors.red,colors._INHERIT))}
if not success then
local fail = Div{parent=self.sc_log,height=#util.strwrap(fail_msg, 46)}
TextBox{parent=fail,x=3,text=fail_msg,fg_bg=cpair(colors.gray,colors.white)}
end
self.self_check_pass = self.self_check_pass and success
end
end
PushButton{parent=sc,x=1,y=14,text="\x1b Back",callback=function()exit_self_check(main_pane)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.run_test_btn = PushButton{parent=sc,x=40,y=14,min_width=10,text="Run Test",callback=function()self_check()end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
end
-- handle incoming modem messages
---@param side string
---@param sender integer
---@param reply_to integer
---@param message any
---@param distance integer
function check.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 check

View File

@@ -90,6 +90,10 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
load_settings(settings_cfg, true)
load_settings(ini_cfg)
peri_pane.set_value(5)
-- for return to list from saved screen
tmp_cfg.Peripherals = tool_ctl.deep_copy_peri(ini_cfg.Peripherals)
tool_ctl.gen_peri_summary()
else
peri_pane.set_value(6)
end
@@ -145,7 +149,7 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
reposition("This SNA is for reactor unit # .", 46, 1, 31, 4, 7)
self.p_idx.hide()
self.p_assign_btn.hide(true)
self.p_desc_ext.set_value("Before adding lots of SNAs: multiply the \"PEAK\" rate on the flow monitor (after connecting at least 1 SNA) by 10 to get the mB/t of waste that they can process. Enough SNAs to provide 2x to 3x of your max burn rate should be a good margin to catch up after night or cloudy weather. Too many devices (such as SNAs) on one RTU can cause lag.")
self.p_desc_ext.set_value("Warning: too many devices on one RTU Gateway can cause lag. Note that 10x the \"PEAK\x1a\" rate on the flow monitor gives you the mB/t of waste that the SNA(s) can process. Enough SNAs to provide 2x to 3x of that unit's max burn rate should be a good margin to catch up after night or cloudy weather.")
elseif type == "dynamicValve" then
reposition("This is the below system's # dynamic tank.", 29, 4, 17, 6, 8)
self.p_assign_btn.show()

View File

@@ -1,4 +1,5 @@
local constants = require("scada-common.constants")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
local util = require("scada-common.util")
@@ -18,8 +19,10 @@ local NumberField = require("graphics.elements.form.NumberField")
---@class rtu_rs_definition
---@field unit integer|nil
---@field port IO_PORT
---@field relay string|nil
---@field side side
---@field color color|nil
---@field invert true|nil
local tri = util.trinary
@@ -32,15 +35,19 @@ local IO_MODE = rsio.IO_MODE
local LEFT = core.ALIGN.LEFT
local self = {
rs_cfg_port = 1, ---@type IO_PORT
rs_cfg_editing = false, ---@type integer|false
rs_cfg_phy = false, ---@type string|nil|false
rs_cfg_port = 1, ---@type IO_PORT
rs_cfg_editing = false, ---@type integer|false
rs_cfg_selection = nil, ---@type TextBox
rs_cfg_unit_l = nil, ---@type TextBox
rs_cfg_unit = nil, ---@type NumberField
rs_cfg_side_l = nil, ---@type TextBox
rs_cfg_color = nil, ---@type Radio2D
rs_cfg_shortcut = nil ---@type TextBox
rs_cfg_selection = nil, ---@type TextBox
rs_cfg_unit_l = nil, ---@type TextBox
rs_cfg_unit = nil, ---@type NumberField
rs_cfg_side_l = nil, ---@type TextBox
rs_cfg_bundled = nil, ---@type Checkbox
rs_cfg_color = nil, ---@type Radio2D
rs_cfg_inverted = nil, ---@type Checkbox
rs_cfg_shortcut = nil, ---@type TextBox
rs_cfg_advanced = nil ---@type PushButton
}
-- rsio port descriptions
@@ -73,11 +80,12 @@ local PORT_DESC_MAP = {
{ IO.R_PLC_FAULT, "RPS PLC Fault" },
{ IO.R_PLC_TIMEOUT, "RPS Supervisor Timeout" },
{ IO.U_ALARM, "Unit Alarm" },
{ IO.U_EMER_COOL, "Unit Emergency Cool. Valve" }
{ IO.U_EMER_COOL, "Unit Emergency Cool. Valve" },
{ IO.U_AUX_COOL, "Unit Auxiliary Cool. Valve" }
}
-- designation (0 = facility, 1 = unit)
local PORT_DSGN = { [-1] = 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 }
local PORT_DSGN = { [-1] = 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1 }
assert(#PORT_DESC_MAP == rsio.NUM_PORTS)
assert(#PORT_DSGN == rsio.NUM_PORTS)
@@ -103,8 +111,34 @@ local function color_to_idx(color)
end
end
-- select the subset of redstone entries assigned to the given phy
---@param cfg rtu_rs_definition[] the full redstone entry list
---@param phy string|nil which phy to get redstone entries for
---@param invert boolean? true to get all except this phy
---@return rtu_rs_definition[]
local function redstone_subset(cfg, phy, invert)
local subset = {}
for i = 1, #cfg do
if ((not invert) and cfg[i].relay == phy) or (invert and cfg[i].relay ~= phy) then
table.insert(subset, cfg[i])
end
end
return subset
end
local redstone = {}
-- validate a redstone entry
---@param def rtu_rs_definition
function redstone.validate(def)
return tri(PORT_DSGN[def.port] == 1, util.is_int(def.unit) and def.unit > 0 and def.unit <= 4, def.unit == nil) and
rsio.is_valid_port(def.port) and
rsio.is_valid_side(def.side) and
(def.color == nil or (rsio.is_digital(def.port) and rsio.is_color(def.color)))
end
-- create the redstone configuration view
---@param tool_ctl _rtu_cfg_tool_ctl
---@param main_pane MultiPane
@@ -123,20 +157,89 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
--#region Redstone
local rs_c_1 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_2 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_3 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_4 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_5 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_6 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_7 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_1 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_2 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_3 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_4 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_5 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_6 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_7 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_8 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_9 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_10 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7}}
local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7,rs_c_8,rs_c_9,rs_c_10}}
TextBox{parent=rs_cfg,x=1,y=2,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)}
local header = TextBox{parent=rs_cfg,x=1,y=2,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)}
TextBox{parent=rs_c_1,x=1,y=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg}
local rs_list = ListBox{parent=rs_c_1,x=1,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
--#region Interface Selection
TextBox{parent=rs_c_1,x=1,y=1,text="Configure this computer or a redstone relay."}
local iface_list = ListBox{parent=rs_c_1,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
-- update relay interface list
function tool_ctl.update_relay_list()
local mounts = ppm.list_mounts()
iface_list.remove_all()
-- assemble list of configured relays
local relays = {}
for i = 1, #tmp_cfg.Redstone do
local def = tmp_cfg.Redstone[i]
if def.relay and not util.table_contains(relays, def.relay) then
table.insert(relays, def.relay)
end
end
-- add unconfigured connected relays
for name, entry in pairs(mounts) do
if entry.type == "redstone_relay" and not util.table_contains(relays, name) then
table.insert(relays, name)
end
end
local function config_rs(name)
header.set_value(" Redstone Connections (" .. name .. ")")
self.rs_cfg_phy = tri(name == "local", nil, name)
tool_ctl.gen_rs_summary()
rs_pane.set_value(2)
end
local line = Div{parent=iface_list,height=2,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=1,y=1,text="@ local",fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=3,y=2,text="This Computer",fg_bg=cpair(colors.gray,colors.white)}
local count = #redstone_subset(ini_cfg.Redstone, nil)
TextBox{parent=line,x=33,y=2,width=16,alignment=core.ALIGN.RIGHT,text=count.." connections",fg_bg=cpair(colors.gray,colors.white)}
PushButton{parent=line,x=41,y=1,min_width=8,height=1,text="CONFIG",callback=function()config_rs("local")end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
for i = 1, #relays do
local name = relays[i]
line = Div{parent=iface_list,height=2,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=1,y=1,text="@ "..name,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=3,y=2,text="Redstone Relay",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=line,x=18,y=2,text=tri(mounts[name],"ONLINE","OFFLINE"),fg_bg=cpair(tri(mounts[name],colors.green,colors.red),colors.white)}
count = #redstone_subset(ini_cfg.Redstone, name)
TextBox{parent=line,x=33,y=2,width=16,alignment=core.ALIGN.RIGHT,text=count.." connections",fg_bg=cpair(colors.gray,colors.white)}
PushButton{parent=line,x=41,y=1,min_width=8,height=1,text="CONFIG",callback=function()config_rs(name)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
end
end
tool_ctl.update_relay_list()
PushButton{parent=rs_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=rs_c_1,x=27,y=14,min_width=23,text="I don't see my relay!",callback=function()rs_pane.set_value(10)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Configuration List
TextBox{parent=rs_c_2,x=1,y=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg}
local rs_list = ListBox{parent=rs_c_2,x=1,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function rs_revert()
tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone)
@@ -144,39 +247,47 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
end
local function rs_apply()
settings.set("Redstone", tmp_cfg.Redstone)
-- add the changed data to the existing saved data
local new_data = redstone_subset(tmp_cfg.Redstone, self.rs_cfg_phy)
local new_save = redstone_subset(ini_cfg.Redstone, self.rs_cfg_phy, true)
for i = 1, #new_data do table.insert(new_save, new_data[i]) end
settings.set("Redstone", new_save)
if settings.save("/rtu.settings") then
load_settings(settings_cfg, true)
load_settings(ini_cfg)
rs_pane.set_value(4)
else
rs_pane.set_value(5)
-- for return to list from saved screen
-- this will delete unsaved changes for other phy's, which is acceptable
tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone)
tool_ctl.gen_rs_summary()
tool_ctl.update_relay_list()
else
rs_pane.set_value(6)
end
end
PushButton{parent=rs_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}
local rs_revert_btn = PushButton{parent=rs_c_1,x=8,y=14,min_width=16,text="Revert Changes",callback=rs_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=rs_c_1,x=35,y=14,min_width=7,text="New +",callback=function()rs_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
local rs_apply_btn = PushButton{parent=rs_c_1,x=43,y=14,min_width=7,text="Apply",callback=rs_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
local function rs_back()
self.rs_cfg_phy = false
rs_pane.set_value(1)
header.set_value(" Redstone Connections")
end
TextBox{parent=rs_c_6,x=1,y=1,height=5,text="You already configured this input. There can only be one entry for each input.\n\nPlease select a different port."}
PushButton{parent=rs_c_6,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=rs_back,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local rs_revert_btn = PushButton{parent=rs_c_2,x=8,y=14,min_width=16,text="Revert Changes",callback=rs_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=rs_c_2,x=35,y=14,min_width=7,text="New +",callback=function()rs_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
local rs_apply_btn = PushButton{parent=rs_c_2,x=43,y=14,min_width=7,text="Apply",callback=rs_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
TextBox{parent=rs_c_2,x=1,y=1,text="Select one of the below ports to use."}
--#endregion
--#region Port Selection
local rs_ports = ListBox{parent=rs_c_2,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
TextBox{parent=rs_c_3,x=1,y=1,text="Select one of the below ports to use."}
local rs_ports = ListBox{parent=rs_c_3,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function new_rs(port)
if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then
for i = 1, #tmp_cfg.Redstone do
if tmp_cfg.Redstone[i].port == port then
rs_pane.set_value(6)
return
end
end
end
self.rs_cfg_editing = false
local text
@@ -185,6 +296,8 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
self.rs_cfg_color.hide(true)
self.rs_cfg_shortcut.show()
self.rs_cfg_side_l.set_value("Output Side")
self.rs_cfg_bundled.enable()
self.rs_cfg_advanced.disable()
text = "You selected the ALL_WASTE shortcut."
else
self.rs_cfg_shortcut.hide(true)
@@ -195,6 +308,19 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
local io_mode = rsio.get_io_mode(port)
local inv = tri(rsio.digital_is_active(port, IO_LVL.LOW) == true, "inverted ", "")
if rsio.is_analog(port) then
self.rs_cfg_bundled.set_value(false)
self.rs_cfg_bundled.disable()
self.rs_cfg_color.disable()
self.rs_cfg_inverted.set_value(false)
self.rs_cfg_advanced.disable()
else
self.rs_cfg_bundled.enable()
if self.rs_cfg_bundled.get_value() then self.rs_cfg_color.enable() else self.rs_cfg_color.disable() end
self.rs_cfg_inverted.set_value(false)
self.rs_cfg_advanced.enable()
end
if io_mode == IO_MODE.DIGITAL_IN then
io_type = inv .. "digital input "
elseif io_mode == IO_MODE.DIGITAL_OUT then
@@ -218,7 +344,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
self.rs_cfg_selection.set_value(text)
self.rs_cfg_port = port
rs_pane.set_value(3)
rs_pane.set_value(4)
end
-- add entries to redstone option list
@@ -239,43 +365,43 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
TextBox{parent=entry,x=22,y=1,text=PORT_DESC_MAP[i][2],fg_bg=cpair(colors.gray,colors.white)}
end
PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_3,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.rs_cfg_selection = TextBox{parent=rs_c_3,x=1,y=1,height=2,text=""}
--#endregion
--#region Port Configuration
PushButton{parent=rs_c_3,x=36,y=3,text="What's that?",min_width=14,callback=function()rs_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.rs_cfg_selection = TextBox{parent=rs_c_4,x=1,y=1,height=2,text=""}
TextBox{parent=rs_c_7,x=1,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"}
TextBox{parent=rs_c_7,x=1,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"}
TextBox{parent=rs_c_7,x=1,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"}
PushButton{parent=rs_c_7,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_4,x=36,y=3,text="What's that?",min_width=14,callback=function()rs_pane.set_value(8)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.rs_cfg_side_l = TextBox{parent=rs_c_3,x=1,y=4,width=11,text="Output Side"}
local side = Radio2D{parent=rs_c_3,x=1,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red}
self.rs_cfg_side_l = TextBox{parent=rs_c_4,x=1,y=4,width=11,text="Output Side"}
local side = Radio2D{parent=rs_c_4,x=1,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red}
self.rs_cfg_unit_l = TextBox{parent=rs_c_3,x=25,y=7,width=7,text="Unit ID"}
self.rs_cfg_unit = NumberField{parent=rs_c_3,x=33,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
self.rs_cfg_unit_l = TextBox{parent=rs_c_4,x=25,y=7,width=7,text="Unit ID"}
self.rs_cfg_unit = NumberField{parent=rs_c_4,x=33,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
local function set_bundled(bundled)
if bundled then self.rs_cfg_color.enable() else self.rs_cfg_color.disable() end
end
self.rs_cfg_shortcut = TextBox{parent=rs_c_3,x=1,y=9,height=4,text="This shortcut will add entries for each of the 4 waste outputs. If you select bundled, 4 colors will be assigned to the selected side. Otherwise, 4 default sides will be used."}
self.rs_cfg_shortcut = TextBox{parent=rs_c_4,x=1,y=9,height=4,text="This shortcut will add entries for each of the 4 waste outputs. If you select bundled, 4 colors will be assigned to the selected side. Otherwise, 4 default sides will be used."}
self.rs_cfg_shortcut.hide(true)
local bundled = Checkbox{parent=rs_c_3,x=1,y=7,label="Is Bundled?",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=set_bundled}
self.rs_cfg_color = Radio2D{parent=rs_c_3,x=1,y=9,rows=4,columns=4,default=1,options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
self.rs_cfg_bundled = Checkbox{parent=rs_c_4,x=1,y=7,label="Is Bundled?",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=set_bundled,disable_fg_bg=g_lg_fg_bg}
self.rs_cfg_color = Radio2D{parent=rs_c_4,x=1,y=9,rows=4,columns=4,default=1,options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
self.rs_cfg_color.disable()
local rs_err = TextBox{parent=rs_c_3,x=8,y=14,width=30,text="Unit ID must be within 1 to 4.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local rs_err = TextBox{parent=rs_c_4,x=8,y=14,width=30,text="Unit ID invalid.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
rs_err.hide(true)
local function back_from_rs_opts()
rs_err.hide(true)
if self.rs_cfg_editing ~= false then rs_pane.set_value(1) else rs_pane.set_value(2) end
if self.rs_cfg_editing ~= false then rs_pane.set_value(2) else rs_pane.set_value(3) end
end
local function save_rs_entry()
assert(self.rs_cfg_phy ~= false, "tried to save a redstone entry without a phy")
local port = self.rs_cfg_port
local u = tonumber(self.rs_cfg_unit.get_value())
@@ -287,11 +413,23 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
local def = {
unit = tri(PORT_DSGN[port] == 1, u, nil),
port = port,
relay = self.rs_cfg_phy,
side = side_options_map[side.get_value()],
color = tri(bundled.get_value(), color_options_map[self.rs_cfg_color.get_value()], nil)
color = tri(self.rs_cfg_bundled.get_value() and rsio.is_digital(port), color_options_map[self.rs_cfg_color.get_value()], nil),
invert = self.rs_cfg_inverted.get_value() or nil
}
if self.rs_cfg_editing == false then
-- check for duplicate inputs for this unit/facility
if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then
for i = 1, #tmp_cfg.Redstone do
if tmp_cfg.Redstone[i].port == port and tmp_cfg.Redstone[i].unit == def.unit then
rs_pane.set_value(7)
return
end
end
end
table.insert(tmp_cfg.Redstone, def)
else
def.port = tmp_cfg.Redstone[self.rs_cfg_editing].port
@@ -304,33 +442,55 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
table.insert(tmp_cfg.Redstone, {
unit = tri(PORT_DSGN[IO.WASTE_PU + i] == 1, u, nil),
port = IO.WASTE_PU + i,
side = tri(bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]),
color = tri(bundled.get_value(), default_colors[i + 1], nil)
relay = self.rs_cfg_phy,
side = tri(self.rs_cfg_bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]),
color = tri(self.rs_cfg_bundled.get_value(), default_colors[i + 1], nil)
})
end
end
rs_pane.set_value(1)
rs_pane.set_value(2)
tool_ctl.gen_rs_summary()
side.set_value(1)
bundled.set_value(false)
self.rs_cfg_bundled.set_value(false)
self.rs_cfg_color.set_value(1)
self.rs_cfg_color.disable()
self.rs_cfg_inverted.set_value(false)
self.rs_cfg_advanced.disable()
else rs_err.show() end
end
PushButton{parent=rs_c_3,x=1,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_3,x=41,y=14,min_width=9,text="Confirm",callback=save_rs_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_4,x=1,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.rs_cfg_advanced = PushButton{parent=rs_c_4,x=30,y=14,min_width=10,text="Advanced",callback=function()rs_pane.set_value(9)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=rs_c_4,x=41,y=14,min_width=9,text="Confirm",callback=save_rs_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_4,x=1,y=1,text="Settings saved!"}
PushButton{parent=rs_c_4,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_4,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
TextBox{parent=rs_c_5,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=rs_c_5,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_5,x=1,y=1,text="Settings saved!"}
PushButton{parent=rs_c_5,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_6,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=rs_c_6,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_6,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_7,x=1,y=1,height=6,text="You already configured this input for this facility/unit assignment. There can only be one entry for each input per each unit or the facility (for facility inputs).\n\nPlease select a different port."}
PushButton{parent=rs_c_7,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_8,x=1,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"}
TextBox{parent=rs_c_8,x=1,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"}
TextBox{parent=rs_c_8,x=1,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"}
PushButton{parent=rs_c_8,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_9,x=1,y=1,height=5,text="Advanced Options"}
self.rs_cfg_inverted = Checkbox{parent=rs_c_9,x=1,y=3,label="Invert",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=function()end,disable_fg_bg=g_lg_fg_bg}
TextBox{parent=rs_c_9,x=3,y=4,height=4,text="Digital I/O is already inverted (or not) based on intended use. If you have a non-standard setup, you can use this option to avoid needing a redstone inverter.",fg_bg=cpair(colors.gray,colors.lightGray)}
PushButton{parent=rs_c_9,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_10,x=1,y=1,height=10,text="Make sure your relay is either touching the RTU gateway or connected via wired modems. There should be a wired modem on a side of the RTU gateway then one on the device, connected by a cable. The modem on the device needs to be right clicked to connect it (which will turn its border red), at which point the peripheral name will be shown in the chat."}
PushButton{parent=rs_c_10,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Tool Functions
@@ -356,6 +516,16 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
text = text .. "the facility)."
end
if rsio.is_analog(def.port) then
self.rs_cfg_bundled.set_value(false)
self.rs_cfg_bundled.disable()
self.rs_cfg_advanced.disable()
else
self.rs_cfg_bundled.enable()
self.rs_cfg_bundled.set_value(def.color ~= nil)
self.rs_cfg_advanced.enable()
end
local value = 1
if def.color ~= nil then
value = color_to_idx(def.color)
@@ -367,9 +537,9 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
self.rs_cfg_selection.set_value(text)
self.rs_cfg_side_l.set_value(tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "Input Side", "Output Side"))
side.set_value(side_to_idx(def.side))
bundled.set_value(def.color ~= nil)
self.rs_cfg_color.set_value(value)
rs_pane.set_value(3)
self.rs_cfg_inverted.set_value(def.invert or false)
rs_pane.set_value(4)
end
local function delete_rs_entry(idx)
@@ -379,33 +549,41 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
-- generate the redstone summary list
function tool_ctl.gen_rs_summary()
assert(self.rs_cfg_phy ~= false, "tried to generate a summary without a phy set")
rs_list.remove_all()
local modified = #ini_cfg.Redstone ~= #tmp_cfg.Redstone
local ini = redstone_subset(ini_cfg.Redstone, self.rs_cfg_phy)
local tmp = redstone_subset(tmp_cfg.Redstone, self.rs_cfg_phy)
local modified = #ini ~= #tmp
for i = 1, #tmp_cfg.Redstone do
local def = tmp_cfg.Redstone[i]
local name = rsio.to_string(def.port)
local io_dir = tri(rsio.get_io_mode(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
local conn = def.side
local unit = util.strval(def.unit or "F")
if def.relay == self.rs_cfg_phy then
local name = rsio.to_string(def.port)
local io_dir = tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
local io_c = tri(rsio.is_digital(def.port), colors.blue, colors.purple)
local conn = def.side
local unit = util.strval(def.unit or "F")
if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end
if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end
local entry = Div{parent=rs_list,height=1}
TextBox{parent=entry,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=entry,x=2,y=1,width=14,text=name}
TextBox{parent=entry,x=16,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=entry,x=33,y=1,width=1,text=unit,fg_bg=cpair(colors.gray,colors.white)}
PushButton{parent=entry,x=35,y=1,min_width=6,height=1,text="EDIT",callback=function()edit_rs_entry(i)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
PushButton{parent=entry,x=41,y=1,min_width=8,height=1,text="DELETE",callback=function()delete_rs_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
local entry = Div{parent=rs_list,height=1}
TextBox{parent=entry,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(tri(def.invert,colors.orange,io_c),colors.white)}
TextBox{parent=entry,x=2,y=1,width=14,text=name}
TextBox{parent=entry,x=16,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=entry,x=33,y=1,width=1,text=unit,fg_bg=cpair(colors.gray,colors.white)}
PushButton{parent=entry,x=35,y=1,min_width=6,height=1,text="EDIT",callback=function()edit_rs_entry(i)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
PushButton{parent=entry,x=41,y=1,min_width=8,height=1,text="DELETE",callback=function()delete_rs_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
if not modified then
local a = ini_cfg.Redstone[i]
local b = tmp_cfg.Redstone[i]
if not modified then
local a = ini_cfg.Redstone[i]
local b = tmp_cfg.Redstone[i]
modified = (a.unit ~= b.unit) or (a.port ~= b.port) or (a.side ~= b.side) or (a.color ~= b.color)
modified = (a.unit ~= b.unit) or (a.port ~= b.port) or (a.relay ~= b.relay) or (a.side ~= b.side) or (a.color ~= b.color) or (a.invert ~= b.invert)
end
end
end

View File

@@ -646,7 +646,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if string.len(val) > val_max_w then
if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end

View File

@@ -7,6 +7,7 @@ local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local check = require("rtu.config.check")
local peripherals = require("rtu.config.peripherals")
local redstone = require("rtu.config.redstone")
local system = require("rtu.config.system")
@@ -34,7 +35,9 @@ local changes = {
{ "v1.7.9", { "ConnTimeout can now have a fractional part" } },
{ "v1.7.15", { "Added front panel UI theme", "Added color accessibility modes" } },
{ "v1.9.2", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
{ "v1.10.2", { "Re-organized peripheral configuration UI, resulting in some input fields being re-ordered" } }
{ "v1.10.2", { "Re-organized peripheral configuration UI, resulting in some input fields being re-ordered" } },
{ "v1.11.8", { "Added advanced option to invert digital redstone signals" } },
{ "v1.12.0", { "Added support for redstone relays" } }
}
---@class rtu_configurator
@@ -55,6 +58,7 @@ style.btn_dis_fg_bg = cpair(colors.lightGray, colors.white)
---@class _rtu_cfg_tool_ctl
local tool_ctl = {
launch_startup = false,
ask_config = false,
has_config = false,
viewing_config = false,
@@ -73,6 +77,7 @@ local tool_ctl = {
gen_summary = nil, ---@type function
load_legacy = nil, ---@type function
update_peri_list = nil, ---@type function
update_relay_list = nil, ---@type function
gen_peri_summary = nil, ---@type function
gen_rs_summary = nil, ---@type function
}
@@ -114,6 +119,7 @@ local fields = {
}
-- deep copy peripherals defs
---@param data rtu_peri_definition[]
function tool_ctl.deep_copy_peri(data)
local array = {}
for _, d in ipairs(data) do table.insert(array, { unit = d.unit, index = d.index, name = d.name }) end
@@ -121,9 +127,10 @@ function tool_ctl.deep_copy_peri(data)
end
-- deep copy redstone defs
---@param data rtu_rs_definition[]
function tool_ctl.deep_copy_rs(data)
local array = {}
for _, d in ipairs(data) do table.insert(array, { unit = d.unit, port = d.port, side = d.side, color = d.color }) end
for _, d in ipairs(data) do table.insert(array, { unit = d.unit, port = d.port, relay = d.relay, side = d.side, color = d.color, invert = d.invert }) end
return array
end
@@ -168,15 +175,16 @@ local function config_view(display)
local changelog = Div{parent=root_pane_div,x=1,y=1}
local peri_cfg = Div{parent=root_pane_div,x=1,y=1}
local rs_cfg = Div{parent=root_pane_div,x=1,y=1}
local check_sys = Div{parent=root_pane_div,x=1,y=1}
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,spkr_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog,peri_cfg,rs_cfg}}
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,spkr_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog,peri_cfg,rs_cfg,check_sys}}
--#region Main Page
local y_start = 2
if tool_ctl.ask_config then
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device had no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
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 RTU gateway. 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
else
TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the RTU gateway configurator! Please select one of the following options."}
@@ -202,7 +210,6 @@ local function config_view(display)
end
local function show_rs_conns()
tool_ctl.gen_rs_summary()
main_pane.set_value(9)
end
@@ -218,9 +225,18 @@ local function config_view(display)
main_pane.set_value(5)
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}
tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(7)end,fg_bg=nav_fg_bg,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}
PushButton{parent=main_page,x=39,y=y_start,min_width=12,text="Self-Check",callback=function()main_pane.set_value(10)end,fg_bg=nav_fg_bg,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+2,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=main_page,x=39,y=y_start+4,min_width=12,text="Change Log",callback=function()main_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
if tool_ctl.ask_config then start_btn.disable() end
if not tool_ctl.has_config then
tool_ctl.view_gw_cfg.disable()
@@ -274,6 +290,12 @@ local function config_view(display)
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
--#region Self-Check
check.create(main_pane, settings_cfg, check_sys, style)
--#endregion
end
-- reset terminal screen
@@ -308,7 +330,7 @@ function configurator.configure(ask_config)
config_view(display)
while true do
local event, param1, param2, param3 = util.pull_event()
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event
if event == "timer" then
@@ -321,14 +343,18 @@ function configurator.configure(ask_config)
if k_e then display.handle_key(k_e) end
elseif event == "paste" then
display.handle_paste(param1)
elseif event == "modem_message" then
check.receive_sv(param1, param2, param3, param4, param5)
elseif event == "peripheral_detach" then
---@diagnostic disable-next-line: discard-returns
ppm.handle_unmount(param1)
tool_ctl.update_peri_list()
tool_ctl.update_relay_list()
elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1)
tool_ctl.update_peri_list()
tool_ctl.update_relay_list()
end
if event == "terminate" then return end
@@ -346,7 +372,7 @@ function configurator.configure(ask_config)
println("configurator error: " .. error)
end
return status, error
return status, error, tool_ctl.launch_startup
end
return configurator

View File

@@ -11,10 +11,14 @@ local digital_write = rsio.digital_write
-- create new redstone device
---@nodiscard
---@param relay? table optional redstone relay to use instead of the computer's redstone interface
---@return rtu_rs_device interface, boolean faulted
function redstone_rtu.new()
function redstone_rtu.new(relay)
local unit = rtu.init_unit()
-- physical interface to use
local phy = relay or rs
-- get RTU interface
local interface = unit.interface()
@@ -30,85 +34,114 @@ function redstone_rtu.new()
write_holding_reg = interface.write_holding_reg
}
-- change the phy in use (a relay or rs)
---@param new_phy table
function public.remount_phy(new_phy) phy = new_phy end
-- NOTE: for runtime speed, inversion logic results in extra code here but less code when functions are called
-- link digital input
---@param side string
---@param color integer
function public.link_di(side, color)
local f_read ---@type function
---@param invert boolean|nil
---@return integer count count of digital inputs
function public.link_di(side, color, invert)
local f_read ---@type function
if color then
f_read = function ()
return digital_read(rs.testBundledInput(side, color))
if invert then
f_read = function () return digital_read(not phy.testBundledInput(side, color)) end
else
f_read = function () return digital_read(phy.testBundledInput(side, color)) end
end
else
f_read = function ()
return digital_read(rs.getInput(side))
if invert then
f_read = function () return digital_read(not phy.getInput(side)) end
else
f_read = function () return digital_read(phy.getInput(side)) end
end
end
unit.connect_di(f_read)
return unit.connect_di(f_read)
end
-- link digital output
---@param side string
---@param color integer
function public.link_do(side, color)
local f_read ---@type function
local f_write ---@type function
---@param invert boolean|nil
---@return integer count count of digital outputs
function public.link_do(side, color, invert)
local f_read ---@type function
local f_write ---@type function
if color then
f_read = function ()
return digital_read(colors.test(rs.getBundledOutput(side), color))
end
if invert then
f_read = function () return digital_read(not colors.test(phy.getBundledOutput(side), color)) end
f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
local output = rs.getBundledOutput(side)
f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
local output = phy.getBundledOutput(side)
if digital_write(level) then
output = colors.combine(output, color)
else
output = colors.subtract(output, color)
-- inverted conditions
if digital_write(level) then
output = colors.subtract(output, color)
else output = colors.combine(output, color) end
phy.setBundledOutput(side, output)
end
end
else
f_read = function () return digital_read(colors.test(phy.getBundledOutput(side), color)) end
rs.setBundledOutput(side, output)
f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
local output = phy.getBundledOutput(side)
if digital_write(level) then
output = colors.combine(output, color)
else output = colors.subtract(output, color) end
phy.setBundledOutput(side, output)
end
end
end
else
f_read = function ()
return digital_read(rs.getOutput(side))
end
if invert then
f_read = function () return digital_read(not phy.getOutput(side)) end
f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
rs.setOutput(side, digital_write(level))
f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
phy.setOutput(side, not digital_write(level))
end
end
else
f_read = function () return digital_read(phy.getOutput(side)) end
f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
phy.setOutput(side, digital_write(level))
end
end
end
end
unit.connect_coil(f_read, f_write)
return unit.connect_coil(f_read, f_write)
end
-- link analog input
---@param side string
---@return integer count count of analog inputs
function public.link_ai(side)
unit.connect_input_reg(
function ()
return rs.getAnalogInput(side)
end
)
return unit.connect_input_reg(function () return phy.getAnalogInput(side) end)
end
-- link analog output
---@param side string
---@return integer count count of analog outputs
function public.link_ao(side)
unit.connect_holding_reg(
function ()
return rs.getAnalogOutput(side)
end,
function (value)
rs.setAnalogOutput(side, value)
end
return unit.connect_holding_reg(
function () return phy.getAnalogOutput(side) end,
function (value) phy.setAnalogOutput(side, value) end
)
end

View File

@@ -399,43 +399,41 @@ function modbus.new(rtu_dev, use_parallel_read)
return public
end
-- create an error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@param code MODBUS_EXCODE exception code
---@return modbus_packet reply
local function excode_reply(packet, code)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
reply.make(packet.txn_id, packet.unit_id, fcode, { code })
return reply
end
-- return a SERVER_DEVICE_FAIL error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply
function modbus.reply__srv_device_fail(packet) return excode_reply(packet, MODBUS_EXCODE.SERVER_DEVICE_FAIL) end
-- return a SERVER_DEVICE_BUSY error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply
function modbus.reply__srv_device_busy(packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.SERVER_DEVICE_BUSY }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
function modbus.reply__srv_device_busy(packet) return excode_reply(packet, MODBUS_EXCODE.SERVER_DEVICE_BUSY) end
-- return a NEG_ACKNOWLEDGE error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply
function modbus.reply__neg_ack(packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.NEG_ACKNOWLEDGE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
function modbus.reply__neg_ack(packet) return excode_reply(packet, MODBUS_EXCODE.NEG_ACKNOWLEDGE) end
-- return a GATEWAY_PATH_UNAVAILABLE error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply
function modbus.reply__gw_unavailable(packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
function modbus.reply__gw_unavailable(packet) return excode_reply(packet, MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE) end
return modbus

View File

@@ -19,7 +19,8 @@ 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 LINK_STATE = types.PANEL_LINK_STATE
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local ALIGN = core.ALIGN
@@ -35,13 +36,15 @@ local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC
local function init(panel, units)
local disabled_fg = style.fp.disabled_fg
local term_w, term_h = term.getSize()
TextBox{parent=panel,y=1,text="RTU GATEWAY",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
--
-- system indicators
--
local system = Div{parent=panel,width=14,height=18,x=2,y=3}
local system = Div{parent=panel,width=14,height=term_h-5,x=2,y=3}
local on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn}
@@ -53,7 +56,7 @@ local function init(panel, units)
local modem = LED{parent=system,label="MODEM",colors=ind_grn}
if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,style.ind_bkg}}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.ind_bkg}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(databus.ps, "link_state", network.update)
else
@@ -100,17 +103,17 @@ local function init(panel, units)
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=disabled_fg}
TextBox{parent=system,x=1,y=14,text="SPEAKERS",width=8,fg_bg=style.fp.text_fg}
local speaker_count = DataIndicator{parent=system,x=10,y=14,label="",format="%3d",value=0,width=3,fg_bg=style.theme.field_box}
TextBox{parent=system,y=term_h-5,text="SPEAKERS",width=8,fg_bg=style.fp.text_fg}
local speaker_count = DataIndicator{parent=system,x=10,y=term_h-5,label="",format="%3d",value=0,width=3,fg_bg=style.theme.field_box}
speaker_count.register(databus.ps, "speaker_count", speaker_count.update)
--
-- about label
--
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
local about = Div{parent=panel,width=15,height=2,y=term_h-1,fg_bg=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(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@@ -119,38 +122,53 @@ local function init(panel, units)
-- unit status list
--
local threads = Div{parent=panel,width=8,height=18,x=17,y=3}
local threads = Div{parent=panel,width=8,height=term_h-3,x=17,y=3}
-- display up to 16 units
local list_length = math.min(#units, 16)
-- display as many units as we can with 1 line of padding above and below
local list_length = math.min(#units, term_h - 3)
-- show routine statuses
for i = 1, list_length do
TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i)}
local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=ind_grn}
local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=util.trinary(units[i].type~=RTU_UNIT_TYPE.REDSTONE,ind_grn,cpair(style.ind_bkg,style.ind_bkg))}
rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update)
end
local unit_hw_statuses = Div{parent=panel,height=18,x=25,y=3}
local unit_hw_statuses = Div{parent=panel,height=term_h-3,x=25,y=3}
local relay_counter = 0
-- show hardware statuses
for i = 1, list_length do
local unit = units[i]
local is_rs = unit.type == RTU_UNIT_TYPE.REDSTONE
-- hardware status
local unit_hw = RGBLED{parent=unit_hw_statuses,y=i,label="",colors={colors.red,colors.orange,colors.yellow,colors.green}}
unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update)
-- unit name identifier (type + index)
local function get_name(t) return util.c(UNIT_TYPE_LABELS[t + 1], " ", util.trinary(util.is_int(unit.index), unit.index, "")) end
local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(unit.type),width=15}
local function get_name()
if is_rs then
local is_local = unit.name == "redstone_local"
relay_counter = relay_counter + util.trinary(is_local, 0, 1)
return util.c("REDSTONE", util.trinary(is_local, "", " RELAY " .. relay_counter))
else
return util.c(UNIT_TYPE_LABELS[unit.type + 1], " ", util.trinary(util.is_int(unit.index), unit.index, ""))
end
end
name_box.register(databus.ps, "unit_type_" .. i, function (t) name_box.set_value(get_name(t)) end)
local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(),width=util.trinary(is_rs,24,15)}
name_box.register(databus.ps, "unit_type_" .. i, function () name_box.set_value(get_name()) end)
-- assignment (unit # or facility)
local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor)
TextBox{parent=unit_hw_statuses,y=i,x=19,text=for_unit,fg_bg=disabled_fg}
if unit.reactor then
local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor)
TextBox{parent=unit_hw_statuses,y=i,x=term_w-32,text=for_unit,fg_bg=disabled_fg}
end
end
end

View File

@@ -19,6 +19,7 @@ local MGMT_TYPE = comms.MGMT_TYPE
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
---@type rtu_config
---@diagnostic disable-next-line: missing-fields
local config = {}
rtu.config = config
@@ -45,36 +46,42 @@ function rtu.load_config()
config.FrontPanelTheme = settings.get("FrontPanelTheme")
config.ColorMode = settings.get("ColorMode")
return rtu.validate_config(config)
end
-- validate an RTU gateway configuration
---@param cfg rtu_config
function rtu.validate_config(cfg)
local cfv = util.new_validator()
cfv.assert_type_num(config.SpeakerVolume)
cfv.assert_range(config.SpeakerVolume, 0, 3)
cfv.assert_type_num(cfg.SpeakerVolume)
cfv.assert_range(cfg.SpeakerVolume, 0, 3)
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.RTU_Channel)
cfv.assert_type_num(config.ConnTimeout)
cfv.assert_min(config.ConnTimeout, 2)
cfv.assert_type_num(config.TrustedRange)
cfv.assert_min(config.TrustedRange, 0)
cfv.assert_type_str(config.AuthKey)
cfv.assert_channel(cfg.SVR_Channel)
cfv.assert_channel(cfg.RTU_Channel)
cfv.assert_type_num(cfg.ConnTimeout)
cfv.assert_min(cfg.ConnTimeout, 2)
cfv.assert_type_num(cfg.TrustedRange)
cfv.assert_min(cfg.TrustedRange, 0)
cfv.assert_type_str(cfg.AuthKey)
if type(config.AuthKey) == "string" then
local len = string.len(config.AuthKey)
if type(cfg.AuthKey) == "string" then
local len = string.len(cfg.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(cfg.LogMode)
cfv.assert_range(cfg.LogMode, 0, 1)
cfv.assert_type_str(cfg.LogPath)
cfv.assert_type_bool(cfg.LogDebug)
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)
cfv.assert_type_int(cfg.FrontPanelTheme)
cfv.assert_range(cfg.FrontPanelTheme, 1, 2)
cfv.assert_type_int(cfg.ColorMode)
cfv.assert_range(cfg.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
cfv.assert_type_table(config.Peripherals)
cfv.assert_type_table(config.Redstone)
cfv.assert_type_table(cfg.Peripherals)
cfv.assert_type_table(cfg.Redstone)
return cfv.valid()
end
@@ -331,13 +338,7 @@ function rtu.comms(version, nic, conn_watchdog)
local unit = units[i]
if unit.type ~= nil then
local advert = { unit.type, unit.index, unit.reactor }
if unit.type == RTU_UNIT_TYPE.REDSTONE then
insert(advert, unit.device)
end
insert(advertisement, advert)
insert(advertisement, { unit.type, unit.index, unit.reactor or -1, unit.rs_conns })
end
end
@@ -470,9 +471,10 @@ function rtu.comms(version, nic, conn_watchdog)
local unit = units[packet.unit_id]
local unit_dbg_tag = " (unit " .. packet.unit_id .. ")"
if unit.name == "redstone_io" then
if unit.type == RTU_UNIT_TYPE.REDSTONE then
-- immediately execute redstone RTU requests
return_code, reply = unit.modbus_io.handle_packet(packet)
if not return_code then
log.warning("requested MODBUS operation failed" .. unit_dbg_tag)
end
@@ -480,18 +482,16 @@ function rtu.comms(version, nic, conn_watchdog)
-- check validity then pass off to unit comms thread
return_code, reply = unit.modbus_io.check_request(packet)
if return_code then
-- check if there are more than 3 active transactions
-- still queue the packet, but this may indicate a problem
-- check if there are more than 3 active transactions, which will be treated as busy
if unit.pkt_queue.length() > 3 then
reply = modbus.reply__srv_device_busy(packet)
log.debug("queueing new request with " .. unit.pkt_queue.length() ..
" transactions already in the queue" .. unit_dbg_tag)
log.warning("device busy, discarding new request" .. unit_dbg_tag)
else
-- queue the command if not busy
unit.pkt_queue.push_packet(packet)
end
-- always queue the command even if busy
unit.pkt_queue.push_packet(packet)
else
log.warning("cannot perform requested MODBUS operation" .. unit_dbg_tag)
log.warning("requested MODBUS operation failed" .. unit_dbg_tag)
end
end
else

View File

@@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "v1.10.14"
local RTU_VERSION = "v1.12.1"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_HW_STATE = databus.RTU_HW_STATE
@@ -140,32 +140,36 @@ local function main()
local rtu_redstone = config.Redstone
local rtu_devices = config.Peripherals
-- get a string representation of a port interface
---@param entry rtu_rs_definition
---@return string
local function entry_iface_name(entry)
return util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side)
end
-- configure RTU gateway based on settings file definitions
local function sys_config()
-- redstone interfaces
local rs_rtus = {} ---@type { rtu: rtu_rs_device, capabilities: IO_PORT[] }[]
--#region Redstone Interfaces
local rs_rtus = {} ---@type { name: string, hw_state: RTU_HW_STATE, rtu: rtu_rs_device, phy: table, banks: rtu_rs_definition[][] }[]
local all_conns = { [0] = {}, {}, {}, {}, {} }
-- go through redstone definitions list
for entry_idx = 1, #rtu_redstone do
local entry = rtu_redstone[entry_idx]
local assignment
local for_reactor = entry.unit
local iface_name = util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side)
local phy = entry.relay or 0
local phy_name = entry.relay or "local"
local iface_name = entry_iface_name(entry)
if util.is_int(entry.unit) and entry.unit > 0 and entry.unit < 5 then
---@cast for_reactor integer
assignment = "reactor unit " .. entry.unit
if rs_rtus[for_reactor] == nil then
log.debug(util.c("sys_config> allocated redstone RTU for reactor unit ", entry.unit))
rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} }
end
elseif entry.unit == nil then
assignment = "facility"
for_reactor = 0
if rs_rtus[for_reactor] == nil then
log.debug(util.c("sys_config> allocated redstone RTU for the facility"))
rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} }
end
else
local message = util.c("sys_config> invalid unit assignment at block index #", entry_idx)
println(message)
@@ -173,14 +177,44 @@ local function main()
return false
end
-- create the appropriate RTU if it doesn't exist and check relay name validity
if entry.relay then
if type(entry.relay) ~= "string" then
local message = util.c("sys_config> invalid redstone relay '", entry.relay, '"')
println(message)
log.fatal(message)
return false
elseif not rs_rtus[entry.relay] then
log.debug(util.c("sys_config> allocated relay redstone RTU on interface ", entry.relay))
local hw_state = RTU_HW_STATE.OK
local relay = ppm.get_periph(entry.relay)
if not relay then
hw_state = RTU_HW_STATE.OFFLINE
log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not connected"))
local _, v_device = ppm.mount_virtual()
relay = v_device
elseif ppm.get_type(entry.relay) ~= "redstone_relay" then
hw_state = RTU_HW_STATE.FAULTED
log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not a redstone relay"))
end
rs_rtus[entry.relay] = { name = entry.relay, hw_state = hw_state, rtu = redstone_rtu.new(relay), phy = relay, banks = { [0] = {}, {}, {}, {}, {} } }
end
elseif rs_rtus[0] == nil then
log.debug(util.c("sys_config> allocated local redstone RTU"))
rs_rtus[0] = { name = "redstone_local", hw_state = RTU_HW_STATE.OK, rtu = redstone_rtu.new(), phy = rs, banks = { [0] = {}, {}, {}, {}, {} } }
end
-- verify configuration
local valid = false
if rsio.is_valid_port(entry.port) and rsio.is_valid_side(entry.side) then
valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color))
end
local rs_rtu = rs_rtus[for_reactor].rtu
local capabilities = rs_rtus[for_reactor].capabilities
local bank = rs_rtus[phy].banks[for_reactor]
local conns = all_conns[for_reactor]
if not valid then
local message = util.c("sys_config> invalid redstone definition at block index #", entry_idx)
@@ -192,73 +226,105 @@ local function main()
local mode = rsio.get_io_mode(entry.port)
if mode == rsio.IO_MODE.DIGITAL_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, entry.port) then
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name)
if util.table_contains(conns, entry.port) then
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name)
println(message)
log.warning(message)
else
rs_rtu.link_di(entry.side, entry.color)
table.insert(bank, entry)
end
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
rs_rtu.link_do(entry.side, entry.color)
elseif mode == rsio.IO_MODE.ANALOG_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, entry.port) then
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name)
if util.table_contains(conns, entry.port) then
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name)
println(message)
log.warning(message)
else
rs_rtu.link_ai(entry.side)
table.insert(bank, entry)
end
elseif mode == rsio.IO_MODE.ANALOG_OUT then
rs_rtu.link_ao(entry.side)
elseif (mode == rsio.IO_MODE.DIGITAL_OUT) or (mode == rsio.IO_MODE.ANALOG_OUT) then
table.insert(bank, entry)
else
-- should be unreachable code, we already validated ports
log.error("sys_config> fell through if chain attempting to identify IO mode at block index #" .. entry_idx, true)
log.fatal("sys_config> failed to identify IO mode at block index #" .. entry_idx)
println("sys_config> encountered a software error, check logs")
return false
end
table.insert(capabilities, entry.port)
table.insert(conns, entry.port)
log.debug(util.c("sys_config> linked redstone ", #capabilities, ": ", rsio.to_string(entry.port), " (", iface_name, ") for ", assignment))
log.debug(util.c("sys_config> banked redstone ", #conns, ": ", rsio.to_string(entry.port), " (", iface_name, " @ ", phy_name, ") for ", assignment))
end
end
-- create unit entries for redstone RTUs
for for_reactor, def in pairs(rs_rtus) do
---@class rtu_registry_entry
for _, def in pairs(rs_rtus) do
local rtu_conns = { [0] = {}, {}, {}, {}, {} }
-- connect the IO banks
for for_reactor = 0, #def.banks do
local bank = def.banks[for_reactor]
local conns = rtu_conns[for_reactor]
local assign = util.trinary(for_reactor > 0, "reactor unit " .. for_reactor, "the facility")
-- link redstone to the RTU
for i = 1, #bank do
local conn = bank[i]
local phy_name = conn.relay or "local"
local mode = rsio.get_io_mode(conn.port)
if mode == rsio.IO_MODE.DIGITAL_IN then
def.rtu.link_di(conn.side, conn.color, conn.invert)
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
def.rtu.link_do(conn.side, conn.color, conn.invert)
elseif mode == rsio.IO_MODE.ANALOG_IN then
def.rtu.link_ai(conn.side)
elseif mode == rsio.IO_MODE.ANALOG_OUT then
def.rtu.link_ao(conn.side)
else
log.fatal(util.c("sys_config> failed to identify IO mode of ", rsio.to_string(conn.port), " (", entry_iface_name(conn), " @ ", phy_name, ") for ", assign))
println("sys_config> encountered a software error, check logs")
return false
end
table.insert(conns, conn.port)
log.debug(util.c("sys_config> linked redstone ", for_reactor, ".", #conns, ": ", rsio.to_string(conn.port), " (", entry_iface_name(conn), ")", " @ ", phy_name, ") for ", assign))
end
end
---@type rtu_registry_entry
local unit = {
uid = 0, ---@type integer
name = "redstone_io", ---@type string
type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE
index = false, ---@type integer|false
reactor = for_reactor, ---@type integer
device = def.capabilities, ---@type IO_PORT[] use device field for redstone ports
is_multiblock = false, ---@type boolean
formed = nil, ---@type boolean|nil
hw_state = RTU_HW_STATE.OK, ---@type RTU_HW_STATE
rtu = def.rtu, ---@type rtu_device|rtu_rs_device
uid = 0,
name = def.name,
type = RTU_UNIT_TYPE.REDSTONE,
index = false,
reactor = nil,
device = def.phy,
rs_conns = rtu_conns,
is_multiblock = false,
formed = nil,
hw_state = def.hw_state,
rtu = def.rtu,
modbus_io = modbus.new(def.rtu, false),
pkt_queue = nil, ---@type mqueue|nil
thread = nil ---@type parallel_thread|nil
pkt_queue = nil,
thread = nil
}
table.insert(units, unit)
local for_message = "facility"
if util.is_int(for_reactor) then
for_message = util.c("reactor unit ", for_reactor)
end
local type = util.trinary(def.phy == rs, "redstone", "redstone_relay")
log.info(util.c("sys_config> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message))
log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", unit.name, " (", type, ")"))
unit.uid = #units
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
end
-- mounted peripherals
--#endregion
--#region Mounted Peripherals
for i = 1, #rtu_devices do
local entry = rtu_devices[i] ---@type rtu_peri_definition
local name = entry.name
@@ -338,9 +404,8 @@ local function main()
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed boiler multiblock"))
return false
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.warning(util.c("sys_config> failed to check if '", name, "' is a formed boiler multiblock"))
end
elseif type == "turbineValve" then
-- turbine multiblock
@@ -353,9 +418,8 @@ local function main()
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed turbine multiblock"))
return false
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.warning(util.c("sys_config> failed to check if '", name, "' is a formed turbine multiblock"))
end
elseif type == "dynamicValve" then
-- dynamic tank multiblock
@@ -373,9 +437,8 @@ local function main()
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed dynamic tank multiblock"))
return false
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.warning(util.c("sys_config> failed to check if '", name, "' is a formed dynamic tank multiblock"))
end
elseif type == "inductionPort" then
-- induction matrix multiblock
@@ -387,9 +450,8 @@ local function main()
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed induction matrix multiblock"))
return false
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.warning(util.c("sys_config> failed to check if '", name, "' is a formed induction matrix multiblock"))
end
elseif type == "spsPort" then
-- SPS multiblock
@@ -401,9 +463,8 @@ local function main()
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed SPS multiblock"))
return false
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.warning(util.c("sys_config> failed to check if '", name, "' is a formed SPS multiblock"))
end
elseif type == "solarNeutronActivator" then
-- SNA
@@ -431,7 +492,9 @@ local function main()
if is_multiblock then
if not formed then
log.info(util.c("sys_config> device '", name, "' is not formed"))
if formed == false then
log.info(util.c("sys_config> device '", name, "' is not formed"))
else formed = false end
elseif faulted then
-- sometimes there is a race condition on server boot where it reports formed, but
-- the other functions are not yet defined (that's the theory at least). mark as unformed to attempt connection later
@@ -442,26 +505,27 @@ local function main()
---@class rtu_registry_entry
local rtu_unit = {
uid = 0, ---@type integer
name = name, ---@type string
type = rtu_type, ---@type RTU_UNIT_TYPE
index = index or false, ---@type integer|false
reactor = for_reactor, ---@type integer
device = device, ---@type table peripheral reference
is_multiblock = is_multiblock, ---@type boolean
formed = formed, ---@type boolean|nil
hw_state = RTU_HW_STATE.OFFLINE, ---@type RTU_HW_STATE
rtu = rtu_iface, ---@type rtu_device|rtu_rs_device
modbus_io = modbus.new(rtu_iface, true),
pkt_queue = mqueue.new(), ---@type mqueue|nil
thread = nil ---@type parallel_thread|nil
uid = 0, ---@type integer RTU unit ID
name = name, ---@type string unit name
type = rtu_type, ---@type RTU_UNIT_TYPE unit type
index = index or false, ---@type integer|false device index
reactor = for_reactor, ---@type integer|nil unit/facility assignment
device = device, ---@type table peripheral reference
rs_conns = nil, ---@type IO_PORT[][]|nil available redstone connections
is_multiblock = is_multiblock, ---@type boolean if this is for a multiblock peripheral
formed = formed, ---@type boolean|nil if this peripheral is currently formed
hw_state = RTU_HW_STATE.OFFLINE, ---@type RTU_HW_STATE hardware device status
rtu = rtu_iface, ---@type rtu_device|rtu_rs_device RTU hardware interface
modbus_io = modbus.new(rtu_iface, true), ---@type modbus MODBUS interface
pkt_queue = mqueue.new(), ---@type mqueue|nil packet queue
thread = nil ---@type parallel_thread|nil associated RTU thread
}
rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
table.insert(units, rtu_unit)
local for_message = "facility"
local for_message = "the facility"
if for_reactor > 0 then
for_message = util.c("reactor ", for_reactor)
end
@@ -488,6 +552,8 @@ local function main()
databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state)
end
--#endregion
return true
end
@@ -498,17 +564,6 @@ local function main()
log.debug("boot> running sys_config()")
if sys_config() then
-- start UI
local message
rtu_state.fp_ok, message = renderer.try_start_ui(units, config.FrontPanelTheme, config.ColorMode)
if not rtu_state.fp_ok then
println_ts(util.c("UI error: ", message))
println("startup> running without front panel")
log.error(util.c("front panel GUI render failed with error ", message))
log.info("startup> running in headless mode without front panel")
end
-- check modem
if smem_dev.modem == nil then
println("startup> wireless modem not found")
@@ -530,6 +585,17 @@ local function main()
databus.tx_hw_spkr_count(#smem_dev.sounders)
-- start UI
local message
rtu_state.fp_ok, message = renderer.try_start_ui(units, config.FrontPanelTheme, config.ColorMode)
if not rtu_state.fp_ok then
println_ts(util.c("UI error: ", message))
println("startup> running without front panel")
log.error(util.c("front panel GUI render failed with error ", message))
log.info("startup> running in headless mode without front panel")
end
-- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("startup> conn watchdog started")
@@ -556,7 +622,7 @@ local function main()
-- run threads
parallel.waitForAll(table.unpack(_threads))
else
println("configuration failed, exiting...")
println("system initialization failed, exiting...")
end
renderer.close_ui()

View File

@@ -132,6 +132,8 @@ local function handle_unit_mount(smem, println_ts, iface, type, device, unit)
unit.rtu, faulted = sna_rtu.new(device)
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
unit.rtu, faulted = envd_rtu.new(device)
elseif unit.type == RTU_UNIT_TYPE.REDSTONE then
unit.rtu.remount_phy(device)
else
unknown = true
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
@@ -466,6 +468,9 @@ end
---@param smem rtu_shared_memory
---@param unit rtu_registry_entry
function threads.thread__unit_comms(smem, unit)
-- print a log message to the terminal as long as the UI isn't running
local function println_ts(message) if not smem.rtu_state.fp_ok then util.println_ts(message) end end
---@class parallel_thread
local public = {}
@@ -483,7 +488,9 @@ function threads.thread__unit_comms(smem, unit)
local last_f_check = 0
local detail_name = util.c(types.rtu_type_to_string(unit.type), " (", unit.name, ") [", unit.index, "] for reactor ", unit.reactor)
local detail_name = util.c(types.rtu_type_to_string(unit.type), " (", unit.name, ") ",
util.trinary(unit.index == false, "", util.c("[", unit.index, "] ")), "for ",
util.trinary(unit.reactor == 0, "the facility", util.c("reactor ", unit.reactor)))
local short_name = util.c(types.rtu_type_to_string(unit.type), " (", unit.name, ")")
if packet_queue == nil then
@@ -538,6 +545,15 @@ function threads.thread__unit_comms(smem, unit)
rtu_comms.send_remounted(unit.uid)
elseif (is_formed == false) and unit.formed then
log.warning(util.c(detail_name, " is no longer formed"))
elseif (is_formed == nil) and (unit.hw_state ~= RTU_HW_STATE.OFFLINE) then
log.error(util.c(detail_name, " failed to check if formed, attempting remount..."))
local type, dev = ppm.remount(unit.name)
if type and dev then
handle_unit_mount(smem, println_ts, unit.name, type, dev, unit)
else
log.error(util.c(detail_name, " failed to remount"))
end
end
unit.formed = is_formed

View File

@@ -17,8 +17,8 @@ local max_distance = nil
local comms = {}
-- protocol/data versions (protocol/data independent changes tracked by util.lua version)
comms.version = "3.0.1"
comms.api_version = "0.0.6"
comms.version = "3.0.6"
comms.api_version = "0.0.9"
---@enum PROTOCOL
local PROTOCOL = {
@@ -60,16 +60,19 @@ local MGMT_TYPE = {
---@enum CRDN_TYPE
local CRDN_TYPE = {
INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator
FAC_BUILDS = 1, -- facility RTU builds
FAC_STATUS = 2, -- state of facility and facility devices
FAC_CMD = 3, -- faility command
UNIT_BUILDS = 4, -- build of each reactor unit (reactor + RTUs)
UNIT_STATUSES = 5, -- state of each of the reactor units
UNIT_CMD = 6, -- command a reactor unit
API_GET_FAC = 7, -- API: get all the facility data
API_GET_UNIT = 8, -- API: get reactor unit data
API_GET_CTRL = 9, -- API: get data used for the control app
API_GET_PROC = 10 -- API: get data used for the process app
PROCESS_READY = 1, -- process init is complete + last set of info for supervisor startup recovery
FAC_BUILDS = 2, -- facility RTU builds
FAC_STATUS = 3, -- state of facility and facility devices
FAC_CMD = 4, -- faility command
UNIT_BUILDS = 5, -- build of each reactor unit (reactor + RTUs)
UNIT_STATUSES = 6, -- state of each of the reactor units
UNIT_CMD = 7, -- command a reactor unit
API_GET_FAC = 8, -- API: get the facility general data
API_GET_FAC_DTL = 9, -- API: get (detailed) data for the facility app
API_GET_UNIT = 10, -- API: get reactor unit data
API_GET_CTRL = 11, -- API: get data for the control app
API_GET_PROC = 12, -- API: get data for the process app
API_GET_WASTE = 13 -- API: get data for the waste app
}
---@enum ESTABLISH_ACK

View File

@@ -72,6 +72,8 @@ local rs = {}
rs.IMATRIX_CHARGE_LOW = 0.05 -- activation threshold (less than) for F_MATRIX_LOW
rs.IMATRIX_CHARGE_HIGH = 0.95 -- activation threshold (greater than) for F_MATRIX_HIGH
rs.AUX_COOL_ENABLE = 0.60 -- actiation threshold (less than or equal) for U_AUX_COOL
rs.AUX_COOL_DISABLE = 1.00 -- deactivation threshold (greater than or equal) for U_AUX_COOL
constants.RS_THRESHOLDS = rs

View File

@@ -2,6 +2,9 @@
-- Crash Handler
--
---@diagnostic disable-next-line: undefined-global
local _is_pocket_env = pocket -- luacheck: ignore pocket
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local util = require("scada-common.util")
@@ -36,6 +39,74 @@ local function log_versions(log_msg)
if has_lockbox then log_msg(util.c("LOCKBOX VERSION: ", lockbox.version)) end
end
-- render the standard computer crash screen
---@param exit function callback on exit button press
---@return DisplayBox display
local function draw_computer_crash(exit)
local DisplayBox = require("graphics.elements.DisplayBox")
local Div = require("graphics.elements.Div")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton")
local display = DisplayBox{window=term.current(),fg_bg=core.cpair(colors.white,colors.lightGray)}
local warning = Div{parent=display,x=2,y=2}
TextBox{parent=warning,x=7,text="\x90\n \x90\n \x90\n \x90\n \x90",fg_bg=core.cpair(colors.yellow,colors.lightGray)}
TextBox{parent=warning,x=5,y=1,text="\x9f ",width=2,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=warning,x=4,text="\x9f ",width=4,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=warning,x=3,text="\x9f ",width=6,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=warning,x=2,text="\x9f ",width=8,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=warning,text="\x9f ",width=10,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=warning,text="\x8f\x8f\x8f\x8f\x8f\x8f\x8f\x8f\x8f\x8f\x8f",width=11,fg_bg=core.cpair(colors.yellow,colors.lightGray)}
TextBox{parent=warning,x=6,y=3,text=" \n \x83",width=1,fg_bg=core.cpair(colors.yellow,colors.white)}
TextBox{parent=display,x=13,y=2,text="Critical Software Fault Encountered",alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.yellow,colors._INHERIT)}
TextBox{parent=display,x=15,y=4,text="Please consider reporting this on the cc-mek-scada Discord or GitHub.",width=36,alignment=core.ALIGN.CENTER}
TextBox{parent=display,x=14,y=7,text="refer to the log file for more info",alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.gray,colors._INHERIT)}
local box = Rectangle{parent=display,x=2,y=9,width=display.get_width()-2,height=8,border=core.border(1,colors.gray,true),thin=true,fg_bg=core.cpair(colors.black,colors.white)}
TextBox{parent=box,text=err}
PushButton{parent=display,x=23,y=18,text=" Exit ",callback=exit,active_fg_bg=core.cpair(colors.white,colors.gray),fg_bg=core.cpair(colors.black,colors.red)}
return display
end
-- render the pocket crash screen
---@param exit function callback on exit button press
---@return DisplayBox display
local function draw_pocket_crash(exit)
local DisplayBox = require("graphics.elements.DisplayBox")
local Div = require("graphics.elements.Div")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton")
local display = DisplayBox{window=term.current(),fg_bg=core.cpair(colors.white,colors.lightGray)}
local warning = Div{parent=display,x=2,y=1}
TextBox{parent=warning,x=4,y=1,text="\x90",width=1,fg_bg=core.cpair(colors.yellow,colors.lightGray)}
TextBox{parent=warning,x=3,text="\x81 ",width=2,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=warning,x=5,y=2,text="\x94",width=1,fg_bg=core.cpair(colors.yellow,colors.lightGray)}
TextBox{parent=warning,x=2,text="\x81 ",width=4,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=warning,x=6,y=3,text="\x94",width=1,fg_bg=core.cpair(colors.yellow,colors.lightGray)}
TextBox{parent=warning,text="\x8e\x8f\x8f\x8e\x8f\x8f\x84",width=7,fg_bg=core.cpair(colors.yellow,colors.lightGray)}
TextBox{parent=warning,x=4,y=2,text="\x90",width=1,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=warning,x=4,y=3,text="\x85",width=1,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=display,x=10,y=2,text=" Critical Software Fault",width=16,alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.yellow,colors._INHERIT)}
TextBox{parent=display,x=2,y=5,text="Consider reporting this on the cc-mek-scada Discord or GitHub.",width=36,alignment=core.ALIGN.CENTER}
local box = Rectangle{parent=display,y=9,width=display.get_width(),height=8,fg_bg=core.cpair(colors.black,colors.white)}
TextBox{parent=box,text=err}
PushButton{parent=display,x=11,y=18,text=" Exit ",callback=exit,active_fg_bg=core.cpair(colors.white,colors.gray),fg_bg=core.cpair(colors.black,colors.red)}
TextBox{parent=display,x=2,y=20,text="see logs for details",width=24,alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.gray,colors._INHERIT)}
return display
end
-- when running with debug logs, log the useful information that the crash handler knows
function crash.dbg_log_env() log_versions(log.debug) end
@@ -54,9 +125,41 @@ end
-- final error print on failed xpcall, app exits here
function crash.exit()
local handled, run = false, true
local display ---@type DisplayBox
-- special graphical crash screen
if has_graphics then
handled, display = pcall(util.trinary(_is_pocket_env, draw_pocket_crash, draw_computer_crash), function () run = false end)
-- event loop
while display and run do
local event, param1, param2, param3 = util.pull_event()
-- handle event
if event == "mouse_click" or event == "mouse_up" or event == "double_click" then
local mouse = core.events.new_mouse_event(event, param1, param2, param3)
if mouse then display.handle_mouse(mouse) end
elseif event == "terminate" then
break
end
end
display.delete()
term.setCursorPos(1, 1)
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
term.clear()
end
log.close()
util.println("fatal error occured in main application:")
error(err, 0)
-- default text failure message
if not handled then
util.println("fatal error occured in main application:")
error(err, 0)
end
end
return crash

View File

@@ -8,6 +8,10 @@ local util = require("scada-common.util")
local DBG_TAG, INF_TAG, WRN_TAG, ERR_TAG, FTL_TAG = "[DBG] ", "[INF] ", "[WRN] ", "[ERR] ", "[FTL] "
local COLON, FUNC, ARROW = ":", "():", " > "
local MIN_SPACE = 512
local OUT_OF_SPACE = "Out of space"
local TIME_FMT = "%F %T "
---@class logger
local log = {}
@@ -34,14 +38,20 @@ local free_space = fs.getFreeSpace
-- PRIVATE FUNCTIONS --
-----------------------
-- check if the provided error indicates out of space or if insufficient space available
---@param err_msg string|nil error message
---@return boolean out_of_space
local function check_out_of_space(err_msg)
return (free_space(logger.path) < MIN_SPACE) or ((err_msg ~= nil) and (string.find(err_msg, OUT_OF_SPACE) ~= nil))
end
-- private log write function
---@param msg_bits any[]
local function _log(msg_bits)
if logger.not_ready then return end
local out_of_space = false
local time_stamp = os.date("[%c] ")
local stamped = util.c(time_stamp, table.unpack(msg_bits))
local time_stamp = os.date(TIME_FMT)
local stamped = util.c(time_stamp, table.unpack(msg_bits))
-- attempt to write log
local status, result = pcall(function ()
@@ -50,18 +60,7 @@ local function _log(msg_bits)
end)
-- if we don't have space, we need to create a new log file
if (not status) and (result ~= nil) then
out_of_space = string.find(result, "Out of space") ~= nil
if out_of_space then
-- will delete log file
else
util.println("unknown error writing to logfile: " .. result)
end
end
if out_of_space or (free_space(logger.path) < 512) then
if check_out_of_space() then
-- delete the old log file before opening a new one
logger.file.close()
fs.delete(logger.path)
@@ -69,10 +68,12 @@ local function _log(msg_bits)
-- re-init logger and pass dmesg_out so that it doesn't change
log.init(logger.path, logger.mode, logger.debug, logger.dmesg_out)
-- leave a message
logger.file.writeLine(time_stamp .. "recycled log file")
-- log the message and recycle warning
logger.file.writeLine(time_stamp .. WRN_TAG .. "recycled log file")
logger.file.writeLine(stamped)
logger.file.flush()
elseif (not status) and (result ~= nil) then
util.println("unexpected error writing to the log file: " .. result)
end
end
@@ -86,15 +87,12 @@ end
---@param include_debug boolean whether or not to include debug logs
---@param dmesg_redirect? Redirect terminal/window to direct dmesg to
function log.init(path, write_mode, include_debug, dmesg_redirect)
local err_msg
logger.path = path
logger.mode = write_mode
logger.debug = include_debug
if logger.mode == MODE.APPEND then
logger.file = fs.open(path, "a")
else
logger.file = fs.open(path, "w")
end
logger.file, err_msg = fs.open(path, util.trinary(logger.mode == MODE.APPEND, "a", "w"))
if dmesg_redirect then
logger.dmesg_out = dmesg_redirect
@@ -102,6 +100,25 @@ function log.init(path, write_mode, include_debug, dmesg_redirect)
logger.dmesg_out = term.current()
end
-- check for space issues
local out_of_space = check_out_of_space(err_msg)
-- try to handle problems
if logger.file == nil or out_of_space then
if out_of_space then
if fs.exists(logger.path) then
fs.delete(logger.path)
logger.file, err_msg = fs.open(path, util.trinary(logger.mode == MODE.APPEND, "a", "w"))
if logger.file then
logger.file.writeLine(os.date(TIME_FMT) .. WRN_TAG .. "init recycled log file")
logger.file.flush()
else error("failed to setup the log file: " .. err_msg) end
else error("failed to make space for the log file, please delete unused files") end
else error("unexpected error setting up the log file: " .. err_msg) end
end
logger.not_ready = false
end

View File

@@ -242,7 +242,7 @@ function ppm.mount_all()
end
end
-- mount a particular device
-- mount a specified device
---@nodiscard
---@param iface string CC peripheral interface
---@return string|nil type, table|nil device
@@ -266,6 +266,33 @@ function ppm.mount(iface)
return pm_type, pm_dev
end
-- unmount and remount a specified device
---@nodiscard
---@param iface string CC peripheral interface
---@return string|nil type, table|nil device
function ppm.remount(iface)
local ifaces = peripheral.getNames()
local pm_dev = nil
local pm_type = nil
for i = 1, #ifaces do
if iface == ifaces[i] then
log.info(util.c("PPM: remount(", iface, ") -> is a ", pm_type))
ppm.unmount(ppm_sys.mounts[iface].dev)
ppm_sys.mounts[iface] = peri_init(iface)
pm_type = ppm_sys.mounts[iface].type
pm_dev = ppm_sys.mounts[iface].dev
log.info(util.c("PPM: remount(", iface, ") -> remounted a ", pm_type))
break
end
end
return pm_type, pm_dev
end
-- mount a virtual, placeholder device (specifically designed for RTU startup with missing devices)
---@nodiscard
---@return string type, table device

View File

@@ -74,6 +74,13 @@ function psil.create()
end
end
-- get the currently stored value for a key, or nil if not set
---@param key string data key
---@return any
function public.get(key)
if ic[key] ~= nil then return ic[key].value else return nil end
end
-- clear the contents of the interconnect
function public.purge() ic = {} end

View File

@@ -78,6 +78,7 @@ local IO_PORT = {
-- unit outputs
U_ALARM = 25, -- active high, unit alarm
U_EMER_COOL = 26, -- active low, emergency coolant control
U_AUX_COOL = 30, -- active low, auxiliary coolant control
-- analog outputs --
@@ -90,8 +91,8 @@ rsio.IO_DIR = IO_DIR
rsio.IO_MODE = IO_MODE
rsio.IO = IO_PORT
rsio.NUM_PORTS = 29
rsio.NUM_DIG_PORTS = 28
rsio.NUM_PORTS = 30
rsio.NUM_DIG_PORTS = 29
rsio.NUM_ANA_PORTS = 1
-- self checks
@@ -149,6 +150,7 @@ local MODES = {
[IO.R_PLC_TIMEOUT] = IO_MODE.DIGITAL_OUT,
[IO.U_ALARM] = IO_MODE.DIGITAL_OUT,
[IO.U_EMER_COOL] = IO_MODE.DIGITAL_OUT,
[IO.U_AUX_COOL] = IO_MODE.DIGITAL_OUT,
[IO.F_MATRIX_CHG] = IO_MODE.ANALOG_OUT
}
@@ -208,10 +210,11 @@ local RS_DIO_MAP = {
[IO.R_PLC_TIMEOUT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.U_ALARM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.U_EMER_COOL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }
[IO.U_EMER_COOL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
[IO.U_AUX_COOL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }
}
assert(rsio.NUM_DIG_PORTS == #RS_DIO_MAP, "RS_DIO_MAP length incorrect")
assert(rsio.NUM_DIG_PORTS == util.table_len(RS_DIO_MAP), "RS_DIO_MAP length incorrect")
-- get the I/O direction of a port
---@nodiscard

View File

@@ -125,7 +125,7 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
---@field type RTU_UNIT_TYPE
---@field index integer|false
---@field reactor integer
---@field rsio IO_PORT[]|nil
---@field rs_conns IO_PORT[][]|nil
-- create a new reactor database
---@nodiscard
@@ -253,6 +253,61 @@ types.ENERGY_SCALE_UNITS = {
"RF"
}
local GENERIC_STATE = {
OFFLINE = 1,
UNFORMED = 2,
FAULT = 3,
IDLE = 4,
ACTIVE = 5
}
---@enum REACTOR_STATE
types.REACTOR_STATE = {
OFFLINE = 1,
UNFORMED = 2,
FAULT = 3,
DISABLED = 4,
ACTIVE = 5,
SCRAMMED = 6,
FORCE_DISABLED = 7
}
---@enum BOILER_STATE
types.BOILER_STATE = GENERIC_STATE
---@enum TURBINE_STATE
types.TURBINE_STATE = {
OFFLINE = 1,
UNFORMED = 2,
FAULT = 3,
IDLE = 4,
ACTIVE = 5,
TRIPPED = 6
}
---@enum TANK_STATE
types.TANK_STATE = {
OFFLINE = 1,
UNFORMED = 2,
FAULT = 3,
ONLINE = 4,
LOW_FILL = 5,
HIGH_FILL = 6
}
---@enum IMATRIX_STATE
types.IMATRIX_STATE = {
OFFLINE = 1,
UNFORMED = 2,
FAULT = 3,
ONLINE = 4,
LOW_CHARGE = 5,
HIGH_CHARGE = 6
}
---@enum SPS_STATE
types.SPS_STATE = GENERIC_STATE
---@enum PANEL_LINK_STATE
types.PANEL_LINK_STATE = {
LINKED = 1,
@@ -363,6 +418,12 @@ types.AUTO_GROUP_NAMES = {
"Backup"
}
---@enum COOLANT_TYPE
types.COOLANT_TYPE = {
WATER = 1,
SODIUM = 2
}
---@enum WASTE_MODE
types.WASTE_MODE = {
AUTO = 1,
@@ -404,7 +465,8 @@ types.ALARM = {
ReactorHighWaste = 9,
RPSTransient = 10,
RCSTransient = 11,
TurbineTrip = 12
TurbineTrip = 12,
FacilityRadiation = 13
}
types.ALARM_NAMES = {
@@ -419,7 +481,8 @@ types.ALARM_NAMES = {
"ReactorHighWaste",
"RPSTransient",
"RCSTransient",
"TurbineTrip"
"TurbineTrip",
"FacilityRadiation"
}
---@enum ALARM_PRIORITY

View File

@@ -24,7 +24,7 @@ local t_pack = table.pack
local util = {}
-- scada-common version
util.version = "1.4.6"
util.version = "1.5.2"
util.TICK_TIME_S = 0.05
util.TICK_TIME_MS = 50

View File

@@ -1,4 +1,4 @@
local BOOTLOADER_VERSION = "1.1"
local BOOTLOADER_VERSION = "1.2"
print("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION)
print("BOOT> SCANNING FOR APPLICATIONS...")

137
supervisor/alarm_ctl.lua Normal file
View File

@@ -0,0 +1,137 @@
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local ALARM_STATE = types.ALARM_STATE
---@class alarm_def
---@field state ALARM_INT_STATE internal alarm state
---@field trip_time integer time (ms) when first tripped
---@field hold_time integer time (s) to hold before tripping
---@field id ALARM alarm ID
---@field tier integer alarm urgency tier (0 = highest)
local AISTATE_NAMES = {
"INACTIVE",
"TRIPPING",
"TRIPPED",
"ACKED",
"RING_BACK",
"RING_BACK_TRIPPING"
}
---@enum ALARM_INT_STATE
local AISTATE = {
INACTIVE = 1,
TRIPPING = 2,
TRIPPED = 3,
ACKED = 4,
RING_BACK = 5,
RING_BACK_TRIPPING = 6
}
local alarm_ctl = {}
alarm_ctl.AISTATE = AISTATE
alarm_ctl.AISTATE_NAMES = AISTATE_NAMES
-- update an alarm state based on its current status and if it is tripped
---@param caller_tag string tag to use in log messages
---@param alarm_states { [ALARM]: ALARM_STATE } unit instance
---@param tripped boolean if the alarm condition is sti ll active
---@param alarm alarm_def alarm table
---@param no_ring_back boolean? true to skip the ring back state, returning to inactive instead
---@return boolean new_trip if the alarm just changed to being tripped
function alarm_ctl.update_alarm_state(caller_tag, alarm_states, tripped, alarm, no_ring_back)
local int_state = alarm.state
local ext_state = alarm_states[alarm.id]
-- alarm inactive
if int_state == AISTATE.INACTIVE then
if tripped then
alarm.trip_time = util.time_ms()
if alarm.hold_time > 0 then
alarm.state = AISTATE.TRIPPING
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
else
alarm.state = AISTATE.TRIPPED
alarm_states[alarm.id] = ALARM_STATE.TRIPPED
log.info(util.c(caller_tag, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end
else
alarm.trip_time = util.time_ms()
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
end
-- alarm condition met, but not yet for required hold time
elseif (int_state == AISTATE.TRIPPING) or (int_state == AISTATE.RING_BACK_TRIPPING) then
if tripped then
local elapsed = util.time_ms() - alarm.trip_time
if elapsed > (alarm.hold_time * 1000) then
alarm.state = AISTATE.TRIPPED
alarm_states[alarm.id] = ALARM_STATE.TRIPPED
log.info(util.c(caller_tag, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end
elseif int_state == AISTATE.RING_BACK_TRIPPING then
alarm.trip_time = 0
alarm.state = AISTATE.RING_BACK
alarm_states[alarm.id] = ALARM_STATE.RING_BACK
else
alarm.trip_time = 0
alarm.state = AISTATE.INACTIVE
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
end
-- alarm tripped and alarming
elseif int_state == AISTATE.TRIPPED then
if tripped then
if ext_state == ALARM_STATE.ACKED then
-- was acked by coordinator
alarm.state = AISTATE.ACKED
end
elseif no_ring_back then
alarm.state = AISTATE.INACTIVE
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
else
alarm.state = AISTATE.RING_BACK
alarm_states[alarm.id] = ALARM_STATE.RING_BACK
end
-- alarm acknowledged but still tripped
elseif int_state == AISTATE.ACKED then
if not tripped then
if no_ring_back then
alarm.state = AISTATE.INACTIVE
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
else
alarm.state = AISTATE.RING_BACK
alarm_states[alarm.id] = ALARM_STATE.RING_BACK
end
end
-- alarm no longer tripped, operator must reset to clear
elseif int_state == AISTATE.RING_BACK then
if tripped then
alarm.trip_time = util.time_ms()
if alarm.hold_time > 0 then
alarm.state = AISTATE.RING_BACK_TRIPPING
else
alarm.state = AISTATE.TRIPPED
alarm_states[alarm.id] = ALARM_STATE.TRIPPED
end
elseif ext_state == ALARM_STATE.INACTIVE then
-- was reset by coordinator
alarm.state = AISTATE.INACTIVE
alarm.trip_time = 0
end
else
log.error(util.c(caller_tag, " invalid alarm state for alarm ", alarm.id), true)
end
-- check for state change
if alarm.state ~= int_state then
local change_str = util.c(AISTATE_NAMES[int_state], " -> ", AISTATE_NAMES[alarm.state])
log.debug(util.c(caller_tag, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): ", change_str))
return alarm.state == AISTATE.TRIPPED
else return false end
end
return alarm_ctl

View File

@@ -18,12 +18,148 @@ local tri = util.trinary
local cpair = core.cpair
local self = {
vis_ftanks = {}, ---@type { line: Div, pipe_conn?: TextBox, pipe_chain?: TextBox, pipe_direct?: TextBox, label?: TextBox }[]
vis_utanks = {} ---@type { line: Div, label: TextBox }[]
tank_fluid_opts = {}, ---@type Radio2D[]
vis_draw = nil, ---@type function
draw_fluid_ops = nil, ---@type function
vis_ftanks = {}, ---@type { line: Div, pipe_conn?: TextBox, pipe_chain?: TextBox, pipe_direct?: TextBox, label?: TextBox }[]
vis_utanks = {} ---@type { line: Div, label: TextBox }[]
}
local facility = {}
-- generate the tank list and tank connections tables
---@param mode integer facility tank mode
---@param defs table facility tank definitions
---@return table tank_list, table tank_conns
function facility.generate_tank_list_and_conns(mode, defs)
local tank_mode = mode
local tank_defs = defs
local tank_list = { table.unpack(tank_defs) }
local tank_conns = { table.unpack(tank_defs) }
local function calc_fdef(start_idx, end_idx)
local first = 4
for i = start_idx, end_idx do
if tank_defs[i] == 2 then
if i < first then first = i end
end
end
return first
end
-- set units using their own tanks as connected to their respective unit tank
for i = 1, #tank_defs do
if tank_defs[i] == 1 then tank_conns[i] = i end
end
if tank_mode == 1 then
-- (1) 1 total facility tank (A A A A)
local first_fdef = calc_fdef(1, #tank_defs)
for i = 1, #tank_defs do
if (i >= first_fdef) and (tank_defs[i] == 2) then
tank_conns[i] = first_fdef
if i > first_fdef then tank_list[i] = 0 end
end
end
elseif tank_mode == 2 then
-- (2) 2 total facility tanks (A A A B)
local first_fdef = calc_fdef(1, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (i >= first_fdef) and (tank_defs[i] == 2) then
if i == 4 then
tank_conns[i] = 4
else
tank_conns[i] = first_fdef
if i > first_fdef then tank_list[i] = 0 end
end
end
end
elseif tank_mode == 3 then
-- (3) 2 total facility tanks (A A B B)
for _, a in pairs({ 1, 3 }) do
local b = a + 1
if tank_defs[a] == 2 then
tank_conns[a] = a
elseif tank_defs[b] == 2 then
tank_conns[b] = b
end
if (tank_defs[a] == 2) and (tank_defs[b] == 2) then
tank_list[b] = 0
tank_conns[b] = a
end
end
elseif tank_mode == 4 then
-- (4) 2 total facility tanks (A B B B)
local first_fdef = calc_fdef(2, #tank_defs)
for i = 1, #tank_defs do
if tank_defs[i] == 2 then
if i == 1 then
tank_conns[i] = 1
elseif i >= first_fdef then
tank_conns[i] = first_fdef
if i > first_fdef then tank_list[i] = 0 end
end
end
end
elseif tank_mode == 5 then
-- (5) 3 total facility tanks (A A B C)
local first_fdef = calc_fdef(1, math.min(2, #tank_defs))
for i = 1, #tank_defs do
if (i >= first_fdef) and (tank_defs[i] == 2) then
if i == 3 or i == 4 then
tank_conns[i] = i
elseif i >= first_fdef then
tank_conns[i] = first_fdef
if i > first_fdef then tank_list[i] = 0 end
end
end
end
elseif tank_mode == 6 then
-- (6) 3 total facility tanks (A B B C)
local first_fdef = calc_fdef(2, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if tank_defs[i] == 2 then
if i == 1 or i == 4 then
tank_conns[i] = i
elseif i >= first_fdef then
tank_conns[i] = first_fdef
if i > first_fdef then tank_list[i] = 0 end
end
end
end
elseif tank_mode == 7 then
-- (7) 3 total facility tanks (A B C C)
local first_fdef = calc_fdef(3, #tank_defs)
for i = 1, #tank_defs do
if tank_defs[i] == 2 then
if i == 1 or i == 2 then
tank_conns[i] = i
elseif i >= first_fdef then
tank_conns[i] = first_fdef
if i > first_fdef then tank_list[i] = 0 end
end
end
end
elseif tank_mode == 8 then
-- (8) 4 total facility tanks (A B C D)
for i = 1, #tank_defs do
if tank_defs[i] == 2 then tank_conns[i] = i end
end
end
return tank_list, tank_conns
end
-- create the facility configuration view
---@param tool_ctl _svr_cfg_tool_ctl
---@param main_pane MultiPane
@@ -48,14 +184,19 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
local fac_c_5 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_c_6 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_c_7 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_c_8 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_c_9 = 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,fac_c_4,fac_c_5,fac_c_6,fac_c_7}}
local fac_pane = MultiPane{parent=fac_cfg,x=1,y=4,panes={fac_c_1,fac_c_2,fac_c_3,fac_c_4,fac_c_5,fac_c_6,fac_c_7,fac_c_8,fac_c_9}}
TextBox{parent=fac_cfg,x=1,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)}
--#region Unit Count
TextBox{parent=fac_c_1,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_1,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_1,x=7,y=5,text="reactors"}
TextBox{parent=fac_c_1,x=1,y=7,height=3,text="If you already configured your coordinator, make sure you update the coordinator's configured unit count.",fg_bg=cpair(colors.yellow,colors._INHERIT)}
local nu_error = TextBox{parent=fac_c_1,x=8,y=14,width=35,text="Please set the number of reactors.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
@@ -65,10 +206,18 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
nu_error.hide(true)
tmp_cfg.UnitCount = count
local confs = tool_ctl.cooling_elems
if count >= 2 then confs[2].line.show() else confs[2].line.hide(true) end
if count >= 3 then confs[3].line.show() else confs[3].line.hide(true) end
if count == 4 then confs[4].line.show() else confs[4].line.hide(true) end
local c_confs = tool_ctl.cooling_elems
local a_confs = tool_ctl.aux_cool_elems
for i = 2, 4 do
if count >= i then
c_confs[i].line.show()
a_confs[i].line.show()
else
c_confs[i].line.hide(true)
a_confs[i].line.hide(true)
end
end
fac_pane.set_value(2)
else nu_error.show() end
@@ -77,6 +226,9 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
PushButton{parent=fac_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=fac_c_1,x=44,y=14,text="Next \x1a",callback=submit_num_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Cooling Configuration
TextBox{parent=fac_c_2,x=1,y=1,height=4,text="Please provide the reactor cooling configuration below. This includes the number of turbines, boilers, and if that reactor has a connection to a dynamic tank for emergency coolant."}
TextBox{parent=fac_c_2,x=1,y=6,text="UNIT TURBINES BOILERS HAS TANK CONNECTION?",fg_bg=g_lg_fg_bg}
@@ -142,6 +294,14 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
else elem.div.hide(true) end
end
if not any_has_tank then
tmp_cfg.FacilityTankMode = 0
tmp_cfg.FacilityTankDefs = {}
tmp_cfg.FacilityTankList = {}
tmp_cfg.FacilityTankConns = {}
tmp_cfg.TankFluidTypes = {}
end
if any_has_tank then fac_pane.set_value(3) else main_pane.set_value(3) end
end
end
@@ -149,6 +309,9 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
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_cooling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Facility Tanks Option
TextBox{parent=fac_c_3,x=1,y=1,height=6,text="You have set one or more of your units to use dynamic tanks for emergency coolant. You have two paths for configuration. The first is to assign dynamic tanks to reactor units; one tank per reactor, only connected to that reactor. RTU configurations must also assign it as such."}
TextBox{parent=fac_c_3,x=1,y=8,height=3,text="Alternatively, you can configure them as facility tanks to connect to multiple reactor units. These can intermingle with unit-specific tanks."}
@@ -161,6 +324,16 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
else
tmp_cfg.FacilityTankMode = 0
tmp_cfg.FacilityTankDefs = {}
-- on facility tank mode 0, setup tank defs to match unit tank option
for i = 1, tmp_cfg.UnitCount do
tmp_cfg.FacilityTankDefs[i] = tri(tmp_cfg.CoolingConfig[i].TankConnection, 1, 0)
end
tmp_cfg.FacilityTankList, tmp_cfg.FacilityTankConns = facility.generate_tank_list_and_conns(tmp_cfg.FacilityTankMode, tmp_cfg.FacilityTankDefs)
self.draw_fluid_ops()
fac_pane.set_value(7)
end
end
@@ -168,6 +341,9 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
PushButton{parent=fac_c_3,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(2)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=submit_en_fac_tank,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Facility Tank Connections
TextBox{parent=fac_c_4,x=1,y=1,height=4,text="Please set unit connections to dynamic tanks, selecting at least one facility tank. The layout for facility tanks will be configured next."}
for i = 1, 4 do
@@ -220,7 +396,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
self.vis_utanks[i].line.hide(true)
end
tool_ctl.vis_draw(tmp_cfg.FacilityTankMode)
self.vis_draw(tmp_cfg.FacilityTankMode)
if any_fac then
tank_err.hide(true)
@@ -231,6 +407,9 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
PushButton{parent=fac_c_4,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_4,x=44,y=14,text="Next \x1a",callback=submit_tank_defs,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Facility Tank Mode
TextBox{parent=fac_c_5,x=1,y=1,text="Please select your dynamic tank layout."}
TextBox{parent=fac_c_5,x=12,y=3,text="Facility Tanks Unit Tanks",fg_bg=g_lg_fg_bg}
@@ -269,7 +448,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
-- draw the pipe visualization
---@param mode integer pipe mode
function tool_ctl.vis_draw(mode)
function self.vis_draw(mode)
-- is a facility tank connected to this unit
---@param i integer unit 1 - 4
---@return boolean connected
@@ -391,7 +570,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
local function change_mode(mode)
tmp_cfg.FacilityTankMode = mode
tool_ctl.vis_draw(mode)
self.vis_draw(mode)
end
local tank_modes = { "Mode 1", "Mode 2", "Mode 3", "Mode 4", "Mode 5", "Mode 6", "Mode 7", "Mode 8" }
@@ -399,33 +578,161 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
--#endregion
local function next_from_tank_mode()
-- determine tank list and connections
tmp_cfg.FacilityTankList, tmp_cfg.FacilityTankConns = facility.generate_tank_list_and_conns(tmp_cfg.FacilityTankMode, tmp_cfg.FacilityTankDefs)
self.draw_fluid_ops()
fac_pane.set_value(7)
end
PushButton{parent=fac_c_5,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_5,x=44,y=14,text="Next \x1a",callback=function()fac_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_5,x=44,y=14,text="Next \x1a",callback=next_from_tank_mode,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_5,x=8,y=14,min_width=7,text="About",callback=function()fac_pane.set_value(6)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Facility Tank Mode About
TextBox{parent=fac_c_6,height=3,text="This visualization tool shows the pipe connections required for a particular dynamic tank configuration you have selected."}
TextBox{parent=fac_c_6,y=5,height=4,text="Examples: A U2 tank should be configured on an RTU as the dynamic tank for unit #2. An F3 tank should be configured on an RTU as the #3 dynamic tank for the facility."}
TextBox{parent=fac_c_6,y=10,height=3,text="Some modes may look the same if you are not using 4 total reactor units. The wiki has details. Modes that look the same will function the same.",fg_bg=g_lg_fg_bg}
PushButton{parent=fac_c_6,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=fac_c_7,height=6,text="Charge control provides automatic control to maintain an induction matrix charge level. In order to have smoother control, reactors that were activated will be held on at 0.01 mB/t for a short period before allowing them to turn off. This minimizes overshooting the charge target."}
TextBox{parent=fac_c_7,y=8,height=3,text="You can extend this to a full minute to minimize reactors flickering on/off, but there may be more overshoot of the target."}
--#endregion
--#region Dynamic Tank Fluid Types
local ext_idling = Checkbox{parent=fac_c_7,x=1,y=12,label="Enable Extended Idling",default=ini_cfg.ExtChargeIdling,box_fg_bg=cpair(colors.yellow,colors.black)}
TextBox{parent=fac_c_7,height=3,text="Specify each tank's coolant type, for display use only. Water is the only option if one or more of the connected units is water cooled."}
local function back_from_idling()
local tank_fluid_list = Div{parent=fac_c_7,x=1,y=5,height=8}
function self.draw_fluid_ops()
tank_fluid_list.remove_all()
local tank_list = tmp_cfg.FacilityTankList
local tank_conns = tmp_cfg.FacilityTankConns
local next_f = 1
for i = 1, #tank_list do
local type = tmp_cfg.TankFluidTypes[i]
if type == 0 then type = 1 end
self.tank_fluid_opts[i] = nil
if tank_list[i] == 1 then
local row = Div{parent=tank_fluid_list,height=2}
TextBox{parent=row,width=11,text="Unit Tank "..i}
TextBox{parent=row,text="Connected to: Unit "..i,fg_bg=cpair(colors.gray,colors.lightGray)}
local tank_fluid = Radio2D{parent=row,x=34,y=1,rows=1,columns=2,default=type,options={"Water","Sodium"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.yellow,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
if tmp_cfg.CoolingConfig[i].BoilerCount == 0 then
tank_fluid.set_value(1)
tank_fluid.disable()
end
self.tank_fluid_opts[i] = tank_fluid
elseif tank_list[i] == 2 then
local row = Div{parent=tank_fluid_list,height=2}
TextBox{parent=row,width=15,text="Facility Tank "..next_f}
local conns = ""
local any_bwr = false
for u = 1, #tank_conns do
if tank_conns[u] == i then
conns = conns .. tri(conns == "", "", ", ") .. "Unit " .. u
any_bwr = any_bwr or (tmp_cfg.CoolingConfig[u].BoilerCount == 0)
end
end
TextBox{parent=row,text="Connected to: "..conns,fg_bg=cpair(colors.gray,colors.lightGray)}
local tank_fluid = Radio2D{parent=row,x=34,y=1,rows=1,columns=2,default=type,options={"Water","Sodium"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.yellow,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
if any_bwr then
tank_fluid.set_value(1)
tank_fluid.disable()
end
self.tank_fluid_opts[i] = tank_fluid
next_f = next_f + 1
end
end
end
local function back_from_fluids()
fac_pane.set_value(tri(tmp_cfg.FacilityTankMode == 0, 3, 5))
end
local function submit_tank_fluids()
tmp_cfg.TankFluidTypes = {}
for i = 1, #tmp_cfg.FacilityTankList do
if self.tank_fluid_opts[i] ~= nil then
tmp_cfg.TankFluidTypes[i] = self.tank_fluid_opts[i].get_value()
else
tmp_cfg.TankFluidTypes[i] = 0
end
end
fac_pane.set_value(8)
end
PushButton{parent=fac_c_7,x=1,y=14,text="\x1b Back",callback=back_from_fluids,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_7,x=44,y=14,text="Next \x1a",callback=submit_tank_fluids,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Auxiliary Coolant
TextBox{parent=fac_c_8,height=5,text="Auxiliary water coolant can be enabled for units to provide extra water during turbine ramp-up. For water cooled reactors, this goes to the reactor. For sodium cooled reactors, water goes to the boiler."}
for i = 1, 4 do
local line = Div{parent=fac_c_8,x=1,y=7+i,height=1}
TextBox{parent=line,text="Unit "..i.." -",width=8}
local aux_cool = Checkbox{parent=line,x=10,y=1,label="Has Auxiliary Coolant",default=ini_cfg.AuxiliaryCoolant[i],box_fg_bg=cpair(colors.yellow,colors.black)}
tool_ctl.aux_cool_elems[i] = { line = line, enable = aux_cool }
end
local function submit_aux_cool()
tmp_cfg.AuxiliaryCoolant = {}
for i = 1, tmp_cfg.UnitCount do
tmp_cfg.AuxiliaryCoolant[i] = tool_ctl.aux_cool_elems[i].enable.get_value()
end
fac_pane.set_value(9)
end
PushButton{parent=fac_c_8,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_8,x=44,y=14,text="Next \x1a",callback=submit_aux_cool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Extended Idling
TextBox{parent=fac_c_9,height=6,text="Charge control provides automatic control to maintain an induction matrix charge level. In order to have smoother control, reactors that were activated will be held on at 0.01 mB/t for a short period before allowing them to turn off. This minimizes overshooting the charge target."}
TextBox{parent=fac_c_9,y=8,height=3,text="You can extend this to a full minute to minimize reactors flickering on/off, but there may be more overshoot of the target."}
local ext_idling = Checkbox{parent=fac_c_9,x=1,y=12,label="Enable Extended Idling",default=ini_cfg.ExtChargeIdling,box_fg_bg=cpair(colors.yellow,colors.black)}
local function submit_idling()
tmp_cfg.ExtChargeIdling = ext_idling.get_value()
main_pane.set_value(3)
end
PushButton{parent=fac_c_7,x=1,y=14,text="\x1b Back",callback=back_from_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_7,x=44,y=14,text="Next \x1a",callback=submit_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_9,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(8)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_9,x=44,y=14,text="Next \x1a",callback=submit_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#endregion

View File

@@ -1,6 +1,9 @@
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local facility = require("supervisor.config.facility")
local core = require("graphics.core")
local themes = require("graphics.themes")
@@ -399,6 +402,10 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
try_set(tool_ctl.tank_elems[i].tank_opt, ini_cfg.FacilityTankDefs[i])
end
for i = 1, #ini_cfg.AuxiliaryCoolant do
try_set(tool_ctl.aux_cool_elems[i].enable, ini_cfg.AuxiliaryCoolant[i])
end
tool_ctl.en_fac_tanks.set_value(ini_cfg.FacilityTankMode > 0)
tool_ctl.view_cfg.enable()
@@ -508,8 +515,15 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
else
tmp_cfg.FacilityTankMode = 0
tmp_cfg.FacilityTankDefs = {}
-- on facility tank mode 0, setup tank defs to match unit tank option
for i = 1, tmp_cfg.UnitCount do
tmp_cfg.FacilityTankDefs[i] = tri(tmp_cfg.CoolingConfig[i].TankConnection, 1, 0)
end
end
tmp_cfg.FacilityTankList, tmp_cfg.FacilityTankConns = facility.generate_tank_list_and_conns(tmp_cfg.FacilityTankMode, tmp_cfg.FacilityTankDefs)
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
tmp_cfg.PLC_Channel = config.PLC_CHANNEL
tmp_cfg.RTU_Channel = config.RTU_CHANNEL
@@ -557,6 +571,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
local val_max_w = (inner_width - label_w) + 1
local raw = cfg[f[1]]
local val = util.strval(raw)
local skip = false
if f[1] == "AuthKey" then val = string.rep("*", string.len(val))
elseif f[1] == "LogMode" then val = tri(raw == log.MODE.APPEND, "append", "replace")
@@ -577,45 +592,114 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
end
if val == "" then val = "no facility tanks" end
elseif f[1] == "FacilityTankMode" and raw == 0 then val = "0 (n/a, unit mode)"
elseif f[1] == "FacilityTankMode" and raw == 0 then val = "no facility tanks"
elseif f[1] == "FacilityTankDefs" and type(cfg.FacilityTankDefs) == "table" then
local tank_name_list = { table.unpack(cfg.FacilityTankList) } ---@type (string|integer)[]
local next_f = 1
val = ""
for idx = 1, #tank_name_list do
if tank_name_list[idx] == 1 then
tank_name_list[idx] = "U" .. idx
elseif tank_name_list[idx] == 2 then
tank_name_list[idx] = "F" .. next_f
next_f = next_f + 1
end
end
for idx = 1, #cfg.FacilityTankDefs do
local t_mode = "not connected to a tank"
if cfg.FacilityTankDefs[idx] == 1 then
t_mode = "connected to its unit tank"
t_mode = "connected to its unit tank (" .. tank_name_list[cfg.FacilityTankConns[idx]] .. ")"
elseif cfg.FacilityTankDefs[idx] == 2 then
t_mode = "connected to a facility tank"
t_mode = "connected to facility tank " .. tank_name_list[cfg.FacilityTankConns[idx]]
end
val = val .. tri(idx == 1, "", "\n") .. util.sprintf(" \x07 unit %d - %s", idx, t_mode)
end
if val == "" then val = "no facility tanks" end
elseif f[1] == "FacilityTankList" or f[1] == "FacilityTankConns" then
-- hide these since this info is available in the FacilityTankDefs list (connections) and TankFluidTypes list (list of tanks)
skip = true
elseif f[1] == "TankFluidTypes" and type(cfg.TankFluidTypes) == "table" and type(cfg.FacilityTankList) == "table" then
local tank_list = cfg.FacilityTankList
local next_f = 1
val = ""
local count = 0
for idx = 1, #tank_list do
if tank_list[idx] > 0 then count = count + 1 end
end
local bullet = tri(count < 2, "", " \x07 ")
for idx = 1, #tank_list do
local prefix = "?"
local fluid = "water"
local type = cfg.TankFluidTypes[idx]
if tank_list[idx] > 0 then
if tank_list[idx] == 1 then
prefix = "U" .. idx
elseif tank_list[idx] == 2 then
prefix = "F" .. next_f
next_f = next_f + 1
end
if type == types.COOLANT_TYPE.SODIUM then
fluid = "sodium"
end
val = val .. tri(val == "", "", "\n") .. util.sprintf(bullet .. "tank %s - %s", prefix, fluid)
end
end
if val == "" then val = "no emergency coolant tanks" end
elseif f[1] == "AuxiliaryCoolant" then
val = ""
local count = 0
for idx = 1, #cfg.AuxiliaryCoolant do
if cfg.AuxiliaryCoolant[idx] then count = count + 1 end
end
local bullet = tri(count < 2, "", " \x07 ")
for idx = 1, #cfg.AuxiliaryCoolant do
if cfg.AuxiliaryCoolant[idx] then
val = val .. tri(val == "", "", "\n") .. util.sprintf(bullet .. "unit %d", idx)
end
end
if val == "" then val = "no auxiliary coolant" end
end
if val == "nil" then val = "<not set>" end
if not skip then
if val == "nil" then val = "<not set>" end
local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if string.len(val) > val_max_w then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end
local line = Div{parent=setting_list,height=height,fg_bg=c}
TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)}
local textbox
if height > 1 then
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end
if f[1] == "AuthKey" then self.auth_key_textbox = textbox end
end
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

View File

@@ -30,7 +30,8 @@ local CENTER = core.ALIGN.CENTER
-- changes to the config data/format to let the user know
local changes = {
{ "v1.2.12", { "Added front panel UI theme", "Added color accessibility modes" } },
{ "v1.3.2", { "Added standard with black off state color mode", "Added blue indicator color modes" } }
{ "v1.3.2", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
{ "v1.6.0", { "Added sodium emergency coolant option" } }
}
---@class svr_configurator
@@ -51,6 +52,7 @@ style.btn_dis_fg_bg = cpair(colors.lightGray, colors.white)
---@class _svr_cfg_tool_ctl
local tool_ctl = {
launch_startup = false,
ask_config = false,
has_config = false,
viewing_config = false,
@@ -70,15 +72,20 @@ local tool_ctl = {
load_legacy = nil, ---@type function
cooling_elems = {}, ---@type { line: Div, turbines: NumberField, boilers: NumberField, tank: Checkbox }[]
tank_elems = {} ---@type { div: Div, tank_opt: Radio2D, no_tank: TextBox }[]
tank_elems = {}, ---@type { div: Div, tank_opt: Radio2D, no_tank: TextBox }[]
aux_cool_elems = {} ---@type { line: Div, enable: Checkbox }[]
}
---@class svr_config
local tmp_cfg = {
UnitCount = 1,
CoolingConfig = {}, ---@type { TurbineCount: integer, BoilerCount: integer, TankConnection: boolean }[]
FacilityTankMode = 0,
FacilityTankDefs = {}, ---@type integer[]
FacilityTankMode = 0, -- dynamic tank emergency coolant layout
FacilityTankDefs = {}, ---@type integer[] each unit's tank connection target (0 = disconnected, 1 = unit, 2 = facility)
FacilityTankList = {}, ---@type integer[] list of tanks by slot (0 = none or covered by an above tank, 1 = unit tank, 2 = facility tank)
FacilityTankConns = {}, ---@type integer[] map of unit tank connections (indicies are units, values are tank indicies in the tank list)
TankFluidTypes = {}, ---@type integer[] which type of fluid each tank in the tank list should be containing
AuxiliaryCoolant = {}, ---@type boolean[] if a unit has auxiliary coolant
ExtChargeIdling = false,
SVR_Channel = nil, ---@type integer
PLC_Channel = nil, ---@type integer
@@ -109,6 +116,10 @@ local fields = {
{ "CoolingConfig", "Cooling Configuration", {} },
{ "FacilityTankMode", "Facility Tank Mode", 0 },
{ "FacilityTankDefs", "Facility Tank Definitions", {} },
{ "FacilityTankList", "Facility Tank List", {} }, -- hidden
{ "FacilityTankConns", "Facility Tank Connections", {} }, -- hidden
{ "TankFluidTypes", "Tank Fluid Types", {} },
{ "AuxiliaryCoolant", "Auxiliary Water Coolant", {} },
{ "ExtChargeIdling", "Extended Charge Idling", false },
{ "SVR_Channel", "SVR Channel", 16240 },
{ "PLC_Channel", "PLC Channel", 16241 },
@@ -175,7 +186,7 @@ local function config_view(display)
TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the Supervisor configurator! Please select one of the following options."}
if tool_ctl.ask_config then
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device had no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
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 supervisor. 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
@@ -201,9 +212,17 @@ local function config_view(display)
main_pane.set_value(5)
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}
tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(7)end,fg_bg=nav_fg_bg,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=btn_dis_fg_bg}
PushButton{parent=main_page,x=39,y=y_start+2,min_width=12,text="Change Log",callback=function()main_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
if tool_ctl.ask_config then start_btn.disable() end
if not tool_ctl.has_config then
tool_ctl.view_cfg.disable()
@@ -266,6 +285,10 @@ function configurator.configure(ask_config)
load_settings(settings_cfg, true)
tool_ctl.has_config = load_settings(ini_cfg)
-- these need to be initialized as they are used before being set
tmp_cfg.FacilityTankMode = ini_cfg.FacilityTankMode
tmp_cfg.TankFluidTypes = { table.unpack(ini_cfg.TankFluidTypes) }
reset_term()
-- set overridden colors
@@ -308,7 +331,7 @@ function configurator.configure(ask_config)
println("configurator error: " .. error)
end
return status, error
return status, error, tool_ctl.launch_startup
end
return configurator

View File

@@ -2,13 +2,19 @@ local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local alarm_ctl = require("supervisor.alarm_ctl")
local unit = require("supervisor.unit")
local fac_update = require("supervisor.facility_update")
local rsctl = require("supervisor.session.rsctl")
local svsessions = require("supervisor.session.svsessions")
local AISTATE = alarm_ctl.AISTATE
local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE
local AUTO_GROUP = types.AUTO_GROUP
local PRIO = types.ALARM_PRIORITY
local PROCESS = types.PROCESS
local RTU_ID_FAIL = types.RTU_ID_FAIL
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
@@ -17,7 +23,7 @@ local WASTE = types.WASTE_PRODUCT
---@enum AUTO_SCRAM
local AUTO_SCRAM = {
NONE = 0,
MATRIX_DC = 1,
MATRIX_FAULT = 1,
MATRIX_FILL = 2,
CRIT_ALARM = 3,
RADIATION = 4,
@@ -31,6 +37,17 @@ local START_STATUS = {
BLADE_MISMATCH = 2
}
---@enum RECOVERY_STATE
local RCV_STATE = {
INACTIVE = 0,
PRIMED = 1,
RUNNING = 2,
STOPPED = 3
}
local CHARGE_SCALER = 1000000 -- convert MFE to FE
local GEN_SCALER = 1000 -- convert kFE to FE
---@class facility_management
local facility = {}
@@ -41,7 +58,7 @@ function facility.new(config)
---@class _facility_self
local self = {
units = {}, ---@type reactor_unit[]
types = { AUTO_SCRAM = AUTO_SCRAM, START_STATUS = START_STATUS },
types = { AUTO_SCRAM = AUTO_SCRAM, START_STATUS = START_STATUS, RCV_STATE = RCV_STATE },
status_text = { "START UP", "initializing..." },
all_sys_ok = false,
allow_testing = false,
@@ -51,7 +68,10 @@ function facility.new(config)
r_cool = config.CoolingConfig,
fac_tank_mode = config.FacilityTankMode,
fac_tank_defs = config.FacilityTankDefs,
fac_tank_list = {} ---@type integer[]
fac_tank_list = config.FacilityTankList,
fac_tank_conns = config.FacilityTankConns,
tank_fluid_types = config.TankFluidTypes,
aux_coolant = config.AuxiliaryCoolant
},
-- rtus
rtu_gw_conn_count = 0,
@@ -64,12 +84,15 @@ function facility.new(config)
-- redstone I/O control
io_ctl = nil, ---@type rs_controller
-- process control
recovery = RCV_STATE.INACTIVE, ---@type RECOVERY_STATE
recovery_boot_state = nil, ---@type sv_boot_state|nil
last_unit_states = {}, ---@type boolean[]
units_ready = false,
mode = PROCESS.INACTIVE,
last_mode = PROCESS.INACTIVE,
return_mode = PROCESS.INACTIVE,
mode_set = PROCESS.MAX_BURN,
start_fail = START_STATUS.OK,
mode = PROCESS.INACTIVE, ---@type PROCESS
last_mode = PROCESS.INACTIVE, ---@type PROCESS
return_mode = PROCESS.INACTIVE, ---@type PROCESS
mode_set = PROCESS.MAX_BURN, ---@type PROCESS
start_fail = START_STATUS.OK, ---@type START_STATUS
max_burn_combined = 0.0, -- maximum burn rate to clamp at
burn_target = 0.1, -- burn rate target for aggregate burn mode
charge_setpoint = 0, -- FE charge target setpoint
@@ -81,7 +104,7 @@ function facility.new(config)
ascram_reason = AUTO_SCRAM.NONE,
---@class ascram_status
ascram_status = {
matrix_dc = false,
matrix_fault = false,
matrix_fill = false,
crit_alarm = false,
radiation = false,
@@ -91,16 +114,16 @@ function facility.new(config)
charge_conversion = 1.0,
time_start = 0.0,
initial_ramp = true,
waiting_on_ramp = false,
waiting_on_stable = false,
waiting_on_ramp = false, -- waiting on auto ramping
waiting_on_stable = false, -- waiting on gen rate stabilization
accumulator = 0.0,
saturated = false,
last_update = 0,
last_error = 0.0,
last_time = 0.0,
-- waste processing
waste_product = WASTE.PLUTONIUM,
current_waste_product = WASTE.PLUTONIUM,
waste_product = WASTE.PLUTONIUM, ---@type WASTE_PRODUCT
current_waste_product = WASTE.PLUTONIUM, ---@type WASTE_PRODUCT
pu_fallback = false,
sps_low_power = false,
disabled_sps = false,
@@ -121,24 +144,36 @@ function facility.new(config)
imtx_last_charge = 0,
imtx_last_charge_t = 0,
-- track faulted induction matrix update times to reject
imtx_faulted_times = { 0, 0, 0 }
imtx_faulted_times = { 0, 0, 0 },
-- facility alarms
---@type { [string]: alarm_def }
alarms = {
-- radiation monitor alarm for the facility
FacilityRadiation = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.FacilityRadiation, tier = PRIO.CRITICAL },
},
---@type { [ALARM]: ALARM_STATE }
alarm_states = {
[ALARM.FacilityRadiation] = ALARM_STATE.INACTIVE
}
}
--#region SETUP
-- provide self to facility update functions
local f_update = fac_update(self)
-- create units
for i = 1, config.UnitCount do
table.insert(self.units,
unit.new(i, self.cooling_conf.r_cool[i].BoilerCount, self.cooling_conf.r_cool[i].TurbineCount, config.ExtChargeIdling))
table.insert(self.units, unit.new(i, self.cooling_conf.r_cool[i].BoilerCount, self.cooling_conf.r_cool[i].TurbineCount, config.ExtChargeIdling, self.cooling_conf.aux_coolant[i]))
table.insert(self.group_map, AUTO_GROUP.MANUAL)
table.insert(self.last_unit_states, false)
end
-- list for RTU session management
self.rtu_list = { self.redstone, self.induction, self.sps, self.tanks, self.envd }
-- init redstone RTU I/O controller
self.io_ctl = rsctl.new(self.redstone)
self.io_ctl = rsctl.new(self.redstone, 0)
-- fill blank alarm/tone states
for _ = 1, 12 do table.insert(self.test_alarm_states, false) end
@@ -147,99 +182,70 @@ function facility.new(config)
table.insert(self.test_tone_states, false)
end
--#region decode tank configuration
local cool_conf = self.cooling_conf
-- determine tank information
if cool_conf.fac_tank_mode == 0 then
cool_conf.fac_tank_defs = {}
-- on facility tank mode 0, setup tank defs to match unit tank option
for i = 1, config.UnitCount do
cool_conf.fac_tank_defs[i] = util.trinary(cool_conf.r_cool[i].TankConnection, 1, 0)
end
cool_conf.fac_tank_list = { table.unpack(cool_conf.fac_tank_defs) }
else
-- decode the layout of tanks from the connections definitions
local tank_mode = cool_conf.fac_tank_mode
local tank_defs = cool_conf.fac_tank_defs
local tank_list = { table.unpack(tank_defs) }
local function calc_fdef(start_idx, end_idx)
local first = 4
for i = start_idx, end_idx do
if tank_defs[i] == 2 then
if i < first then first = i end
end
end
return first
end
if tank_mode == 1 then
-- (1) 1 total facility tank (A A A A)
local first_fdef = calc_fdef(1, #tank_defs)
for i = 1, #tank_defs do
if i > first_fdef and tank_defs[i] == 2 then
tank_list[i] = 0
end
end
elseif tank_mode == 2 then
-- (2) 2 total facility tanks (A A A B)
local first_fdef = calc_fdef(1, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 3 then
-- (3) 2 total facility tanks (A A B B)
for _, a in pairs({ 1, 3 }) do
local b = a + 1
if (tank_defs[a] == 2) and (tank_defs[b] == 2) then
tank_list[b] = 0
end
end
elseif tank_mode == 4 then
-- (4) 2 total facility tanks (A B B B)
local first_fdef = calc_fdef(2, #tank_defs)
for i = 1, #tank_defs do
if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 5 then
-- (5) 3 total facility tanks (A A B C)
local first_fdef = calc_fdef(1, math.min(2, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 6 then
-- (6) 3 total facility tanks (A B B C)
local first_fdef = calc_fdef(2, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 7 then
-- (7) 3 total facility tanks (A B C C)
local first_fdef = calc_fdef(3, #tank_defs)
for i = 1, #tank_defs do
if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
end
cool_conf.fac_tank_list = tank_list
-- init next boot state
settings.set("LastProcessState", PROCESS.INACTIVE)
settings.set("LastUnitStates", self.last_unit_states)
if not settings.save("/supervisor.settings") then
log.warning("FAC: failed to save initial control state into supervisor settings file")
end
--#endregion
-- PRIVATE FUNCTIONS --
-- check an auto process control configuration and save it if its valid (does not start the process)
---@param auto_cfg start_auto_config configuration
---@return boolean ready, number[] unit_limits
local function _auto_check_and_save(auto_cfg)
local ready = false
-- load up current limits
local limits = {}
for i = 1, config.UnitCount do
limits[i] = self.units[i].get_control_inf().lim_br100 * 100
end
-- only allow changes if not running
if self.mode == PROCESS.INACTIVE then
if (type(auto_cfg.mode) == "number") and (auto_cfg.mode > PROCESS.INACTIVE) and (auto_cfg.mode <= PROCESS.GEN_RATE) then
self.mode_set = auto_cfg.mode
end
if (type(auto_cfg.burn_target) == "number") and auto_cfg.burn_target >= 0.1 then
self.burn_target = auto_cfg.burn_target
end
if (type(auto_cfg.charge_target) == "number") and auto_cfg.charge_target >= 0 then
self.charge_setpoint = auto_cfg.charge_target * CHARGE_SCALER
end
if (type(auto_cfg.gen_target) == "number") and auto_cfg.gen_target >= 0 then
self.gen_rate_setpoint = auto_cfg.gen_target * GEN_SCALER
end
if (type(auto_cfg.limits) == "table") and (#auto_cfg.limits == config.UnitCount) then
for i = 1, config.UnitCount do
local limit = auto_cfg.limits[i]
if (type(limit) == "number") and (limit >= 0.1) then
limits[i] = limit
self.units[i].set_burn_limit(limit)
end
end
end
ready = self.mode_set > 0
if ((self.mode_set == PROCESS.CHARGE) and (self.charge_setpoint <= 0)) or
((self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_setpoint <= 0)) or
((self.mode_set == PROCESS.BURN_RATE) and (self.burn_target < 0.1)) then
ready = false
end
end
return ready, limits
end
-- PUBLIC FUNCTIONS --
---@class facility
@@ -330,6 +336,9 @@ function facility.new(config)
-- update (iterate) the facility management
function public.update()
-- run reboot recovery routine if needed
f_update.boot_recovery()
-- run process control and evaluate automatic SCRAM
f_update.pre_auto()
f_update.auto_control(config.ExtChargeIdling)
@@ -342,6 +351,9 @@ function facility.new(config)
-- unit tasks
f_update.unit_mgmt()
-- update alarm states right before updating the audio
f_update.update_alarms()
-- update alarm tones
f_update.alarm_audio()
end
@@ -358,6 +370,50 @@ function facility.new(config)
--#endregion
--#region Startup Recovery
-- on exit, use this to clear the boot state so we don't resume when exiting cleanly
function public.clear_boot_state()
settings.unset("LastProcessState")
settings.unset("LastUnitStates")
if not settings.save("/supervisor.settings") then
log.warning("facility.clear_boot_state(): failed to save supervisor settings file")
else
log.debug("FAC: cleared boot state on exit")
end
end
-- initialize facility resume boot recovery
---@param state sv_boot_state|nil
function public.boot_recovery_init(state)
if self.recovery == RCV_STATE.INACTIVE and state then
self.recovery_boot_state = state
self.recovery = RCV_STATE.PRIMED
log.info("FAC: startup resume ready")
end
end
-- attempt facility resume boot recovery
---@param auto_cfg start_auto_config configuration
function public.boot_recovery_start(auto_cfg)
if self.recovery == RCV_STATE.PRIMED then
self.recovery = util.trinary(_auto_check_and_save(auto_cfg), RCV_STATE.RUNNING, RCV_STATE.STOPPED)
log.info(util.c("FAC: startup resume ", util.trinary(self.recovery == RCV_STATE.RUNNING, "started", "failed")))
else self.recovery = RCV_STATE.STOPPED end
end
-- used on certain coordinator commands to end reboot recovery (remain in current operational state)
function public.cancel_recovery()
if self.recovery == RCV_STATE.RUNNING then
self.recovery = RCV_STATE.STOPPED
self.recovery_boot_state = nil
log.info("FAC: process startup resume cancelled by user operation")
end
end
--#endregion
--#region Commands
-- SCRAM all reactor units
@@ -367,10 +423,14 @@ function facility.new(config)
end
end
-- ack all alarms on all reactor units
-- ack all alarms on all reactor units and the facility
function public.ack_all()
for i = 1, #self.units do
self.units[i].ack_all()
-- unit alarms
for i = 1, #self.units do self.units[i].ack_all() end
-- facility alarms
for id, state in pairs(self.alarm_states) do
if state == ALARM_STATE.TRIPPED then self.alarm_states[id] = ALARM_STATE.ACKED end
end
end
@@ -381,59 +441,13 @@ function facility.new(config)
function public.auto_stop() self.mode = PROCESS.INACTIVE end
-- set automatic control configuration and start the process
---@param auto_cfg sys_auto_config configuration
---@param auto_cfg start_auto_config configuration
---@return table response ready state (successfully started) and current configuration (after updating)
function public.auto_start(auto_cfg)
local charge_scaler = 1000000 -- convert MFE to FE
local gen_scaler = 1000 -- convert kFE to FE
local ready = false
local ready, limits = _auto_check_and_save(auto_cfg)
-- load up current limits
local limits = {}
for i = 1, config.UnitCount do
limits[i] = self.units[i].get_control_inf().lim_br100 * 100
end
-- only allow changes if not running
if self.mode == PROCESS.INACTIVE then
if (type(auto_cfg.mode) == "number") and (auto_cfg.mode > PROCESS.INACTIVE) and (auto_cfg.mode <= PROCESS.GEN_RATE) then
self.mode_set = auto_cfg.mode
end
if (type(auto_cfg.burn_target) == "number") and auto_cfg.burn_target >= 0.1 then
self.burn_target = auto_cfg.burn_target
end
if (type(auto_cfg.charge_target) == "number") and auto_cfg.charge_target >= 0 then
self.charge_setpoint = auto_cfg.charge_target * charge_scaler
end
if (type(auto_cfg.gen_target) == "number") and auto_cfg.gen_target >= 0 then
self.gen_rate_setpoint = auto_cfg.gen_target * gen_scaler
end
if (type(auto_cfg.limits) == "table") and (#auto_cfg.limits == config.UnitCount) then
for i = 1, config.UnitCount do
local limit = auto_cfg.limits[i]
if (type(limit) == "number") and (limit >= 0.1) then
limits[i] = limit
self.units[i].set_burn_limit(limit)
end
end
end
ready = self.mode_set > 0
if ((self.mode_set == PROCESS.CHARGE) and (self.charge_setpoint <= 0)) or
((self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_setpoint <= 0)) or
((self.mode_set == PROCESS.BURN_RATE) and (self.burn_target < 0.1)) then
ready = false
end
ready = ready and self.units_ready
if ready then self.mode = self.mode_set end
if ready and self.units_ready then
self.mode = self.mode_set
end
log.debug(util.c("FAC: process start ", util.trinary(ready, "accepted", "rejected")))
@@ -442,8 +456,8 @@ function facility.new(config)
ready,
self.mode_set,
self.burn_target,
self.charge_setpoint / charge_scaler,
self.gen_rate_setpoint / gen_scaler,
self.charge_setpoint / CHARGE_SCALER,
self.gen_rate_setpoint / GEN_SCALER,
limits
}
end
@@ -599,7 +613,7 @@ function facility.new(config)
self.waiting_on_ramp or self.waiting_on_stable,
self.at_max_burn or self.saturated,
self.ascram,
astat.matrix_dc,
astat.matrix_fault,
astat.matrix_fill,
astat.crit_alarm,
astat.radiation,

View File

@@ -1,17 +1,23 @@
local audio = require("scada-common.audio")
local const = require("scada-common.constants")
local log = require("scada-common.log")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local audio = require("scada-common.audio")
local const = require("scada-common.constants")
local log = require("scada-common.log")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local qtypes = require("supervisor.session.rtu.qtypes")
local alarm_ctl = require("supervisor.alarm_ctl")
local plc = require("supervisor.session.plc")
local svsessions = require("supervisor.session.svsessions")
local qtypes = require("supervisor.session.rtu.qtypes")
local TONE = audio.TONE
local ALARM = types.ALARM
local PRIO = types.ALARM_PRIORITY
local ALARM_STATE = types.ALARM_STATE
local AUTO_GROUP = types.AUTO_GROUP
local CONTAINER_MODE = types.CONTAINER_MODE
local PROCESS = types.PROCESS
local PROCESS_NAMES = types.PROCESS_NAMES
@@ -131,6 +137,54 @@ end
--#region PUBLIC FUNCTIONS
-- run reboot recovery routine if needed
function update.boot_recovery()
local RCV_STATE = self.types.RCV_STATE
-- attempt reboot recovery if in progress
if self.recovery == RCV_STATE.RUNNING then
local was_inactive = self.recovery_boot_state.mode == PROCESS.INACTIVE or self.recovery_boot_state.mode == PROCESS.SYSTEM_ALARM_IDLE
-- try to start auto control
if self.recovery_boot_state.mode ~= nil and self.units_ready then
if not was_inactive then
self.mode = self.mode_set
log.info("FAC: process startup resume initiated")
end
self.recovery_boot_state.mode = nil
end
local recovered = self.recovery_boot_state.mode == nil or was_inactive
-- restore manual control reactors
for i = 1, #self.units do
local u = self.units[i]
if self.recovery_boot_state.unit_states[i] and self.group_map[i] == AUTO_GROUP.MANUAL then
recovered = false
if u.get_control_inf().ready then
local plc_s = svsessions.get_reactor_session(i)
if plc_s ~= nil then
plc_s.in_queue.push_command(plc.PLC_S_CMDS.ENABLE)
log.info("FAC: startup resume enabling manually controlled reactor unit #" .. i)
-- only execute once
self.recovery_boot_state.unit_states[i] = nil
end
end
end
end
if recovered then
self.recovery = RCV_STATE.STOPPED
self.recovery_boot_state = nil
log.info("FAC: startup resume sequence completed")
end
end
end
-- automatic control pre-update logic
function update.pre_auto()
-- unlink RTU sessions if they are closed
@@ -243,6 +297,11 @@ function update.auto_control(ExtChargeIdling)
log.debug(util.c("FAC: state changed from ", PROCESS_NAMES[self.last_mode + 1], " to ", PROCESS_NAMES[self.mode + 1]))
settings.set("LastProcessState", self.mode)
if not settings.save("/supervisor.settings") then
log.warning("facility_update.auto_control(): failed to save supervisor settings file")
end
if (self.last_mode == PROCESS.INACTIVE) or (self.last_mode == PROCESS.GEN_RATE_FAULT_IDLE) then
self.start_fail = START_STATUS.OK
@@ -341,9 +400,17 @@ function update.auto_control(ExtChargeIdling)
if state_changed then
self.time_start = now
self.saturated = true
self.waiting_on_ramp = true
self.status_text = { "MONITORED MODE", "running reactors at limit" }
self.status_text = { "MONITORED MODE", "ramping reactors to limit" }
log.info("FAC: MAX_BURN process mode started")
elseif self.waiting_on_ramp then
if all_units_ramped() then
self.waiting_on_ramp = false
self.status_text = { "MONITORED MODE", "running reactors at limit" }
log.info("FAC: MAX_BURN process mode initial ramp completed")
end
end
allocate_burn_rate(self.max_burn_combined, true)
@@ -351,8 +418,17 @@ function update.auto_control(ExtChargeIdling)
-- a total aggregate burn rate
if state_changed then
self.time_start = now
self.status_text = { "BURN RATE MODE", "running" }
self.waiting_on_ramp = true
self.status_text = { "BURN RATE MODE", "ramping to target" }
log.info("FAC: BURN_RATE process mode started")
elseif self.waiting_on_ramp then
if all_units_ramped() then
self.waiting_on_ramp = false
self.status_text = { "BURN RATE MODE", "running" }
log.info("FAC: BURN_RATE process mode initial ramp completed")
end
end
local unallocated = allocate_burn_rate(self.burn_target, true)
@@ -511,13 +587,19 @@ function update.auto_safety()
local astatus = self.ascram_status
-- matrix related checks
if self.induction[1] ~= nil then
local db = self.induction[1].get_db()
-- clear matrix disconnected
if astatus.matrix_dc then
astatus.matrix_dc = false
log.info("FAC: induction matrix reconnected, clearing ASCRAM condition")
-- check for unformed or faulted state
local i_ok = db.formed and not self.induction[1].is_faulted()
-- clear matrix fault if ok again
if astatus.matrix_fault and i_ok then
astatus.matrix_fault = false
log.info("FAC: induction matrix OK, clearing ASCRAM condition")
else
astatus.matrix_fault = not i_ok
end
-- check matrix fill too high
@@ -528,42 +610,42 @@ function update.auto_safety()
log.info(util.c("FAC: charge state of induction matrix entered acceptable range <= ", ALARM_LIMS.CHARGE_RE_ENABLE * 100, "%"))
end
-- check for critical unit alarms
astatus.crit_alarm = false
for i = 1, #self.units do
local u = self.units[i]
if u.has_alarm_min_prio(PRIO.CRITICAL) then
astatus.crit_alarm = true
break
end
end
-- check for facility radiation
if #self.envd > 0 then
local max_rad = 0
for i = 1, #self.envd do
local envd = self.envd[i]
local e_db = envd.get_db()
if e_db.radiation_raw > max_rad then max_rad = e_db.radiation_raw end
end
astatus.radiation = max_rad >= ALARM_LIMS.FAC_HIGH_RAD
else
-- don't clear, if it is true then we lost it with high radiation, so just keep alarming
-- operator can restart the system or hit the stop/reset button
end
-- system not ready, will need to restart GEN_RATE mode
-- clears when we enter the fault waiting state
astatus.gen_fault = self.mode == PROCESS.GEN_RATE and not self.units_ready
else
astatus.matrix_dc = true
astatus.matrix_fault = true
end
-- check for critical unit alarms
astatus.crit_alarm = false
for i = 1, #self.units do
local u = self.units[i]
if u.has_alarm_min_prio(PRIO.CRITICAL) then
astatus.crit_alarm = true
break
end
end
-- check for facility radiation
if #self.envd > 0 then
local max_rad = 0
for i = 1, #self.envd do
local envd = self.envd[i]
local e_db = envd.get_db()
if e_db.radiation_raw > max_rad then max_rad = e_db.radiation_raw end
end
astatus.radiation = max_rad >= ALARM_LIMS.FAC_HIGH_RAD
else
-- don't clear, if it is true then we lost it with high radiation, so just keep alarming
-- operator can restart the system or hit the stop/reset button
end
if (self.mode ~= PROCESS.INACTIVE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then
local scram = astatus.matrix_dc or astatus.matrix_fill or astatus.crit_alarm or astatus.gen_fault
local scram = astatus.matrix_fault or astatus.matrix_fill or astatus.crit_alarm or astatus.radiation or astatus.gen_fault
if scram and not self.ascram then
-- SCRAM all units
@@ -587,14 +669,14 @@ function update.auto_safety()
self.status_text = { "AUTOMATIC SCRAM", "facility radiation high" }
log.info("FAC: automatic SCRAM due to high facility radiation")
elseif astatus.matrix_dc then
elseif astatus.matrix_fault then
next_mode = PROCESS.MATRIX_FAULT_IDLE
self.ascram_reason = AUTO_SCRAM.MATRIX_DC
self.status_text = { "AUTOMATIC SCRAM", "induction matrix disconnected" }
self.ascram_reason = AUTO_SCRAM.MATRIX_FAULT
self.status_text = { "AUTOMATIC SCRAM", "induction matrix fault" }
if self.mode ~= PROCESS.MATRIX_FAULT_IDLE then self.return_mode = self.mode end
log.info("FAC: automatic SCRAM due to induction matrix disconnection")
log.info("FAC: automatic SCRAM due to induction matrix disconnected, unformed, or faulted")
elseif astatus.matrix_fill then
next_mode = PROCESS.MATRIX_FAULT_IDLE
self.ascram_reason = AUTO_SCRAM.MATRIX_FILL
@@ -619,25 +701,32 @@ function update.auto_safety()
self.ascram_reason = AUTO_SCRAM.NONE
-- reset PLC RPS trips if we should
for i = 1, #self.units do
local u = self.units[i]
u.auto_cond_rps_reset()
for i = 1, #self.prio_defs do
for _, u in pairs(self.prio_defs[i]) do
u.auto_cond_rps_reset()
end
end
end
end
end
-- update last mode and set next mode
-- update last mode, set next mode, and update saved state as needed
function update.post_auto()
self.last_mode = self.mode
self.mode = next_mode
end
-- update facility alarm states
function update.update_alarms()
-- Facility Radiation
alarm_ctl.update_alarm_state("FAC", self.alarm_states, self.ascram_status.radiation, self.alarms.FacilityRadiation, true)
end
-- update alarm audio control
function update.alarm_audio()
local allow_test = self.allow_testing and self.test_tone_set
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false, false }
-- reset tone states before re-evaluting
for i = 1, #self.tone_states do self.tone_states[i] = false end
@@ -653,8 +742,11 @@ function update.alarm_audio()
end
end
-- record facility alarms
alarms[ALARM.FacilityRadiation] = self.alarm_states[ALARM.FacilityRadiation] == ALARM_STATE.TRIPPED
-- clear testing alarms if we aren't using them
if not self.test_tone_reset then
-- clear testing alarms if we aren't using them
for i = 1, #self.test_alarm_states do self.test_alarm_states[i] = false end
end
end
@@ -693,7 +785,7 @@ function update.alarm_audio()
end
-- radiation is a big concern, always play this CRITICAL level alarm if active
if alarms[ALARM.ContainmentRadiation] then
if alarms[ALARM.ContainmentRadiation] or alarms[ALARM.FacilityRadiation] then
self.tone_states[TONE.T_800Hz_1000Hz_Alt] = true
-- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled
-- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one
@@ -769,6 +861,7 @@ end
function update.unit_mgmt()
local insufficent_po_rate = false
local need_emcool = false
local write_state = false
for i = 1, #self.units do
local u = self.units[i]
@@ -784,6 +877,21 @@ function update.unit_mgmt()
if (self.cooling_conf.fac_tank_mode > 0) and u.is_emer_cool_tripped() and (self.cooling_conf.fac_tank_defs[i] == 2) then
need_emcool = true
end
-- check for enabled state changes to save
if self.last_unit_states[i] ~= u.is_reactor_enabled() then
self.last_unit_states[i] = u.is_reactor_enabled()
write_state = true
end
end
-- record unit control states
if write_state then
settings.set("LastUnitStates", self.last_unit_states)
if not settings.save("/supervisor.settings") then
log.warning("facility_update.unit_mgmt(): failed to save supervisor settings file")
end
end
-- update waste product

View File

@@ -25,6 +25,8 @@ local function init(parent, id)
local label_fg = style.fp.label_fg
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=style.theme.highlight_box_bright}
@@ -40,9 +42,9 @@ local function init(parent, id)
local pdg_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,fg_bg=label_fg}
pdg_fw_v.register(databus.ps, ps_prefix .. "fw", pdg_fw_v.set_value)
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4}
local pdg_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,fg_bg=label_fg}
TextBox{parent=entry,x=term_w-16,y=2,text="RTT:",width=4}
local pdg_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}
pdg_rtt.register(databus.ps, ps_prefix .. "rtt", pdg_rtt.update)
pdg_rtt.register(databus.ps, ps_prefix .. "rtt_color", pdg_rtt.recolor)

View File

@@ -25,6 +25,8 @@ local function init(parent, id)
local label_fg = style.fp.label_fg
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=style.theme.highlight_box_bright}
@@ -40,13 +42,13 @@ local function init(parent, id)
local unit_count = DataIndicator{parent=entry,x=17,y=2,label="",unit="",format="%2d",value=0,width=2,fg_bg=style.fp.label_d_fg}
unit_count.register(databus.ps, ps_prefix .. "units", unit_count.set_value)
TextBox{parent=entry,x=21,y=2,text="FW:",width=3}
local rtu_fw_v = TextBox{parent=entry,x=25,y=2,text=" ------- ",width=9,fg_bg=label_fg}
TextBox{parent=entry,x=term_w-30,y=2,text="FW:",width=3}
local rtu_fw_v = TextBox{parent=entry,x=term_w-26,y=2,text=" ------- ",width=9,fg_bg=label_fg}
rtu_fw_v.register(databus.ps, ps_prefix .. "fw", rtu_fw_v.set_value)
TextBox{parent=entry,x=36,y=2,text="RTT:",width=4}
local rtu_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,fg_bg=label_fg}
TextBox{parent=entry,x=term_w-15,y=2,text="RTT:",width=4}
local rtu_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}
rtu_rtt.register(databus.ps, ps_prefix .. "rtt", rtu_rtt.update)
rtu_rtt.register(databus.ps, ps_prefix .. "rtt_color", rtu_rtt.recolor)

View File

@@ -41,6 +41,8 @@ local function init(panel)
local label_fg = style.fp.label_fg
local label_d_fg = style.fp.label_d_fg
local term_w, term_h = term.getSize()
TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
local page_div = Div{parent=panel,x=1,y=3}
@@ -73,9 +75,9 @@ local function init(panel)
-- about footer
--
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
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(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@@ -87,7 +89,7 @@ local function init(panel)
-- plc sessions page
local plc_page = Div{parent=page_div,x=1,y=1,hidden=true}
local plc_list = Div{parent=plc_page,x=2,y=2,width=49}
local plc_list = Div{parent=plc_page,x=2,y=2,width=term_w-2}
for i = 1, supervisor.config.UnitCount do
local ps_prefix = "plc_" .. i .. "_"
@@ -103,13 +105,13 @@ local function init(panel)
local plc_addr = TextBox{parent=plc_entry,x=17,y=2,text=" --- ",width=5,fg_bg=label_d_fg}
plc_addr.register(databus.ps, ps_prefix .. "addr", plc_addr.set_value)
TextBox{parent=plc_entry,x=23,y=2,text="FW:",width=3}
local plc_fw_v = TextBox{parent=plc_entry,x=27,y=2,text=" ------- ",width=9,fg_bg=label_fg}
TextBox{parent=plc_entry,x=term_w-28,y=2,text="FW:",width=3}
local plc_fw_v = TextBox{parent=plc_entry,x=term_w-24,y=2,text=" ------- ",width=9,fg_bg=label_fg}
plc_fw_v.register(databus.ps, ps_prefix .. "fw", plc_fw_v.set_value)
TextBox{parent=plc_entry,x=37,y=2,text="RTT:",width=4}
local plc_rtt = DataIndicator{parent=plc_entry,x=42,y=2,label="",unit="",format="%4d",value=0,width=4,fg_bg=label_fg}
TextBox{parent=plc_entry,x=47,y=2,text="ms",width=4,fg_bg=label_fg}
TextBox{parent=plc_entry,x=term_w-14,y=2,text="RTT:",width=4}
local plc_rtt = DataIndicator{parent=plc_entry,x=term_w-9,y=2,label="",unit="",format="%4d",value=0,width=4,fg_bg=label_fg}
TextBox{parent=plc_entry,x=term_w-4,y=2,text="ms",width=4,fg_bg=label_fg}
plc_rtt.register(databus.ps, ps_prefix .. "rtt", plc_rtt.update)
plc_rtt.register(databus.ps, ps_prefix .. "rtt_color", plc_rtt.recolor)
@@ -119,13 +121,13 @@ local function init(panel)
-- rtu sessions page
local rtu_page = Div{parent=page_div,x=1,y=1,hidden=true}
local rtu_list = ListBox{parent=rtu_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local rtu_list = ListBox{parent=rtu_page,y=1,height=term_h-2,width=term_w,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=rtu_list,height=1} -- padding
-- coordinator session page
local crd_page = Div{parent=page_div,x=1,y=1,hidden=true}
local crd_box = Div{parent=crd_page,x=2,y=2,width=49,height=4,fg_bg=s_hi_bright}
local crd_box = Div{parent=crd_page,x=2,y=2,width=term_w-2,height=4,fg_bg=s_hi_bright}
local crd_conn = LED{parent=crd_box,x=2,y=2,label="CONNECTION",colors=cpair(colors.green_hc,colors.green_off)}
crd_conn.register(databus.ps, "crd_conn", crd_conn.update)
@@ -138,27 +140,27 @@ local function init(panel)
local crd_fw_v = TextBox{parent=crd_box,x=26,y=2,text=" ------- ",width=9,fg_bg=label_fg}
crd_fw_v.register(databus.ps, "crd_fw", crd_fw_v.set_value)
TextBox{parent=crd_box,x=36,y=2,text="RTT:",width=4}
local crd_rtt = DataIndicator{parent=crd_box,x=41,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=crd_box,x=47,y=2,text="ms",width=4,fg_bg=label_fg}
TextBox{parent=crd_box,x=term_w-15,y=2,text="RTT:",width=4}
local crd_rtt = DataIndicator{parent=crd_box,x=term_w-10,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=crd_box,x=term_w-4,y=2,text="ms",width=4,fg_bg=label_fg}
crd_rtt.register(databus.ps, "crd_rtt", crd_rtt.update)
crd_rtt.register(databus.ps, "crd_rtt_color", crd_rtt.recolor)
-- pocket sessions page
local pkt_page = Div{parent=page_div,x=1,y=1,hidden=true}
local pdg_list = ListBox{parent=pkt_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local pkt_page = Div{parent=page_div,y=1,hidden=true}
local pdg_list = ListBox{parent=pkt_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=pdg_list,height=1} -- padding
-- RTU device ID check/diagnostics page
local chk_page = Div{parent=page_div,x=1,y=1,hidden=true}
local chk_list = ListBox{parent=chk_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local chk_page = Div{parent=page_div,y=1,hidden=true}
local chk_list = ListBox{parent=chk_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=chk_list,height=1} -- padding
-- info page
local info_page = Div{parent=page_div,x=1,y=1,hidden=true}
local info_page = Div{parent=page_div,y=1,hidden=true}
local info = Div{parent=info_page,height=6,x=2,y=2}
TextBox{parent=info,text="SVR \x1a Supervisor Status"}
@@ -168,7 +170,7 @@ local function init(panel)
TextBox{parent=info,text="PKT \x1a Pocket Connections"}
TextBox{parent=info,text="DEV \x1a RTU Device/Configuration Alerts"}
local notes = Div{parent=info_page,width=49,height=8,x=2,y=9,fg_bg=style.fp.disabled_fg}
local notes = Div{parent=info_page,width=term_w-2,height=8,x=2,y=9,fg_bg=style.fp.disabled_fg}
TextBox{parent=notes,text="The DEV tab will show missing devices and devices that connected with incorrect information. Missing entries will indicate how the configuration should be, duplicate entries will indicate what is a duplicate, and out-of-range entries will indicate the invalid entry. An out-of-range example is a #2 turbine when you should only have 1 turbine for that unit."}

View File

@@ -234,6 +234,23 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
if pkt.type == CRDN_TYPE.INITIAL_BUILDS then
-- acknowledgement to coordinator receiving builds
self.acks.builds = true
elseif pkt.type == CRDN_TYPE.PROCESS_READY then
if pkt.length == 5 then
-- coordinator has sent all initial process data, power-on recovery is now possible
---@type start_auto_config
local config = {
mode = pkt.data[1],
burn_target = pkt.data[2],
charge_target = pkt.data[3],
gen_target = pkt.data[4],
limits = pkt.data[5]
}
facility.boot_recovery_start(config)
else
log.debug(log_tag .. "CRDN process ready packet length mismatch")
end
elseif pkt.type == CRDN_TYPE.FAC_BUILDS then
-- acknowledgement to coordinator receiving builds
self.acks.fac_builds = true
@@ -243,8 +260,11 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
if cmd == FAC_COMMAND.SCRAM_ALL then
facility.scram_all()
facility.cancel_recovery()
_send(CRDN_TYPE.FAC_CMD, { cmd, true })
elseif cmd == FAC_COMMAND.STOP then
facility.cancel_recovery()
local was_active = facility.auto_is_active()
if was_active then
@@ -253,14 +273,16 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
_send(CRDN_TYPE.FAC_CMD, { cmd, was_active })
elseif cmd == FAC_COMMAND.START then
facility.cancel_recovery()
if pkt.length == 6 then
---@type sys_auto_config
---@class start_auto_config
local config = {
mode = pkt.data[2],
burn_target = pkt.data[3],
charge_target = pkt.data[4],
gen_target = pkt.data[5],
limits = pkt.data[6]
mode = pkt.data[2], ---@type PROCESS
burn_target = pkt.data[3], ---@type number
charge_target = pkt.data[4], ---@type number
gen_target = pkt.data[5], ---@type number
limits = pkt.data[6] ---@type number[]
}
_send(CRDN_TYPE.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) })
@@ -278,13 +300,13 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
end
elseif cmd == FAC_COMMAND.SET_PU_FB then
if pkt.length == 2 then
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_pu_fallback(pkt.data[2]) })
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.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
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_sps_low_power(pkt.data[2]) })
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_sps_low_power(pkt.data[2] == true) })
else
log.debug(log_tag .. "CRDN set sps low power packet length mismatch")
end
@@ -312,8 +334,11 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
local manual = facility.get_group(uid) == AUTO_GROUP.MANUAL
if cmd == UNIT_COMMAND.SCRAM then
facility.cancel_recovery()
out_queue.push_data(SV_Q_DATA.SCRAM, data)
elseif cmd == UNIT_COMMAND.START then
facility.cancel_recovery()
if manual then
out_queue.push_data(SV_Q_DATA.START, data)
else
@@ -323,6 +348,8 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
elseif cmd == UNIT_COMMAND.RESET_RPS then
out_queue.push_data(SV_Q_DATA.RESET_RPS, data)
elseif cmd == UNIT_COMMAND.SET_BURN then
facility.cancel_recovery()
if pkt.length == 3 then
if manual then
out_queue.push_data(SV_Q_DATA.SET_BURN, data)
@@ -353,6 +380,8 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
log.debug(log_tag .. "CRDN unit command reset alarm missing alarm id")
end
elseif cmd == UNIT_COMMAND.SET_GROUP then
facility.cancel_recovery()
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
facility.set_group(unit.get_id(), pkt.data[3])

View File

@@ -53,15 +53,15 @@ local PERIODICS = {
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
---@param initial_reset boolean[] initial PLC reset on timeout flags, indexed by reactor_id
---@param fp_ok boolean if the front panel UI is running
function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue, timeout, fp_ok)
function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue, timeout, initial_reset, fp_ok)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end
local log_tag = "plc_session(" .. id .. "): "
local self = {
commanded_state = false,
commanded_burn_rate = 0.0,
auto_cmd_token = 0,
ramping_rate = false,
@@ -72,6 +72,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
connected = true,
received_struct = false,
received_status_cache = false,
received_rps_status = false,
conn_watchdog = util.new_watchdog(timeout),
last_rtt = 0,
-- periodic messages
@@ -381,6 +382,16 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
local status = pcall(_copy_rps_status, pkt.data)
if status then
-- copied in RPS status data OK
self.received_rps_status = true
-- try initial reset if needed
if initial_reset[reactor_id] then
initial_reset[reactor_id] = false
if self.sDB.rps_trip_cause == "timeout" then
_send(RPLC_TYPE.RPS_AUTO_RESET, {})
log.debug(log_tag .. "initial RPS reset on timeout status sent")
end
end
else
-- error copying RPS status data
log.error(log_tag .. "failed to parse RPS status packet data")
@@ -394,6 +405,16 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
local status = pcall(_copy_rps_status, { true, table.unpack(pkt.data) })
if status then
-- copied in RPS status data OK
self.received_rps_status = true
-- try initial reset if needed
if initial_reset[reactor_id] then
initial_reset[reactor_id] = false
if self.sDB.rps_trip_cause == "timeout" then
_send(RPLC_TYPE.RPS_AUTO_RESET, {})
log.debug(log_tag .. "initial RPS reset on timeout alarm sent")
end
end
else
-- error copying RPS status data
log.error(log_tag .. "failed to parse RPS alarm status data")
@@ -487,6 +508,10 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
---@nodiscard
function public.get_db() return self.sDB end
-- check if the reactor structure, status, and RPS status have been received
---@nodiscard
function public.check_received_all_data() return self.received_struct and self.received_status_cache and self.received_rps_status end
-- check if ramping is completed by first verifying auto command token ack
---@nodiscard
function public.is_ramp_complete()

View File

@@ -9,7 +9,8 @@ local rsctl = {}
-- create a new redstone RTU I/O controller
---@nodiscard
---@param redstone_rtus redstone_session[] redstone RTU sessions
function rsctl.new(redstone_rtus)
---@param bank integer I/O bank (unit/facility assignment) to interface with
function rsctl.new(redstone_rtus, bank)
---@class rs_controller
local public = {}
@@ -18,7 +19,7 @@ function rsctl.new(redstone_rtus)
---@return boolean
function public.is_connected(port)
for i = 1, #redstone_rtus do
if redstone_rtus[i].get_db().io[port] ~= nil then return true end
if redstone_rtus[i].get_db().io[bank][port] ~= nil then return true end
end
return false
@@ -29,7 +30,7 @@ function rsctl.new(redstone_rtus)
---@param value boolean
function public.digital_write(port, value)
for i = 1, #redstone_rtus do
local io = redstone_rtus[i].get_db().io[port]
local io = redstone_rtus[i].get_db().io[bank][port]
if io ~= nil then io.write(value) end
end
end
@@ -40,7 +41,7 @@ function rsctl.new(redstone_rtus)
---@return boolean|nil
function public.digital_read(port)
for i = 1, #redstone_rtus do
local io = redstone_rtus[i].get_db().io[port]
local io = redstone_rtus[i].get_db().io[bank][port]
if io ~= nil then return io.read() --[[@as boolean|nil]] end
end
end
@@ -52,7 +53,7 @@ function rsctl.new(redstone_rtus)
---@param max number maximum value for scaling 0 to 15
function public.analog_write(port, value, min, max)
for i = 1, #redstone_rtus do
local io = redstone_rtus[i].get_db().io[port]
local io = redstone_rtus[i].get_db().io[bank][port]
if io ~= nil then io.write(rsio.analog_write(value, min, max)) end
end
end

View File

@@ -93,7 +93,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
type = self.advert[i][1],
index = self.advert[i][2],
reactor = self.advert[i][3],
rsio = self.advert[i][4]
rs_conns = self.advert[i][4]
}
local u_type = unit_advert.type ---@type RTU_UNIT_TYPE|boolean
@@ -104,14 +104,17 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
advert_validator.assert(util.is_int(unit_advert.index) or (unit_advert.index == false))
advert_validator.assert_type_int(unit_advert.reactor)
if u_type == RTU_UNIT_TYPE.REDSTONE then
advert_validator.assert_type_table(unit_advert.rsio)
end
if advert_validator.valid() then
if util.is_int(unit_advert.index) then advert_validator.assert_min(unit_advert.index, 1) end
advert_validator.assert_min(unit_advert.reactor, 0)
advert_validator.assert_max(unit_advert.reactor, #self.fac_units)
if (unit_advert.reactor == -1) or (u_type == RTU_UNIT_TYPE.REDSTONE) then
advert_validator.assert((unit_advert.reactor == -1) and (u_type == RTU_UNIT_TYPE.REDSTONE))
advert_validator.assert_type_table(unit_advert.rs_conns)
else
advert_validator.assert_min(unit_advert.reactor, 0)
advert_validator.assert_max(unit_advert.reactor, #self.fac_units)
end
if not advert_validator.valid() then u_type = false end
else
u_type = false
@@ -126,15 +129,34 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
-- validation fail
log.debug(log_tag .. "_handle_advertisement(): advertisement unit validation failure")
else
if unit_advert.reactor > 0 then
local target_unit = self.fac_units[unit_advert.reactor]
-- unit RTUs
if unit_advert.reactor == -1 then
-- redstone RTUs can be used in multiple different assignments
if u_type == RTU_UNIT_TYPE.REDSTONE then
-- redstone
unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_redstone(unit) end
elseif u_type == RTU_UNIT_TYPE.BOILER_VALVE then
-- link this to any subsystems this RTU provides connections for
if type(unit) ~= "nil" then
for assignment, conns in pairs(unit_advert.rs_conns) do
if #conns > 0 then
if assignment == 0 then
facility.add_redstone(unit)
elseif assignment > 0 and assignment <= #self.fac_units then
self.fac_units[assignment].add_redstone(unit)
else
log.warning(util.c(log_tag, "_handle_advertisement(): invalid redstone RTU assignment ", assignment))
end
end
end
end
else
log.warning(util.c(log_tag, "_handle_advertisement(): encountered unsupported multi-assignment RTU type ", type_string))
end
elseif unit_advert.reactor > 0 then
local target_unit = self.fac_units[unit_advert.reactor]
-- unit RTUs
if u_type == RTU_UNIT_TYPE.BOILER_VALVE then
-- boiler
unit = svrs_boilerv.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_boiler(unit) end

View File

@@ -105,27 +105,39 @@ function boilerv.new(session_id, unit_id, advert, out_queue)
-- PRIVATE FUNCTIONS --
-- query if the multiblock is formed
local function _request_formed()
---@param time_now integer
local function _request_formed(time_now)
-- read discrete input 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 })
if self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 }) ~= false then
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
end
-- query the build of the device
local function _request_build()
---@param time_now integer
local function _request_build(time_now)
-- read input registers 1 through 12 (start = 1, count = 12)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 12 })
if self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 12 }) ~= false then
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
end
-- query the state of the device
local function _request_state()
---@param time_now integer
local function _request_state(time_now)
-- read input registers 13 through 15 (start = 13, count = 3)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 13, 3 })
if self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 13, 3 }) ~= false then
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
end
-- query the tanks of the device
local function _request_tanks()
---@param time_now integer
local function _request_tanks(time_now)
-- read input registers 16 through 27 (start = 16, count = 12)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 16, 12 })
if self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 16, 12 }) ~= false then
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
end
-- PUBLIC FUNCTIONS --
@@ -210,26 +222,12 @@ function boilerv.new(session_id, unit_id, advert, out_queue)
-- update this runner
---@param time_now integer milliseconds
function public.update(time_now)
if self.periodics.next_formed_req <= time_now then
_request_formed()
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
if self.periodics.next_formed_req <= time_now then _request_formed(time_now) end
if self.db.formed then
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
if self.periodics.next_state_req <= time_now then
_request_state()
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
if self.periodics.next_tanks_req <= time_now then
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
if not self.has_build and self.periodics.next_build_req <= time_now then _request_build(time_now) end
if self.periodics.next_state_req <= time_now then _request_state(time_now) end
if self.periodics.next_tanks_req <= time_now then _request_tanks(time_now) end
end
self.session.post_update()

View File

@@ -42,6 +42,8 @@ local PERIODICS = {
TANKS = 500
}
local WRITE_BUSY_WAIT = 1000
-- create a new dynamicv rtu session runner
---@nodiscard
---@param session_id integer RTU gateway session ID
@@ -63,6 +65,8 @@ function dynamicv.new(session_id, unit_id, advert, out_queue)
local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
has_build = false,
mode_cmd = nil, ---@type container_mode|nil
resend_mode = false,
periodics = {
next_formed_req = 0,
next_build_req = 0,
@@ -101,45 +105,77 @@ function dynamicv.new(session_id, unit_id, advert, out_queue)
-- increment the container mode
local function _inc_cont_mode()
-- set mode command
if self.mode_cmd == "BOTH" then self.mode_cmd = "FILL"
elseif self.mode_cmd == "FILL" then self.mode_cmd = "EMPTY"
elseif self.mode_cmd == "EMPTY" then self.mode_cmd = "BOTH"
end
-- write coil 1 with unused value 0
self.session.send_request(TXN_TYPES.INC_CONT, MODBUS_FCODE.WRITE_SINGLE_COIL, { 1, 0 })
if self.session.send_request(TXN_TYPES.INC_CONT, MODBUS_FCODE.WRITE_SINGLE_COIL, { 1, 0 }, WRITE_BUSY_WAIT) == false then
self.resend_mode = true
end
end
-- decrement the container mode
local function _dec_cont_mode()
-- set mode command
if self.mode_cmd == "BOTH" then self.mode_cmd = "EMPTY"
elseif self.mode_cmd == "EMPTY" then self.mode_cmd = "FILL"
elseif self.mode_cmd == "FILL" then self.mode_cmd = "BOTH"
end
-- write coil 2 with unused value 0
self.session.send_request(TXN_TYPES.DEC_CONT, MODBUS_FCODE.WRITE_SINGLE_COIL, { 2, 0 })
if self.session.send_request(TXN_TYPES.DEC_CONT, MODBUS_FCODE.WRITE_SINGLE_COIL, { 2, 0 , WRITE_BUSY_WAIT}) == false then
self.resend_mode = false
end
end
-- set the container mode
---@param mode container_mode
local function _set_cont_mode(mode)
self.mode_cmd = mode
-- write holding register 1
self.session.send_request(TXN_TYPES.SET_CONT, MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, { 1, mode })
if self.session.send_request(TXN_TYPES.SET_CONT, MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, { 1, mode }, WRITE_BUSY_WAIT) == false then
self.resend_mode = false
end
end
-- query if the multiblock is formed
local function _request_formed()
---@param time_now integer
local function _request_formed(time_now)
-- read discrete input 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 })
if self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 }) ~= false then
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
end
-- query the build of the device
local function _request_build()
---@param time_now integer
local function _request_build(time_now)
-- read input registers 1 through 7 (start = 1, count = 7)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 7 })
if self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 7 }) ~= false then
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
end
-- query the state of the device
local function _request_state()
---@param time_now integer
local function _request_state(time_now)
-- read holding register 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, 1 })
if self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, 1 }) ~= false then
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
end
-- query the tanks of the device
local function _request_tanks()
---@param time_now integer
local function _request_tanks(time_now)
-- read input registers 8 through 9 (start = 8, count = 2)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 8, 2 })
if self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 8, 2 }) ~= false then
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
end
-- PUBLIC FUNCTIONS --
@@ -182,6 +218,10 @@ function dynamicv.new(session_id, unit_id, advert, out_queue)
if m_pkt.length == 1 then
self.db.state.last_update = util.time_ms()
self.db.state.container_mode = m_pkt.data[1]
if self.mode_cmd == nil then
self.mode_cmd = self.db.state.container_mode
end
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
@@ -247,30 +287,22 @@ function dynamicv.new(session_id, unit_id, advert, out_queue)
end
end
-- try to resend mode if needed
if self.resend_mode then
self.resend_mode = false
_set_cont_mode(self.mode_cmd)
end
time_now = util.time()
-- handle periodics
if self.periodics.next_formed_req <= time_now then
_request_formed()
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
if self.periodics.next_formed_req <= time_now then _request_formed(time_now) end
if self.db.formed then
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
if self.periodics.next_state_req <= time_now then
_request_state()
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
if self.periodics.next_tanks_req <= time_now then
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
if not self.has_build and self.periodics.next_build_req <= time_now then _request_build(time_now) end
if self.periodics.next_state_req <= time_now then _request_state(time_now) end
if self.periodics.next_tanks_req <= time_now then _request_tanks(time_now) end
end
self.session.post_update()

View File

@@ -58,9 +58,12 @@ function envd.new(session_id, unit_id, advert, out_queue)
-- PRIVATE FUNCTIONS --
-- query the radiation readings of the device
local function _request_radiation()
---@param time_now integer
local function _request_radiation(time_now)
-- read input registers 1 and 2 (start = 1, count = 2)
self.session.send_request(TXN_TYPES.RAD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 2 })
if self.session.send_request(TXN_TYPES.RAD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 2 }) ~= false then
self.periodics.next_rad_req = time_now + PERIODICS.RAD
end
end
-- PUBLIC FUNCTIONS --
@@ -90,10 +93,7 @@ function envd.new(session_id, unit_id, advert, out_queue)
-- update this runner
---@param time_now integer milliseconds
function public.update(time_now)
if self.periodics.next_rad_req <= time_now then
_request_radiation()
self.periodics.next_rad_req = time_now + PERIODICS.RAD
end
if self.periodics.next_rad_req <= time_now then _request_radiation(time_now) end
self.session.post_update()
end

View File

@@ -89,27 +89,39 @@ function imatrix.new(session_id, unit_id, advert, out_queue)
-- PRIVATE FUNCTIONS --
-- query if the multiblock is formed
local function _request_formed()
---@param time_now integer
local function _request_formed(time_now)
-- read discrete input 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 })
if self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 }) ~= false then
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
end
-- query the build of the device
local function _request_build()
---@param time_now integer
local function _request_build(time_now)
-- read input registers 1 through 9 (start = 1, count = 9)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 9 })
if self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 9 }) ~= false then
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
end
-- query the state of the device
local function _request_state()
---@param time_now integer
local function _request_state(time_now)
-- read input register 10 through 11 (start = 10, count = 2)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 10, 2 })
if self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 10, 2 }) ~= false then
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
end
-- query the tanks of the device
local function _request_tanks()
---@param time_now integer
local function _request_tanks(time_now)
-- read input registers 12 through 15 (start = 12, count = 3)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 12, 3 })
if self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 12, 3 }) ~= false then
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
end
-- PUBLIC FUNCTIONS --
@@ -181,26 +193,12 @@ function imatrix.new(session_id, unit_id, advert, out_queue)
-- update this runner
---@param time_now integer milliseconds
function public.update(time_now)
if self.periodics.next_formed_req <= time_now then
_request_formed()
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
if self.periodics.next_formed_req <= time_now then _request_formed(time_now) end
if self.db.formed then
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
if self.periodics.next_state_req <= time_now then
_request_state()
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
if self.periodics.next_tanks_req <= time_now then
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
if not self.has_build and self.periodics.next_build_req <= time_now then _request_build(time_now) end
if self.periodics.next_state_req <= time_now then _request_state(time_now) end
if self.periodics.next_tanks_req <= time_now then _request_tanks(time_now) end
end
self.session.post_update()

View File

@@ -10,7 +10,6 @@ local redstone = {}
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local MODBUS_FCODE = types.MODBUS_FCODE
local IO_PORT = rsio.IO
local IO_LVL = rsio.IO_LVL
local IO_MODE = rsio.IO_MODE
@@ -39,6 +38,9 @@ local PERIODICS = {
OUTPUT_SYNC = 200
}
-- create a new block of IO banks (facility, then each unit)
local function new_io_block() return { [0] = {}, {}, {}, {}, {} } end
---@class dig_phy_entry
---@field phy IO_LVL actual value
---@field req IO_LVL commanded value
@@ -74,27 +76,27 @@ function redstone.new(session_id, unit_id, advert, out_queue)
next_ir_req = 0,
next_hr_sync = 0
},
---@class rs_io_list
io_list = {
digital_in = {}, ---@type IO_PORT[] discrete inputs
digital_out = {}, ---@type IO_PORT[] coils
analog_in = {}, ---@type IO_PORT[] input registers
analog_out = {} ---@type IO_PORT[] holding registers
---@class rs_io_map
io_map = {
digital_in = {}, ---@type { bank: integer, port: IO_PORT }[] discrete inputs
digital_out = {}, ---@type { bank: integer, port: IO_PORT }[] coils
analog_in = {}, ---@type { bank: integer, port: IO_PORT }[] input registers
analog_out = {} ---@type { bank: integer, port: IO_PORT }[] holding registers
},
phy_trans = { coils = -1, hold_regs = -1 },
-- last set/read ports (reflecting the current state of the RTU)
---@class rs_io_states
phy_io = {
digital_in = {}, ---@type dig_phy_entry[] discrete inputs
digital_out = {}, ---@type dig_phy_entry[] coils
analog_in = {}, ---@type ana_phy_entry[] input registers
analog_out = {} ---@type ana_phy_entry[] holding registers
digital_in = new_io_block(), ---@type dig_phy_entry[][] discrete inputs
digital_out = new_io_block(), ---@type dig_phy_entry[][] coils
analog_in = new_io_block(), ---@type ana_phy_entry[][] input registers
analog_out = new_io_block() ---@type ana_phy_entry[][] holding registers
},
---@class redstone_session_db
db = {
-- read/write functions for connected I/O
---@type (rs_db_dig_io|rs_db_ana_io)[]
io = {}
---@type (rs_db_dig_io|rs_db_ana_io)[][]
io = new_io_block()
}
}
@@ -103,93 +105,91 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- INITIALIZE --
-- create all ports as disconnected
for _ = 1, #IO_PORT do
table.insert(self.db, IO_LVL.DISCONNECT)
end
-- setup I/O
for i = 1, #advert.rsio do
local port = advert.rsio[i]
for bank = 0, 4 do
for i = 1, #advert.rs_conns[bank] do
local port = advert.rs_conns[bank][i]
if rsio.is_valid_port(port) then
local mode = rsio.get_io_mode(port)
if rsio.is_valid_port(port) then
local mode = rsio.get_io_mode(port)
local io_entry = { bank = bank, port = port }
if mode == IO_MODE.DIGITAL_IN then
self.has_di = true
table.insert(self.io_list.digital_in, port)
if mode == IO_MODE.DIGITAL_IN then
self.has_di = true
table.insert(self.io_map.digital_in, io_entry)
self.phy_io.digital_in[port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING }
self.phy_io.digital_in[bank][port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING }
---@class rs_db_dig_io
local io_f = {
---@nodiscard
read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[port].phy) end,
write = function () end
}
---@class rs_db_dig_io
local io_f = {
---@nodiscard
read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[bank][port].phy) end,
write = function () end
}
self.db.io[port] = io_f
elseif mode == IO_MODE.DIGITAL_OUT then
self.has_do = true
table.insert(self.io_list.digital_out, port)
self.db.io[bank][port] = io_f
elseif mode == IO_MODE.DIGITAL_OUT then
self.has_do = true
table.insert(self.io_map.digital_out, io_entry)
self.phy_io.digital_out[port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING }
self.phy_io.digital_out[bank][port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING }
---@class rs_db_dig_io
local io_f = {
---@nodiscard
read = function () return rsio.digital_is_active(port, self.phy_io.digital_out[port].phy) end,
---@param active boolean
write = function (active)
local level = rsio.digital_write_active(port, active)
if level ~= nil then self.phy_io.digital_out[port].req = level end
end
}
self.db.io[port] = io_f
elseif mode == IO_MODE.ANALOG_IN then
self.has_ai = true
table.insert(self.io_list.analog_in, port)
self.phy_io.analog_in[port] = { phy = 0, req = 0 }
---@class rs_db_ana_io
local io_f = {
---@nodiscard
---@return integer
read = function () return self.phy_io.analog_in[port].phy end,
write = function () end
}
self.db.io[port] = io_f
elseif mode == IO_MODE.ANALOG_OUT then
self.has_ao = true
table.insert(self.io_list.analog_out, port)
self.phy_io.analog_out[port] = { phy = 0, req = 0 }
---@class rs_db_ana_io
local io_f = {
---@nodiscard
---@return integer
read = function () return self.phy_io.analog_out[port].phy end,
---@param value integer
write = function (value)
if value >= 0 and value <= 15 then
self.phy_io.analog_out[port].req = value
---@class rs_db_dig_io
local io_f = {
---@nodiscard
read = function () return rsio.digital_is_active(port, self.phy_io.digital_out[bank][port].phy) end,
---@param active boolean
write = function (active)
local level = rsio.digital_write_active(port, active)
if level ~= nil then self.phy_io.digital_out[bank][port].req = level end
end
end
}
}
self.db.io[port] = io_f
self.db.io[bank][port] = io_f
elseif mode == IO_MODE.ANALOG_IN then
self.has_ai = true
table.insert(self.io_map.analog_in, io_entry)
self.phy_io.analog_in[bank][port] = { phy = 0, req = 0 }
---@class rs_db_ana_io
local io_f = {
---@nodiscard
---@return integer
read = function () return self.phy_io.analog_in[bank][port].phy end,
write = function () end
}
self.db.io[bank][port] = io_f
elseif mode == IO_MODE.ANALOG_OUT then
self.has_ao = true
table.insert(self.io_map.analog_out, io_entry)
self.phy_io.analog_out[bank][port] = { phy = 0, req = 0 }
---@class rs_db_ana_io
local io_f = {
---@nodiscard
---@return integer
read = function () return self.phy_io.analog_out[bank][port].phy end,
---@param value integer
write = function (value)
if value >= 0 and value <= 15 then
self.phy_io.analog_out[bank][port].req = value
end
end
}
self.db.io[bank][port] = io_f
else
-- should be unreachable code, we already validated ports
log.error(util.c(log_tag, "failed to identify advertisement port IO mode (", bank, ":", port, ")"), true)
return nil
end
else
-- should be unreachable code, we already validated ports
log.error(util.c(log_tag, "failed to identify advertisement port IO mode (", port, ")"), true)
log.error(util.c(log_tag, "invalid advertisement port (", bank, ":", port, ")"), true)
return nil
end
else
log.error(util.c(log_tag, "invalid advertisement port (", port, ")"), true)
return nil
end
end
@@ -197,12 +197,12 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- query discrete inputs
local function _request_discrete_inputs()
self.session.send_request(TXN_TYPES.DI_READ, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, #self.io_list.digital_in })
self.session.send_request(TXN_TYPES.DI_READ, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, #self.io_map.digital_in })
end
-- query input registers
local function _request_input_registers()
self.session.send_request(TXN_TYPES.INPUT_REG_READ, MODBUS_FCODE.READ_INPUT_REGS, { 1, #self.io_list.analog_in })
self.session.send_request(TXN_TYPES.INPUT_REG_READ, MODBUS_FCODE.READ_INPUT_REGS, { 1, #self.io_map.analog_in })
end
-- write all coil outputs
@@ -210,9 +210,9 @@ function redstone.new(session_id, unit_id, advert, out_queue)
local params = { 1 }
local outputs = self.phy_io.digital_out
for i = 1, #self.io_list.digital_out do
local port = self.io_list.digital_out[i]
table.insert(params, outputs[port].req)
for i = 1, #self.io_map.digital_out do
local entry = self.io_map.digital_out[i]
table.insert(params, outputs[entry.bank][entry.port].req)
end
self.phy_trans.coils = self.session.send_request(TXN_TYPES.COIL_WRITE, MODBUS_FCODE.WRITE_MUL_COILS, params)
@@ -220,7 +220,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- read all coil outputs
local function _read_coils()
self.session.send_request(TXN_TYPES.COIL_READ, MODBUS_FCODE.READ_COILS, { 1, #self.io_list.digital_out })
self.session.send_request(TXN_TYPES.COIL_READ, MODBUS_FCODE.READ_COILS, { 1, #self.io_map.digital_out })
end
-- write all holding register outputs
@@ -228,9 +228,9 @@ function redstone.new(session_id, unit_id, advert, out_queue)
local params = { 1 }
local outputs = self.phy_io.analog_out
for i = 1, #self.io_list.analog_out do
local port = self.io_list.analog_out[i]
table.insert(params, outputs[port].req)
for i = 1, #self.io_map.analog_out do
local entry = self.io_map.analog_out[i]
table.insert(params, outputs[entry.bank][entry.port].req)
end
self.phy_trans.hold_regs = self.session.send_request(TXN_TYPES.HOLD_REG_WRITE, MODBUS_FCODE.WRITE_MUL_HOLD_REGS, params)
@@ -238,7 +238,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- read all holding register outputs
local function _read_holding_registers()
self.session.send_request(TXN_TYPES.HOLD_REG_READ, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, #self.io_list.analog_out })
self.session.send_request(TXN_TYPES.HOLD_REG_READ, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, #self.io_map.analog_out })
end
-- PUBLIC FUNCTIONS --
@@ -259,24 +259,24 @@ function redstone.new(session_id, unit_id, advert, out_queue)
end
elseif txn_type == TXN_TYPES.DI_READ then
-- discrete input read response
if m_pkt.length == #self.io_list.digital_in then
if m_pkt.length == #self.io_map.digital_in then
for i = 1, m_pkt.length do
local port = self.io_list.digital_in[i]
local entry = self.io_map.digital_in[i]
local value = m_pkt.data[i]
self.phy_io.digital_in[port].phy = value
self.phy_io.digital_in[entry.bank][entry.port].phy = value
end
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.INPUT_REG_READ then
-- input register read response
if m_pkt.length == #self.io_list.analog_in then
if m_pkt.length == #self.io_map.analog_in then
for i = 1, m_pkt.length do
local port = self.io_list.analog_in[i]
local entry = self.io_map.analog_in[i]
local value = m_pkt.data[i]
self.phy_io.analog_in[port].phy = value
self.phy_io.analog_in[entry.bank][entry.port].phy = value
end
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
@@ -288,15 +288,14 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- update phy I/O table
-- if there are multiple outputs for the same port, they will overwrite eachother (but *should* be identical)
-- given these are redstone outputs, if one worked they all should have, so no additional verification will be done
if m_pkt.length == #self.io_list.digital_out then
if m_pkt.length == #self.io_map.digital_out then
for i = 1, m_pkt.length do
local port = self.io_list.digital_out[i]
local entry = self.io_map.digital_out[i]
local state = self.phy_io.digital_out[entry.bank][entry.port]
local value = m_pkt.data[i]
self.phy_io.digital_out[port].phy = value
if self.phy_io.digital_out[port].req == IO_LVL.FLOATING then
self.phy_io.digital_out[port].req = value
end
state.phy = value
if state.req == IO_LVL.FLOATING then state.req = value end
end
self.phy_trans.coils = TXN_READY
@@ -310,12 +309,12 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- update phy I/O table
-- if there are multiple outputs for the same port, they will overwrite eachother (but *should* be identical)
-- given these are redstone outputs, if one worked they all should have, so no additional verification will be done
if m_pkt.length == #self.io_list.analog_out then
if m_pkt.length == #self.io_map.analog_out then
for i = 1, m_pkt.length do
local port = self.io_list.analog_out[i]
local entry = self.io_map.analog_out[i]
local value = m_pkt.data[i]
self.phy_io.analog_out[port].phy = value
self.phy_io.analog_out[entry.bank][entry.port].phy = value
end
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
@@ -343,8 +342,17 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- sync digital outputs
if self.has_do then
if (self.periodics.next_cl_sync <= time_now) and (self.phy_trans.coils == TXN_READY) then
for _, entry in pairs(self.phy_io.digital_out) do
if entry.phy ~= entry.req then
for bank = 0, 4 do
local changed = false
for _, entry in pairs(self.phy_io.digital_out[bank]) do
if entry.phy ~= entry.req then
changed = true
break
end
end
if changed then
_write_coils()
break
end
@@ -365,8 +373,17 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- sync analog outputs
if self.has_ao then
if (self.periodics.next_hr_sync <= time_now) and (self.phy_trans.hold_regs == TXN_READY) then
for _, entry in pairs(self.phy_io.analog_out) do
if entry.phy ~= entry.req then
for bank = 0, 4 do
local changed = false
for _, entry in pairs(self.phy_io.analog_out[bank]) do
if entry.phy ~= entry.req then
changed = true
break
end
end
if changed then
_write_holding_registers()
break
end
@@ -379,9 +396,10 @@ function redstone.new(session_id, unit_id, advert, out_queue)
self.session.post_update()
end
-- invalidate build cache
-- force a re-read of cached outputs
function public.invalidate_cache()
-- no build cache for this device
_read_coils()
_read_holding_registers()
end
-- get the unit session database

View File

@@ -80,21 +80,30 @@ function sna.new(session_id, unit_id, advert, out_queue)
-- PRIVATE FUNCTIONS --
-- query the build of the device
local function _request_build()
---@param time_now integer
local function _request_build(time_now)
-- read input registers 1 through 2 (start = 1, count = 2)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 2 })
if self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 2 }) ~= false then
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
end
-- query the state of the device
local function _request_state()
---@param time_now integer
local function _request_state(time_now)
-- read input registers 3 through 4 (start = 3, count = 2)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 3, 2 })
if self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 3, 2 }) ~= false then
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
end
-- query the tanks of the device
local function _request_tanks()
---@param time_now integer
local function _request_tanks(time_now)
-- read input registers 5 through 10 (start = 5, count = 6)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 5, 6 })
if self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 5, 6 }) ~= false then
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
end
-- PUBLIC FUNCTIONS --
@@ -152,20 +161,9 @@ function sna.new(session_id, unit_id, advert, out_queue)
-- update this runner
---@param time_now integer milliseconds
function public.update(time_now)
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
if self.periodics.next_state_req <= time_now then
_request_state()
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
if self.periodics.next_tanks_req <= time_now then
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
if not self.has_build and self.periodics.next_build_req <= time_now then _request_build(time_now) end
if self.periodics.next_state_req <= time_now then _request_state(time_now) end
if self.periodics.next_tanks_req <= time_now then _request_tanks(time_now) end
self.session.post_update()
end

View File

@@ -94,27 +94,39 @@ function sps.new(session_id, unit_id, advert, out_queue)
-- PRIVATE FUNCTIONS --
-- query if the multiblock is formed
local function _request_formed()
---@param time_now integer
local function _request_formed(time_now)
-- read discrete input 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 })
if self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 }) ~= false then
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
end
-- query the build of the device
local function _request_build()
---@param time_now integer
local function _request_build(time_now)
-- read input registers 1 through 9 (start = 1, count = 9)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 9 })
if self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 9 }) ~= false then
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
end
-- query the state of the device
local function _request_state()
---@param time_now integer
local function _request_state(time_now)
-- read input register 10 (start = 10, count = 1)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 10, 1 })
if self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 10, 1 }) ~= false then
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
end
-- query the tanks of the device
local function _request_tanks()
---@param time_now integer
local function _request_tanks(time_now)
-- read input registers 11 through 19 (start = 11, count = 9)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 11, 9 })
if self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 11, 9 }) ~= false then
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
end
-- PUBLIC FUNCTIONS --
@@ -191,26 +203,12 @@ function sps.new(session_id, unit_id, advert, out_queue)
-- update this runner
---@param time_now integer milliseconds
function public.update(time_now)
if self.periodics.next_formed_req <= time_now then
_request_formed()
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
if self.periodics.next_formed_req <= time_now then _request_formed(time_now) end
if self.db.formed then
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
if self.periodics.next_state_req <= time_now then
_request_state()
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
if self.periodics.next_tanks_req <= time_now then
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
if not self.has_build and self.periodics.next_build_req <= time_now then _request_build(time_now) end
if self.periodics.next_state_req <= time_now then _request_state(time_now) end
if self.periodics.next_tanks_req <= time_now then _request_tanks(time_now) end
end
self.session.post_update()

View File

@@ -42,6 +42,8 @@ local PERIODICS = {
TANKS = 1000
}
local WRITE_BUSY_WAIT = 1000
-- create a new turbinev rtu session runner
---@nodiscard
---@param session_id integer RTU gateway session ID
@@ -63,6 +65,8 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
has_build = false,
mode_cmd = nil, ---@type dumping_mode|nil
resend_mode = false,
periodics = {
next_formed_req = 0,
next_build_req = 0,
@@ -116,45 +120,77 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
-- increment the dumping mode
local function _inc_dump_mode()
-- set mode command
if self.mode_cmd == "IDLE" then self.mode_cmd = "DUMPING_EXCESS"
elseif self.mode_cmd == "DUMPING_EXCESS" then self.mode_cmd = "DUMPING"
elseif self.mode_cmd == "DUMPING" then self.mode_cmd = "IDLE"
end
-- write coil 1 with unused value 0
self.session.send_request(TXN_TYPES.INC_DUMP, MODBUS_FCODE.WRITE_SINGLE_COIL, { 1, 0 })
if self.session.send_request(TXN_TYPES.INC_DUMP, MODBUS_FCODE.WRITE_SINGLE_COIL, { 1, 0 }, WRITE_BUSY_WAIT) == false then
self.resend_mode = true
end
end
-- decrement the dumping mode
local function _dec_dump_mode()
-- set mode command
if self.mode_cmd == "IDLE" then self.mode_cmd = "DUMPING"
elseif self.mode_cmd == "DUMPING_EXCESS" then self.mode_cmd = "IDLE"
elseif self.mode_cmd == "DUMPING" then self.mode_cmd = "DUMPING_EXCESS"
end
-- write coil 2 with unused value 0
self.session.send_request(TXN_TYPES.DEC_DUMP, MODBUS_FCODE.WRITE_SINGLE_COIL, { 2, 0 })
if self.session.send_request(TXN_TYPES.DEC_DUMP, MODBUS_FCODE.WRITE_SINGLE_COIL, { 2, 0 }, WRITE_BUSY_WAIT) == false then
self.resend_mode = true
end
end
-- set the dumping mode
---@param mode dumping_mode
local function _set_dump_mode(mode)
self.mode_cmd = mode
-- write holding register 1
self.session.send_request(TXN_TYPES.SET_DUMP, MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, { 1, mode })
if self.session.send_request(TXN_TYPES.SET_DUMP, MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, { 1, mode }, WRITE_BUSY_WAIT) == false then
self.resend_mode = true
end
end
-- query if the multiblock is formed
local function _request_formed()
---@param time_now integer
local function _request_formed(time_now)
-- read discrete input 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 })
if self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 }) ~= false then
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
end
-- query the build of the device
local function _request_build()
---@param time_now integer
local function _request_build(time_now)
-- read input registers 1 through 15 (start = 1, count = 15)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 15 })
if self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 15 }) ~= false then
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
end
-- query the state of the device
local function _request_state()
---@param time_now integer
local function _request_state(time_now)
-- read input registers 16 through 19 (start = 16, count = 4)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 16, 4 })
if self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 16, 4 }) ~= false then
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
end
-- query the tanks of the device
local function _request_tanks()
---@param time_now integer
local function _request_tanks(time_now)
-- read input registers 20 through 25 (start = 20, count = 6)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 20, 6 })
if self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 20, 6 }) ~= false then
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
end
-- PUBLIC FUNCTIONS --
@@ -208,6 +244,10 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
self.db.state.prod_rate = m_pkt.data[2]
self.db.state.steam_input_rate = m_pkt.data[3]
self.db.state.dumping_mode = m_pkt.data[4]
if self.mode_cmd == nil then
self.mode_cmd = self.db.state.dumping_mode
end
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
@@ -277,30 +317,22 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
end
end
-- try to resend mode if needed
if self.resend_mode then
self.resend_mode = false
_set_dump_mode(self.mode_cmd)
end
time_now = util.time()
-- handle periodics
if self.periodics.next_formed_req <= time_now then
_request_formed()
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
if self.periodics.next_formed_req <= time_now then _request_formed(time_now) end
if self.db.formed then
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
if self.periodics.next_state_req <= time_now then
_request_state()
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
if self.periodics.next_tanks_req <= time_now then
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
if not self.has_build and self.periodics.next_build_req <= time_now then _request_build(time_now) end
if self.periodics.next_state_req <= time_now then _request_state(time_now) end
if self.periodics.next_tanks_req <= time_now then _request_tanks(time_now) end
end
self.session.post_update()

View File

@@ -22,6 +22,8 @@ local RTU_US_DATA = {
unit_session.RTU_US_CMDS = RTU_US_CMDS
unit_session.RTU_US_DATA = RTU_US_DATA
local DEFAULT_BUSY_WAIT = 3000
-- create a new unit session runner
---@nodiscard
---@param session_id integer RTU gateway session ID
@@ -36,7 +38,8 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
reactor = advert.reactor,
transaction_controller = txnctrl.new(),
connected = true,
device_fail = false
device_fail = false,
last_busy = 0
}
---@class _unit_session
@@ -53,14 +56,21 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
---@param txn_type integer transaction type
---@param f_code MODBUS_FCODE function code
---@param register_param (number|string)[] register range or register and values
---@return integer txn_id transaction ID of this transaction
function protected.send_request(txn_type, f_code, register_param)
local m_pkt = comms.modbus_packet()
local txn_id = self.transaction_controller.create(txn_type)
---@param busy_wait integer|nil milliseconds to wait (>0), or uses the default
---@return integer|false txn_id transaction ID of this transaction or false if not sent due to being busy
function protected.send_request(txn_type, f_code, register_param, busy_wait)
local txn_id = false ---@type integer|false
m_pkt.make(txn_id, unit_id, f_code, register_param)
busy_wait = busy_wait or DEFAULT_BUSY_WAIT
out_queue.push_packet(m_pkt)
if (util.time_ms() - self.last_busy) >= busy_wait then
local m_pkt = comms.modbus_packet()
txn_id = self.transaction_controller.create(txn_type)
m_pkt.make(txn_id, unit_id, f_code, register_param)
out_queue.push_packet(m_pkt)
end
return txn_id
end
@@ -99,9 +109,9 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
-- will have to wait on reply, renew the transaction
self.transaction_controller.renew(m_pkt.txn_id, txn_type)
elseif ex == MODBUS_EXCODE.SERVER_DEVICE_BUSY then
-- will have to wait on reply, renew the transaction
self.transaction_controller.renew(m_pkt.txn_id, txn_type)
log.debug(log_tag .. "MODBUS: device busy" .. txn_tag)
-- will have to try again later
self.last_busy = util.time_ms()
log.warning(log_tag .. "MODBUS: device busy" .. txn_tag)
elseif ex == MODBUS_EXCODE.NEG_ACKNOWLEDGE then
-- general failure
log.error(log_tag .. "MODBUS: negative acknowledge (bad request)" .. txn_tag)

View File

@@ -45,13 +45,16 @@ local self = {
fp_ok = false,
config = nil, ---@type svr_config
facility = nil, ---@type facility|nil
plc_ini_reset = {},
-- lists of connected sessions
---@diagnostic disable: missing-fields
sessions = {
rtu = {}, ---@type rtu_session_struct
plc = {}, ---@type plc_session_struct
crd = {}, ---@type crd_session_struct
pdg = {} ---@type pdg_session_struct
},
---@diagnostic enable: missing-fields
-- next session IDs
next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 },
-- rtu device tracking and invalid assignment detection
@@ -389,6 +392,7 @@ function svsessions.init(nic, fp_ok, config, facility)
conns.tanks[1] = true
end
self.plc_ini_reset[i] = true
self.dev_dbg.connected.units[i] = conns
end
end
@@ -484,7 +488,7 @@ function svsessions.establish_plc_session(source_addr, i_seq_num, for_reactor, v
local id = self.next_ids.plc
plc_s.instance = plc.new_session(id, source_addr, i_seq_num, for_reactor, plc_s.in_queue, plc_s.out_queue, self.config.PLC_Timeout, self.fp_ok)
plc_s.instance = plc.new_session(id, source_addr, i_seq_num, for_reactor, plc_s.in_queue, plc_s.out_queue, self.config.PLC_Timeout, self.plc_ini_reset, self.fp_ok)
table.insert(self.sessions.plc, plc_s)
local units = self.facility.get_units()

View File

@@ -10,6 +10,7 @@ local log = require("scada-common.log")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local types = require("scada-common.types")
local util = require("scada-common.util")
local core = require("graphics.core")
@@ -22,7 +23,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v1.5.10"
local SUPERVISOR_VERSION = "v1.7.0"
local println = util.println
local println_ts = util.println_ts
@@ -72,6 +73,21 @@ if config.FacilityTankMode > 0 then
cfv.assert_type_int(def)
cfv.assert_range(def, 0, 2)
assert(cfv.valid(), "startup> invalid facility tank definition for reactor unit " .. i)
local entry = config.FacilityTankList[i]
cfv.assert_type_int(entry)
cfv.assert_range(entry, 0, 2)
assert(cfv.valid(), "startup> invalid facility tank list entry for tank " .. i)
local conn = config.FacilityTankConns[i]
cfv.assert_type_int(conn)
cfv.assert_range(conn, 0, #config.FacilityTankDefs)
assert(cfv.valid(), "startup> invalid facility tank connection for reactor unit " .. i)
local type = config.TankFluidTypes[i]
cfv.assert_type_int(type)
cfv.assert_range(type, 0, types.COOLANT_TYPE.SODIUM)
assert(cfv.valid(), "startup> invalid tank fluid type for tank " .. i)
end
end
@@ -147,6 +163,9 @@ local function main()
-- halve the rate heartbeat LED flash
local heartbeat_toggle = true
-- init startup recovery
sv_facility.boot_recovery_init(supervisor.boot_state)
-- event loop
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
@@ -237,6 +256,8 @@ local function main()
end
end
sv_facility.clear_boot_state()
renderer.close_ui()
util.println_ts("exited")

View File

@@ -14,18 +14,37 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE
---@type svr_config
---@diagnostic disable-next-line: missing-fields
local config = {}
supervisor.config = config
-- load the supervisor configuration
-- control state from last unexpected shutdown
supervisor.boot_state = nil ---@type sv_boot_state|nil
-- load the supervisor configuration and startup state
function supervisor.load_config()
if not settings.load("/supervisor.settings") then return false end
---@class sv_boot_state
local boot_state = {
mode = settings.get("LastProcessState"), ---@type PROCESS
unit_states = settings.get("LastUnitStates") ---@type boolean[]
}
-- only record boot state if likely valid
if type(boot_state.mode) == "number" and type(boot_state.unit_states) == "table" then
supervisor.boot_state = boot_state
end
config.UnitCount = settings.get("UnitCount")
config.CoolingConfig = settings.get("CoolingConfig")
config.FacilityTankMode = settings.get("FacilityTankMode")
config.FacilityTankDefs = settings.get("FacilityTankDefs")
config.FacilityTankList = settings.get("FacilityTankList")
config.FacilityTankConns = settings.get("FacilityTankConns")
config.TankFluidTypes = settings.get("TankFluidTypes")
config.AuxiliaryCoolant = settings.get("AuxiliaryCoolant")
config.ExtChargeIdling = settings.get("ExtChargeIdling")
config.SVR_Channel = settings.get("SVR_Channel")
@@ -55,8 +74,12 @@ function supervisor.load_config()
cfv.assert_range(config.UnitCount, 1, 4)
cfv.assert_type_table(config.CoolingConfig)
cfv.assert_type_table(config.FacilityTankDefs)
cfv.assert_type_int(config.FacilityTankMode)
cfv.assert_type_table(config.FacilityTankDefs)
cfv.assert_type_table(config.FacilityTankList)
cfv.assert_type_table(config.FacilityTankConns)
cfv.assert_type_table(config.TankFluidTypes)
cfv.assert_type_table(config.AuxiliaryCoolant)
cfv.assert_range(config.FacilityTankMode, 0, 8)
cfv.assert_type_bool(config.ExtChargeIdling)
@@ -239,20 +262,32 @@ function supervisor.comms(_version, nic, fp_ok, facility)
-- PLC linking request
if packet.length == 4 and type(packet.data[4]) == "number" then
local reactor_id = packet.data[4]
local plc_id = svsessions.establish_plc_session(src_addr, i_seq_num, reactor_id, firmware_v)
if plc_id == false then
-- reactor already has a PLC assigned
if last_ack ~= ESTABLISH_ACK.COLLISION then
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
-- check ID validity
if reactor_id < 1 or reactor_id > config.UnitCount then
-- reactor index out of range
if last_ack ~= ESTABLISH_ACK.DENY then
log.warning(util.c("PLC_ESTABLISH: denied assignment ", reactor_id, " outside of configured unit count ", config.UnitCount))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION)
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
else
-- got an ID; assigned to a reactor successfully
println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \xbb reactor ", reactor_id, " connected"))
log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [@", src_addr, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
-- try to establish the session
local plc_id = svsessions.establish_plc_session(src_addr, i_seq_num, reactor_id, firmware_v)
if plc_id == false then
-- reactor already has a PLC assigned
if last_ack ~= ESTABLISH_ACK.COLLISION then
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION)
else
-- got an ID; assigned to a reactor successfully
println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \xbb reactor ", reactor_id, " connected"))
log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [@", src_addr, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
end
end
else
log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type")

View File

@@ -3,20 +3,23 @@ local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local logic = require("supervisor.unitlogic")
local alarm_ctl = require("supervisor.alarm_ctl")
local unit_logic = require("supervisor.unit_logic")
local plc = require("supervisor.session.plc")
local rsctl = require("supervisor.session.rsctl")
local svsessions = require("supervisor.session.svsessions")
local WASTE_MODE = types.WASTE_MODE
local WASTE = types.WASTE_PRODUCT
local AISTATE = alarm_ctl.AISTATE
local ALARM = types.ALARM
local PRIO = types.ALARM_PRIORITY
local ALARM_STATE = types.ALARM_STATE
local TRI_FAIL = types.TRI_FAIL
local PRIO = types.ALARM_PRIORITY
local RTU_ID_FAIL = types.RTU_ID_FAIL
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local TRI_FAIL = types.TRI_FAIL
local WASTE_MODE = types.WASTE_MODE
local WASTE = types.WASTE_PRODUCT
local PLC_S_CMDS = plc.PLC_S_CMDS
@@ -37,23 +40,6 @@ local DT_KEYS = {
TurbinePower = "TPR"
}
---@enum ALARM_INT_STATE
local AISTATE = {
INACTIVE = 1,
TRIPPING = 2,
TRIPPED = 3,
ACKED = 4,
RING_BACK = 5,
RING_BACK_TRIPPING = 6
}
---@class alarm_def
---@field state ALARM_INT_STATE internal alarm state
---@field trip_time integer time (ms) when first tripped
---@field hold_time integer time (s) to hold before tripping
---@field id ALARM alarm ID
---@field tier integer alarm urgency tier (0 = highest)
-- burn rate to idle at
local IDLE_RATE = 0.01
@@ -66,7 +52,8 @@ local unit = {}
---@param num_boilers integer number of boilers expected
---@param num_turbines integer number of turbines expected
---@param ext_idle boolean extended idling mode
function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
---@param aux_coolant boolean if this unit has auxiliary coolant
function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant)
-- time (ms) to idle for auto idling
local IDLE_TIME = util.trinary(ext_idle, 60000, 10000)
@@ -79,7 +66,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
plc_i = nil, ---@type plc_session
num_boilers = num_boilers,
num_turbines = num_turbines,
types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE },
aux_coolant = aux_coolant,
types = { DT_KEYS = DT_KEYS },
-- rtus
rtu_list = {}, ---@type unit_session[][]
redstone = {}, ---@type redstone_session[]
@@ -90,8 +78,10 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
envd = {}, ---@type envd_session[]
-- redstone control
io_ctl = nil, ---@type rs_controller
---@diagnostic disable-next-line: missing-fields
valves = {}, ---@type unit_valves
emcool_opened = false,
em_cool_opened = false,
aux_cool_opened = false,
-- auto control
auto_engaged = false,
auto_idle = false,
@@ -110,6 +100,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
damage_est_last = 0,
waste_product = WASTE.PLUTONIUM, ---@type WASTE_PRODUCT
status_text = { "UNKNOWN", "awaiting connection..." },
enable_aux_cool = false,
-- logic for alarms
had_reactor = false,
turbine_flow_stable = false,
@@ -253,7 +244,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
self.rtu_list = { self.redstone, self.boilers, self.turbines, self.tanks, self.snas, self.envd }
-- init redstone RTU I/O controller
self.io_ctl = rsctl.new(self.redstone)
self.io_ctl = rsctl.new(self.redstone, reactor_id)
-- init boiler table fields
for _ = 1, num_boilers do
@@ -372,6 +363,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
local waste_po = _make_valve_iface(IO.WASTE_POPL)
local waste_sps = _make_valve_iface(IO.WASTE_AM)
local emer_cool = _make_valve_iface(IO.U_EMER_COOL)
local aux_cool = _make_valve_iface(IO.U_AUX_COOL)
---@class unit_valves
self.valves = {
@@ -379,7 +371,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
waste_sna = waste_sna,
waste_po = waste_po,
waste_sps = waste_sps,
emer_cool = emer_cool
emer_cool = emer_cool,
aux_cool = aux_cool
}
-- route reactor waste for a given waste product
@@ -590,22 +583,22 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
_dt__compute_all()
-- update annunciator logic
logic.update_annunciator(self)
unit_logic.update_annunciator(self)
-- update alarm status
logic.update_alarms(self)
unit_logic.update_alarms(self)
-- if in auto mode, SCRAM on certain alarms
logic.update_auto_safety(public, self)
unit_logic.update_auto_safety(self, public)
-- update status text
logic.update_status_text(self)
unit_logic.update_status_text(self)
-- handle redstone I/O
if #self.redstone > 0 then
logic.handle_redstone(self)
unit_logic.handle_redstone(self)
elseif not self.plc_cache.rps_trip then
self.emcool_opened = false
self.em_cool_opened = false
end
end
@@ -723,7 +716,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
-- queue a command to clear timeout/auto-scram if set
function public.auto_cond_rps_reset()
if self.plc_s ~= nil and self.plc_i ~= nil and (not self.auto_was_alarmed) and (not self.emcool_opened) then
if self.plc_s ~= nil and self.plc_i ~= nil and (not self.auto_was_alarmed) and (not self.em_cool_opened) then
local rps = self.plc_i.get_rps()
if rps.timeout or rps.automatic then
self.plc_i.auto_lock(true) -- if it timed out/restarted, auto lock was lost, so re-lock it
@@ -768,10 +761,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
-- acknowledge all alarms (if possible)
function public.ack_all()
for i = 1, #self.db.alarm_states do
if self.db.alarm_states[i] == ALARM_STATE.TRIPPED then
self.db.alarm_states[i] = ALARM_STATE.ACKED
end
for id, state in pairs(self.db.alarm_states) do
if state == ALARM_STATE.TRIPPED then self.db.alarm_states[id] = ALARM_STATE.ACKED end
end
end
@@ -813,7 +804,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
if limit > 0 then
self.db.control.lim_br100 = math.floor(limit * 100)
if self.plc_i ~= nil then
if (self.plc_i ~= nil) and (type(self.plc_i.get_struct().max_burn) == "number") then
if limit > self.plc_i.get_struct().max_burn then
self.db.control.lim_br100 = math.floor(self.plc_i.get_struct().max_burn * 100)
end
@@ -839,6 +830,12 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
return false
end
-- check the active state of the reactor (if connected)
---@nodiscard
function public.is_reactor_enabled()
if self.plc_i ~= nil then return self.plc_i.get_status().status else return false end
end
-- check if the reactor is connected, is stopped, the RPS is not tripped, and no alarms are active
---@nodiscard
function public.is_safe_idle()
@@ -858,7 +855,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
-- check if emergency coolant activation has been tripped
---@nodiscard
function public.is_emer_cool_tripped() return self.emcool_opened end
function public.is_emer_cool_tripped() return self.em_cool_opened end
-- get build properties of machines
--
@@ -986,7 +983,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
local db = self.snas[i].get_db()
total_peak = total_peak + db.state.peak_production
total_avail = total_avail + db.state.production_rate
total_out = total_out + math.min(db.tanks.input.amount / 10, db.state.production_rate)
local out_from_in = util.trinary(db.tanks.input.amount >= 10, db.tanks.input.amount / 10, 0)
total_out = total_out + math.min(out_from_in, db.state.production_rate)
end
status.sna = { #self.snas, total_peak, total_avail, total_out }
@@ -1051,7 +1049,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
v.waste_sna.check(),
v.waste_po.check(),
v.waste_sps.check(),
v.emer_cool.check()
v.emer_cool.check(),
v.aux_cool.check()
}
end

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