Compare commits

...

278 Commits

Author SHA1 Message Date
Mikayla
fb102502dd Merge pull request #646 from MikaylaFischler/580-wired-comms-networking
580 wired comms networking
2025-11-09 16:06:33 -05:00
Mikayla Fischler
2b015759fd #580 close all channels on standby modems on connect 2025-11-09 15:44:57 -05:00
Mikayla Fischler
82aff3b30b #580 updated hardware labeling to not touch the edges 2025-11-09 15:42:14 -05:00
Mikayla Fischler
12d688daec #580 added more new! tags and updated change logs 2025-11-09 15:36:14 -05:00
Mikayla Fischler
d4c5140003 #636 coordinator close UI on connection switch for now 2025-11-09 00:34:45 -05:00
Mikayla Fischler
21c36c70be #634 removed checking wireless on disconnect due to peripheral already being gone, changed to checking if it belonged to the wireless nic 2025-11-09 00:20:34 -05:00
Mikayla Fischler
b020dde122 #638 fixed pocket test config checkbox initialization 2025-11-09 00:06:55 -05:00
Mikayla Fischler
fa551f0b4f #638 update info on testing denial and don't send close to unlinked hosts 2025-11-09 00:06:36 -05:00
Mikayla Fischler
29513bac38 #638 try reconnecting to both supervisor and coordinator at the same time 2025-11-09 00:06:02 -05:00
Mikayla Fischler
1254d668a9 decrease reactor PLC reconnect time 2025-11-08 23:49:50 -05:00
Mikayla Fischler
3987d337c4 #636 close connections on nic switch 2025-11-08 19:14:34 -05:00
Mikayla Fischler
76fc5751c9 fixed reactor PLC reporting as degraded if everything is OK on boot 2025-11-08 19:14:05 -05:00
Mikayla Fischler
6e26ca4fac #634 fixed wired inactive disconnected logging as unassigned modem 2025-11-08 19:13:49 -05:00
Mikayla Fischler
32af935e9e supervisor establish log message updates 2025-11-08 18:27:17 -05:00
Mikayla Fischler
46b23414b0 #642 RTU gateway connection test 2025-11-08 18:22:27 -05:00
Mikayla Fischler
3f1cf217ac #580 fix to initial wireless modem DOWN message 2025-11-08 18:18:29 -05:00
Mikayla Fischler
6dea501946 #642 reactor PLC self-check updates 2025-11-08 17:27:10 -05:00
Mikayla Fischler
cc36aafccd #580 supervisor config check fix 2025-11-08 17:20:51 -05:00
Mikayla Fischler
569358a4e1 #642 updated reactor PLC connection test 2025-11-08 17:20:24 -05:00
Mikayla Fischler
504dce64c2 #580 review changes/fixes 2025-11-08 15:20:28 -05:00
Mikayla Fischler
699ba9f71e #580 coordinator front panel updates for wired modem and new labeling 2025-11-08 14:16:46 -05:00
Mikayla Fischler
c0f9ba6ba6 #580 increased clarity of warning about connecting wired comms modem to peripherals 2025-11-08 14:00:07 -05:00
Mikayla Fischler
138c10ad1f #634 fixed monitor state indicators not always being updated 2025-11-08 13:56:37 -05:00
Mikayla Fischler
6774f60605 fixed coordinator bug with fp init, fixed incorrect comments 2025-11-08 13:55:51 -05:00
Mikayla
2ecb662b0a review fixes 2025-11-07 23:19:14 +00:00
Mikayla
50dedaa7c8 OS log tagging to match PLC 2025-11-07 18:52:13 +00:00
Mikayla
55e4fed9d8 luacheck fixes 2025-11-07 18:51:42 +00:00
Mikayla
b9a9c018a1 #634 RTU gateway backplane log message updates 2025-11-07 17:50:25 +00:00
Mikayla
e0a0c34b54 connected vs reconnected consistency 2025-11-07 17:46:41 +00:00
Mikayla
25207f39c0 #634 moved most monitor connect handling to the backplane and made display indicators on the front panel 3-state 2025-11-07 17:43:12 +00:00
Mikayla
8c8d3faf72 #634 moved most monitor disconnect handling to the backplane 2025-11-07 17:08:05 +00:00
Mikayla
9ff183b17d #634 RTU gateway backplane fixes 2025-11-07 16:52:01 +00:00
Mikayla
212e1f8fe8 #634 coordinator backplane attach/detach 2025-11-07 16:51:43 +00:00
Mikayla
b2baaa2090 reactor PLC queue types refactor and properly report wireless modem connected when another is found 2025-11-06 22:54:48 +00:00
Mikayla
44340f42d4 consistent ordering of attach/detach functions 2025-11-06 22:49:22 +00:00
Mikayla
61305621c3 RTU cleanup and fixes 2025-11-06 22:48:42 +00:00
Mikayla Fischler
6123d5dad7 #580 minor updates for consistency 2025-11-03 18:47:55 -05:00
Mikayla Fischler
5abe687f69 #634 coordinator backplane interface 2025-11-03 18:47:36 -05:00
Mikayla Fischler
299c6bcf7a #580 coordinator config loading and checking 2025-11-02 19:02:49 -05:00
Mikayla Fischler
7745e94fbe #580 rtu and plc configurator enhancements 2025-11-02 17:45:11 -05:00
Mikayla Fischler
9a58bf1bb7 #580 supervisor configurator fixes 2025-11-02 17:44:24 -05:00
Mikayla Fischler
6bfa26407a #580 coordinator wired networking configurator changes 2025-11-02 16:36:20 -05:00
Mikayla Fischler
645c2bacbd removed old "new!" labels 2025-11-02 16:23:07 -05:00
Mikayla Fischler
802ef149c5 #638 require pocket testing enabled rather than HMAC for alarm testing 2025-11-02 15:09:45 -05:00
Mikayla Fischler
cc9d5fe2d6 #580 rtu gateway front panel changes and updates to plc and supervisor front panels 2025-11-02 15:02:56 -05:00
Mikayla Fischler
d9001090c2 #580 comment updates, rtu listen fix, and trusted range init change 2025-11-02 14:30:02 -05:00
Mikayla Fischler
0417986c15 #580 updated supervisor front panel 2025-11-02 13:30:25 -05:00
Mikayla Fischler
4c8d5bc4a0 #580 plc front panel updates 2025-11-02 13:19:04 -05:00
Mikayla Fischler
8bbf385c41 #580 fixed supervisor listen mode logic 2025-11-02 13:05:07 -05:00
Mikayla
d7a280bb04 #634 reactor PLC backplane and front panel updates 2025-11-02 17:10:01 +00:00
Mikayla
f88dc0b5b9 #634 RTU gateway backplane rework 2025-11-02 17:03:41 +00:00
Mikayla
34eb16df00 #634 supervisor backplane updates 2025-11-02 17:01:55 +00:00
Mikayla
0cc62b3447 removed unused databus functions 2025-11-02 17:01:20 +00:00
Mikayla Fischler
cddd9f7437 #634 work on reactor PLC backplane 2025-10-26 22:19:37 -04:00
Mikayla Fischler
5acc6470e3 PPM parameter clarity 2025-10-26 20:41:25 -04:00
Mikayla Fischler
e57c6205e2 comment cleanup 2025-10-26 20:41:02 -04:00
Mikayla Fischler
96acb03f73 #580 reactor PLC wired/wireless configurator updates, RTU gateway and supervisor updates to theirs as well 2025-10-26 20:31:35 -04:00
Mikayla Fischler
2fefe4fbd6 version increment and log message formatting 2025-10-26 18:15:18 -04:00
Mikayla Fischler
1f9e86f6ea Merge branch '580-wired-comms-networking' of github.com:MikaylaFischler/cc-mek-scada into 580-wired-comms-networking 2025-10-26 18:02:12 -04:00
Mikayla Fischler
a48c8c1efe merge fixes 2025-10-26 17:22:07 -04:00
Mikayla Fischler
deeeb612b1 Merge branch 'devel' into 580-wired-comms-networking 2025-10-26 17:08:12 -04:00
Mikayla
c62eaeb5a2 #580 work on reactor PLC wired comms 2025-10-26 20:54:09 +00:00
Mikayla
1a7cb9eaa8 Merge pull request #641 from MikaylaFischler/640-plc-initialization-enhancements
640 plc initialization enhancements
2025-10-26 16:50:29 -04:00
Mikayla Fischler
3139dc2176 review updates 2025-10-26 16:49:19 -04:00
Mikayla Fischler
25fc0050c3 fixed trailing whitespace 2025-10-26 16:06:16 -04:00
Mikayla
b57aceff15 comment updates 2025-10-26 20:05:19 +00:00
Mikayla
8fd04e44f3 #580 require a modem config in RTU gateway config check 2025-10-26 20:04:27 +00:00
Mikayla
869b342db2 #634 supervisor backplane logging changes 2025-10-26 20:03:42 +00:00
Mikayla Fischler
f0251efec6 #580 RTU backplane and logging updates 2025-10-26 15:41:43 -04:00
Mikayla Fischler
c7e02efbc7 #580 RTU configurator updates 2025-10-26 15:39:30 -04:00
Mikayla Fischler
db8bed583f #580 additional supervisor configurator fixes 2025-10-26 15:34:47 -04:00
Mikayla Fischler
2d44014e2e #580 fixes for listen mode enum change 2025-10-26 15:30:14 -04:00
Mikayla Fischler
452fe71ab8 #580 modem list fix 2025-10-26 15:28:30 -04:00
Mikayla Fischler
22208e91aa #580 supervisor network configurator updates 2025-10-26 15:17:15 -04:00
Mikayla Fischler
d412f61a5f tool_ctl tank_fluid_opts for prior commit 2025-10-26 15:16:02 -04:00
Mikayla Fischler
c62ec1e786 include tank fluid types and aux coolant correctly in load_legacy and save_and_continue 2025-10-26 15:02:03 -04:00
Mikayla Fischler
1890f0a983 remove needless empty graphics callbacks 2025-10-26 14:21:40 -04:00
Mikayla Fischler
390cf98b0a private scada-common data refactor 2025-10-26 13:41:59 -04:00
Mikayla Fischler
7ddd6f32c5 more log tags 2025-10-26 13:39:58 -04:00
Mikayla Fischler
18a488f1b9 #640 formed bugfix 2025-10-26 13:38:37 -04:00
Mikayla Fischler
4c7ad0c539 #640 bug fixes and enhancements 2025-10-24 15:30:26 -04:00
Mikayla
a083f8983b #640 reworked PLC initialization 2025-10-23 23:13:13 +00:00
Mikayla Fischler
cb11ece73d #580 WIP updated supervisor network config 2025-10-19 20:28:34 -04:00
Mikayla Fischler
fc24f39991 #580 supervisor wired/wireless dual networking 2025-10-19 17:30:05 -04:00
Mikayla Fischler
4d6c388f37 #580 supervisor backplane 2025-10-19 15:19:30 -04:00
Mikayla Fischler
9e3922a972 ppm wired modem list 2025-10-18 18:38:42 -04:00
Mikayla Fischler
1fcc91e98b #580 RTU gateway multi-modem wired/wireless failover networking 2025-10-18 17:23:06 -04:00
Mikayla Fischler
fe9ee313f9 fixed Checkbox set_value type hint 2025-10-18 17:17:33 -04:00
Mikayla Fischler
194a266730 #580 RTU gateway backplane 2025-10-18 14:44:33 -04:00
Mikayla Fischler
88862726e3 migrate RTU initialization to new file 2025-10-18 12:37:27 -04:00
Mikayla
2aa5c93404 comment and formatting fixes 2025-10-17 20:59:36 +00:00
Mikayla
859e04712f Merge branch 'devel' into 580-wired-comms-networking 2025-09-30 13:08:45 +00:00
Mikayla Fischler
f7fe9754fe #629 luacheck fix for remaining unused variables 2025-09-13 16:07:12 -04:00
Mikayla Fischler
ff68eeae1a #629 removed unused render commands 2025-09-13 16:04:45 -04:00
Mikayla Fischler
20f949a9dd fixed pocket main thread crash on nil current page, added info page to alarm test app 2025-09-13 15:50:25 -04:00
Mikayla
99c9fec195 Merge pull request #628 from MikaylaFischler/pocket-beta-dev
Pocket Beta
2025-09-12 21:56:12 -04:00
Mikayla Fischler
e7a859438e added additional terms to glossary 2025-09-12 21:54:58 -04:00
Mikayla
c81c83f432 Merge pull request #630 from MikaylaFischler/629-pocket-release-cleanup
629 pocket release cleanup
2025-09-12 21:49:26 -04:00
Mikayla Fischler
6ad63aedeb include reordering 2025-09-12 21:49:08 -04:00
Mikayla Fischler
a1bb5ce50b #629 rework of alarm testing app and other cleanup 2025-09-12 21:46:02 -04:00
Mikayla
afc89ac727 #629 code cleanup 2025-09-13 00:55:15 +00:00
Mikayla Fischler
55685fb6a6 #629 home page cleanup 2025-09-12 20:24:42 -04:00
Mikayla Fischler
61f1af7f4e #403 resolved todos in documentation 2025-09-12 20:20:11 -04:00
Mikayla Fischler
16f62bc32a pocket version update 2025-09-12 19:54:51 -04:00
Mikayla Fischler
a2ec418d53 luacheck fix 2025-09-12 19:52:58 -04:00
Mikayla Fischler
384ebb461f #622 comment change and version increment 2025-09-12 19:50:43 -04:00
Mikayla Fischler
0392385037 #622 reinforced induction matrix support 2025-09-12 19:45:15 -04:00
Mikayla Fischler
9e020b2852 #403 remaining documentation 2025-09-12 19:24:29 -04:00
Mikayla Fischler
4f7285573f #403 work on configuration guide 2025-09-12 00:00:01 -04:00
Mikayla Fischler
59f99f70a4 #403 basic connections guide 2025-09-11 23:39:06 -04:00
Mikayla Fischler
df9f1195e3 #403 fixed scroll bar sticking around sometimes 2025-09-11 22:30:39 -04:00
Mikayla Fischler
aba79e88cf #403 auto control usage guide 2025-09-11 22:28:58 -04:00
Mikayla
d0276e149b #400 revert changes 2025-09-12 01:22:27 +00:00
Mikayla Fischler
eb95f2331d #403 waste control doc updates 2025-09-02 14:33:10 -04:00
Mikayla
28150042cc #403 waste control usage documentation 2025-09-02 15:38:43 +00:00
Mikayla
4bc5af46ab #403 spelling fixes 2025-09-02 14:17:43 +00:00
Mikayla Fischler
2cee1ea895 #403 start of auto and waste control docs 2025-09-01 23:06:13 -04:00
Mikayla Fischler
7d0bbafd6c #403 manual control guide 2025-09-01 19:09:49 -04:00
Mikayla Fischler
dc19127836 #403 guide loading detailed info 2025-09-01 18:18:51 -04:00
Mikayla Fischler
6db6a7d7b7 #403 flow display documentation 2025-09-01 17:54:21 -04:00
Mikayla Fischler
017deec06e #403 unit display documentation 2025-09-01 17:05:31 -04:00
Mikayla Fischler
d0401fe51f #403 main display documentation 2025-09-01 16:37:21 -04:00
Mikayla Fischler
92113671ff #403 front panel documentation complete 2025-08-24 19:33:31 -04:00
Mikayla Fischler
e3d0692dcc #400 start of peripherals list app 2025-08-24 18:46:07 -04:00
Mikayla Fischler
83e29abea7 #401 bug fixes 2025-08-19 22:14:19 -04:00
Mikayla Fischler
4a1730ec47 #401 cleanup and global RTT limit constants 2025-08-19 18:43:43 -04:00
Mikayla Fischler
691b781c52 #401 slowed polling rate for computer app 2025-08-19 18:42:48 -04:00
Mikayla Fischler
3b856655c3 #401 comment updates 2025-08-13 11:46:26 -03:00
Mikayla Fischler
415cb71294 #401 working RTU and PKT computer lists 2025-08-13 11:32:49 -03:00
Mikayla Fischler
a678b8dbe0 #401 fix for supervisor coordinator RTT reporting 2025-08-13 11:17:30 -03:00
Mikayla Fischler
9bb3f59496 reworked pocket app loader 2025-08-13 11:15:33 -03:00
Mikayla Fischler
170cba702c PSIL updates stored value before notifying subscribers 2025-08-13 10:46:09 -03:00
Mikayla Fischler
fe78360948 #401 working main page of computer list app 2025-08-13 10:45:32 -03:00
Mikayla
e29a88eeea #401 sidebar updates 2025-08-06 16:00:28 +00:00
Mikayla
f3eb6d0464 #401 PLC, RTU, PKT computer lists 2025-08-06 15:56:06 +00:00
Mikayla
f4d4de659c #401 supervisor and coordinator computer display 2025-08-05 13:35:29 +00:00
Mikayla
1ce0bbfc65 #401 work on pocket computer list app 2025-08-01 21:43:19 +00:00
Mikayla
0bfe767710 #401 pocket handling of computer data 2025-08-01 21:41:40 +00:00
Mikayla
4fb39213f2 #401 supervisor support of pocket computer list app 2025-07-01 14:41:09 +00:00
Mikayla Fischler
acaa9369f4 added new! tags to reactor PLC configurator 2025-06-29 16:32:40 -04:00
Mikayla
9591668f87 #580 supervisor comms config verification 2025-06-28 17:57:47 +00:00
Mikayla
4a38ca7dd1 #580 nic constructor simplification 2025-06-28 17:41:25 +00:00
Mikayla
250db00794 #580 supervisor front panel updates for wired modem support 2025-06-28 17:26:49 +00:00
Mikayla
391b68d357 #580 supervisor wired comms networking logic 2025-06-28 17:17:31 +00:00
Mikayla Fischler
2998371b89 #623 PLC option to invert emergency coolant control 2025-06-27 11:46:50 -04:00
Mikayla
12664c6190 Merge pull request #624 from MikaylaFischler/pocket-alpha-dev
Radiation App
2025-06-21 14:59:46 -04:00
Mikayla Fischler
abe0c45534 cleanup, #593 fixes, version increment 2025-06-21 14:58:45 -04:00
Mikayla Fischler
a104d8ba83 #593 radiation app fixes/cleanup 2025-06-21 10:45:43 -04:00
Mikayla
a629c04d11 #593 radiation monitor lists 2025-06-16 19:21:16 +00:00
Mikayla
6d3b35a41d #621 support environment_detector peripheral type 2025-06-16 15:07:56 +00:00
Mikayla Fischler
e1ac42f5f8 #593 structure and graphics elements for the radiation monitor app 2025-06-15 18:24:36 -04:00
Mikayla Fischler
4a7fc6200e #580 work on supervisor wired modem configuration 2025-06-15 15:43:04 -04:00
Mikayla
9e59883a84 #593 radiation monitor data comms 2025-06-03 14:10:30 +00:00
Mikayla Fischler
79d63fce78 #593 start of radiation monitor app 2025-05-24 17:17:57 -04:00
Mikayla
bee1cdf01c return wireless modem interface name 2025-05-24 20:03:17 +00:00
Mikayla
c6143934d8 check interface side in network logic before handling packets 2025-05-24 20:02:19 +00:00
Mikayla
c319039a4e simplified RTU gateway startup check failure code 2025-05-24 20:01:05 +00:00
Mikayla
4b61037170 Merge branch 'devel' into 580-wired-comms-networking 2025-05-19 13:26:36 +00:00
Mikayla Fischler
ce92fd15ef Merge branch 'devel' into pocket-alpha-dev 2025-05-10 17:51:17 -04:00
Mikayla Fischler
264edc0030 #592 fixed bug with pocket help page linking navigation 2025-05-10 17:30:54 -04:00
Mikayla Fischler
fcb17ae5e7 show "new!" next to new config fields 2025-05-10 11:26:19 -04:00
Mikayla Fischler
35f82af2e2 fixed CONFIG button coloring 2025-05-10 11:25:52 -04:00
Mikayla
1a2ecd0599 Merge pull request #619 from MikaylaFischler/rtu-redstone-enhancements
604 redstone relay integration
2025-05-09 11:41:53 -04:00
Mikayla
5f8c947105 cleanup and fixes 2025-05-09 15:41:14 +00:00
Mikayla Fischler
41e6d89a4b incremented comms version for RTU advertisement changes 2025-05-07 20:06:04 -04:00
Mikayla Fischler
f01fb62863 #604 updated emergency coolant annunciator logic 2025-05-07 20:05:03 -04:00
Mikayla Fischler
8f6425b814 #604 fixed supervisor bugs with new redstone 2025-05-07 20:04:39 -04:00
Mikayla Fischler
069a7ce0ad #604 front panel updates and hw state tracking fixes 2025-05-07 20:03:48 -04:00
Mikayla Fischler
8eff1c0d76 #604 refresh connections count on saving an interface 2025-05-07 20:03:20 -04:00
Mikayla Fischler
7404e6da31 #604 updated self check for relays and added duplicate input detection 2025-05-07 11:48:32 -04:00
Mikayla Fischler
12ead136a3 #604 configuration of redstone RTUs 2025-05-07 11:27:53 -04:00
Mikayla Fischler
e3dbda3c54 fixed logic for duplicate input detection 2025-05-07 10:42:52 -04:00
Mikayla
028a161af0 #580 RTU gateway changes for wired comms modems 2025-05-05 17:57:54 +00:00
Mikayla
454d166ac9 #580 reactor PLC changes for wired comms modems 2025-05-05 17:54:47 +00:00
Mikayla
41b6a558d5 init RTU gateway UI after checking for modem to prevent that failure making a UI mess 2025-05-05 16:35:44 +00:00
Mikayla Fischler
07bb0f13e3 Merge branch 'devel' into rtu-redstone-enhancements 2025-05-03 09:54:29 -04:00
Mikayla
f1014ce941 Merge pull request #618 from MikaylaFischler/572-audible-alarms-on-facility-radiation
572 audible alarms on facility radiation
2025-04-30 10:27:08 -04:00
Mikayla Fischler
0debbdc167 fixed facility not scram'ing on radiation 2025-04-30 10:25:30 -04:00
Mikayla Fischler
0a26629e20 fixed no_ring_back going to RING_BACK if leaving TRIPPED not ACKED 2025-04-30 10:17:28 -04:00
Mikayla Fischler
9393b1830d restored use of self for unit logic function calls 2025-04-30 10:17:09 -04:00
Mikayla
0df1e48780 reorganized unit logic inclusion to work like facility update 2025-04-29 20:11:12 +00:00
Mikayla
b8c30ba8a4 cleanup 2025-04-29 19:47:29 +00:00
Mikayla
eafd39fa35 #572 added facility radiation alarm 2025-04-29 19:41:52 +00:00
Mikayla
86dc92f09a Merge branch 'devel' into pocket-alpha-dev 2025-04-29 14:33:05 +00:00
Mikayla
e6f5ab8ef4 #604 reworked supervisor redstone RTU interface 2025-04-29 02:38:42 +00:00
Mikayla
be462db50b #604 new redstone initialization logic 2025-04-29 01:44:52 +00:00
Mikayla Fischler
1dc3d82e59 #604 work on redstone RTU rework 2025-04-27 22:40:42 -04:00
Mikayla Fischler
fa2a6d7786 Merge branch 'devel' into rtu-redstone-enhancements 2025-04-26 15:30:31 -04:00
Mikayla Fischler
04c53c7074 #616 pocket pellet color options 2025-04-26 15:24:50 -04:00
Mikayla Fischler
1af2cdba8d #616 fixes to coordinator pellet color option 2025-04-26 15:21:09 -04:00
Mikayla Fischler
0d7302dc8e #604 start of total rework of redstone RTUs for relay functionalitiy 2025-04-21 22:18:09 -04:00
Mikayla Fischler
48ec973695 #616 added pellet color option to the coordinator 2025-04-21 22:13:58 -04:00
Mikayla Fischler
ee868eb607 #616 updated flow view to match fluid colors not pellet colors 2025-04-20 21:36:40 -04:00
Mikayla
e4da9a62d9 Merge pull request #617 from MikaylaFischler/rtu-redstone-enhancements
redstone invert
2025-04-20 17:41:26 -04:00
Mikayla Fischler
c8910bfc40 clear inverted on analog creation 2025-04-20 17:40:40 -04:00
Mikayla Fischler
d6e3a67562 #484 redstone inversion support 2025-04-20 17:36:16 -04:00
Mikayla Fischler
f7c0a1d97d #484 work on redstone inversion 2025-04-20 11:28:02 -04:00
Mikayla
13509136b8 cleanup 2025-04-19 22:20:57 +00:00
Mikayla Fischler
bfab2d6af2 #583 fixes for pocket crash display 2025-04-19 18:09:43 -04:00
Mikayla Fischler
ae055a7d99 luacheck fixes and version increment 2025-04-19 00:21:15 -04:00
Mikayla Fischler
592f1110ed define pocket global in craftos-pc environment pocket application 2025-04-19 00:16:43 -04:00
Mikayla Fischler
97875f4e52 #583 graphical crash screens 2025-04-19 00:15:41 -04:00
Mikayla
657261642c Merge pull request #615 from MikaylaFischler/364-rtu-configurator-self-check
364 rtu configurator self check
2025-04-05 21:18:15 -04:00
Mikayla Fischler
0da944c3ea #364 updated scroll height max and reduced duplicate text 2025-04-05 21:17:19 -04:00
Mikayla Fischler
1b692b5b9a #364 fail self check if using a side for both bundled and unbundled redstone 2025-04-05 21:14:33 -04:00
Mikayla Fischler
b4a9366f73 #364 fixes to redstone and peripheral checks 2025-04-05 21:00:16 -04:00
Mikayla Fischler
2b3099ac59 #364 check validity of redstone and peripheral entries and check redstone side/color combos are not repeated 2025-04-04 00:17:40 -04:00
Mikayla Fischler
cd654fb9b8 shorter variable for self.settings in PLC self check 2025-04-03 23:21:31 -04:00
Mikayla Fischler
ad834218c2 #364 check all configured RTU peripherals in self check 2025-04-03 23:21:06 -04:00
Mikayla Fischler
c6a7de2669 #364 base RTU gateway self checks 2025-04-03 23:06:45 -04:00
Mikayla Fischler
d374967cb7 #614 fixed reactor PLC self check when configured as not networked 2025-04-03 22:56:54 -04:00
Mikayla 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
122 changed files with 8969 additions and 3663 deletions

494
coordinator/backplane.lua Normal file
View File

@@ -0,0 +1,494 @@
--
-- Coordinator System Core Peripheral Backplane
--
local log = require("scada-common.log")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol")
local sounder = require("coordinator.sounder")
local println = util.println
local log_sys = coordinator.log_sys
local log_boot = coordinator.log_boot
local log_comms = coordinator.log_comms
---@class crd_backplane
local backplane = {}
local _bp = {
smem = nil, ---@type crd_shared_memory
wlan_pref = true,
lan_iface = "",
act_nic = nil, ---@type nic
wd_nic = nil, ---@type nic|nil
wl_nic = nil, ---@type nic|nil
speaker = nil, ---@type Speaker|nil
---@class crd_displays
displays = {
main = nil, ---@type Monitor|nil
main_iface = "",
flow = nil, ---@type Monitor|nil
flow_iface = "",
unit_displays = {}, ---@type Monitor[]
unit_ifaces = {} ---@type string[]
}
}
-- initialize the display peripheral backplane
---@param config crd_config
---@return boolean success, string error_msg
function backplane.init_displays(config)
local displays = _bp.displays
local w, h, _
log.info("BKPLN: DISPLAY INIT")
-- monitor configuration verification
local mon_cfv = util.new_validator()
mon_cfv.assert_type_str(config.MainDisplay)
if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end
mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount)
for i = 1, #config.UnitDisplays do
mon_cfv.assert_type_str(config.UnitDisplays[i])
end
if not mon_cfv.valid() then
return false, "Monitor configuration invalid."
end
-- setup and check display peripherals
-- main display
local disp, iface = ppm.get_periph(config.MainDisplay), config.MainDisplay
displays.main = disp
displays.main_iface = iface
log.info("BKPLN: DISPLAY LINK_" .. util.trinary(disp, "UP", "DOWN") .. " MAIN/" .. iface)
iocontrol.fp_monitor_state("main", util.trinary(disp, 2, 1))
if not disp then
return false, "Main monitor is not connected."
end
disp.setTextScale(0.5)
w, _ = ppm.monitor_block_size(disp.getSize())
if w ~= 8 then
log.info("BKPLN: DISPLAY MAIN/" .. iface .. " BAD RESOLUTION")
return false, util.c("Main monitor width is incorrect (was ", w, ", must be 8).")
end
-- flow display
if not config.DisableFlowView then
disp, iface = ppm.get_periph(config.FlowDisplay), config.FlowDisplay
displays.flow = disp
displays.flow_iface = iface
log.info("BKPLN: DISPLAY LINK_" .. util.trinary(disp, "UP", "DOWN") .. " FLOW/" .. iface)
iocontrol.fp_monitor_state("flow", util.trinary(disp, 2, 1))
if not disp then
return false, "Flow monitor is not connected."
end
disp.setTextScale(0.5)
w, _ = ppm.monitor_block_size(disp.getSize())
if w ~= 8 then
log.info("BKPLN: DISPLAY FLOW/" .. iface .. " BAD RESOLUTION")
return false, util.c("Flow monitor width is incorrect (was ", w, ", must be 8).")
end
end
-- unit display(s)
for i = 1, config.UnitCount do
disp, iface = ppm.get_periph(config.UnitDisplays[i]), config.UnitDisplays[i]
displays.unit_displays[i] = disp
displays.unit_ifaces[i] = iface
log.info("BKPLN: DISPLAY LINK_" .. util.trinary(disp, "UP", "DOWN") .. " UNIT_" .. i .. "/" .. iface)
iocontrol.fp_monitor_state(i, util.trinary(disp, 2, 1))
if not disp then
return false, "Unit " .. i .. " monitor is not connected."
end
disp.setTextScale(0.5)
w, h = ppm.monitor_block_size(disp.getSize())
if w ~= 4 or h ~= 4 then
log.info("BKPLN: DISPLAY UNIT_" .. i .. "/" .. iface .. " BAD RESOLUTION")
return false, util.c("Unit ", i, " monitor size is incorrect (was ", w, " by ", h,", must be 4 by 4).")
end
end
log.info("BKPLN: DISPLAY INIT OK")
return true, ""
end
-- initialize the system peripheral backplane
---@param config crd_config
---@param __shared_memory crd_shared_memory
---@return boolean success
function backplane.init(config, __shared_memory)
_bp.smem = __shared_memory
_bp.wlan_pref = config.PreferWireless
_bp.lan_iface = config.WiredModem
-- Modem Init
-- init wired NIC
if type(_bp.lan_iface) == "string" then
local modem = ppm.get_modem(_bp.lan_iface)
local wd_nic = network.nic(modem)
log.info("BKPLN: WIRED PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. _bp.lan_iface)
log_comms("wired comms modem " .. util.trinary(modem, "connected", "not found"))
_bp.wd_nic = wd_nic
_bp.act_nic = wd_nic -- set this as active for now
wd_nic.closeAll()
wd_nic.open(config.CRD_Channel)
iocontrol.fp_has_wd_modem(modem ~= nil)
end
-- init wireless NIC(s)
if config.WirelessModem then
local modem, iface = ppm.get_wireless_modem()
local wl_nic = network.nic(modem)
log.info("BKPLN: WIRELESS PHY_" .. util.trinary(modem, "UP ", "DOWN") .. (iface or ""))
log_comms("wireless comms modem " .. util.trinary(modem, "connected", "not found"))
-- set this as active if connected or if both modems are disconnected and this is preferred
if (modem and _bp.wlan_pref) or not (_bp.act_nic and _bp.act_nic.is_connected()) then
_bp.act_nic = wl_nic
log.info("BKPLN: switched active to preferred wireless")
end
_bp.wl_nic = wl_nic
wl_nic.closeAll()
wl_nic.open(config.CRD_Channel)
iocontrol.fp_has_wl_modem(modem ~= nil)
end
-- at least one comms modem is required
if not ((_bp.wd_nic and _bp.wd_nic.is_connected()) or (_bp.wl_nic and _bp.wl_nic.is_connected())) then
log_comms("no comms modem found")
println("startup> no comms modem found")
log.warning("BKPLN: no comms modem on startup")
return false
end
-- Speaker Init
_bp.speaker = ppm.get_device("speaker")
if not _bp.speaker then
log_boot("annunciator alarm speaker not found")
println("startup> speaker not found")
log.fatal("BKPLN: no annunciator alarm speaker found")
return false
else
log.info("BKPLN: SPEAKER LINK_UP " .. ppm.get_iface(_bp.speaker))
log_boot("annunciator alarm speaker connected")
local sounder_start = util.time_ms()
sounder.init(_bp.speaker, config.SpeakerVolume)
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
log_sys("annunciator alarm configured")
iocontrol.fp_has_speaker(true)
end
return true
end
-- get the active NIC
function backplane.active_nic() return _bp.act_nic end
-- get the wireless NIC
function backplane.wireless_nic() return _bp.wl_nic end
-- get the configured displays
function backplane.displays() return _bp.displays end
-- handle a backplane peripheral attach
---@param type string
---@param device table
---@param iface string
function backplane.attach(type, device, iface)
local MQ__RENDER_CMD = _bp.smem.q_types.MQ__RENDER_CMD
local MQ__RENDER_DATA = _bp.smem.q_types.MQ__RENDER_DATA
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
local comms = _bp.smem.crd_sys.coord_comms
if type == "modem" then
---@cast device Modem
local m_is_wl = device.isWireless()
log.info(util.c("BKPLN: ", util.trinary(m_is_wl, "WIRELESS", "WIRED"), " PHY_ATTACH ", iface))
if wd_nic and (_bp.lan_iface == iface) then
-- connect this as the wired NIC
wd_nic.connect(device)
log.info("BKPLN: WIRED PHY_UP " .. iface)
log_sys("wired comms modem reconnected")
iocontrol.fp_has_wd_modem(true)
if (_bp.act_nic ~= wd_nic) and not _bp.wlan_pref then
-- switch back to preferred wired
_bp.act_nic = wd_nic
_bp.smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wired modem (preferred)")
end
elseif wl_nic and (not wl_nic.is_connected()) and m_is_wl then
-- connect this as the wireless NIC
wl_nic.connect(device)
log.info("BKPLN: WIRELESS PHY_UP " .. iface)
log_sys("wireless comms modem reconnected")
iocontrol.fp_has_wl_modem(true)
if (_bp.act_nic ~= wl_nic) and _bp.wlan_pref then
-- switch back to preferred wireless
_bp.act_nic = wl_nic
_bp.smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wireless modem (preferred)")
end
elseif wl_nic and m_is_wl then
-- the wireless NIC already has a modem
device.closeAll()
log_sys("standby wireless modem connected")
log.info("BKPLN: standby wireless modem connected")
else
device.closeAll()
log_sys("unassigned modem connected")
log.warning("BKPLN: unassigned modem connected")
end
elseif type == "monitor" then
---@cast device Monitor
local is_used = false
log.info("BKPLN: DISPLAY LINK_UP " .. iface)
if _bp.displays.main_iface == iface then
is_used = true
_bp.displays.main = device
log.info("BKPLN: main display reconnected")
iocontrol.fp_monitor_state("main", 2)
elseif _bp.displays.flow_iface == iface then
is_used = true
_bp.displays.flow = device
log.info("BKPLN: flow display reconnected")
iocontrol.fp_monitor_state("flow", 2)
else
for idx, monitor in ipairs(_bp.displays.unit_ifaces) do
if monitor == iface then
is_used = true
_bp.displays.unit_displays[idx] = device
log.info("BKPLN: unit " .. idx .. " display reconnected")
iocontrol.fp_monitor_state(idx, 2)
break
end
end
end
-- notify renderer if it is using it
if is_used then
log_sys(util.c("configured monitor ", iface, " reconnected"))
_bp.smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, iface)
else
log_sys(util.c("unused monitor ", iface, " connected"))
end
elseif type == "speaker" then
---@cast device Speaker
log.info("BKPLN: SPEAKER LINK_UP " .. iface)
sounder.reconnect(device)
log_sys("alarm sounder speaker reconnected")
iocontrol.fp_has_speaker(true)
end
end
-- handle a backplane peripheral detach
---@param type string
---@param device table
---@param iface string
function backplane.detach(type, device, iface)
local MQ__RENDER_CMD = _bp.smem.q_types.MQ__RENDER_CMD
local MQ__RENDER_DATA = _bp.smem.q_types.MQ__RENDER_DATA
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
local comms = _bp.smem.crd_sys.coord_comms
if type == "modem" then
---@cast device Modem
log.info(util.c("BKPLN: PHY_DETACH ", iface))
if wd_nic and wd_nic.is_modem(device) then
wd_nic.disconnect()
log.info("BKPLN: WIRED PHY_DOWN " .. iface)
iocontrol.fp_has_wd_modem(false)
elseif wl_nic and wl_nic.is_modem(device) then
wl_nic.disconnect()
log.info("BKPLN: WIRELESS PHY_DOWN " .. iface)
iocontrol.fp_has_wl_modem(false)
end
-- we only care if this is our active comms modem
if _bp.act_nic.is_modem(device) then
log_sys("active comms modem disconnected")
log.warning("BKPLN: active comms modem disconnected")
-- failover and try to find a new comms modem
if _bp.act_nic == wl_nic then
-- wireless active disconnected
-- try to find another wireless modem, otherwise switch to wired
local modem, m_iface = ppm.get_wireless_modem()
if wl_nic and modem then
log_sys("found another wireless modem, using it for comms")
log.info("BKPLN: found another wireless modem, using it for comms")
wl_nic.connect(modem)
log.info("BKPLN: WIRELESS PHY_UP " .. m_iface)
iocontrol.fp_has_wl_modem(true)
elseif wd_nic and wd_nic.is_connected() then
_bp.act_nic = wd_nic
_bp.smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wired modem")
else
-- close out main UI
_bp.smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
-- alert user to status
log_sys("awaiting comms modem reconnect...")
end
elseif wl_nic and wl_nic.is_connected() then
-- wired active disconnected, wireless available
_bp.act_nic = wl_nic
_bp.smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wireless modem")
else
-- wired active disconnected, wireless unavailable
end
elseif wd_nic and wd_nic.is_modem(device) then
-- wired, but not active
log_sys("standby wired modem disconnected")
log.info("BKPLN: standby wired modem disconnected")
elseif wl_nic and wl_nic.is_modem(device) then
-- wireless, but not active
log_sys("standby wireless modem disconnected")
log.info("BKPLN: standby wireless modem disconnected")
else
log_sys("unassigned modem disconnected")
log.warning("BKPLN: unassigned modem disconnected")
end
elseif type == "monitor" then
---@cast device Monitor
local is_used = false
log.info("BKPLN: DISPLAY LINK_DOWN " .. iface)
if _bp.displays.main == device then
is_used = true
log.info("BKPLN: main display disconnected")
iocontrol.fp_monitor_state("main", 1)
elseif _bp.displays.flow == device then
is_used = true
log.info("BKPLN: flow display disconnected")
iocontrol.fp_monitor_state("flow", 1)
else
for idx, monitor in pairs(_bp.displays.unit_displays) do
if monitor == device then
is_used = true
log.info("BKPLN: unit " .. idx .. " display disconnected")
iocontrol.fp_monitor_state(idx, 1)
break
end
end
end
-- notify renderer if it was using it
if is_used then
log_sys("lost a configured monitor")
_bp.smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, iface)
else
log_sys("lost an unused monitor")
end
elseif type == "speaker" then
---@cast device Speaker
log.info("BKPLN: SPEAKER LINK_DOWN " .. iface)
log_sys("alarm sounder speaker disconnected")
iocontrol.fp_has_speaker(false)
end
end
return backplane

View File

@@ -149,18 +149,39 @@ local function handle_timeout()
end
-- attempt a connection to the supervisor to get cooling info
local function sv_connect()
---@param cfg crd_config current configuration for modem settings
local function sv_connect(cfg)
self.sv_conn_button.disable()
self.sv_conn_detail.set_value("")
local modem = ppm.get_wireless_modem()
local modem = nil
if cfg.WirelessModem then
modem = ppm.get_wireless_modem()
if cfg.WiredModem then
local wd_modem = ppm.get_modem(cfg.WiredModem)
if cfg.PreferWireless then
if not modem then
modem = wd_modem
end
else
if wd_modem then
modem = wd_modem
end
end
end
elseif cfg.WiredModem then
modem = ppm.get_modem(cfg.WiredModem)
end
if modem == nil then
self.sv_conn_status.set_value("Please connect an ender/wireless modem.")
self.sv_conn_status.set_value("Could not find configured modem(s).")
else
self.sv_conn_status.set_value("Modem found, connecting...")
if self.nic == nil then self.nic = network.nic(modem) end
self.nic.closeAll()
self.nic = network.nic(modem)
self.nic.open(self.tmp_cfg.CRD_Channel)
self.sv_addr = comms.BROADCAST
@@ -209,7 +230,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
self.sv_conn_status = TextBox{parent=fac_c_1,x=11,y=9,text=""}
self.sv_conn_detail = TextBox{parent=fac_c_1,x=1,y=11,height=2,text=""}
self.sv_conn_button = PushButton{parent=fac_c_1,x=1,y=9,text="Connect",min_width=9,callback=function()sv_connect()end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
self.sv_conn_button = PushButton{parent=fac_c_1,x=1,y=9,text="Connect",min_width=9,callback=function()sv_connect(tmp_cfg)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
local function sv_skip()
tcd.abort(handle_timeout)

View File

@@ -234,19 +234,23 @@ 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}
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"},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"}
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+)"},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}
tool_ctl.temp_scale = RadioButton{parent=crd_c_1,x=1,y=9,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,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,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

@@ -2,6 +2,7 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local types = require("scada-common.types")
local util = require("scada-common.util")
@@ -31,6 +32,9 @@ local RIGHT = core.ALIGN.RIGHT
local self = {
importing_legacy = false,
api_en = nil, ---@type Checkbox
pkt_chan = nil, ---@type NumberField
api_timeout = nil, ---@type NumberField
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type PushButton
auth_key_textbox = nil, ---@type TextBox
@@ -63,100 +67,204 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_5 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_6 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4,net_c_5,net_c_6}}
TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."}
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 3 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=1,text="Please select the network interface(s)."}
TextBox{parent=net_c_1,x=41,y=1,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_1,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
local function on_wired_change(_) tool_ctl.gen_modem_list() end
TextBox{parent=net_c_1,x=1,y=10,width=19,text="Coordinator Channel"}
local crd_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=10,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
local wireless = Checkbox{parent=net_c_1,x=1,y=3,label="Wireless/Ender Modem",default=ini_cfg.WirelessModem,box_fg_bg=cpair(colors.lightBlue,colors.black)}
TextBox{parent=net_c_1,x=24,y=3,text="(required for Pocket)",fg_bg=g_lg_fg_bg}
local wired = Checkbox{parent=net_c_1,x=1,y=5,label="Wired Modem",default=ini_cfg.WiredModem~=false,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=on_wired_change}
TextBox{parent=net_c_1,x=3,y=6,text="this one MUST ONLY connect to SCADA computers",fg_bg=cpair(colors.red,colors._INHERIT)}
TextBox{parent=net_c_1,x=3,y=7,text="connecting it to peripherals will cause issues",fg_bg=g_lg_fg_bg}
local modem_list = ListBox{parent=net_c_1,x=1,y=8,height=5,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"}
local pkt_chan = NumberField{parent=net_c_1,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
local modem_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_interfaces()
tmp_cfg.WirelessModem = wireless.get_value()
if not wired.get_value() then
tmp_cfg.WiredModem = false
tool_ctl.gen_modem_list()
end
if not (wired.get_value() or wireless.get_value()) then
modem_err.set_value("Please select a modem type.")
modem_err.show()
elseif wired.get_value() and type(tmp_cfg.WiredModem) ~= "string" then
modem_err.set_value("Please select a wired modem.")
modem_err.show()
else
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
self.wl_pref.enable()
else
self.wl_pref.set_value(tmp_cfg.WirelessModem)
self.wl_pref.disable()
end
if not tmp_cfg.WirelessModem then
self.api_en.set_value(false)
self.api_en.disable()
else
self.api_en.enable()
end
net_pane.set_value(2)
modem_err.hide(true)
end
end
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_interfaces,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,text="If you selected multiple interfaces, please specify if this device should prefer wireless or otherwise wired. The preferred interface is switched too when reconnected even if failover has succeeded onto the fallback interface."}
self.wl_pref = Checkbox{parent=net_c_2,y=7,label="Prefer Wireless",default=ini_cfg.PreferWireless,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=19,y=7,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
TextBox{parent=net_c_2,y=9,text="With a wireless modem, configure Pocket access."}
self.api_en = Checkbox{parent=net_c_2,y=11,label="Enable Pocket Access",default=ini_cfg.API_Enabled,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=24,y=11,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
local function submit_net_cfg_opts()
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
tmp_cfg.PreferWireless = self.wl_pref.get_value()
else
tmp_cfg.PreferWireless = tmp_cfg.WirelessModem
end
tmp_cfg.API_Enabled = tri(tmp_cfg.WirelessModem, self.api_en.get_value(), false)
if tmp_cfg.API_Enabled then
self.pkt_chan.enable()
self.api_timeout.enable()
else
self.pkt_chan.disable()
self.api_timeout.disable()
end
net_pane.set_value(3)
end
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_net_cfg_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,text="Please set the network channels below."}
TextBox{parent=net_c_3,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 3 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=8,width=18,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_3,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_3,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=10,width=19,text="Coordinator Channel"}
local crd_chan = NumberField{parent=net_c_3,x=21,y=10,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_3,x=29,y=10,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=12,width=14,text="Pocket Channel"}
self.pkt_chan = NumberField{parent=net_c_3,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=net_c_3,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
local chan_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_channels()
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(self.pkt_chan.get_value())
if not tmp_cfg.API_Enabled then pkt_c = tmp_cfg.PKT_Channel or 16244 end
if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then
tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c
net_pane.set_value(2)
net_pane.set_value(4)
chan_err.hide(true)
else chan_err.show() end
end
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,text="Please set the connection timeouts below."}
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=1,y=1,text="Please set the connection timeouts below."}
TextBox{parent=net_c_4,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=8,width=19,text="Supervisor Timeout"}
local svr_timeout = NumberField{parent=net_c_2,x=20,y=8,width=7,default=ini_cfg.SVR_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_4,x=1,y=8,width=19,text="Supervisor Timeout"}
local svr_timeout = NumberField{parent=net_c_4,x=20,y=8,width=7,default=ini_cfg.SVR_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=1,y=10,width=14,text="Pocket Timeout"}
local api_timeout = NumberField{parent=net_c_2,x=20,y=10,width=7,default=ini_cfg.API_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_4,x=1,y=10,width=14,text="Pocket Timeout"}
self.api_timeout = NumberField{parent=net_c_4,x=20,y=10,width=7,default=ini_cfg.API_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=net_c_2,x=28,y=8,height=4,width=7,text="seconds\n\nseconds",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=28,y=8,height=4,width=7,text="seconds\n\nseconds",fg_bg=g_lg_fg_bg}
local ct_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local ct_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_timeouts()
local svr_cto, api_cto = tonumber(svr_timeout.get_value()), tonumber(api_timeout.get_value())
local svr_cto, api_cto = tonumber(svr_timeout.get_value()), tonumber(self.api_timeout.get_value())
if not tmp_cfg.API_Enabled then api_cto = tmp_cfg.API_Timeout or 5 end
if svr_cto ~= nil and api_cto ~= nil then
tmp_cfg.SVR_Timeout, tmp_cfg.API_Timeout = svr_cto, api_cto
net_pane.set_value(3)
if tmp_cfg.WirelessModem then
net_pane.set_value(5)
else
tmp_cfg.TrustedRange = 0
tmp_cfg.AuthKey = ""
network.deinit_mac()
-- prep supervisor connection screen
tool_ctl.init_sv_connect_ui()
main_pane.set_value(3)
end
ct_err.hide(true)
else ct_err.show() end
end
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,text="Please set the trusted range below."}
TextBox{parent=net_c_3,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_5,x=1,y=1,text="Please set the wireless trusted range below."}
TextBox{parent=net_c_5,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents wireless connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_5,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local range = NumberField{parent=net_c_5,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local tr_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local tr_err = TextBox{parent=net_c_5,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_tr()
local range_val = tonumber(range.get_value())
if range_val ~= nil then
tmp_cfg.TrustedRange = range_val
comms.set_trusted_range(range_val)
net_pane.set_value(4)
net_pane.set_value(6)
tr_err.hide(true)
else tr_err.show() end
end
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_5,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_5,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
TextBox{parent=net_c_4,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_6,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
TextBox{parent=net_c_6,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for wireless security on multiplayer servers. All devices on the same wireless network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=1,y=11,text="Facility Auth Key"}
local key, _ = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
TextBox{parent=net_c_6,x=1,y=11,text="Auth Key (Wireless Only, Not Used for Wired)"}
local key, _ = TextField{parent=net_c_6,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
local function censor_key(enable) key.censor(tri(enable, "*", nil)) end
local hide_key = Checkbox{parent=net_c_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
local hide_key = Checkbox{parent=net_c_6,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
hide_key.set_value(true)
censor_key(true)
local key_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local key_err = TextBox{parent=net_c_6,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_auth()
local v = key.get_value()
@@ -174,8 +282,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
else key_err.show() end
end
PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_6,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_6,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
@@ -188,7 +296,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
@@ -230,10 +338,10 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
TextBox{parent=clr_c_1,x=1,y=7,text="Main UI Theme"}
local main_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.MainTheme,options=themes.UI_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
local main_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.MainTheme,options=themes.UI_THEME_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_1,x=18,y=7,text="Front Panel Theme"}
local fp_theme = RadioButton{parent=clr_c_1,x=18,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
local fp_theme = RadioButton{parent=clr_c_1,x=18,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will usually be split up."}
@@ -370,16 +478,21 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
load_settings(settings_cfg, true)
load_settings(ini_cfg)
try_set(wireless, ini_cfg.WirelessModem)
try_set(wired, ini_cfg.WiredModem ~= false)
try_set(self.wl_pref, ini_cfg.PreferWireless)
try_set(self.api_en, ini_cfg.API_Enabled)
try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(crd_chan, ini_cfg.CRD_Channel)
try_set(pkt_chan, ini_cfg.PKT_Channel)
try_set(self.pkt_chan, ini_cfg.PKT_Channel)
try_set(svr_timeout, ini_cfg.SVR_Timeout)
try_set(api_timeout, ini_cfg.API_Timeout)
try_set(self.api_timeout, ini_cfg.API_Timeout)
try_set(range, ini_cfg.TrustedRange)
try_set(key, ini_cfg.AuthKey)
try_set(tool_ctl.num_units, ini_cfg.UnitCount)
try_set(tool_ctl.dis_flow_view, ini_cfg.DisableFlowView)
try_set(tool_ctl.s_vol, ini_cfg.SpeakerVolume)
try_set(tool_ctl.pellet_color, ini_cfg.GreenPuPellet)
try_set(tool_ctl.clock_fmt, tri(ini_cfg.Time24Hour, 1, 2))
try_set(tool_ctl.temp_scale, ini_cfg.TempScale)
try_set(tool_ctl.energy_scale, ini_cfg.EnergyScale)
@@ -528,6 +641,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 +665,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
@@ -571,6 +686,59 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
end
end
-- generate the list of available/assigned wired modems
function tool_ctl.gen_modem_list()
modem_list.remove_all()
local enable = wired.get_value()
local function select(iface)
tmp_cfg.WiredModem = iface
tool_ctl.gen_modem_list()
end
local modems = ppm.get_wired_modem_list()
local missing = { tmp = true, ini = true }
for iface, _ in pairs(modems) do
if ini_cfg.WiredModem == iface then missing.ini = false end
if tmp_cfg.WiredModem == iface then missing.tmp = false end
end
if missing.tmp and tmp_cfg.WiredModem then
local line = Div{parent=modem_list,x=1,y=1,height=1}
TextBox{parent=line,x=1,y=1,width=4,text="Used",fg_bg=cpair(tri(enable,colors.blue,colors.gray),colors.white)}
PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}.disable()
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
TextBox{parent=line,x=25,y=1,text=tmp_cfg.WiredModem}
end
if missing.ini and ini_cfg.WiredModem and (tmp_cfg.WiredModem ~= ini_cfg.WiredModem) then
local line = Div{parent=modem_list,x=1,y=1,height=1}
local used = tmp_cfg.WiredModem == ini_cfg.WiredModem
TextBox{parent=line,x=1,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(ini_cfg.WiredModem)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
TextBox{parent=line,x=25,y=1,text=ini_cfg.WiredModem}
if used or not enable then select_btn.disable() end
end
-- list wired modems
for iface, _ in pairs(modems) do
local line = Div{parent=modem_list,x=1,y=1,height=1}
local used = tmp_cfg.WiredModem == iface
TextBox{parent=line,x=1,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(iface)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
TextBox{parent=line,x=15,y=1,text=iface}
if used or not enable then select_btn.disable() end
end
end
--#endregion
end

View File

@@ -35,7 +35,9 @@ 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" } },
{ "v1.7.0", { "Added support for wired communications modems", "Added option for allowing Pocket connections" } }
}
---@class crd_configurator
@@ -77,6 +79,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
@@ -87,7 +90,9 @@ local tool_ctl = {
is_int_min_max = nil, ---@type function
update_mon_reqs = nil, ---@type function
gen_mon_list = function () end
gen_mon_list = function () end,
gen_modem_list = function () end
}
---@class crd_config
@@ -95,12 +100,17 @@ local tmp_cfg = {
UnitCount = 1,
SpeakerVolume = 1.0,
Time24Hour = true,
GreenPuPellet = false,
TempScale = 1, ---@type TEMP_SCALE
EnergyScale = 1, ---@type ENERGY_SCALE
DisableFlowView = false,
MainDisplay = nil, ---@type string
FlowDisplay = nil, ---@type string
UnitDisplays = {}, ---@type string[]
WirelessModem = true,
WiredModem = false, ---@type string|false
PreferWireless = true,
API_Enabled = true,
SVR_Channel = nil, ---@type integer
CRD_Channel = nil, ---@type integer
PKT_Channel = nil, ---@type integer
@@ -129,9 +139,14 @@ 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 },
{ "WirelessModem", "Wireless/Ender Comms Modem", true },
{ "WiredModem", "Wired Comms Modem", false },
{ "PreferWireless", "Prefer Wireless Modem", true },
{ "API_Enabled", "Pocket API Connectivity", true },
{ "SVR_Channel", "SVR Channel", 16240 },
{ "CRD_Channel", "CRD Channel", 16243 },
{ "PKT_Channel", "PKT Channel", 16244 },
@@ -323,6 +338,9 @@ function configurator.configure(start_code, message)
-- copy in some important values to start with
preset_monitor_fields()
-- this needs to be initialized as it is used before being set
tmp_cfg.WiredModem = ini_cfg.WiredModem
reset_term()
ppm.mount_all()
@@ -337,6 +355,7 @@ function configurator.configure(start_code, message)
config_view(display)
tool_ctl.gen_mon_list()
tool_ctl.gen_modem_list()
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
@@ -360,8 +379,10 @@ function configurator.configure(start_code, message)
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1)
tool_ctl.gen_mon_list()
tool_ctl.gen_modem_list()
elseif event == "monitor_resize" then
tool_ctl.gen_mon_list()
tool_ctl.gen_modem_list()
elseif event == "modem_message" then
facility.receive_sv(param1, param2, param3, param4, param5)
end

View File

@@ -1,6 +1,5 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local types = require("scada-common.types")
@@ -29,15 +28,14 @@ local config = {}
coordinator.config = config
-- load the coordinator configuration<br>
-- status of 0 is OK, 1 is bad config, 2 is bad monitor config
---@return 0|1|2 status, nil|monitors_struct|string monitors (or error message)
-- load the coordinator configuration
function coordinator.load_config()
if not settings.load("/coordinator.settings") then return 1 end
if not settings.load("/coordinator.settings") then return false end
config.UnitCount = settings.get("UnitCount")
config.SpeakerVolume = settings.get("SpeakerVolume")
config.Time24Hour = settings.get("Time24Hour")
config.GreenPuPellet = settings.get("GreenPuPellet")
config.TempScale = settings.get("TempScale")
config.EnergyScale = settings.get("EnergyScale")
@@ -46,6 +44,10 @@ function coordinator.load_config()
config.FlowDisplay = settings.get("FlowDisplay")
config.UnitDisplays = settings.get("UnitDisplays")
config.WirelessModem = settings.get("WirelessModem")
config.WiredModem = settings.get("WiredModem")
config.PreferWireless = settings.get("PreferWireless")
config.API_Enabled = settings.get("API_Enabled")
config.SVR_Channel = settings.get("SVR_Channel")
config.CRD_Channel = settings.get("CRD_Channel")
config.PKT_Channel = settings.get("PKT_Channel")
@@ -67,6 +69,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)
@@ -78,6 +81,13 @@ function coordinator.load_config()
cfv.assert_type_num(config.SpeakerVolume)
cfv.assert_range(config.SpeakerVolume, 0, 3)
cfv.assert_type_bool(config.WirelessModem)
cfv.assert((config.WiredModem == false) or (type(config.WiredModem) == "string"))
cfv.assert(config.WirelessModem or (type(config.WiredModem) == "string"))
cfv.assert_type_bool(config.PreferWireless)
cfv.assert_type_bool(config.API_Enabled)
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.CRD_Channel)
cfv.assert_channel(config.PKT_Channel)
@@ -108,85 +118,7 @@ function coordinator.load_config()
cfv.assert_type_int(config.ColorMode)
cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
-- Monitor Setup
---@class monitors_struct
local monitors = {
main = nil, ---@type Monitor|nil
main_name = "",
flow = nil, ---@type Monitor|nil
flow_name = "",
unit_displays = {}, ---@type Monitor[]
unit_name_map = {} ---@type string[]
}
local mon_cfv = util.new_validator()
-- get all interface names
local names = {}
for iface, _ in pairs(ppm.get_monitor_list()) do table.insert(names, iface) end
local function setup_monitors()
mon_cfv.assert_type_str(config.MainDisplay)
if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end
mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount)
if mon_cfv.valid() then
local w, h, _
if not util.table_contains(names, config.MainDisplay) then
return 2, "Main monitor is not connected."
end
monitors.main = ppm.get_periph(config.MainDisplay)
monitors.main_name = config.MainDisplay
monitors.main.setTextScale(0.5)
w, _ = ppm.monitor_block_size(monitors.main.getSize())
if w ~= 8 then
return 2, util.c("Main monitor width is incorrect (was ", w, ", must be 8).")
end
if not config.DisableFlowView then
if not util.table_contains(names, config.FlowDisplay) then
return 2, "Flow monitor is not connected."
end
monitors.flow = ppm.get_periph(config.FlowDisplay)
monitors.flow_name = config.FlowDisplay
monitors.flow.setTextScale(0.5)
w, _ = ppm.monitor_block_size(monitors.flow.getSize())
if w ~= 8 then
return 2, util.c("Flow monitor width is incorrect (was ", w, ", must be 8).")
end
end
for i = 1, config.UnitCount do
local display = config.UnitDisplays[i]
if type(display) ~= "string" or not util.table_contains(names, display) then
return 2, "Unit " .. i .. " monitor is not connected."
end
monitors.unit_displays[i] = ppm.get_periph(display)
monitors.unit_name_map[i] = display
monitors.unit_displays[i].setTextScale(0.5)
w, h = ppm.monitor_block_size(monitors.unit_displays[i].getSize())
if w ~= 4 or h ~= 4 then
return 2, util.c("Unit ", i, " monitor size is incorrect (was ", w, " by ", h,", must be 4 by 4).")
end
end
else return 2, "Monitor configuration invalid." end
end
if cfv.valid() then
local ok, result, message = pcall(setup_monitors)
assert(ok, util.c("fatal error while trying to verify monitors: ", result))
if result == 2 then return 2, message end
else return 1 end
return 0, monitors
return cfv.valid()
end
-- dmesg print wrapper
@@ -230,9 +162,10 @@ end
-- coordinator communications
---@nodiscard
---@param version string coordinator version
---@param nic nic network interface device
---@param nic nic active network interface device
---@param wl_nic nic|nil pocket wireless network interface device
---@param sv_watchdog watchdog
function coordinator.comms(version, nic, sv_watchdog)
function coordinator.comms(version, nic, wl_nic, sv_watchdog)
local self = {
sv_linked = false,
sv_addr = comms.BROADCAST,
@@ -247,16 +180,16 @@ function coordinator.comms(version, nic, sv_watchdog)
est_task_done = nil
}
comms.set_trusted_range(config.TrustedRange)
-- configure network channels
nic.closeAll()
nic.open(config.CRD_Channel)
if config.WirelessModem then
comms.set_trusted_range(config.TrustedRange)
end
-- pass config to apisessions
apisessions.init(nic, config)
if config.API_Enabled and wl_nic then
apisessions.init(wl_nic, config)
end
-- PRIVATE FUNCTIONS --
--#region PRIVATE FUNCTIONS --
-- send a packet to the supervisor
---@param msg_type MGMT_TYPE|CRDN_TYPE
@@ -291,7 +224,8 @@ function coordinator.comms(version, nic, sv_watchdog)
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data })
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt)
---@diagnostic disable-next-line: need-check-nil
wl_nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt)
self.last_api_est_acks[packet.src_addr()] = ack
end
@@ -307,11 +241,20 @@ function coordinator.comms(version, nic, sv_watchdog)
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
end
-- PUBLIC FUNCTIONS --
--#endregion
--#region PUBLIC FUNCTIONS --
---@class coord_comms
local public = {}
-- switch the current active NIC
---@param act_nic nic
function public.switch_nic(act_nic)
public.close()
nic = act_nic
end
-- try to connect to the supervisor if not already linked
---@param abort boolean? true to print out cancel info if not linked (use on program terminate)
---@return boolean ok, boolean start_ui
@@ -380,6 +323,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)
@@ -454,7 +409,9 @@ function coordinator.comms(version, nic, sv_watchdog)
if l_chan ~= config.CRD_Channel then
log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == config.PKT_Channel then
if not self.sv_linked then
if not config.API_Enabled then
-- log.debug("discarding pocket API packet due to the API being disabled")
elseif not self.sv_linked then
log.debug("discarding pocket API packet before linked to supervisor")
elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
@@ -770,6 +727,8 @@ function coordinator.comms(version, nic, sv_watchdog)
---@nodiscard
function public.is_linked() return self.sv_linked end
--#endregion
return public
end

View File

@@ -34,18 +34,10 @@ local HIGH_RTT = 1500 -- 3.33x as long as expected w/ 0 ping
local iocontrol = {}
---@class ioctl
local io = {}
-- initialize front panel PSIL
---@param firmware_v string coordinator version
---@param comms_v string comms version
function iocontrol.init_fp(firmware_v, comms_v)
local io = {
---@class ioctl_front_panel
io.fp = { ps = psil.create() }
io.fp.ps.publish("version", firmware_v)
io.fp.ps.publish("comms_version", comms_v)
end
fp = { ps = psil.create() }
}
-- initialize the coordinator IO controller
---@param conf facility_conf configuration
@@ -132,7 +124,9 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
sps_data_tbl = {}, ---@type sps_session_db[]
tank_ps_tbl = {}, ---@type psil[]
tank_data_tbl = {} ---@type dynamicv_session_db[]
tank_data_tbl = {}, ---@type dynamicv_session_db[]
rad_monitors = {} ---@type { radiation: radiation_reading, raw: number }[]
}
-- create induction and SPS tables (currently only 1 of each is supported)
@@ -164,6 +158,7 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
num_turbines = 0,
num_snas = 0,
has_tank = conf.cooling.r_cool[i].TankConnection,
aux_coolant = conf.cooling.aux_coolant[i],
status_lines = { "", "" },
@@ -241,7 +236,9 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
turbine_data_tbl = {}, ---@type turbinev_session_db[]
tank_ps_tbl = {}, ---@type psil[]
tank_data_tbl = {} ---@type dynamicv_session_db[]
tank_data_tbl = {}, ---@type dynamicv_session_db[]
rad_monitors = {} ---@type { radiation: radiation_reading, raw: number }[]
}
-- on other facility modes, overwrite unit TANK option with facility tank defs
@@ -285,9 +282,21 @@ end
-- toggle heartbeat indicator
function iocontrol.heartbeat() io.fp.ps.toggle("heartbeat") end
-- report presence of the wireless modem
-- report versions to front panel
---@param firmware_v string coordinator version
---@param comms_v string comms version
function iocontrol.fp_versions(firmware_v, comms_v)
io.fp.ps.publish("version", firmware_v)
io.fp.ps.publish("comms_version", comms_v)
end
-- report presence of the wired comms modem
---@param has_modem boolean
function iocontrol.fp_has_modem(has_modem) io.fp.ps.publish("has_modem", has_modem) end
function iocontrol.fp_has_wd_modem(has_modem) io.fp.ps.publish("has_wd_modem", has_modem) end
-- report presence of the wireless comms modem
---@param has_modem boolean
function iocontrol.fp_has_wl_modem(has_modem) io.fp.ps.publish("has_wl_modem", has_modem) end
-- report presence of the speaker
---@param has_speaker boolean
@@ -299,6 +308,7 @@ function iocontrol.fp_link_state(state) io.fp.ps.publish("link_state", state) en
-- report monitor connection state
---@param id string|integer unit ID for unit monitor, "main" for main monitor, or "flow" for flow monitor
---@param connected 1|2|3 1 for disconnected, 2 for connected but no view (may not fit), 3 for connected with view rendered
function iocontrol.fp_monitor_state(id, connected)
local name = nil
@@ -495,6 +505,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
@@ -616,6 +669,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)
@@ -752,7 +806,9 @@ function iocontrol.update_facility_status(status)
if type(rtu_statuses.envds) == "table" then
local max_rad, max_reading, any_conn, any_faulted = 0, types.new_zero_radiation_reading(), false, false
for _, envd in pairs(rtu_statuses.envds) do
fac.rad_monitors = {}
for id, envd in pairs(rtu_statuses.envds) do
local rtu_faulted = envd[1] ---@type boolean
local radiation = envd[2] ---@type radiation_reading
local rad_raw = envd[3] ---@type number
@@ -764,6 +820,10 @@ function iocontrol.update_facility_status(status)
max_rad = rad_raw
max_reading = radiation
end
if not rtu_faulted then
fac.rad_monitors[id] = { radiation = radiation, raw = rad_raw }
end
end
if any_conn then
@@ -1054,9 +1114,12 @@ function iocontrol.update_unit_statuses(statuses)
if type(rtu_statuses.envds) == "table" then
local max_rad, max_reading, any_conn = 0, types.new_zero_radiation_reading(), false
for _, envd in pairs(rtu_statuses.envds) do
local radiation = envd[2] ---@type radiation_reading
local rad_raw = envd[3] ---@type number
unit.rad_monitors = {}
for id, envd in pairs(rtu_statuses.envds) do
local rtu_faulted = envd[1] ---@type boolean
local radiation = envd[2] ---@type radiation_reading
local rad_raw = envd[3] ---@type number
any_conn = true
@@ -1064,6 +1127,10 @@ function iocontrol.update_unit_statuses(statuses)
max_rad = rad_raw
max_reading = radiation
end
if not rtu_faulted then
unit.rad_monitors[id] = { radiation = radiation, raw = rad_raw }
end
end
if any_conn then
@@ -1170,7 +1237,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)
@@ -1181,6 +1248,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

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

View File

@@ -28,8 +28,9 @@ local renderer = {}
-- render engine
local engine = {
config = nil, ---@type crd_config
color_mode = 1, ---@type COLOR_MODE
monitors = nil, ---@type monitors_struct|nil
monitors = nil, ---@type crd_displays|nil
dmesg_window = nil, ---@type Window|nil
ui_ready = false,
fp_ready = false,
@@ -76,25 +77,18 @@ end
-- apply renderer configurations
---@param config crd_config
function renderer.configure(config)
style.set_themes(config.MainTheme, config.FrontPanelTheme, config.ColorMode)
engine.config = config
engine.color_mode = config.ColorMode
engine.disable_flow_view = config.DisableFlowView
end
-- link to the monitor peripherals
---@param monitors monitors_struct
function renderer.set_displays(monitors)
engine.monitors = monitors
-- report to front panel as connected
iocontrol.fp_monitor_state("main", engine.monitors.main ~= nil)
iocontrol.fp_monitor_state("flow", engine.monitors.flow ~= nil)
for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end
style.set_themes(config.MainTheme, config.FrontPanelTheme, config.ColorMode)
end
-- init all displays in use by the renderer
function renderer.init_displays()
---@param monitors crd_displays
function renderer.init_displays(monitors)
engine.monitors = monitors
-- init main and flow monitors
_init_display(engine.monitors.main)
if not engine.disable_flow_view then _init_display(engine.monitors.flow) end
@@ -137,8 +131,8 @@ 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}
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
engine.ui.front_panel = DisplayBox{window=term.current(),fg_bg=style.fp.root}
panel_view(engine.ui.front_panel, engine.config)
end)
if status then
@@ -199,6 +193,7 @@ function renderer.try_start_ui()
if engine.monitors.main ~= nil then
engine.ui.main_display = DisplayBox{window=engine.monitors.main,fg_bg=style.root}
main_view(engine.ui.main_display)
iocontrol.fp_monitor_state("main", 3)
util.nop()
end
@@ -206,6 +201,7 @@ function renderer.try_start_ui()
if engine.monitors.flow ~= nil then
engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root}
flow_view(engine.ui.flow_display)
iocontrol.fp_monitor_state("flow", 3)
util.nop()
end
@@ -213,6 +209,7 @@ function renderer.try_start_ui()
for idx, display in pairs(engine.monitors.unit_displays) do
engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
unit_view(engine.ui.unit_displays[idx], idx)
iocontrol.fp_monitor_state(idx, 3)
util.nop()
end
end)
@@ -239,9 +236,21 @@ function renderer.close_ui()
end
-- delete element trees
if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end
if engine.ui.flow_display ~= nil then engine.ui.flow_display.delete() end
for _, display in pairs(engine.ui.unit_displays) do display.delete() end
if engine.ui.main_display ~= nil then
engine.ui.main_display.delete()
iocontrol.fp_monitor_state("main", 2)
end
if engine.ui.flow_display ~= nil then
engine.ui.flow_display.delete()
iocontrol.fp_monitor_state("flow", 2)
end
for idx, display in pairs(engine.ui.unit_displays) do
display.delete()
iocontrol.fp_monitor_state(idx, 2)
end
-- report ui as not ready
engine.ui_ready = false
@@ -275,90 +284,51 @@ function renderer.fp_ready() return engine.fp_ready end
function renderer.ui_ready() return engine.ui_ready end
-- handle a monitor peripheral being disconnected
---@param device Monitor monitor
---@return boolean is_used if the monitor is one of the configured monitors
function renderer.handle_disconnect(device)
local is_used = false
---@param iface string monitor interface
function renderer.handle_disconnect(iface)
if not engine.monitors then return false end
if engine.monitors.main == device then
if engine.monitors.main_iface == iface then
if engine.ui.main_display ~= nil then
-- delete element tree and clear root UI elements
engine.ui.main_display.delete()
log_render("closed main view due to monitor disconnect")
end
is_used = true
engine.monitors.main = nil
engine.ui.main_display = nil
iocontrol.fp_monitor_state("main", false)
elseif engine.monitors.flow == device then
elseif engine.monitors.flow_iface == iface then
if engine.ui.flow_display ~= nil then
-- delete element tree and clear root UI elements
engine.ui.flow_display.delete()
log_render("closed flow view due to monitor disconnect")
end
is_used = true
engine.monitors.flow = nil
engine.ui.flow_display = nil
iocontrol.fp_monitor_state("flow", false)
else
for idx, monitor in pairs(engine.monitors.unit_displays) do
if monitor == device then
for idx, u_iface in pairs(engine.monitors.unit_ifaces) do
if u_iface == iface then
if engine.ui.unit_displays[idx] ~= nil then
-- delete element tree and clear root UI elements
engine.ui.unit_displays[idx].delete()
log_render("closed unit" .. idx .. "view due to monitor disconnect")
end
is_used = true
engine.monitors.unit_displays[idx] = nil
engine.ui.unit_displays[idx] = nil
iocontrol.fp_monitor_state(idx, false)
break
end
end
end
return is_used
end
-- handle a monitor peripheral being reconnected
---@param name string monitor name
---@param device Monitor monitor
---@return boolean is_used if the monitor is one of the configured monitors
function renderer.handle_reconnect(name, device)
local is_used = false
if not engine.monitors then return false end
function renderer.handle_reconnect(name)
-- note: handle_resize is a more adaptive way of re-initializing a connected monitor
-- since it can handle a monitor being reconnected that isn't the right size
if engine.monitors.main_name == name then
is_used = true
engine.monitors.main = device
renderer.handle_resize(name)
elseif engine.monitors.flow_name == name then
is_used = true
engine.monitors.flow = device
renderer.handle_resize(name)
else
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
if monitor == name then
is_used = true
engine.monitors.unit_displays[idx] = device
renderer.handle_resize(name)
break
end
end
end
return is_used
renderer.handle_resize(name)
end
-- handle a monitor being resized<br>
@@ -372,7 +342,7 @@ function renderer.handle_resize(name)
if not engine.monitors then return false, false end
if engine.monitors.main_name == name and engine.monitors.main then
if engine.monitors.main_iface == name and engine.monitors.main then
local device = engine.monitors.main ---@type Monitor
-- this is necessary if the bottom left block was broken and on reconnect
@@ -390,7 +360,7 @@ function renderer.handle_resize(name)
ui.main_display = nil
end
iocontrol.fp_monitor_state("main", true)
iocontrol.fp_monitor_state("main", 2)
engine.dmesg_window.setVisible(not engine.ui_ready)
@@ -402,6 +372,8 @@ function renderer.handle_resize(name)
end)
if ok then
iocontrol.fp_monitor_state("main", 3)
log_render("main view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
else
if ui.main_display then
@@ -411,11 +383,10 @@ function renderer.handle_resize(name)
_print_too_small(device)
iocontrol.fp_monitor_state("main", false)
is_ok = false
end
else engine.dmesg_window.redraw() end
elseif engine.monitors.flow_name == name and engine.monitors.flow then
elseif engine.monitors.flow_iface == name and engine.monitors.flow then
local device = engine.monitors.flow ---@type Monitor
-- this is necessary if the bottom left block was broken and on reconnect
@@ -428,7 +399,7 @@ function renderer.handle_resize(name)
ui.flow_display = nil
end
iocontrol.fp_monitor_state("flow", true)
iocontrol.fp_monitor_state("flow", 2)
if engine.ui_ready then
local draw_start = util.time_ms()
@@ -438,6 +409,8 @@ function renderer.handle_resize(name)
end)
if ok then
iocontrol.fp_monitor_state("flow", 3)
log_render("flow view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
else
if ui.flow_display then
@@ -447,12 +420,11 @@ function renderer.handle_resize(name)
_print_too_small(device)
iocontrol.fp_monitor_state("flow", false)
is_ok = false
end
end
else
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
for idx, monitor in ipairs(engine.monitors.unit_ifaces) do
local device = engine.monitors.unit_displays[idx]
if monitor == name and device then
@@ -466,7 +438,7 @@ function renderer.handle_resize(name)
ui.unit_displays[idx] = nil
end
iocontrol.fp_monitor_state(idx, true)
iocontrol.fp_monitor_state(idx, 2)
if engine.ui_ready then
local draw_start = util.time_ms()
@@ -476,6 +448,8 @@ function renderer.handle_resize(name)
end)
if ok then
iocontrol.fp_monitor_state(idx, 3)
log_render("unit " .. idx .. " view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
else
if ui.unit_displays[idx] then
@@ -485,7 +459,6 @@ function renderer.handle_resize(name)
_print_too_small(device)
iocontrol.fp_monitor_state(idx, false)
is_ok = false
end
end
@@ -505,12 +478,12 @@ function renderer.handle_mouse(event)
if engine.fp_ready and event.monitor == "terminal" then
engine.ui.front_panel.handle_mouse(event)
elseif engine.ui_ready then
if event.monitor == engine.monitors.main_name then
if event.monitor == engine.monitors.main_iface then
if engine.ui.main_display then engine.ui.main_display.handle_mouse(event) end
elseif event.monitor == engine.monitors.flow_name then
elseif event.monitor == engine.monitors.flow_iface then
if engine.ui.flow_display then engine.ui.flow_display.handle_mouse(event) end
else
for id, monitor in ipairs(engine.monitors.unit_name_map) do
for id, monitor in ipairs(engine.monitors.unit_ifaces) do
local display = engine.ui.unit_displays[id]
if event.monitor == monitor and display then
if display then display.handle_mouse(event) end

View File

@@ -69,7 +69,7 @@ end
-- PUBLIC FUNCTIONS --
-- initialize apisessions
---@param nic nic network interface
---@param nic nic API network interface
---@param config crd_config coordinator config
function apisessions.init(nic, config)
self.nic = nic

View File

@@ -260,11 +260,52 @@ 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]]
@@ -386,6 +427,13 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
}
_send(CRDN_TYPE.API_GET_WASTE, data)
elseif pkt.type == CRDN_TYPE.API_GET_RAD then
local data = {}
for i = 1, #db.units do data[i] = db.units[i].rad_monitors end
data[#db.units + 1] = db.facility.rad_monitors
_send(CRDN_TYPE.API_GET_RAD, data)
else
log.debug(log_tag .. "handler received unsupported CRDN packet type " .. pkt.type)
end

View File

@@ -12,6 +12,7 @@ local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local backplane = require("coordinator.backplane")
local configure = require("coordinator.configure")
local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol")
@@ -19,7 +20,7 @@ local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder")
local threads = require("coordinator.threads")
local COORDINATOR_VERSION = "v1.6.2"
local COORDINATOR_VERSION = "v1.7.0"
local CHUNK_LOAD_DELAY_S = 30.0
@@ -36,45 +37,13 @@ local log_crypto = coordinator.log_crypto
-- get configuration
----------------------------------------
-- mount connected devices (required for monitor setup)
ppm.mount_all()
local wait_on_load = true
local loaded, monitors = coordinator.load_config()
-- if the computer just started, its chunk may have just loaded (...or the user rebooted)
-- if monitor config failed, maybe an adjacent chunk containing all or part of a monitor has not loaded yet, so keep trying
while wait_on_load and loaded == 2 and os.clock() < CHUNK_LOAD_DELAY_S do
term.clear()
term.setCursorPos(1, 1)
println("There was a monitor configuration problem at boot.\n")
println("Startup will keep trying every 2s in case of chunk load delays.\n")
println(util.sprintf("The configurator will be started in %ds if all attempts fail.\n", math.max(0, CHUNK_LOAD_DELAY_S - os.clock())))
println("(click to skip to the configurator)")
local timer_id = util.start_timer(2)
while true do
local event, param1 = util.pull_event()
if event == "timer" and param1 == timer_id then
-- remount and re-attempt
ppm.mount_all()
loaded, monitors = coordinator.load_config()
break
elseif event == "mouse_click" or event == "terminate" then
wait_on_load = false
break
end
end
end
if loaded ~= 0 then
-- first pass configuration check before validating monitors
if not coordinator.load_config() then
-- try to reconfigure (user action)
local success, error = configure.configure(loaded, monitors)
local success, error = configure.configure(1)
if success then
loaded, monitors = coordinator.load_config()
if loaded ~= 0 then
println(util.trinary(loaded == 2, "monitor configuration invalid", "failed to load a valid configuration") .. ", please reconfigure")
if not coordinator.load_config() then
println("failed to load a valid configuration, please reconfigure")
return
end
else
@@ -83,9 +52,6 @@ if loaded ~= 0 then
end
end
-- passed checks, good now
---@cast monitors monitors_struct
local config = coordinator.config
----------------------------------------
@@ -102,6 +68,65 @@ println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<")
crash.set_env("coordinator", COORDINATOR_VERSION)
crash.dbg_log_env()
----------------------------------------
-- display init
----------------------------------------
-- mount connected devices (required for monitor setup)
ppm.mount_all()
local wait_on_load = true
local disp_ok, disp_err = backplane.init_displays(config)
-- if the computer just started, its chunk may have just loaded (...or the user rebooted)
-- if monitor config failed, maybe an adjacent chunk containing all or part of a monitor has not loaded yet, so keep trying
while wait_on_load and (not disp_ok) and os.clock() < CHUNK_LOAD_DELAY_S do
term.clear()
term.setCursorPos(1, 1)
println("There was a monitor configuration problem at boot.\n")
println("Startup will keep trying every 2s in case of chunk load delays.\n")
println(util.sprintf("The configurator will be started in %ds if all attempts fail.\n", math.max(0, CHUNK_LOAD_DELAY_S - os.clock())))
println("(click to skip to the configurator)")
local timer_id = util.start_timer(2)
while true do
local event, param1 = util.pull_event()
if event == "timer" and param1 == timer_id then
-- remount and re-attempt
ppm.mount_all()
disp_ok, disp_err = backplane.init_displays(config)
break
elseif event == "mouse_click" or event == "terminate" then
wait_on_load = false
break
end
end
end
if not disp_ok then
-- try to reconfigure (user action)
local success, error = configure.configure(2, disp_err)
if success then
if not coordinator.load_config() then
println("failed to load a valid configuration, please reconfigure")
return
else
disp_ok, disp_err = backplane.init_displays(config)
if not disp_ok then
println(disp_err)
println("please reconfigure")
return
end
end
else
println("configuration error: " .. error)
return
end
end
----------------------------------------
-- main application
----------------------------------------
@@ -111,16 +136,12 @@ local function main()
-- system startup
----------------------------------------
-- log mounts now since mounting was done before logging was ready
ppm.log_mounts()
-- report versions/init fp PSIL
iocontrol.init_fp(COORDINATOR_VERSION, comms.version)
-- report versions
iocontrol.fp_versions(COORDINATOR_VERSION, comms.version)
-- init renderer
renderer.configure(config)
renderer.set_displays(monitors)
renderer.init_displays()
renderer.init_displays(backplane.displays())
renderer.init_dmesg()
-- lets get started!
@@ -130,6 +151,12 @@ local function main()
log_sys("system start on " .. os.date("%c"))
log_boot("starting " .. COORDINATOR_VERSION)
-- message authentication init
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
local init_time = network.init_mac(config.AuthKey)
log_crypto("HMAC init took " .. init_time .. "ms")
end
----------------------------------------
-- memory allocation
----------------------------------------
@@ -149,15 +176,9 @@ local function main()
shutdown = false
},
-- core coordinator devices
crd_dev = {
modem = ppm.get_wireless_modem(),
speaker = ppm.get_device("speaker") ---@type Speaker|nil
},
-- system objects
---@class crd_sys
crd_sys = {
nic = nil, ---@type nic
coord_comms = nil, ---@type coord_comms
conn_watchdog = nil ---@type watchdog
},
@@ -165,68 +186,33 @@ local function main()
-- message queues
q = {
mq_render = mqueue.new()
},
-- message queue message types
q_types = {
MQ__RENDER_CMD = {
START_MAIN_UI = 1,
CLOSE_MAIN_UI = 2
},
MQ__RENDER_DATA = {
MON_CONNECT = 1,
MON_DISCONNECT = 2,
MON_RESIZE = 3
}
}
}
local smem_dev = __shared_memory.crd_dev
local smem_sys = __shared_memory.crd_sys
local smem_sys = __shared_memory.crd_sys
local crd_state = __shared_memory.crd_state
----------------------------------------
-- setup alarm sounder subsystem
-- init system
----------------------------------------
if smem_dev.speaker == nil then
log_boot("annunciator alarm speaker not found")
println("startup> speaker not found")
log.fatal("no annunciator alarm speaker found")
return
else
local sounder_start = util.time_ms()
log_boot("annunciator alarm speaker connected")
sounder.init(smem_dev.speaker, config.SpeakerVolume)
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
log_sys("annunciator alarm configured")
iocontrol.fp_has_speaker(true)
end
-- modem and speaker initialization
if not backplane.init(config, __shared_memory) then return end
----------------------------------------
-- setup communications
----------------------------------------
-- message authentication init
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
local init_time = network.init_mac(config.AuthKey)
log_crypto("HMAC init took " .. init_time .. "ms")
end
-- get the communications modem
if smem_dev.modem == nil then
log_comms("wireless modem not found")
println("startup> wireless modem not found")
log.fatal("no wireless modem on startup")
return
else
log_comms("wireless modem connected")
iocontrol.fp_has_modem(true)
end
-- create connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.SVR_Timeout)
smem_sys.conn_watchdog.cancel()
log.debug("startup> conn watchdog created")
-- create network interface then setup comms
smem_sys.nic = network.nic(smem_dev.modem)
smem_sys.coord_comms = coordinator.comms(COORDINATOR_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
log.debug("startup> comms init")
log_comms("comms initialized")
----------------------------------------
-- start front panel
----------------------------------------
log_render("starting front panel UI...")
local fp_message
@@ -238,6 +224,16 @@ local function main()
return
else log_render("front panel ready") end
-- create connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.SVR_Timeout)
smem_sys.conn_watchdog.cancel()
log.debug("startup> conn watchdog created")
-- setup comms
smem_sys.coord_comms = coordinator.comms(COORDINATOR_VERSION, backplane.active_nic(), backplane.wireless_nic(), smem_sys.conn_watchdog)
log.debug("startup> comms init")
log_comms("comms initialized")
----------------------------------------
-- start system
----------------------------------------

View File

@@ -4,6 +4,7 @@ local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local backplane = require("coordinator.backplane")
local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
@@ -20,19 +21,8 @@ local log_comms = coordinator.log_comms
local threads = {}
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
local MQ__RENDER_CMD = {
START_MAIN_UI = 1,
CLOSE_MAIN_UI = 2
}
local MQ__RENDER_DATA = {
MON_CONNECT = 1,
MON_DISCONNECT = 2,
MON_RESIZE = 3
}
local MAIN_CLOCK = 0.5 -- 2Hz, 10 ticks
local RENDER_SLEEP = 100 -- 100ms, 2 ticks
-- main thread
---@nodiscard
@@ -44,7 +34,7 @@ function threads.thread__main(smem)
-- execute thread
function public.exec()
iocontrol.fp_rt_status("main", true)
log.debug("main thread start")
log.debug("OS: main thread start")
local loop_clock = util.new_clock(MAIN_CLOCK)
@@ -54,10 +44,12 @@ function threads.thread__main(smem)
log_sys("system started successfully")
-- load in from shared memory
local crd_state = smem.crd_state
local nic = smem.crd_sys.nic
local coord_comms = smem.crd_sys.coord_comms
local conn_watchdog = smem.crd_sys.conn_watchdog
local crd_state = smem.crd_state
local coord_comms = smem.crd_sys.coord_comms
local conn_watchdog = smem.crd_sys.conn_watchdog
local MQ__RENDER_CMD = smem.q_types.MQ__RENDER_CMD
local MQ__RENDER_DATA = smem.q_types.MQ__RENDER_DATA
-- event loop
while true do
@@ -66,66 +58,13 @@ function threads.thread__main(smem)
-- handle event
if event == "peripheral_detach" then
local type, device = ppm.handle_unmount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
---@cast device Modem
-- we only really care if this is our wireless modem
-- if it is another modem, handle other peripheral losses separately
if nic.is_modem(device) then
nic.disconnect()
log_sys("comms modem disconnected")
local other_modem = ppm.get_wireless_modem()
if other_modem then
log_sys("found another wireless modem, using it for comms")
nic.connect(other_modem)
else
-- close out main UI
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
-- alert user to status
log_sys("awaiting comms modem reconnect...")
iocontrol.fp_has_modem(false)
end
else
log_sys("non-comms modem disconnected")
end
elseif type == "monitor" then
---@cast device Monitor
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, device)
elseif type == "speaker" then
---@cast device Speaker
log_sys("lost alarm sounder speaker")
iocontrol.fp_has_speaker(false)
end
backplane.detach(type, device, param1)
end
elseif event == "peripheral" then
local type, device = ppm.mount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
---@cast device Modem
if device.isWireless() and not nic.is_connected() then
-- reconnected modem
log_sys("comms modem reconnected")
nic.connect(device)
iocontrol.fp_has_modem(true)
elseif device.isWireless() then
log.info("unused wireless modem reconnected")
else
log_sys("wired modem reconnected")
end
elseif type == "monitor" then
---@cast device Monitor
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, { name = param1, device = device })
elseif type == "speaker" then
---@cast device Speaker
log_sys("alarm sounder speaker reconnected")
sounder.reconnect(device)
iocontrol.fp_has_speaker(true)
end
backplane.attach(type, device, param1)
end
elseif event == "monitor_resize" then
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_RESIZE, param1)
@@ -137,18 +76,16 @@ function threads.thread__main(smem)
iocontrol.heartbeat()
-- maintain connection
if nic.is_connected() then
local ok, start_ui = coord_comms.try_connect()
if not ok then
crd_state.link_fail = true
crd_state.shutdown = true
log_sys("supervisor connection failed, shutting down...")
log.fatal("failed to connect to supervisor")
break
elseif start_ui then
log_sys("supervisor connected, dispatching main UI start")
smem.q.mq_render.push_command(MQ__RENDER_CMD.START_MAIN_UI)
end
local ok, start_ui = coord_comms.try_connect()
if not ok then
crd_state.link_fail = true
crd_state.shutdown = true
log_sys("supervisor connection failed, shutting down...")
log.fatal("failed to connect to supervisor")
break
elseif start_ui then
log_sys("supervisor connected, dispatching main UI start")
smem.q.mq_render.push_command(MQ__RENDER_CMD.START_MAIN_UI)
end
-- iterate sessions and free any closed ones
@@ -206,10 +143,10 @@ function threads.thread__main(smem)
-- check for termination request or UI crash
if event == "terminate" or ppm.should_terminate() then
crd_state.shutdown = true
log.info("terminate requested, main thread exiting")
log.info("OS: terminate requested, main thread exiting")
elseif not crd_state.ui_ok then
crd_state.shutdown = true
log.info("terminating due to fatal UI error")
log.info("OS: terminating due to fatal UI error")
end
if crd_state.shutdown then
@@ -247,7 +184,7 @@ function threads.thread__main(smem)
-- if status is true, then we are probably exiting, so this won't matter
-- this thread cannot be slept because it will miss events (namely "terminate")
if not crd_state.shutdown then
log.info("main thread restarting now...")
log.info("OS: main thread restarting now...")
end
end
end
@@ -265,11 +202,14 @@ function threads.thread__render(smem)
-- execute thread
function public.exec()
iocontrol.fp_rt_status("render", true)
log.debug("render thread start")
log.debug("OS: render thread start")
-- load in from shared memory
local crd_state = smem.crd_state
local render_queue = smem.q.mq_render
local crd_state = smem.crd_state
local render_queue = smem.q.mq_render
local MQ__RENDER_CMD = smem.q_types.MQ__RENDER_CMD
local MQ__RENDER_DATA = smem.q_types.MQ__RENDER_DATA
local last_update = util.time()
@@ -317,18 +257,10 @@ function threads.thread__render(smem)
if cmd.key == MQ__RENDER_DATA.MON_CONNECT then
-- monitor connected
if renderer.handle_reconnect(cmd.val.name, cmd.val.device) then
log_sys(util.c("configured monitor ", cmd.val.name, " reconnected"))
else
log_sys(util.c("unused monitor ", cmd.val.name, " connected"))
end
renderer.handle_reconnect(cmd.val)
elseif cmd.key == MQ__RENDER_DATA.MON_DISCONNECT then
-- monitor disconnected
if renderer.handle_disconnect(cmd.val) then
log_sys("lost a configured monitor")
else
log_sys("lost an unused monitor")
end
renderer.handle_disconnect(cmd.val)
elseif cmd.key == MQ__RENDER_DATA.MON_RESIZE then
-- monitor resized
local is_used, is_ok = renderer.handle_resize(cmd.val)
@@ -347,7 +279,7 @@ function threads.thread__render(smem)
-- check for termination request
if crd_state.shutdown then
log.info("render thread exiting")
log.info("OS: render thread exiting")
break
end
@@ -369,7 +301,7 @@ function threads.thread__render(smem)
iocontrol.fp_rt_status("render", false)
if not crd_state.shutdown then
log.info("render thread restarting in 5 seconds...")
log.info("OS: render thread restarting in 5 seconds...")
util.psleep(5)
end
end

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

@@ -246,7 +246,7 @@ local function new_view(root, x, y)
-------------------------
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
mode.register(facility.ps, "process_mode", mode.set_value)
@@ -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)
@@ -486,7 +486,7 @@ local function init(parent, id)
local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37}
local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1}
local group = RadioButton{parent=auto_div,options=types.AUTO_GROUP_NAMES,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
local group = RadioButton{parent=auto_div,options=types.AUTO_GROUP_NAMES,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end)

View File

@@ -59,7 +59,7 @@ local function make(parent, x, y, wide, unit_id)
local tank_conns = facility.tank_conns
local tank_types = facility.tank_fluid_types
local v_start = 1 + ((unit.unit_id - 1) * 5)
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 = {
@@ -94,11 +94,21 @@ local function make(parent, x, y, wide, unit_id)
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
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
@@ -169,12 +179,12 @@ local function make(parent, x, y, wide, unit_id)
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),
@@ -222,17 +232,21 @@ local function make(parent, x, y, wide, unit_id)
_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

@@ -268,7 +268,7 @@ local function init(main)
for i = 1, facility.num_units do
local y_offset = y_ofs(i)
unit_flow(main, flow_x, 5 + y_offset, #emcool_pipes == 0, i)
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.cyan, true, true))
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.green, true, true))
util.nop()
end
@@ -286,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)
@@ -294,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 --
-------------------

View File

@@ -17,6 +17,7 @@ local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local TabBar = require("graphics.elements.controls.TabBar")
@@ -30,15 +31,20 @@ local LINK_STATE = types.PANEL_LINK_STATE
local ALIGN = core.ALIGN
local cpair = core.cpair
local border = core.border
local led_grn = style.led_grn
-- create new front panel view
---@param panel DisplayBox main displaybox
---@param num_units integer number of units (number of unit monitors)
local function init(panel, num_units)
---@param config crd_config configuration
local function init(panel, config)
local s_hi_box = style.fp_theme.highlight_box
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}
@@ -58,10 +64,18 @@ local function init(panel, num_units)
heartbeat.register(ps, "heartbeat", heartbeat.update)
local modem = LED{parent=system,label="MODEM",colors=led_grn}
if config.WirelessModem and config.WiredModem then
local wd_modem = LED{parent=system,label="WD MODEM",colors=led_grn}
local wl_modem = LED{parent=system,label="WL MODEM",colors=led_grn}
wd_modem.register(ps, "has_wd_modem", wd_modem.update)
wl_modem.register(ps, "has_wl_modem", wl_modem.update)
else
local modem = LED{parent=system,label="MODEM",colors=led_grn}
modem.register(ps, util.trinary(config.WirelessModem, "has_wl_modem", "has_wd_modem"), modem.update)
end
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
@@ -95,48 +109,44 @@ local function init(panel, num_units)
system.line_break()
modem.register(ps, "has_modem", modem.update)
local speaker = LED{parent=system,label="SPEAKER",colors=led_grn}
speaker.register(ps, "has_speaker", speaker.update)
system.line_break()
local rt_main = LED{parent=system,label="RT MAIN",colors=led_grn}
local rt_render = LED{parent=system,label="RT RENDER",colors=led_grn}
rt_main.register(ps, "routine__main", rt_main.update)
rt_render.register(ps, "routine__render", rt_render.update)
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=style.fp.disabled_fg}
local hmi_devs = Div{parent=main_page,width=16,height=17,x=18,y=2}
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
local speaker = LED{parent=hmi_devs,label="SPEAKER",colors=led_grn}
speaker.register(ps, "has_speaker", speaker.update)
local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=led_grn}
main_monitor.register(ps, "main_monitor", main_monitor.update)
hmi_devs.line_break()
local flow_monitor = LED{parent=monitors,label="FLOW MONITOR",colors=led_grn}
flow_monitor.register(ps, "flow_monitor", flow_monitor.update)
local main_disp = LEDPair{parent=hmi_devs,label="MAIN DISPLAY",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
main_disp.register(ps, "main_monitor", main_disp.update)
monitors.line_break()
local flow_disp = LEDPair{parent=hmi_devs,label="FLOW DISPLAY",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
flow_disp.register(ps, "flow_monitor", flow_disp.update)
for i = 1, num_units do
local unit_monitor = LED{parent=monitors,label="UNIT "..i.." MONITOR",colors=led_grn}
unit_monitor.register(ps, "unit_monitor_" .. i, unit_monitor.update)
hmi_devs.line_break()
for i = 1, config.UnitCount do
local unit_disp = LEDPair{parent=hmi_devs,label="UNIT "..i.." DISPLAY",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
unit_disp.register(ps, "unit_monitor_" .. i, unit_disp.update)
end
--
-- about footer
-- hardware labeling
--
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 hw_labels = Rectangle{parent=main_page,x=2,y=term_h-7,width=14,height=5,border=border(1,s_hi_box.bkg,true),even_inner=true}
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)
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("%03d", os.getComputerID())
TextBox{parent=hw_labels,text="FW "..ps.get("version"),fg_bg=s_hi_box}
TextBox{parent=hw_labels,text="NT v"..ps.get("comms_version"),fg_bg=s_hi_box}
TextBox{parent=hw_labels,text="SN "..comp_id.."-CRD",fg_bg=s_hi_box}
--
-- page handling
@@ -145,7 +155,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
@@ -223,27 +227,34 @@ style.sps = {
}
}
style.waste = {
-- auto waste processing states
states = {
{ color = cpair(colors.black, colors.green), text = "PLUTONIUM" },
{ color = cpair(colors.black, colors.cyan), text = "POLONIUM" },
{ color = cpair(colors.black, colors.purple), text = "ANTI MATTER" }
},
states_abbrv = {
{ color = cpair(colors.black, colors.green), text = "Pu" },
{ color = cpair(colors.black, colors.cyan), text = "Po" },
{ color = cpair(colors.black, colors.purple), text = "AM" }
},
-- process radio button options
options = { "Plutonium", "Polonium", "Antimatter" },
-- unit waste selection
unit_opts = {
{ text = "Auto", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.white, colors.gray) },
{ text = "Pu", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.green) },
{ text = "Po", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.cyan) },
{ text = "AM", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.purple) }
-- 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.7"
core.version = "2.4.8"
core.flasher = flasher
core.events = events

View File

@@ -86,6 +86,13 @@ return function (args)
e.redraw()
end
-- change the foreground color of the text
---@param c color
function e.recolor(c)
e.w_set_fgd(c)
e.redraw()
end
---@class TextBox:graphics_element
local TextBox, id = e.complete(true)

View File

@@ -102,7 +102,7 @@ return function (args)
end
-- set the value
---@param val integer new value
---@param val boolean new value
function e.set_value(val)
e.value = val
draw()

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"},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,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,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
@@ -197,7 +216,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
TextBox{parent=log_c_1,x=1,y=1,text="Configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=24,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
@@ -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
@@ -64,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
@@ -84,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 },

View File

@@ -17,7 +17,6 @@ local ENERGY_UNITS = types.ENERGY_SCALE_UNITS
local TEMP_SCALE = types.TEMP_SCALE
local TEMP_UNITS = types.TEMP_SCALE_UNITS
---@todo nominal trip time is ping (0ms to 10ms usually)
local WARN_TT = 40
local HIGH_TT = 80
@@ -35,8 +34,9 @@ iocontrol.LINK_STATE = LINK_STATE
---@class pocket_ioctl
local io = {
version = "unknown",
ps = psil.create()
version = "unknown", -- pocket version
ps = psil.create(), -- pocket PSIL
loader_require = { sv = false, api = false }
}
local config = nil ---@type pkt_config
@@ -85,19 +85,22 @@ function iocontrol.init_core(pkt_comms, nav, cfg)
get_tone_states = function () comms.diag__get_alarm_tones() end,
ready_warn = nil, ---@type TextBox
tone_buttons = {}, ---@type SwitchButton[]
alarm_buttons = {}, ---@type Checkbox[]
tone_indicators = {} ---@type IndicatorLight[] indicators to update from supervisor tone states
tone_buttons = {}, ---@type SwitchButton[]
alarm_buttons = {} ---@type Checkbox[]
}
-- computer list
io.diag.get_comps = function () comms.diag__get_computers() end
-- 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_waste = function () comms.api__get_waste() end
get_waste = function () comms.api__get_waste() end,
get_rad = function () comms.api__get_rad() end
}
end
@@ -183,7 +186,9 @@ function iocontrol.init_fac(conf)
sps_data_tbl = {}, ---@type sps_session_db[]
tank_ps_tbl = {}, ---@type psil[]
tank_data_tbl = {} ---@type dynamicv_session_db[]
tank_data_tbl = {}, ---@type dynamicv_session_db[]
rad_monitors = {} ---@type { radiation: radiation_reading, raw: number }[]
}
-- create induction and SPS tables (currently only 1 of each is supported)
@@ -192,6 +197,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
@@ -255,7 +268,9 @@ function iocontrol.init_fac(conf)
turbine_data_tbl = {}, ---@type turbinev_session_db[]
tank_ps_tbl = {}, ---@type psil[]
tank_data_tbl = {} ---@type dynamicv_session_db[]
tank_data_tbl = {}, ---@type dynamicv_session_db[]
rad_monitors = {} ---@type { radiation: radiation_reading, raw: number }[]
}
-- on other facility modes, overwrite unit TANK option with facility tank defs

View File

@@ -2,16 +2,21 @@
-- I/O Control's Data Receive (Rx) Handlers
--
local comms = require("scada-common.comms")
local const = require("scada-common.constants")
local types = require("scada-common.types")
local util = require("scada-common.util")
local DEV_TYPE = comms.DEVICE_TYPE
local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE
local BLR_STATE = types.BOILER_STATE
local TRB_STATE = types.TURBINE_STATE
local TNK_STATE = types.TANK_STATE
local MTX_STATE = types.IMATRIX_STATE
local SPS_STATE = types.SPS_STATE
local io ---@type pocket_ioctl
local iorx = {} ---@class iorx
@@ -55,6 +60,10 @@ local function _record_multiblock_status(faulted, data, ps)
ps.publish("formed", data.formed)
ps.publish("faulted", faulted)
if data.build then
for key, val in pairs(data.build) do ps.publish(key, val) end
end
for key, val in pairs(data.state) do ps.publish(key, val) end
for key, val in pairs(data.tanks) do ps.publish(key, val) end
end
@@ -307,7 +316,7 @@ function iorx.record_unit_data(data)
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
-- for k, _ in pairs(unit.alarms) do
-- unit.alarms[k] = ALARM_STATE.TRIPPED
-- end
-- end
@@ -647,10 +656,299 @@ function iorx.record_waste_data(data)
fac.ps.publish("po_am_rate", fac.waste_stats[5])
fac.ps.publish("spent_waste_rate", fac.waste_stats[6])
fac.ps.publish("sps_computed_status", f_data[8])
fac.sps_ps_tbl[1].publish("SPSStateStatus", f_data[8])
fac.ps.publish("sps_process_rate", f_data[9])
end
-- update facility app with facility and unit data from API_GET_FAC_DTL
---@param data table
function iorx.record_fac_detail_data(data)
local fac = io.facility
local tank_statuses = data[5]
local next_t_stat = 1
-- annunciator
fac.all_sys_ok = data[1]
fac.rtu_count = data[2]
fac.auto_scram = data[3]
fac.ascram_status = data[4]
fac.ps.publish("all_sys_ok", fac.all_sys_ok)
fac.ps.publish("rtu_count", fac.rtu_count)
fac.ps.publish("auto_scram", fac.auto_scram)
fac.ps.publish("as_matrix_fault", fac.ascram_status.matrix_fault)
fac.ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill)
fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm)
fac.ps.publish("as_radiation", fac.ascram_status.radiation)
fac.ps.publish("as_gen_fault", fac.ascram_status.gen_fault)
-- unit data
local units = data[12]
for i = 1, io.facility.num_units do
local unit = io.units[i]
local u_rx = units[i]
unit.connected = u_rx[1]
unit.annunciator = u_rx[2]
unit.reactor_data = u_rx[3]
local control_status = 1
if unit.connected then
if unit.reactor_data.rps_tripped then control_status = 2 end
if unit.reactor_data.mek_status.status then
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
end
end
unit.unit_ps.publish("U_ControlStatus", control_status)
unit.tank_data_tbl = u_rx[4]
for id = 1, #unit.tank_data_tbl do
local tank = unit.tank_data_tbl[id]
local ps = unit.tank_ps_tbl[id]
local c_stat = tank_statuses[next_t_stat]
local tank_status = 1
if c_stat ~= TNK_STATE.OFFLINE then
if c_stat == TNK_STATE.FAULT then
tank_status = 3
elseif tank.formed then
tank_status = 4
else
tank_status = 2
end
end
ps.publish("DynamicTankStatus", tank_status)
ps.publish("DynamicTankStateStatus", c_stat)
next_t_stat = next_t_stat + 1
end
end
-- facility dynamic tank data
fac.tank_data_tbl = data[6]
for id = 1, #fac.tank_data_tbl do
local tank = fac.tank_data_tbl[id]
local ps = fac.tank_ps_tbl[id]
local c_stat = tank_statuses[next_t_stat]
local tank_status = 1
if c_stat ~= TNK_STATE.OFFLINE then
if c_stat == TNK_STATE.FAULT then
tank_status = 3
elseif tank.formed then
tank_status = 4
else
tank_status = 2
end
_record_multiblock_status(c_stat == TNK_STATE.FAULT, tank, ps)
end
ps.publish("DynamicTankStatus", tank_status)
ps.publish("DynamicTankStateStatus", c_stat)
next_t_stat = next_t_stat + 1
end
-- induction matrix data
fac.induction_data_tbl[1] = data[8]
local matrix = fac.induction_data_tbl[1]
local m_ps = fac.induction_ps_tbl[1]
local m_stat = data[7]
local mtx_status = 1
if m_stat ~= MTX_STATE.OFFLINE then
if m_stat == MTX_STATE.FAULT then
mtx_status = 3
elseif matrix.formed then
mtx_status = 4
else
mtx_status = 2
end
_record_multiblock_status(m_stat == MTX_STATE.FAULT, matrix, m_ps)
end
m_ps.publish("InductionMatrixStatus", mtx_status)
m_ps.publish("InductionMatrixStateStatus", m_stat)
m_ps.publish("eta_string", data[9][1])
m_ps.publish("avg_charge", data[9][2])
m_ps.publish("avg_inflow", data[9][3])
m_ps.publish("avg_outflow", data[9][4])
m_ps.publish("is_charging", data[9][5])
m_ps.publish("is_discharging", data[9][6])
m_ps.publish("at_max_io", data[9][7])
-- sps data
fac.sps_data_tbl[1] = data[11]
local sps = fac.sps_data_tbl[1]
local s_ps = fac.sps_ps_tbl[1]
local s_stat = data[10]
local sps_status = 1
if s_stat ~= SPS_STATE.OFFLINE then
if s_stat == SPS_STATE.FAULT then
sps_status = 3
elseif sps.formed then
sps_status = 4
else
sps_status = 2
end
_record_multiblock_status(s_stat == SPS_STATE.FAULT, sps, s_ps)
end
s_ps.publish("SPSStatus", sps_status)
s_ps.publish("SPSStateStatus", s_stat)
end
-- update the radiation monitor app with radiation monitor data from API_GET_RAD
---@param data table
function iorx.record_radiation_data(data)
-- unit radiation monitors
for u_id = 1, #io.units do
local unit = io.units[u_id]
local max_rad = 0
local connected = {}
unit.radiation = types.new_zero_radiation_reading()
unit.rad_monitors = data[u_id]
for id, mon in pairs(unit.rad_monitors) do
table.insert(connected, id)
unit.unit_ps.publish("radiation@" .. id, mon.radiation)
if mon.raw > max_rad then
max_rad = mon.raw
unit.radiation = mon.radiation
end
end
unit.unit_ps.publish("radiation", unit.radiation)
unit.unit_ps.publish("radiation_monitors", textutils.serialize(connected))
end
-- facility radiation monitors
local fac = io.facility
fac.radiation = types.new_zero_radiation_reading()
fac.rad_monitors = data[#io.units + 1]
local max_rad = 0
local connected = {}
for id, mon in pairs(fac.rad_monitors) do
table.insert(connected, id)
fac.ps.publish("radiation@" .. id, mon.radiation)
if mon.raw > max_rad then
max_rad = mon.raw
fac.radiation = mon.radiation
end
end
fac.ps.publish("radiation", fac.radiation)
fac.ps.publish("radiation_monitors", textutils.serialize(connected))
end
local comp_record = {}
-- update the computers app with the network data from INFO_LIST_CMP
---@param data table
function iorx.record_network_data(data)
local ps = io.ps
local connected = {}
local crd_online = false
ps.publish("comp_online", #data)
-- add/update connected computers
for i = 1, #data do
local entry = data[i]
local type = entry[1]
local id = entry[2]
local pfx = "comp_" .. id
connected[id] = true
if type == DEV_TYPE.SVR then
ps.publish("comp_svr_addr", id)
ps.publish("comp_svr_fw", entry[3])
elseif type == DEV_TYPE.CRD then
crd_online = true
ps.publish("comp_crd_addr", id)
ps.publish("comp_crd_fw", entry[3])
ps.publish("comp_crd_rtt", entry[4])
else
ps.publish(pfx .. "_type", entry[1])
ps.publish(pfx .. "_addr", id)
ps.publish(pfx .. "_fw", entry[3])
ps.publish(pfx .. "_rtt", entry[4])
if type == DEV_TYPE.PLC then
ps.publish(pfx .. "_unit", entry[5])
end
if not comp_record[id] then
comp_record[id] = true
-- trigger the app to create the new element
ps.publish("comp_connect", id)
end
end
end
-- handle the coordinator being online or not
-- no need to worry about the supervisor since this data is from the supervisor, so it has to be 'online' if received
ps.publish("comp_crd_online", crd_online)
if not crd_online then
ps.publish("comp_crd_addr", "---")
ps.publish("comp_crd_fw", "---")
ps.publish("comp_crd_rtt", "---")
end
-- reset the published value
ps.publish("comp_connect", false)
-- remove disconnected computers
for id, state in pairs(comp_record) do
if state and not connected[id] then
comp_record[id] = false
-- trigger the app to delete the element
ps.publish("comp_disconnect", id)
end
end
-- reset the published value
ps.publish("comp_disconnect", false)
end
-- clear the tracked connected computer record
function iorx.clear_comp_record() comp_record = {} end
return function (io_obj)
io = io_obj
return iorx

View File

@@ -16,16 +16,10 @@ local LINK_STATE = iocontrol.LINK_STATE
local pocket = {}
local MQ__RENDER_CMD = {
UNLOAD_SV_APPS = 1,
UNLOAD_API_APPS = 2
}
local MQ__RENDER_DATA = {
LOAD_APP = 1
}
pocket.MQ__RENDER_CMD = MQ__RENDER_CMD
pocket.MQ__RENDER_DATA = MQ__RENDER_DATA
---@type pkt_config
@@ -38,6 +32,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")
@@ -54,6 +49,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)
@@ -86,18 +82,20 @@ local APP_ID = {
-- core UI
ROOT = 1,
LOADER = 2,
-- main app pages
-- main apps
UNITS = 3,
CONTROL = 4,
PROCESS = 5,
WASTE = 6,
GUIDE = 7,
ABOUT = 8,
-- diagnostic app pages
ALARMS = 9,
-- other
DUMMY = 10,
NUM_APPS = 10
FACILITY = 4,
CONTROL = 5,
PROCESS = 6,
WASTE = 7,
GUIDE = 8,
ABOUT = 9,
RADMON = 10,
-- diagnostic apps
ALARMS = 11,
COMPS = 12,
-- count
NUM_APPS = 12
}
pocket.APP_ID = APP_ID
@@ -146,7 +144,7 @@ function pocket.init_nav(smem)
---@class pocket_app
local app = {
loaded = false,
cur_page = nil, ---@type nav_tree_page
cur_page = nil, ---@type nav_tree_page|nil
pane = pane,
paned_pages = {}, ---@type nav_tree_page[]
sidebar_items = {} ---@type sidebar_entry[]
@@ -266,21 +264,28 @@ function pocket.init_nav(smem)
-- open an app
---@param app_id POCKET_APP_ID
---@param on_loaded? function
function nav.open_app(app_id, on_loaded)
---@param on_ready? function
function nav.open_app(app_id, on_ready)
-- reset help return on navigating out of an app
if app_id == APP_ID.ROOT then self.help_return = nil end
local app = self.apps[app_id]
if app then
if app.requires_conn() and not smem.pkt_sys.pocket_comms.is_linked() then
local p_comms = smem.pkt_sys.pocket_comms
local req_sv, req_api = app.check_requires()
if (req_sv and not p_comms.is_sv_linked()) or (req_api and not p_comms.is_api_linked()) then
-- report required connction(s)
iocontrol.get_db().loader_require = { sv = req_sv, api = req_api }
iocontrol.get_db().ps.toggle("loader_reqs")
-- bring up the app loader
self.loader_return = app_id
app_id = APP_ID.LOADER
app = self.apps[app_id]
else self.loader_return = nil end
if not app.loaded then smem.q.mq_render.push_data(MQ__RENDER_DATA.LOAD_APP, { app_id, on_loaded }) 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)
@@ -288,6 +293,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
@@ -335,7 +342,7 @@ function pocket.init_nav(smem)
function nav.get_containers() return self.containers end
-- get the currently active page
---@return nav_tree_page
---@return nav_tree_page|nil
function nav.get_current_page()
return self.apps[self.cur_app].get_current_page()
end
@@ -364,8 +371,7 @@ function pocket.init_nav(smem)
self.help_return = self.cur_app
nav.open_app(APP_ID.GUIDE, function ()
local show = self.help_map[key]
if show then show() end
if self.help_map[key] then self.help_map[key]() end
end)
end
@@ -485,20 +491,28 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
function public.close_sv()
sv_watchdog.cancel()
nav.unload_sv()
self.sv.linked = false
self.sv.r_seq_num = nil
self.sv.addr = comms.BROADCAST
_send_sv(MGMT_TYPE.CLOSE, {})
if self.sv.linked then
self.sv.linked = false
_send_sv(MGMT_TYPE.CLOSE, {})
end
end
-- close connection to coordinator API server
function public.close_api()
api_watchdog.cancel()
nav.unload_api()
self.api.linked = false
self.api.r_seq_num = nil
self.api.addr = comms.BROADCAST
_send_crd(MGMT_TYPE.CLOSE, {})
if self.api.linked then
self.api.linked = false
_send_crd(MGMT_TYPE.CLOSE, {})
end
end
-- close the connections to the servers
@@ -509,24 +523,18 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
-- attempt to re-link if any of the dependent links aren't active
function public.link_update()
if not self.sv.linked then
if not (self.sv.linked and self.api.linked) then
if self.api.linked then
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY, false, nil)
elseif self.sv.linked then
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, nil, false)
else
iocontrol.report_link_state(LINK_STATE.UNLINKED, false, false)
end
if self.establish_delay_counter <= 0 then
_send_sv_establish()
self.establish_delay_counter = 4
else
self.establish_delay_counter = self.establish_delay_counter - 1
end
elseif not self.api.linked then
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, nil, false)
if self.establish_delay_counter <= 0 then
_send_api_establish()
if not self.api.linked then _send_api_establish() end
if not self.sv.linked then _send_sv_establish() end
self.establish_delay_counter = 4
else
self.establish_delay_counter = self.establish_delay_counter - 1
@@ -553,6 +561,16 @@ 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
-- supervisor get connected computers
function public.diag__get_computers()
if self.sv.linked then _send_sv(MGMT_TYPE.INFO_LIST_CMP, {}) end
end
-- coordinator get facility app data
function public.api__get_facility()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_FAC_DTL, {}) end
end
-- coordinator get unit data
function public.api__get_unit(unit)
if self.api.linked then _send_api(CRDN_TYPE.API_GET_UNIT, { unit }) end
@@ -573,6 +591,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
if self.api.linked then _send_api(CRDN_TYPE.API_GET_WASTE, {}) end
end
-- coordinator get radiation app data
function public.api__get_rad()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_RAD, {}) end
end
-- send a facility command
---@param cmd FAC_COMMAND command
---@param option any? optional option options for the optional options (like waste mode)
@@ -649,6 +672,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
---@param packet mgmt_frame|crdn_frame|nil
function public.handle_packet(packet)
local diag = iocontrol.get_db().diag
local ps = iocontrol.get_db().ps
if packet ~= nil then
local l_chan = packet.scada_frame.local_channel()
@@ -729,6 +753,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
if _check_length(packet, 11) then
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.rx.record_unit_data(packet.data)
@@ -745,6 +773,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
if _check_length(packet, #iocontrol.get_db().units + 1) then
iocontrol.rx.record_waste_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_RAD then
if _check_length(packet, #iocontrol.get_db().units + 1) then
iocontrol.rx.record_radiation_data(packet.data)
end
else _fail_type(packet) end
else
log.debug("discarding coordinator SCADA_CRDN packet before linked")
@@ -892,23 +924,23 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
elseif packet.type == MGMT_TYPE.DIAG_TONE_GET then
if _check_length(packet, 8) then
for i = 1, #packet.data do
diag.tone_test.tone_indicators[i].update(packet.data[i] == true)
ps.publish("alarm_tone_" .. i, packet.data[i] == true)
end
end
elseif packet.type == MGMT_TYPE.DIAG_TONE_SET then
if packet.length == 1 and packet.data[1] == false then
diag.tone_test.ready_warn.set_value("testing denied")
ps.publish("alarm_ready_warn", "testing denied")
log.debug("supervisor SCADA diag tone set failed")
elseif packet.length == 2 and type(packet.data[2]) == "table" then
local ready = packet.data[1]
local states = packet.data[2]
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready"))
ps.publish("alarm_ready_warn", util.trinary(ready, "", "system not idle"))
for i = 1, #states do
if diag.tone_test.tone_buttons[i] ~= nil then
diag.tone_test.tone_buttons[i].set_value(states[i] == true)
diag.tone_test.tone_indicators[i].update(states[i] == true)
ps.publish("alarm_tone_" .. i, states[i] == true)
end
end
else
@@ -916,13 +948,13 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
end
elseif packet.type == MGMT_TYPE.DIAG_ALARM_SET then
if packet.length == 1 and packet.data[1] == false then
diag.tone_test.ready_warn.set_value("testing denied")
ps.publish("alarm_ready_warn", "testing denied")
log.debug("supervisor SCADA diag alarm set failed")
elseif packet.length == 2 and type(packet.data[2]) == "table" then
local ready = packet.data[1]
local states = packet.data[2]
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready"))
ps.publish("alarm_ready_warn", util.trinary(ready, "", "system not idle"))
for i = 1, #states do
if diag.tone_test.alarm_buttons[i] ~= nil then
@@ -932,6 +964,8 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
else
log.debug("supervisor SCADA diag alarm set packet length/type mismatch")
end
elseif packet.type == MGMT_TYPE.INFO_LIST_CMP then
iocontrol.rx.record_network_data(packet.data)
else _fail_type(packet) end
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- connection with supervisor established

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.13-alpha"
local POCKET_VERSION = "v1.0.4"
local println = util.println
local println_ts = util.println_ts

View File

@@ -11,10 +11,9 @@ local core = require("graphics.core")
local threads = {}
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
local MAIN_CLOCK = 0.5 -- 2Hz, 10 ticks
local RENDER_SLEEP = 100 -- 100ms, 2 ticks
local MQ__RENDER_CMD = pocket.MQ__RENDER_CMD
local MQ__RENDER_DATA = pocket.MQ__RENDER_DATA
-- main thread
@@ -58,8 +57,10 @@ function threads.thread__main(smem)
pocket_comms.link_update()
-- update any tasks for the active page
local page_tasks = nav.get_current_page().tasks
for i = 1, #page_tasks do page_tasks[i]() end
if nav.get_current_page() then
local page_tasks = nav.get_current_page().tasks
for i = 1, #page_tasks do page_tasks[i]() end
end
loop_clock.start()
elseif sv_wd.is_timer(param1) then
@@ -157,9 +158,6 @@ function threads.thread__render(smem)
if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command
if msg.message == MQ__RENDER_CMD.UNLOAD_SV_APPS then
elseif msg.message == MQ__RENDER_CMD.UNLOAD_API_APPS then
end
elseif msg.qtype == mqueue.TYPE.DATA then
-- received data
local cmd = msg.message ---@type queue_data

View File

@@ -1,5 +1,5 @@
--
-- System Apps
-- About Page
--
local comms = require("scada-common.comms")
@@ -24,25 +24,21 @@ local cpair = core.cpair
local APP_ID = pocket.APP_ID
-- create system app pages
-- create about page view
---@param root Container parent
local function create_pages(root)
local db = iocontrol.get_db()
----------------
-- About Page --
----------------
local frame = Div{parent=root,x=1,y=1}
local about_root = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.ABOUT, frame)
local about_app = db.nav.register_app(APP_ID.ABOUT, about_root)
local about_page = app.new_page(nil, 1)
local nt_page = app.new_page(about_page, 2)
local fw_page = app.new_page(about_page, 3)
local hw_page = app.new_page(about_page, 4)
local about_page = about_app.new_page(nil, 1)
local nt_page = about_app.new_page(about_page, 2)
local fw_page = about_app.new_page(about_page, 3)
local hw_page = about_app.new_page(about_page, 4)
local about = Div{parent=about_root,x=1,y=2}
local about = Div{parent=frame,x=1,y=2}
TextBox{parent=about,y=1,text="System Information",alignment=ALIGN.CENTER}
@@ -58,7 +54,7 @@ local function create_pages(root)
local config = pocket.config
local nt_div = Div{parent=about_root,x=1,y=2}
local nt_div = Div{parent=frame,x=1,y=2}
TextBox{parent=nt_div,y=1,text="Network Details",alignment=ALIGN.CENTER}
PushButton{parent=nt_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
@@ -87,7 +83,7 @@ local function create_pages(root)
--#region Firmware Versions
local fw_div = Div{parent=about_root,x=1,y=2}
local fw_div = Div{parent=frame,x=1,y=2}
TextBox{parent=fw_div,y=1,text="Firmware Versions",alignment=ALIGN.CENTER}
PushButton{parent=fw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
@@ -123,7 +119,7 @@ local function create_pages(root)
--#region Host Versions
local hw_div = Div{parent=about_root,x=1,y=2}
local hw_div = Div{parent=frame,x=1,y=2}
TextBox{parent=hw_div,y=1,text="Host Versions",alignment=ALIGN.CENTER}
PushButton{parent=hw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
@@ -138,9 +134,9 @@ local function create_pages(root)
--#endregion
local root_pane = MultiPane{parent=about_root,x=1,y=1,panes={about,nt_div,fw_div,hw_div}}
local root_pane = MultiPane{parent=frame,x=1,y=1,panes={about,nt_div,fw_div,hw_div}}
about_app.set_root_pane(root_pane)
app.set_root_pane(root_pane)
end
return create_pages

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

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

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

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

View File

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

View File

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

View File

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

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

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

View File

@@ -9,7 +9,6 @@ local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local docs = require("pocket.ui.docs")
-- local style = require("pocket.ui.style")
local guide_section = require("pocket.ui.pages.guide_section")
@@ -31,10 +30,6 @@ 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
-- new system guide view
---@param root Container parent
local function new_view(root)
@@ -47,14 +42,21 @@ local function new_view(root)
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.cyan,colors._INHERIT)}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
local load_text_1 = TextBox{parent=load_div,y=14,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.lightGray,colors._INHERIT)}
local load_text_2 = TextBox{parent=load_div,y=15,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.lightGray,colors._INHERIT)}
-- give more detailed information so the user doesn't give up
local function load_text(a, b)
if a then load_text_1.set_value(a) end
load_text_2.set_value(b or "")
end
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
local btn_fg_bg = cpair(colors.cyan, colors.black)
local btn_active = cpair(colors.white, colors.black)
local btn_disable = cpair(colors.gray, colors.black)
app.set_sidebar({{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home }})
@@ -71,7 +73,7 @@ local function new_view(root)
app.set_sidebar(list)
page_div = Div{parent=main,y=2}
local p_width = page_div.get_width() - 2
local p_width = page_div.get_width() - 1
local main_page = app.new_page(nil, 1)
local search_page = app.new_page(main_page, 2)
@@ -104,6 +106,8 @@ local function new_view(root)
PushButton{parent=home,text="Glossary >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_page.nav_to}
PushButton{parent=home,y=10,text="Wiki and Discord >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=lnk_page.nav_to}
load_text("Search")
TextBox{parent=search,y=1,text="Search",alignment=ALIGN.CENTER}
local query_field = TextField{parent=search,x=1,y=3,width=18,fg_bg=cpair(colors.white,colors.gray)}
@@ -171,14 +175,29 @@ local function new_view(root)
util.nop()
load_text("System Usage")
TextBox{parent=use,y=1,text="System Usage",alignment=ALIGN.CENTER}
PushButton{parent=use,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
PushButton{parent=use,y=3,text="Configuring Devices >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=use,text="Connecting Devices >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=use,text="Manual Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=use,text="Automatic Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=use,text="Waste Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
load_text(false, "Connecting Devices")
local conn_dev_page = guide_section(sect_construct_data, use_page, "Connecting Devs", docs.usage.conn, 110)
load_text(false, "Configuring Devices")
local config_dev_page = guide_section(sect_construct_data, use_page, "Configuring Devs", docs.usage.config, 350)
load_text(false, "Manual Control")
local man_ctrl_page = guide_section(sect_construct_data, use_page, "Manual Control", docs.usage.manual, 100)
load_text(false, "Auto Control")
local auto_ctrl_page = guide_section(sect_construct_data, use_page, "Auto Control", docs.usage.auto, 200)
load_text(false, "Waste Control")
local waste_ctrl_page = guide_section(sect_construct_data, use_page, "Waste Control", docs.usage.waste, 120)
PushButton{parent=use,y=3,text="Connecting Devices >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=conn_dev_page.nav_to}
PushButton{parent=use,text="Configuring Devices >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=config_dev_page.nav_to}
PushButton{parent=use,text="Manual Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=man_ctrl_page.nav_to}
PushButton{parent=use,text="Automatic Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=auto_ctrl_page.nav_to}
PushButton{parent=use,text="Waste Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=waste_ctrl_page.nav_to}
load_text("Operator UIs")
TextBox{parent=uis,y=1,text="Operator UIs",alignment=ALIGN.CENTER}
PushButton{parent=uis,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
@@ -187,51 +206,84 @@ local function new_view(root)
local annunc_div = Div{parent=page_div,x=2}
table.insert(panes, annunc_div)
local coord_page = app.new_page(uis_page, #panes + 1)
local coord_div = Div{parent=page_div,x=2}
table.insert(panes, coord_div)
load_text(false, "Alarms")
local alarms_page = guide_section(sect_construct_data, uis_page, "Alarms", docs.alarms, 100)
PushButton{parent=uis,y=3,text="Alarms >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=alarms_page.nav_to}
PushButton{parent=uis,text="Annunciators >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=annunc_page.nav_to}
PushButton{parent=uis,text="Pocket UI >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=uis,text="Coordinator UI >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=uis,text="Coordinator UI >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=coord_page.nav_to}
load_text(false, "Annunciators")
TextBox{parent=annunc_div,y=1,text="Annunciators",alignment=ALIGN.CENTER}
PushButton{parent=annunc_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=uis_page.nav_to}
local fac_annunc_page = guide_section(sect_construct_data, annunc_page, "Facility", docs.annunc.facility.main_section, 110)
local unit_gen_page = guide_section(sect_construct_data, annunc_page, "Unit General", docs.annunc.unit.main_section, 170)
local unit_rps_page = guide_section(sect_construct_data, annunc_page, "Unit RPS", docs.annunc.unit.rps_section, 100)
local unit_rcs_page = guide_section(sect_construct_data, annunc_page, "Unit RCS", docs.annunc.unit.rcs_section, 170)
local fac_annunc_page = guide_section(sect_construct_data, annunc_page, "Facility", docs.annunc.facility.main_section, 110)
PushButton{parent=annunc_div,y=3,text="Unit General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_gen_page.nav_to}
PushButton{parent=annunc_div,y=3,text="Facility General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fac_annunc_page.nav_to}
PushButton{parent=annunc_div,text="Unit General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_gen_page.nav_to}
PushButton{parent=annunc_div,text="Unit RPS >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_rps_page.nav_to}
PushButton{parent=annunc_div,text="Unit RCS >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_rcs_page.nav_to}
PushButton{parent=annunc_div,text="Facility General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fac_annunc_page.nav_to}
PushButton{parent=annunc_div,text="Waste & Valves >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
load_text(false, "Coordinator UI")
TextBox{parent=coord_div,y=1,text="Coordinator UI",alignment=ALIGN.CENTER}
PushButton{parent=coord_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=uis_page.nav_to}
load_text(false, "Main Display")
local main_disp_page = guide_section(sect_construct_data, coord_page, "Main Display", docs.c_ui.main, 300)
load_text(false, "Flow Display")
local flow_disp_page = guide_section(sect_construct_data, coord_page, "Flow Display", docs.c_ui.flow, 210)
load_text(false, "Unit Displays")
local unit_disp_page = guide_section(sect_construct_data, coord_page, "Unit Displays", docs.c_ui.unit, 150)
PushButton{parent=coord_div,y=3,text="Main Display >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_disp_page.nav_to}
PushButton{parent=coord_div,text="Flow Display >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=flow_disp_page.nav_to}
PushButton{parent=coord_div,text="Unit Displays >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_disp_page.nav_to}
load_text("Front Panels")
TextBox{parent=fps,y=1,text="Front Panels",alignment=ALIGN.CENTER}
PushButton{parent=fps,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
load_text(false, "Common Items")
local fp_common_page = guide_section(sect_construct_data, fps_page, "Common Items", docs.fp.common, 100)
local fp_rplc_page = guide_section(sect_construct_data, fps_page, "Reactor PLC", docs.fp.r_plc, 180)
load_text(false, "Reactor PLC")
local fp_rplc_page = guide_section(sect_construct_data, fps_page, "Reactor PLC", docs.fp.r_plc, 190)
load_text(false, "RTU Gateway")
local fp_rtu_page = guide_section(sect_construct_data, fps_page, "RTU Gateway", docs.fp.rtu_gw, 100)
load_text(false, "Supervisor")
local fp_supervisor_page = guide_section(sect_construct_data, fps_page, "Supervisor", docs.fp.supervisor, 160)
load_text(false, "Coordinator")
local fp_coordinator_page = guide_section(sect_construct_data, fps_page, "Coordinator", docs.fp.coordinator, 80)
PushButton{parent=fps,y=3,text="Common Items >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_common_page.nav_to}
PushButton{parent=fps,text="Reactor PLC >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_rplc_page.nav_to}
PushButton{parent=fps,text="RTU Gateway >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_rtu_page.nav_to}
PushButton{parent=fps,text="Supervisor >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_supervisor_page.nav_to}
PushButton{parent=fps,text="Coordinator >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=fps,text="Coordinator >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_coordinator_page.nav_to}
load_text("Glossary")
TextBox{parent=gls,y=1,text="Glossary",alignment=ALIGN.CENTER}
PushButton{parent=gls,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
local gls_abbv_page = guide_section(sect_construct_data, gls_page, "Abbreviations", docs.glossary.abbvs, 140)
local gls_term_page = guide_section(sect_construct_data, gls_page, "Terminology", docs.glossary.terms, 100)
local gls_term_page = guide_section(sect_construct_data, gls_page, "Terminology", docs.glossary.terms, 120)
PushButton{parent=gls,y=3,text="Abbreviations >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_abbv_page.nav_to}
PushButton{parent=gls,text="Terminology >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_term_page.nav_to}
load_text("Links")
TextBox{parent=lnk,y=1,text="Wiki and Discord",alignment=ALIGN.CENTER}
PushButton{parent=lnk,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}

View File

@@ -32,16 +32,29 @@ local function create_pages(root)
local root_pane = MultiPane{parent=main,x=1,y=1,panes={conn_sv_wait,conn_api_wait,main_pane}}
root_pane.register(db.ps, "link_state", function (state)
if state == LINK_STATE.UNLINKED or state == LINK_STATE.API_LINK_ONLY then
local function update()
local state = db.ps.get("link_state")
if state == LINK_STATE.UNLINKED then
root_pane.set_value(1)
elseif state == LINK_STATE.API_LINK_ONLY then
if not db.loader_require.sv then
root_pane.set_value(3)
db.nav.on_loader_connected()
else root_pane.set_value(1) end
elseif state == LINK_STATE.SV_LINK_ONLY then
root_pane.set_value(2)
if not db.loader_require.api then
root_pane.set_value(3)
db.nav.on_loader_connected()
else root_pane.set_value(2) end
else
root_pane.set_value(3)
db.nav.on_loader_connected()
end
end)
end
root_pane.register(db.ps, "link_state", update)
root_pane.register(db.ps, "loader_reqs", update)
TextBox{parent=main_pane,text="Connected!",x=1,y=6,alignment=core.ALIGN.CENTER}
end

View File

@@ -1,5 +1,5 @@
--
-- Process Control Page
-- Process Control App
--
local types = require("scada-common.types")
@@ -162,7 +162,7 @@ local function new_view(root)
TextBox{parent=o_div,y=1,text="Process Options",alignment=ALIGN.CENTER}
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
local mode = RadioButton{parent=o_div,x=1,y=3,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.purple,dis_fg_bg=style.btn_disable}
local mode = RadioButton{parent=o_div,x=1,y=3,options=ctl_opts,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.purple,dis_fg_bg=style.btn_disable}
mode.register(f_ps, "process_mode", mode.set_value)
@@ -194,7 +194,7 @@ local function new_view(root)
TextBox{parent=c_div,y=1,text="Process Control",alignment=ALIGN.CENTER}
local u_stat = Rectangle{parent=c_div,border=border(1,colors.gray,true),thin=true,width=21,height=5,x=1,y=3,fg_bg=cpair(colors.black,colors.lightGray)}
local u_stat = Rectangle{parent=c_div,border=border(1,colors.gray,true),thin=true,width=21,height=5,x=1,y=3,fg_bg=cpair(colors.black,colors.lightGray)}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",alignment=ALIGN.CENTER}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",height=2,alignment=ALIGN.CENTER,trim_whitespace=true,fg_bg=cpair(colors.gray,colors.lightGray)}
@@ -210,7 +210,7 @@ local function new_view(root)
end
local start = HazardButton{parent=c_div,x=2,y=9,text="START",accent=colors.lightBlue,callback=_start_auto,timeout=3,fg_bg=hzd_fg_bg,dis_colors=dis_colors}
local stop = HazardButton{parent=c_div,x=13,y=9,text="STOP",accent=colors.red,callback=process.process_stop,timeout=3,fg_bg=hzd_fg_bg,dis_colors=dis_colors}
local stop = HazardButton{parent=c_div,x=13,y=9,text="STOP",accent=colors.red,callback=process.process_stop,timeout=3,fg_bg=hzd_fg_bg,dis_colors=dis_colors}
db.facility.start_ack = start.on_response
db.facility.stop_ack = stop.on_response

View File

@@ -0,0 +1,219 @@
--
-- Radiation Monitor App
--
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local RadIndicator = require("graphics.elements.indicators.RadIndicator")
local ALIGN = core.ALIGN
local cpair = core.cpair
local border = core.border
local APP_ID = pocket.APP_ID
local label_fg_bg = style.label
local lu_col = style.label_unit_pair
-- new radiation monitor 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.RADMON, 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.yellow,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[]
-- create all page divs
for _ = 1, db.facility.num_units + 2 do
local div = Div{parent=page_div}
table.insert(panes, div)
end
local last_update = 0
-- refresh data callback, every 500ms it will re-send the query
local function update()
if util.time_ms() - last_update >= 500 then
db.api.get_rad()
last_update = util.time_ms()
end
end
-- create a new radiation monitor list
---@param parent Container
---@param ps psil
local function new_mon_list(parent, ps)
local mon_list = ListBox{parent=parent,y=6,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local elem_list = {} ---@type graphics_element[]
mon_list.register(ps, "radiation_monitors", function (data)
local ids = textutils.unserialize(data)
-- delete any disconnected monitors
for id, elem in pairs(elem_list) do
if not util.table_contains(ids, id) then
elem.delete()
elem_list[id] = nil
end
end
-- add newly connected monitors
for _, id in pairs(ids) do
if not elem_list[id] then
elem_list[id] = Div{parent=mon_list,height=5}
local mon_rect = Rectangle{parent=elem_list[id],height=4,x=2,width=20,border=border(1,colors.gray,true),thin=true,fg_bg=cpair(colors.black,colors.lightGray)}
TextBox{parent=mon_rect,text="Env. Detector "..id}
local mon_rad = RadIndicator{parent=mon_rect,x=2,label="",format="%13.3f",lu_colors=cpair(colors.gray,colors.gray),width=18}
mon_rad.register(ps, "radiation@" .. id, mon_rad.update)
end
end
end)
end
--#region unit radiation monitors
for i = 1, db.facility.num_units do
local u_pane = panes[i]
local u_div = Div{parent=u_pane}
local unit = db.units[i]
local u_ps = unit.unit_ps
local u_page = app.new_page(nil, i)
u_page.tasks = { update }
TextBox{parent=u_div,y=1,text="Unit #"..i.." Monitors",alignment=ALIGN.CENTER}
TextBox{parent=u_div,x=2,y=3,text="Max Radiation",fg_bg=label_fg_bg}
local radiation = RadIndicator{parent=u_div,x=2,label="",format="%17.3f",lu_colors=lu_col,width=21}
radiation.register(u_ps, "radiation", radiation.update)
new_mon_list(u_div, u_ps)
end
--#endregion
--#region overview page
local s_pane = panes[db.facility.num_units + 1]
local s_div = Div{parent=s_pane,x=2,width=main.get_width()-2}
local stat_page = app.new_page(nil, db.facility.num_units + 1)
stat_page.tasks = { update }
TextBox{parent=s_div,y=1,text=" Radiation Monitoring",alignment=ALIGN.CENTER}
TextBox{parent=s_div,y=3,text="Max Facility Rad.",fg_bg=label_fg_bg}
local s_f_rad = RadIndicator{parent=s_div,label="",format="%17.3f",lu_colors=lu_col,width=21}
s_f_rad.register(f_ps, "radiation", s_f_rad.update)
for i = 1, db.facility.num_units do
local unit = db.units[i]
local u_ps = unit.unit_ps
s_div.line_break()
TextBox{parent=s_div,text="Max Unit "..i.." Radiation",fg_bg=label_fg_bg}
local s_u_rad = RadIndicator{parent=s_div,label="",format="%17.3f",lu_colors=lu_col,width=21}
s_u_rad.register(u_ps, "radiation", s_u_rad.update)
end
--#endregion
--#region overview page
local f_pane = panes[db.facility.num_units + 2]
local f_div = Div{parent=f_pane,width=main.get_width()}
local fac_page = app.new_page(nil, db.facility.num_units + 2)
fac_page.tasks = { update }
TextBox{parent=f_div,y=1,text="Facility Monitors",alignment=ALIGN.CENTER}
TextBox{parent=f_div,x=2,y=3,text="Max Radiation",fg_bg=label_fg_bg}
local f_rad = RadIndicator{parent=f_div,x=2,label="",format="%17.3f",lu_colors=lu_col,width=21}
f_rad.register(f_ps, "radiation", f_rad.update)
new_mon_list(f_div, f_ps)
--#endregion
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)
-- setup sidebar
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = " \x1e ", color = core.cpair(colors.black, colors.blue), callback = stat_page.nav_to },
{ label = "FAC", color = core.cpair(colors.black, colors.yellow), callback = fac_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 = function () app.switcher(i) end })
end
app.set_sidebar(list)
-- done, show the app
stat_page.nav_to()
load_pane.set_value(2)
end
-- delete the elements and switch back to the loading screen
local function unload()
if page_div then
page_div.delete()
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.delete_pages()
-- show loading screen
load_pane.set_value(1)
end
app.set_load(load)
app.set_unload(unload)
return main
end
return new_view

View File

@@ -1,5 +1,5 @@
--
-- Unit Overview Page
-- Unit Overview App
--
local util = require("scada-common.util")
@@ -33,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
@@ -313,8 +312,6 @@ local function new_view(root)
c_emg.register(u_ps, "EmergencyCoolant", c_emg.update)
c_mwrf.register(u_ps, "MaxWaterReturnFeed", c_mwrf.update)
-- rcs_div.line_break()
-- TextBox{parent=rcs_div,text="Mismatches",alignment=ALIGN.CENTER,fg_bg=label}
local c_cfm = IconIndicator{parent=rcs_div,label="Coolant Feed",states=yel_ind_s}
local c_brm = IconIndicator{parent=rcs_div,label="Boil Rate",states=yel_ind_s}
local c_sfm = IconIndicator{parent=rcs_div,label="Steam Feed",states=yel_ind_s}
@@ -324,7 +321,6 @@ local function new_view(root)
c_sfm.register(u_ps, "SteamFeedMismatch", c_sfm.update)
rcs_div.line_break()
-- TextBox{parent=rcs_div,text="Aggregate Checks",alignment=ALIGN.CENTER,fg_bg=label}
if unit.num_boilers > 0 then
local wll = IconIndicator{parent=rcs_div,label="Boiler Water Lo",states=red_ind_s}

View File

@@ -1,5 +1,5 @@
--
-- Waste Control Page
-- Waste Control App
--
local util = require("scada-common.util")
@@ -33,9 +33,7 @@ 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
@@ -97,8 +95,8 @@ local function new_view(root)
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.waste.states_abbrv,value=1,min_width=6}
local waste_mode = RadioButton{parent=u_div,y=3,options=style.waste.unit_opts,callback=set_waste,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.white}
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)
@@ -161,8 +159,8 @@ local function new_view(root)
TextBox{parent=c_div,y=1,text="Waste Control",alignment=ALIGN.CENTER}
local status = StateIndicator{parent=c_div,x=3,y=3,states=style.waste.states,value=1,min_width=17}
local waste_prod = RadioButton{parent=c_div,y=5,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.white}
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)
@@ -249,7 +247,7 @@ local function new_view(root)
local sps_status = StateIndicator{parent=s_div,x=5,y=3,states=style.sps.states,value=1,min_width=12}
sps_status.register(f_ps, "sps_computed_status", sps_status.update)
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}
@@ -264,8 +262,8 @@ local function new_view(root)
--#endregion
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)
local w_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(w_pane)
-- setup sidebar

View File

@@ -1,3 +1,7 @@
--
-- All the text documentation used in the Guide app is defined in this file.
--
local const = require("scada-common.constants")
local docs = {}
@@ -7,7 +11,9 @@ local DOC_ITEM_TYPE = {
SECTION = 1,
SUBSECTION = 2,
TEXT = 3,
LIST = 4
NOTE = 4,
TIP = 5,
LIST = 6
}
---@enum DOC_LIST_TYPE
@@ -51,6 +57,18 @@ local function text(body)
table.insert(target, item)
end
local function note(body)
---@class pocket_doc_note
local item = { type = DOC_ITEM_TYPE.NOTE, text = body }
table.insert(target, item)
end
local function tip(body)
---@class pocket_doc_tip
local item = { type = DOC_ITEM_TYPE.TIP, text = body }
table.insert(target, item)
end
---@param type DOC_LIST_TYPE
---@param items table
---@param colors table|nil colors for indicators or nil for normal lists
@@ -60,14 +78,140 @@ local function list(type, items, colors)
table.insert(target, list_def)
end
-- important to note in the future: The PLC should always be in a chunk with the reactor to ensure it can protect it on chunk load if you do not keep it all chunk loaded
--#region System Usage
docs.usage = {
conn = {}, config = {}, manual = {}, auto = {}, waste = {}
}
target = docs.usage.conn
sect("Overview")
tip("For the best setup experience, see the Wiki on GitHub or the YouTube channel! This app does not contain all information.")
text("Mekanism devices are connected to ComputerCraft computers that form the SCADA control system.")
sect("Mekanism Conns")
text("Multiblocks and single block devices are both connected directly to a computer by touching it or via wired modems.")
doc("usage_conn_mb", "Multiblocks", "For multiblocks, a logic adapter is used if it exists for that multiblock, otherwise a valve or port block is used.")
text("A wired modem is only connected to the block when you right click it and it gets a red border and you see a message in the chat with the peripheral name.")
tip("Do not connect all peripherals in the system on the same network cable, since Reactor PLCs will grab the first reactor they find and you may accidentally duplicate RTUs.")
sect("Computer Conns")
tip("It helps to be familiar with how ComputerCraft manages peripherals before using this system, though it is not necessary.")
doc("usage_conn_network", "Network", "All computers in the system communicate with each other via wireless or ender modems. Ender modems are preferred due to the unlimited range.")
text("Five different network channels are used and must have the same value for each name across all devices.")
text("For example, the supervisor channel SVR_CHANNEL must be set to the same channel for all devices in your system. Two different named channels should not share the same value (such as SVR_CHANNEL vs CRD_CHANNEL).")
doc("usage_conn_peri", "Peripherals", "ComputerCraft peripherals like monitors and speakers need to touch the computer or be connected via wired modems.")
target = docs.usage.config
sect("Overview")
tip("For the best setup experience, see the Wiki on GitHub or the YouTube channel! This app does not contain all information.")
text("All devices have a configurator program you can launch by running the 'configure' command.")
sect("Networking")
doc("usage_cfg_id", "Computer ID", "A computer ID must NEVER be the identical between devices, which can only happen if you duplicate a computer (such as if you middle-click on it and place it again in creative mode).")
doc("usage_cfg_chan", "Channels", "Channels are used for the computer to computer communication, described in the connection guide section. Channels with the same name must have the same value across all devices in your system and channels with different names cannot overlap.")
doc("usage_cfg_to", "Conn Timeout", "After this period of time the device will close the connection assuming the other device is unresponsive.")
doc("usage_cfg_tr", "Trusted Range", "Devices further than this block distance away will have any network traffic rejected by this device.")
doc("usage_cfg_auth", "Authentication", "To provide a level of security, you can enable facility-wide authentication by setting keys, which must be the same (and set) on all your devices. This adds computation time to each network transmission so you should only do this if you need it on multiplayer.")
sect("Logging")
text("Logs are automatically saved to a log.txt file in the root of the computer. You can change the path to it, if it contains verbose debug messages, and if it is appended to or overwritten each time the program runs.")
text("If you intend to be able to share logs, you should leave it to append.")
doc("usage_cfg_log_upload", "Sharing Logs", "To share logs, you would run 'pastebin put log.txt' where your log file is then share the code.")
sect("Reactor PLC")
text("The Reactor PLC must be connected to a single fission reactor that it will manage. Use the configurator to choose if you would like it to operate as networked or not.")
tip("The Reactor PLC should always be in a chunk with the reactor to ensure it can protect it on server start and/or chunk load.")
doc("usage_cfg_plc_nonet", "Non-Networked", "This lets you use this device as an advanced standalone safety system rather than a basic redstone breaker for easier safety protection.")
doc("usage_cfg_plc_net", "Networked", "This is the most commonly used mode. The Reactor PLC will require a connection to the Supervisor to operate and will allow usage through that for more advanced functionality.")
doc("usage_cfg_plc_unit", "Unit ID", "When networked, you can set any unit ID ranging from 1 to 4. Multiple Reactor PLCs cannot share the same unit ID.")
sect("RTU Gateway")
text("The RTU Gateway allows connecting multiple RTU interfaces to the SCADA system. These interfaces may be external peripherals or redstone.")
text("All devices except for fission reactors must be connected via an RTU Gateway.")
sect("Supervisor")
text("The Supervisor configuration is core to the entire system. If you change things about the system, such as the cooling devices or reactor count, it must be updated here.")
text("This configuration contains many settings that are detailed better in the configurator so they will not be covered here.")
doc("usage_cfg_sv_tanks", "Dynamic Tanks", "Dynamic tanks can be used to provide emergency coolant (and/or auxiliary coolant) to the system. Many layouts are supported by using a mix of facility tanks (connect to 1+ units) and unit tanks (connect to only one unit).")
doc("usage_cfg_sv_aux", "Auxiliary Coolant", "This coolant is enabled at the start of reactors to prevent water levels from dropping in the reactor or boiler while the turbine ramps up. This can be connected to a dynamic tank, a sink, or any other water supply.")
sect("Coordinator")
text("The Coordinator configuration is mainly focused around setting up your displays. This is best to do last after everything else. See the wiki on the GitHub for details on monitor sizing.")
tip("When changing the unit count on the Supervisor, you must also update it on the Coordinator.")
doc("usage_cfg_crd_main", "Main Monitor", "The main monitor contains the main interface and overview. It is always 8 block wide with varying height depending on how many units you have.")
doc("usage_cfg_crd_flow", "Flow Monitor", "The flow monitor contains the waste and coolant flow diagram. It is always 8 block wide with varying height depending on how many units you have.")
doc("usage_cfg_crd_unit", "Unit Monitor", "You need one unit monitor per reactor, and it is always a 4x4 monitor.")
text("Monitors can be connected by direct contact or via wired modems.")
text("Various unit and color options are available to customize the display to your liking. Using energy scales other than RF can impact the precision of your power-related auto control setpoints as RF is always used internally.")
sect("Pocket")
text("You're already here, so not much to mention!")
sect("Self-Check")
text("Most application configurators provide a self-check function that will check the validity of your configuration and the network connection. You should run this if you are having issues with that device.")
sect("Config Changes")
text("When an update adds or removes or otherwise modifies configuration requirements, you will be warned that you need to re-configure. You will not lose any prior data as updates will preserve configurations, you just need to step through the instructions again to add or change any new data.")
target = docs.usage.manual
sect("Overview")
text("Manual reactor control still includes safety checks and monitoring, but the burn rate is not automatically controlled.")
text("A unit is under manual control when the AUTO CTRL option Manual is selected on the unit display.")
note("Specific UIs will not be discussed here. If you need help with the UI, refer to Operator UIs > Coordinator UI > Unit Displays.")
sect("Manual Control")
text("The unit display on the Coordinator is used to run manual control. You may also start/stop and set the burn rate via the Mekanism UI on the Fission Reactor.")
tip("If some controls are grayed out on the unit display, that operation isn't currently available, such as due to the reactor being already started or being under auto control.")
text("Manual control is started by the START button and runs at the commanded burn rate next to it, which can be modified before starting or after having started by selecting a value then pressing SET.")
text("The reactor can be stopped via SCRAM, then the RPS needs to be reset via RESET.")
target = docs.usage.auto
sect("Overview")
text("A main feature of this system is automatic reactor control that supports various managed control modes.")
tip("You should first review the Main Display and Unit Display documentation under Operator UIs > Coordinator before proceeding if you are not familiar with the interfaces.")
sect("Configuration")
note("Configurations cannot be modified while auto control is active.")
doc("usage_auto_assign", "Unit Assignments", "Auto control only applies to units set to a mode other than Manual. To prefer certain units or only use the minimum number necessary, priority groups are used to split up the required burn rate.")
text("Primary units will be used first, followed by secondary, etc. If multiple are assigned to a group, burn rate will be assigned evenly between them.")
text("The next priority group will only be used once the previous one cannot keep up with the total required burn rate for auto control at that moment.")
doc("usage_auto_setpoints", "Setpoints", "Three setpoint spinner inputs are available for the three setpoint-based auto control modes. The system will do its best to meet the requested value, with the current value listed below the input.")
doc("usage_auto_limits", "Unit Limits", "Each unit can be limited to a maximum auto control burn rate to prevent exceeding any safe levels that you know of.")
doc("usage_auto_states", "Unit States", "Any assigned units must be shown as Ready and not Degraded to use auto control. See Operator UIs > Coordinator > Main Display for more.")
sect("Operation Modes")
text("Four auto control modes are available that function based on configurations set on the main display. All modes except Monitored Max Burn will try to only use the primary group until it can't keep up, then the secondary, etc.")
note("No units will be set to a burn rate higher than their limit.")
doc("usage_op_mon_max", "Monitored Max Burn", "This mode runs all units assigned to auto control at their unit limit burn rate regardless of priority group.")
doc("usage_op_com_rate", "Combined Burn Rate", "Assigned units will be commanded to meet the Burn Target setpoint.")
doc("usage_op_chg_level", "Charge Level", "Assigned units will be commanded to bring the induction matrix up to the requested Charge Target.")
doc("usage_op_gen_rate", "Generation Rate", "Assigned units will be commanded to maintain the requested Generation Target.")
note("The rate used is the input rate into the induction matrix, so using other power generation sources may disrupt this control mode.")
sect("Start and Stop")
text("A text box is used to indicate the system status. It will also provide information of why the system has paused control or failed to start.")
text("You cannot start auto control until all assigned units have all their devices connected and functional and the reactor's RPS is not tripped.")
doc("usage_op_save", "SAVE", "SAVE will save the configuration without starting control.")
doc("usage_op_start", "START", "START will attempt to start auto control, which includes first saving the configuration.")
doc("usage_op_stop", "STOP", "STOP will stop all reactors assigned to automatic control.")
target = docs.usage.waste
sect("Overview")
text("When 'valves' are connected for routing waste, this system can manage which waste product(s) are made. The flow monitor shows the diagram of how valves are meant to be connected.")
text("There are three waste products, listed below with the colors generally associated with them.")
list(DOC_LIST_TYPE.LED, { "Pu - Plutonium", "Po - Polonium", "AM - Antimatter" }, { colors.cyan, colors.green, colors.purple })
note("The Po and Pu colors are swapped in older versions of Mekanism.")
sect("Unit Waste")
text("Units can be set to specific waste products via buttons at the bottom right of a unit display.")
note("Refer to Operator UIs > Coordinator UI > Unit Displays for details.")
text("If 'Auto' is selected instead of a waste product, that unit's waste will be processed per the facility waste control.")
sect("Facility Waste")
text("Facility waste control adds additional functionality to waste processing through automatic control.")
text("The waste control interface on the main display lets you set a target waste type along with options that can change that based on circumstances.")
note("Refer to Operator UIs > Coordinator UI > Main Display for information on the display and control interface.")
doc("usage_waste_fallback", "Pu Fallback", "This option switches facility waste control to plutonium when the SNAs cannot keep up, such as at night.")
doc("usage_waste_sps_lc", "Low Charge SPS", "This option prevents the facility waste control from stopping antimatter production at low induction matrix charge (< 10%, resumes after reaching 15%).")
text("With that option enabled, antimatter production will continue. With it disabled, it will switch to polonium if set to antimatter while charge is low.")
note("Pu Fallback takes priority and will switch to plutonium when appropriate regardless of the Low Charge SPS setting.")
--#endregion
--#region Operator UIs
--#region Alarms
docs.alarms = {}
target = docs.alarms
doc("ContainmentBreach", "Containment Breach", "Reactor disconnected or indicated unformed while being at or above 100% damage; explosion assumed.")
doc("ContainmentRadiation", "Containment Radiation", "Environment detector(s) assigned to the unit have observed high levels of radiation.")
doc("ReactorLost", "Reactor Lost", "Reactor PLC has stopped communicating with the supervisor.")
doc("ReactorLost", "Reactor Lost", "Reactor PLC has stopped communicating with the Supervisor.")
doc("CriticalDamage", "Damage Critical", "Reactor damage has reached or exceeded 100%, so it will explode at any moment.")
doc("ReactorDamage", "Reactor Damage", "Reactor temperature causing increasing damage to the reactor casing.")
doc("ReactorOverTemp", "Reactor Over Temp", "Reactor temperature is at or above maximum safe temperature, so it is now taking damage.")
@@ -78,6 +222,10 @@ doc("RPSTransient", "RPS Transient", "Reactor protection system was activated.")
doc("RCSTransient", "RCS Transient", "Something is wrong with the reactor coolant system, check RCS indicators for details.")
doc("TurbineTripAlarm", "Turbine Trip", "A turbine stopped rotating, likely due to having full energy storage. This will prevent cooling, so it needs to be resolved before using that unit.")
--#endregion
--#region Annunciators
docs.annunc = {
unit = {
main_section = {}, rps_section = {}, rcs_section = {}
@@ -89,8 +237,8 @@ docs.annunc = {
target = docs.annunc.unit.main_section
sect("Unit Status")
doc("PLCOnline", "PLC Online", "Indicates if the fission reactor PLC is connected. If it isn't, check that your PLC is on and configured properly.")
doc("PLCHeartbeat", "PLC Heartbeat", "An indicator of status data being live. As status messages are received from the PLC, this light will turn on and off. If it gets stuck, the supervisor has stopped receiving data or a screen has frozen.")
doc("PLCOnline", "PLC Online", "Indicates if the fission Reactor PLC is connected. If it isn't, check that your PLC is on and configured properly.")
doc("PLCHeartbeat", "PLC Heartbeat", "An indicator of status data being live. As status messages are received from the PLC, this light will turn on and off. If it gets stuck, the Supervisor has stopped receiving data or a screen has frozen.")
doc("RadiationMonitor", "Radiation Monitor", "On if at least one environment detector is connected and assigned to this unit.")
doc("AutoControl", "Automatic Control", "On if the reactor is under the control of one of the automatic control modes.")
sect("Safety Status")
@@ -98,7 +246,7 @@ doc("ReactorSCRAM", "Reactor SCRAM", "On if the reactor protection system is hol
doc("ManualReactorSCRAM", "Manual Reactor SCRAM", "On if the operator (you) initiated a SCRAM.")
doc("AutoReactorSCRAM", "Auto Reactor SCRAM", "On if the automatic control system initiated a SCRAM. The main view screen annunciator will have an indication as to why.")
doc("RadiationWarning", "Radiation Warning", "On if radiation levels are above normal. There is likely a leak somewhere, so that should be identified and fixed. Hazmat suit recommended.")
doc("RCPTrip", "RCP Trip", "Reactor coolant pump tripped. This is a technical concept not directly mapping to Mekansim. Here, it indicates if there is either high heated coolant or low cooled coolant that caused an RPS trip. Check the coolant system if this occurs.")
doc("RCPTrip", "RCP Trip", "Reactor coolant pump tripped. This is a technical concept not directly mapping to Mekanism. Here, it indicates if there is either high heated coolant or low cooled coolant that caused an RPS trip. Check the coolant system if this occurs.")
doc("RCSFlowLow", "RCS Flow Low", "Indicates if the reactor coolant system flow is low. This is observed when the cooled coolant level in the reactor is dropping. This can occur while a turbine spins up, but if it persists, check that the cooling system is operating properly. This can occur with smaller boilers or when using pipes and not having enough.")
doc("CoolantLevelLow", "Coolant Level Low", "On if the reactor coolant level is lower than it should be. Check the coolant system.")
doc("ReactorTempHigh", "Reactor Temp. High", "On if the reactor temperature is above expected maximum operating temperature. This is not yet damaging, but should be attended to. Check coolant system.")
@@ -118,7 +266,7 @@ doc("high_temp", "Temperature High", "Indicates if the RPS tripped due to reachi
doc("low_cool", "Coolant Level Low Low", "Indicates if the RPS tripped due to very low coolant levels that result in the temperature uncontrollably rising. Ensure that the cooling system can provide sufficient cooled coolant flow.")
doc("no_fuel", "No Fuel", "Indicates if the RPS tripped due to no fuel being available. Check fuel input.")
doc("fault", "PPM Fault", "Indicates if the RPS tripped due to a peripheral access fault. Something went wrong interfacing with the reactor, try restarting the PLC.")
doc("timeout", "Connection Timeout", "Indicates if the RPS tripped due to losing connection with the supervisory computer. Check that your PLC and supervisor remain chunk loaded.")
doc("timeout", "Connection Timeout", "Indicates if the RPS tripped due to losing connection with the supervisory computer. Check that your PLC and Supervisor remain chunk loaded.")
doc("sys_fail", "System Failure", "Indicates if the RPS tripped due to the reactor not being formed. Ensure that the multi-block is formed.")
target = docs.annunc.unit.rcs_section
@@ -130,7 +278,7 @@ doc("SteamFeedMismatch", "Steam Feed Mismatch", "There is an above tolerance dif
doc("MaxWaterReturnFeed", "Max Water Return Feed", "The turbines are condensing the max rate of water that they can per the structure build. If water return is insufficient, add more saturating condensers to your turbine(s).")
doc("WaterLevelLow", "Water Level Low", "The water level in the boiler is low. A larger boiler water tank may help, or you can feed additional water into the boiler from elsewhere.")
doc("HeatingRateLow", "Heating Rate Low", "The boiler is not hot enough to boil water, but it is receiving heated coolant. This is almost never a safety concern.")
doc("SteamDumpOpen", "Steam Relief Valve Open", "This turns yellow if the turbine is set to dumping excess and red if it is set to dumping [all]. 'Relief Valve' in this case is that setting allowing the venting of steam. You should never have this set to dumping [all]. Emergency coolant activation from the supervisor will automatically set it to dumping excess to ensure there is no backup of steam as water is added.")
doc("SteamDumpOpen", "Steam Relief Valve Open", "This turns yellow if the turbine is set to dumping excess and red if it is set to dumping [all]. 'Relief Valve' in this case is that setting allowing the venting of steam. You should never have this set to dumping [all]. Emergency coolant activation from the Supervisor will automatically set it to dumping excess to ensure there is no backup of steam as water is added.")
doc("TurbineOverSpeed", "Turbine Over Speed", "The turbine is at steam capacity, but not tripped. You may need more turbines if they can't keep up.")
doc("GeneratorTrip", "Generator Trip", "The turbine is no longer outputting power due to it having nowhere to go. Likely due to full power storage. This will lead to a Turbine Trip if not addressed.")
doc("TurbineTrip", "Turbine Trip", "The turbine has reached its maximum power charge and has stopped rotating, and as a result stopped cooling steam to water. Ensure the turbine has somewhere to output power, as this is the most common cause of reactor meltdowns. However, the likelihood of a meltdown with this system in place is much lower, especially with emergency coolant helping during turbine trips.")
@@ -154,28 +302,144 @@ doc("as_crit_alarm", "Unit Critical Alarm", "Automatic SCRAM occurred due to cri
doc("as_radiation", "Facility Radiation High", "Automatic SCRAM occurred due to high facility radiation levels.")
doc("as_gen_fault", "Gen. Control Fault", "Automatic SCRAM occurred due to assigned units being degraded/no longer ready during generation mode. The system will automatically resume (starting with initial ramp) once the problem is resolved.")
docs.fp = {
common = {}, r_plc = {}, rtu_gw = {}, supervisor = {}
--#endregion
--#region Coordinator UI
docs.c_ui = {
main = {}, flow = {}, unit = {}
}
--comp id "This must never be the identical between devices, and that can only happen if you duplicate a computer (such as middle-click on it and place it elsewhere in creative mode)."
target = docs.c_ui.main
sect("Facility Diagram")
text("The facility overview diagram is made up of unit diagrams showing the reactor, boiler(s) if present, and turbine(s). This includes values of various key statistics such as temperatures along with bars showing the fill percentage of the tanks in each multiblock.")
text("Boilers are shown under the reactor, listed in order of index (#1 then #2 below). Turbines are shown to the right, also listed in order of index (indexes are per unit and set in the RTU Gateway configuration).")
text("Pipe connections are visualized with color-coded lines, which are primarily to indicate connections, as not all facilities may use pipes.")
note("If a component you have is not showing up, ensure the Supervisor is configured for your actual cooling configuration.")
sect("Facility Status")
note("The annunciator here is described in Operator UIs > Annunciators.")
doc("ui_fac_scram", "FAC SCRAM", "This SCRAMs all units in the facility.")
doc("ui_fac_ack", "ACK \x13", "This acknowledges (mutes) all alarms for all units in the facility.")
doc("ui_fac_rad", "Radiation", "The facility radiation, which is the current maximum of all connected facility radiation monitors (excludes unit monitors).")
doc("ui_fac_linked", "Linked RTUs", "The number of RTU Gateways connected.")
sect("Automatic Control")
text("This interface is used for managing automatic facility control, which only applies to units set via the unit display to be under auto control. This includes setpoints, status, configuration, and control.")
doc("ui_fac_auto_bt", "Burn Target", "When set to Combined Burn Rate mode, assigned units will ramp up to meet this combined target.")
doc("ui_fac_auto_ct", "Charge Target", "When set to Charge Level mode, assigned units will run to reach and maintain this induction matrix charge level.")
doc("ui_fac_auto_gt", "Gen. Target", "When set to Generation Rate mode, assigned units will run to reach and maintain this continuous power output, using the induction matrix input rate.")
doc("ui_fac_save", "SAVE", "This saves your configuration without starting control.")
doc("ui_fac_start", "START", "This starts the configured automatic control.")
tip("START also includes the SAVE operation.")
doc("ui_fac_stop", "STOP", "This terminates automatic control, stopping assigned units.")
text("There are four automatic control modes, detailed further in System Usage > Automatic Control")
doc("ui_fac_auto_mmb", "Monitored Max Burn", "This runs all assigned units at the maximum configured rate.")
doc("ui_fac_auto_cbr", "Combined Burn Rate", "This runs assigned units to meet the target combined rate.")
doc("ui_fac_auto_cl", "Charge Level", "This runs assigned units to maintain an induction matrix charge level.")
doc("ui_fac_auto_gr", "Generation Rate", "This runs assigned units to meet a target induction matrix power input rate.")
doc("ui_fac_auto_lim", "Unit Limit", "Each unit can have a limit set that auto control will never exceed.")
doc("ui_fac_unit_ready", "Unit Status Ready", "A unit is only ready for auto control if all multiblocks are formed, online with data received, and there is no RPS trip.")
doc("ui_fac_unit_degraded", "Unit Status Degraded", "A unit is degraded if the reactor, boiler(s), and/or turbine(s) are faulted or not connected.")
sect("Waste Control")
text("Above unit statuses are the unit waste statuses, showing which are set to the auto waste mode and the actual current waste production of that unit.")
text("The facility automatic waste control interface is surrounded by a brown border and lets you configure that system, starting with the requested waste product.")
doc("ui_fac_waste_pu_fall_act", "Fallback Active", "When the system is falling back to plutonium production while SNAs cannot keep up.")
doc("ui_fac_waste_sps_lc_act", "SPS Disabled LC", "When the system is falling back to polonium production to prevent draining all power with the SPS while the induction matrix charge has dropped below 10% and not yet reached 15%.")
doc("ui_fac_waste_pu_fall", "Pu Fallback", "Switch from Po or Antimatter when the SNAs can't keep up (like at night).")
doc("ui_fac_waste_sps_lc", "Low Charge SPS", "Continue running antimatter production even at low induction matrix charge levels (<10%).")
sect("Induction Matrix")
text("The induction matrix statistics are shown at the bottom right, including fill bars for the FILL, I (input rate), and O (output rate).")
text("Averages are computed by the system while other data is directly from the device.")
doc("ui_fac_im_charge", "Charging", "Charge is increasing (more input than output).")
doc("ui_fac_im_charge", "Discharging", "Charge is draining (more output than input).")
doc("ui_fac_im_charge", "Max I/O Rate", "The induction providers are at their maximum rate.")
doc("ui_fac_eta", "ETA", "The ETA is based off a longer average so it may take a minute to stabilize, but will give a rough estimate of time to charge/discharge.")
target = docs.c_ui.flow
sect("Flow Diagram")
text("The coolant and waste flow monitor is one large P&ID (process and instrumentation diagram) showing an overview of those flows.")
text("Color-coded pipes are used to show the connections, and valve symbols \x10\x11 are used to show valves (redstone controlled pipes).")
doc("ui_flow_rates", "Flow Rates", "Flow rates are always shown below their respective pipes and sourced from devices when possible. The waste flow is based on the reactor burn rate, then everything downstream of the SNAs are based on the SNA production rate.")
doc("ui_flow_valves", "Standard Valves", "Valve naming (PV00-XX) is based on P&ID naming conventions. These count up across the whole facility, and use tags at the end to add clarity.")
note("The indicator next to the label turns on when the associated redstone RTU is connected.")
list(DOC_LIST_TYPE.BULLET, { "PU: Plutonium", "PO: Polonium", "PL: Po Pellets", "AM: Antimatter", "EMC: Emer. Coolant", "AUX: Aux. Coolant" })
doc("ui_flow_valve_open", "OPEN", "This indicates if the respective valve is commanded open.")
doc("ui_flow_prv", "PRVs", "Pressure Relief Valves (PRVs) are used to show the turbine steam dumping states of each turbine.")
list(DOC_LIST_TYPE.LED, { "Not Dumping", "Dumping Excess", "Dumping" }, { colors.gray, colors.yellow, colors.red })
sect("SNAs")
text("Solar Neutron Activators are shown on the flow diagram as a combined block due to the large variable count supported.")
tip("SNAs consume 10x the waste as they produce in antimatter, so take that into account before connecting too many SNAs.")
doc("ui_flow_sna_act", "ACTIVE", "The SNAs have a non-zero total flow.")
doc("ui_flow_sna_cnt", "CNT", "The count of SNAs assigned to the unit.")
doc("ui_flow_sna_peak_o", "PEAK\x1a", "The combined theoretical peak output the SNAs can achieve under full sunlight.")
doc("ui_flow_sna_max_o", "MAX \x1a", "The current combined maximum output rate of the SNAs (based on current sunlight).")
doc("ui_flow_sna_max_i", "\x1aMAX", "The computed combined maximum input rate (10x the output rate).")
doc("ui_flow_sna_in", "\x1aIN", "The current input rate into the SNAs.")
sect("Dynamic Tanks")
text("Dynamic tanks configured for the system are listed to the left. The title may start with U for unit tanks or F for facility tanks.")
text("The fill information and water level are shown below the status label.")
doc("ui_flow_dyn_fill", "FILL", "If filling is enabled by the tank mode (via Mekanism UI).")
doc("ui_flow_dyn_empty", "EMPTY", "If emptying is enabled by the tank mode (via Mekanism UI).")
sect("SPS")
doc("ui_flow_sps_in", "Input Rate", "The rate of polonium into the SPS.")
doc("ui_flow_sps_prod", "Production Rate", "The rate of antimatter produced by the SPS.")
sect("Statistics")
text("The sum of all unit's waste rate statistics are shown under the SPS block. These are combined current rates, not long-term sums.")
doc("ui_flow_stat_raw", "RAW WASTE", "The combined rate of raw waste generated by the reactors before processing.")
doc("ui_flow_stat_proc", "PROC. WASTE", "The combined rates of different waste product production. Pu is plutonium, Po is polonium, and PoPl is polonium pellets. Antimatter is shown in the SPS block.")
doc("ui_flow_stat_spent", "SPENT WASTE", "The combined rate of spent waste generated after processing.")
sect("Other Blocks")
text("Other blocks, such as CENTRIFUGE, correspond to devices that are not intended to be connected and/or serve as labels.")
target = docs.c_ui.unit
sect("Data Display")
text("The unit monitor contains extensive data information, including annunciator and alarm displays described in the associated sections in the Operator UIs section.")
doc("ui_unit_core", "Core Map", "A core map diagram is shown at the top right, colored by core temperature. The layout is based off of the multiblock dimensions.")
list(DOC_LIST_TYPE.BULLET, { "Gray <= 300\xb0C", "Blue <= 350\xb0C", "Green < 600\xb0C", "Yellow < 100\xb0C", "Orange < 1200\xb0C", "Red < 1300\xb0C", "Pink >= 1300\xb0C" })
text("Internal tanks (fuel, cooled coolant, heated coolant, and waste) are displayed below the core map, labeled F, C, H, and W, respectively.")
doc("ui_unit_rad", "Radiation", "The unit radiation, which is the current maximum of all connected radiation monitors assigned to this unit.")
text("Multiple other data values are shown but should be self-explanatory.")
sect("Controls")
text("A set of buttons and the burn rate input are used for manual reactor control. When in auto mode, unavailable controls are disabled. The burn rate is only applied after SET is pressed.")
doc("ui_unit_start", "START", "This starts the reactor at the requested burn rate.")
doc("ui_unit_scram", "SCRAM", "This SCRAMs the reactor.")
doc("ui_unit_ack", "ACK \x13", "This acknowledges alarms on this unit.")
doc("ui_unit_reset", "RESET", "This resets the RPS for this unit.")
sect("Auto Control")
text("To put this unit under auto control, select an option other than Manual. You must press SET to apply this, but cannot change this while auto control is active. The priorities available are described in System Usage > Automatic Control.")
doc("ui_unit_prio", "Prio. Group", "This displays the unit's auto control priority group.")
doc("ui_unit_ready", "READY", "This indicates if the unit is ready for auto control. A unit is only ready for auto control if all multiblocks are formed, online with data received, and there is no RPS trip.")
doc("ui_unit_standby", "STANDBY", "This indicates if the unit is set to auto control and that is active, but the auto control does not currently need this reactor to run at the moment, so it is idle.")
sect("Waste Processing")
text("The unit's waste output configuration can be set via these buttons. Auto will put this unit under control of the facility waste control, otherwise the system will always command the requested option for this unit.")
--#endregion
--#endregion
--#region Front Panels
docs.fp = {
common = {}, r_plc = {}, rtu_gw = {}, supervisor = {}, coordinator = {}
}
target = docs.fp.common
sect("Core Status")
doc("fp_status", "STATUS", "This is always lit, except on the Reactor PLC (see Reactor PLC section).")
doc("fp_heartbeat", "HEARTBEAT", "This alternates between lit and unlit as the main loop on the device runs. If this freezes, something is wrong and the logs will indicate why.")
sect("Hardware & Network")
doc("fp_modem", "MODEM", "This lights up if the wireless/ender modem is connected. In parentheses is the unique computer ID of this device, which will show up in places such as the supervisor's connection lists.")
doc("fp_modem", "MODEM", "This lights up if the wireless/ender modem is connected. In parentheses is the unique computer ID of this device, which will show up in places such as the Supervisor's connection lists.")
doc("fp_modem", "NETWORK", "This is present when in standard color modes and indicates the network status using multiple colors.")
list(DOC_LIST_TYPE.LED, { "not linked", "linked", "link denied", "bad comms version", "duplicate PLC" }, { colors.gray, colors.green, colors.red, colors.orange, colors.yellow })
text("You can fix \"bad comms version\" by ensuring all devices are up-to-date, as this indicates a communications protocol version mismatch. Note that yellow is Reactor PLC-specific, indicating duplicate unit IDs in use.")
doc("fp_nt_linked", "NT LINKED", "(color accessibility modes only)", "This indicates the device is linked to the supervisor.")
doc("fp_nt_version", "NT VERSION", "(color accessibility modes only)", "This indicates the communications versions of the supervisor and this device do not match. Make sure everything is up-to-date.")
doc("fp_nt_linked", "NT LINKED", "(color accessibility modes only)", "This indicates the device is linked to the Supervisor.")
doc("fp_nt_version", "NT VERSION", "(color accessibility modes only)", "This indicates the communications versions of the Supervisor and this device do not match. Make sure everything is up-to-date.")
sect("Versions")
doc("fp_fw", "FW", "Firmware application version of this device.")
doc("fp_nt", "NT", "Network (comms) version this device has. These must match between devices in order for them to connect.")
target = docs.fp.r_plc
sect("Overview")
text("Documentation for Reactor PLC-specific front panel items are below. Refer to 'Common Items' for the items not covered in this section.")
sect("Core Status")
doc("fp_status", "STATUS", "This is green once the PLC is initialized and OK (has all its peripherals) and red if something is wrong, in which case you should refer to the other indicator lights (REACTOR & MODEM).")
sect("Hardware & Network")
@@ -194,8 +458,8 @@ doc("fp_emer_cool", "EMER COOLANT", "This is only present if PLC-controlled emer
doc("fp_rps_trip", "RPS TRIP", "Flashes when the RPS has SCRAM'd the reactor due to a safety trip.")
sect("RPS Conditions")
doc("fp_rps_man", "MANUAL", "The RPS was tripped manually (SCRAM by user, not via the Mekanism Reactor UI).")
doc("fp_rps_auto", "AUTOMATIC", "The RPS was tripped by the supervisor automatically.")
doc("fp_rps_to", "TIMEOUT", "The RPS tripped due to losing the supervisor connection.")
doc("fp_rps_auto", "AUTOMATIC", "The RPS was tripped by the Supervisor automatically.")
doc("fp_rps_to", "TIMEOUT", "The RPS tripped due to losing the Supervisor connection.")
doc("fp_rps_pflt", "PLC FAULT", "The RPS tripped due to a peripheral error.")
doc("fp_rps_rflt", "RCT FAULT", "The RPS tripped due to the reactor not being formed.")
doc("fp_rps_temp", "HI DAMAGE", "The RPS tripped due to being >=" .. const.RPS_LIMITS.MAX_DAMAGE_PERCENT .. "% damaged.")
@@ -206,6 +470,9 @@ doc("fp_rps_ccool", "LO CCOOLANT", "The RPS tripped due to having low levels of
doc("fp_rps_ccool", "HI HCOOLANT", "The RPS tripped due to having high levels of heated coolant (>" .. (const.RPS_LIMITS.MAX_HEATED_COLLANT_FILL * 100) .. "%).")
target = docs.fp.rtu_gw
sect("Overview")
text("Documentation for RTU Gateway-specific front panel items are below. Refer to 'Common Items' for the items not covered in this section.")
doc("fp_rtu_spkr", "SPEAKERS", "This is the count of speaker peripherals connected to this RTU Gateway.")
sect("Co-Routine States")
doc("fp_rtu_rt_main", "RT MAIN", "This indicates if the device's main loop co-routine is running.")
doc("fp_rtu_rt_comms", "RT COMMS", "This indicates if the communications handler co-routine is running.")
@@ -218,30 +485,49 @@ doc("fp_rtu_rt", "Device Assignment", "In each RTU entry row, the device identif
target = docs.fp.supervisor
sect("Round Trip Times")
doc("fp_sv_fw", "RTT", "Each connection has a round trip time, or RTT. Since the supervisor updates at a rate of 150ms, RTTs from ~150ms to ~300ms are typical. Higher RTTs indicate lag, and if they end up in the thousands there will be performance problems.")
doc("fp_sv_rtt", "RTT", "Each connection has a round trip time, or RTT. Since the Supervisor updates at a rate of 150ms, RTTs from ~150ms to ~300ms are typical. Higher RTTs indicate lag, and if they end up in the thousands there will be performance problems.")
list(DOC_LIST_TYPE.BULLET, { "green: <=300ms", "yellow: <=500ms ", "red: >500ms" })
sect("SVR Tab")
text("This tab includes information about the supervisor, covered by 'Common Items'.")
text("This tab includes information about the Supervisor, covered by 'Common Items'.")
sect("PLC Tab")
text("This tab lists the expected PLC connections based on the number of configured units. Status information about each connection is shown when linked.")
doc("fp_sv_link", "LINK", "This indicates if the reactor PLC is linked.")
doc("fp_sv_p_cmpid", "PLC Computer ID", "This shows the computer ID of the reactor PLC, or --- if disconnected.")
doc("fp_sv_p_fw", "PLC FW", "This shows the firmware version of the reactor PLC.")
doc("fp_sv_link", "LINK", "This indicates if the Reactor PLC is linked.")
doc("fp_sv_p_cmpid", "PLC Computer ID", "This shows the computer ID of the Reactor PLC, or --- if disconnected.")
doc("fp_sv_p_fw", "PLC FW", "This shows the firmware version of the Reactor PLC.")
sect("RTU Tab")
text("As RTU gateways connect to the supervisor, they will show up here along with some information.")
doc("fp_sv_r_cmpid", "RTU Computer ID", "At the start of the entry is an @ sign followed by the computer ID of the RTU gateway.")
doc("fp_sv_r_units", "UNITS", "This is a count of the number of RTUs configured on the RTU gateway (each line on the RTU gateway's front panel).")
doc("fp_sv_r_fw", "RTU FW", "This shows the firmware version of the RTU gateway.")
text("As RTU gateways connect to the Supervisor, they will show up here along with some information.")
doc("fp_sv_r_cmpid", "RTU Computer ID", "At the start of the entry is an @ sign followed by the computer ID of the RTU Gateway.")
doc("fp_sv_r_units", "UNITS", "This is a count of the number of RTUs configured on the RTU Gateway (each line on the RTU Gateway's front panel).")
doc("fp_sv_r_fw", "RTU FW", "This shows the firmware version of the RTU Gateway.")
sect("PKT Tab")
text("As pocket computers connect to the supervisor, they will show up here along with some information. The properties listed are the same as with RTU gateways (except for UNITS), so they will not be further described here.")
text("As pocket computers connect to the Supervisor, they will show up here along with some information. The properties listed are the same as with RTU gateways (except for UNITS), so they will not be further described here.")
sect("DEV Tab")
text("If nothing is connected, this will list all the expected RTU devices that aren't found. This page should be blank if everything is connected and configured correctly. If not, it will list certain types of detectable problems.")
doc("fp_sv_d_miss", "MISSING", "These items list missing devices, with the details that should be used in the RTU's configuration.")
doc("fp_sv_d_oor", "BAD INDEX", "If you have a configuration entry that has an index outside of the maximum number of devices configured on the supervisor, this will show up indicating what entry is incorrect. For example, if you specified a unit has 2 turbines and a #3 connected, it would show up here as out of range.")
doc("fp_sv_d_oor", "BAD INDEX", "If you have a configuration entry that has an index outside of the maximum number of devices configured on the Supervisor, this will show up indicating what entry is incorrect. For example, if you specified a unit has 2 turbines and a #3 connected, it would show up here as out of range.")
doc("fp_sv_d_dupe", "DUPLICATE", "If a device tries to connect that is configured the same as another, it will be rejected and show up here. If you try to connect two #1 turbines for a unit, that would fail and one would appear here.")
sect("INF Tab")
text("This tab gives information about the other tabs, along with extra details on the DEV tab.")
target = docs.fp.coordinator
sect("Round Trip Times")
doc("fp_crd_rtt", "RTT", "Each connection has a round trip time, or RTT. Since the Coordinator updates at a rate of 500ms, RTTs ~500ms - ~1000ms are typical. Higher RTTs indicate lag, which results in performance problems.")
list(DOC_LIST_TYPE.BULLET, { "green: <=1000ms", "yellow: <=1500ms ", "red: >1500ms" })
sect("CRD Tab")
text("This tab includes information about the Coordinator, partially covered by 'Common Items'.")
doc("fp_crd_spkr", "SPEAKER", "This indicates if the speaker is connected.")
doc("fp_crd_rt_main", "RT MAIN", "This indicates that the device's main loop co-routine is running.")
doc("fp_crd_rt_render", "RT RENDER", "This indicates that the Coordinator graphics renderer co-routine is running.")
doc("fp_crd_mon_main", "MAIN MONITOR", "The connection status of the main display monitor.")
doc("fp_crd_mon_flow", "FLOW MONITOR", "The connection status of the coolant and waste flow display monitor.")
doc("fp_crd_mon_unit", "UNIT X MONITOR", "The connection status of the monitor associated with a given unit.")
sect("API Tab")
text("This tab lists connected pocket computers. Refer to the Supervisor PKT tab documentation for details on fields.")
--#endregion
--#region Glossary
docs.glossary = {
abbvs = {}, terms = {}
}
@@ -249,8 +535,7 @@ docs.glossary = {
target = docs.glossary.abbvs
doc("G_ACK", "ACK", "Alarm ACKnowledge. Pressing this acknowledges that you understand an alarm occurred and would like to stop the audio tone(s).")
doc("G_Auto", "Auto", "Automatic.")
doc("G_CRD", "CRD", "Coordinator. Abbreviation for the coordinator computer.")
doc("G_DBG", "DBG", "Debug. Abbreviation for the debugging sessions from pocket computers found on the supervisor's front panel.")
doc("G_CRD", "CRD", "Coordinator. Abbreviation for the Coordinator computer.")
doc("G_FP", "FP", "Front Panel. See Terminology.")
doc("G_Hi", "Hi", "High.")
doc("G_Lo", "Lo", "Low.")
@@ -260,7 +545,7 @@ doc("G_PLC", "PLC", "Programmable Logic Controller. A device that not only repor
doc("G_PPM", "PPM", "Protected Peripheral Manager. This is an abstraction layer created for this project that prevents peripheral calls from crashing applications.")
doc("G_RCP", "RCP", "Reactor Coolant Pump. This is from real-world terminology with water-cooled (boiling water and pressurized water) reactors, but in this system it just reflects to the functioning of reactor coolant flow. See the annunciator page on it for more information.")
doc("G_RCS", "RCS", "Reactor Cooling System. The combination of all machines used to cool the reactor (turbines, boilers, dynamic tanks).")
doc("G_RPS", "RPS", "Reactor Protection System. A component of the reactor PLC responsible for keeping the reactor safe.")
doc("G_RPS", "RPS", "Reactor Protection System. A component of the Reactor PLC responsible for keeping the reactor safe.")
doc("G_RTU", "RT", "co-RouTine. This is used to identify the status of core Lua co-routines on front panels.")
doc("G_RTU", "RTU", "Remote Terminal Unit. Provides monitoring to and basic output from a SCADA system, interfacing with various types of devices/interfaces.")
doc("G_SCADA", "SCADA", "Supervisory Control and Data Acquisition. A control systems architecture used in a wide variety process control applications.")
@@ -269,6 +554,8 @@ doc("G_UI", "UI", "User Interface.")
target = docs.glossary.terms
doc("G_AssignedUnit", "Assigned Unit", "A unit that is assigned to an automatic control group (not assigned to Manual).")
doc("G_AuxCoolant", "Auxiliary Coolant", "A separate water input to the reactor or boiler to supplement return water from a turbine during initial ramp-up.")
doc("G_EmerCoolant", "Emergency Coolant", "A dynamic tank or other water supply used when a reactor or boiler does not have enough water to stop a runaway reactor overheat.")
doc("G_Fault", "Fault", "Something has gone wrong and/or failed to function.")
doc("G_FrontPanel", "Front Panel", "A basic interface on the front of a device for viewing and sometimes modifying its state. This is what you see when looking at a computer running one of the SCADA applications.")
doc("G_HighHigh", "High High", "Very High.")
@@ -282,4 +569,6 @@ doc("G_Tripped", "Tripped", "An alarm condition has been met, and is still met."
doc("G_Tripping", "Tripping", "Alarm condition(s) is/are met, but has/have not reached the minimum time before the condition(s) is/are deemed a problem.")
doc("G_TurbineTrip", "Turbine Trip", "The turbine stopped, which prevents heated coolant from being cooled. In Mekanism, this would occur when a turbine cannot generate any more energy due to filling its buffer and having no output with any remaining energy capacity.")
--#endregion
return docs

View File

@@ -7,13 +7,15 @@ local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local about_app = require("pocket.ui.apps.about")
local alarm_app = require("pocket.ui.apps.alarm")
local comps_app = require("pocket.ui.apps.comps")
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 rad_app = require("pocket.ui.apps.radiation")
local unit_app = require("pocket.ui.apps.unit")
local waste_app = require("pocket.ui.apps.waste")
@@ -45,7 +47,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)}
@@ -65,14 +67,16 @@ 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)
rad_app(page_div)
loader_app(page_div)
sys_apps(page_div)
diag_apps(page_div)
dummy_app(page_div)
about_app(page_div)
alarm_app(page_div)
comps_app(page_div)
-- verify all apps were created
assert(util.table_len(db.nav.get_containers()) == APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered")

View File

@@ -1,3 +1,7 @@
--
-- Dynamic Tank View
--
local types = require("scada-common.types")
local util = require("scada-common.util")
@@ -16,8 +20,9 @@ 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 COOLANT_TYPE = types.COOLANT_TYPE
local ALIGN = core.ALIGN
local cpair = core.cpair
local label = style.label
@@ -31,7 +36,7 @@ local mode_ind_s = {
-- create a dynamic tank view for the unit or facility app
---@param app pocket_app
---@param page nav_tree_page
---@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)
@@ -46,22 +51,35 @@ return function (app, page, panes, tank_pane, tank_id, ps, update)
local tank_page = app.new_page(page, #panes)
tank_page.tasks = { update }
TextBox{parent=tank_div,y=1,text="Dyn Tank",width=9}
local status = StateIndicator{parent=tank_div,x=10,y=1,states=style.dtank.states,value=1,min_width=12}
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=3,text="Fill",width=10,fg_bg=label}
local tank_pcnt = DataIndicator{parent=tank_div,x=14,y=3,label="",format="%5.2f",value=100,unit="%",lu_colors=lu_col,width=8,fg_bg=text_fg}
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=6,text=util.trinary(is_water,"Water","Sodium").." Level",width=12,fg_bg=label}
local level = HorizontalBar{parent=tank_div,y=7,bar_fg_bg=cpair(util.trinary(is_water,colors.blue,colors.lightBlue),colors.gray),height=1,width=21}
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=9,text="Tank Fill Mode",width=14,fg_bg=label}
local can_fill = IconIndicator{parent=tank_div,y=10,label="Fill",states=mode_ind_s}
local can_empty = IconIndicator{parent=tank_div,y=11,label="Empty",states=mode_ind_s}
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))

View File

@@ -0,0 +1,125 @@
--
-- Induction Matrix View
--
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,88 @@
--
-- SPS View
--
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

@@ -1,3 +1,7 @@
--
-- A Guide App Subsection
--
local log = require("scada-common.log")
local util = require("scada-common.util")
@@ -17,7 +21,7 @@ local LED = require("graphics.elements.indicators.LED")
local ALIGN = core.ALIGN
local cpair = core.cpair
local DOC_TYPE = docs.DOC_ITEM_TYPE
local DOC_TYPE = docs.DOC_ITEM_TYPE
local LIST_TYPE = docs.DOC_LIST_TYPE
-- new guide documentation section
@@ -34,13 +38,13 @@ return function (data, base_page, title, items, scroll_height)
local section_div = Div{parent=page_div,x=2}
table.insert(panes, section_div)
TextBox{parent=section_div,y=1,text=title,alignment=ALIGN.CENTER}
PushButton{parent=section_div,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=base_page.nav_to}
PushButton{parent=section_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=base_page.nav_to}
local view_page = app.new_page(section_page, #panes + 1)
local section_view_div = Div{parent=page_div,x=2}
table.insert(panes, section_view_div)
TextBox{parent=section_view_div,y=1,text=title,alignment=ALIGN.CENTER}
PushButton{parent=section_view_div,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=section_page.nav_to}
PushButton{parent=section_view_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=section_page.nav_to}
local name_list = ListBox{parent=section_div,x=1,y=3,scroll_height=60,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local def_list = ListBox{parent=section_view_div,x=1,y=3,scroll_height=scroll_height,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
@@ -49,7 +53,7 @@ return function (data, base_page, title, items, scroll_height)
local page_end
for i = 1, #items do
local item = items[i] ---@type pocket_doc_sect|pocket_doc_subsect|pocket_doc_text|pocket_doc_list
local item = items[i] ---@type pocket_doc_sect|pocket_doc_subsect|pocket_doc_text|pocket_doc_note|pocket_doc_tip|pocket_doc_list
if item.type == DOC_TYPE.SECTION then
---@cast item pocket_doc_sect
@@ -73,6 +77,8 @@ return function (data, base_page, title, items, scroll_height)
local _ = Div{parent=name_list,height=1}
end
table.insert(search_db, { string.lower(item.name), item.name, title, view })
local name_title = Div{parent=name_list,height=1}
TextBox{parent=name_title,x=1,text=title_text,fg_bg=cpair(colors.lightGray,colors.black)}
PushButton{parent=name_title,x=title_offs,y=1,text=item.name,alignment=ALIGN.LEFT,fg_bg=cpair(colors.green,colors.black),active_fg_bg=btn_active,callback=view}
@@ -108,6 +114,19 @@ return function (data, base_page, title, items, scroll_height)
TextBox{parent=def_list,text=item.text}
page_end = Div{parent=def_list,height=1,can_focus=true}
elseif item.type == DOC_TYPE.NOTE then
---@cast item pocket_doc_note
TextBox{parent=def_list,text=item.text,fg_bg=cpair(colors.gray,colors._INHERIT)}
page_end = Div{parent=def_list,height=1,can_focus=true}
elseif item.type == DOC_TYPE.TIP then
---@cast item pocket_doc_tip
TextBox{parent=def_list,text="TIP!",fg_bg=cpair(colors.orange,colors._INHERIT)}
TextBox{parent=def_list,text=item.text}
page_end = Div{parent=def_list,height=1,can_focus=true}
elseif item.type == DOC_TYPE.LIST then
---@cast item pocket_doc_list

View File

@@ -9,11 +9,9 @@ local core = require("graphics.core")
local AppMultiPane = require("graphics.elements.AppMultiPane")
local Div = require("graphics.elements.Div")
local TextBox = require("graphics.elements.TextBox")
local App = require("graphics.elements.controls.App")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
@@ -46,19 +44,16 @@ 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.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}
App{parent=apps_1,x=16,y=7,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=2,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}
TextBox{parent=apps_2,text="Diagnostic Apps",x=1,y=2,alignment=ALIGN.CENTER}
App{parent=apps_2,x=2,y=4,text="\x0f",title="Alarm",callback=function()open(APP_ID.ALARMS)end,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=active_fg_bg}
App{parent=apps_2,x=9,y=4,text="\x1e",title="LoopT",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg}
App{parent=apps_2,x=16,y=4,text="@",title="Comps",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
App{parent=apps_2,x=2,y=2,text="\x0f",title="Alarm",callback=function()open(APP_ID.ALARMS)end,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=active_fg_bg}
App{parent=apps_2,x=9,y=2,text="@",title="Comps",callback=function()open(APP_ID.COMPS)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
App{parent=apps_2,x=16,y=2,text="\x1e",title="Rad",callback=function()open(APP_ID.RADMON)end,app_fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=active_fg_bg}
return main
end

View File

@@ -1,3 +1,7 @@
--
-- Unit Boiler View
--
local types = require("scada-common.types")
local util = require("scada-common.util")
@@ -13,8 +17,8 @@ local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local VerticalBar = require("graphics.elements.indicators.VerticalBar")
local ALIGN = core.ALIGN

View File

@@ -1,3 +1,7 @@
--
-- Unit Reactor View
--
local types = require("scada-common.types")
local util = require("scada-common.util")
@@ -13,8 +17,8 @@ local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local VerticalBar = require("graphics.elements.indicators.VerticalBar")
local ALIGN = core.ALIGN

View File

@@ -1,3 +1,7 @@
--
-- Unit Turbine View
--
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")

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)
@@ -171,22 +177,29 @@ style.sps = {
}
}
style.waste = {
-- auto waste processing states
states = {
{ color = cpair(colors.black, colors.green), text = "PLUTONIUM" },
{ color = cpair(colors.black, colors.cyan), text = "POLONIUM" },
{ color = cpair(colors.black, colors.purple), text = "ANTI MATTER" }
},
states_abbrv = {
{ color = cpair(colors.black, colors.green), text = "Pu" },
{ color = cpair(colors.black, colors.cyan), text = "Po" },
{ color = cpair(colors.black, colors.purple), text = "AM" }
},
-- process radio button options
options = { "Plutonium", "Polonium", "Antimatter" },
-- unit waste selection
unit_opts = { "Auto", "Plutonium", "Polonium", "Antimatter" }
}
-- 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

324
reactor-plc/backplane.lua Normal file
View File

@@ -0,0 +1,324 @@
--
-- Reactor PLC System Core Peripheral Backplane
--
local log = require("scada-common.log")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local println = util.println
---@class plc_backplane
local backplane = {}
local _bp = {
smem = nil, ---@type plc_shared_memory
wlan_pref = true,
lan_iface = "",
act_nic = nil, ---@type nic
wd_nic = nil, ---@type nic|nil
wl_nic = nil ---@type nic|nil
}
-- initialize the system peripheral backplane<br>
---@param config plc_config
---@param __shared_memory plc_shared_memory
--- EVENT_CONSUMER: this function consumes events
function backplane.init(config, __shared_memory)
_bp.smem = __shared_memory
_bp.wlan_pref = config.PreferWireless
_bp.lan_iface = config.WiredModem
local plc_dev = __shared_memory.plc_dev
local plc_state = __shared_memory.plc_state
plc_state.degraded = false
-- Modem Init
if _bp.smem.networked then
-- init wired NIC
if type(_bp.lan_iface) == "string" then
local modem = ppm.get_modem(_bp.lan_iface)
local wd_nic = network.nic(modem)
log.info("BKPLN: WIRED PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. _bp.lan_iface)
_bp.wd_nic = wd_nic
_bp.act_nic = wd_nic -- set this as active for now
wd_nic.closeAll()
wd_nic.open(config.PLC_Channel)
plc_state.wd_modem = wd_nic.is_connected()
end
-- init wireless NIC(s)
if config.WirelessModem then
local modem, iface = ppm.get_wireless_modem()
local wl_nic = network.nic(modem)
log.info("BKPLN: WIRELESS PHY_" .. util.trinary(modem, "UP ", "DOWN") .. (iface or ""))
-- set this as active if connected or if both modems are disconnected and this is preferred
if (modem and _bp.wlan_pref) or not (_bp.act_nic and _bp.act_nic.is_connected()) then
_bp.act_nic = wl_nic
log.info("BKPLN: switched active to preferred wireless")
end
_bp.wl_nic = wl_nic
wl_nic.closeAll()
wl_nic.open(config.PLC_Channel)
plc_state.wl_modem = wl_nic.is_connected()
end
-- comms modem is required if networked
if not (plc_state.wd_modem or plc_state.wl_modem) then
println("startup> no comms modem found")
log.warning("BKPLN: no comms modem on startup")
plc_state.degraded = true
end
end
-- Reactor Init
---@diagnostic disable-next-line: assign-type-mismatch
plc_dev.reactor = ppm.get_fission_reactor()
plc_state.no_reactor = plc_dev.reactor == nil
-- we need a reactor, can at least do some things even if it isn't formed though
if plc_state.no_reactor then
log.info("BKPLN: REACTOR LINK_DOWN")
println("startup> fission reactor not found")
log.warning("BKPLN: no reactor on startup")
plc_state.degraded = true
plc_state.reactor_formed = false
-- mount a virtual peripheral to init the RPS with
local _, dev = ppm.mount_virtual()
plc_dev.reactor = dev
log.info("BKPLN: mounted virtual device as reactor")
else
log.info("BKPLN: REACTOR LINK_UP " .. ppm.get_iface(plc_dev.reactor))
if not plc_dev.reactor.isFormed() then
println("startup> fission reactor is not formed")
log.warning("BKPLN: reactor logic adapter detected, but reactor is not formed")
plc_state.degraded = true
plc_state.reactor_formed = false
end
end
end
-- get the active NIC
function backplane.active_nic() return _bp.act_nic end
-- handle a backplane peripheral attach
---@param iface string
---@param type string
---@param device table
---@param print_no_fp function
function backplane.attach(iface, type, device, print_no_fp)
local MQ__RPS_CMD = _bp.smem.q_types.MQ__RPS_CMD
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
local networked = _bp.smem.networked
local state = _bp.smem.plc_state
local dev = _bp.smem.plc_dev
local sys = _bp.smem.plc_sys
if type ~= nil and device ~= nil then
if state.no_reactor and (type == "fissionReactorLogicAdapter") then
-- reconnected reactor
log.info("BKPLN: REACTOR LINK_UP " .. iface)
dev.reactor = device
state.no_reactor = false
print_no_fp("reactor connected")
log.info("BKPLN: reactor connected")
-- we need to assume formed here as we cannot check in this main loop
-- RPS will identify if it isn't and this will get set false later
state.reactor_formed = true
-- determine if we are still in a degraded state
if ((not networked) or (state.wd_modem or state.wl_modem)) and state.reactor_formed then
state.degraded = false
end
_bp.smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
sys.rps.reconnect_reactor(dev.reactor)
if networked then
sys.plc_comms.reconnect_reactor(dev.reactor)
end
-- partial reset of RPS, specific to becoming formed/reconnected
-- without this, auto control can't resume on chunk load
sys.rps.reset_reattach()
elseif networked and type == "modem" then
---@cast device Modem
local m_is_wl = device.isWireless()
log.info(util.c("BKPLN: ", util.trinary(m_is_wl, "WIRELESS", "WIRED"), " PHY_ATTACH ", iface))
if wd_nic and (_bp.lan_iface == iface) then
-- connect this as the wired NIC
wd_nic.connect(device)
log.info("BKPLN: WIRED PHY_UP " .. iface)
print_no_fp("wired comms modem connected")
state.wd_modem = true
if (_bp.act_nic ~= wd_nic) and not _bp.wlan_pref then
-- switch back to preferred wired
_bp.act_nic = wd_nic
sys.plc_comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wired modem (preferred)")
end
elseif wl_nic and (not wl_nic.is_connected()) and m_is_wl then
-- connect this as the wireless NIC
wl_nic.connect(device)
log.info("BKPLN: WIRELESS PHY_UP " .. iface)
print_no_fp("wireless comms modem connected")
state.wl_modem = true
if (_bp.act_nic ~= wl_nic) and _bp.wlan_pref then
-- switch back to preferred wireless
_bp.act_nic = wl_nic
sys.plc_comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wireless modem (preferred)")
end
elseif wl_nic and m_is_wl then
-- the wireless NIC already has a modem
device.closeAll()
print_no_fp("standby wireless modem connected")
log.info("BKPLN: standby wireless modem connected")
else
device.closeAll()
print_no_fp("unassigned modem connected")
log.warning("BKPLN: unassigned modem connected")
end
-- determine if we are still in a degraded state
if (state.wd_modem or state.wl_modem) and state.reactor_formed and not state.no_reactor then
state.degraded = false
end
end
end
end
-- handle a backplane peripheral detach
---@param iface string
---@param type string
---@param device table
---@param print_no_fp function
function backplane.detach(iface, type, device, print_no_fp)
local MQ__RPS_CMD = _bp.smem.q_types.MQ__RPS_CMD
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
local state = _bp.smem.plc_state
local dev = _bp.smem.plc_dev
local sys = _bp.smem.plc_sys
if device == dev.reactor then
log.info("BKPLN: REACTOR LINK_DOWN " .. iface)
print_no_fp("reactor disconnected")
log.warning("BKPLN: reactor disconnected")
state.no_reactor = true
state.degraded = true
elseif _bp.smem.networked and type == "modem" then
---@cast device Modem
log.info(util.c("BKPLN: PHY_DETACH ", iface))
if wd_nic and wd_nic.is_modem(device) then
wd_nic.disconnect()
log.info("BKPLN: WIRED PHY_DOWN " .. iface)
state.wd_modem = false
elseif wl_nic and wl_nic.is_modem(device) then
wl_nic.disconnect()
log.info("BKPLN: WIRELESS PHY_DOWN " .. iface)
state.wl_modem = false
end
-- we only care if this is our active comms modem
if _bp.act_nic.is_modem(device) then
print_no_fp("active comms modem disconnected")
log.warning("BKPLN: active comms modem disconnected")
-- failover and try to find a new comms modem
if _bp.act_nic == wl_nic then
-- wireless active disconnected
-- try to find another wireless modem, otherwise switch to wired
local modem, m_iface = ppm.get_wireless_modem()
if wl_nic and modem then
log.info("BKPLN: found another wireless modem, using it for comms")
wl_nic.connect(modem)
log.info("BKPLN: WIRELESS PHY_UP " .. m_iface)
state.wl_modem = true
elseif wd_nic and wd_nic.is_connected() then
_bp.act_nic = wd_nic
sys.plc_comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wired modem")
else
-- no other wireless modems, wired unavailable
state.degraded = true
_bp.smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
end
elseif wl_nic and wl_nic.is_connected() then
-- wired active disconnected, wireless available
_bp.act_nic = wl_nic
sys.plc_comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wireless modem")
else
-- wired active disconnected, wireless unavailable
state.degraded = true
_bp.smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
end
elseif wd_nic and wd_nic.is_modem(device) then
-- wired, but not active
print_no_fp("standby wired modem disconnected")
log.info("BKPLN: standby wired modem disconnected")
elseif wl_nic and wl_nic.is_modem(device) then
-- wireless, but not active
print_no_fp("standby wireless modem disconnected")
log.info("BKPLN: standby wireless modem disconnected")
else
print_no_fp("unassigned modem disconnected")
log.warning("BKPLN: unassigned modem disconnected")
end
end
end
return backplane

View File

@@ -24,10 +24,12 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE
local self = {
checking_wl = true,
wd_modem = nil, ---@type Modem|nil
wl_modem = nil, ---@type Modem|nil
nic = nil, ---@type nic
net_listen = false,
sv_addr = comms.BROADCAST,
sv_seq_num = util.time_ms() * 10,
self_check_pass = true,
@@ -48,7 +50,7 @@ local function check_complete()
TextBox{parent=more,text="- ask for help on GitHub discussions or Discord"}
end
-- send a management packet to the supervisor
-- send a management packet to the supervisor (one-time broadcast)
---@param msg_type MGMT_TYPE
---@param msg table
local function send_sv(msg_type, msg)
@@ -56,10 +58,9 @@ local function send_sv(msg_type, msg)
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())
s_pkt.make(comms.BROADCAST, util.time_ms() * 10, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
self.nic.transmit(self.settings.SVR_Channel, self.settings.PLC_Channel, s_pkt)
self.sv_seq_num = self.sv_seq_num + 1
end
-- handle an establish message from the supervisor
@@ -75,16 +76,13 @@ local function handle_packet(packet)
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
-- success
elseif est_ack == ESTABLISH_ACK.DENY then
error_msg = "error: supervisor connection denied"
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
@@ -97,18 +95,20 @@ local function handle_packet(packet)
end
self.net_listen = false
self.run_test_btn.enable()
if error_msg then
self.self_check_msg(nil, false, error_msg)
else
self.self_check_msg(nil, true, "")
end
util.push_event("conn_test_complete", error_msg == nil)
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")
util.push_event("conn_test_complete", false)
end
-- execute the self-check
@@ -120,11 +120,23 @@ local function self_check()
self.self_check_pass = true
local modem = ppm.get_wireless_modem()
local cfg = self.settings
self.wd_modem = ppm.get_modem(cfg.WiredModem)
self.wl_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)
-- check for comms modems
if cfg.Networked then
if cfg.WiredModem then
self.self_check_msg("> check wired comms modem connected...", self.wd_modem, "please connect the wired comms modem " .. cfg.WiredModem)
end
if cfg.WirelessModem then
self.self_check_msg("> check wireless/ender modem connected...", self.wl_modem, "please connect an ender or wireless modem for wireless comms")
end
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,27 +144,37 @@ 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
self.self_check_msg("> check supervisor connection...")
if cfg.Networked and valid_cfg then
self.checking_wl = true
-- init mac as needed
if self.settings.AuthKey and string.len(self.settings.AuthKey) >= 8 then
network.init_mac(self.settings.AuthKey)
if cfg.WirelessModem and self.wl_modem then
self.self_check_msg("> check wireless 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
comms.set_trusted_range(cfg.TrustedRange)
self.nic = network.nic(self.wl_modem)
self.nic.closeAll()
self.nic.open(cfg.PLC_Channel)
self.net_listen = true
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, comms.CONN_TEST_FWV, DEVICE_TYPE.PLC, cfg.UnitID })
tcd.dispatch_unique(8, handle_timeout)
elseif cfg.WiredModem and self.wd_modem then
-- skip to wired
util.push_event("conn_test_complete", true)
else
network.deinit_mac()
self.self_check_msg("> no modem, can't test supervisor connection", false)
end
self.nic = network.nic(modem)
self.nic.closeAll()
self.nic.open(self.settings.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 })
tcd.dispatch_unique(8, handle_timeout)
else
if self.self_check_pass then check_complete() end
self.run_test_btn.enable()
@@ -236,4 +258,44 @@ function check.receive_sv(side, sender, reply_to, message, distance)
end
end
-- handle completed connection tests
---@param pass boolean
function check.conn_test_callback(pass)
local cfg = self.settings
if self.checking_wl then
if not pass then
self.self_check_msg(nil, false, "make sure your supervisor is running, listening on the wireless interface, 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
if cfg.WiredModem and self.wd_modem then
self.checking_wl = false
self.self_check_msg("> check wired supervisor connection...")
comms.set_trusted_range(0)
self.nic = network.nic(self.wd_modem)
self.nic.closeAll()
self.nic.open(cfg.PLC_Channel)
self.net_listen = true
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, comms.CONN_TEST_FWV, DEVICE_TYPE.PLC, cfg.UnitID })
tcd.dispatch_unique(8, handle_timeout)
else
if self.self_check_pass then check_complete() end
self.run_test_btn.enable()
end
else
if not pass then
self.self_check_msg(nil, false, "make sure your supervisor is running, listening on the wired interface, the wire is intact, and your channels are correct")
end
if self.self_check_pass then check_complete() end
self.run_test_btn.enable()
end
end
return check

View File

@@ -1,4 +1,5 @@
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
local util = require("scada-common.util")
@@ -20,6 +21,8 @@ local TextField = require("graphics.elements.form.TextField")
local IndLight = require("graphics.elements.indicators.IndicatorLight")
local tri = util.trinary
local cpair = core.cpair
local RIGHT = core.ALIGN.RIGHT
@@ -30,6 +33,10 @@ local self = {
set_networked = nil, ---@type function
bundled_emcool = nil, ---@type function
wireless = nil, ---@type Checkbox
wl_pref = nil, ---@type Checkbox
wired = nil, ---@type Checkbox
range = nil, ---@type NumberField
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type PushButton
auth_key_textbox = nil, ---@type TextBox
@@ -82,8 +89,9 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
local plc_c_2 = Div{parent=plc_cfg,x=2,y=4,width=49}
local plc_c_3 = Div{parent=plc_cfg,x=2,y=4,width=49}
local plc_c_4 = Div{parent=plc_cfg,x=2,y=4,width=49}
local plc_c_5 = Div{parent=plc_cfg,x=2,y=4,width=49}
local plc_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4}}
local plc_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4,plc_c_5}}
TextBox{parent=plc_cfg,x=1,y=2,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)}
@@ -152,13 +160,20 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
function self.bundled_emcool(en) if en then color.enable() else color.disable() end end
TextBox{parent=plc_c_5,x=1,y=1,height=5,text="Advanced Options"}
local invert = Checkbox{parent=plc_c_5,x=1,y=3,label="Invert",default=ini_cfg.EmerCoolInvert,box_fg_bg=cpair(colors.orange,colors.black)}
TextBox{parent=plc_c_5,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=plc_c_5,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local function submit_emcool()
tmp_cfg.EmerCoolSide = side_options_map[side.get_value()]
tmp_cfg.EmerCoolColor = util.trinary(bundled.get_value(), color_options_map[color.get_value()], nil)
tmp_cfg.EmerCoolColor = tri(bundled.get_value(), color_options_map[color.get_value()], nil)
tmp_cfg.EmerCoolInvert = invert.get_value()
next_from_plc()
end
PushButton{parent=plc_c_4,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=plc_c_4,x=33,y=14,min_width=10,text="Advanced",callback=function()plc_pane.set_value(5)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=plc_c_4,x=44,y=14,text="Next \x1a",callback=submit_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
@@ -168,22 +183,88 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."}
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=1,text="Please select the network interface(s)."}
TextBox{parent=net_c_1,x=41,y=1,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
TextBox{parent=net_c_1,x=1,y=8,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=11,text="PLC Channel"}
local plc_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=9,y=12,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg}
local function en_dis_pref()
if self.wireless.get_value() and self.wired.get_value() then
self.wl_pref.enable()
else
self.wl_pref.set_value(self.wireless.get_value())
self.wl_pref.disable()
end
end
local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function on_wired_change(_)
en_dis_pref()
tool_ctl.gen_modem_list()
end
self.wireless = Checkbox{parent=net_c_1,x=1,y=3,label="Wireless/Ender Modem",default=ini_cfg.WirelessModem,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=en_dis_pref}
self.wl_pref = Checkbox{parent=net_c_1,x=30,y=3,label="Prefer Wireless",default=ini_cfg.PreferWireless,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
self.wired = Checkbox{parent=net_c_1,x=1,y=5,label="Wired Modem",default=ini_cfg.WiredModem~=false,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=on_wired_change}
TextBox{parent=net_c_1,x=3,y=6,text="this one MUST ONLY connect to SCADA computers",fg_bg=cpair(colors.red,colors._INHERIT)}
TextBox{parent=net_c_1,x=3,y=7,text="connecting it to peripherals will cause issues",fg_bg=g_lg_fg_bg}
local modem_list = ListBox{parent=net_c_1,x=1,y=8,height=5,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local modem_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
en_dis_pref()
local function submit_interfaces()
tmp_cfg.WirelessModem = self.wireless.get_value()
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
tmp_cfg.PreferWireless = self.wl_pref.get_value()
else
tmp_cfg.PreferWireless = tmp_cfg.WirelessModem
self.wl_pref.set_value(tmp_cfg.PreferWireless)
end
if not self.wired.get_value() then
tmp_cfg.WiredModem = false
tool_ctl.gen_modem_list()
end
if not (self.wired.get_value() or self.wireless.get_value()) then
modem_err.set_value("Please select a modem type.")
modem_err.show()
elseif self.wired.get_value() and type(tmp_cfg.WiredModem) ~= "string" then
modem_err.set_value("Please select a wired modem.")
modem_err.show()
else
if tmp_cfg.WirelessModem then
self.range.enable()
else
self.range.set_value(0)
self.range.disable()
end
net_pane.set_value(2)
modem_err.hide(true)
end
end
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_interfaces,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,text="Please set the network channels below."}
TextBox{parent=net_c_2,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=8,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_2,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=11,text="PLC Channel"}
local plc_chan = NumberField{parent=net_c_2,x=1,y=12,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=9,y=12,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg}
local chan_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_channels()
local svr_c = tonumber(svr_chan.get_value())
@@ -191,7 +272,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
if svr_c ~= nil and plc_c ~= nil then
tmp_cfg.SVR_Channel = svr_c
tmp_cfg.PLC_Channel = plc_c
net_pane.set_value(2)
net_pane.set_value(3)
chan_err.hide(true)
elseif svr_c == nil then
chan_err.set_value("Please set the supervisor channel.")
@@ -202,54 +283,62 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
end
end
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,text="Connection Timeout"}
local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,text="Connection Timeout"}
local timeout = NumberField{parent=net_c_3,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_3,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=3,height=4,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=8,text="Trusted Range"}
local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=1,y=10,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=8,text="Trusted Range (Wireless Only)"}
self.range = NumberField{parent=net_c_3,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=net_c_3,x=1,y=10,height=4,text="Setting this to a value larger than 0 prevents wireless connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
local p2_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local n3_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_ct_tr()
local timeout_val = tonumber(timeout.get_value())
local range_val = tonumber(range.get_value())
if timeout_val ~= nil and range_val ~= nil then
tmp_cfg.ConnTimeout = timeout_val
tmp_cfg.TrustedRange = range_val
net_pane.set_value(3)
p2_err.hide(true)
elseif timeout_val == nil then
p2_err.set_value("Please set the connection timeout.")
p2_err.show()
local range_val = tonumber(self.range.get_value())
if timeout_val == nil then
n3_err.set_value("Please set the connection timeout.")
n3_err.show()
elseif tmp_cfg.WirelessModem and (range_val == nil) then
n3_err.set_value("Please set the trusted range.")
n3_err.show()
else
p2_err.set_value("Please set the trusted range.")
p2_err.show()
tmp_cfg.ConnTimeout = timeout_val
tmp_cfg.TrustedRange = tri(tmp_cfg.WirelessModem, range_val, 0)
if tmp_cfg.WirelessModem then
net_pane.set_value(4)
else
main_pane.set_value(4)
tmp_cfg.AuthKey = ""
end
n3_err.hide(true)
end
end
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
TextBox{parent=net_c_3,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
TextBox{parent=net_c_4,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for wireless security on multiplayer servers. All devices on the same wireless network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=11,text="Facility Auth Key"}
local key, _ = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
TextBox{parent=net_c_4,x=1,y=11,text="Auth Key (Wireless Only, Not Used for Wired)"}
local key, _ = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
local function censor_key(enable) key.censor(util.trinary(enable, "*", nil)) end
local function censor_key(enable) key.censor(tri(enable, "*", nil)) end
local hide_key = Checkbox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
local hide_key = Checkbox{parent=net_c_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
hide_key.set_value(true)
censor_key(true)
local key_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local key_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_auth()
local v = key.get_value()
@@ -260,8 +349,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
else key_err.show() end
end
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
@@ -274,7 +363,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
@@ -320,7 +409,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
TextBox{parent=clr_c_1,x=1,y=7,text="Front Panel Theme"}
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."}
@@ -359,7 +448,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local function back_from_colors()
main_pane.set_value(util.trinary(tool_ctl.jumped_to_color, 1, 4))
main_pane.set_value(tri(tool_ctl.jumped_to_color, 1, 4))
tool_ctl.jumped_to_color = false
recolor(1)
end
@@ -461,10 +550,14 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
try_set(side, side_to_idx(ini_cfg.EmerCoolSide))
try_set(bundled, ini_cfg.EmerCoolColor ~= nil)
if ini_cfg.EmerCoolColor ~= nil then try_set(color, color_to_idx(ini_cfg.EmerCoolColor)) end
try_set(invert, ini_cfg.EmerCoolInvert)
try_set(self.wireless, ini_cfg.WirelessModem)
try_set(self.wired, ini_cfg.WiredModem ~= false)
try_set(self.wl_pref, ini_cfg.PreferWireless)
try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(plc_chan, ini_cfg.PLC_Channel)
try_set(timeout, ini_cfg.ConnTimeout)
try_set(range, ini_cfg.TrustedRange)
try_set(self.range, ini_cfg.TrustedRange)
try_set(key, ini_cfg.AuthKey)
try_set(mode, ini_cfg.LogMode)
try_set(path, ini_cfg.LogPath)
@@ -533,9 +626,11 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
if tmp_cfg.EmerCoolEnable then
tmp_cfg.EmerCoolSide = config.EMERGENCY_COOL.side
tmp_cfg.EmerCoolColor = config.EMERGENCY_COOL.color
tmp_cfg.EmerCoolInvert = false
else
tmp_cfg.EmerCoolSide = nil
tmp_cfg.EmerCoolColor = nil
tmp_cfg.EmerCoolInvert = false
end
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
@@ -579,7 +674,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
local val = util.strval(raw)
if f[1] == "AuthKey" and raw then val = string.rep("*", string.len(val))
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "LogMode" then val = tri(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "EmerCoolColor" and raw ~= nil then val = rsio.color_name(raw)
elseif f[1] == "FrontPanelTheme" then
val = util.strval(themes.fp_theme_name(raw))
@@ -589,10 +684,10 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
if val == "nil" then val = "<not set>" end
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
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
@@ -611,6 +706,59 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
end
end
-- generate the list of available/assigned wired modems
function tool_ctl.gen_modem_list()
modem_list.remove_all()
local enable = self.wired.get_value()
local function select(iface)
tmp_cfg.WiredModem = iface
tool_ctl.gen_modem_list()
end
local modems = ppm.get_wired_modem_list()
local missing = { tmp = true, ini = true }
for iface, _ in pairs(modems) do
if ini_cfg.WiredModem == iface then missing.ini = false end
if tmp_cfg.WiredModem == iface then missing.tmp = false end
end
if missing.tmp and tmp_cfg.WiredModem then
local line = Div{parent=modem_list,x=1,y=1,height=1}
TextBox{parent=line,x=1,y=1,width=4,text="Used",fg_bg=cpair(tri(enable,colors.blue,colors.gray),colors.white)}
PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}.disable()
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
TextBox{parent=line,x=25,y=1,text=tmp_cfg.WiredModem}
end
if missing.ini and ini_cfg.WiredModem and (tmp_cfg.WiredModem ~= ini_cfg.WiredModem) then
local line = Div{parent=modem_list,x=1,y=1,height=1}
local used = tmp_cfg.WiredModem == ini_cfg.WiredModem
TextBox{parent=line,x=1,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(ini_cfg.WiredModem)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
TextBox{parent=line,x=25,y=1,text=ini_cfg.WiredModem}
if used or not enable then select_btn.disable() end
end
-- list wired modems
for iface, _ in pairs(modems) do
local line = Div{parent=modem_list,x=1,y=1,height=1}
local used = tmp_cfg.WiredModem == iface
TextBox{parent=line,x=1,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(iface)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
TextBox{parent=line,x=15,y=1,text=iface}
if used or not enable then select_btn.disable() end
end
end
--#endregion
end

View File

@@ -3,6 +3,7 @@
--
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
@@ -32,7 +33,9 @@ local changes = {
{ "v1.6.2", { "AuthKey minimum length is now 8 (if set)" } },
{ "v1.6.8", { "ConnTimeout can now have a fractional part" } },
{ "v1.6.15", { "Added front panel UI theme", "Added color accessibility modes" } },
{ "v1.7.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } }
{ "v1.7.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
{ "v1.8.21", { "Added option to invert emergency coolant redstone control" } },
{ "v1.10.0", { "Added support for wired communications modems" } }
}
---@class plc_configurator
@@ -67,6 +70,8 @@ local tool_ctl = {
gen_summary = nil, ---@type function
load_legacy = nil, ---@type function
gen_modem_list = function () end
}
---@class plc_config
@@ -76,6 +81,10 @@ local tmp_cfg = {
EmerCoolEnable = false,
EmerCoolSide = nil, ---@type string|nil
EmerCoolColor = nil, ---@type color|nil
EmerCoolInvert = false, ---@type boolean
WirelessModem = true,
WiredModem = false, ---@type string|false
PreferWireless = true,
SVR_Channel = nil, ---@type integer
PLC_Channel = nil, ---@type integer
ConnTimeout = nil, ---@type number
@@ -100,6 +109,10 @@ local fields = {
{ "EmerCoolEnable", "Emergency Coolant", false },
{ "EmerCoolSide", "Emergency Coolant Side", nil },
{ "EmerCoolColor", "Emergency Coolant Color", nil },
{ "EmerCoolInvert", "Emergency Coolant Invert", false },
{ "WirelessModem", "Wireless/Ender Comms Modem", true },
{ "WiredModem", "Wired Comms Modem", false },
{ "PreferWireless", "Prefer Wireless Modem", true },
{ "SVR_Channel", "SVR Channel", 16240 },
{ "PLC_Channel", "PLC Channel", 16241 },
{ "ConnTimeout", "Connection Timeout", 5 },
@@ -258,8 +271,13 @@ function configurator.configure(ask_config)
load_settings(settings_cfg, true)
tool_ctl.has_config = load_settings(ini_cfg)
-- set tmp_cfg so interface lists are correct
tmp_cfg.WiredModem = ini_cfg.WiredModem
reset_term()
ppm.mount_all()
-- set overridden colors
for i = 1, #style.colors do
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
@@ -269,6 +287,8 @@ function configurator.configure(ask_config)
local display = DisplayBox{window=term.current(),fg_bg=style.root}
config_view(display)
tool_ctl.gen_modem_list()
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
@@ -285,6 +305,16 @@ function configurator.configure(ask_config)
display.handle_paste(param1)
elseif event == "modem_message" then
check.receive_sv(param1, param2, param3, param4, param5)
elseif event == "conn_test_complete" then
check.conn_test_callback(param1)
elseif event == "peripheral_detach" then
---@diagnostic disable-next-line: discard-returns
ppm.handle_unmount(param1)
tool_ctl.gen_modem_list()
elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1)
tool_ctl.gen_modem_list()
end
if event == "terminate" then return end

View File

@@ -33,7 +33,7 @@ function databus.rps_scram() dbus_iface.rps_scram() end
-- transmit a command to the RPS to reset
function databus.rps_reset() dbus_iface.rps_reset() end
-- transmit firmware versions across the bus
-- transmit firmware versions
---@param plc_v string PLC version
---@param comms_v string comms version
function databus.tx_versions(plc_v, comms_v)
@@ -41,19 +41,19 @@ function databus.tx_versions(plc_v, comms_v)
databus.ps.publish("comms_version", comms_v)
end
-- transmit unit ID across the bus
-- transmit unit ID
---@param id integer unit ID
function databus.tx_id(id)
databus.ps.publish("unit_id", id)
end
-- transmit hardware status across the bus
-- transmit hardware status
---@param plc_state plc_state
function databus.tx_hw_status(plc_state)
databus.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2)))
databus.ps.publish("has_modem", not plc_state.no_modem)
databus.ps.publish("degraded", plc_state.degraded)
databus.ps.publish("init_ok", plc_state.init_ok)
databus.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2)))
databus.ps.publish("has_wd_modem", plc_state.wd_modem)
databus.ps.publish("has_wl_modem", plc_state.wl_modem)
end
-- transmit thread (routine) statuses
@@ -63,19 +63,19 @@ function databus.tx_rt_status(thread, ok)
databus.ps.publish(util.c("routine__", thread), ok)
end
-- transmit supervisor link state across the bus
-- transmit supervisor link state
---@param state integer
function databus.tx_link_state(state)
databus.ps.publish("link_state", state)
end
-- transmit reactor enable state across the bus
-- transmit reactor enable state
---@param active any reactor active
function databus.tx_reactor_state(active)
databus.ps.publish("reactor_active", active == true)
end
-- transmit RPS data across the bus
-- transmit RPS data
---@param tripped boolean RPS tripped
---@param status boolean[] RPS status
---@param emer_cool_active boolean RPS activated the emergency coolant
@@ -95,11 +95,4 @@ function databus.tx_rps(tripped, status, emer_cool_active)
databus.ps.publish("emer_cool", emer_cool_active)
end
-- link a function to receive data from the bus
---@param field string field name
---@param func function function to link
function databus.rx_field(field, func)
databus.ps.subscribe(field, func)
end
return databus

View File

@@ -35,10 +35,11 @@ local ind_red = style.ind_red
-- create new front panel view
---@param panel DisplayBox main displaybox
local function init(panel)
---@param config plc_config configuraiton
local function init(panel, config)
local s_hi_box = style.theme.highlight_box
local disabled_fg = style.fp.disabled_fg
local term_w, _ = 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)
@@ -49,18 +50,32 @@ local function init(panel)
local system = Div{parent=panel,width=14,height=18,x=2,y=3}
local init_ok = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local degraded = LED{parent=system,label="STATUS",colors=cpair(colors.red,colors.green)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn}
system.line_break()
init_ok.register(databus.ps, "init_ok", init_ok.update)
degraded.register(databus.ps, "degraded", degraded.update)
heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green}
local modem = LED{parent=system,label="MODEM",colors=ind_grn}
reactor.register(databus.ps, "reactor_dev_state", reactor.update)
if config.Networked then
if config.WirelessModem and config.WiredModem then
local wd_modem = LED{parent=system,label="WD MODEM",colors=ind_grn}
local wl_modem = LED{parent=system,label="WL MODEM",colors=ind_grn}
wd_modem.register(databus.ps, "has_wd_modem", wd_modem.update)
wl_modem.register(databus.ps, "has_wl_modem", wl_modem.update)
else
local modem = LED{parent=system,label="MODEM",colors=ind_grn}
modem.register(databus.ps, util.trinary(config.WirelessModem, "has_wl_modem", "has_wd_modem"), modem.update)
end
else
local _ = LED{parent=system,label="MODEM",colors=ind_grn}
end
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
@@ -97,9 +112,6 @@ local function init(panel)
system.line_break()
reactor.register(databus.ps, "reactor_dev_state", reactor.update)
modem.register(databus.ps, "has_modem", modem.update)
local rt_main = LED{parent=system,label="RT MAIN",colors=ind_grn}
local rt_rps = LED{parent=system,label="RT RPS",colors=ind_grn}
local rt_cmtx = LED{parent=system,label="RT COMMS TX",colors=ind_grn}
@@ -113,15 +125,11 @@ local function init(panel)
rt_cmrx.register(databus.ps, "routine__comms_rx", rt_cmrx.update)
rt_sctl.register(databus.ps, "routine__spctl", rt_sctl.update)
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=5,width=6,text=comp_id,fg_bg=disabled_fg}
--
-- status & controls
-- status & controls & hardware labeling
--
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,34 +139,33 @@ 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)
--
-- about footer
--
local hw_labels = Rectangle{parent=status,width=status.get_width()-2,height=5,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true}
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"}
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("%03d", os.getComputerID())
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)
TextBox{parent=hw_labels,text="FW "..databus.ps.get("version"),fg_bg=s_hi_box}
TextBox{parent=hw_labels,text="NT v"..databus.ps.get("comms_version"),fg_bg=s_hi_box}
TextBox{parent=hw_labels,text="SN "..comp_id.."-PLC",fg_bg=s_hi_box}
--
-- 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,8 +23,7 @@ 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."
@@ -44,7 +43,11 @@ function plc.load_config()
config.EmerCoolEnable = settings.get("EmerCoolEnable")
config.EmerCoolSide = settings.get("EmerCoolSide")
config.EmerCoolColor = settings.get("EmerCoolColor")
config.EmerCoolInvert = settings.get("EmerCoolInvert")
config.WirelessModem = settings.get("WirelessModem")
config.WiredModem = settings.get("WiredModem")
config.PreferWireless = settings.get("PreferWireless")
config.SVR_Channel = settings.get("SVR_Channel")
config.PLC_Channel = settings.get("PLC_Channel")
config.ConnTimeout = settings.get("ConnTimeout")
@@ -70,7 +73,11 @@ function plc.validate_config(cfg)
cfv.assert_type_int(cfg.UnitID)
cfv.assert_type_bool(cfg.EmerCoolEnable)
if cfg.Networked == true then
if cfg.Networked then
cfv.assert_type_bool(cfg.WirelessModem)
cfv.assert((cfg.WiredModem == false) or (type(cfg.WiredModem) == "string"))
cfv.assert(cfg.WirelessModem or (type(cfg.WiredModem) == "string"))
cfv.assert_type_bool(cfg.PreferWireless)
cfv.assert_channel(cfg.SVR_Channel)
cfv.assert_channel(cfg.PLC_Channel)
cfv.assert_type_num(cfg.ConnTimeout)
@@ -99,6 +106,7 @@ function plc.validate_config(cfg)
if cfg.EmerCoolEnable then
cfv.assert_eq(rsio.is_valid_side(cfg.EmerCoolSide), true)
cfv.assert_eq(cfg.EmerCoolColor == nil or rsio.is_color(cfg.EmerCoolColor), true)
cfv.assert_type_bool(cfg.EmerCoolInvert)
end
return cfv.valid()
@@ -117,7 +125,7 @@ function plc.rps_init(reactor, is_formed)
reactor_enabled = false,
enabled_at = 0,
emer_cool_active = nil, ---@type boolean
formed = is_formed,
formed = is_formed, ---@type boolean|nil
force_disabled = false,
tripped = false,
trip_cause = "ok" ---@type rps_trip_cause
@@ -167,7 +175,8 @@ function plc.rps_init(reactor, is_formed)
local function _set_emer_cool(state)
-- check if this was configured: if it's a table, fields have already been validated.
if config.EmerCoolEnable then
local level = rsio.digital_write_active(rsio.IO.U_EMER_COOL, state)
-- use ~= as XOR for simple inversion
local level = rsio.digital_write_active(rsio.IO.U_EMER_COOL, config.EmerCoolInvert ~= state)
if level ~= false then
if rsio.is_color(config.EmerCoolColor) then
@@ -362,29 +371,35 @@ function plc.rps_init(reactor, is_formed)
return public.activate()
end
-- check all safety conditions
-- check all safety conditions if we have a formed reactor, otherwise handle a subset of conditions
---@nodiscard
---@param has_reactor boolean if the PLC state indicates we have a reactor
---@return boolean tripped, rps_trip_cause trip_status, boolean first_trip
function public.check()
function public.check(has_reactor)
local status = RPS_TRIP_CAUSE.OK
local was_tripped = self.tripped
local first_trip = false
if self.formed then
-- update state
parallel.waitForAll(
_is_formed,
_is_force_disabled,
_high_damage,
_high_temp,
_low_coolant,
_excess_waste,
_excess_heated_coolant,
_insufficient_fuel
)
if has_reactor then
if self.formed then
-- update state
parallel.waitForAll(
_is_formed,
_is_force_disabled,
_high_damage,
_high_temp,
_low_coolant,
_excess_waste,
_excess_heated_coolant,
_insufficient_fuel
)
else
-- check to see if its now formed
_is_formed()
end
else
-- check to see if its now formed
_is_formed()
self.formed = nil
self.state[CHK.SYS_FAIL] = true
end
-- check system states in order of severity
@@ -472,6 +487,7 @@ function plc.rps_init(reactor, is_formed)
---@nodiscard
function public.is_active() return self.reactor_enabled end
---@nodiscard
---@return boolean|nil formed true if formed, false if not, nil if unknown
function public.is_formed() return self.formed end
---@nodiscard
function public.is_force_disabled() return self.force_disabled end
@@ -493,14 +509,14 @@ function plc.rps_init(reactor, is_formed)
end
-- partial RPS reset that only clears fault and sys_fail
function public.reset_formed()
function public.reset_reattach()
self.tripped = false
self.trip_cause = RPS_TRIP_CAUSE.OK
self.state[CHK.FAULT] = false
self.state[CHK.SYS_FAIL] = false
log.info("RPS: partial reset on formed")
log.info("RPS: partial reset on connected or formed")
end
-- reset the automatic and timeout trip flags, then clear trip if that was the trip cause
@@ -543,13 +559,11 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
max_burn_rate = nil
}
comms.set_trusted_range(config.TrustedRange)
if config.WirelessModem then
comms.set_trusted_range(config.TrustedRange)
end
-- PRIVATE FUNCTIONS --
-- configure network channels
nic.closeAll()
nic.open(config.PLC_Channel)
--#region PRIVATE FUNCTIONS --
-- send an RPLC packet
---@param msg_type RPLC_TYPE
@@ -582,11 +596,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
-- dynamic reactor status information, excluding heating rate
---@return table data_table, boolean faulted
local function _get_reactor_status()
local fuel = nil
local waste = nil
local coolant = nil
local hcoolant = nil
local fuel, waste, coolant, hcoolant = nil, nil, nil, nil
local data_table = {}
reactor.__p_disable_afc()
@@ -705,11 +715,126 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
reactor.__p_enable_afc()
end
-- PUBLIC FUNCTIONS --
-- handle a burn rate command
---@param packet rplc_frame
---@param setpoints plc_setpoints
--- EVENT_CONSUMER: this function consumes events
local function _handle_burn_rate(packet, setpoints)
if (packet.length == 2) and (type(packet.data[1]) == "number") then
local success = false
local burn_rate = math.floor(packet.data[1] * 10) / 10
local ramp = packet.data[2]
-- if no known max burn rate, check again
if self.max_burn_rate == nil then
self.max_burn_rate = reactor.getMaxBurnRate()
end
-- if we know our max burn rate, update current burn rate setpoint if in range
if self.max_burn_rate ~= ppm.ACCESS_FAULT then
if burn_rate > 0 and burn_rate <= self.max_burn_rate then
if ramp then
setpoints.burn_rate_en = true
setpoints.burn_rate = burn_rate
success = true
else
reactor.setBurnRate(burn_rate)
success = reactor.__p_is_ok()
end
else
log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate)
end
end
_send_ack(packet.type, success)
else
log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate")
end
end
-- handle an auto burn rate command
---@param packet rplc_frame
---@param setpoints plc_setpoints
--- EVENT_CONSUMER: this function consumes events
local function _handle_auto_burn_rate(packet, setpoints)
if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then
local ack = AUTO_ACK.FAIL
local burn_rate = math.floor(packet.data[1] * 100) / 100
local ramp = packet.data[2]
self.auto_ack_token = packet.data[3]
-- if no known max burn rate, check again
if self.max_burn_rate == nil then
self.max_burn_rate = reactor.getMaxBurnRate()
end
-- if we know our max burn rate, update current burn rate setpoint if in range
if self.max_burn_rate ~= ppm.ACCESS_FAULT then
if burn_rate < 0.01 then
if rps.is_active() then
-- auto scram to disable
log.debug("AUTO: stopping the reactor to meet 0.0 burn rate")
if rps.scram() then
ack = AUTO_ACK.ZERO_DIS_OK
else
log.warning("AUTO: automatic reactor stop failed")
end
else
ack = AUTO_ACK.ZERO_DIS_OK
end
elseif burn_rate <= self.max_burn_rate then
if not rps.is_active() then
-- activate the reactor
log.debug("AUTO: activating the reactor")
reactor.setBurnRate(0.01)
if reactor.__p_is_faulted() then
log.warning("AUTO: failed to reset burn rate for auto activation")
else
if not rps.auto_activate() then
log.warning("AUTO: automatic reactor activation failed")
end
end
end
-- if active, set/ramp burn rate
if rps.is_active() then
if ramp then
log.debug(util.c("AUTO: setting burn rate ramp to ", burn_rate))
setpoints.burn_rate_en = true
setpoints.burn_rate = burn_rate
ack = AUTO_ACK.RAMP_SET_OK
else
log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate))
reactor.setBurnRate(burn_rate)
ack = util.trinary(reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK)
end
end
else
log.debug(util.c(burn_rate, " rate outside of 0 < x <= ", self.max_burn_rate))
end
end
_send_ack(packet.type, ack)
else
log.debug("RPLC set automatic burn rate packet length mismatch or non-numeric burn rate")
end
end
--#endregion
--#region PUBLIC FUNCTIONS --
---@class plc_comms
local public = {}
-- switch the current active NIC
---@param act_nic nic
function public.switch_nic(act_nic)
public.close()
nic = act_nic
end
-- reconnect a newly connected reactor
---@param new_reactor table
function public.reconnect_reactor(new_reactor)
@@ -746,8 +871,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
---@param formed boolean reactor formed (from PLC state)
function public.send_status(no_reactor, formed)
if self.linked then
local mek_data = nil ---@type table
local heating_rate = 0.0 ---@type number
local mek_data = nil ---@type table
local heating_rate = 0.0 ---@type number
if (not no_reactor) and rps.is_formed() then
if _update_status_cache() then mek_data = self.status_cache end
@@ -801,15 +926,11 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
-- get as RPLC packet
if s_pkt.protocol() == PROTOCOL.RPLC then
local rplc_pkt = comms.rplc_packet()
if rplc_pkt.decode(s_pkt) then
pkt = rplc_pkt.get()
end
if rplc_pkt.decode(s_pkt) then pkt = rplc_pkt.get() end
-- get as SCADA management packet
elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get()
end
if mgmt_pkt.decode(s_pkt) then pkt = mgmt_pkt.get() end
else
log.debug("unsupported packet type " .. s_pkt.protocol(), true)
end
@@ -821,16 +942,13 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
-- handle RPLC and MGMT packets
---@param packet rplc_frame|mgmt_frame packet frame
---@param plc_state plc_state PLC state
---@param setpoints setpoints setpoint control table
function public.handle_packet(packet, plc_state, setpoints)
-- print a log message to the terminal as long as the UI isn't running
local function println_ts(message) if not plc_state.fp_ok then util.println_ts(message) end end
---@param setpoints plc_setpoints setpoint control table
---@param println_ts function console print, when UI isn't running
function public.handle_packet(packet, plc_state, setpoints, println_ts)
local protocol = packet.scada_frame.protocol()
local l_chan = packet.scada_frame.local_channel()
local src_addr = packet.scada_frame.src_addr()
-- handle packets now that we have prints setup
if l_chan == config.PLC_Channel then
-- check sequence number
if self.r_seq_num == nil then
@@ -865,36 +983,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
log.debug("sent out structure again, did supervisor miss it?")
elseif packet.type == RPLC_TYPE.MEK_BURN_RATE then
-- set the burn rate
if (packet.length == 2) and (type(packet.data[1]) == "number") then
local success = false
local burn_rate = math.floor(packet.data[1] * 10) / 10
local ramp = packet.data[2]
-- if no known max burn rate, check again
if self.max_burn_rate == nil then
self.max_burn_rate = reactor.getMaxBurnRate()
end
-- if we know our max burn rate, update current burn rate setpoint if in range
if self.max_burn_rate ~= ppm.ACCESS_FAULT then
if burn_rate > 0 and burn_rate <= self.max_burn_rate then
if ramp then
setpoints.burn_rate_en = true
setpoints.burn_rate = burn_rate
success = true
else
reactor.setBurnRate(burn_rate)
success = reactor.__p_is_ok()
end
else
log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate)
end
end
_send_ack(packet.type, success)
else
log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate")
end
_handle_burn_rate(packet, setpoints)
elseif packet.type == RPLC_TYPE.RPS_ENABLE then
-- enable the reactor
self.scrammed = false
@@ -923,68 +1012,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
_send_ack(packet.type, true)
elseif packet.type == RPLC_TYPE.AUTO_BURN_RATE then
-- automatic control requested a new burn rate
if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then
local ack = AUTO_ACK.FAIL
local burn_rate = math.floor(packet.data[1] * 100) / 100
local ramp = packet.data[2]
self.auto_ack_token = packet.data[3]
-- if no known max burn rate, check again
if self.max_burn_rate == nil then
self.max_burn_rate = reactor.getMaxBurnRate()
end
-- if we know our max burn rate, update current burn rate setpoint if in range
if self.max_burn_rate ~= ppm.ACCESS_FAULT then
if burn_rate < 0.01 then
if rps.is_active() then
-- auto scram to disable
log.debug("AUTO: stopping the reactor to meet 0.0 burn rate")
if rps.scram() then
ack = AUTO_ACK.ZERO_DIS_OK
else
log.warning("AUTO: automatic reactor stop failed")
end
else
ack = AUTO_ACK.ZERO_DIS_OK
end
elseif burn_rate <= self.max_burn_rate then
if not rps.is_active() then
-- activate the reactor
log.debug("AUTO: activating the reactor")
reactor.setBurnRate(0.01)
if reactor.__p_is_faulted() then
log.warning("AUTO: failed to reset burn rate for auto activation")
else
if not rps.auto_activate() then
log.warning("AUTO: automatic reactor activation failed")
end
end
end
-- if active, set/ramp burn rate
if rps.is_active() then
if ramp then
log.debug(util.c("AUTO: setting burn rate ramp to ", burn_rate))
setpoints.burn_rate_en = true
setpoints.burn_rate = burn_rate
ack = AUTO_ACK.RAMP_SET_OK
else
log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate))
reactor.setBurnRate(burn_rate)
ack = util.trinary(reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK)
end
end
else
log.debug(util.c(burn_rate, " rate outside of 0 < x <= ", self.max_burn_rate))
end
end
_send_ack(packet.type, ack)
else
log.debug("RPLC set automatic burn rate packet length mismatch or non-numeric burn rate")
end
_handle_auto_burn_rate(packet, setpoints)
else
log.debug("received unknown RPLC packet type " .. packet.type)
end
@@ -1084,6 +1112,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
---@nodiscard
function public.is_linked() return self.linked end
--#endregion
return public
end

View File

@@ -18,15 +18,14 @@ local ui = {
}
-- try to start the UI
---@param theme FP_THEME front panel theme
---@param color_mode COLOR_MODE color mode
---@param config plc_config configuration
---@return boolean success, any error_msg
function renderer.try_start_ui(theme, color_mode)
function renderer.try_start_ui(config)
local status, msg = true, nil
if ui.display == nil then
-- set theme
style.set_theme(theme, color_mode)
style.set_theme(config.FrontPanelTheme, config.ColorMode)
-- reset terminal
term.setTextColor(colors.white)
@@ -40,7 +39,7 @@ function renderer.try_start_ui(theme, color_mode)
end
-- apply color mode
local c_mode_overrides = style.theme.color_modes[color_mode]
local c_mode_overrides = style.theme.color_modes[config.ColorMode]
for i = 1, #c_mode_overrides do
term.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
end
@@ -48,7 +47,7 @@ function renderer.try_start_ui(theme, color_mode)
-- init front panel view
status, msg = pcall(function ()
ui.display = DisplayBox{window=term.current(),fg_bg=style.fp.root}
panel_view(ui.display)
panel_view(ui.display, config)
end)
if status then

View File

@@ -3,6 +3,7 @@
--
require("/initenv").init_env()
local backplane = require("reactor-plc.backplane")
local comms = require("scada-common.comms")
local crash = require("scada-common.crash")
@@ -18,7 +19,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.14"
local R_PLC_VERSION = "v1.10.0"
local println = util.println
local println_ts = util.println_ts
@@ -66,7 +67,7 @@ local function main()
-- startup
----------------------------------------
-- record firmware versions and ID
-- report versions and ID
databus.tx_versions(R_PLC_VERSION, comms.version)
databus.tx_id(config.UnitID)
@@ -87,32 +88,32 @@ local function main()
-- PLC system state flags
---@class plc_state
plc_state = {
init_ok = true,
fp_ok = false,
shutdown = false,
degraded = true,
reactor_formed = true,
no_reactor = true,
no_modem = true
reactor_formed = true,
wd_modem = false,
wl_modem = false
},
-- control setpoints
---@class setpoints
---@class plc_setpoints
setpoints = {
burn_rate_en = false,
burn_rate = 0.0
},
-- core PLC devices
-- global PLC devices, still initialized by the backplane
---@class plc_dev
plc_dev = {
reactor = ppm.get_fission_reactor(),
modem = ppm.get_wireless_modem()
reactor = nil ---@type table
},
-- system objects
---@class plc_sys
plc_sys = {
rps = nil, ---@type rps
nic = nil, ---@type nic
plc_comms = nil, ---@type plc_comms
conn_watchdog = nil ---@type watchdog
},
@@ -122,6 +123,18 @@ local function main()
mq_rps = mqueue.new(),
mq_comms_tx = mqueue.new(),
mq_comms_rx = mqueue.new()
},
-- message queue message types
q_types = {
MQ__RPS_CMD = {
SCRAM = 1,
DEGRADED_SCRAM = 2,
TRIP_TIMEOUT = 3
},
MQ__COMM_CMD = {
SEND_STATUS = 1
}
}
}
@@ -130,110 +143,65 @@ local function main()
local plc_state = __shared_memory.plc_state
-- initial state evaluation
plc_state.no_reactor = smem_dev.reactor == nil
plc_state.no_modem = smem_dev.modem == nil
-- reactor and modem initialization
backplane.init(config, __shared_memory)
-- we need a reactor, can at least do some things even if it isn't formed though
if plc_state.no_reactor then
println("init> fission reactor not found")
log.warning("init> no reactor on startup")
plc_state.init_ok = false
plc_state.degraded = true
elseif not smem_dev.reactor.isFormed() then
println("init> fission reactor is not formed")
log.warning("init> reactor logic adapter present, but reactor is not formed")
plc_state.degraded = true
plc_state.reactor_formed = false
-- 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
log.debug("startup> power-on SCRAM")
smem_dev.reactor.scram()
end
-- modem is required if networked
if __shared_memory.networked and plc_state.no_modem then
println("init> wireless modem not found")
log.warning("init> no wireless modem on startup")
-- setup front panel
local message
plc_state.fp_ok, message = renderer.try_start_ui(config)
-- scram reactor if present and enabled
if (smem_dev.reactor ~= nil) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
smem_dev.reactor.scram()
end
plc_state.init_ok = false
plc_state.degraded = true
-- ...or not
if not plc_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
-- print a log message to the terminal as long as the UI isn't running
local function _println_no_fp(message) if not plc_state.fp_ok then println(message) end end
local function _println_no_fp(msg) if not plc_state.fp_ok then println(msg) end end
-- 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
smem_dev.reactor.scram()
end
----------------------------------------
-- initialize PLC
----------------------------------------
-- front panel time!
if not renderer.ui_ready() then
local message
plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode)
-- init reactor protection system
smem_sys.rps = plc.rps_init(smem_dev.reactor, util.trinary(plc_state.no_reactor, nil, plc_state.reactor_formed))
log.debug("startup> rps init")
-- ...or not
if not plc_state.fp_ok then
println_ts(util.c("UI error: ", message))
println("init> running without front panel")
log.error(util.c("front panel GUI render failed with error ", message))
log.info("init> running in headless mode without front panel")
end
end
if plc_state.init_ok then
-- init reactor protection system
smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed)
log.debug("init> rps init")
if __shared_memory.networked then
-- comms watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("init> conn watchdog started")
-- create network interface then setup comms
smem_sys.nic = network.nic(smem_dev.modem)
smem_sys.plc_comms = plc.comms(R_PLC_VERSION, smem_sys.nic, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
log.debug("init> comms init")
else
_println_no_fp("init> starting in offline mode")
log.info("init> running without networking")
end
-- notify user of emergency coolant configuration status
if config.EmerCoolEnable then
println("init> emergency coolant control ready")
log.info("init> running with emergency coolant control available")
end
util.push_event("clock_start")
_println_no_fp("init> completed")
log.info("init> startup completed")
else
_println_no_fp("init> system in degraded state, awaiting devices...")
log.warning("init> started in a degraded state, awaiting peripheral connections...")
end
databus.tx_hw_status(plc_state)
-- notify user of emergency coolant configuration status
if config.EmerCoolEnable then
_println_no_fp("startup> emergency coolant control ready")
log.info("startup> emergency coolant control available")
end
----------------------------------------
-- start system
----------------------------------------
-- conditionally init comms
if __shared_memory.networked then
-- comms watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("startup> conn watchdog started")
-- initialize PLC
init()
-- create network interface then setup comms
smem_sys.plc_comms = plc.comms(R_PLC_VERSION, backplane.active_nic(), smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
log.debug("startup> comms init")
else
_println_no_fp("startup> starting in non-networked mode")
log.info("startup> starting without networking")
end
databus.tx_hw_status(plc_state)
_println_no_fp("startup> completed")
log.info("startup> completed")
-- init threads
local main_thread = threads.thread__main(__shared_memory, init)
local main_thread = threads.thread__main(__shared_memory)
local rps_thread = threads.thread__rps(__shared_memory)
if __shared_memory.networked then
@@ -247,14 +215,12 @@ local function main()
-- run threads
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec, comms_thread_tx.p_exec, comms_thread_rx.p_exec, sp_ctrl_thread.p_exec)
if plc_state.init_ok then
-- send status one last time after RPS shutdown
smem_sys.plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed)
smem_sys.plc_comms.send_rps_status()
-- send status one last time after RPS shutdown
smem_sys.plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed)
smem_sys.plc_comms.send_rps_status()
-- close connection
smem_sys.plc_comms.close()
end
-- close connection
smem_sys.plc_comms.close()
else
-- run threads, excluding comms
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec)

View File

@@ -1,38 +1,28 @@
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local databus = require("reactor-plc.databus")
local renderer = require("reactor-plc.renderer")
local backplane = require("reactor-plc.backplane")
local databus = require("reactor-plc.databus")
local renderer = require("reactor-plc.renderer")
local core = require("graphics.core")
local core = require("graphics.core")
local threads = {}
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local RPS_SLEEP = 250 -- (250ms, 5 ticks)
local COMMS_SLEEP = 150 -- (150ms, 3 ticks)
local SP_CTRL_SLEEP = 250 -- (250ms, 5 ticks)
local MAIN_CLOCK = 0.5 -- 2Hz, 10 ticks
local RPS_SLEEP = 250 -- 250ms, 5 ticks
local COMMS_SLEEP = 150 -- 150ms, 3 ticks
local SP_CTRL_SLEEP = 250 -- 250ms, 5 ticks
local BURN_RATE_RAMP_mB_s = 5.0
local MQ__RPS_CMD = {
SCRAM = 1,
DEGRADED_SCRAM = 2,
TRIP_TIMEOUT = 3
}
local MQ__COMM_CMD = {
SEND_STATUS = 1
}
-- main thread
---@nodiscard
---@param smem plc_shared_memory
---@param init function
function threads.thread__main(smem, init)
function threads.thread__main(smem)
-- print a log message to the terminal as long as the UI isn't running
local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
@@ -42,32 +32,33 @@ function threads.thread__main(smem, init)
-- execute thread
function public.exec()
databus.tx_rt_status("main", true)
log.debug("main thread init, clock inactive")
log.debug("OS: main thread start")
-- send status updates at 2Hz (every 10 server ticks) (every loop tick)
-- send link requests at 0.5Hz (every 40 server ticks) (every 8 loop ticks)
local LINK_TICKS = 8
local LINK_TICKS = 2
local ticks_to_update = 0
local loop_clock = util.new_clock(MAIN_CLOCK)
-- load in from shared memory
local networked = smem.networked
local plc_state = smem.plc_state
local plc_dev = smem.plc_dev
local networked = smem.networked
local plc_state = smem.plc_state
local rps = smem.plc_sys.rps
local plc_comms = smem.plc_sys.plc_comms
local conn_watchdog = smem.plc_sys.conn_watchdog
local MQ__RPS_CMD = smem.q_types.MQ__RPS_CMD
local MQ__COMM_CMD = smem.q_types.MQ__COMM_CMD
-- start clock
loop_clock.start()
-- event loop
while true do
-- get plc_sys fields (may have been set late due to degraded boot)
local rps = smem.plc_sys.rps
local nic = smem.plc_sys.nic
local plc_comms = smem.plc_sys.plc_comms
local conn_watchdog = smem.plc_sys.conn_watchdog
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event
if event == "timer" and loop_clock.is_clock(param1) then
-- note: loop clock is only running if init_ok = true
-- blink heartbeat indicator
databus.heartbeat()
@@ -75,10 +66,10 @@ function threads.thread__main(smem, init)
loop_clock.start()
-- send updated data
if networked and nic.is_connected() then
if networked then
if plc_comms.is_linked() then
smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS)
else
elseif backplane.active_nic().is_connected() then
if ticks_to_update == 0 then
plc_comms.send_link_req()
ticks_to_update = LINK_TICKS
@@ -93,23 +84,23 @@ function threads.thread__main(smem, init)
-- reactor now formed
plc_state.reactor_formed = true
println_ts("reactor is now formed.")
println_ts("reactor is now formed")
log.info("reactor is now formed")
-- SCRAM newly formed reactor
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
-- determine if we are still in a degraded state
if (not networked) or nic.is_connected() then
if (not networked) or backplane.active_nic().is_connected() then
plc_state.degraded = false
end
-- partial reset of RPS, specific to becoming formed
-- without this, auto control can't resume on chunk load
rps.reset_formed()
elseif plc_state.reactor_formed and not rps.is_formed() then
rps.reset_reattach()
elseif plc_state.reactor_formed and (rps.is_formed() == false) then
-- reactor no longer formed
println_ts("reactor is no longer formed.")
println_ts("reactor is no longer formed")
log.info("reactor is no longer formed")
plc_state.reactor_formed = false
@@ -118,14 +109,14 @@ function threads.thread__main(smem, init)
-- update indicators
databus.tx_hw_status(plc_state)
elseif event == "modem_message" and networked and plc_state.init_ok and nic.is_connected() then
elseif event == "modem_message" and networked then
-- got a packet
local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5)
if packet ~= nil then
-- pass the packet onto the comms message queue
smem.q.mq_comms_rx.push_packet(packet)
end
elseif event == "timer" and networked and plc_state.init_ok and conn_watchdog.is_timer(param1) then
elseif event == "timer" and networked and conn_watchdog.is_timer(param1) then
-- haven't heard from server recently? close connection and shutdown reactor
plc_comms.close()
smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT)
@@ -135,41 +126,8 @@ function threads.thread__main(smem, init)
elseif event == "peripheral_detach" then
-- peripheral disconnect
local type, device = ppm.handle_unmount(param1)
if type ~= nil and device ~= nil then
if device == plc_dev.reactor then
println_ts("reactor disconnected!")
log.error("reactor logic adapter disconnected")
plc_state.no_reactor = true
plc_state.degraded = true
elseif networked and type == "modem" then
---@cast device Modem
-- we only care if this is our wireless modem
-- note, check init_ok first since nic will be nil if it is false
if plc_state.init_ok and nic.is_modem(device) then
nic.disconnect()
println_ts("comms modem disconnected!")
log.warning("comms modem disconnected")
local other_modem = ppm.get_wireless_modem()
if other_modem then
log.info("found another wireless modem, using it for comms")
nic.connect(other_modem)
else
plc_state.no_modem = true
plc_state.degraded = true
if plc_state.init_ok then
-- try to scram reactor if it is still connected
smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
end
end
else
log.warning("a modem was disconnected")
end
end
backplane.detach(param1, type, device, println_ts)
end
-- update indicators
@@ -177,66 +135,8 @@ function threads.thread__main(smem, init)
elseif event == "peripheral" then
-- peripheral connect
local type, device = ppm.mount(param1)
if type ~= nil and device ~= nil then
if plc_state.no_reactor and (type == "fissionReactorLogicAdapter") then
-- reconnected reactor
plc_dev.reactor = device
plc_state.no_reactor = false
println_ts("reactor reconnected.")
log.info("reactor reconnected")
-- we need to assume formed here as we cannot check in this main loop
-- RPS will identify if it isn't and this will get set false later
plc_state.reactor_formed = true
-- determine if we are still in a degraded state
if (not networked or not plc_state.no_modem) and plc_state.reactor_formed then
plc_state.degraded = false
end
if plc_state.init_ok then
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
rps.reconnect_reactor(plc_dev.reactor)
if networked then
plc_comms.reconnect_reactor(plc_dev.reactor)
end
-- partial reset of RPS, specific to becoming formed/reconnected
-- without this, auto control can't resume on chunk load
rps.reset_formed()
end
elseif networked and type == "modem" then
---@cast device Modem
-- note, check init_ok first since nic will be nil if it is false
if device.isWireless() and not (plc_state.init_ok and nic.is_connected()) then
-- reconnected modem
plc_dev.modem = device
plc_state.no_modem = false
if plc_state.init_ok then nic.connect(device) end
println_ts("wireless modem reconnected.")
log.info("comms modem reconnected")
-- determine if we are still in a degraded state
if not plc_state.no_reactor then
plc_state.degraded = false
end
elseif device.isWireless() then
log.info("unused wireless modem reconnected")
else
log.info("wired modem reconnected")
end
end
end
-- if not init'd and no longer degraded, proceed to init
if not plc_state.init_ok and not plc_state.degraded then
plc_state.init_ok = true
init()
backplane.attach(param1, type, device, println_ts)
end
-- update indicators
@@ -245,15 +145,11 @@ function threads.thread__main(smem, init)
event == "double_click" then
-- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
elseif event == "clock_start" then
-- start loop clock
loop_clock.start()
log.debug("main thread clock started")
end
-- check for termination request
if event == "terminate" or ppm.should_terminate() then
log.info("terminate requested, main thread exiting")
log.info("OS: terminate requested, main thread exiting")
-- rps handles reactor shutdown
plc_state.shutdown = true
break
@@ -277,8 +173,7 @@ function threads.thread__main(smem, init)
-- if not, we need to restart the clock
-- this thread cannot be slept because it will miss events (namely "terminate" otherwise)
if not plc_state.shutdown then
log.info("main thread restarting now...")
util.push_event("clock_start")
log.info("OS: main thread restarting now...")
end
end
end
@@ -299,7 +194,7 @@ function threads.thread__rps(smem)
-- execute thread
function public.exec()
databus.tx_rt_status("rps", true)
log.debug("rps thread start")
log.debug("OS: rps thread start")
-- load in from shared memory
local networked = smem.networked
@@ -308,6 +203,8 @@ function threads.thread__rps(smem)
local rps_queue = smem.q.mq_rps
local MQ__RPS_CMD = smem.q_types.MQ__RPS_CMD
local was_linked = false
local last_update = util.time()
@@ -316,49 +213,36 @@ function threads.thread__rps(smem)
-- get plc_sys fields (may have been set late due to degraded boot)
local rps = smem.plc_sys.rps
local plc_comms = smem.plc_sys.plc_comms
-- get reactor, may have changed do to disconnect/reconnect
-- get reactor, it may have changed due to a disconnect/reconnect
local reactor = plc_dev.reactor
-- RPS checks
if plc_state.init_ok then
-- SCRAM if no open connection
if networked and not plc_comms.is_linked() then
if was_linked then
was_linked = false
rps.trip_timeout()
end
else
was_linked = true
-- SCRAM if no open connection
if networked and not plc_comms.is_linked() then
if was_linked then
was_linked = false
rps.trip_timeout()
end
else was_linked = true end
if (not plc_state.no_reactor) and rps.is_formed() then
-- check reactor status
---@diagnostic disable-next-line: need-check-nil
local reactor_status = reactor.getStatus()
databus.tx_reactor_state(reactor_status)
-- check reactor status
if (not plc_state.no_reactor) and rps.is_formed() then
local reactor_status = reactor.getStatus()
databus.tx_reactor_state(reactor_status)
-- if we tried to SCRAM but failed, keep trying
-- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check)
if rps.is_tripped() and reactor_status then
rps.scram()
end
end
-- if we tried to SCRAM but failed, keep trying
-- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check)
if rps.is_tripped() and reactor_status then rps.scram() end
end
-- if we are in standalone mode and the front panel isn't working, continuously reset RPS
-- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable
if not (networked or smem.plc_state.fp_ok) then rps.reset(true) end
-- if we are in standalone mode and the front panel isn't working, continuously reset RPS
-- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable
if not (networked or smem.plc_state.fp_ok) then rps.reset(true) end
-- check safety (SCRAM occurs if tripped)
if not plc_state.no_reactor then
local rps_tripped, rps_status_string, rps_first = rps.check()
if rps_tripped and rps_first then
println_ts("[RPS] SCRAM! safety trip: " .. rps_status_string)
if networked and not plc_state.no_modem then
plc_comms.send_rps_alarm(rps_status_string)
end
end
end
-- check safety (SCRAM occurs if tripped)
local rps_tripped, rps_status_string, rps_first = rps.check(not plc_state.no_reactor)
if rps_tripped and rps_first then
println_ts("RPS: SCRAM on safety trip (" .. rps_status_string .. ")")
if networked then plc_comms.send_rps_alarm(rps_status_string) end
end
-- check for messages in the message queue
@@ -368,19 +252,19 @@ function threads.thread__rps(smem)
if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command
if plc_state.init_ok then
if msg.message == MQ__RPS_CMD.SCRAM then
-- SCRAM
rps.scram()
elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then
-- lost peripheral(s)
rps.trip_fault()
elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then
-- watchdog tripped
rps.trip_timeout()
println_ts("server timeout")
log.warning("server timeout")
end
if msg.message == MQ__RPS_CMD.SCRAM then
-- SCRAM
log.info("RPS: OS requested SCRAM")
rps.scram()
elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then
-- lost peripheral(s)
log.info("RPS: received PLC degraded alert")
rps.trip_fault()
elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then
-- watchdog tripped
println_ts("RPS: supervisor timeout")
log.warning("RPS: received supervisor timeout alert")
rps.trip_timeout()
end
elseif msg.qtype == mqueue.TYPE.DATA then
-- received data
@@ -396,17 +280,17 @@ function threads.thread__rps(smem)
-- check for termination request
if plc_state.shutdown then
-- safe exit
log.info("rps thread shutdown initiated")
if plc_state.init_ok then
if rps.scram() then
println_ts("reactor disabled")
log.info("rps thread reactor SCRAM OK")
else
println_ts("exiting, reactor failed to disable")
log.error("rps thread failed to SCRAM reactor on exit")
end
log.info("OS: rps thread shutdown initiated")
if rps.scram() then
println_ts("exiting, reactor disabled")
log.info("OS: rps thread reactor SCRAM OK on exit")
else
println_ts("exiting, reactor failed to disable")
log.error("OS: rps thread failed to SCRAM reactor on exit")
end
log.info("rps thread exiting")
log.info("OS: rps thread exiting")
break
end
@@ -428,8 +312,8 @@ function threads.thread__rps(smem)
databus.tx_rt_status("rps", false)
if not plc_state.shutdown then
if plc_state.init_ok then smem.plc_sys.rps.scram() end
log.info("rps thread restarting in 5 seconds...")
smem.plc_sys.rps.scram()
log.info("OS: rps thread restarting in 5 seconds...")
util.psleep(5)
end
end
@@ -448,11 +332,13 @@ function threads.thread__comms_tx(smem)
-- execute thread
function public.exec()
databus.tx_rt_status("comms_tx", true)
log.debug("comms tx thread start")
log.debug("OS: comms tx thread start")
-- load in from shared memory
local plc_state = smem.plc_state
local comms_queue = smem.q.mq_comms_tx
local plc_state = smem.plc_state
local comms_queue = smem.q.mq_comms_tx
local MQ__COMM_CMD = smem.q_types.MQ__COMM_CMD
local last_update = util.time()
@@ -465,7 +351,7 @@ function threads.thread__comms_tx(smem)
while comms_queue.ready() and not plc_state.shutdown do
local msg = comms_queue.pop()
if msg ~= nil and plc_state.init_ok then
if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command
if msg.message == MQ__COMM_CMD.SEND_STATUS then
@@ -486,7 +372,7 @@ function threads.thread__comms_tx(smem)
-- check for termination request
if plc_state.shutdown then
log.info("comms tx thread exiting")
log.info("OS: comms tx thread exiting")
break
end
@@ -508,7 +394,7 @@ function threads.thread__comms_tx(smem)
databus.tx_rt_status("comms_tx", false)
if not plc_state.shutdown then
log.info("comms tx thread restarting in 5 seconds...")
log.info("OS: comms tx thread restarting in 5 seconds...")
util.psleep(5)
end
end
@@ -521,13 +407,16 @@ end
---@nodiscard
---@param smem plc_shared_memory
function threads.thread__comms_rx(smem)
-- print a log message to the terminal as long as the UI isn't running
local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
---@class parallel_thread
local public = {}
-- execute thread
function public.exec()
databus.tx_rt_status("comms_rx", true)
log.debug("comms rx thread start")
log.debug("OS: comms rx thread start")
-- load in from shared memory
local plc_state = smem.plc_state
@@ -546,7 +435,7 @@ function threads.thread__comms_rx(smem)
while comms_queue.ready() and not plc_state.shutdown do
local msg = comms_queue.pop()
if msg ~= nil and plc_state.init_ok then
if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command
elseif msg.qtype == mqueue.TYPE.DATA then
@@ -555,7 +444,7 @@ function threads.thread__comms_rx(smem)
-- received a packet
-- handle the packet (setpoints passed to update burn rate setpoint)
-- (plc_state passed to check if degraded)
plc_comms.handle_packet(msg.message, plc_state, setpoints)
plc_comms.handle_packet(msg.message, plc_state, setpoints, println_ts)
end
end
@@ -565,7 +454,7 @@ function threads.thread__comms_rx(smem)
-- check for termination request
if plc_state.shutdown then
log.info("comms rx thread exiting")
log.info("OS: comms rx thread exiting")
break
end
@@ -587,7 +476,7 @@ function threads.thread__comms_rx(smem)
databus.tx_rt_status("comms_rx", false)
if not plc_state.shutdown then
log.info("comms rx thread restarting in 5 seconds...")
log.info("OS: comms rx thread restarting in 5 seconds...")
util.psleep(5)
end
end
@@ -606,7 +495,7 @@ function threads.thread__setpoint_control(smem)
-- execute thread
function public.exec()
databus.tx_rt_status("spctl", true)
log.debug("setpoint control thread start")
log.debug("OS: setpoint control thread start")
-- load in from shared memory
local plc_state = smem.plc_state
@@ -629,9 +518,7 @@ function threads.thread__setpoint_control(smem)
-- get reactor, may have changed do to disconnect/reconnect
local reactor = plc_dev.reactor
if plc_state.init_ok and (not plc_state.no_reactor) then
---@cast reactor table won't be nil
if not plc_state.no_reactor then
-- check if we should start ramping
if setpoints.burn_rate_en and (setpoints.burn_rate ~= last_burn_sp) then
local cur_burn_rate = reactor.getBurnRate()
@@ -698,7 +585,7 @@ function threads.thread__setpoint_control(smem)
-- check for termination request
if plc_state.shutdown then
log.info("setpoint control thread exiting")
log.info("OS: setpoint control thread exiting")
break
end
@@ -720,7 +607,7 @@ function threads.thread__setpoint_control(smem)
databus.tx_rt_status("spctl", false)
if not plc_state.shutdown then
log.info("setpoint control thread restarting in 5 seconds...")
log.info("OS: setpoint control thread restarting in 5 seconds...")
util.psleep(5)
end
end

275
rtu/backplane.lua Normal file
View File

@@ -0,0 +1,275 @@
--
-- RTU Gateway System Core Peripheral Backplane
--
local log = require("scada-common.log")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local databus = require("rtu.databus")
local rtu = require("rtu.rtu")
local println = util.println
---@class rtu_backplane
local backplane = {}
local _bp = {
smem = nil, ---@type rtu_shared_memory
wlan_pref = true,
lan_iface = "",
act_nic = nil, ---@type nic
wd_nic = nil, ---@type nic|nil
wl_nic = nil, ---@type nic|nil
sounders = {} ---@type rtu_speaker_sounder[]
}
-- initialize the system peripheral backplane
---@param config rtu_config
---@param __shared_memory rtu_shared_memory
---@return boolean success
function backplane.init(config, __shared_memory)
_bp.smem = __shared_memory
_bp.wlan_pref = config.PreferWireless
_bp.lan_iface = config.WiredModem
-- Modem Init
-- init wired NIC
if type(_bp.lan_iface) == "string" then
local modem = ppm.get_modem(_bp.lan_iface)
local wd_nic = network.nic(modem)
log.info("BKPLN: WIRED PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. _bp.lan_iface)
_bp.wd_nic = wd_nic
_bp.act_nic = wd_nic -- set this as active for now
wd_nic.closeAll()
wd_nic.open(config.RTU_Channel)
databus.tx_hw_wd_modem(modem ~= nil)
end
-- init wireless NIC(s)
if config.WirelessModem then
local modem, iface = ppm.get_wireless_modem()
local wl_nic = network.nic(modem)
log.info("BKPLN: WIRELESS PHY_" .. util.trinary(modem, "UP ", "DOWN") .. (iface or ""))
-- set this as active if connected or if both modems are disconnected and this is preferred
if (modem and _bp.wlan_pref) or not (_bp.act_nic and _bp.act_nic.is_connected()) then
_bp.act_nic = wl_nic
end
_bp.wl_nic = wl_nic
wl_nic.closeAll()
wl_nic.open(config.RTU_Channel)
databus.tx_hw_wl_modem(modem ~= nil)
end
-- at least one comms modem is required
if not ((_bp.wd_nic and _bp.wd_nic.is_connected()) or (_bp.wl_nic and _bp.wl_nic.is_connected())) then
println("startup> no comms modem found")
log.warning("BKPLN: no comms modem on startup")
return false
end
-- Speaker Init
-- find and setup all speakers
local speakers = ppm.get_all_devices("speaker")
for _, s in pairs(speakers) do
log.info("BKPLN: SPEAKER LINK_UP " .. ppm.get_iface(s))
local sounder = rtu.init_sounder(s)
table.insert(_bp.sounders, sounder)
log.debug(util.c("BKPLN: added speaker sounder, attached as ", sounder.name))
end
databus.tx_hw_spkr_count(#_bp.sounders)
return true
end
-- get the active NIC
function backplane.active_nic() return _bp.act_nic end
-- get the sounder interfaces
function backplane.sounders() return _bp.sounders end
-- handle a backplane peripheral attach
---@param type string
---@param device table
---@param iface string
---@param print_no_fp function
function backplane.attach(type, device, iface, print_no_fp)
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
local comms = _bp.smem.rtu_sys.rtu_comms
if type == "modem" then
---@cast device Modem
local m_is_wl = device.isWireless()
log.info(util.c("BKPLN: ", util.trinary(m_is_wl, "WIRELESS", "WIRED"), " PHY_ATTACH ", iface))
if wd_nic and (_bp.lan_iface == iface) then
-- connect this as the wired NIC
wd_nic.connect(device)
log.info("BKPLN: WIRED PHY_UP " .. iface)
print_no_fp("wired comms modem reconnected")
databus.tx_hw_wd_modem(true)
if (_bp.act_nic ~= wd_nic) and not _bp.wlan_pref then
-- switch back to preferred wired
_bp.act_nic = wd_nic
comms.switch_nic(_bp.act_nic, _bp.smem.rtu_state)
log.info("BKPLN: switched comms to wired modem (preferred)")
end
elseif wl_nic and (not wl_nic.is_connected()) and m_is_wl then
-- connect this as the wireless NIC
wl_nic.connect(device)
log.info("BKPLN: WIRELESS PHY_UP " .. iface)
print_no_fp("wireless comms modem reconnected")
databus.tx_hw_wl_modem(true)
if (_bp.act_nic ~= wl_nic) and _bp.wlan_pref then
-- switch back to preferred wireless
_bp.act_nic = wl_nic
comms.switch_nic(_bp.act_nic, _bp.smem.rtu_state)
log.info("BKPLN: switched comms to wireless modem (preferred)")
end
elseif wl_nic and m_is_wl then
-- the wireless NIC already has a modem
device.closeAll()
print_no_fp("standby wireless modem connected")
log.info("BKPLN: standby wireless modem connected")
else
device.closeAll()
print_no_fp("unassigned modem connected")
log.warning("BKPLN: unassigned modem connected")
end
elseif type == "speaker" then
---@cast device Speaker
log.info("BKPLN: SPEAKER LINK_UP " .. iface)
table.insert(_bp.sounders, rtu.init_sounder(device))
print_no_fp("a speaker was connected")
log.info("BKPLN: setup speaker sounder for speaker " .. iface)
databus.tx_hw_spkr_count(#_bp.sounders)
end
end
-- handle a backplane peripheral detach
---@param type string
---@param device table
---@param iface string
---@param print_no_fp function
function backplane.detach(type, device, iface, print_no_fp)
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
local comms = _bp.smem.rtu_sys.rtu_comms
if type == "modem" then
---@cast device Modem
log.info(util.c("BKPLN: PHY_DETACH ", iface))
if wd_nic and wd_nic.is_modem(device) then
wd_nic.disconnect()
log.info("BKPLN: WIRED PHY_DOWN " .. iface)
databus.tx_hw_wd_modem(false)
elseif wl_nic and wl_nic.is_modem(device) then
wl_nic.disconnect()
log.info("BKPLN: WIRELESS PHY_DOWN " .. iface)
databus.tx_hw_wl_modem(false)
end
-- we only care if this is our active comms modem
if _bp.act_nic.is_modem(device) then
print_no_fp("active comms modem disconnected")
log.warning("BKPLN: active comms modem disconnected")
-- failover and try to find a new comms modem
if _bp.act_nic == wl_nic then
-- wireless active disconnected
-- try to find another wireless modem, otherwise switch to wired
local modem, m_iface = ppm.get_wireless_modem()
if wl_nic and modem then
log.info("BKPLN: found another wireless modem, using it for comms")
wl_nic.connect(modem)
log.info("BKPLN: WIRELESS PHY_UP " .. m_iface)
databus.tx_hw_wl_modem(true)
elseif wd_nic and wd_nic.is_connected() then
_bp.act_nic = wd_nic
comms.switch_nic(_bp.act_nic, _bp.smem.rtu_state)
log.info("BKPLN: switched comms to wired modem")
end
elseif wl_nic and wl_nic.is_connected() then
-- wired active disconnected, wireless available
_bp.act_nic = wl_nic
comms.switch_nic(_bp.act_nic, _bp.smem.rtu_state)
log.info("BKPLN: switched comms to wireless modem")
else
-- wired active disconnected, wireless unavailable
end
elseif wd_nic and wd_nic.is_modem(device) then
-- wired, but not active
print_no_fp("standby wired modem disconnected")
log.info("BKPLN: standby wired modem disconnected")
elseif wl_nic and wl_nic.is_modem(device) then
-- wireless, but not active
print_no_fp("standby wireless modem disconnected")
log.info("BKPLN: standby wireless modem disconnected")
else
print_no_fp("unassigned modem disconnected")
log.warning("BKPLN: unassigned modem disconnected")
end
elseif type == "speaker" then
---@cast device Speaker
log.info("BKPLN: SPEAKER LINK_DOWN " .. iface)
for i = 1, #_bp.sounders do
if _bp.sounders[i].speaker == device then
table.remove(_bp.sounders, i)
print_no_fp("a speaker was disconnected")
log.warning("BKPLN: speaker sounder " .. iface .. " disconnected")
databus.tx_hw_spkr_count(#_bp.sounders)
break
end
end
end
end
return backplane

378
rtu/config/check.lua Normal file
View File

@@ -0,0 +1,378 @@
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 = {
checking_wl = true,
wd_modem = nil, ---@type Modem|nil
wl_modem = nil, ---@type Modem|nil
nic = nil, ---@type nic
net_listen = false,
self_check_pass = true,
self_check_wireless = 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(comms.BROADCAST, util.time_ms() * 10, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
self.nic.transmit(self.settings.SVR_Channel, self.settings.RTU_Channel, s_pkt)
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
-- OK
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
if error_msg then
self.self_check_msg(nil, false, error_msg)
else
self.self_check_msg(nil, true, "")
end
util.push_event("conn_test_complete", error_msg == nil)
end
-- handle supervisor connection failure
local function handle_timeout()
self.net_listen = false
util.push_event("conn_test_complete", false)
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
self.wd_modem = ppm.get_modem(cfg.WiredModem)
self.wl_modem = ppm.get_wireless_modem()
local valid_cfg = rtu.validate_config(cfg)
if cfg.WiredModem then
self.self_check_msg("> check wired comms modem connected...", self.wd_modem, "please connect the wired comms modem " .. cfg.WiredModem)
end
if cfg.WirelessModem then
self.self_check_msg("> check wireless/ender modem connected...", self.wl_modem, "please connect an ender or wireless modem for wireless comms")
end
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" or p_type == "environment_detector" 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 == "reinforcedInductionPort" 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 then
self.checking_wl = true
if cfg.WirelessModem and self.wl_modem then
self.self_check_msg("> check wireless 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
comms.set_trusted_range(cfg.TrustedRange)
self.nic = network.nic(self.wl_modem)
self.nic.closeAll()
self.nic.open(cfg.RTU_Channel)
self.net_listen = true
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, comms.CONN_TEST_FWV, DEVICE_TYPE.RTU, {} })
tcd.dispatch_unique(8, handle_timeout)
elseif cfg.WiredModem and self.wd_modem then
-- skip to wired
util.push_event("conn_test_complete", true)
else
self.self_check_msg("> no modem, can't test supervisor connection", false)
end
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
-- handle completed connection tests
---@param pass boolean
function check.conn_test_callback(pass)
local cfg = self.settings
if self.checking_wl then
if not pass then
self.self_check_msg(nil, false, "make sure your supervisor is running, listening on the wireless interface, 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
if cfg.WiredModem and self.wd_modem then
self.checking_wl = false
self.self_check_msg("> check wired supervisor connection...")
comms.set_trusted_range(0)
self.nic = network.nic(self.wd_modem)
self.nic.closeAll()
self.nic.open(cfg.RTU_Channel)
self.net_listen = true
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, comms.CONN_TEST_FWV, 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
else
if not pass then
self.self_check_msg(nil, false, "make sure your supervisor is running, listening on the wired interface, the wire is intact, and your channels are correct")
end
if self.self_check_pass then check_complete() end
self.run_test_btn.enable()
end
end
return check

View File

@@ -43,8 +43,8 @@ local self = {
local peripherals = {}
local RTU_DEV_TYPES = { "boilerValve", "turbineValve", "dynamicValve", "inductionPort", "spsPort", "solarNeutronActivator", "environmentDetector" }
local NEEDS_UNIT = { "boilerValve", "turbineValve", "dynamicValve", "solarNeutronActivator", "environmentDetector" }
local RTU_DEV_TYPES = { "boilerValve", "turbineValve", "dynamicValve", "inductionPort", "reinforcedInductionPort", "spsPort", "solarNeutronActivator", "environmentDetector", "environment_detector" }
local NEEDS_UNIT = { "boilerValve", "turbineValve", "dynamicValve", "solarNeutronActivator", "environmentDetector", "environment_detector" }
-- create the peripherals configuration view
---@param tool_ctl _rtu_cfg_tool_ctl
@@ -149,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()
@@ -165,14 +165,14 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
end
self.p_desc.set_value("Each reactor unit can have at most 1 tank and the facility can have at most 4. Each facility tank must have a unique # 1 through 4, regardless of where it is connected. Only a total of 4 tanks can be displayed on the flow monitor.")
elseif type == "environmentDetector" then
elseif type == "environmentDetector" or type == "environment_detector" then
reposition("This is the below system's # env. detector.", 29, 99, 17, 6, 8)
self.p_assign_btn.show()
self.p_assign_btn.redraw()
if self.p_assign_btn.get_value() == 1 then self.p_unit.disable() else self.p_unit.enable() end
self.p_desc.set_value("You can connect more than one environment detector for a particular unit or the facility. In that case, the maximum radiation reading from those assigned to that particular unit or the facility will be used for alarms and display.")
elseif type == "inductionPort" or type == "spsPort" then
local dev = tri(type == "inductionPort", "induction matrix", "SPS")
elseif type == "inductionPort" or type == "reinforcedInductionPort" or type == "spsPort" then
local dev = tri(type == "inductionPort" or type == "reinforcedInductionPort", "induction matrix", "SPS")
self.p_idx.hide(true)
self.p_unit.hide(true)
self.p_prompt.set_value("This is the " .. dev .. " for the facility.")
@@ -212,10 +212,10 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
tool_ctl.update_peri_list()
TextBox{parent=peri_c_3,x=1,y=1,height=4,text="This feature is intended for advanced users. If you are clicking this just because your device is not shown, follow the connection instructions in 'I don't see my device!'."}
TextBox{parent=peri_c_3,x=1,y=6,height=4,text="Peripheral Name"}
local p_name = TextField{parent=peri_c_3,x=1,y=7,width=49,height=1,max_len=128,fg_bg=bw_fg_bg}
local p_type = Radio2D{parent=peri_c_3,x=1,y=9,rows=4,columns=2,default=1,options=RTU_DEV_TYPES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple}
TextBox{parent=peri_c_3,x=1,y=1,height=4,text="This feature is intended for advanced users. If you just can't see your device, click 'I don't see my device!' instead."}
TextBox{parent=peri_c_3,x=1,y=5,height=4,text="Peripheral Name"}
local p_name = TextField{parent=peri_c_3,x=1,y=6,width=49,height=1,max_len=128,fg_bg=bw_fg_bg}
local p_type = Radio2D{parent=peri_c_3,x=1,y=8,rows=5,columns=2,default=1,options=RTU_DEV_TYPES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple}
local man_p_err = TextBox{parent=peri_c_3,x=8,y=14,width=35,text="Please enter a peripheral name.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
man_p_err.hide(true)
@@ -281,7 +281,7 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
local idx = tonumber(self.p_idx.get_value())
if util.table_contains(NEEDS_UNIT, peri_type) then
if (peri_type == "dynamicValve" or peri_type == "environmentDetector") and for_facility then
if (peri_type == "dynamicValve" or peri_type == "environmentDetector" or peri_type == "environment_detector") and for_facility then
-- skip
elseif not (util.is_int(u) and u > 0 and u < 5) then
self.p_err.set_value("Unit ID must be within 1 to 4.")
@@ -310,7 +310,7 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
else index = idx end
elseif peri_type == "dynamicValve" then
index = 1
elseif peri_type == "environmentDetector" then
elseif peri_type == "environmentDetector" or peri_type == "environment_detector" then
if not (util.is_int(idx) and idx > 0) then
self.p_err.set_value("Index must be greater than 0.")
self.p_err.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,6 +35,7 @@ local IO_MODE = rsio.IO_MODE
local LEFT = core.ALIGN.LEFT
local self = {
rs_cfg_phy = false, ---@type string|nil|false
rs_cfg_port = 1, ---@type IO_PORT
rs_cfg_editing = false, ---@type integer|false
@@ -41,7 +45,9 @@ local self = {
rs_cfg_side_l = nil, ---@type TextBox
rs_cfg_bundled = nil, ---@type Checkbox
rs_cfg_color = nil, ---@type Radio2D
rs_cfg_shortcut = nil ---@type TextBox
rs_cfg_inverted = nil, ---@type Checkbox
rs_cfg_shortcut = nil, ---@type TextBox
rs_cfg_advanced = nil ---@type PushButton
}
-- rsio port descriptions
@@ -74,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)
@@ -104,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
@@ -124,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)
@@ -145,43 +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)
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(5)
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
@@ -190,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)
@@ -204,9 +312,13 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
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
@@ -232,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
@@ -253,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)
self.rs_cfg_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,disable_fg_bg=g_lg_fg_bg}
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())
@@ -301,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(self.rs_cfg_bundled.get_value() and rsio.is_digital(port), 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
@@ -318,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,
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)
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),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
@@ -373,9 +519,11 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
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
@@ -390,7 +538,8 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
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))
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)
@@ -400,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

@@ -30,9 +30,13 @@ local self = {
importing_legacy = false,
importing_any_dc = false,
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type PushButton
auth_key_textbox = nil, ---@type TextBox
wireless = nil, ---@type Checkbox
wl_pref = nil, ---@type Checkbox
wired = nil, ---@type Checkbox
range = nil, ---@type NumberField
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type PushButton
auth_key_textbox = nil, ---@type TextBox
auth_key_value = ""
}
@@ -90,22 +94,88 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."}
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=1,text="Please select the network interface(s)."}
TextBox{parent=net_c_1,x=41,y=1,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
TextBox{parent=net_c_1,x=1,y=8,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=11,text="RTU Channel"}
local rtu_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=9,y=12,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg}
local function en_dis_pref()
if self.wireless.get_value() and self.wired.get_value() then
self.wl_pref.enable()
else
self.wl_pref.set_value(self.wireless.get_value())
self.wl_pref.disable()
end
end
local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function on_wired_change(_)
en_dis_pref()
tool_ctl.gen_modem_list()
end
self.wireless = Checkbox{parent=net_c_1,x=1,y=3,label="Wireless/Ender Modem",default=ini_cfg.WirelessModem,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=en_dis_pref}
self.wl_pref = Checkbox{parent=net_c_1,x=30,y=3,label="Prefer Wireless",default=ini_cfg.PreferWireless,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
self.wired = Checkbox{parent=net_c_1,x=1,y=5,label="Wired Modem",default=ini_cfg.WiredModem~=false,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=on_wired_change}
TextBox{parent=net_c_1,x=3,y=6,text="this one MUST ONLY connect to SCADA computers",fg_bg=cpair(colors.red,colors._INHERIT)}
TextBox{parent=net_c_1,x=3,y=7,text="connecting it to peripherals will cause issues",fg_bg=g_lg_fg_bg}
local modem_list = ListBox{parent=net_c_1,x=1,y=8,height=5,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local modem_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
en_dis_pref()
local function submit_interfaces()
tmp_cfg.WirelessModem = self.wireless.get_value()
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
tmp_cfg.PreferWireless = self.wl_pref.get_value()
else
tmp_cfg.PreferWireless = tmp_cfg.WirelessModem
self.wl_pref.set_value(tmp_cfg.PreferWireless)
end
if not self.wired.get_value() then
tmp_cfg.WiredModem = false
tool_ctl.gen_modem_list()
end
if not (self.wired.get_value() or self.wireless.get_value()) then
modem_err.set_value("Please select a modem type.")
modem_err.show()
elseif self.wired.get_value() and type(tmp_cfg.WiredModem) ~= "string" then
modem_err.set_value("Please select a wired modem.")
modem_err.show()
else
if tmp_cfg.WirelessModem then
self.range.enable()
else
self.range.set_value(0)
self.range.disable()
end
net_pane.set_value(2)
modem_err.hide(true)
end
end
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_interfaces,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,text="Please set the network channels below."}
TextBox{parent=net_c_2,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=8,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_2,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=11,text="RTU Channel"}
local rtu_chan = NumberField{parent=net_c_2,x=1,y=12,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=9,y=12,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg}
local chan_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_channels()
local svr_c = tonumber(svr_chan.get_value())
@@ -113,7 +183,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
if svr_c ~= nil and rtu_c ~= nil then
tmp_cfg.SVR_Channel = svr_c
tmp_cfg.RTU_Channel = rtu_c
net_pane.set_value(2)
net_pane.set_value(3)
chan_err.hide(true)
elseif svr_c == nil then
chan_err.set_value("Please set the supervisor channel.")
@@ -124,54 +194,62 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
end
end
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,text="Connection Timeout"}
local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,text="Connection Timeout"}
local timeout = NumberField{parent=net_c_3,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_3,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=3,height=4,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=8,text="Trusted Range"}
local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=1,y=10,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=8,text="Trusted Range (Wireless Only)"}
self.range = NumberField{parent=net_c_3,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=net_c_3,x=1,y=10,height=4,text="Setting this to a value larger than 0 prevents wireless connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
local p2_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local n3_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_ct_tr()
local timeout_val = tonumber(timeout.get_value())
local range_val = tonumber(range.get_value())
if timeout_val ~= nil and range_val ~= nil then
tmp_cfg.ConnTimeout = timeout_val
tmp_cfg.TrustedRange = range_val
net_pane.set_value(3)
p2_err.hide(true)
elseif timeout_val == nil then
p2_err.set_value("Please set the connection timeout.")
p2_err.show()
local range_val = tonumber(self.range.get_value())
if timeout_val == nil then
n3_err.set_value("Please set the connection timeout.")
n3_err.show()
elseif tmp_cfg.WirelessModem and (range_val == nil) then
n3_err.set_value("Please set the trusted range.")
n3_err.show()
else
p2_err.set_value("Please set the trusted range.")
p2_err.show()
tmp_cfg.ConnTimeout = timeout_val
tmp_cfg.TrustedRange = tri(tmp_cfg.WirelessModem, range_val, 0)
if tmp_cfg.WirelessModem then
net_pane.set_value(4)
else
main_pane.set_value(4)
tmp_cfg.AuthKey = ""
end
n3_err.hide(true)
end
end
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
TextBox{parent=net_c_3,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
TextBox{parent=net_c_4,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for wireless security on multiplayer servers. All devices on the same wireless network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=11,text="Facility Auth Key"}
local key, _ = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
TextBox{parent=net_c_4,x=1,y=11,text="Auth Key (Wireless Only, Not Used for Wired)"}
local key, _ = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
local function censor_key(enable) key.censor(tri(enable, "*", nil)) end
local hide_key = Checkbox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
local hide_key = Checkbox{parent=net_c_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
hide_key.set_value(true)
censor_key(true)
local key_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local key_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_auth()
local v = key.get_value()
@@ -182,8 +260,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
else key_err.show() end
end
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
@@ -196,7 +274,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
@@ -238,7 +316,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
TextBox{parent=clr_c_1,x=1,y=7,text="Front Panel Theme"}
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."}
@@ -382,10 +460,13 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
load_settings(ini_cfg)
try_set(s_vol, ini_cfg.SpeakerVolume)
try_set(self.wireless, ini_cfg.WirelessModem)
try_set(self.wired, ini_cfg.WiredModem ~= false)
try_set(self.wl_pref, ini_cfg.PreferWireless)
try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(rtu_chan, ini_cfg.RTU_Channel)
try_set(timeout, ini_cfg.ConnTimeout)
try_set(range, ini_cfg.TrustedRange)
try_set(self.range, ini_cfg.TrustedRange)
try_set(key, ini_cfg.AuthKey)
try_set(mode, ini_cfg.LogMode)
try_set(path, ini_cfg.LogPath)
@@ -506,7 +587,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
local u, idx = def.unit, def.index
if util.table_contains(NEEDS_UNIT, mount.type) then
if (mount.type == "dynamicValve" or mount.type == "environmentDetector") and for_facility then
if (mount.type == "dynamicValve" or mount.type == "environmentDetector" or mount.type == "environment_detector") and for_facility then
-- skip
elseif not (util.is_int(u) and u > 0 and u < 5) then
err = true
@@ -527,7 +608,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
else index = idx end
elseif mount.type == "dynamicValve" then
index = 1
elseif mount.type == "environmentDetector" then
elseif mount.type == "environmentDetector" or mount.type == "environment_detector" then
if not (util.is_int(idx) and idx > 0) then
err = true
else index = idx end
@@ -646,7 +727,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
@@ -665,6 +746,59 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
end
end
-- generate the list of available/assigned wired modems
function tool_ctl.gen_modem_list()
modem_list.remove_all()
local enable = self.wired.get_value()
local function select(iface)
tmp_cfg.WiredModem = iface
tool_ctl.gen_modem_list()
end
local modems = ppm.get_wired_modem_list()
local missing = { tmp = true, ini = true }
for iface, _ in pairs(modems) do
if ini_cfg.WiredModem == iface then missing.ini = false end
if tmp_cfg.WiredModem == iface then missing.tmp = false end
end
if missing.tmp and tmp_cfg.WiredModem then
local line = Div{parent=modem_list,x=1,y=1,height=1}
TextBox{parent=line,x=1,y=1,width=4,text="Used",fg_bg=cpair(tri(enable,colors.blue,colors.gray),colors.white)}
PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}.disable()
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
TextBox{parent=line,x=25,y=1,text=tmp_cfg.WiredModem}
end
if missing.ini and ini_cfg.WiredModem and (tmp_cfg.WiredModem ~= ini_cfg.WiredModem) then
local line = Div{parent=modem_list,x=1,y=1,height=1}
local used = tmp_cfg.WiredModem == ini_cfg.WiredModem
TextBox{parent=line,x=1,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(ini_cfg.WiredModem)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
TextBox{parent=line,x=25,y=1,text=ini_cfg.WiredModem}
if used or not enable then select_btn.disable() end
end
-- list wired modems
for iface, _ in pairs(modems) do
local line = Div{parent=modem_list,x=1,y=1,height=1}
local used = tmp_cfg.WiredModem == iface
TextBox{parent=line,x=1,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(iface)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
TextBox{parent=line,x=15,y=1,text=iface}
if used or not enable then select_btn.disable() end
end
end
--#endregion
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,10 @@ 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" } },
{ "v1.13.0", { "Added support for wired communications modems" } }
}
---@class rtu_configurator
@@ -61,38 +65,44 @@ local tool_ctl = {
viewing_config = false,
jumped_to_color = false,
view_gw_cfg = nil, ---@type PushButton
dev_cfg = nil, ---@type PushButton
rs_cfg = nil, ---@type PushButton
color_cfg = nil, ---@type PushButton
color_next = nil, ---@type PushButton
color_apply = nil, ---@type PushButton
settings_apply = nil, ---@type PushButton
settings_confirm = nil, ---@type PushButton
view_gw_cfg = nil, ---@type PushButton
dev_cfg = nil, ---@type PushButton
rs_cfg = nil, ---@type PushButton
color_cfg = nil, ---@type PushButton
color_next = nil, ---@type PushButton
color_apply = nil, ---@type PushButton
settings_apply = nil, ---@type PushButton
settings_confirm = nil, ---@type PushButton
go_home = nil, ---@type function
gen_summary = nil, ---@type function
load_legacy = nil, ---@type function
update_peri_list = nil, ---@type function
gen_peri_summary = nil, ---@type function
gen_rs_summary = nil, ---@type function
go_home = nil, ---@type function
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
gen_modem_list = function () end
}
---@class rtu_config
local tmp_cfg = {
SpeakerVolume = 1.0,
Peripherals = {}, ---@type rtu_peri_definition[]
Redstone = {}, ---@type rtu_rs_definition[]
SVR_Channel = nil, ---@type integer
RTU_Channel = nil, ---@type integer
ConnTimeout = nil, ---@type number
TrustedRange = nil, ---@type number
AuthKey = nil, ---@type string|nil
LogMode = 0, ---@type LOG_MODE
Peripherals = {}, ---@type rtu_peri_definition[]
Redstone = {}, ---@type rtu_rs_definition[]
WirelessModem = true,
WiredModem = false, ---@type string|false
PreferWireless = true,
SVR_Channel = nil, ---@type integer
RTU_Channel = nil, ---@type integer
ConnTimeout = nil, ---@type number
TrustedRange = nil, ---@type number
AuthKey = nil, ---@type string
LogMode = 0, ---@type LOG_MODE
LogPath = "",
LogDebug = false,
FrontPanelTheme = 1, ---@type FP_THEME
ColorMode = 1 ---@type COLOR_MODE
FrontPanelTheme = 1, ---@type FP_THEME
ColorMode = 1 ---@type COLOR_MODE
}
---@class rtu_config
@@ -102,6 +112,9 @@ local settings_cfg = {}
local fields = {
{ "SpeakerVolume", "Speaker Volume", 1.0 },
{ "WirelessModem", "Wireless/Ender Comms Modem", true },
{ "WiredModem", "Wired Comms Modem", false },
{ "PreferWireless", "Prefer Wireless Modem", true },
{ "SVR_Channel", "SVR Channel", 16240 },
{ "RTU_Channel", "RTU Channel", 16242 },
{ "ConnTimeout", "Connection Timeout", 5 },
@@ -115,6 +128,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
@@ -122,9 +136,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
@@ -169,8 +184,9 @@ 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
@@ -203,7 +219,6 @@ local function config_view(display)
end
local function show_rs_conns()
tool_ctl.gen_rs_summary()
main_pane.set_value(9)
end
@@ -226,8 +241,9 @@ local function config_view(display)
PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
local start_btn = PushButton{parent=main_page,x=42,y=17,min_width=9,text="Startup",callback=startup,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
tool_ctl.color_cfg = PushButton{parent=main_page,x=36,y=y_start,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=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}
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
@@ -283,6 +299,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
@@ -300,6 +322,9 @@ function configurator.configure(ask_config)
load_settings(settings_cfg, true)
tool_ctl.has_config = load_settings(ini_cfg)
-- set tmp_cfg so interface lists are correct
tmp_cfg.WiredModem = ini_cfg.WiredModem
tmp_cfg.Peripherals = tool_ctl.deep_copy_peri(ini_cfg.Peripherals)
tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone)
@@ -316,8 +341,10 @@ function configurator.configure(ask_config)
local display = DisplayBox{window=term.current(),fg_bg=style.root}
config_view(display)
tool_ctl.gen_modem_list()
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
@@ -330,14 +357,22 @@ 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 == "conn_test_complete" then
check.conn_test_callback(param1)
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()
tool_ctl.gen_modem_list()
elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1)
tool_ctl.update_peri_list()
tool_ctl.update_relay_list()
tool_ctl.gen_modem_list()
end
if event == "terminate" then return end

View File

@@ -23,7 +23,7 @@ databus.RTU_HW_STATE = RTU_HW_STATE
-- call to toggle heartbeat signal
function databus.heartbeat() databus.ps.toggle("heartbeat") end
-- transmit firmware versions across the bus
-- transmit firmware versions
---@param rtu_v string RTU version
---@param comms_v string comms version
function databus.tx_versions(rtu_v, comms_v)
@@ -31,10 +31,16 @@ function databus.tx_versions(rtu_v, comms_v)
databus.ps.publish("comms_version", comms_v)
end
-- transmit hardware status for modem connection state
-- transmit hardware status for the wired comms modem
---@param has_modem boolean
function databus.tx_hw_modem(has_modem)
databus.ps.publish("has_modem", has_modem)
function databus.tx_hw_wd_modem(has_modem)
databus.ps.publish("has_wd_modem", has_modem)
end
-- transmit hardware status for the wireless comms modem
---@param has_modem boolean
function databus.tx_hw_wl_modem(has_modem)
databus.ps.publish("has_wl_modem", has_modem)
end
-- transmit the number of speakers connected
@@ -43,14 +49,14 @@ function databus.tx_hw_spkr_count(count)
databus.ps.publish("speaker_count", count)
end
-- transmit unit hardware type across the bus
-- transmit unit hardware type
---@param uid integer unit ID
---@param type RTU_UNIT_TYPE
function databus.tx_unit_hw_type(uid, type)
databus.ps.publish("unit_type_" .. uid, type)
end
-- transmit unit hardware status across the bus
-- transmit unit hardware status
---@param uid integer unit ID
---@param status RTU_HW_STATE
function databus.tx_unit_hw_status(uid, status)
@@ -64,17 +70,10 @@ function databus.tx_rt_status(thread, ok)
databus.ps.publish(util.c("routine__", thread), ok)
end
-- transmit supervisor link state across the bus
-- transmit supervisor link state
---@param state integer
function databus.tx_link_state(state)
databus.ps.publish("link_state", state)
end
-- link a function to receive data from the bus
---@param field string field name
---@param func function function to link
function databus.rx_field(field, func)
databus.ps.subscribe(field, func)
end
return databus

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

@@ -12,6 +12,7 @@ local style = require("rtu.panel.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
@@ -19,11 +20,13 @@ 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
local cpair = core.cpair
local border = core.border
local ind_grn = style.ind_grn
@@ -31,17 +34,22 @@ local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC
-- create new front panel view
---@param panel DisplayBox main displaybox
---@param config rtu_config configuraiton
---@param units rtu_registry_entry[] unit list
local function init(panel, units)
local function init(panel, config, units)
local s_hi_box = style.theme.highlight_box
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}
@@ -50,10 +58,18 @@ local function init(panel, units)
heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
local modem = LED{parent=system,label="MODEM",colors=ind_grn}
if config.WirelessModem and config.WiredModem then
local wd_modem = LED{parent=system,label="WD MODEM",colors=ind_grn}
local wl_modem = LED{parent=system,label="WL MODEM",colors=ind_grn}
wd_modem.register(databus.ps, "has_wd_modem", wd_modem.update)
wl_modem.register(databus.ps, "has_wl_modem", wl_modem.update)
else
local modem = LED{parent=system,label="MODEM",colors=ind_grn}
modem.register(databus.ps, util.trinary(config.WirelessModem, "has_wl_modem", "has_wd_modem"), modem.update)
end
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
@@ -87,8 +103,6 @@ local function init(panel, units)
system.line_break()
modem.register(databus.ps, "has_modem", modem.update)
local rt_main = LED{parent=system,label="RT MAIN",colors=ind_grn}
local rt_comm = LED{parent=system,label="RT COMMS",colors=ind_grn}
system.line_break()
@@ -96,61 +110,78 @@ local function init(panel, units)
rt_main.register(databus.ps, "routine__main", rt_main.update)
rt_comm.register(databus.ps, "routine__comms", rt_comm.update)
--
-- hardware labeling
--
local hw_labels = Rectangle{parent=panel,x=2,y=term_h-6,width=14,height=5,border=border(1,s_hi_box.bkg,true),even_inner=true}
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=disabled_fg}
local comp_id = util.sprintf("%03d", os.getComputerID())
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=hw_labels,text="FW "..databus.ps.get("version"),fg_bg=s_hi_box}
TextBox{parent=hw_labels,text="NT v"..databus.ps.get("comms_version"),fg_bg=s_hi_box}
TextBox{parent=hw_labels,text="SN "..comp_id.."-RTU",fg_bg=s_hi_box}
--
-- speaker count
--
TextBox{parent=panel,x=2,y=term_h-1,text="SPEAKERS",width=8,fg_bg=style.fp.text_fg}
local speaker_count = DataIndicator{parent=panel,x=11,y=term_h-1,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"}
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)
--
-- 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

@@ -18,16 +18,15 @@ local ui = {
}
-- try to start the UI
---@param config rtu_config configuration
---@param units rtu_registry_entry[] RTU entries
---@param theme FP_THEME front panel theme
---@param color_mode COLOR_MODE color mode
---@return boolean success, any error_msg
function renderer.try_start_ui(units, theme, color_mode)
function renderer.try_start_ui(config, units)
local status, msg = true, nil
if ui.display == nil then
-- set theme
style.set_theme(theme, color_mode)
style.set_theme(config.FrontPanelTheme, config.ColorMode)
-- reset terminal
term.setTextColor(colors.white)
@@ -41,7 +40,7 @@ function renderer.try_start_ui(units, theme, color_mode)
end
-- apply color mode
local c_mode_overrides = style.theme.color_modes[color_mode]
local c_mode_overrides = style.theme.color_modes[config.ColorMode]
for i = 1, #c_mode_overrides do
term.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
end
@@ -49,7 +48,7 @@ function renderer.try_start_ui(units, theme, color_mode)
-- init front panel view
status, msg = pcall(function ()
ui.display = DisplayBox{window=term.current(),fg_bg=style.fp.root}
panel_view(ui.display, units)
panel_view(ui.display, config, units)
end)
if status then

View File

@@ -33,6 +33,9 @@ function rtu.load_config()
config.SpeakerVolume = settings.get("SpeakerVolume")
config.WirelessModem = settings.get("WirelessModem")
config.WiredModem = settings.get("WiredModem")
config.PreferWireless = settings.get("PreferWireless")
config.SVR_Channel = settings.get("SVR_Channel")
config.RTU_Channel = settings.get("RTU_Channel")
config.ConnTimeout = settings.get("ConnTimeout")
@@ -46,36 +49,46 @@ 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_type_bool(cfg.WirelessModem)
cfv.assert((cfg.WiredModem == false) or (type(cfg.WiredModem) == "string"))
cfv.assert(cfg.WirelessModem or (type(cfg.WiredModem) == "string"))
cfv.assert_type_bool(cfg.PreferWireless)
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
@@ -293,13 +306,11 @@ function rtu.comms(version, nic, conn_watchdog)
local insert = table.insert
comms.set_trusted_range(config.TrustedRange)
if config.WirelessModem then
comms.set_trusted_range(config.TrustedRange)
end
-- PRIVATE FUNCTIONS --
-- configure modem channels
nic.closeAll()
nic.open(config.RTU_Channel)
--#region PRIVATE FUNCTIONS --
-- send a scada management packet
---@param msg_type MGMT_TYPE
@@ -332,31 +343,26 @@ 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
return advertisement
end
-- PUBLIC FUNCTIONS --
--#endregion
--#region PUBLIC FUNCTIONS --
---@class rtu_comms
local public = {}
-- send a MODBUS TCP packet
---@param m_pkt modbus_packet
function public.send_modbus(m_pkt)
local s_pkt = comms.scada_packet()
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
self.seq_num = self.seq_num + 1
-- switch the current active NIC
---@param act_nic nic
---@param rtu_state rtu_state
function public.switch_nic(act_nic, rtu_state)
public.close(rtu_state)
nic = act_nic
end
-- unlink from the server
@@ -376,6 +382,17 @@ function rtu.comms(version, nic, conn_watchdog)
_send(MGMT_TYPE.CLOSE, {})
end
-- send a MODBUS TCP packet
---@param m_pkt modbus_packet
function public.send_modbus(m_pkt)
local s_pkt = comms.scada_packet()
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
self.seq_num = self.seq_num + 1
end
-- send establish request (includes advertisement)
---@param units table
function public.send_establish(units)
@@ -471,9 +488,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
@@ -481,18 +499,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
@@ -595,6 +611,8 @@ function rtu.comms(version, nic, conn_watchdog)
end
end
--#endregion
return public
end

View File

@@ -1,40 +1,27 @@
--
-- RTU: Remote Terminal Unit
-- RTU Gateway: Remote Terminal Unit Gateway
--
require("/initenv").init_env()
local audio = require("scada-common.audio")
local comms = require("scada-common.comms")
local crash = require("scada-common.crash")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local audio = require("scada-common.audio")
local comms = require("scada-common.comms")
local crash = require("scada-common.crash")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local configure = require("rtu.configure")
local databus = require("rtu.databus")
local modbus = require("rtu.modbus")
local renderer = require("rtu.renderer")
local rtu = require("rtu.rtu")
local threads = require("rtu.threads")
local backplane = require("rtu.backplane")
local configure = require("rtu.configure")
local databus = require("rtu.databus")
local renderer = require("rtu.renderer")
local rtu = require("rtu.rtu")
local threads = require("rtu.threads")
local uinit = require("rtu.uinit")
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
local dynamicv_rtu = require("rtu.dev.dynamicv_rtu")
local envd_rtu = require("rtu.dev.envd_rtu")
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
local redstone_rtu = require("rtu.dev.redstone_rtu")
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.21"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_HW_STATE = databus.RTU_HW_STATE
local RTU_VERSION = "v1.13.0"
local println = util.println
local println_ts = util.println_ts
@@ -82,7 +69,7 @@ local function main()
-- startup
----------------------------------------
-- record firmware versions and ID
-- report versions
databus.tx_versions(RTU_VERSION, comms.version)
-- mount connected devices
@@ -106,15 +93,9 @@ local function main()
shutdown = false
},
-- RTU gateway devices (not RTU units)
rtu_dev = {
modem = ppm.get_wireless_modem(),
sounders = {} ---@type rtu_speaker_sounder[]
},
-- system objects
---@class rtu_sys
rtu_sys = {
nic = nil, ---@type nic
rtu_comms = nil, ---@type rtu_comms
conn_watchdog = nil, ---@type watchdog
units = {} ---@type rtu_registry_entry[]
@@ -126,378 +107,23 @@ local function main()
}
}
local smem_sys = __shared_memory.rtu_sys
local smem_dev = __shared_memory.rtu_dev
local smem_sys = __shared_memory.rtu_sys
local rtu_state = __shared_memory.rtu_state
local units = __shared_memory.rtu_sys.units
----------------------------------------
-- interpret config and init units
-- init and start system
----------------------------------------
local units = __shared_memory.rtu_sys.units
-- modem and speaker initialization
if not backplane.init(config, __shared_memory) then return end
local rtu_redstone = config.Redstone
local rtu_devices = config.Peripherals
log.debug("startup> running uinit()")
-- 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[] }[]
-- 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)
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)
log.fatal(message)
return false
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
if not valid then
local message = util.c("sys_config> invalid redstone definition at block index #", entry_idx)
println(message)
log.fatal(message)
return false
else
-- link redstone in RTU
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)
println(message)
log.warning(message)
else
rs_rtu.link_di(entry.side, entry.color)
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)
println(message)
log.warning(message)
else
rs_rtu.link_ai(entry.side)
end
elseif mode == rsio.IO_MODE.ANALOG_OUT then
rs_rtu.link_ao(entry.side)
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)
println("sys_config> encountered a software error, check logs")
return false
end
table.insert(capabilities, entry.port)
log.debug(util.c("sys_config> linked redstone ", #capabilities, ": ", rsio.to_string(entry.port), " (", iface_name, ") for ", assignment))
end
end
-- create unit entries for redstone RTUs
for for_reactor, def in pairs(rs_rtus) do
---@class 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
modbus_io = modbus.new(def.rtu, false),
pkt_queue = nil, ---@type mqueue|nil
thread = nil ---@type parallel_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
log.info(util.c("sys_config> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message))
unit.uid = #units
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
end
-- mounted peripherals
for i = 1, #rtu_devices do
local entry = rtu_devices[i] ---@type rtu_peri_definition
local name = entry.name
local index = entry.index
local for_reactor = util.trinary(entry.unit == nil, 0, entry.unit)
-- CHECK: name is a string
if type(name) ~= "string" then
local message = util.c("sys_config> device entry #", i, ": device ", name, " isn't a string")
println(message)
log.fatal(message)
return false
end
-- CHECK: index type
if (index ~= nil) and (not util.is_int(index)) then
local message = util.c("sys_config> device entry #", i, ": index ", index, " isn't valid")
println(message)
log.fatal(message)
return false
end
-- CHECK: index range
local function validate_index(min, max)
if (not util.is_int(index)) or ((index < min) and (max ~= nil and index > max)) then
local message = util.c("sys_config> device entry #", i, ": index ", index, " isn't >= ", min)
if max ~= nil then message = util.c(message, " and <= ", max) end
println(message)
log.fatal(message)
return false
else return true end
end
-- CHECK: reactor is an integer >= 0
local function validate_assign(for_facility)
if for_facility and for_reactor ~= 0 then
local message = util.c("sys_config> device entry #", i, ": must only be for the facility")
println(message)
log.fatal(message)
return false
elseif (not for_facility) and ((not util.is_int(for_reactor)) or (for_reactor < 1) or (for_reactor > 4)) then
local message = util.c("sys_config> device entry #", i, ": unit assignment ", for_reactor, " isn't vaild")
println(message)
log.fatal(message)
return false
else return true end
end
local device = ppm.get_periph(name)
local type ---@type string|nil
local rtu_iface ---@type rtu_device
local rtu_type ---@type RTU_UNIT_TYPE
local is_multiblock = false ---@type boolean
local formed = nil ---@type boolean|nil
local faulted = nil ---@type boolean|nil
if device == nil then
local message = util.c("sys_config> '", name, "' not found, using placeholder")
println(message)
log.warning(message)
-- mount a virtual (placeholder) device
type, device = ppm.mount_virtual()
else
type = ppm.get_type(name)
end
if type == "boilerValve" then
-- boiler multiblock
if not validate_index(1, 2) then return false end
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.BOILER_VALVE
rtu_iface, faulted = boilerv_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
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
if not validate_index(1, 3) then return false end
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE
rtu_iface, faulted = turbinev_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
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
if entry.unit == nil then
if not validate_index(1, 4) then return false end
if not validate_assign(true) then return false end
else
if not validate_index(1, 1) then return false end
if not validate_assign() then return false end
end
rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE
rtu_iface, faulted = dynamicv_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
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
if not validate_assign(true) then return false end
rtu_type = RTU_UNIT_TYPE.IMATRIX
rtu_iface, faulted = imatrix_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
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
if not validate_assign(true) then return false end
rtu_type = RTU_UNIT_TYPE.SPS
rtu_iface, faulted = sps_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
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
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.SNA
rtu_iface, faulted = sna_rtu.new(device)
elseif type == "environmentDetector" then
-- advanced peripherals environment detector
if not validate_index(1) then return false end
if not validate_assign(entry.unit == nil) then return false end
rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
rtu_iface, faulted = envd_rtu.new(device)
elseif type == ppm.VIRTUAL_DEVICE_TYPE then
-- placeholder device
rtu_type = RTU_UNIT_TYPE.VIRTUAL
rtu_iface = rtu.init_unit().interface()
else
local message = util.c("sys_config> device '", name, "' is not a known type (", type, ")")
println_ts(message)
log.fatal(message)
return false
end
if is_multiblock then
if not formed then
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
formed = false
log.warning(util.c("sys_config> device '", name, "' is formed, but initialization had one or more faults: marked as unformed"))
end
end
---@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
}
rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
table.insert(units, rtu_unit)
local for_message = "the facility"
if for_reactor > 0 then
for_message = util.c("reactor ", for_reactor)
end
local index_str = util.trinary(index ~= nil, util.c(" [", index, "]"), "")
log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ")", index_str, " for ", for_message))
rtu_unit.uid = #units
-- determine hardware status
if rtu_unit.type == RTU_UNIT_TYPE.VIRTUAL then
rtu_unit.hw_state = RTU_HW_STATE.OFFLINE
else
if rtu_unit.is_multiblock then
rtu_unit.hw_state = util.trinary(rtu_unit.formed == true, RTU_HW_STATE.OK, RTU_HW_STATE.UNFORMED)
elseif faulted then
rtu_unit.hw_state = RTU_HW_STATE.FAULTED
else
rtu_unit.hw_state = RTU_HW_STATE.OK
end
end
-- report hardware status
databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state)
end
return true
end
----------------------------------------
-- start system
----------------------------------------
log.debug("boot> running sys_config()")
if sys_config() then
if uinit(config, __shared_memory) then
-- start UI
local message
rtu_state.fp_ok, message = renderer.try_start_ui(units, config.FrontPanelTheme, config.ColorMode)
rtu_state.fp_ok, message = renderer.try_start_ui(config, units)
if not rtu_state.fp_ok then
println_ts(util.c("UI error: ", message))
@@ -506,34 +132,12 @@ local function main()
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")
log.fatal("no wireless modem on startup")
return
end
databus.tx_hw_modem(true)
-- find and setup all speakers
local speakers = ppm.get_all_devices("speaker")
for _, s in pairs(speakers) do
local sounder = rtu.init_sounder(s)
table.insert(smem_dev.sounders, sounder)
log.debug(util.c("startup> added speaker, attached as ", sounder.name))
end
databus.tx_hw_spkr_count(#smem_dev.sounders)
-- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("startup> conn watchdog started")
-- setup comms
smem_sys.nic = network.nic(smem_dev.modem)
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, backplane.active_nic(), smem_sys.conn_watchdog)
log.debug("startup> comms init")
-- init threads
@@ -553,7 +157,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

@@ -5,10 +5,10 @@ local tcd = require("scada-common.tcd")
local types = require("scada-common.types")
local util = require("scada-common.util")
local backplane = require("rtu.backplane")
local databus = require("rtu.databus")
local modbus = require("rtu.modbus")
local renderer = require("rtu.renderer")
local rtu = require("rtu.rtu")
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
local dynamicv_rtu = require("rtu.dev.dynamicv_rtu")
@@ -25,8 +25,8 @@ local threads = {}
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_HW_STATE = databus.RTU_HW_STATE
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
local MAIN_CLOCK = 0.5 -- 2Hz, 10 ticks
local COMMS_SLEEP = 100 -- 100ms, 2 ticks
---@param smem rtu_shared_memory
---@param println_ts function
@@ -74,7 +74,7 @@ local function handle_unit_mount(smem, println_ts, iface, type, device, unit)
end
unit.type = RTU_UNIT_TYPE.DYNAMIC_VALVE
elseif type == "inductionPort" then
elseif type == "inductionPort" or type == "reinforcedInductionPort" then
-- induction matrix multiblock
if unit.reactor ~= 0 then fail(util.c("induction matrix '", unit.name, "' cannot init, not assigned to facility")) end
@@ -89,7 +89,7 @@ local function handle_unit_mount(smem, println_ts, iface, type, device, unit)
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("SNA '", unit.name, "' cannot init, not assigned to a valid unit")) end
unit.type = RTU_UNIT_TYPE.SNA
elseif type == "environmentDetector" then
elseif type == "environmentDetector" or type == "environment_detector" then
-- advanced peripherals environment detector
if unit.reactor < 0 or unit.reactor > 4 then fail(util.c("environment detector '", unit.name, "' cannot init, no valid assignment provided")) end
if (unit.index == false) or unit.index < 1 then fail(util.c("environment detector '", unit.name, "' cannot init, invalid index provided")) end
@@ -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)
@@ -182,19 +184,19 @@ function threads.thread__main(smem)
-- execute thread
function public.exec()
databus.tx_rt_status("main", true)
log.debug("main thread start")
log.debug("OS: main thread start")
-- main loop clock
local loop_clock = util.new_clock(MAIN_CLOCK)
-- load in from shared memory
local rtu_state = smem.rtu_state
local sounders = smem.rtu_dev.sounders
local nic = smem.rtu_sys.nic
local rtu_comms = smem.rtu_sys.rtu_comms
local conn_watchdog = smem.rtu_sys.conn_watchdog
local units = smem.rtu_sys.units
local sounders = backplane.sounders()
-- start unlinked (in case of restart)
rtu_comms.unlink(rtu_state)
@@ -244,38 +246,8 @@ function threads.thread__main(smem)
local type, device = ppm.handle_unmount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
---@cast device Modem
-- we only care if this is our wireless modem
if nic.is_modem(device) then
nic.disconnect()
println_ts("wireless modem disconnected!")
log.warning("comms modem disconnected")
local other_modem = ppm.get_wireless_modem()
if other_modem then
log.info("found another wireless modem, using it for comms")
nic.connect(other_modem)
else
databus.tx_hw_modem(false)
end
else
log.warning("non-comms modem disconnected")
end
elseif type == "speaker" then
---@cast device Speaker
for i = 1, #sounders do
if sounders[i].speaker == device then
table.remove(sounders, i)
log.warning(util.c("speaker ", param1, " disconnected"))
println_ts("speaker disconnected")
databus.tx_hw_spkr_count(#sounders)
break
end
end
if type == "modem" or type == "speaker" then
backplane.detach(type, device, param1, println_ts)
else
for i = 1, #units do
-- find disconnected device
@@ -299,29 +271,8 @@ function threads.thread__main(smem)
local type, device = ppm.mount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
---@cast device Modem
if device.isWireless() and not nic.is_connected() then
-- reconnected modem
nic.connect(device)
println_ts("wireless modem reconnected.")
log.info("comms modem reconnected")
databus.tx_hw_modem(true)
elseif device.isWireless() then
log.info("unused wireless modem reconnected")
else
log.info("wired modem reconnected")
end
elseif type == "speaker" then
---@cast device Speaker
table.insert(sounders, rtu.init_sounder(device))
println_ts("speaker connected")
log.info(util.c("connected speaker ", param1))
databus.tx_hw_spkr_count(#sounders)
if type == "modem" or type == "speaker" then
backplane.attach(type, device, param1, println_ts)
else
-- relink lost peripheral to correct unit entry
for i = 1, #units do
@@ -347,7 +298,7 @@ function threads.thread__main(smem)
-- check for termination request
if event == "terminate" or ppm.should_terminate() then
rtu_state.shutdown = true
log.info("terminate requested, main thread exiting")
log.info("OS: terminate requested, main thread exiting")
break
end
end
@@ -366,7 +317,7 @@ function threads.thread__main(smem)
databus.tx_rt_status("main", false)
if not rtu_state.shutdown then
log.info("main thread restarting in 5 seconds...")
log.info("OS: main thread restarting in 5 seconds...")
util.psleep(5)
end
end
@@ -385,16 +336,16 @@ function threads.thread__comms(smem)
-- execute thread
function public.exec()
databus.tx_rt_status("comms", true)
log.debug("comms thread start")
log.debug("OS: comms thread start")
-- load in from shared memory
local rtu_state = smem.rtu_state
local sounders = smem.rtu_dev.sounders
local rtu_comms = smem.rtu_sys.rtu_comms
local units = smem.rtu_sys.units
local comms_queue = smem.q.mq_comms
local sounders = backplane.sounders()
local last_update = util.time()
-- thread loop
@@ -419,7 +370,7 @@ function threads.thread__comms(smem)
-- max 100ms spent processing queue
if util.time() - handle_start > 100 then
log.warning("comms thread exceeded 100ms queue process limit")
log.warning("OS: comms thread exceeded 100ms queue process limit")
break
end
end
@@ -430,7 +381,7 @@ function threads.thread__comms(smem)
-- check for termination request
if rtu_state.shutdown then
rtu_comms.close(rtu_state)
log.info("comms thread exiting")
log.info("OS: comms thread exiting")
break
end
@@ -452,7 +403,7 @@ function threads.thread__comms(smem)
databus.tx_rt_status("comms", false)
if not rtu_state.shutdown then
log.info("comms thread restarting in 5 seconds...")
log.info("OS: comms thread restarting in 5 seconds...")
util.psleep(5)
end
end
@@ -475,7 +426,7 @@ function threads.thread__unit_comms(smem, unit)
-- execute thread
function public.exec()
databus.tx_rt_status("unit_" .. unit.uid, true)
log.debug(util.c("rtu unit thread start -> ", types.rtu_type_to_string(unit.type), " (", unit.name, ")"))
log.debug(util.c("OS: rtu unit thread start -> ", types.rtu_type_to_string(unit.type), " (", unit.name, ")"))
-- load in from shared memory
local rtu_state = smem.rtu_state
@@ -492,7 +443,7 @@ function threads.thread__unit_comms(smem, unit)
local short_name = util.c(types.rtu_type_to_string(unit.type), " (", unit.name, ")")
if packet_queue == nil then
log.error("rtu unit thread created without a message queue, exiting...", true)
log.error("OS: rtu unit thread created without a message queue, exiting...", true)
return
end
@@ -520,7 +471,7 @@ function threads.thread__unit_comms(smem, unit)
-- check for termination request
if rtu_state.shutdown then
log.info("rtu unit thread exiting -> " .. short_name)
log.info("OS: rtu unit thread exiting -> " .. short_name)
break
end
@@ -585,7 +536,7 @@ function threads.thread__unit_comms(smem, unit)
databus.tx_rt_status("unit_" .. unit.uid, false)
if not rtu_state.shutdown then
log.info(util.c("rtu unit thread ", types.rtu_type_to_string(unit.type), " (", unit.name, ") restarting in 5 seconds..."))
log.info(util.c("OS: rtu unit thread ", types.rtu_type_to_string(unit.type), " (", unit.name, ") restarting in 5 seconds..."))
util.psleep(5)
end
end

441
rtu/uinit.lua Normal file
View File

@@ -0,0 +1,441 @@
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local databus = require("rtu.databus")
local modbus = require("rtu.modbus")
local rtu = require("rtu.rtu")
local threads = require("rtu.threads")
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
local dynamicv_rtu = require("rtu.dev.dynamicv_rtu")
local envd_rtu = require("rtu.dev.envd_rtu")
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
local redstone_rtu = require("rtu.dev.redstone_rtu")
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 println = util.println
local println_ts = util.println_ts
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_HW_STATE = databus.RTU_HW_STATE
-- print and log a fatal error during startup
---@param msg string
local function log_fail(msg)
println(msg)
log.fatal(msg)
end
-- 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
---@param config rtu_config
---@param __shared_memory rtu_shared_memory
---@return boolean success
return function(config, __shared_memory)
local units = __shared_memory.rtu_sys.units
local rtu_redstone = config.Redstone
local rtu_devices = config.Peripherals
--#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 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
elseif entry.unit == nil then
assignment = "facility"
for_reactor = 0
else
log_fail(util.c("uinit> invalid unit assignment at block index #", entry_idx))
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
log_fail(util.c("uinit> invalid redstone relay '", entry.relay, '"'))
return false
elseif not rs_rtus[entry.relay] then
log.debug(util.c("uinit> 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("uinit> 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("uinit> 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("uinit> 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 bank = rs_rtus[phy].banks[for_reactor]
local conns = all_conns[for_reactor]
if not valid then
log_fail(util.c("uinit> invalid redstone definition at block index #", entry_idx))
return false
else
-- link redstone in RTU
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(conns, entry.port) then
local message = util.c("uinit> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name)
println(message)
log.warning(message)
else
table.insert(bank, entry)
end
elseif mode == rsio.IO_MODE.ANALOG_IN then
-- can't have duplicate inputs
if util.table_contains(conns, entry.port) then
local message = util.c("uinit> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name)
println(message)
log.warning(message)
else
table.insert(bank, entry)
end
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.fatal("uinit> failed to identify IO mode at block index #" .. entry_idx)
println("uinit> encountered a software error, check logs")
return false
end
table.insert(conns, entry.port)
log.debug(util.c("uinit> banked redstone ", #conns, ": ", rsio.to_string(entry.port), " (", iface_name, " @ ", phy_name, ") for ", assignment))
end
end
-- create unit entries for redstone RTUs
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("uinit> failed to identify IO mode of ", rsio.to_string(conn.port), " (", entry_iface_name(conn), " @ ", phy_name, ") for ", assign))
println("uinit> encountered a software error, check logs")
return false
end
table.insert(conns, conn.port)
log.debug(util.c("uinit> 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,
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,
thread = nil
}
table.insert(units, unit)
local type = util.trinary(def.phy == rs, "redstone", "redstone_relay")
log.info(util.c("uinit> initialized RTU unit #", #units, ": ", unit.name, " (", type, ")"))
unit.uid = #units
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
end
--#endregion
--#region Mounted Peripherals
for i = 1, #rtu_devices do
local entry = rtu_devices[i] ---@type rtu_peri_definition
local name = entry.name
local index = entry.index
local for_reactor = util.trinary(entry.unit == nil, 0, entry.unit)
-- CHECK: name is a string
if type(name) ~= "string" then
log_fail(util.c("uinit> device entry #", i, ": device ", name, " isn't a string"))
return false
end
-- CHECK: index type
if (index ~= nil) and (not util.is_int(index)) then
log_fail(util.c("uinit> device entry #", i, ": index ", index, " isn't valid"))
return false
end
-- CHECK: index range
local function validate_index(min, max)
if (not util.is_int(index)) or ((index < min) and (max ~= nil and index > max)) then
local message = util.c("uinit> device entry #", i, ": index ", index, " isn't >= ", min)
if max ~= nil then message = util.c(message, " and <= ", max) end
log_fail(message)
return false
else return true end
end
-- CHECK: reactor is an integer >= 0
local function validate_assign(for_facility)
if for_facility and for_reactor ~= 0 then
log_fail(util.c("uinit> device entry #", i, ": must only be for the facility"))
return false
elseif (not for_facility) and ((not util.is_int(for_reactor)) or (for_reactor < 1) or (for_reactor > 4)) then
log_fail(util.c("uinit> device entry #", i, ": unit assignment ", for_reactor, " isn't vaild"))
return false
else return true end
end
local device = ppm.get_periph(name)
local type ---@type string|nil
local rtu_iface ---@type rtu_device
local rtu_type ---@type RTU_UNIT_TYPE
local is_multiblock = false ---@type boolean
local formed = nil ---@type boolean|nil
local faulted = nil ---@type boolean|nil
if device == nil then
local message = util.c("uinit> '", name, "' not found, using placeholder")
println(message)
log.warning(message)
-- mount a virtual (placeholder) device
type, device = ppm.mount_virtual()
else
type = ppm.get_type(name)
end
if type == "boilerValve" then
-- boiler multiblock
if not validate_index(1, 2) then return false end
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.BOILER_VALVE
rtu_iface, faulted = boilerv_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("uinit> failed to check if '", name, "' is formed"))
log.warning(util.c("uinit> failed to check if '", name, "' is a formed boiler multiblock"))
end
elseif type == "turbineValve" then
-- turbine multiblock
if not validate_index(1, 3) then return false end
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE
rtu_iface, faulted = turbinev_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("uinit> failed to check if '", name, "' is formed"))
log.warning(util.c("uinit> failed to check if '", name, "' is a formed turbine multiblock"))
end
elseif type == "dynamicValve" then
-- dynamic tank multiblock
if entry.unit == nil then
if not validate_index(1, 4) then return false end
if not validate_assign(true) then return false end
else
if not validate_index(1, 1) then return false end
if not validate_assign() then return false end
end
rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE
rtu_iface, faulted = dynamicv_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("uinit> failed to check if '", name, "' is formed"))
log.warning(util.c("uinit> failed to check if '", name, "' is a formed dynamic tank multiblock"))
end
elseif type == "inductionPort" or type == "reinforcedInductionPort" then
-- induction matrix multiblock (normal or reinforced)
if not validate_assign(true) then return false end
rtu_type = RTU_UNIT_TYPE.IMATRIX
rtu_iface, faulted = imatrix_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("uinit> failed to check if '", name, "' is formed"))
log.warning(util.c("uinit> failed to check if '", name, "' is a formed induction matrix multiblock"))
end
elseif type == "spsPort" then
-- SPS multiblock
if not validate_assign(true) then return false end
rtu_type = RTU_UNIT_TYPE.SPS
rtu_iface, faulted = sps_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("uinit> failed to check if '", name, "' is formed"))
log.warning(util.c("uinit> failed to check if '", name, "' is a formed SPS multiblock"))
end
elseif type == "solarNeutronActivator" then
-- SNA
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.SNA
rtu_iface, faulted = sna_rtu.new(device)
elseif type == "environmentDetector" or type == "environment_detector" then
-- advanced peripherals environment detector
if not validate_index(1) then return false end
if not validate_assign(entry.unit == nil) then return false end
rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
rtu_iface, faulted = envd_rtu.new(device)
elseif type == ppm.VIRTUAL_DEVICE_TYPE then
-- placeholder device
rtu_type = RTU_UNIT_TYPE.VIRTUAL
rtu_iface = rtu.init_unit().interface()
else
log_fail(util.c("uinit> device '", name, "' is not a known type (", type, ")"))
return false
end
if is_multiblock then
if not formed then
if formed == false then
log.info(util.c("uinit> 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
formed = false
log.warning(util.c("uinit> device '", name, "' is formed, but initialization had one or more faults: marked as unformed"))
end
end
---@class rtu_registry_entry
local rtu_unit = {
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 = "the facility"
if for_reactor > 0 then
for_message = util.c("reactor ", for_reactor)
end
local index_str = util.trinary(index ~= nil, util.c(" [", index, "]"), "")
log.info(util.c("uinit> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ")", index_str, " for ", for_message))
rtu_unit.uid = #units
-- determine hardware status
if rtu_unit.type == RTU_UNIT_TYPE.VIRTUAL then
rtu_unit.hw_state = RTU_HW_STATE.OFFLINE
else
if rtu_unit.is_multiblock then
rtu_unit.hw_state = util.trinary(rtu_unit.formed == true, RTU_HW_STATE.OK, RTU_HW_STATE.UNFORMED)
elseif faulted then
rtu_unit.hw_state = RTU_HW_STATE.FAULTED
else
rtu_unit.hw_state = RTU_HW_STATE.OK
end
end
-- report hardware status
databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state)
end
--#endregion
return true
end

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.3"
comms.api_version = "0.0.8"
comms.version = "3.1.0"
comms.api_version = "0.0.10"
---@enum PROTOCOL
local PROTOCOL = {
@@ -49,28 +49,33 @@ local MGMT_TYPE = {
ESTABLISH = 0, -- establish new connection
KEEP_ALIVE = 1, -- keep alive packet w/ RTT
CLOSE = 2, -- close a connection
RTU_ADVERT = 3, -- RTU capability advertisement
RTU_DEV_REMOUNT = 4, -- RTU multiblock possbily changed (formed, unformed) due to PPM remount
RTU_TONE_ALARM = 5, -- instruct RTUs to play specified alarm tones
DIAG_TONE_GET = 6, -- diagnostic: get alarm tones
DIAG_TONE_SET = 7, -- diagnostic: set alarm tones
DIAG_ALARM_SET = 8 -- diagnostic: set alarm to simulate audio for
PROBE = 3,
RTU_ADVERT = 4, -- RTU capability advertisement
RTU_DEV_REMOUNT = 5, -- RTU multiblock possbily changed (formed, unformed) due to PPM remount
RTU_TONE_ALARM = 6, -- instruct RTUs to play specified alarm tones
DIAG_TONE_GET = 7, -- (API) diagnostic: get alarm tones
DIAG_TONE_SET = 8, -- (API) diagnostic: set alarm tones
DIAG_ALARM_SET = 9, -- (API) diagnostic: set alarm to simulate audio for
INFO_LIST_CMP = 10 -- (API) info: list all computers on the network
}
---@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 for the control app
API_GET_PROC = 10, -- API: get data for the process app
API_GET_WASTE = 11 -- API: get data for the waste 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
API_GET_RAD = 14 -- API: get data for the radiation monitor app
}
---@enum ESTABLISH_ACK
@@ -85,6 +90,12 @@ local ESTABLISH_ACK = {
---@enum DEVICE_TYPE device types for establish messages
local DEVICE_TYPE = { PLC = 0, RTU = 1, SVR = 2, CRD = 3, PKT = 4 }
---@enum PROBE_ACK
local PROBE_ACK = {
OPEN = 0,
CONFLICT = 1
}
---@enum PLC_AUTO_ACK
local PLC_AUTO_ACK = {
FAIL = 0, -- failed to set burn rate/burn rate invalid
@@ -126,6 +137,8 @@ comms.CRDN_TYPE = CRDN_TYPE
comms.ESTABLISH_ACK = ESTABLISH_ACK
comms.DEVICE_TYPE = DEVICE_TYPE
comms.PROBE_ACK = PROBE_ACK
comms.PLC_AUTO_ACK = PLC_AUTO_ACK
comms.UNIT_COMMAND = UNIT_COMMAND
@@ -134,6 +147,9 @@ comms.FAC_COMMAND = FAC_COMMAND
-- destination broadcast address (to all devices)
comms.BROADCAST = -1
-- firmware version used to indicate an establish packet is a connection test
comms.CONN_TEST_FWV = "CONN_TEST"
---@alias packet scada_packet|modbus_packet|rplc_packet|mgmt_packet|crdn_packet
---@alias frame modbus_frame|rplc_frame|mgmt_frame|crdn_frame
@@ -201,7 +217,7 @@ function comms.scada_packet()
if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then
-- outside of maximum allowable transmission distance
-- log.debug("comms.scada_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
-- log.debug("COMMS: scada_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
else
if type(self.raw) == "table" then
if #self.raw == 5 then
@@ -247,6 +263,8 @@ function comms.scada_packet()
---@nodiscard
function public.raw_sendable() return self.raw end
---@nodiscard
function public.interface() return self.modem_msg_in.iface end
---@nodiscard
function public.local_channel() return self.modem_msg_in.s_channel end
---@nodiscard
@@ -322,7 +340,7 @@ function comms.authd_packet()
if (type(max_distance) == "number") and ((type(distance) ~= "number") or (distance > max_distance)) then
-- outside of maximum allowable transmission distance
-- log.debug("comms.authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
-- log.debug("COMMS: authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
else
if type(self.raw) == "table" then
if #self.raw == 4 then
@@ -408,7 +426,7 @@ function comms.modbus_packet()
self.raw = { self.txn_id, self.unit_id, self.func_code }
for i = 1, self.length do insert(self.raw, data[i]) end
else
log.error("comms.modbus_packet.make(): data not a table")
log.error("COMMS: modbus_packet.make(): data not a table")
end
end
@@ -431,11 +449,11 @@ function comms.modbus_packet()
return size_ok and valid
else
log.debug("attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true)
log.debug("COMMS: attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
log.debug("nil frame encountered", true)
log.debug("COMMS: nil frame encountered", true)
return false
end
end
@@ -494,7 +512,7 @@ function comms.rplc_packet()
self.raw = { self.id, self.type }
for i = 1, #data do insert(self.raw, data[i]) end
else
log.error("comms.rplc_packet.make(): data not a table")
log.error("COMMS: rplc_packet.make(): data not a table")
end
end
@@ -517,11 +535,11 @@ function comms.rplc_packet()
return ok
else
log.debug("attempted RPLC parse of incorrect protocol " .. frame.protocol(), true)
log.debug("COMMS: attempted RPLC parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
log.debug("nil frame encountered", true)
log.debug("COMMS: nil frame encountered", true)
return false
end
end
@@ -576,7 +594,7 @@ function comms.mgmt_packet()
self.raw = { self.type }
for i = 1, #data do insert(self.raw, data[i]) end
else
log.error("comms.mgmt_packet.make(): data not a table")
log.error("COMMS: mgmt_packet.make(): data not a table")
end
end
@@ -597,11 +615,11 @@ function comms.mgmt_packet()
return ok
else
log.debug("attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true)
log.debug("COMMS: attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
log.debug("nil frame encountered", true)
log.debug("COMMS: nil frame encountered", true)
return false
end
end
@@ -655,7 +673,7 @@ function comms.crdn_packet()
self.raw = { self.type }
for i = 1, #data do insert(self.raw, data[i]) end
else
log.error("comms.crdn_packet.make(): data not a table")
log.error("COMMS: crdn_packet.make(): data not a table")
end
end
@@ -676,11 +694,11 @@ function comms.crdn_packet()
return ok
else
log.debug("attempted SCADA_CRDN parse of incorrect protocol " .. frame.protocol(), true)
log.debug("COMMS: attempted SCADA_CRDN parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
log.debug("nil frame encountered", true)
log.debug("COMMS: nil frame encountered", true)
return false
end
end

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
@@ -86,6 +88,7 @@ constants.FLOW_STABILITY_DELAY_MS = 10000
-- - background radiation 0.0000001 Sv/h (99.99 nSv/h)
-- - "green tint" radiation 0.00001 Sv/h (10 uSv/h)
-- - damaging radiation 0.00006 Sv/h (60 uSv/h)
constants.LOW_RADIATION = 0.00001
constants.HAZARD_RADIATION = 0.00006
constants.HIGH_RADIATION = 0.001
@@ -93,6 +96,11 @@ constants.VERY_HIGH_RADIATION = 0.1
constants.SEVERE_RADIATION = 8.0
constants.EXTREME_RADIATION = 100.0
-- nominal RTT is ping (0ms to 10ms usually) + 150ms for SV main loop tick
constants.WARN_RTT = 300 -- 2x as long as expected w/ 0 ping
constants.HIGH_RTT = 500 -- 3.33x as long as expected w/ 0 ping
--#endregion
--#region Mekanism Configuration Constants

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

@@ -20,7 +20,7 @@ local MODE = { APPEND = 0, NEW = 1 }
log.MODE = MODE
local logger = {
local _log = {
not_ready = true,
path = "/log.txt",
mode = MODE.APPEND,
@@ -42,36 +42,36 @@ local free_space = fs.getFreeSpace
---@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))
return (free_space(_log.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 function write_log(msg_bits)
if _log.not_ready then return end
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 ()
logger.file.writeLine(stamped)
logger.file.flush()
_log.file.writeLine(stamped)
_log.file.flush()
end)
-- if we don't have space, we need to create a new log file
if check_out_of_space() then
-- delete the old log file before opening a new one
logger.file.close()
fs.delete(logger.path)
_log.file.close()
fs.delete(_log.path)
-- re-init logger and pass dmesg_out so that it doesn't change
log.init(logger.path, logger.mode, logger.debug, logger.dmesg_out)
log.init(_log.path, _log.mode, _log.debug, _log.dmesg_out)
-- log the message and recycle warning
logger.file.writeLine(time_stamp .. WRN_TAG .. "recycled log file")
logger.file.writeLine(stamped)
logger.file.flush()
_log.file.writeLine(time_stamp .. WRN_TAG .. "recycled log file")
_log.file.writeLine(stamped)
_log.file.flush()
elseif (not status) and (result ~= nil) then
util.println("unexpected error writing to the log file: " .. result)
end
@@ -89,45 +89,45 @@ end
function log.init(path, write_mode, include_debug, dmesg_redirect)
local err_msg
logger.path = path
logger.mode = write_mode
logger.debug = include_debug
logger.file, err_msg = fs.open(path, util.trinary(logger.mode == MODE.APPEND, "a", "w"))
_log.path = path
_log.mode = write_mode
_log.debug = include_debug
_log.file, err_msg = fs.open(path, util.trinary(_log.mode == MODE.APPEND, "a", "w"))
if dmesg_redirect then
logger.dmesg_out = dmesg_redirect
_log.dmesg_out = dmesg_redirect
else
logger.dmesg_out = term.current()
_log.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 _log.file == nil or out_of_space then
if out_of_space then
if fs.exists(logger.path) then
fs.delete(logger.path)
if fs.exists(_log.path) then
fs.delete(_log.path)
logger.file, err_msg = fs.open(path, util.trinary(logger.mode == MODE.APPEND, "a", "w"))
_log.file, err_msg = fs.open(path, util.trinary(_log.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()
if _log.file then
_log.file.writeLine(os.date(TIME_FMT) .. WRN_TAG .. "init recycled log file")
_log.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
_log.not_ready = false
end
-- close the log file handle
function log.close() logger.file.close() end
function log.close() _log.file.close() end
-- direct dmesg output to a monitor/window
---@param window Window window or terminal reference
function log.direct_dmesg(window) logger.dmesg_out = window end
function log.direct_dmesg(window) _log.dmesg_out = window end
-- dmesg style logging for boot because I like linux-y things
---@param msg any message
@@ -142,7 +142,7 @@ function log.dmesg(msg, tag, tag_color)
tag = util.strval(tag or "")
local t_stamp = string.format("%12.2f", os.clock())
local out = logger.dmesg_out
local out = _log.dmesg_out
if out ~= nil then
local out_w, out_h = out.getSize()
@@ -180,7 +180,7 @@ function log.dmesg(msg, tag, tag_color)
if cur_y == out_h then
out.scroll(1)
out.setCursorPos(1, cur_y)
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1
_log.dmesg_scroll_count = _log.dmesg_scroll_count + 1
else
out.setCursorPos(1, cur_y + 1)
end
@@ -216,7 +216,7 @@ function log.dmesg(msg, tag, tag_color)
if cur_y == out_h then
out.scroll(1)
out.setCursorPos(1, cur_y)
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1
_log.dmesg_scroll_count = _log.dmesg_scroll_count + 1
else
out.setCursorPos(1, cur_y + 1)
end
@@ -225,9 +225,9 @@ function log.dmesg(msg, tag, tag_color)
out.write(lines[i])
end
logger.dmesg_restore_coord = { out.getCursorPos() }
_log.dmesg_restore_coord = { out.getCursorPos() }
_log{"[", t_stamp, "] [", tag, "] ", msg}
write_log{"[", t_stamp, "] [", tag, "] ", msg}
end
return ts_coord
@@ -241,9 +241,9 @@ end
---@return function update, function done
function log.dmesg_working(msg, tag, tag_color)
local ts_coord = log.dmesg(msg, tag, tag_color)
local initial_scroll = logger.dmesg_scroll_count
local initial_scroll = _log.dmesg_scroll_count
local out = logger.dmesg_out
local out = _log.dmesg_out
local width = (ts_coord.x2 - ts_coord.x1) + 1
if out ~= nil then
@@ -252,7 +252,7 @@ function log.dmesg_working(msg, tag, tag_color)
local counter = 0
local function update(sec_remaining)
local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll)
local new_y = ts_coord.y - (_log.dmesg_scroll_count - initial_scroll)
if new_y < 1 then return end
local time = util.sprintf("%ds", sec_remaining)
@@ -280,11 +280,11 @@ function log.dmesg_working(msg, tag, tag_color)
counter = counter + 1
out.setCursorPos(table.unpack(logger.dmesg_restore_coord))
out.setCursorPos(table.unpack(_log.dmesg_restore_coord))
end
local function done(ok)
local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll)
local new_y = ts_coord.y - (_log.dmesg_scroll_count - initial_scroll)
if new_y < 1 then return end
out.setCursorPos(ts_coord.x1, new_y)
@@ -299,7 +299,7 @@ function log.dmesg_working(msg, tag, tag_color)
out.setTextColor(initial_color)
out.setCursorPos(table.unpack(logger.dmesg_restore_coord))
out.setCursorPos(table.unpack(_log.dmesg_restore_coord))
end
return update, done
@@ -312,28 +312,28 @@ end
---@param msg any message
---@param trace? boolean include file trace
function log.debug(msg, trace)
if logger.debug then
if _log.debug then
if trace then
local info = debug.getinfo(2)
if info.name ~= nil then
_log{DBG_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
write_log{DBG_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
else
_log{DBG_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
write_log{DBG_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
end
else
_log{DBG_TAG, msg}
write_log{DBG_TAG, msg}
end
end
end
-- log info messages
---@param msg any message
function log.info(msg) _log{INF_TAG, msg} end
function log.info(msg) write_log{INF_TAG, msg} end
-- log warning messages
---@param msg any message
function log.warning(msg) _log{WRN_TAG, msg} end
function log.warning(msg) write_log{WRN_TAG, msg} end
-- log error messages
---@param msg any message
@@ -343,17 +343,17 @@ function log.error(msg, trace)
local info = debug.getinfo(2)
if info.name ~= nil then
_log{ERR_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
write_log{ERR_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
else
_log{ERR_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
write_log{ERR_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
end
else
_log{ERR_TAG, msg}
write_log{ERR_TAG, msg}
end
end
-- log fatal errors
---@param msg any message
function log.fatal(msg) _log{FTL_TAG, msg} end
function log.fatal(msg) write_log{FTL_TAG, msg} end
return log

View File

@@ -1,9 +1,10 @@
--
-- Network Communications
-- Network Communications and Message Authentication
--
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local md5 = require("lockbox.digest.md5")
@@ -17,7 +18,7 @@ local array = require("lockbox.util.array")
local network = {}
-- cryptography engine
local c_eng = {
local _crypt = {
key = nil,
hmac = nil
}
@@ -39,23 +40,23 @@ function network.init_mac(passkey)
key_deriv.setPassword(passkey)
key_deriv.finish()
c_eng.key = array.fromHex(key_deriv.asHex())
_crypt.key = array.fromHex(key_deriv.asHex())
-- initialize HMAC
c_eng.hmac = hmac()
c_eng.hmac.setBlockSize(64)
c_eng.hmac.setDigest(md5)
c_eng.hmac.setKey(c_eng.key)
_crypt.hmac = hmac()
_crypt.hmac.setBlockSize(64)
_crypt.hmac.setDigest(md5)
_crypt.hmac.setKey(_crypt.key)
local init_time = util.time_ms() - start
log.info("network.init_mac completed in " .. init_time .. "ms")
log.info("NET: network.init_mac() completed in " .. init_time .. "ms")
return init_time
end
-- de-initialize message authentication system
function network.deinit_mac()
c_eng.key, c_eng.hmac = nil, nil
_crypt.key, _crypt.hmac = nil, nil
end
-- generate HMAC of message
@@ -64,29 +65,41 @@ end
local function compute_hmac(message)
-- local start = util.time_ms()
c_eng.hmac.init()
c_eng.hmac.update(stream.fromString(message))
c_eng.hmac.finish()
_crypt.hmac.init()
_crypt.hmac.update(stream.fromString(message))
_crypt.hmac.finish()
local hash = c_eng.hmac.asHex()
local hash = _crypt.hmac.asHex()
-- log.debug("compute_hmac(): hmac-md5 = " .. util.strval(hash) .. " (took " .. (util.time_ms() - start) .. "ms)")
-- log.debug("NET: compute_hmac(): hmac-md5 = " .. util.strval(hash) .. " (took " .. (util.time_ms() - start) .. "ms)")
return hash
end
-- NIC: Network Interface Controller<br>
-- utilizes HMAC-MD5 for message authentication, if enabled
---@param modem Modem modem to use
-- utilizes HMAC-MD5 for message authentication, if enabled and this is wireless
---@param modem Modem|nil modem to use
function network.nic(modem)
local self = {
connected = true, -- used to avoid costly MAC calculations if modem isn't even present
-- modem interface name
iface = "?",
-- phy name
name = "?",
-- used to quickly return out of tx/rx functions if there is nothing to do
connected = false,
-- used to avoid costly MAC calculations if not required
use_hash = false,
-- open channels
channels = {}
}
---@class nic:Modem
local public = {}
-- get the phy name
---@nodiscard
function public.phy_name() return self.name end
-- check if this NIC has a connected modem
---@nodiscard
function public.is_connected() return self.connected end
@@ -95,9 +108,14 @@ function network.nic(modem)
---@param reconnected_modem Modem
function public.connect(reconnected_modem)
modem = reconnected_modem
self.connected = true
-- open previously opened channels
self.iface = ppm.get_iface(modem)
self.name = util.c(util.trinary(modem.isWireless(), "WLAN_PHY", "ETH_PHY"), "{", self.iface, "}")
self.connected = true
self.use_hash = _crypt.hmac and modem.isWireless()
-- open only previously opened channels
modem.closeAll()
for _, channel in ipairs(self.channels) do
modem.open(channel)
end
@@ -117,13 +135,13 @@ function network.nic(modem)
function public.is_modem(device) return device == modem end
-- wrap modem functions, then create custom functions
public.connect(modem)
if modem then public.connect(modem) end
-- open a channel on the modem<br>
-- if disconnected *after* opening, previousy opened channels will be re-opened on reconnection
---@param channel integer
function public.open(channel)
modem.open(channel)
if modem then modem.open(channel) end
local already_open = false
for i = 1, #self.channels do
@@ -141,7 +159,7 @@ function network.nic(modem)
-- close a channel on the modem
---@param channel integer
function public.close(channel)
modem.close(channel)
if modem then modem.close(channel) end
for i = 1, #self.channels do
if self.channels[i] == channel then
@@ -153,7 +171,7 @@ function network.nic(modem)
-- close all channels on the modem
function public.closeAll()
modem.closeAll()
if modem then modem.closeAll() end
self.channels = {}
end
@@ -165,17 +183,20 @@ function network.nic(modem)
if self.connected then
local tx_packet = packet ---@type authd_packet|scada_packet
if c_eng.hmac ~= nil then
if self.use_hash then
-- local start = util.time_ms()
tx_packet = comms.authd_packet()
---@cast tx_packet authd_packet
tx_packet.make(packet, compute_hmac)
-- log.debug("network.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms")
-- log.debug("NET: network.modem.transmit(): data processing took " .. (util.time_ms() - start) .. "ms")
end
---@diagnostic disable-next-line: need-check-nil
modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable())
else
log.debug("NET: network.transmit() tx dropped, link is down")
end
end
@@ -190,10 +211,10 @@ function network.nic(modem)
function public.receive(side, sender, reply_to, message, distance)
local packet = nil
if self.connected then
if self.connected and side == self.iface then
local s_packet = comms.scada_packet()
if c_eng.hmac ~= nil then
if self.use_hash then
-- parse packet as an authenticated SCADA packet
local a_packet = comms.authd_packet()
a_packet.receive(side, sender, reply_to, message, distance)
@@ -206,10 +227,10 @@ function network.nic(modem)
local computed_hmac = compute_hmac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true }))
if a_packet.mac() == computed_hmac then
-- log.debug("network.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms")
-- log.debug("NET: network.modem.receive(): HMAC verified in " .. (util.time_ms() - start) .. "ms")
s_packet.stamp_authenticated()
else
-- log.debug("network.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
-- log.debug("NET: network.modem.receive(): HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
end
end
end

View File

@@ -22,7 +22,7 @@ ppm.VIRTUAL_DEVICE_TYPE = VIRTUAL_DEVICE_TYPE
local REPORT_FREQUENCY = 20 -- log every 20 faults per function
local ppm_sys = {
local _ppm = {
mounts = {}, ---@type { [string]: ppm_entry }
next_vid = 0,
auto_cf = false,
@@ -66,7 +66,7 @@ local function peri_init(iface)
if status then
-- auto fault clear
if self.auto_cf then self.faulted = false end
if ppm_sys.auto_cf then ppm_sys.faulted = false end
if _ppm.auto_cf then _ppm.faulted = false end
self.fault_counts[key] = 0
@@ -78,10 +78,10 @@ local function peri_init(iface)
self.faulted = true
self.last_fault = result
ppm_sys.faulted = true
ppm_sys.last_fault = result
_ppm.faulted = true
_ppm.last_fault = result
if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
if not _ppm.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
local count_str = ""
if self.fault_counts[key] > 0 then
count_str = " [" .. self.fault_counts[key] .. " total faults]"
@@ -92,7 +92,7 @@ local function peri_init(iface)
self.fault_counts[key] = self.fault_counts[key] + 1
if result == "Terminated" then ppm_sys.terminate = true end
if result == "Terminated" then _ppm.terminate = true end
return ACCESS_FAULT, result
end
@@ -159,10 +159,10 @@ local function peri_init(iface)
self.faulted = true
self.last_fault = UNDEFINED_FIELD
ppm_sys.faulted = true
ppm_sys.last_fault = UNDEFINED_FIELD
_ppm.faulted = true
_ppm.last_fault = UNDEFINED_FIELD
if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
if not _ppm.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
local count_str = ""
if self.fault_counts[key] > 0 then
count_str = " [" .. self.fault_counts[key] .. " total calls]"
@@ -193,35 +193,35 @@ end
-- REPORTING --
-- silence error prints
function ppm.disable_reporting() ppm_sys.mute = true end
function ppm.disable_reporting() _ppm.mute = true end
-- allow error prints
function ppm.enable_reporting() ppm_sys.mute = false end
function ppm.enable_reporting() _ppm.mute = false end
-- FAULT MEMORY --
-- enable automatically clearing fault flag
function ppm.enable_afc() ppm_sys.auto_cf = true end
function ppm.enable_afc() _ppm.auto_cf = true end
-- disable automatically clearing fault flag
function ppm.disable_afc() ppm_sys.auto_cf = false end
function ppm.disable_afc() _ppm.auto_cf = false end
-- clear fault flag
function ppm.clear_fault() ppm_sys.faulted = false end
function ppm.clear_fault() _ppm.faulted = false end
-- check fault flag
---@nodiscard
function ppm.is_faulted() return ppm_sys.faulted end
function ppm.is_faulted() return _ppm.faulted end
-- get the last fault message
---@nodiscard
function ppm.get_last_fault() return ppm_sys.last_fault end
function ppm.get_last_fault() return _ppm.last_fault end
-- TERMINATION --
-- if a caught error was a termination request
---@nodiscard
function ppm.should_terminate() return ppm_sys.terminate end
function ppm.should_terminate() return _ppm.terminate end
-- MOUNTING --
@@ -229,12 +229,12 @@ function ppm.should_terminate() return ppm_sys.terminate end
function ppm.mount_all()
local ifaces = peripheral.getNames()
ppm_sys.mounts = {}
_ppm.mounts = {}
for i = 1, #ifaces do
ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i])
_ppm.mounts[ifaces[i]] = peri_init(ifaces[i])
log.info(util.c("PPM: found a ", ppm_sys.mounts[ifaces[i]].type, " (", ifaces[i], ")"))
log.info(util.c("PPM: found a ", _ppm.mounts[ifaces[i]].type, " (", ifaces[i], ")"))
end
if #ifaces == 0 then
@@ -253,10 +253,10 @@ function ppm.mount(iface)
for i = 1, #ifaces do
if iface == ifaces[i] then
ppm_sys.mounts[iface] = peri_init(iface)
_ppm.mounts[iface] = peri_init(iface)
pm_type = ppm_sys.mounts[iface].type
pm_dev = ppm_sys.mounts[iface].dev
pm_type = _ppm.mounts[iface].type
pm_dev = _ppm.mounts[iface].dev
log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type))
break
@@ -278,12 +278,12 @@ function ppm.remount(iface)
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.unmount(_ppm.mounts[iface].dev)
ppm_sys.mounts[iface] = peri_init(iface)
_ppm.mounts[iface] = peri_init(iface)
pm_type = ppm_sys.mounts[iface].type
pm_dev = ppm_sys.mounts[iface].dev
pm_type = _ppm.mounts[iface].type
pm_dev = _ppm.mounts[iface].dev
log.info(util.c("PPM: remount(", iface, ") -> remounted a ", pm_type))
break
@@ -293,28 +293,28 @@ function ppm.remount(iface)
return pm_type, pm_dev
end
-- mount a virtual, placeholder device (specifically designed for RTU startup with missing devices)
-- mount a virtual placeholder device
---@nodiscard
---@return string type, table device
function ppm.mount_virtual()
local iface = "ppm_vdev_" .. ppm_sys.next_vid
local iface = "ppm_vdev_" .. _ppm.next_vid
ppm_sys.mounts[iface] = peri_init("__virtual__")
ppm_sys.next_vid = ppm_sys.next_vid + 1
_ppm.mounts[iface] = peri_init("__virtual__")
_ppm.next_vid = _ppm.next_vid + 1
log.info(util.c("PPM: mount_virtual() -> allocated new virtual device ", iface))
return ppm_sys.mounts[iface].type, ppm_sys.mounts[iface].dev
return _ppm.mounts[iface].type, _ppm.mounts[iface].dev
end
-- manually unmount a peripheral from the PPM
---@param device table device table
function ppm.unmount(device)
if device then
for iface, data in pairs(ppm_sys.mounts) do
for iface, data in pairs(_ppm.mounts) do
if data.dev == device then
log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", iface))
ppm_sys.mounts[iface] = nil
_ppm.mounts[iface] = nil
break
end
end
@@ -330,7 +330,7 @@ function ppm.handle_unmount(iface)
local pm_type = nil
-- what got disconnected?
local lost_dev = ppm_sys.mounts[iface]
local lost_dev = _ppm.mounts[iface]
if lost_dev then
pm_type = lost_dev.type
@@ -341,18 +341,18 @@ function ppm.handle_unmount(iface)
log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface))
end
ppm_sys.mounts[iface] = nil
_ppm.mounts[iface] = nil
return pm_type, pm_dev
end
-- log all mounts, to be used if `ppm.mount_all` is called before logging is ready
function ppm.log_mounts()
for iface, mount in pairs(ppm_sys.mounts) do
for iface, mount in pairs(_ppm.mounts) do
log.info(util.c("PPM: had found a ", mount.type, " (", iface, ")"))
end
if util.table_len(ppm_sys.mounts) == 0 then
if util.table_len(_ppm.mounts) == 0 then
log.warning("PPM: no devices had been found")
end
end
@@ -369,7 +369,7 @@ function ppm.list_avail() return peripheral.getNames() end
---@return { [string]: ppm_entry } mounts
function ppm.list_mounts()
local list = {}
for k, v in pairs(ppm_sys.mounts) do list[k] = v end
for k, v in pairs(_ppm.mounts) do list[k] = v end
return list
end
@@ -379,7 +379,7 @@ end
---@return string|nil iface CC peripheral interface
function ppm.get_iface(device)
if device then
for iface, data in pairs(ppm_sys.mounts) do
for iface, data in pairs(_ppm.mounts) do
if data.dev == device then return iface end
end
end
@@ -392,8 +392,8 @@ end
---@param iface string CC peripheral interface
---@return { [string]: function }|nil device function table
function ppm.get_periph(iface)
if ppm_sys.mounts[iface] then
return ppm_sys.mounts[iface].dev
if _ppm.mounts[iface] then
return _ppm.mounts[iface].dev
else return nil end
end
@@ -402,20 +402,20 @@ end
---@param iface string CC peripheral interface
---@return string|nil type
function ppm.get_type(iface)
if ppm_sys.mounts[iface] then
return ppm_sys.mounts[iface].type
if _ppm.mounts[iface] then
return _ppm.mounts[iface].type
else return nil end
end
-- get all mounted peripherals by type
---@nodiscard
---@param name string type name
---@param type string type name
---@return table devices device function tables
function ppm.get_all_devices(name)
function ppm.get_all_devices(type)
local devices = {}
for _, data in pairs(ppm_sys.mounts) do
if data.type == name then
for _, data in pairs(_ppm.mounts) do
if data.type == type then
table.insert(devices, data.dev)
end
end
@@ -430,7 +430,7 @@ end
function ppm.get_device(name)
local device = nil
for _, data in pairs(ppm_sys.mounts) do
for _, data in pairs(_ppm.mounts) do
if data.type == name then
device = data.dev
break
@@ -447,22 +447,49 @@ end
---@return table|nil reactor function table
function ppm.get_fission_reactor() return ppm.get_device("fissionReactorLogicAdapter") end
-- get a modem by name
---@nodiscard
---@param iface string CC peripheral interface
---@return Modem|nil modem function table
function ppm.get_modem(iface)
local modem = nil
local device = _ppm.mounts[iface]
if device and device.type == "modem" then modem = device.dev end
return modem
end
-- get the wireless modem (if multiple, returns the first)<br>
-- if this is in a CraftOS emulated environment, wired modems will be used instead
---@nodiscard
---@return Modem|nil modem function table
---@return Modem|nil modem, string|nil iface
function ppm.get_wireless_modem()
local w_modem = nil
local w_modem, w_iface = nil, nil
local emulated_env = periphemu ~= nil
for _, device in pairs(ppm_sys.mounts) do
for iface, device in pairs(_ppm.mounts) do
if device.type == "modem" and (emulated_env or device.dev.isWireless()) then
w_iface = iface
w_modem = device.dev
break
end
end
return w_modem
return w_modem, w_iface
end
-- list all connected wired modems
---@nodiscard
---@return { [string]: ppm_entry } modems
function ppm.get_wired_modem_list()
local list = {}
for iface, device in pairs(_ppm.mounts) do
if device.type == "modem" and not device.dev.isWireless() then list[iface] = device end
end
return list
end
-- list all connected monitors
@@ -471,7 +498,7 @@ end
function ppm.get_monitor_list()
local list = {}
for iface, device in pairs(ppm_sys.mounts) do
for iface, device in pairs(_ppm.mounts) do
if device.type == "monitor" then list[iface] = device end
end

View File

@@ -53,12 +53,12 @@ function psil.create()
if ic[key] == nil then alloc(key) end
if ic[key].value ~= value then
ic[key].value = value
for i = 1, #ic[key].subscribers do
ic[key].subscribers[i].notify(value)
end
end
ic[key].value = value
end
-- publish a toggled boolean value to a given key, passing it to all subscribers if it has changed<br>

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
@@ -212,6 +212,13 @@ end
--#region ENUMERATION TYPES
---@enum LISTEN_MODE
types.LISTEN_MODE = {
WIRELESS = 1,
WIRED = 2,
ALL = 3
}
---@enum TEMP_SCALE
types.TEMP_SCALE = {
KELVIN = 1,
@@ -465,7 +472,8 @@ types.ALARM = {
ReactorHighWaste = 9,
RPSTransient = 10,
RCSTransient = 11,
TurbineTrip = 12
TurbineTrip = 12,
FacilityRadiation = 13
}
types.ALARM_NAMES = {
@@ -480,7 +488,8 @@ types.ALARM_NAMES = {
"ReactorHighWaste",
"RPSTransient",
"RCSTransient",
"TurbineTrip"
"TurbineTrip",
"FacilityRadiation"
}
---@enum ALARM_PRIORITY
@@ -559,7 +568,7 @@ types.ALARM_STATE_NAMES = {
---| "websocket_failure"
---| "websocket_message"
---| "websocket_success"
---| "clock_start" (custom)
---| "conn_test_complete" (custom)
---@alias fluid
---| "mekanism:empty_gas"

View File

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

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

171
supervisor/backplane.lua Normal file
View File

@@ -0,0 +1,171 @@
--
-- Supervisor System Core Peripheral Backplane
--
local log = require("scada-common.log")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local databus = require("supervisor.databus")
local println = util.println
---@class supervisor_backplane
local backplane = {}
local _bp = {
config = nil, ---@type svr_config
lan_iface = false, ---@type string|false wired comms modem name
wd_nic = nil, ---@type nic|nil wired nic
wl_nic = nil, ---@type nic|nil wireless nic
nic_map = {} ---@type nic[] connected nics
}
-- network interfaces indexed by peripheral names
backplane.nics = _bp.nic_map
-- initialize the system peripheral backplane
---@param config svr_config
---@return boolean success
function backplane.init(config)
_bp.lan_iface = config.WiredModem
-- setup the wired modem, if configured
if type(_bp.lan_iface) == "string" then
local modem = ppm.get_modem(_bp.lan_iface)
local wd_nic = network.nic(modem)
log.info("BKPLN: WIRED PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. _bp.lan_iface)
if not (modem and _bp.lan_iface) then
println("startup> wired comms modem not found")
log.fatal("BKPLN: no wired comms modem on startup")
return false
end
_bp.wd_nic = wd_nic
_bp.nic_map[_bp.lan_iface] = wd_nic
wd_nic.closeAll()
wd_nic.open(config.SVR_Channel)
databus.tx_hw_wd_modem(true)
end
-- setup the wireless modem, if configured
if config.WirelessModem then
local modem, iface = ppm.get_wireless_modem()
local wl_nic = network.nic(modem)
log.info("BKPLN: WIRELESS PHY_" .. util.trinary(modem, "UP ", "DOWN") .. (iface or ""))
if not (modem and iface) then
println("startup> wireless comms modem not found")
log.fatal("BKPLN: no wireless comms modem on startup")
return false
end
_bp.wl_nic = wl_nic
_bp.nic_map[iface] = wl_nic
wl_nic.closeAll()
wl_nic.open(config.SVR_Channel)
databus.tx_hw_wl_modem(true)
end
return true
end
-- handle a backplane peripheral attach
---@param iface string
---@param type string
---@param device table
---@param print_no_fp function
function backplane.attach(iface, type, device, print_no_fp)
if type == "modem" then
---@cast device Modem
local m_is_wl = device.isWireless()
log.info(util.c("BKPLN: ", util.trinary(m_is_wl, "WIRELESS", "WIRED"), " PHY_ATTACH ", iface))
if _bp.wd_nic and (_bp.lan_iface == iface) then
-- connect this as the wired NIC
_bp.wd_nic.connect(device)
log.info("BKPLN: WIRED PHY_UP " .. iface)
print_no_fp("wired comms modem reconnected")
databus.tx_hw_wd_modem(true)
elseif _bp.wl_nic and (not _bp.wl_nic.is_connected()) and m_is_wl then
-- connect this as the wireless NIC
_bp.wl_nic.connect(device)
_bp.nic_map[iface] = _bp.wl_nic
log.info("BKPLN: WIRELESS PHY_UP " .. iface)
print_no_fp("wireless comms modem reconnected")
databus.tx_hw_wl_modem(true)
elseif _bp.wl_nic and m_is_wl then
-- the wireless NIC already has a modem
device.closeAll()
print_no_fp("standby wireless modem connected")
log.info("BKPLN: standby wireless modem connected")
else
device.closeAll()
print_no_fp("unassigned modem connected")
log.warning("BKPLN: unassigned modem connected")
end
end
end
-- handle a backplane peripheral detach
---@param iface string
---@param type string
---@param device table
---@param print_no_fp function
function backplane.detach(iface, type, device, print_no_fp)
if type == "modem" then
---@cast device Modem
log.info(util.c("BKPLN: PHY_DETACH ", iface))
_bp.nic_map[iface] = nil
if _bp.wd_nic and _bp.wd_nic.is_modem(device) then
_bp.wd_nic.disconnect()
log.info("BKPLN: WIRED PHY_DOWN " .. iface)
print_no_fp("wired modem disconnected")
log.warning("BKPLN: wired comms modem disconnected")
databus.tx_hw_wd_modem(false)
elseif _bp.wl_nic and _bp.wl_nic.is_modem(device) then
_bp.wl_nic.disconnect()
log.info("BKPLN: WIRELESS PHY_DOWN " .. iface)
print_no_fp("wireless comms modem disconnected")
log.warning("BKPLN: wireless comms modem disconnected")
local modem, m_iface = ppm.get_wireless_modem()
if modem then
log.info("BKPLN: found another wireless modem, using it for comms")
_bp.wl_nic.connect(modem)
log.info("BKPLN: WIRELESS PHY_UP " .. m_iface)
else
databus.tx_hw_wl_modem(false)
end
else
print_no_fp("unassigned modem disconnected")
log.warning("BKPLN: unassigned modem disconnected")
end
end
end
return backplane

View File

@@ -18,8 +18,6 @@ local tri = util.trinary
local cpair = core.cpair
local self = {
tank_fluid_opts = {}, ---@type Radio2D[]
vis_draw = nil, ---@type function
draw_fluid_ops = nil, ---@type function
@@ -185,8 +183,9 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
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, fac_c_8}}
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)}
@@ -205,10 +204,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
@@ -285,6 +292,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
@@ -604,7 +619,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
if type == 0 then type = 1 end
self.tank_fluid_opts[i] = nil
tool_ctl.tank_fluid_opts[i] = nil
if tank_list[i] == 1 then
local row = Div{parent=tank_fluid_list,height=2}
@@ -619,7 +634,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
tank_fluid.disable()
end
self.tank_fluid_opts[i] = tank_fluid
tool_ctl.tank_fluid_opts[i] = tank_fluid
elseif tank_list[i] == 2 then
local row = Div{parent=tank_fluid_list,height=2}
@@ -644,7 +659,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
tank_fluid.disable()
end
self.tank_fluid_opts[i] = tank_fluid
tool_ctl.tank_fluid_opts[i] = tank_fluid
next_f = next_f + 1
end
@@ -659,11 +674,9 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
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
if tool_ctl.tank_fluid_opts[i] ~= nil then
tmp_cfg.TankFluidTypes[i] = tool_ctl.tank_fluid_opts[i].get_value()
else tmp_cfg.TankFluidTypes[i] = 0 end
end
fac_pane.set_value(8)
@@ -672,25 +685,48 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
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_8,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_8,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."}
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_8,x=1,y=12,label="Enable Extended Idling",default=ini_cfg.ExtChargeIdling,box_fg_bg=cpair(colors.yellow,colors.black)}
local function back_from_idling()
fac_pane.set_value(tri(tmp_cfg.FacilityTankMode == 0, 3, 7))
end
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_8,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_8,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

View File

@@ -1,4 +1,5 @@
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local types = require("scada-common.types")
local util = require("scada-common.util")
@@ -14,6 +15,7 @@ local TextBox = require("graphics.elements.TextBox")
local Checkbox = require("graphics.elements.controls.Checkbox")
local PushButton = require("graphics.elements.controls.PushButton")
local Radio2D = require("graphics.elements.controls.Radio2D")
local RadioButton = require("graphics.elements.controls.RadioButton")
local NumberField = require("graphics.elements.form.NumberField")
@@ -25,14 +27,22 @@ local tri = util.trinary
local cpair = core.cpair
local LISTEN_MODE = types.LISTEN_MODE
local RIGHT = core.ALIGN.RIGHT
local self = {
importing_legacy = false,
update_net_cfg = nil, ---@type function
show_auth_key = nil, ---@type function
pkt_test = nil, ---@type Checkbox
pkt_chan = nil, ---@type NumberField
pkt_timeout = nil, ---@type NumberField
show_key_btn = nil, ---@type PushButton
auth_key_textbox = nil, ---@type TextBox
auth_key_value = ""
}
@@ -62,115 +72,230 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_5 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_6 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4,net_c_5,net_c_6}}
TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."}
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=1,text="Please select the network interface(s)."}
TextBox{parent=net_c_1,x=41,y=1,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_1,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
local function on_wired_change(_) tool_ctl.gen_modem_list() end
TextBox{parent=net_c_1,x=1,y=9,width=11,text="PLC Channel"}
local plc_chan = NumberField{parent=net_c_1,x=21,y=9,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=9,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg}
local wireless = Checkbox{parent=net_c_1,x=1,y=3,label="Wireless/Ender Modem",default=ini_cfg.WirelessModem,box_fg_bg=cpair(colors.lightBlue,colors.black)}
TextBox{parent=net_c_1,x=24,y=3,text="(required for Pocket)",fg_bg=g_lg_fg_bg}
local wired = Checkbox{parent=net_c_1,x=1,y=5,label="Wired Modem",default=ini_cfg.WiredModem~=false,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=on_wired_change}
TextBox{parent=net_c_1,x=3,y=6,text="this one MUST ONLY connect to SCADA computers",fg_bg=cpair(colors.red,colors._INHERIT)}
TextBox{parent=net_c_1,x=3,y=7,text="connecting it to peripherals will cause issues",fg_bg=g_lg_fg_bg}
local modem_list = ListBox{parent=net_c_1,x=1,y=8,height=5,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
TextBox{parent=net_c_1,x=1,y=10,width=19,text="RTU Gateway Channel"}
local rtu_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=10,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg}
local modem_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
TextBox{parent=net_c_1,x=1,y=11,width=19,text="Coordinator Channel"}
local crd_chan = NumberField{parent=net_c_1,x=21,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
local function submit_interfaces()
tmp_cfg.WirelessModem = wireless.get_value()
TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"}
local pkt_chan = NumberField{parent=net_c_1,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
if not wired.get_value() then
tmp_cfg.WiredModem = false
tool_ctl.gen_modem_list()
end
local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
if not (wired.get_value() or wireless.get_value()) then
modem_err.set_value("Please select a modem type.")
modem_err.show()
elseif wired.get_value() and type(tmp_cfg.WiredModem) ~= "string" then
modem_err.set_value("Please select a wired modem.")
modem_err.show()
else
self.update_net_cfg()
net_pane.set_value(2)
modem_err.hide(true)
end
end
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_interfaces,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,text="Please assign device connection interfaces if you selected multiple network interfaces."}
TextBox{parent=net_c_2,x=39,y=2,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
TextBox{parent=net_c_2,x=1,y=4,text="Reactor PLC\nRTU Gateway\nCoordinator",fg_bg=g_lg_fg_bg}
local opts = { "Wireless", "Wired", "Both" }
local plc_listen = Radio2D{parent=net_c_2,x=14,y=4,rows=1,columns=3,default=ini_cfg.PLC_Listen,options=opts,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lightBlue,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
local rtu_listen = Radio2D{parent=net_c_2,x=14,rows=1,columns=3,default=ini_cfg.RTU_Listen,options=opts,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lightBlue,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
local crd_listen = Radio2D{parent=net_c_2,x=14,rows=1,columns=3,default=ini_cfg.CRD_Listen,options=opts,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lightBlue,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
local function on_pocket_en(en)
if not en then
self.pkt_test.set_value(false)
self.pkt_test.disable()
else self.pkt_test.enable() end
end
TextBox{parent=net_c_2,y=8,text="With a wireless modem, configure Pocket access."}
local pkt_en = Checkbox{parent=net_c_2,y=10,label="Enable Pocket Access",default=ini_cfg.PocketEnabled,callback=on_pocket_en,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=24,y=10,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
self.pkt_test = Checkbox{parent=net_c_2,label="Enable Pocket Remote System Testing",default=ini_cfg.PocketTest,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=39,y=11,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
TextBox{parent=net_c_2,x=3,text="This allows remotely playing alarm sounds.",fg_bg=g_lg_fg_bg}
local function submit_net_cfg_opts()
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
tmp_cfg.PLC_Listen = plc_listen.get_value()
tmp_cfg.RTU_Listen = rtu_listen.get_value()
tmp_cfg.CRD_Listen = crd_listen.get_value()
else
if tmp_cfg.WiredModem then
tmp_cfg.PLC_Listen = LISTEN_MODE.WIRED
tmp_cfg.RTU_Listen = LISTEN_MODE.WIRED
tmp_cfg.CRD_Listen = LISTEN_MODE.WIRED
else
tmp_cfg.PLC_Listen = LISTEN_MODE.WIRELESS
tmp_cfg.RTU_Listen = LISTEN_MODE.WIRELESS
tmp_cfg.CRD_Listen = LISTEN_MODE.WIRELESS
end
end
if tmp_cfg.WirelessModem then
tmp_cfg.PocketEnabled = pkt_en.get_value()
tmp_cfg.PocketTest = self.pkt_test.get_value()
else
tmp_cfg.PocketEnabled = false
tmp_cfg.PocketTest = false
end
if tmp_cfg.PocketEnabled then
self.pkt_chan.enable()
self.pkt_timeout.enable()
else
self.pkt_chan.disable()
self.pkt_timeout.disable()
end
net_pane.set_value(3)
end
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_net_cfg_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,text="Please set the network channels below."}
TextBox{parent=net_c_3,x=1,y=3,height=4,text="Each of the 5 uniquely named channels must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=8,width=18,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_3,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_3,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=9,width=11,text="PLC Channel"}
local plc_chan = NumberField{parent=net_c_3,x=21,y=9,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_3,x=29,y=9,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=10,width=19,text="RTU Gateway Channel"}
local rtu_chan = NumberField{parent=net_c_3,x=21,y=10,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_3,x=29,y=10,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=11,width=19,text="Coordinator Channel"}
local crd_chan = NumberField{parent=net_c_3,x=21,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_3,x=29,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=12,width=14,text="Pocket Channel"}
self.pkt_chan = NumberField{parent=net_c_3,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=net_c_3,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
local chan_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_channels()
local svr_c, plc_c, rtu_c = tonumber(svr_chan.get_value()), tonumber(plc_chan.get_value()), tonumber(rtu_chan.get_value())
local crd_c, pkt_c = tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
local crd_c, pkt_c = tonumber(crd_chan.get_value()), tonumber(self.pkt_chan.get_value())
if not tmp_cfg.PocketEnabled then pkt_c = tmp_cfg.PKT_Channel or 16244 end
if svr_c ~= nil and plc_c ~= nil and rtu_c ~= nil and crd_c ~= nil and pkt_c ~= nil then
tmp_cfg.SVR_Channel, tmp_cfg.PLC_Channel, tmp_cfg.RTU_Channel = svr_c, plc_c, rtu_c
tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = crd_c, pkt_c
net_pane.set_value(2)
net_pane.set_value(4)
chan_err.hide(true)
else chan_err.show() end
end
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,text="Please set the connection timeouts below."}
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=1,y=1,text="Please set the connection timeouts below."}
TextBox{parent=net_c_4,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=8,width=11,text="PLC Timeout"}
local plc_timeout = NumberField{parent=net_c_2,x=21,y=8,width=7,default=ini_cfg.PLC_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_4,x=1,y=8,width=11,text="PLC Timeout"}
local plc_timeout = NumberField{parent=net_c_4,x=21,y=8,width=7,default=ini_cfg.PLC_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=1,y=9,width=19,text="RTU Gateway Timeout"}
local rtu_timeout = NumberField{parent=net_c_2,x=21,y=9,width=7,default=ini_cfg.RTU_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_4,x=1,y=9,width=19,text="RTU Gateway Timeout"}
local rtu_timeout = NumberField{parent=net_c_4,x=21,y=9,width=7,default=ini_cfg.RTU_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=1,y=10,width=19,text="Coordinator Timeout"}
local crd_timeout = NumberField{parent=net_c_2,x=21,y=10,width=7,default=ini_cfg.CRD_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_4,x=1,y=10,width=19,text="Coordinator Timeout"}
local crd_timeout = NumberField{parent=net_c_4,x=21,y=10,width=7,default=ini_cfg.CRD_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=1,y=11,width=14,text="Pocket Timeout"}
local pkt_timeout = NumberField{parent=net_c_2,x=21,y=11,width=7,default=ini_cfg.PKT_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_4,x=1,y=11,width=14,text="Pocket Timeout"}
self.pkt_timeout = NumberField{parent=net_c_4,x=21,y=11,width=7,default=ini_cfg.PKT_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=net_c_2,x=29,y=8,height=4,width=7,text="seconds\nseconds\nseconds\nseconds",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=29,y=8,height=4,width=7,text="seconds\nseconds\nseconds\nseconds",fg_bg=g_lg_fg_bg}
local ct_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local ct_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_timeouts()
local plc_cto, rtu_cto, crd_cto, pkt_cto = tonumber(plc_timeout.get_value()), tonumber(rtu_timeout.get_value()), tonumber(crd_timeout.get_value()), tonumber(pkt_timeout.get_value())
local plc_cto, rtu_cto, crd_cto, pkt_cto = tonumber(plc_timeout.get_value()), tonumber(rtu_timeout.get_value()), tonumber(crd_timeout.get_value()), tonumber(self.pkt_timeout.get_value())
if not tmp_cfg.PocketEnabled then pkt_cto = tmp_cfg.PKT_Timeout or 5 end
if plc_cto ~= nil and rtu_cto ~= nil and crd_cto ~= nil and pkt_cto ~= nil then
tmp_cfg.PLC_Timeout, tmp_cfg.RTU_Timeout, tmp_cfg.CRD_Timeout, tmp_cfg.PKT_Timeout = plc_cto, rtu_cto, crd_cto, pkt_cto
net_pane.set_value(3)
if tmp_cfg.WirelessModem then
net_pane.set_value(5)
else
tmp_cfg.TrustedRange = 0
tmp_cfg.AuthKey = ""
main_pane.set_value(4)
end
ct_err.hide(true)
else ct_err.show() end
end
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,text="Please set the trusted range below."}
TextBox{parent=net_c_3,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_5,x=1,y=1,text="Please set the wireless trusted range below."}
TextBox{parent=net_c_5,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents wireless connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_5,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local range = NumberField{parent=net_c_5,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local tr_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local tr_err = TextBox{parent=net_c_5,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_tr()
local range_val = tonumber(range.get_value())
if range_val ~= nil then
tmp_cfg.TrustedRange = range_val
net_pane.set_value(4)
net_pane.set_value(6)
tr_err.hide(true)
else tr_err.show() end
end
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_5,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_5,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
TextBox{parent=net_c_4,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_6,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
TextBox{parent=net_c_6,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for wireless security on multiplayer servers. All devices on the same wireless network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=1,y=11,text="Facility Auth Key"}
local key, _ = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
TextBox{parent=net_c_6,x=1,y=11,text="Auth Key (Wireless Only, Not Used for Wired)"}
local key, _ = TextField{parent=net_c_6,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
local function censor_key(enable) key.censor(tri(enable, "*", nil)) end
local hide_key = Checkbox{parent=net_c_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
local hide_key = Checkbox{parent=net_c_6,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
hide_key.set_value(true)
censor_key(true)
local key_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local key_err = TextBox{parent=net_c_6,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_auth()
local v = key.get_value()
@@ -181,8 +306,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
else key_err.show() end
end
PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_6,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_6,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
@@ -195,7 +320,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
@@ -237,7 +362,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
TextBox{parent=clr_c_1,x=1,y=7,text="Front Panel Theme"}
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."}
@@ -374,15 +499,22 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
try_set(tool_ctl.num_units, ini_cfg.UnitCount)
try_set(tool_ctl.tank_mode, ini_cfg.FacilityTankMode)
try_set(wireless, ini_cfg.WirelessModem)
try_set(wired, ini_cfg.WiredModem ~= false)
try_set(plc_listen, ini_cfg.PLC_Listen)
try_set(rtu_listen, ini_cfg.RTU_Listen)
try_set(crd_listen, ini_cfg.CRD_Listen)
try_set(pkt_en, ini_cfg.PocketEnabled)
try_set(self.pkt_test, ini_cfg.PocketTest)
try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(plc_chan, ini_cfg.PLC_Channel)
try_set(rtu_chan, ini_cfg.RTU_Channel)
try_set(crd_chan, ini_cfg.CRD_Channel)
try_set(pkt_chan, ini_cfg.PKT_Channel)
try_set(self.pkt_chan, ini_cfg.PKT_Channel)
try_set(plc_timeout, ini_cfg.PLC_Timeout)
try_set(rtu_timeout, ini_cfg.RTU_Timeout)
try_set(crd_timeout, ini_cfg.CRD_Timeout)
try_set(pkt_timeout, ini_cfg.PKT_Timeout)
try_set(self.pkt_timeout, ini_cfg.PKT_Timeout)
try_set(range, ini_cfg.TrustedRange)
try_set(key, ini_cfg.AuthKey)
try_set(mode, ini_cfg.LogMode)
@@ -402,6 +534,21 @@ 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
for i = 1, #ini_cfg.TankFluidTypes do
if tool_ctl.tank_fluid_opts[i] then
if (ini_cfg.TankFluidTypes[i] > 0) then
tool_ctl.tank_fluid_opts[i].enable()
tool_ctl.tank_fluid_opts[i].set_value(ini_cfg.TankFluidTypes[i])
else
tool_ctl.tank_fluid_opts[i].disable()
end
end
end
tool_ctl.en_fac_tanks.set_value(ini_cfg.FacilityTankMode > 0)
tool_ctl.view_cfg.enable()
@@ -466,6 +613,39 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
--#region Tool Functions
-- expose the auth key on the summary page
function self.show_auth_key()
self.show_key_btn.disable()
self.auth_key_textbox.set_value(self.auth_key_value)
end
-- update the network interface configuration options
function self.update_net_cfg()
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
plc_listen.enable()
rtu_listen.enable()
crd_listen.enable()
else
plc_listen.disable()
rtu_listen.disable()
crd_listen.disable()
end
if tmp_cfg.WirelessModem then
pkt_en.enable()
self.pkt_test.enable()
self.pkt_chan.enable()
self.pkt_timeout.enable()
else
pkt_en.set_value(false)
self.pkt_test.set_value(false)
pkt_en.disable()
self.pkt_test.disable()
self.pkt_chan.disable()
self.pkt_timeout.disable()
end
end
-- load a legacy config file
function tool_ctl.load_legacy()
local config = require("supervisor.config")
@@ -520,6 +700,9 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
tmp_cfg.FacilityTankList, tmp_cfg.FacilityTankConns = facility.generate_tank_list_and_conns(tmp_cfg.FacilityTankMode, tmp_cfg.FacilityTankDefs)
for i = 1, tmp_cfg.UnitCount do tmp_cfg.AuxiliaryCoolant[i] = false end
for i = 1, tmp_cfg.FacilityTankList do tmp_cfg.TankFluidTypes[i] = types.COOLANT_TYPE.WATER end
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
tmp_cfg.PLC_Channel = config.PLC_CHANNEL
tmp_cfg.RTU_Channel = config.RTU_CHANNEL
@@ -543,12 +726,6 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
self.importing_legacy = true
end
-- expose the auth key on the summary page
function self.show_auth_key()
self.show_key_btn.disable()
self.auth_key_textbox.set_value(self.auth_key_value)
end
-- generate the summary list
---@param cfg svr_config
function tool_ctl.gen_summary(cfg)
@@ -588,7 +765,7 @@ 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
@@ -625,6 +802,13 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
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"
@@ -642,11 +826,32 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
fluid = "sodium"
end
val = val .. tri(val == "", "", "\n") .. util.sprintf(" \x07 tank %s - %s", prefix, fluid)
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
elseif f[1] == "PLC_Listen" or f[1] == "RTU_Listen" or f[1] == "CRD_Listen" then
if raw == LISTEN_MODE.WIRELESS then val = "Wireless Only"
elseif raw == LISTEN_MODE.WIRED then val = "Wired Only"
elseif raw == LISTEN_MODE.ALL then val = "Wireless and Wired" end
end
if not skip then
@@ -655,7 +860,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, 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
@@ -675,6 +880,59 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
end
end
-- generate the list of available/assigned wired modems
function tool_ctl.gen_modem_list()
modem_list.remove_all()
local enable = wired.get_value()
local function select(iface)
tmp_cfg.WiredModem = iface
tool_ctl.gen_modem_list()
end
local modems = ppm.get_wired_modem_list()
local missing = { tmp = true, ini = true }
for iface, _ in pairs(modems) do
if ini_cfg.WiredModem == iface then missing.ini = false end
if tmp_cfg.WiredModem == iface then missing.tmp = false end
end
if missing.tmp and tmp_cfg.WiredModem then
local line = Div{parent=modem_list,x=1,y=1,height=1}
TextBox{parent=line,x=1,y=1,width=4,text="Used",fg_bg=cpair(tri(enable,colors.blue,colors.gray),colors.white)}
PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}.disable()
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
TextBox{parent=line,x=25,y=1,text=tmp_cfg.WiredModem}
end
if missing.ini and ini_cfg.WiredModem and (tmp_cfg.WiredModem ~= ini_cfg.WiredModem) then
local line = Div{parent=modem_list,x=1,y=1,height=1}
local used = tmp_cfg.WiredModem == ini_cfg.WiredModem
TextBox{parent=line,x=1,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(ini_cfg.WiredModem)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
TextBox{parent=line,x=25,y=1,text=ini_cfg.WiredModem}
if used or not enable then select_btn.disable() end
end
-- list wired modems
for iface, _ in pairs(modems) do
local line = Div{parent=modem_list,x=1,y=1,height=1}
local used = tmp_cfg.WiredModem == iface
TextBox{parent=line,x=1,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(iface)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
TextBox{parent=line,x=15,y=1,text=iface}
if used or not enable then select_btn.disable() end
end
end
--#endregion
end

View File

@@ -3,7 +3,9 @@
--
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local types = require("scada-common.types")
local util = require("scada-common.util")
local facility = require("supervisor.config.facility")
@@ -31,7 +33,8 @@ local CENTER = core.ALIGN.CENTER
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.6.0", { "Added sodium emergency coolant option" } }
{ "v1.6.0", { "Added sodium emergency coolant option" } },
{ "v1.8.0", { "Added support for wired communications modems", "Added option for allowing Pocket connections", "Added option for allowing Pocket test commands" } }
}
---@class svr_configurator
@@ -67,12 +70,16 @@ local tool_ctl = {
num_units = nil, ---@type NumberField
en_fac_tanks = nil, ---@type Checkbox
tank_mode = nil, ---@type RadioButton
tank_fluid_opts = {}, ---@type Radio2D[]
gen_summary = nil, ---@type function
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 }[]
gen_modem_list = function () end
}
---@class svr_config
@@ -84,7 +91,15 @@ local tmp_cfg = {
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,
WirelessModem = true,
WiredModem = false, ---@type string|false
PLC_Listen = 1, ---@type LISTEN_MODE
RTU_Listen = 1, ---@type LISTEN_MODE
CRD_Listen = 1, ---@type LISTEN_MODE
PocketEnabled = true,
PocketTest = true,
SVR_Channel = nil, ---@type integer
PLC_Channel = nil, ---@type integer
RTU_Channel = nil, ---@type integer
@@ -117,7 +132,15 @@ local fields = {
{ "FacilityTankList", "Facility Tank List", {} }, -- hidden
{ "FacilityTankConns", "Facility Tank Connections", {} }, -- hidden
{ "TankFluidTypes", "Tank Fluid Types", {} },
{ "AuxiliaryCoolant", "Auxiliary Water Coolant", {} },
{ "ExtChargeIdling", "Extended Charge Idling", false },
{ "WirelessModem", "Wireless/Ender Comms Modem", true },
{ "WiredModem", "Wired Comms Modem", false },
{ "PLC_Listen", "PLC Listen Mode", types.LISTEN_MODE.WIRELESS },
{ "RTU_Listen", "RTU Gateway Listen Mode", types.LISTEN_MODE.WIRELESS },
{ "CRD_Listen", "Coordinator Listen Mode", types.LISTEN_MODE.WIRELESS },
{ "PocketEnabled", "Pocket Connectivity", true },
{ "PocketTest", "Pocket Testing Features", true },
{ "SVR_Channel", "SVR Channel", 16240 },
{ "PLC_Channel", "PLC Channel", 16241 },
{ "RTU_Channel", "RTU Channel", 16242 },
@@ -128,7 +151,7 @@ local fields = {
{ "CRD_Timeout", "CRD Connection Timeout", 5 },
{ "PKT_Timeout", "PKT Connection Timeout", 5 },
{ "TrustedRange", "Trusted Range", 0 },
{ "AuthKey", "Facility Auth Key" , ""},
{ "AuthKey", "Facility Auth Key" , "" },
{ "LogMode", "Log Mode", log.MODE.APPEND },
{ "LogPath", "Log Path", "/log.txt" },
{ "LogDebug", "Log Debug Messages", false },
@@ -283,11 +306,14 @@ function configurator.configure(ask_config)
tool_ctl.has_config = load_settings(ini_cfg)
-- these need to be initialized as they are used before being set
tmp_cfg.WiredModem = ini_cfg.WiredModem
tmp_cfg.FacilityTankMode = ini_cfg.FacilityTankMode
tmp_cfg.TankFluidTypes = { table.unpack(ini_cfg.TankFluidTypes) }
reset_term()
ppm.mount_all()
-- set overridden colors
for i = 1, #style.colors do
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
@@ -297,6 +323,8 @@ function configurator.configure(ask_config)
local display = DisplayBox{window=term.current(),fg_bg=style.root}
config_view(display)
tool_ctl.gen_modem_list()
while true do
local event, param1, param2, param3 = util.pull_event()
@@ -311,6 +339,14 @@ 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 == "peripheral_detach" then
---@diagnostic disable-next-line: discard-returns
ppm.handle_unmount(param1)
tool_ctl.gen_modem_list()
elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1)
tool_ctl.gen_modem_list()
end
if event == "terminate" then return end

View File

@@ -2,15 +2,12 @@
-- Data Bus - Central Communication Linking for Supervisor Front Panel
--
local psil = require("scada-common.psil")
local util = require("scada-common.util")
local const = require("scada-common.constants")
local psil = require("scada-common.psil")
local util = require("scada-common.util")
local pgi = require("supervisor.panel.pgi")
-- nominal RTT is ping (0ms to 10ms usually) + 150ms for SV main loop tick
local WARN_RTT = 300 -- 2x as long as expected w/ 0 ping
local HIGH_RTT = 500 -- 3.33x as long as expected w/ 0 ping
local databus = {}
-- databus PSIL
@@ -19,7 +16,7 @@ databus.ps = psil.create()
-- call to toggle heartbeat signal
function databus.heartbeat() databus.ps.toggle("heartbeat") end
-- transmit firmware versions across the bus
-- transmit firmware versions
---@param sv_v string supervisor version
---@param comms_v string comms version
function databus.tx_versions(sv_v, comms_v)
@@ -27,10 +24,16 @@ function databus.tx_versions(sv_v, comms_v)
databus.ps.publish("comms_version", comms_v)
end
-- transmit hardware status for modem connection state
-- transmit hardware status for the wired comms modem
---@param has_modem boolean
function databus.tx_hw_modem(has_modem)
databus.ps.publish("has_modem", has_modem)
function databus.tx_hw_wd_modem(has_modem)
databus.ps.publish("has_wd_modem", has_modem)
end
-- transmit hardware status for the wireless comms modem
---@param has_modem boolean
function databus.tx_hw_wl_modem(has_modem)
databus.ps.publish("has_wl_modem", has_modem)
end
-- transmit PLC firmware version and session connection state
@@ -59,9 +62,9 @@ end
function databus.tx_plc_rtt(reactor_id, rtt)
databus.ps.publish("plc_" .. reactor_id .. "_rtt", rtt)
if rtt > HIGH_RTT then
if rtt > const.HIGH_RTT then
databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.red)
elseif rtt > WARN_RTT then
elseif rtt > const.WARN_RTT then
databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.yellow_hc)
else
databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.green_hc)
@@ -90,9 +93,9 @@ end
function databus.tx_rtu_rtt(session_id, rtt)
databus.ps.publish("rtu_" .. session_id .. "_rtt", rtt)
if rtt > HIGH_RTT then
if rtt > const.HIGH_RTT then
databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.red)
elseif rtt > WARN_RTT then
elseif rtt > const.WARN_RTT then
databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.yellow_hc)
else
databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.green_hc)
@@ -129,9 +132,9 @@ end
function databus.tx_crd_rtt(rtt)
databus.ps.publish("crd_rtt", rtt)
if rtt > HIGH_RTT then
if rtt > const.HIGH_RTT then
databus.ps.publish("crd_rtt_color", colors.red)
elseif rtt > WARN_RTT then
elseif rtt > const.WARN_RTT then
databus.ps.publish("crd_rtt_color", colors.yellow_hc)
else
databus.ps.publish("crd_rtt_color", colors.green_hc)
@@ -160,20 +163,13 @@ end
function databus.tx_pdg_rtt(session_id, rtt)
databus.ps.publish("pdg_" .. session_id .. "_rtt", rtt)
if rtt > HIGH_RTT then
if rtt > const.HIGH_RTT then
databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.red)
elseif rtt > WARN_RTT then
elseif rtt > const.WARN_RTT then
databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.yellow_hc)
else
databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.green_hc)
end
end
-- link a function to receive data from the bus
---@param field string field name
---@param func function function to link
function databus.rx_field(field, func)
databus.ps.subscribe(field, func)
end
return databus

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
@@ -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,
@@ -53,7 +70,8 @@ function facility.new(config)
fac_tank_defs = config.FacilityTankDefs,
fac_tank_list = config.FacilityTankList,
fac_tank_conns = config.FacilityTankConns,
tank_fluid_types = config.TankFluidTypes
tank_fluid_types = config.TankFluidTypes,
aux_coolant = config.AuxiliaryCoolant
},
-- rtus
rtu_gw_conn_count = 0,
@@ -66,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
@@ -101,8 +122,8 @@ function facility.new(config)
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,
@@ -123,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
@@ -149,6 +182,70 @@ function facility.new(config)
table.insert(self.test_tone_states, false)
end
-- 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
@@ -239,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)
@@ -251,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
@@ -267,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
@@ -276,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
@@ -290,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")))
@@ -351,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

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
@@ -586,7 +645,7 @@ function update.auto_safety()
end
if (self.mode ~= PROCESS.INACTIVE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then
local scram = astatus.matrix_fault 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
@@ -642,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
@@ -676,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
@@ -716,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
@@ -792,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]
@@ -807,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)

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