Compare commits

...

468 Commits

Author SHA1 Message Date
Mikayla
8e19418701 Merge pull request #547 from MikaylaFischler/devel
2024.09.08 Release
2024-09-11 21:29:36 -04:00
Mikayla Fischler
fb56634fc4 Merge branch 'devel' of github.com:MikaylaFischler/cc-mek-scada into devel 2024-09-11 21:08:28 -04:00
Mikayla Fischler
48fa715aaa incremented util version 2024-09-11 21:08:15 -04:00
Mikayla
753f062bfc #403 doc spelling fix 2024-09-10 19:44:40 +00:00
Mikayla Fischler
356657c9c0 incremented API version 2024-09-08 17:17:30 -04:00
Mikayla
a4452ebbd2 Merge pull request #546 from MikaylaFischler/pocket-alpha-dev
Start of Pocket Controls
2024-09-08 16:54:22 -04:00
Mikayla Fischler
35134822a9 coordinator handle SPS low power ack 2024-09-08 16:49:23 -04:00
Mikayla Fischler
f56d68d972 removed unused iocontrol functions 2024-09-08 16:35:02 -04:00
Mikayla Fischler
06933b2fb7 removed more unused pocket code 2024-09-08 16:27:50 -04:00
Mikayla Fischler
a1494b4afd luacheck fix 2024-09-08 16:11:46 -04:00
Mikayla Fischler
2933b24318 cleanup 2024-09-08 16:05:20 -04:00
Mikayla Fischler
402d8607b6 added AUTO_GROUP enum 2024-09-08 13:26:43 -04:00
Mikayla Fischler
2e978db859 cleanup and version increments 2024-09-08 13:23:37 -04:00
Mikayla Fischler
a7b3a2a0b8 removed unused variables 2024-09-07 23:08:49 -04:00
Mikayla Fischler
6ff096fd31 #498 auto control mode based UI disabling and increased timeouts 2024-09-07 21:39:16 -04:00
Mikayla Fischler
13bb6cb026 #498 fixed wrong facility SCRAM ack 2024-09-07 01:11:13 -04:00
Mikayla Fischler
5b311fcfbc #498 pocket facility scram and ack all alarms 2024-09-06 23:31:01 -04:00
Mikayla Fischler
d6a9f9c5f3 #498 clear requestors on ack 2024-09-06 21:26:41 -04:00
Mikayla Fischler
8ffbbb5ac9 #498 supervisor block disallowed commands based on state, removed unused acks 2024-09-06 21:11:56 -04:00
Mikayla Fischler
bf10b3241e #403 RPS FP indicator doc updates 2024-09-05 22:19:24 -04:00
Mikayla Fischler
ab11ff03b5 #498 functioning pocket manual unit controls 2024-09-05 22:18:59 -04:00
Mikayla Fischler
66fae0695c #498 handle pocket manual unit commands 2024-09-05 22:01:58 -04:00
Mikayla Fischler
dbd79cbc4f #498 coordinator process handle system for manual controls 2024-09-05 21:49:47 -04:00
Mikayla
b5b67b425a #498 work on command acknowledgement handling 2024-09-04 21:12:43 +00:00
Mikayla Fischler
f8bd79a234 #498 work on command handling 2024-09-02 22:25:33 -04:00
Mikayla Fischler
07c3b3ec63 #403 improved guide UI and added supervisor front panel docs 2024-08-31 00:17:39 -04:00
Mikayla Fischler
d7ea68ed3a #403 reactor PLC docs 2024-08-29 22:49:20 -04:00
Mikayla Fischler
db94ac7ff5 #403 section headers and details on RTU front panel 2024-08-29 20:56:20 -04:00
Mikayla Fischler
ee922a3aed #403 fixed section focusing 2024-08-29 19:56:36 -04:00
Mikayla Fischler
75c77cc5b5 #403 weight exact matches over start of key matches 2024-08-28 23:02:08 -04:00
Mikayla Fischler
7683293c5e #403 additional guide section doc types and some more documentation 2024-08-28 22:52:55 -04:00
Mikayla Fischler
672a9c8dd1 Merge branch 'devel' into pocket-alpha-dev 2024-08-28 21:19:39 -04:00
Mikayla
3c10e28d03 #403 guide lists 2024-08-29 01:19:26 +00:00
Mikayla Fischler
035a26cc07 #543 reset remote sequence numbers when linking 2024-08-28 21:01:04 -04:00
Mikayla Fischler
097edc5bf9 adjusted guide section heights and moved process init to have facility access 2024-08-27 23:21:49 -04:00
Mikayla
8a0d05c94b #403 guide additions for front panel docs 2024-08-28 03:12:38 +00:00
Mikayla Fischler
fbbd7e1ccd WIP rearchitecting process command orchestration 2024-08-27 23:05:46 -04:00
Mikayla Fischler
0f40c1d7f2 removed unused set burn ack 2024-08-27 23:03:42 -04:00
Mikayla Fischler
c299dce8ef #498 work on pocket control app and support process code 2024-08-27 23:02:31 -04:00
Mikayla Fischler
11e9c11cf7 GitHub and Discord links in pocket guide 2024-08-27 23:00:29 -04:00
Mikayla Fischler
61ff055d60 allow right alignment for numeric inputs 2024-08-26 20:31:36 -04:00
Mikayla Fischler
f4be6519e8 refactoring and removed unused set_waste_ack 2024-08-26 20:30:30 -04:00
Mikayla
705494bb7e specify python version 2024-08-26 13:55:13 +00:00
Mikayla
610fb12bb3 actions dependency version updates 2024-08-26 13:52:47 +00:00
Mikayla
07406ca5fc Merge pull request #542 from MikaylaFischler/devel
2024.08.25 Release
2024-08-25 22:50:18 -04:00
Mikayla Fischler
6b20445446 added INF tab to supervisor to provide helpful info and removed some redundant alignment specifiers 2024-08-25 22:45:41 -04:00
Mikayla Fischler
f93db02793 incremented common version 2024-08-25 21:29:20 -04:00
Mikayla
fe1b916b1f Merge pull request #541 from MikaylaFischler/pocket-alpha-dev
display pocket connecting failure reasons
2024-08-25 20:41:12 -04:00
Mikayla Fischler
ebeeecc5ab luacheck fix 2024-08-25 20:40:30 -04:00
Mikayla Fischler
dbabcd13b0 luacheck fix 2024-08-25 20:39:21 -04:00
Mikayla Fischler
acc8e1c058 incremented graphics version and disabled listbox debug messages for now 2024-08-25 20:38:01 -04:00
Mikayla Fischler
5a38acf2a7 #540 display pocket connecting failure reasons 2024-08-25 20:29:52 -04:00
Mikayla Fischler
b3be2d4bfc #537 close sessions on receiving an ESTABLISH packet to allow for retries 2024-08-24 14:46:58 -04:00
Mikayla
0ab2d57b66 Merge pull request #538 from MikaylaFischler/367-list-duplicate-and-missing-device-ids
Supervisor Listing of Missing and Bad Device IDs
2024-08-24 14:11:46 -04:00
Mikayla
183af8a5ca #539 logging for investigations 2024-08-22 18:18:13 +00:00
Mikayla
6f63092d4b #367 check facility dynamic tank linking 2024-08-22 16:45:36 +00:00
Mikayla
a087eda0ee #367 RTU fail enum and logging messages 2024-08-22 16:42:57 +00:00
Mikayla Fischler
a1b6ff4bcc luacheck fixes 2024-08-21 19:18:55 -04:00
Mikayla Fischler
8c6b264f6b #367 simplified chk_entry 2024-08-21 19:15:12 -04:00
Mikayla Fischler
8a5c468606 #367 fixes and removed computer ID display 2024-08-21 18:53:52 -04:00
Mikayla
12f187f596 #367 logic for missing device detection and user-friendly messages 2024-08-21 21:23:16 +00:00
Mikayla
01a1c374ab Merge branch 'devel' into 367-list-duplicate-and-missing-device-ids 2024-08-21 13:56:50 +00:00
Mikayla Fischler
465875b287 coordinator receives tank list from supervisor 2024-08-20 22:28:41 -04:00
Mikayla Fischler
45d4b4e653 fixed PLC status retry packet type 2024-08-20 21:35:05 -04:00
Mikayla Fischler
fc7441b2f6 #367 reworked ownership of tank data and facility instance to make more sense 2024-08-20 21:32:54 -04:00
Mikayla Fischler
6917697290 #536 proper clearing of cleared config values 2024-08-20 20:56:41 -04:00
Mikayla
c323967b6a #536 fix for clearing settings 2024-08-20 20:52:38 +00:00
Mikayla Fischler
4775639245 #367 WIP listing ID check failures and missing devices 2024-08-18 23:04:44 -04:00
Mikayla Fischler
072613959c facility tank list generation on supervisor 2024-08-18 23:04:07 -04:00
Mikayla Fischler
f259f85a99 fixed wrong function name 2024-08-18 19:12:13 -04:00
Mikayla Fischler
e076e327d8 split up facility logic into two files 2024-08-18 19:10:43 -04:00
Mikayla
f34747372f #367 work on device ID check failure list 2024-08-16 21:19:25 +00:00
Mikayla
affe2d6c6d listbox debugging 2024-08-16 21:17:36 +00:00
Mikayla
5597ea2097 comment updates for clarity around RTU gateway vs RTU 2024-08-16 19:53:43 +00:00
Mikayla
0f4a8b6dfc refactoring and RTU gateway terminology cleanup 2024-08-16 18:17:03 +00:00
Mikayla
ab97f8935d #367 reject and record bad or duplicate RTU IDs 2024-08-16 18:08:53 +00:00
Mikayla
b0342654e7 added off-line installation to installation options 2024-08-12 09:55:19 -04:00
Mikayla Fischler
bee96ed12e #517 ccmsi print wrapping and other adjustments for pocket environment 2024-08-11 22:11:57 -04:00
Mikayla Fischler
50bd59781e #534 fixed PLC self-check UI problem 2024-08-11 20:21:26 -04:00
Mikayla Fischler
196e0b1daf #519 fixed issue with turbine stability evaluation 2024-08-11 19:58:29 -04:00
Mikayla
f725eb0eef Merge pull request #533 from MikaylaFischler/devel
2024.07.28 Release
2024-07-28 17:21:26 -04:00
Mikayla Fischler
bcc55628cf don't disable self-check even if there is no config 2024-07-28 16:41:39 -04:00
Mikayla Fischler
9bffd6feee incremented coordinator version 2024-07-27 21:28:37 -04:00
Mikayla
1500004481 Merge pull request #532 from MikaylaFischler/configurator-updates
Configurator Updates
2024-07-27 20:55:25 -04:00
Mikayla Fischler
2904621e81 fixed wrong disable format on self-check button 2024-07-27 20:55:00 -04:00
Mikayla Fischler
08eee198c8 cleanup and rewording notices 2024-07-27 20:35:09 -04:00
Mikayla Fischler
e750ffe69d updated element asserts for power indicator and incremented graphics version 2024-07-27 16:23:37 -04:00
Mikayla Fischler
de6d8a89ca avoid redundant calls to report_link_state 2024-07-27 16:23:19 -04:00
Mikayla Fischler
f00751edeb still display supervisor/coordinator address info if not linked to both 2024-07-27 13:17:56 -04:00
Mikayla Fischler
d58a6a3369 #531 pocket energy scale options 2024-07-27 12:51:46 -04:00
Mikayla Fischler
340c6689a9 #523 coordinator configurator updates 2024-07-27 12:35:26 -04:00
Mikayla Fischler
7cc088ca95 #523 coordinator energy scale options 2024-07-27 12:34:01 -04:00
Mikayla Fischler
01f6b1e190 #363 added tip about self-check 2024-07-27 11:15:23 -04:00
Mikayla Fischler
3ffc79b181 #530 fix RTU reconnection issue 2024-07-27 11:15:05 -04:00
Mikayla Fischler
8e4bb583a8 #528 reactor PLC configurator fixes 2024-07-26 23:06:42 -04:00
Mikayla
ec107929bc #528 reactor PLC configurator cleanup 2024-07-27 00:27:38 +00:00
Mikayla Fischler
3406d12681 #363 check config 2024-07-24 22:42:14 -04:00
Mikayla Fischler
03bbf8a891 updated coordinator configurator connection sequence number logic to match new system 2024-07-22 23:45:25 -04:00
Mikayla Fischler
b61867be3c updated RTU configurator change log 2024-07-22 23:44:56 -04:00
Mikayla Fischler
1358d95269 cc strings infinite loop mitigation 2024-07-22 23:44:34 -04:00
Mikayla Fischler
fd06730e46 #363 PLC configurator self check WIP 2024-07-22 23:44:12 -04:00
Mikayla
fb5f3b9474 #363 work on PLC self-check 2024-07-20 18:17:36 +00:00
Mikayla
3afc1e6cfa #512 rtu help text updates 2024-07-20 18:14:59 +00:00
Mikayla Fischler
715765d442 #512 increased clarity of peripheral assignments 2024-07-16 18:07:37 -04:00
Mikayla
3762e9dced #524 fix tank layout render reset 2024-07-16 21:03:52 +00:00
Mikayla Fischler
022d1f9f49 updated main's manifest workflow 2024-07-06 00:55:03 -04:00
Mikayla
b1da76c2f6 Merge pull request #516 from MikaylaFischler/devel
2024.07.05 Release
2024-07-06 00:51:28 -04:00
Mikayla Fischler
0364b4df7b fixed pocket crash due to guide section height too small 2024-07-06 00:07:21 -04:00
Mikayla Fischler
e04bd032fe incremented common version 2024-07-06 00:07:00 -04:00
Mikayla
34fe5dc382 Merge pull request #518 from MikaylaFischler/pocket-alpha-dev
Pocket alpha dev
2024-07-05 23:29:06 -04:00
Mikayla
1f8ea56095 Merge pull request #515 from MikaylaFischler/514-retry-file-downloads-on-failure
514 retry file downloads on failure
2024-07-05 13:39:20 -04:00
Mikayla Fischler
f2cd98c57a #194 fixes to log file handling, improved failure behavior, skip extra dialogs if nothing can be updated 2024-07-03 21:14:39 -04:00
Mikayla
1e341af8a5 #514 optimizations and fixes 2024-07-03 15:01:43 +00:00
Mikayla Fischler
2fb3d9b515 #514 cleaned up download logic and added retries 2024-07-02 22:07:12 -04:00
Mikayla
220f9d152f Merge branch 'devel' into pocket-alpha-dev 2024-07-01 16:36:36 +00:00
Mikayla
604b4a1927 Merge pull request #511 from MikaylaFischler/500-remove-height=1-from-textbox-elements
#500 removed now redundant height=1 from TextBox elements
2024-06-30 14:14:20 -04:00
Mikayla Fischler
0ecaa42a7f restored incorrectly modified height 2024-06-30 14:05:34 -04:00
Mikayla Fischler
9614407c37 #500 removed now redundant height=1 from TextBox elements 2024-06-30 13:55:13 -04:00
Mikayla Fischler
f1c4f8c00a keep main on old file path for now 2024-06-30 12:40:23 -04:00
Mikayla Fischler
8a409f0313 manifest build fix 2024-06-30 12:36:22 -04:00
Mikayla
da68398fa4 Merge pull request #510 from MikaylaFischler/506-single-file-off-line-installer
506 single file off line installer
2024-06-30 12:34:27 -04:00
Mikayla Fischler
375e969161 cleanup 2024-06-30 12:33:52 -04:00
Mikayla Fischler
c93cd4d0bd remove UTF-8 copyright symbol 2024-06-30 00:01:00 -04:00
Mikayla Fischler
e69cbc8633 luacheck fix 3 who could see this coming 2024-06-29 23:53:37 -04:00
Mikayla Fischler
ea1bcbf81c luacheck fix 2 2024-06-29 23:52:34 -04:00
Mikayla Fischler
f13f03fddc luacheck fix 2024-06-29 23:51:32 -04:00
Mikayla Fischler
72a480e475 luacheck the offline script but with an ignore 2024-06-29 23:49:33 -04:00
Mikayla Fischler
ec1b56b853 disable luacheck on offline script 2024-06-29 23:42:22 -04:00
Mikayla Fischler
c5fb299f55 message rewording, fixed colors on deletions 2024-06-29 23:38:51 -04:00
Mikayla Fischler
63c990a3cf #506 two-file bundled offline installer generation 2024-06-29 22:44:12 -04:00
Mikayla Fischler
89e84f9711 #194 ccmsi updates around log handling 2024-06-29 22:39:58 -04:00
Mikayla Fischler
270aeb13ca removed config.lua from luacheck 2024-06-29 22:38:11 -04:00
Mikayla Fischler
df8c71f12e #506 use minified files for off-line installer 2024-06-29 16:02:25 -04:00
Mikayla Fischler
347f67c8ee Merge branch '465-safe-lua-minifier' into 506-single-file-off-line-installer 2024-06-29 15:57:07 -04:00
Mikayla
8de2d7071e Merge pull request #509 from MikaylaFischler/465-safe-lua-minifier
465 safe lua minifier
2024-06-29 15:55:11 -04:00
Mikayla Fischler
d424cf74d3 Merge branch 'devel' into 506-single-file-off-line-installer 2024-06-29 15:53:27 -04:00
Mikayla Fischler
a1b571d7c0 copy over LICENSE to minified output directory 2024-06-29 15:52:47 -04:00
Mikayla Fischler
8e0e4df3eb rename package zip script 2024-06-29 15:52:34 -04:00
Mikayla Fischler
b025958173 comments in minifier 2024-06-29 15:41:04 -04:00
Mikayla Fischler
f83eecf2e2 script to package zips for installation without internet but with filesystem upload access 2024-06-29 15:28:16 -04:00
Mikayla Fischler
0b2f7b13a1 moved build scripts to new build directory 2024-06-29 15:14:43 -04:00
Mikayla Fischler
3ad3cbb4eb Merge branch 'devel' into 465-safe-lua-minifier 2024-06-29 15:11:55 -04:00
Mikayla Fischler
e868fd3397 cleanup of bootloader 2024-06-29 15:11:16 -04:00
Mikayla Fischler
a4add9370c RTU modem init consistency and cleanup 2024-06-29 15:08:11 -04:00
Mikayla
4f48ba8abc #403 work on guide docs 2024-06-29 18:59:39 +00:00
Mikayla
3cc6781844 Merge pull request #507 from MikaylaFischler/488-accelerate-hmac-computation
488 accelerate hmac computation
2024-06-29 14:52:15 -04:00
Mikayla Fischler
c05a45c29a more cleanup 2024-06-29 14:51:15 -04:00
Mikayla Fischler
f2937b47e9 cleanup 2024-06-29 14:49:26 -04:00
Mikayla
8e14fa1591 disable a diagnostic message in ccmsi 2024-06-29 18:30:32 +00:00
Mikayla
aebb9f42be #506 work on off-line installer generation script 2024-06-29 18:29:49 +00:00
Mikayla Fischler
2de30ef064 #488 fixes to sequence number changes and auth packet data 2024-06-29 14:10:58 -04:00
Mikayla Fischler
8dedb092e7 Merge branch 'devel' into 488-accelerate-hmac-computation 2024-06-29 12:51:52 -04:00
Mikayla
807e575580 Merge pull request #505 from MikaylaFischler/502-supervisor-crash-arithmetic-operation-on-nil-value
502 supervisor crash arithmetic operation on nil value
2024-06-29 12:48:28 -04:00
Mikayla Fischler
55dc203cdd increment reactor plc version to 1.8.0 2024-06-29 12:47:56 -04:00
Mikayla Fischler
a15cbadd32 #497 initial loading screen 2024-06-29 12:29:28 -04:00
Mikayla Fischler
bc76c01aa5 #504 fixed reactor idle status on pocket display 2024-06-29 12:18:55 -04:00
Mikayla
d2bc4f6bc0 #488 HMAC acceleration and seq_num changes 2024-06-29 02:27:55 +00:00
Mikayla Fischler
4cdbe3b07f some more cleanup 2024-06-27 21:05:53 -04:00
Mikayla Fischler
897a3ed22d #502 much needed refresh and cleanup of PLC struct and status packet handling 2024-06-27 21:03:53 -04:00
Mikayla Fischler
2bc20ec312 cleanup 2024-06-27 20:01:53 -04:00
Mikayla Fischler
fc42049aa0 removed deprecated high temp constant 2024-06-27 19:57:55 -04:00
Mikayla Fischler
4a7028f401 #497 instantly launch pocket program, block network dependent apps until connected 2024-06-27 19:57:43 -04:00
Mikayla
006c5e6adf Merge pull request #501 from MikaylaFischler/devel
2024.06.15 Release
2024-06-15 18:27:17 -04:00
Mikayla Fischler
f64db66448 comms version updates 2024-06-14 17:58:39 -04:00
Mikayla
a8e6bc0e35 Merge pull request #499 from MikaylaFischler/pocket-alpha-dev
Pocket June Update
2024-06-14 17:50:47 -04:00
Mikayla Fischler
4a39ed9d38 removed stray newline 2024-06-14 17:50:16 -04:00
Mikayla Fischler
9fe0669fda updated guide section heights and added a debug message to track height usage 2024-06-14 17:49:43 -04:00
Mikayla Fischler
219f02b188 print render crash cause to user 2024-06-14 17:42:03 -04:00
Mikayla Fischler
ea8f62dea6 #497 exit app if it is unloaded 2024-06-14 17:38:45 -04:00
Mikayla
1c719ad67b cleanup for pull request 2024-06-14 21:10:42 +00:00
Mikayla
87a91e309d #403 guide updates 2024-06-14 21:09:14 +00:00
Mikayla
c66ad44adb pocket cleanup 2024-06-14 16:32:25 +00:00
Mikayla
14736e414f luacheck ignore 2024-06-14 16:20:53 +00:00
Mikayla
fb1f85a626 possible luacheck suppression 2024-06-14 16:16:41 +00:00
Mikayla
697a3d6f6b luacheck fixes 2024-06-14 16:14:24 +00:00
Mikayla
00cacd6d0a #497 unload apps when required connections are lost 2024-06-14 16:10:04 +00:00
Mikayla Fischler
0b97d4d4b0 updated header message 2024-06-13 21:52:13 -04:00
Mikayla Fischler
def5b49327 #496 threaded app loading 2024-06-13 21:43:56 -04:00
Mikayla Fischler
38457cfbbc enforce pocket computer requirement 2024-06-13 20:34:39 -04:00
Mikayla
e851a5275f #496 pocket threading 2024-06-13 16:45:44 +00:00
Mikayla Fischler
5848c2ac1a test code for debugging 2024-06-12 20:22:41 -04:00
Mikayla Fischler
f8a5dd9c32 #200 additional fields for info display 2024-06-12 20:20:30 -04:00
Mikayla Fischler
ff8ae5e609 #200 updated status info display fields 2024-06-12 20:17:31 -04:00
Mikayla Fischler
95b93eb795 #403 nav up button sends back to prior app if open_help was used 2024-06-12 20:10:16 -04:00
Mikayla Fischler
356caf7b4d #403 guide searching 2024-06-12 20:01:31 -04:00
Mikayla Fischler
09c44a6969 multi-line push button and keep focus on keyboard select 2024-06-12 19:59:19 -04:00
Mikayla Fischler
83ba6e3961 #403 updated search navigation 2024-06-06 23:10:19 -04:00
Mikayla Fischler
e88e1afcc4 #403 started work on guide searching 2024-06-06 23:05:22 -04:00
Mikayla Fischler
b457edbc71 #403 variable sizing on listbox heights for sections 2024-06-06 22:54:26 -04:00
Mikayla
5e70e4131e #403 more work on guide and help linking 2024-06-07 02:30:55 +00:00
Mikayla Fischler
375b7f680e fixes to graphics constraint logic 2024-06-05 19:38:22 -04:00
Mikayla Fischler
e37b8758cd #403 guide fixes and focusing improvements 2024-06-05 19:38:02 -04:00
Mikayla
9404b50a8c #403 additional work on guide app 2024-06-05 22:07:38 +00:00
Mikayla
58fb35e85b keyboard and paste support for pocket 2024-06-05 00:31:47 +00:00
Mikayla
b9030d6bed #403 work on guide app 2024-06-05 00:31:29 +00:00
Mikayla
25ebf2c8c7 graphics automatic constraints 2024-06-05 00:31:06 +00:00
Mikayla Fischler
4d87887709 #403 pocket guide fixes 2024-06-03 20:52:59 -04:00
Mikayla
b63a17bda0 Merge branch 'pocket-alpha-dev' of https://github.com/MikaylaFischler/cc-mek-scada into pocket-alpha-dev 2024-06-04 00:24:41 +00:00
Mikayla
39233dae8a Merge branch 'pocket-alpha-dev' of https://github.com/MikaylaFischler/cc-mek-scada into pocket-alpha-dev 2024-06-04 00:22:06 +00:00
Mikayla
a2af0d3829 #403 work on pocket guide app 2024-06-04 00:21:54 +00:00
Mikayla Fischler
db901129f9 #200 status display updates 2024-06-02 22:00:42 -04:00
Mikayla Fischler
c1c49ea3fb #200 unit app updates 2024-06-02 16:06:32 -04:00
Mikayla Fischler
be6c3755a4 #207 pocket turbine view 2024-06-01 00:50:19 -04:00
Mikayla Fischler
ac2d189c1a reactor and boiler view fixes 2024-05-31 19:25:36 -04:00
Mikayla Fischler
0fa0324940 #206 pocket boiler view 2024-05-31 18:16:04 -04:00
Mikayla Fischler
3181ab96f1 #206 work on boiler view and reorganized app code 2024-05-29 22:08:36 -04:00
Mikayla Fischler
30c9215658 #202 pocket reactor view 2024-05-27 23:53:35 -04:00
Mikayla Fischler
946c28c929 record additional reactor unit data 2024-05-27 23:49:53 -04:00
Mikayla Fischler
e6d6353d05 added temperature units to pocket and to common types 2024-05-27 19:31:24 -04:00
Mikayla Fischler
0e81391144 #200 fixes to alarm/info display 2024-05-22 21:45:52 -04:00
Mikayla Fischler
b18cadb53e Merge branch 'devel' into pocket-alpha-dev 2024-05-22 18:46:23 -04:00
Mikayla Fischler
9a500d8f96 Merge branch 'pocket-alpha-dev' of github.com:MikaylaFischler/cc-mek-scada into pocket-alpha-dev 2024-05-22 18:15:18 -04:00
Mikayla
a268a770f2 #200 pocket alarm/status informational display, ECAM style 2024-05-22 21:55:59 +00:00
Mikayla
b96eb7d89d Merge pull request #492 from MikaylaFischler/devel
2024.05.16 Beta Hotfix
2024-05-16 22:13:01 -04:00
Mikayla Fischler
9b8947fba2 #491 fixed ps table indexing for boiler/turbine online 2024-05-16 22:06:53 -04:00
Mikayla
afc38e7e7a Merge pull request #490 from MikaylaFischler/devel
2024.05.15 Release
2024-05-15 17:22:18 -04:00
Mikayla Fischler
41b7a68f3e #487 stop retrying failed disable when needing to enable 2024-05-14 20:10:21 -04:00
Mikayla Fischler
8968ebede3 #486 fixed pcall messages for newer CC versions 2024-05-14 20:00:34 -04:00
Mikayla
50a9168b06 Merge pull request #489 from MikaylaFischler/pocket-alpha-dev
Pocket Update
2024-05-12 18:27:03 -04:00
Mikayla Fischler
76e85da9d5 version increments and small fix 2024-05-12 18:19:21 -04:00
Mikayla Fischler
be560cd532 luacheck fixes 2024-05-12 15:19:01 -04:00
Mikayla Fischler
2b0a536292 #200 pocket RCS overview 2024-05-12 15:14:58 -04:00
Mikayla Fischler
afed6f514d removed stray space in annunciator Coolant Level Low 2024-05-12 15:05:36 -04:00
Mikayla Fischler
3e6a0a8869 #200 updated RPS indicator text 2024-05-12 14:20:48 -04:00
Mikayla Fischler
b99cf19be0 #200 placeholder for alarm page, start of RCS page 2024-05-12 13:36:46 -04:00
Mikayla Fischler
7f009f9c86 #200 RPS view on pocket 2024-05-12 13:28:34 -04:00
Mikayla Fischler
6a8ed311f3 #200 functioning pocket unit overview 2024-05-11 19:19:52 -04:00
Mikayla Fischler
c181142f75 added network details to about app 2024-05-11 15:03:14 -04:00
Mikayla Fischler
0cb964a177 diagnostic disables 2024-05-10 19:18:21 -04:00
Mikayla Fischler
3cd832ca20 sidebar updates 2024-05-10 19:17:52 -04:00
Mikayla Fischler
0c6c7bf9a5 #200 work in progress unit views and sidebar/app updates 2024-05-09 23:05:55 -04:00
Mikayla Fischler
3bfcb1d83c #485 fixed assertion with height auto incrementing y when inheriting height 2024-05-04 13:50:53 -04:00
Mikayla
4fe6792804 Merge pull request #483 from MikaylaFischler/devel
2024.04.30 Release
2024-04-30 20:35:24 -04:00
Mikayla Fischler
25dc47d520 fixed recording bad stats on induction matrix faults 2024-04-30 20:28:07 -04:00
Mikayla Fischler
f958b0e3b7 fixed at max i/o indicator 2024-04-30 20:27:04 -04:00
Mikayla Fischler
f621ff2482 added some value inits and unit labels 2024-04-30 18:54:01 -04:00
Mikayla Fischler
eb45ff899b #455 calculate reactor temp high limit 2024-04-29 22:03:54 -04:00
Mikayla
e91fd2fcaa Merge pull request #482 from MikaylaFischler/412-additional-matrix-integrations
412 additional matrix integrations
2024-04-28 13:08:51 -04:00
Mikayla Fischler
d35b824458 luacheck fix and cleanup 2024-04-28 13:08:16 -04:00
Mikayla Fischler
165d1497f8 reverted test change that got committed 2024-04-28 02:01:40 -04:00
Mikayla Fischler
50bf057ca6 #412 optionally disable SPS at low power 2024-04-28 02:01:21 -04:00
Mikayla Fischler
6f768ef6b3 #469 made ETA tolerant to induction matrix capacity changes 2024-04-28 01:26:44 -04:00
Mikayla Fischler
826086951e return zero on mov_avg compute if no samples 2024-04-27 19:50:35 -04:00
Mikayla Fischler
35bf56663f #469 induction matrix charge ETAs and misc cleanup/updates 2024-04-27 16:27:01 -04:00
Mikayla
43b44cc425 #465 first pass at minifier 2024-04-24 21:11:41 +00:00
Mikayla Fischler
7b8cea4a5c Merge branch 'devel' into 412-additional-matrix-integrations 2024-04-21 13:57:47 -04:00
Mikayla Fischler
51d4a22532 #478 simplified reactor PLC reactor formed handling 2024-04-21 13:55:39 -04:00
Mikayla Fischler
fb85c2f05b RTU configurator updates for redstone I/O clarity 2024-04-21 13:54:14 -04:00
Mikayla Fischler
712d018806 Merge branch 'devel' into 412-additional-matrix-integrations 2024-04-21 12:09:53 -04:00
Mikayla Fischler
00a8d64a88 fixed coordinator not showing FAIL on unit count mismatch when connecting 2024-04-20 20:38:55 -04:00
Mikayla Fischler
d9efd5b8d2 #412 updates to RSIO for induction matrix low, high, and analog charge level 2024-04-20 16:32:18 -04:00
Mikayla Fischler
a786404092 #476 fixed PPM not initing fault counter in __index handler when a function is found 2024-04-16 15:55:40 -04:00
Mikayla
1bd03c0b1a Merge pull request #473 from MikaylaFischler/devel
2024.04.14 Release
2024-04-15 11:59:40 -04:00
Mikayla Fischler
0d8d7aeb15 readme updates 2024-04-15 10:20:48 -04:00
Mikayla Fischler
0b60dc9fa4 #474 run main reactor-plc loop clock when not networked 2024-04-14 19:24:12 -04:00
Mikayla Fischler
48b91ac808 fixed reactor-plc configurator auth key displaying *** when not networked 2024-04-14 19:08:19 -04:00
Mikayla Fischler
4c9efc78b1 bumped up scada-common version 2024-04-14 16:30:50 -04:00
Mikayla
3c5857cd68 Merge pull request #472 from MikaylaFischler/pocket-alpha-dev
Work on Pocket Alpha App
2024-04-14 16:17:14 -04:00
Mikayla Fischler
473b0dd10d fixes and comments 2024-04-14 16:16:18 -04:00
Mikayla Fischler
d2df7db18b luacheck fixes 2024-04-14 15:30:13 -04:00
Mikayla Fischler
0ca002316f work on pocket, bumped comms version for PR 2024-04-14 15:28:38 -04:00
Mikayla Fischler
9fe34648c2 added system information/about app 2024-04-13 16:18:27 -04:00
Mikayla Fischler
57dc20692b need to use a compact signal bar due to how edge extensions work 2024-04-13 16:15:20 -04:00
Mikayla Fischler
f22d699baf flipped app pane scroll direction 2024-04-13 16:14:38 -04:00
Mikayla Fischler
23b31e0049 #410 pocket nav overhaul 2024-04-13 14:47:20 -04:00
Mikayla Fischler
2b4309afa7 pocket coordinator linking fixes 2024-04-13 11:02:41 -04:00
Mikayla
89cd5a07f2 Merge branch 'pocket-alpha-dev' of https://github.com/MikaylaFischler/cc-mek-scada into pocket-alpha-dev 2024-04-13 00:42:01 +00:00
Mikayla
99213da760 #200 work on pocket comms for unit data 2024-04-13 00:41:47 +00:00
Mikayla Fischler
f23f69d441 Merge branch 'devel' into pocket-alpha-dev 2024-04-12 20:27:21 -04:00
Mikayla
a99b7912f8 Merge pull request #471 from MikaylaFischler/366-charge-control-mode-idling
366 charge control mode idling
2024-04-12 20:07:21 -04:00
Mikayla Fischler
23ca5fb69e clear flow monitor on coordinator ui close 2024-04-12 20:06:39 -04:00
Mikayla Fischler
c243d064ef fixed incorrect render behavior on quick supervisor reconnects 2024-04-12 00:18:35 -04:00
Mikayla Fischler
878c3b92e1 #366 added idling to config and adjusted/fixed some behaviors 2024-04-12 00:13:05 -04:00
Mikayla Fischler
2aa52d2e2c simplified a config validation assert 2024-04-11 20:30:54 -04:00
Mikayla Fischler
dfc1ee6497 cleanup and constants 2024-04-10 22:16:41 -04:00
Mikayla Fischler
a6a1a61954 #470 reworked flow stability logic 2024-04-10 21:30:51 -04:00
Mikayla Fischler
d0b50c834c fixed coordinator not exiting on failed connection 2024-04-10 20:49:14 -04:00
Mikayla Fischler
612a06ba98 use os.clock rather than unix time for control 2024-04-09 22:24:03 -04:00
Mikayla Fischler
65d43d55c7 #366 idling and charge level PD gain changes 2024-04-09 22:23:43 -04:00
Mikayla Fischler
4fcd375ee2 show average charge rather than current charge below charge target 2024-04-09 22:20:46 -04:00
Mikayla Fischler
a22f5562cf only ramp up on reactor plc 2024-04-09 22:19:53 -04:00
Mikayla
0365ea5e8a Merge branch 'pocket-alpha-dev' of https://github.com/MikaylaFischler/cc-mek-scada into pocket-alpha-dev 2024-04-09 14:27:50 +00:00
Mikayla
da300607f1 Merge pull request #468 from MikaylaFischler/460-create-coordinator-ui-thread
460 create coordinator UI thread
2024-04-09 00:10:44 -04:00
Mikayla Fischler
cc3174ee76 comments and whitespace 2024-04-09 00:08:47 -04:00
Mikayla Fischler
98c37caecd #460 cleanup 2024-04-08 23:59:16 -04:00
Mikayla Fischler
cc50e4a020 #460 removed coordinator connecting grace period 2024-04-07 22:33:48 -04:00
Mikayla Fischler
f734c4307b #460 exit on UI crash 2024-04-07 20:55:07 -04:00
Mikayla Fischler
45573be8c7 #460 renderer logging 2024-04-07 20:47:31 -04:00
Mikayla Fischler
eab1ffbe03 #460 cleanup and moved date and time update to be behind UI ready rather than linked 2024-04-07 20:24:27 -04:00
Mikayla Fischler
1504247b33 #460 added yields throughout main UI rendering 2024-04-07 20:21:57 -04:00
Mikayla Fischler
92a4277f73 #460 moved connect/disconnect/resize to render thread 2024-04-07 19:54:22 -04:00
Mikayla Fischler
31a663e86b #460 threaded coordinator and moved UI start to render thread 2024-04-07 19:37:06 -04:00
Mikayla Fischler
5c0e2c32ee #461 prevent log spam in standalone mode when RPS trips continuously 2024-04-07 18:04:11 -04:00
Mikayla
3537c59365 Merge pull request #466 from MikaylaFischler/devel
2024.03.31 Release
2024-03-31 18:53:18 -04:00
Mikayla Fischler
1d7104ae74 Merge branch 'devel' into pocket-alpha-dev 2024-03-31 13:35:23 -04:00
Mikayla Fischler
4629e1ba2a #464 fixed color options button not appearing disabled when disabled 2024-03-31 00:03:28 -04:00
Mikayla Fischler
659644865a #449 include disconnected configured monitors in monitor list 2024-03-30 12:46:01 -04:00
Mikayla Fischler
03d2b3f087 #452 enable emergency coolant on boiler water level low when reactor can't cool 2024-03-29 18:31:17 -04:00
Mikayla
bea7b91ff1 Merge pull request #459 from MikaylaFischler/457-colorblind-independent-color-accessibility-modifiers
457 colorblind independent color accessibility modifiers
2024-03-25 10:13:15 -04:00
Mikayla Fischler
b94c89f4ec #457 cleanup 2024-03-25 10:11:35 -04:00
Mikayla Fischler
55e4e5a68b #457 fix for indicator background 2024-03-24 23:05:52 -04:00
Mikayla Fischler
e1ad76a00d #457 fixes and adjusted text 2024-03-24 13:56:19 -04:00
Mikayla Fischler
93e4590947 #457 added standard with black off 2024-03-24 13:39:24 -04:00
Mikayla Fischler
2442e7f972 #457 added ind_bkg for front panels 2024-03-24 13:03:48 -04:00
Mikayla Fischler
bb2c07963b #457 work on blue indicator modes 2024-03-24 12:56:51 -04:00
Mikayla Fischler
44c6352a8c #458 changed induction matrix title text to be white in dark mode 2024-03-23 01:15:42 -04:00
Mikayla Fischler
968b0a9122 log environment versions when debug logs are enabled 2024-03-23 00:49:19 -04:00
Mikayla Fischler
19869416af #434 #454 PPM improvements and undefined function overhaul 2024-03-23 00:26:58 -04:00
Mikayla
598f6f08af Merge branch 'devel' into pocket-alpha-dev 2024-03-13 13:55:51 +00:00
Mikayla
ca2d8ab7da Merge pull request #450 from MikaylaFischler/devel
2024.03.12 Release
2024-03-12 21:15:24 -04:00
Mikayla Fischler
1c0f61b3e0 changed config defaults to use theme enums 2024-03-12 21:09:37 -04:00
Mikayla Fischler
ecd3575643 Merge branch 'color-update' into devel 2024-03-12 13:35:39 -04:00
Mikayla Fischler
9dc3a09f4d bumped up pocket version for configurator change 2024-03-12 13:35:15 -04:00
Mikayla
7a5d14d67f Merge pull request #448 from MikaylaFischler/color-update
Colorblind Modes
2024-03-12 13:25:36 -04:00
Mikayla Fischler
d6175e5cec switched to using LEDPair elements for colorblind mode network lights 2024-03-12 13:23:39 -04:00
Mikayla Fischler
a00a824a7f cleanup and fixes 2024-03-12 13:15:28 -04:00
Mikayla Fischler
9393632428 removed unused value specifiers 2024-03-12 12:54:31 -04:00
Mikayla Fischler
886bd0d5d5 luacheck fixes 2024-03-12 12:49:33 -04:00
Mikayla Fischler
9c1d83fdfc Merge branch 'devel' into color-update 2024-03-12 12:46:17 -04:00
Mikayla Fischler
bd88244681 #439 remind user to configure peripherals and redstone, and provide buttons to do so 2024-03-12 12:45:47 -04:00
Mikayla
8dae632b25 simplified checks for colorblind mode 2024-03-12 16:24:32 +00:00
Mikayla Fischler
89d56d3101 moved main UI palettes to themes.lua and set configurators to use it 2024-03-11 23:54:03 -04:00
Mikayla Fischler
1f451ff92a #340 coordinator colorblind support 2024-03-11 23:31:31 -04:00
Mikayla Fischler
9d08b51f84 #340 restored softer blue for deuteranopia/protanopia basalt theme 2024-03-11 21:43:11 -04:00
Mikayla Fischler
047ff5c203 Merge branch 'devel' into color-update 2024-03-11 21:26:23 -04:00
Mikayla Fischler
895f768e58 Merge branch 'color-update' of github.com:MikaylaFischler/cc-mek-scada into color-update 2024-03-11 21:25:55 -04:00
Mikayla Fischler
b3f29566ea #340 colorblind modes for rtu, reactor-plc, and supervisor 2024-03-11 21:25:34 -04:00
Mikayla
c0a5c8d504 Merge pull request #447 from MikaylaFischler/color-update
Dark Mode Themes
2024-03-11 12:38:22 -04:00
Mikayla
bbe7b52662 bugfixes and cleanup 2024-03-11 16:35:06 +00:00
Mikayla Fischler
d5b166dcc6 Merge branch 'devel' into color-update 2024-03-09 18:51:21 -05:00
Mikayla
b0aa6d54ac Merge pull request #446 from MikaylaFischler/devel
2024.03.09 Beta Hotfix
2024-03-09 18:14:02 -05:00
Mikayla Fischler
ad240ae44c #445 increment common version 2024-03-09 17:54:04 -05:00
Mikayla Fischler
79c93f1562 #445 fixed PPM undefined field logic and improved RTU unit fault handling 2024-03-09 13:24:06 -05:00
Mikayla Fischler
5d760a0524 #405 helper functions, enums, and name tables added to themes.lua 2024-03-09 12:41:45 -05:00
Mikayla Fischler
6c89b3134c #405 make pu fallback selector visible 2024-03-09 12:39:37 -05:00
Mikayla
3c7fff28c9 Merge pull request #443 from MikaylaFischler/devel
2024.03.08 Release
2024-03-08 22:40:28 -05:00
Mikayla Fischler
814043bf04 #405 basalt theme color adjustments 2024-03-07 20:46:10 -05:00
Mikayla Fischler
fc7896ebd3 #405 #340 rtu and supervisor configurator control of theme and color mode 2024-03-07 19:23:46 -05:00
Mikayla Fischler
48a8eadc55 #405 #340 reactor plc configurator control of theme and color mode 2024-03-07 18:00:33 -05:00
Mikayla
f0f2aadf53 #200 work on pocket comms 2024-03-07 17:27:25 +00:00
Mikayla
ce37a672a3 Merge branch 'devel' into pocket-alpha-dev 2024-03-07 16:21:37 +00:00
Mikayla Fischler
510995b04f #405 #340 coordinator configurator control of theme and color mode 2024-03-06 23:35:30 -05:00
Mikayla Fischler
560061d4ad removed latest shield and added missing common version shield 2024-03-06 20:51:23 -05:00
Mikayla Fischler
d87e3893f0 #405 plc and rtu front panel themes 2024-03-06 12:18:50 -05:00
Mikayla Fischler
fc198cd9d2 #405 supervisor and coordinator front panel themes 2024-03-06 11:43:31 -05:00
Mikayla Fischler
c714e49ad8 Merge branch 'devel' into color-update 2024-03-05 22:03:24 -05:00
Mikayla Fischler
d1e4ea586e supervisor comment cleanup 2024-03-05 21:47:14 -05:00
Mikayla Fischler
4e789ab92d cleanup and refactors 2024-03-05 21:24:17 -05:00
Mikayla Fischler
0892a57d35 #442 return rather than assert on configuration error 2024-03-05 20:17:52 -05:00
Mikayla Fischler
1bc4828010 #438 use reported polonium rate rather than an estimate 2024-03-05 19:35:54 -05:00
Mikayla Fischler
fb5a9d5d9e #432 fixes and enhancements to coordinator waiting on chunk loads 2024-03-05 17:16:35 -05:00
Mikayla Fischler
adbf1f2f78 #441 #431 bugfixes to the bugfixes 2024-03-05 17:12:12 -05:00
Mikayla Fischler
2f99aaeedb #431 handle ppm mount of unformed reactor race condition 2024-03-05 10:56:27 -05:00
Mikayla Fischler
a318ffb283 #405 styling improvements to PLC front panel 2024-03-05 10:51:18 -05:00
Mikayla Fischler
a677e994d6 Merge branch 'devel' into color-update 2024-02-26 14:44:47 -05:00
Mikayla Fischler
f9917b786c #432 wait 20s on computer power on before assuming monitor configuration problem 2024-02-25 18:02:13 -05:00
Mikayla Fischler
dbc1f41c5d #406 bounds check all controls 2024-02-25 15:53:14 -05:00
Mikayla Fischler
d6185e0183 #433 use os.clock instead of util.time_s for coordinator connection timeout to supervisor 2024-02-25 13:13:36 -05:00
Mikayla Fischler
6ef049baa1 #405 reverted yellow, desaturated orange 2024-02-25 13:09:30 -05:00
Mikayla Fischler
a4214e8a4f #405 fixed waste line still always being black and removed colormap test 2024-02-24 19:34:43 -05:00
Mikayla Fischler
45881067df fixed extra space in RCS indicator list 2024-02-24 18:01:30 -05:00
Mikayla Fischler
c40aa229bf #405 flow monitor theme implementation 2024-02-24 17:35:10 -05:00
Mikayla Fischler
51f2bba4d1 #405 theme implementation for unit displays 2024-02-24 15:37:39 -05:00
Mikayla Fischler
628a50e1bd #405 WIP themes and completed main display theme implementation 2024-02-24 14:35:04 -05:00
Mikayla Fischler
cdd31508d9 #430 fixed unit boiler, turbine, and tank status indicators flashing OFF-LINE when online 2024-02-22 21:35:08 -05:00
Mikayla
83d62991f8 Merge pull request #429 from MikaylaFischler/328-coordinator-temperature-unit-options
328 coordinator temperature unit options
2024-02-22 20:50:01 -05:00
Mikayla Fischler
f207a950e4 fixed bug with hmac still being used for connecting in coordinator configurator after clearing key 2024-02-22 19:25:55 -05:00
Mikayla Fischler
0b0051dc2f #328 K, C, F, and R temperature unit options 2024-02-22 19:25:16 -05:00
Mikayla Fischler
f152c37ea9 #387 handle resizing, improved reconnect handling, fixed disconnect detection bug 2024-02-21 20:33:07 -05:00
Mikayla Fischler
372fd426d8 test code for psil allocations 2024-02-21 18:48:55 -05:00
Mikayla
8347afb6d0 Merge pull request #420 from MikaylaFischler/devel
2024.02.19 Release
2024-02-21 13:12:51 -05:00
Mikayla Fischler
10d0a9763a disabled a verbose log message 2024-02-21 12:59:48 -05:00
Mikayla Fischler
96691d773a supervisor debug log messages and #427 fix 2024-02-21 12:58:49 -05:00
Mikayla Fischler
910509d764 coordinator configurator bugfixes 2024-02-20 19:33:14 -05:00
Mikayla Fischler
158cc39b80 #421 remove 'latest' branch 2024-02-19 20:40:05 -05:00
Mikayla
940f71aa35 Merge pull request #422 from MikaylaFischler/145-graphical-configure-utilities
Pocket Configurator and Other Fixes
2024-02-19 20:34:41 -05:00
Mikayla Fischler
788cef8f86 comment cleanup and absolute paths while saving 2024-02-19 20:32:33 -05:00
Mikayla Fischler
baaef862ab #145 coordinator configurator enhancements 2024-02-19 20:26:05 -05:00
Mikayla Fischler
96db709ced ccmsi print fix 2024-02-19 20:24:30 -05:00
Mikayla Fischler
c47fa5433f #408 improvements to pocket configurator 2024-02-19 19:36:27 -05:00
Mikayla Fischler
440b724798 #424 tick comms version up 2024-02-19 19:33:08 -05:00
Mikayla Fischler
126d6eb163 #424 fixed key derivation init 2024-02-19 19:28:12 -05:00
Mikayla Fischler
8ac46faf36 set text scales before checking monitor dimensions 2024-02-19 18:56:24 -05:00
Mikayla Fischler
f112746e12 #408 increment bootloader version for pocket configurator, minification 2024-02-19 14:27:02 -05:00
Mikayla Fischler
76f21e925b #145 removed unneeded references to config.lua files + some minification 2024-02-19 14:18:23 -05:00
Mikayla Fischler
a330249c7e #408 integrate new settings with pocket 2024-02-19 14:07:26 -05:00
Mikayla Fischler
bbc64c8dc2 #145 fixed oversized listboxes 2024-02-19 13:54:23 -05:00
Mikayla Fischler
bb062cf397 #408 added pocket configure to configure launcher 2024-02-19 13:50:38 -05:00
Mikayla Fischler
53bb36ce8d #145 fixed change log page on coordinator 2024-02-19 13:50:03 -05:00
Mikayla Fischler
8237113577 #408 pocket configurator 2024-02-19 13:49:50 -05:00
Mikayla Fischler
02906ae707 add FUNDING.yml 2024-02-19 12:49:26 -05:00
Mikayla Fischler
6d0e777e68 updated copyright and removed coordinator from list mentioning config.lua 2024-02-18 21:40:25 -05:00
Mikayla
36468c4043 Merge pull request #419 from MikaylaFischler/145-graphical-configure-utilities
Coordinator Configurator
2024-02-18 21:36:50 -05:00
Mikayla Fischler
1cf7375311 #309 cleanup 2024-02-18 21:34:25 -05:00
Mikayla Fischler
ca55948286 #309 remove legacy config.lua 2024-02-18 21:25:21 -05:00
Mikayla Fischler
195f59178f #309 bugfix to apisessions still using old config 2024-02-18 21:24:30 -05:00
Mikayla Fischler
e416faf313 #309 integrated process control with new settings file 2024-02-18 20:47:37 -05:00
Mikayla Fischler
20bff48cfd #309 cleanup, fixes, optimizations 2024-02-18 20:21:07 -05:00
Mikayla Fischler
1a9892b291 cleanup and optimizations 2024-02-18 16:49:39 -05:00
Mikayla Fischler
827953c0a1 more luacheck fixes 2024-02-18 15:31:45 -05:00
Mikayla Fischler
56e69e3a29 luacheck fixes 2024-02-18 15:30:18 -05:00
Mikayla Fischler
1984b63837 additional config validations 2024-02-18 15:25:30 -05:00
Mikayla Fischler
36b12d5dea #309 integrated new configuration into coordinator 2024-02-18 15:21:00 -05:00
Mikayla Fischler
3e83c8e2c6 #309 moved monitor block size to ppm and fixed size estimation for monitor requirements 2024-02-18 13:00:18 -05:00
Mikayla Fischler
1fa8c03dff #309 import legacy configs 2024-02-18 00:56:36 -05:00
Mikayla Fischler
d6de9c266b #309 viewing and saving coordinator config 2024-02-17 22:39:50 -05:00
Mikayla Fischler
2142c1b4f7 updated license year 2024-02-17 18:39:02 -05:00
Mikayla Fischler
cafba6c67a #309 legacy options and general improvements 2024-02-17 18:38:36 -05:00
Mikayla Fischler
6eccebbe39 #409 fixed positioning 2024-02-17 18:16:21 -05:00
Mikayla Fischler
5e9f03c900 #418 fixed graphics bug with redraw 2024-02-17 18:12:28 -05:00
Mikayla Fischler
7374bb02d1 #309 speaker and time format configuration 2024-02-14 14:41:34 -05:00
Mikayla Fischler
d19794ae4f #309 unassign unused monitors on unit count reduction and support autofilling fields when editing existing monitor configs 2024-02-14 10:24:27 -05:00
Mikayla Fischler
108cf75cad #309 coordinator monitor configuration 2024-02-14 09:43:30 -05:00
Mikayla Fischler
34cbb6be39 bugfixes 2024-02-03 01:48:56 -05:00
Mikayla
907f27baf8 #309 show data received from supervisor 2024-02-02 23:01:51 +00:00
Mikayla Fischler
4710fa7cee #309 WIP coordinator configurator 2024-01-31 14:10:03 -05:00
Mikayla Fischler
526a54903e #410 moved diagnostic apps to main app list and added app page nav 2024-01-15 17:40:43 -05:00
Mikayla Fischler
e1a632dcc7 Merge branch 'devel' into pocket-alpha-dev 2024-01-14 14:21:09 -05:00
Mikayla Fischler
d5d818e625 pocket signal bars 2024-01-14 14:20:59 -05:00
Mikayla
bfa1f4d0c6 Merge pull request #404 from MikaylaFischler/devel
2023.12.31 Release
2023-12-31 21:34:40 -05:00
Mikayla Fischler
737e0d72b0 changed supervisor facility config color theme as green is for the summary already 2023-12-31 15:41:28 -05:00
Mikayla Fischler
6edeb3e3b8 add default value to sounder volume for very old RTU config imports 2023-12-31 15:00:38 -05:00
Mikayla Fischler
fb00e98a5b more supervisor configurator bugfixes 2023-12-31 15:00:08 -05:00
Mikayla Fischler
4f952eff83 fixed supervisor incorrectly trying to validate tank defs when tank mode is zero 2023-12-31 14:14:35 -05:00
Mikayla Fischler
1eede97c08 fixed supervisor not using proper config on front panel 2023-12-31 14:04:22 -05:00
Mikayla Fischler
95419562ee no longer mention config.lua for supervisor update 2023-12-31 13:17:49 -05:00
Mikayla Fischler
03de90c3d8 fixes to page navigation nav_up 2023-12-31 12:58:24 -05:00
Mikayla Fischler
99096e0fc9 Merge branch 'devel' into pocket-alpha-dev 2023-12-30 22:58:53 -05:00
Mikayla Fischler
7b85d947c4 fixed supervisor always using MAC 2023-12-30 22:57:30 -05:00
Mikayla Fischler
08e670091a #396 fixed fractional connection timeouts being treated as invalid 2023-12-30 20:25:57 -05:00
Mikayla
1348b632a8 Merge pull request #397 from MikaylaFischler/145-graphical-configure-utilities
Supervisor Configurator
2023-12-30 20:19:34 -05:00
Mikayla Fischler
8cd5162362 fixed PLCs not connecting, fixed facility tank mode checkbox not changing after import, and reordered info on tank mode vis about page 2023-12-30 20:18:58 -05:00
Mikayla Fischler
1c410a89d8 incremented comms version due to data change 2023-12-30 19:40:53 -05:00
Mikayla Fischler
6a931fced4 cleanup and fixes 2023-12-30 19:21:44 -05:00
Mikayla Fischler
622e2eeb90 more useful messages, incremented bootloader version 2023-12-30 14:51:25 -05:00
Mikayla Fischler
42cd9fff0c improved number field precision handling and limited decimal precision of timeouts #396 2023-12-30 14:41:03 -05:00
Mikayla Fischler
2a85a438ba #396 connection timeouts can now have a fractional part 2023-12-29 14:33:22 -05:00
Mikayla Fischler
338b3b1615 addressed luacheck warning 2023-12-29 14:29:46 -05:00
Mikayla Fischler
363f164f47 #308 deleted old config.lua 2023-12-29 14:12:54 -05:00
Mikayla Fischler
739f04ece9 #308 integrated new settings file with supervisor 2023-12-29 13:58:28 -05:00
Mikayla Fischler
c6ade68ce2 #308 importing legacy config 2023-12-29 12:40:48 -05:00
Mikayla Fischler
7d60e259e2 #308 supervisor configurator bugfixes and saving of settings 2023-12-29 01:07:50 -05:00
Mikayla Fischler
cd71c6a9c1 #308 summary display of supervisor config 2023-12-29 00:19:17 -05:00
Mikayla Fischler
26fe130609 #308 supervisor configurator completed facility tank mode and network config pages 2023-12-28 15:06:30 -05:00
Mikayla Fischler
95f87b1b05 #308 significantly improved facility dynamic tank configuration visualization 2023-12-26 13:13:05 -05:00
Mikayla Fischler
aebdf3e8df fixed include ordering 2023-12-26 13:11:46 -05:00
Mikayla Fischler
0395aa95b6 indicate pocket app is a work in progress 2023-12-22 22:30:22 -05:00
Mikayla Fischler
c624baed2c #395 fixes to new pocket navigation 2023-12-22 12:46:36 -05:00
Mikayla
1a40321c0f #395 pocket navigation system 2023-12-22 16:12:47 +00:00
Mikayla Fischler
5d4fc36256 #308 WIP supervisor configurator 2023-12-18 15:23:51 -05:00
Mikayla
b799d785b9 Merge pull request #394 from MikaylaFischler/devel
2023.12.17 Release
2023-12-17 21:02:32 -05:00
Mikayla
d55442fa53 Merge pull request #393 from MikaylaFischler/145-graphical-configure-utilities
Bring in changes from 145 branch to devel for release
2023-12-17 20:51:05 -05:00
Mikayla Fischler
c870b749a4 cleanup 2023-12-17 20:48:02 -05:00
Mikayla Fischler
4421cbc0c5 fixed input/output side text being sometimes wrong on rtu configurator redstone editing 2023-12-17 20:47:17 -05:00
Mikayla Fischler
b6a3305f23 minor minification 2023-12-17 20:43:40 -05:00
Mikayla Fischler
5680260136 use existing is_valid_port rather than repeating the code 2023-12-17 20:43:08 -05:00
Mikayla Fischler
bc66ea6ecb #381 fixed plc main thread crash on modem connect after boot with no modem 2023-12-17 20:28:26 -05:00
Mikayla Fischler
1b20218445 #194 #382 ccmsi no longer deletes drive mounts and now prompts to delete unknown files/folders in root 2023-12-17 20:10:11 -05:00
Mikayla Fischler
466e442353 #389 added width to RTU front panel entry name box 2023-12-17 19:39:00 -05:00
Mikayla Fischler
9e6751f47f #391 fixed editing of redstone entries 2023-12-17 19:32:01 -05:00
Mikayla Fischler
f868923905 #392 fixed typo preventing water level low indicator from working 2023-12-17 18:04:09 -05:00
Mikayla Fischler
5d3fd6d939 #390 fixed not being able to edit entries after using ALL_WASTE shortcut 2023-12-17 17:46:18 -05:00
Mikayla Fischler
37659d687e #388 fixed peripherals list not updating on add/delete of config entry 2023-12-17 17:22:29 -05:00
Mikayla Fischler
f23b7e2c2f fixed out of bounds coordinates crashing GUI for form fields 2023-12-17 12:56:08 -05:00
Mikayla Fischler
55ccdd63d4 don't mention config.lua on update for apps that don't have it 2023-12-17 12:55:00 -05:00
Mikayla Fischler
5c88890ed4 removed redundant min_width values 2023-12-14 20:51:54 -05:00
Mikayla Fischler
fa0185c9a4 fixed checkbox width 2023-12-13 12:20:12 -05:00
Mikayla Fischler
e1ed9a8e5e fixed error messages not fitting and say input side when configuring inputs on RTU configurator 2023-11-29 22:25:34 -05:00
154 changed files with 16612 additions and 5602 deletions

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

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

View File

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

View File

@@ -6,7 +6,6 @@ on:
push:
branches:
- main
- latest
- devel
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
@@ -30,54 +29,41 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup Pages
uses: actions/configure-pages@v3
uses: actions/configure-pages@v5
- name: Setup Python
uses: actions/setup-python@v3.1.3
uses: actions/setup-python@v5
with:
python-version: '3.10'
# Generate manifest + shields files for main branch
- name: Checkout main
id: checkout-main
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: 'main'
clean: false
- name: Create outputs folders
if: success() || failure()
shell: bash
run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/latest deploy/manifests/devel
run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/devel
- name: Generate manifest and shields for main branch
id: manifest-main
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
run: python imgen.py shields
run: python build/imgen.py shields
- name: Save main's manifest
if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/main
# Generate manifest for latest branch
- name: Checkout latest
id: checkout-latest
if: success() || failure()
uses: actions/checkout@v3
with:
ref: 'latest'
clean: false
- name: Generate manifest for latest
id: manifest-latest
if: ${{ (success() || failure()) && steps.checkout-latest.outcome == 'success' }}
run: python imgen.py
- name: Save latest's manifest
if: ${{ (success() || failure()) && steps.manifest-latest.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/latest
# Generate manifest for devel branch
- name: Checkout devel
id: checkout-devel
if: success() || failure()
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: 'devel'
clean: false
- name: Generate manifest for devel
id: manifest-devel
if: ${{ (success() || failure()) && steps.checkout-devel.outcome == 'success' }}
run: python imgen.py
run: python build/imgen.py
- name: Save devel's manifest
if: ${{ (success() || failure()) && steps.manifest-devel.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/devel
@@ -85,11 +71,11 @@ jobs:
- name: Upload artifacts
id: upload-artifacts
if: ${{ (success() || failure()) && (steps.manifest-main.outcome == 'success' || steps.manifest-latest.outcome == 'success' || steps.manifest-devel.outcome == 'success') }}
uses: actions/upload-pages-artifact@v1
uses: actions/upload-pages-artifact@v3
with:
# Upload manifest JSON
path: 'deploy/'
- name: Deploy to GitHub Pages
if: ${{ (success() || failure()) && steps.upload-artifacts.outcome == 'success' }}
id: deployment
uses: actions/deploy-pages@v2
uses: actions/deploy-pages@v4

2
.gitignore vendored
View File

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

View File

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

View File

@@ -4,12 +4,11 @@ Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fi
![GitHub](https://img.shields.io/github/license/MikaylaFischler/cc-mek-scada)
![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/MikaylaFischler/cc-mek-scada?include_prereleases)
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=main&label=main)
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=latest&label=latest)
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=devel&label=devel)
### [Join](https://discord.gg/R9NSCkhcwt) the Discord!
### Join [the Discord](https://discord.gg/R9NSCkhcwt)!
![Discord](https://img.shields.io/discord/1129075839288496259)
![Discord](https://img.shields.io/discord/1129075839288496259?logo=Discord&logoColor=white&label=discord)
## Released Component Versions
@@ -17,6 +16,7 @@ Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fi
![Bootloader](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fbootloader.json)
![Comms](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fcommon.json)
![Comms](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fcomms.json)
![Graphics](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fgraphics.json)
![Lockbox](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Flockbox.json)
@@ -45,6 +45,13 @@ v10.1+ is required due to the complete support of CC:Tweaked added in Mekanism v
You can install this on a ComputerCraft computer using either:
* `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua`
* `pastebin get sqUN6VUb ccmsi.lua`
* Off-line (when HTTP is disabled) installation via [release bundles](https://github.com/MikaylaFischler/cc-mek-scada/wiki/Alternative-Installation-Strategies#release-bundles)
## Contributing
Please reach out to me via Discord or email (or GitHub in some way) if you are thinking of making any contributions at this time. I started this project as a challenge for myself and have been enjoying having something I can work on in my own way.
Once this is out of beta I will be more open to contributions, but for now I am hoping to keep them to a minimum as the remaining challenges are ones I am looking forward to solving.
## [SCADA](https://en.wikipedia.org/wiki/SCADA)
> Supervisory control and data acquisition (SCADA) is a control system architecture comprising computers, networked data communications and graphical user interfaces for high-level supervision of machines and processes. It also covers sensors and other devices, such as programmable logic controllers, which interface with process plant or machinery.

118
build/_offline.lua Normal file
View File

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

214
build/bundle.py Normal file
View File

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

237
build/ccmsim.lua Normal file
View File

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

14
build/package.sh Executable file
View File

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

80
build/safemin.py Normal file
View File

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

317
ccmsi.lua
View File

@@ -1,7 +1,7 @@
--[[
CC-MEK-SCADA Installer Utility
Copyright (c) 2023 Mikayla Fischler
Copyright (c) 2023 - 2024 Mikayla Fischler
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
@@ -15,18 +15,66 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]--
local function println(message) print(tostring(message)) end
local function print(message) term.write(tostring(message)) end
local CCMSI_VERSION = "v1.11c"
local CCMSI_VERSION = "v1.17"
local install_dir = "/.install-cache"
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/"
---@diagnostic disable-next-line: undefined-global
local _is_pkt_env = pocket -- luacheck: ignore pocket
local function println(msg) print(tostring(msg)) end
-- stripped down & modified copy of log.dmesg
local function print(msg)
msg = tostring(msg)
local cur_x, cur_y = term.getCursorPos()
local out_w, out_h = term.getSize()
-- jump to next line if needed
if cur_x == out_w then
cur_x = 1
if cur_y == out_h then
term.scroll(1)
term.setCursorPos(1, cur_y)
else
term.setCursorPos(1, cur_y + 1)
end
end
-- wrap
local lines, remaining, s_start, s_end, ln = {}, true, 1, out_w + 1 - cur_x, 1
while remaining do
local line = string.sub(msg, s_start, s_end)
if line == "" then
remaining = false
else
lines[ln] = line
s_start = s_end + 1
s_end = s_end + out_w
ln = ln + 1
end
end
-- print
for i = 1, #lines do
cur_x, cur_y = term.getCursorPos()
if i > 1 and cur_x > 1 then
if cur_y == out_h then
term.scroll(1)
term.setCursorPos(1, cur_y)
else term.setCursorPos(1, cur_y + 1) end
end
term.write(lines[i])
end
end
local opts = { ... }
local mode, app, target
local install_manifest = manifest_path .. "main/install_manifest.json"
local install_manifest = manifest_path.."main/install_manifest.json"
local function red() term.setTextColor(colors.red) end
local function orange() term.setTextColor(colors.orange) end
@@ -59,17 +107,17 @@ local function ask_y_n(question, default)
end
-- print out a white + blue text message
local function pkg_message(message, package) white();print(message .. " ");blue();println(package);white() end
local function pkg_message(message, package) white();print(message.." ");blue();println(package);white() end
-- indicate actions to be taken based on package differences for installs/updates
local function show_pkg_change(name, v)
if v.v_local ~= nil then
if v.v_local ~= v.v_remote then
print("[" .. name .. "] updating ");blue();print(v.v_local);white();print(" \xbb ");blue();println(v.v_remote);white()
print("["..name.."] updating ");blue();print(v.v_local);white();print(" \xbb ");blue();println(v.v_remote);white()
elseif mode == "install" then
pkg_message("[" .. name .. "] reinstalling", v.v_local)
pkg_message("["..name.."] reinstalling", v.v_local)
end
else pkg_message("[" .. name .. "] new install of", v.v_remote) end
else pkg_message("["..name.."] new install of", v.v_remote) end
return v.v_local ~= v.v_remote
end
@@ -90,7 +138,7 @@ local function get_remote_manifest()
local response, error = http.get(install_manifest)
if response == nil then
orange();println("Failed to get installation manifest from GitHub, cannot update or install.")
red();println("HTTP error: " .. error);white()
red();println("HTTP error: "..error);white()
return false, {}
end
@@ -120,8 +168,29 @@ local function write_install_manifest(manifest, dependencies)
imfile.close()
end
-- try at most 3 times to download a file from the repository and write into w_path base directory
local function http_get_file(file, w_path)
local dl, err
for i = 1, 3 do
dl, err = http.get(repo_path..file)
if dl then
if i > 1 then green();println("success!");lgray() end
local f = fs.open(w_path..file, "w")
f.write(dl.readAll())
f.close()
break
else
red();println("HTTP Error: "..err)
if i < 3 then lgray();print("> retrying...") end
---@diagnostic disable-next-line: undefined-field
os.sleep(i/3.0)
end
end
return dl ~= nil
end
-- recursively build a tree out of the file manifest
local function gen_tree(manifest)
local function gen_tree(manifest, log)
local function _tree_add(tree, split)
if #split > 1 then
local name = table.remove(split, 1)
@@ -131,13 +200,14 @@ local function gen_tree(manifest)
return nil
end
local list, tree = {}, {}
local list, tree = { log }, {}
-- make a list of each and every file
for _, files in pairs(manifest.files) do for i = 1, #files do table.insert(list, files[i]) end end
for i = 1, #list do
local split = {}
---@diagnostic disable-next-line: discard-returns
string.gsub(list[i], "([^/]+)", function(c) split[#split + 1] = c end)
if #split == 1 then table.insert(tree, list[i])
else table.insert(tree, _tree_add(tree, split)) end
@@ -155,57 +225,77 @@ local function _clean_dir(dir, tree)
if tree == nil then tree = {} end
local ls = fs.list(dir)
for _, val in pairs(ls) do
local path = dir .. "/" .. val
local path = dir.."/"..val
if fs.isDir(path) then
_clean_dir(path, tree[val])
if #fs.list(path) == 0 then fs.delete(path);println("deleted " .. path) end
elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then ---@fixme remove condition after migration to settings files
if #fs.list(path) == 0 then fs.delete(path);println("deleted "..path) end
elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then ---@todo remove config.lua on full release
fs.delete(path)
println("deleted " .. path)
println("deleted "..path)
end
end
end
-- go through app/common directories to delete unused files
local function clean(manifest)
local root_ext = false
local tree = gen_tree(manifest)
local log = nil
if fs.exists(app..".settings") and settings.load(app..".settings") then
log = settings.get("LogPath")
if log:sub(1, 1) == "/" then log = log:sub(2) end
end
local tree = gen_tree(manifest, log)
table.insert(tree, "install_manifest.json")
table.insert(tree, "ccmsi.lua")
table.insert(tree, "log.txt") ---@fixme fix after migration to settings files?
lgray()
local ls = fs.list("/")
for _, val in pairs(ls) do
if fs.isDir(val) then
if tree[val] ~= nil then _clean_dir("/" .. val, tree[val]) end
if #fs.list(val) == 0 then fs.delete(val);println("deleted " .. val) end
if fs.isDriveRoot(val) then
yellow();println("skipped mount '"..val.."'")
elseif fs.isDir(val) then
if tree[val] ~= nil then lgray();_clean_dir("/"..val, tree[val])
else white(); if ask_y_n("delete the unused directory '"..val.."'") then lgray();_clean_dir("/"..val) end end
if #fs.list(val) == 0 then fs.delete(val);lgray();println("deleted empty directory '"..val.."'") end
elseif not _in_array(val, tree) and (string.find(val, ".settings") == nil) then
root_ext = true
yellow();println(val .. " not used")
white();if ask_y_n("delete the unused file '"..val.."'") then fs.delete(val);lgray();println("deleted "..val) end
end
end
white()
if root_ext then println("Files in root directory won't be automatically deleted.") end
end
-- get and validate command line options
println("-- CC Mekanism SCADA Installer " .. CCMSI_VERSION .. " --")
if _is_pkt_env then println("- SCADA Installer "..CCMSI_VERSION.." -")
else println("-- CC Mekanism SCADA Installer "..CCMSI_VERSION.." --") end
if #opts == 0 or opts[1] == "help" then
println("usage: ccmsi <mode> <app> <branch>")
if _is_pkt_env then
yellow();println("<mode>");lgray()
println(" check - check latest")
println(" install - fresh install")
println(" update - update app")
println(" uninstall - remove app")
yellow();println("<app>");lgray()
println(" reactor-plc")
println(" rtu")
println(" supervisor")
println(" coordinator")
println(" pocket")
println(" installer (update only)")
yellow();println("<branch>");lgray();
println(" main (default) | devel");white()
else
println("<mode>")
lgray()
println(" check - check latest versions available")
yellow()
println(" ccmsi check <branch> for target")
lgray()
println(" install - fresh install, overwrites config.lua")
println(" update - update files EXCEPT for config.lua")
println(" install - fresh install")
println(" update - update files")
println(" uninstall - delete files INCLUDING config/logs")
white();println("<app>");lgray()
println(" reactor-plc - reactor PLC firmware")
@@ -215,7 +305,8 @@ if #opts == 0 or opts[1] == "help" then
println(" pocket - pocket application")
println(" installer - ccmsi installer (update only)")
white();println("<branch>")
lgray();println(" main (default) | latest | devel");white()
lgray();println(" main (default) | devel");white()
end
return
else
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
@@ -235,18 +326,17 @@ else
-- determine target
if mode == "check" then target = opts[2] else target = opts[3] end
if (target ~= "main") and (target ~= "latest") and (target ~= "devel") then
if (target ~= "main") and (target ~= "devel") then
if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end
target = "main"
end
-- set paths
install_manifest = manifest_path .. target .. "/install_manifest.json"
repo_path = repo_path .. target .. "/"
install_manifest = manifest_path..target.."/install_manifest.json"
repo_path = repo_path..target.."/"
end
-- run selected mode
if mode == "check" then
local ok, manifest = get_remote_manifest()
if not ok then return end
@@ -262,20 +352,22 @@ if mode == "check" then
-- list all versions
for key, value in pairs(manifest.versions) do
term.setTextColor(colors.purple)
print(string.format("%-14s", "[" .. key .. "]"))
local tag = string.format("%-14s", "["..key.."]")
if not _is_pkt_env then print(tag) end
if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then
if _is_pkt_env then println(tag) end
blue();print(local_manifest.versions[key])
if value ~= local_manifest.versions[key] then
white();print(" (")
cyan();print(value);white();println(" available)")
else green();println(" (up to date)") end
else
elseif not _is_pkt_env then
lgray();print("not installed");white();print(" (latest ")
cyan();print(value);white();println(")")
end
end
if manifest.versions.installer ~= local_manifest.versions.installer then
if manifest.versions.installer ~= local_manifest.versions.installer and not _is_pkt_env then
yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white()
end
elseif mode == "install" or mode == "update" then
@@ -317,10 +409,10 @@ elseif mode == "install" or mode == "update" then
if not update_installer then yellow();println("A different version of the installer is available, it is recommended to update to it.");white() end
if update_installer or ask_y_n("Would you like to update now") then
lgray();println("GET ccmsi.lua")
local dl, err = http.get(repo_path .. "ccmsi.lua")
local dl, err = http.get(repo_path.."ccmsi.lua")
if dl == nil then
red();println("HTTP Error " .. err)
red();println("HTTP Error: "..err)
println("Installer download failed.");white()
else
local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location
@@ -344,12 +436,8 @@ elseif mode == "install" or mode == "update" then
ver.lockbox.v_remote = manifest.versions.lockbox
green()
if mode == "install" then
println("Installing " .. app .. " files...")
elseif mode == "update" then
println("Updating " .. app .. " files... (keeping old config.lua)")
end
white()
if mode == "install" then print("Installing ") else print("Updating ") end
println(app.." files...");white()
ver.boot.changed = show_pkg_change("bootldr", ver.boot)
ver.common.changed = show_pkg_change("common", ver.common)
@@ -361,9 +449,6 @@ elseif mode == "install" or mode == "update" then
ver.graphics.changed = show_pkg_change("graphics", ver.graphics)
ver.lockbox.changed = show_pkg_change("lockbox", ver.lockbox)
-- ask for confirmation
if not ask_y_n("Continue", false) then return end
--------------------------
-- START INSTALL/UPDATE --
--------------------------
@@ -375,15 +460,35 @@ elseif mode == "install" or mode == "update" then
local file_list = manifest.files
local size_list = manifest.sizes
local dependencies = manifest.depends[app]
local config_file = app .. "/config.lua"
table.insert(dependencies, app)
-- helper function to check if a dependency is unchanged
local function unchanged(dependency)
if dependency == "system" then return not ver.boot.changed
elseif dependency == "graphics" then return not ver.graphics.changed
elseif dependency == "lockbox" then return not ver.lockbox.changed
elseif dependency == "common" then return not (ver.common.changed or ver.comms.changed)
elseif dependency == app then return not ver.app.changed
else return true end
end
local any_change = false
for _, dependency in pairs(dependencies) do
local size = size_list[dependency]
space_required = space_required + size
any_change = any_change or not unchanged(dependency)
end
if mode == "update" and not any_change then
yellow();println("Nothing to do, everything is already up-to-date!");white()
return
end
-- ask for confirmation
if not ask_y_n("Continue", false) then return end
-- check space constraints
if space_available < space_required then
single_file_mode = true
@@ -399,16 +504,6 @@ elseif mode == "install" or mode == "update" then
local success = true
-- helper function to check if a dependency is unchanged
local function unchanged(dependency)
if dependency == "system" then return not ver.boot.changed
elseif dependency == "graphics" then return not ver.graphics.changed
elseif dependency == "lockbox" then return not ver.lockbox.changed
elseif dependency == "common" then return not (ver.common.changed or ver.comms.changed)
elseif dependency == app then return not ver.app.changed
else return true end
end
if not single_file_mode then
if fs.exists(install_dir) then fs.delete(install_dir);fs.makeDir(install_dir) end
@@ -422,20 +517,15 @@ elseif mode == "install" or mode == "update" then
local files = file_list[dependency]
for _, file in pairs(files) do
println("GET " .. file)
local dl, err = http.get(repo_path .. file)
if dl == nil then
red();println("HTTP Error " .. err)
println("GET "..file)
if not http_get_file(file, install_dir.."/") then
red();println("failed to download "..file)
success = false
break
else
local handle = fs.open(install_dir .. "/" .. file, "w")
handle.write(dl.readAll())
handle.close()
end
end
end
if not success then break end
end
-- copy in downloaded files (installation)
@@ -449,11 +539,9 @@ elseif mode == "install" or mode == "update" then
local files = file_list[dependency]
for _, file in pairs(files) do
if mode == "install" or file ~= config_file then
local temp_file = install_dir .. "/" .. file
if fs.exists(file) then fs.delete(file) end
fs.move(temp_file, file)
end
local temp_file = install_dir.."/"..file
if fs.exists(file) then fs.delete(file) end
fs.move(temp_file, file)
end
end
end
@@ -486,22 +574,15 @@ elseif mode == "install" or mode == "update" then
local files = file_list[dependency]
for _, file in pairs(files) do
if mode == "install" or file ~= config_file then
println("GET " .. file)
local dl, err = http.get(repo_path .. file)
if dl == nil then
red();println("HTTP Error " .. err)
success = false
break
else
local handle = fs.open("/" .. file, "w")
handle.write(dl.readAll())
handle.close()
end
println("GET "..file)
if not http_get_file(file, "/") then
red();println("failed to download "..file)
success = false
break
end
end
end
if not success then break end
end
if success then
@@ -528,11 +609,11 @@ elseif mode == "uninstall" then
end
if manifest.versions[app] == nil then
red();println("Error: '" .. app .. "' is not installed.")
red();println("Error: '"..app.."' is not installed.")
return
end
orange();println("Uninstalling all " .. app .. " files...")
orange();println("Uninstalling all "..app.." files...")
-- ask for confirmation
if not ask_y_n("Continue", false) then return end
@@ -545,40 +626,12 @@ elseif mode == "uninstall" then
table.insert(dependencies, app)
-- delete log file
local log_deleted = false
local settings_file = app .. ".settings"
local legacy_config_file = app .. "/config.lua"
lgray()
if fs.exists(legacy_config_file) then
log_deleted = pcall(function ()
local config = require(app .. ".config")
if fs.exists(config.LOG_PATH) then
fs.delete(config.LOG_PATH)
println("deleted log file " .. config.LOG_PATH)
end
end)
elseif fs.exists(settings_file) and settings.load(settings_file) then
local log = settings.get("LogPath")
if log ~= nil and fs.exists(log) then
log_deleted = true
fs.delete(log)
println("deleted log file " .. log)
end
end
if not log_deleted then
red();println("Failed to delete log file.")
white();println("press any key to continue...")
any_key();lgray()
end
-- delete all installed files
lgray()
for _, dependency in pairs(dependencies) do
local files = file_list[dependency]
for _, file in pairs(files) do
if fs.exists(file) then fs.delete(file);println("deleted " .. file) end
if fs.exists(file) then fs.delete(file);println("deleted "..file) end
end
local folder = files[1]
@@ -589,13 +642,31 @@ elseif mode == "uninstall" then
if fs.isDir(folder) then
fs.delete(folder)
println("deleted directory " .. folder)
println("deleted directory "..folder)
end
end
-- delete log file
local log_deleted = false
local settings_file = app..".settings"
if fs.exists(settings_file) and settings.load(settings_file) then
local log = settings.get("LogPath")
if log ~= nil then
log_deleted = true
if fs.exists(log) then
fs.delete(log)
println("deleted log file "..log)
end
end
end
if not log_deleted then
red();println("Failed to delete log file (it may not exist).");lgray()
end
if fs.exists(settings_file) then
fs.delete(settings_file)
println("deleted " .. settings_file)
fs.delete(settings_file);println("deleted "..settings_file)
end
fs.delete("install_manifest.json")

View File

@@ -1,15 +1,10 @@
print("CONFIGURE> SCANNING FOR CONFIGURATOR...")
if fs.exists("reactor-plc/configure.lua") then
require("reactor-plc.configure").configure()
elseif fs.exists("rtu/configure.lua") then
require("rtu.configure").configure()
elseif fs.exists("supervisor/startup.lua") then
print("CONFIGURE> SUPERVISOR CONFIGURATOR NOT YET IMPLEMENTED IN BETA")
elseif fs.exists("coordinator/startup.lua") then
print("CONFIGURE> COORDINATOR CONFIGURATOR NOT YET IMPLEMENTED IN BETA")
elseif fs.exists("pocket/startup.lua") then
print("CONFIGURE> POCKET CONFIGURATOR NOT YET IMPLEMENTED IN BETA")
if fs.exists("reactor-plc/configure.lua") then require("reactor-plc.configure").configure()
elseif fs.exists("rtu/configure.lua") then require("rtu.configure").configure()
elseif fs.exists("supervisor/configure.lua") then require("supervisor.configure").configure()
elseif fs.exists("coordinator/configure.lua") then require("coordinator.configure").configure()
elseif fs.exists("pocket/configure.lua") then require("pocket.configure").configure()
else
print("CONFIGURE> NO CONFIGURATOR FOUND")
print("CONFIGURE> EXIT")

View File

@@ -1,41 +0,0 @@
local config = {}
-- supervisor comms channel
config.SVR_CHANNEL = 16240
-- coordinator comms channel
config.CRD_CHANNEL = 16243
-- pocket comms channel
config.PKT_CHANNEL = 16244
-- max trusted modem message distance (0 to disable check)
config.TRUSTED_RANGE = 0
-- time in seconds (>= 2) before assuming a remote device is no longer active
config.SV_TIMEOUT = 5
config.API_TIMEOUT = 5
-- facility authentication key (do NOT use one of your passwords)
-- this enables verifying that messages are authentic
-- all devices on the same network must use the same key
-- config.AUTH_KEY = "SCADAfacility123"
-- expected number of reactor units, used only to require that number of unit monitors
config.NUM_UNITS = 4
-- alarm sounder volume (0.0 to 3.0, 1.0 being standard max volume, this is the option given to to speaker.play())
-- note: alarm sine waves are at half saturation, so that multiple will be required to reach full scale
config.SOUNDER_VOLUME = 1.0
-- true for 24 hour time on main view screen
config.TIME_24_HOUR = true
-- disable flow view (for legacy layouts)
config.DISABLE_FLOW_VIEW = false
-- log path
config.LOG_PATH = "/log.txt"
-- log mode
-- 0 = APPEND (adds to existing file on start)
-- 1 = NEW (replaces existing file on start)
config.LOG_MODE = 0
-- true to log verbose debug messages
config.LOG_DEBUG = false
return config

1504
coordinator/configure.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,16 +4,13 @@ local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local types = require("scada-common.types")
local themes = require("graphics.themes")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local apisessions = require("coordinator.session.apisessions")
local dialog = require("coordinator.ui.dialog")
local print = util.print
local println = util.println
local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
@@ -26,182 +23,169 @@ local LINK_TIMEOUT = 60.0
local coordinator = {}
-- request the user to select a monitor
---@nodiscard
---@param names table available monitors
---@return boolean|string|nil
local function ask_monitor(names)
println("available monitors:")
for i = 1, #names do
print(" " .. names[i])
end
println("")
println("select a monitor or type c to cancel")
---@type crd_config
local config = {}
local iface = dialog.ask_options(names, "c")
coordinator.config = config
if iface ~= false and iface ~= nil then
util.filter_table(names, function (x) return x ~= iface end)
-- load the coordinator configuration<br>
-- status of 0 is OK, 1 is bad config, 2 is bad monitor config
---@return 0|1|2 status, nil|monitors_struct|string monitors (or error message)
function coordinator.load_config()
if not settings.load("/coordinator.settings") then return 1 end
config.UnitCount = settings.get("UnitCount")
config.SpeakerVolume = settings.get("SpeakerVolume")
config.Time24Hour = settings.get("Time24Hour")
config.TempScale = settings.get("TempScale")
config.EnergyScale = settings.get("EnergyScale")
config.DisableFlowView = settings.get("DisableFlowView")
config.MainDisplay = settings.get("MainDisplay")
config.FlowDisplay = settings.get("FlowDisplay")
config.UnitDisplays = settings.get("UnitDisplays")
config.SVR_Channel = settings.get("SVR_Channel")
config.CRD_Channel = settings.get("CRD_Channel")
config.PKT_Channel = settings.get("PKT_Channel")
config.SVR_Timeout = settings.get("SVR_Timeout")
config.API_Timeout = settings.get("API_Timeout")
config.TrustedRange = settings.get("TrustedRange")
config.AuthKey = settings.get("AuthKey")
config.LogMode = settings.get("LogMode")
config.LogPath = settings.get("LogPath")
config.LogDebug = settings.get("LogDebug")
config.MainTheme = settings.get("MainTheme")
config.FrontPanelTheme = settings.get("FrontPanelTheme")
config.ColorMode = settings.get("ColorMode")
local cfv = util.new_validator()
cfv.assert_type_int(config.UnitCount)
cfv.assert_range(config.UnitCount, 1, 4)
cfv.assert_type_bool(config.Time24Hour)
cfv.assert_type_int(config.TempScale)
cfv.assert_range(config.TempScale, 1, 4)
cfv.assert_type_int(config.EnergyScale)
cfv.assert_range(config.EnergyScale, 1, 3)
cfv.assert_type_bool(config.DisableFlowView)
cfv.assert_type_table(config.UnitDisplays)
cfv.assert_type_num(config.SpeakerVolume)
cfv.assert_range(config.SpeakerVolume, 0, 3)
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.CRD_Channel)
cfv.assert_channel(config.PKT_Channel)
cfv.assert_type_num(config.SVR_Timeout)
cfv.assert_min(config.SVR_Timeout, 2)
cfv.assert_type_num(config.API_Timeout)
cfv.assert_min(config.API_Timeout, 2)
cfv.assert_type_num(config.TrustedRange)
cfv.assert_min(config.TrustedRange, 0)
cfv.assert_type_str(config.AuthKey)
if type(config.AuthKey) == "string" then
local len = string.len(config.AuthKey)
cfv.assert(len == 0 or len >= 8)
end
return iface
end
cfv.assert_type_int(config.LogMode)
cfv.assert_range(config.LogMode, 0, 1)
cfv.assert_type_str(config.LogPath)
cfv.assert_type_bool(config.LogDebug)
cfv.assert_type_int(config.MainTheme)
cfv.assert_range(config.MainTheme, 1, 2)
cfv.assert_type_int(config.FrontPanelTheme)
cfv.assert_range(config.FrontPanelTheme, 1, 2)
cfv.assert_type_int(config.ColorMode)
cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
-- Monitor Setup
-- configure monitor layout
---@param num_units integer number of units expected
---@param disable_flow_view boolean disable flow view (legacy)
---@return boolean success, monitors_struct? monitors
function coordinator.configure_monitors(num_units, disable_flow_view)
---@class monitors_struct
local monitors = {
primary = nil, ---@type table|nil
primary_name = "",
flow = nil, ---@type table|nil
main = nil, ---@type table|nil
main_name = "",
flow = nil, ---@type table|nil
flow_name = "",
unit_displays = {},
unit_name_map = {}
}
local monitors_avail = ppm.get_monitor_list()
local names = {}
local available = {}
local mon_cfv = util.new_validator()
-- get all interface names
for iface, _ in pairs(monitors_avail) do
table.insert(names, iface)
table.insert(available, iface)
end
local names = {}
for iface, _ in pairs(ppm.get_monitor_list()) do table.insert(names, iface) end
-- we need a certain number of monitors (1 per unit + 1 primary display + 1 flow display)
local num_displays_needed = num_units + util.trinary(disable_flow_view, 1, 2)
if #names < num_displays_needed then
local message = "not enough monitors connected (need " .. num_displays_needed .. ")"
println(message)
log.warning(message)
return false
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)
-- attempt to load settings
if not settings.load("/coord.settings") then
log.warning("configure_monitors(): failed to load coordinator settings file (may not exist yet)")
else
local _primary = settings.get("PRIMARY_DISPLAY")
local _flow = settings.get("FLOW_DISPLAY")
local _unitd = settings.get("UNIT_DISPLAYS")
if mon_cfv.valid() then
local w, h, _
-- filter out already assigned monitors
util.filter_table(available, function (x) return x ~= _primary end)
util.filter_table(available, function (x) return x ~= _flow end)
if type(_unitd) == "table" then
util.filter_table(available, function (x) return not util.table_contains(_unitd, x) end)
end
end
---------------------
-- PRIMARY DISPLAY --
---------------------
local iface_primary_display = settings.get("PRIMARY_DISPLAY") ---@type boolean|string|nil
if not util.table_contains(names, iface_primary_display) then
println("primary display is not connected")
local response = dialog.ask_y_n("would you like to change it", true)
if response == false then return false end
iface_primary_display = nil
end
while iface_primary_display == nil and #available > 0 do
iface_primary_display = ask_monitor(available)
end
if type(iface_primary_display) ~= "string" then return false end
settings.set("PRIMARY_DISPLAY", iface_primary_display)
util.filter_table(available, function (x) return x ~= iface_primary_display end)
monitors.primary = ppm.get_periph(iface_primary_display)
monitors.primary_name = iface_primary_display
--------------------------
-- FLOW MONITOR DISPLAY --
--------------------------
if not disable_flow_view then
local iface_flow_display = settings.get("FLOW_DISPLAY") ---@type boolean|string|nil
if not util.table_contains(names, iface_flow_display) then
println("flow monitor display is not connected")
local response = dialog.ask_y_n("would you like to change it", true)
if response == false then return false end
iface_flow_display = nil
end
while iface_flow_display == nil and #available > 0 do
iface_flow_display = ask_monitor(available)
end
if type(iface_flow_display) ~= "string" then return false end
settings.set("FLOW_DISPLAY", iface_flow_display)
util.filter_table(available, function (x) return x ~= iface_flow_display end)
monitors.flow = ppm.get_periph(iface_flow_display)
monitors.flow_name = iface_flow_display
end
-------------------
-- UNIT DISPLAYS --
-------------------
local unit_displays = settings.get("UNIT_DISPLAYS")
if unit_displays == nil then
unit_displays = {}
for i = 1, num_units do
local display = nil
while display == nil and #available > 0 do
println("please select monitor for unit #" .. i)
display = ask_monitor(available)
if not util.table_contains(names, config.MainDisplay) then
return 2, "Main monitor is not connected."
end
if display == false then return false end
monitors.main = ppm.get_periph(config.MainDisplay)
monitors.main_name = config.MainDisplay
unit_displays[i] = display
end
else
-- make sure all displays are connected
for i = 1, num_units do
local display = unit_displays[i]
if not util.table_contains(names, display) then
println("unit #" .. i .. " display is not connected")
local response = dialog.ask_y_n("would you like to change it", true)
if response == false then return false end
display = nil
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
while display == nil and #available > 0 do
display = ask_monitor(available)
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
if display == false then return false 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
unit_displays[i] = display
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
settings.set("UNIT_DISPLAYS", unit_displays)
if not settings.save("/coord.settings") then
log.warning("configure_monitors(): failed to save coordinator settings file")
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
for i = 1, #unit_displays do
monitors.unit_displays[i] = ppm.get_periph(unit_displays[i])
monitors.unit_name_map[i] = unit_displays[i]
end
return true, monitors
return 0, monitors
end
-- dmesg print wrapper
@@ -211,7 +195,7 @@ end
---@return function? update, function? done
local function log_dmesg(message, dmesg_tag, working)
local colors = {
GRAPHICS = colors.green,
RENDER = colors.green,
SYSTEM = colors.cyan,
BOOT = colors.blue,
COMMS = colors.purple,
@@ -225,7 +209,7 @@ local function log_dmesg(message, dmesg_tag, working)
end
end
function coordinator.log_graphics(message) log_dmesg(message, "GRAPHICS") end
function coordinator.log_render(message) log_dmesg(message, "RENDER") end
function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end
function coordinator.log_boot(message) log_dmesg(message, "BOOT") end
function coordinator.log_comms(message) log_dmesg(message, "COMMS") end
@@ -246,18 +230,13 @@ end
---@nodiscard
---@param version string coordinator version
---@param nic nic network interface device
---@param num_units integer number of configured units for number of monitors, checked against SV
---@param crd_channel integer port of configured supervisor
---@param svr_channel integer listening port for supervisor replys
---@param pkt_channel integer listening port for pocket API
---@param range integer trusted device connection range
---@param sv_watchdog watchdog
function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pkt_channel, range, sv_watchdog)
function coordinator.comms(version, nic, sv_watchdog)
local self = {
sv_linked = false,
sv_addr = comms.BROADCAST,
sv_seq_num = 0,
sv_r_seq_num = nil,
sv_seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
sv_r_seq_num = nil, ---@type nil|integer
sv_config_err = false,
last_est_ack = ESTABLISH_ACK.ALLOW,
last_api_est_acks = {},
@@ -267,16 +246,16 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
est_task_done = nil
}
comms.set_trusted_range(range)
-- PRIVATE FUNCTIONS --
comms.set_trusted_range(config.TrustedRange)
-- configure network channels
nic.closeAll()
nic.open(crd_channel)
nic.open(config.CRD_Channel)
-- link nic to apisessions
apisessions.init(nic)
-- pass config to apisessions
apisessions.init(nic, config)
-- PRIVATE FUNCTIONS --
-- send a packet to the supervisor
---@param msg_type MGMT_TYPE|CRDN_TYPE
@@ -296,26 +275,28 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable())
nic.transmit(svr_channel, crd_channel, s_pkt)
nic.transmit(config.SVR_Channel, config.CRD_Channel, s_pkt)
self.sv_seq_num = self.sv_seq_num + 1
end
-- send an API establish request response
---@param packet scada_packet
---@param ack ESTABLISH_ACK
local function _send_api_establish_ack(packet, ack)
---@param data any?
local function _send_api_establish_ack(packet, ack, data)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack })
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(pkt_channel, crd_channel, s_pkt)
nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt)
self.last_api_est_acks[packet.src_addr()] = ack
end
-- attempt connection establishment
local function _send_establish()
self.sv_r_seq_num = nil
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRD })
end
@@ -339,24 +320,24 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
if not self.sv_linked then
if self.est_tick_waiting == nil then
self.est_start = util.time_s()
self.est_start = os.clock()
self.est_last = self.est_start
self.est_tick_waiting, self.est_task_done =
coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. svr_channel)
coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SVR_Channel)
_send_establish()
else
self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (util.time_s() - self.est_start)))
self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (os.clock() - self.est_start)))
end
if abort or (util.time_s() - self.est_start) >= LINK_TIMEOUT then
if abort or (os.clock() - self.est_start) >= LINK_TIMEOUT then
self.est_task_done(false)
if abort then
coordinator.log_comms("supervisor connection attempt cancelled by user")
elseif self.sv_config_err then
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
elseif not self.sv_linked then
if self.last_est_ack == ESTABLISH_ACK.DENY then
coordinator.log_comms("supervisor connection attempt denied")
@@ -371,11 +352,12 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
ok = false
elseif self.sv_config_err then
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
self.est_task_done(false)
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
ok = false
elseif (util.time_s() - self.est_last) > 1.0 then
elseif (os.clock() - self.est_last) > 1.0 then
_send_establish()
self.est_last = util.time_s()
self.est_last = os.clock()
end
elseif self.est_tick_waiting ~= nil then
self.est_task_done(true)
@@ -405,10 +387,10 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
end
-- send the auto process control configuration with a start command
---@param config coord_auto_config configuration
function public.send_auto_start(config)
---@param auto_cfg sys_auto_config configuration
function public.send_auto_start(auto_cfg)
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits
FAC_COMMAND.START, auto_cfg.mode, auto_cfg.burn_target, auto_cfg.charge_target, auto_cfg.gen_target, auto_cfg.limits
})
end
@@ -464,9 +446,9 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
local src_addr = packet.scada_frame.src_addr()
local protocol = packet.scada_frame.protocol()
if l_chan ~= crd_channel then
if l_chan ~= config.CRD_Channel then
log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == pkt_channel then
elseif r_chan == config.PKT_Channel then
if not self.sv_linked then
log.debug("discarding pocket API packet before linked to supervisor")
elseif protocol == PROTOCOL.SCADA_CRDN then
@@ -494,10 +476,11 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
-- validate packet and continue
if packet.length == 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
if packet.length == 4 then
local comms_v = util.strval(packet.data[1])
local firmware_v = util.strval(packet.data[2])
local dev_type = packet.data[3]
local api_v = util.strval(packet.data[4])
if comms_v ~= comms.version then
if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_VERSION then
@@ -505,12 +488,19 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
end
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif api_v ~= comms.api_version then
if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_API_VERSION then
log.info(util.c("dropping API establish packet with incorrect api version v", api_v, " (expected v", comms.api_version, ")"))
end
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_API_VERSION)
elseif dev_type == DEVICE_TYPE.PKT then
-- pocket linking request
local id = apisessions.establish_session(src_addr, firmware_v)
local id = apisessions.establish_session(src_addr, packet.scada_frame.seq_num(), firmware_v)
coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id))
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW)
local conf = iocontrol.get_db().facility.conf
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW, { conf.num_units, conf.cooling })
else
log.debug(util.c("API_ESTABLISH: illegal establish packet for device ", dev_type, " on pocket channel"))
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
@@ -526,18 +516,18 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
else
log.debug("illegal packet type " .. protocol .. " on pocket channel", true)
end
elseif r_chan == svr_channel then
elseif r_chan == config.SVR_Channel then
-- check sequence number
if self.sv_r_seq_num == nil then
self.sv_r_seq_num = packet.scada_frame.seq_num()
elseif self.sv_linked and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.sv_r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: next = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return false
elseif self.sv_linked and src_addr ~= self.sv_addr then
log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?")
return false
else
self.sv_r_seq_num = packet.scada_frame.seq_num()
self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
end
-- feed watchdog on valid sequence number
@@ -586,7 +576,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
local ack = packet.data[2] == true
if cmd == FAC_COMMAND.SCRAM_ALL then
iocontrol.get_db().facility.scram_ack(ack)
process.fac_ack(cmd, ack)
elseif cmd == FAC_COMMAND.STOP then
iocontrol.get_db().facility.stop_ack(ack)
elseif cmd == FAC_COMMAND.START then
@@ -596,11 +586,13 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch")
end
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
iocontrol.get_db().facility.ack_alarms_ack(ack)
process.fac_ack(cmd, ack)
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
process.waste_ack_handle(packet.data[2])
elseif cmd == FAC_COMMAND.SET_PU_FB then
process.pu_fb_ack_handle(packet.data[2])
elseif cmd == FAC_COMMAND.SET_SPS_LP then
process.sps_lp_ack_handle(packet.data[2])
else
log.debug(util.c("received facility command ack with unknown command ", cmd))
end
@@ -635,21 +627,15 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
if unit ~= nil then
if cmd == UNIT_COMMAND.SCRAM then
unit.scram_ack(ack)
process.unit_ack(unit_id, cmd, ack)
elseif cmd == UNIT_COMMAND.START then
unit.start_ack(ack)
process.unit_ack(unit_id, cmd, ack)
elseif cmd == UNIT_COMMAND.RESET_RPS then
unit.reset_rps_ack(ack)
elseif cmd == UNIT_COMMAND.SET_BURN then
unit.set_burn_ack(ack)
elseif cmd == UNIT_COMMAND.SET_WASTE then
unit.set_waste_ack(ack)
process.unit_ack(unit_id, cmd, ack)
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
unit.ack_alarms_ack(ack)
elseif cmd == UNIT_COMMAND.SET_GROUP then
-- UI will be updated to display current group if changed successfully
process.unit_ack(unit_id, cmd, ack)
else
log.debug(util.c("received unit command ack with unknown command ", cmd))
log.debug(util.c("received unsupported unit command ack for command ", cmd))
end
else
log.debug(util.c("received unit command ack with unknown unit ", unit_id))
@@ -699,28 +685,27 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
-- connection with supervisor established
if packet.length == 2 then
local est_ack = packet.data[1]
local config = packet.data[2]
local sv_config = packet.data[2]
if est_ack == ESTABLISH_ACK.ALLOW then
-- reset to disconnected before validating
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
if type(config) == "table" and #config == 2 then
if type(sv_config) == "table" and #sv_config == 2 then
-- get configuration
---@class facility_conf
local conf = {
num_units = config[1], ---@type integer
cooling = config[2] ---@type sv_cooling_conf
num_units = sv_config[1], ---@type integer
cooling = sv_config[2] ---@type sv_cooling_conf
}
if conf.num_units == num_units then
if conf.num_units == config.UnitCount then
-- init io controller
iocontrol.init(conf, public)
iocontrol.init(conf, public, config.TempScale, config.EnergyScale)
self.sv_addr = src_addr
self.sv_linked = true
self.sv_r_seq_num = nil
self.sv_config_err = false
iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED)

View File

@@ -15,6 +15,11 @@ local pgi = require("coordinator.ui.pgi")
local ALARM_STATE = types.ALARM_STATE
local PROCESS = types.PROCESS
local ENERGY_SCALE = types.ENERGY_SCALE
local ENERGY_UNITS = types.ENERGY_SCALE_UNITS
local TEMP_SCALE = types.TEMP_SCALE
local TEMP_UNITS = types.TEMP_SCALE_UNITS
-- nominal RTT is ping (0ms to 10ms usually) + 500ms for CRD main loop tick
local WARN_RTT = 1000 -- 2x as long as expected w/ 0 ping
local HIGH_RTT = 1500 -- 3.33x as long as expected w/ 0 ping
@@ -47,13 +52,44 @@ end
-- initialize the coordinator IO controller
---@param conf facility_conf configuration
---@param comms coord_comms comms reference
function iocontrol.init(conf, comms)
---@param temp_scale TEMP_SCALE temperature unit
---@param energy_scale ENERGY_SCALE energy unit
function iocontrol.init(conf, comms, temp_scale, energy_scale)
io.temp_label = TEMP_UNITS[temp_scale]
io.energy_label = ENERGY_UNITS[energy_scale]
-- temperature unit label and conversion function (from Kelvin)
if temp_scale == TEMP_SCALE.CELSIUS then
io.temp_convert = function (t) return t - 273.15 end
elseif temp_scale == TEMP_SCALE.FAHRENHEIT then
io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end
elseif temp_scale == TEMP_SCALE.RANKINE then
io.temp_convert = function (t) return 1.8 * t end
else
io.temp_label = "K"
io.temp_convert = function (t) return t end
end
-- energy unit label and conversion function (from Joules unless otherwise specified)
if energy_scale == ENERGY_SCALE.FE or energy_scale == ENERGY_SCALE.RF then
io.energy_convert = util.joules_to_fe_rf
io.energy_convert_from_fe = function (t) return t end
io.energy_convert_to_fe = function (t) return t end
else
io.energy_label = "J"
io.energy_convert = function (t) return t end
io.energy_convert_from_fe = util.fe_rf_to_joules
io.energy_convert_to_fe = util.joules_to_fe_rf
end
-- facility data structure
---@class ioctl_facility
io.facility = {
conf = conf,
num_units = conf.num_units,
tank_mode = conf.cooling.fac_tank_mode,
tank_defs = conf.cooling.fac_tank_defs,
tank_list = conf.cooling.fac_tank_list,
all_sys_ok = false,
rtu_count = 0,
@@ -75,14 +111,13 @@ function iocontrol.init(conf, comms)
---@type WASTE_PRODUCT
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
auto_pu_fallback_active = false,
auto_sps_disabled = false,
radiation = types.new_zero_radiation_reading(),
save_cfg_ack = __generic_ack,
start_ack = __generic_ack,
stop_ack = __generic_ack,
scram_ack = __generic_ack,
ack_alarms_ack = __generic_ack,
alarm_tones = { false, false, false, false, false, false, false, false },
@@ -107,92 +142,6 @@ function iocontrol.init(conf, comms)
table.insert(io.facility.sps_ps_tbl, psil.create())
table.insert(io.facility.sps_data_tbl, {})
-- determine tank information
if io.facility.tank_mode == 0 then
io.facility.tank_defs = {}
-- on facility tank mode 0, setup tank defs to match unit TANK option
for i = 1, conf.num_units do
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TANK, 1, 0)
end
io.facility.tank_list = { table.unpack(io.facility.tank_defs) }
else
-- decode the layout of tanks from the connections definitions
local tank_mode = io.facility.tank_mode
local tank_defs = io.facility.tank_defs
local tank_list = { table.unpack(tank_defs) }
local function calc_fdef(start_idx, end_idx)
local first = 4
for i = start_idx, end_idx do
if io.facility.tank_defs[i] == 2 then
if i < first then first = i end
end
end
return first
end
if tank_mode == 1 then
-- (1) 1 total facility tank (A A A A)
local first_fdef = calc_fdef(1, #tank_defs)
for i = 1, #tank_defs do
if i > first_fdef and tank_defs[i] == 2 then
tank_list[i] = 0
end
end
elseif tank_mode == 2 then
-- (2) 2 total facility tanks (A A A B)
local first_fdef = calc_fdef(1, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 3 then
-- (3) 2 total facility tanks (A A B B)
for _, a in pairs({ 1, 3 }) do
local b = a + 1
if (tank_defs[a] == 2) and (tank_defs[b] == 2) then
tank_list[b] = 0
end
end
elseif tank_mode == 4 then
-- (4) 2 total facility tanks (A B B B)
local first_fdef = calc_fdef(2, #tank_defs)
for i = 1, #tank_defs do
if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 5 then
-- (5) 3 total facility tanks (A A B C)
local first_fdef = calc_fdef(1, math.min(2, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 6 then
-- (6) 3 total facility tanks (A B B C)
local first_fdef = calc_fdef(2, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 7 then
-- (7) 3 total facility tanks (A B C C)
local first_fdef = calc_fdef(3, #tank_defs)
for i = 1, #tank_defs do
if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
end
io.facility.tank_list = tank_list
end
-- create facility tank tables
for i = 1, #io.facility.tank_list do
if io.facility.tank_list[i] == 2 then
@@ -210,39 +159,40 @@ function iocontrol.init(conf, comms)
---@class ioctl_unit
local entry = {
unit_id = i,
connected = false,
rtu_hw = { boilers = {}, turbines = {} },
num_boilers = 0,
num_turbines = 0,
num_snas = 0,
has_tank = conf.cooling.r_cool[i].TANK,
has_tank = conf.cooling.r_cool[i].TankConnection,
control_state = false,
burn_rate_cmd = 0.0,
radiation = types.new_zero_radiation_reading(),
sna_prod_rate = 0.0,
sna_peak_rate = 0.0,
sna_max_rate = 0.0,
sna_out_rate = 0.0,
waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
waste_product = types.WASTE_PRODUCT.PLUTONIUM,
-- auto control group
a_group = 0,
last_rate_change_ms = 0,
turbine_flow_stable = false,
start = function () process.start(i) end,
scram = function () process.scram(i) end,
reset_rps = function () process.reset_rps(i) end,
ack_alarms = function () process.ack_all_alarms(i) end,
-- auto control group
a_group = types.AUTO_GROUP.MANUAL,
start = function () io.process.start(i) end,
scram = function () io.process.scram(i) end,
reset_rps = function () io.process.reset_rps(i) end,
ack_alarms = function () io.process.ack_all_alarms(i) end,
set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate
set_waste = function (mode) process.set_unit_waste(i, mode) end, ---@param mode WASTE_MODE waste processing mode
set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0 for manual
start_ack = __generic_ack,
scram_ack = __generic_ack,
reset_rps_ack = __generic_ack,
ack_alarms_ack = __generic_ack,
set_burn_ack = __generic_ack,
set_waste_ack = __generic_ack,
alarm_callbacks = {
c_breach = { ack = function () ack(1) end, reset = function () reset(1) end },
radiation = { ack = function () ack(2) end, reset = function () reset(2) end },
@@ -260,18 +210,18 @@ function iocontrol.init(conf, comms)
---@type alarms
alarms = {
ALARM_STATE.INACTIVE, -- containment breach
ALARM_STATE.INACTIVE, -- containment radiation
ALARM_STATE.INACTIVE, -- reactor lost
ALARM_STATE.INACTIVE, -- damage critical
ALARM_STATE.INACTIVE, -- reactor taking damage
ALARM_STATE.INACTIVE, -- reactor over temperature
ALARM_STATE.INACTIVE, -- reactor high temperature
ALARM_STATE.INACTIVE, -- waste leak
ALARM_STATE.INACTIVE, -- waste level high
ALARM_STATE.INACTIVE, -- RPS transient
ALARM_STATE.INACTIVE, -- RCS transient
ALARM_STATE.INACTIVE -- turbine trip
ALARM_STATE.INACTIVE, -- containment breach
ALARM_STATE.INACTIVE, -- containment radiation
ALARM_STATE.INACTIVE, -- reactor lost
ALARM_STATE.INACTIVE, -- damage critical
ALARM_STATE.INACTIVE, -- reactor taking damage
ALARM_STATE.INACTIVE, -- reactor over temperature
ALARM_STATE.INACTIVE, -- reactor high temperature
ALARM_STATE.INACTIVE, -- waste leak
ALARM_STATE.INACTIVE, -- waste level high
ALARM_STATE.INACTIVE, -- RPS transient
ALARM_STATE.INACTIVE, -- RCS transient
ALARM_STATE.INACTIVE -- turbine trip
},
annunciator = {}, ---@type annunciator
@@ -295,15 +245,17 @@ function iocontrol.init(conf, comms)
end
-- create boiler tables
for _ = 1, conf.cooling.r_cool[i].BOILERS do
for _ = 1, conf.cooling.r_cool[i].BoilerCount do
table.insert(entry.boiler_ps_tbl, psil.create())
table.insert(entry.boiler_data_tbl, {})
table.insert(entry.rtu_hw.boilers, { connected = false, faulted = false })
end
-- create turbine tables
for _ = 1, conf.cooling.r_cool[i].TURBINES do
for _ = 1, conf.cooling.r_cool[i].TurbineCount do
table.insert(entry.turbine_ps_tbl, psil.create())
table.insert(entry.turbine_data_tbl, {})
table.insert(entry.rtu_hw.turbines, { connected = false, faulted = false })
end
-- create tank tables
@@ -320,6 +272,9 @@ function iocontrol.init(conf, comms)
-- pass IO control here since it can't be require'd due to a require loop
process.init(io, comms)
-- coordinator's process handle
io.process = process.create_handle()
end
--#region Front Panel PSIL
@@ -357,6 +312,13 @@ function iocontrol.fp_monitor_state(id, connected)
end
end
-- report thread (routine) statuses
---@param thread string thread name
---@param ok boolean thread state
function iocontrol.fp_rt_status(thread, ok)
io.fp.ps.publish(util.c("routine__", thread), ok)
end
-- report PKT firmware version and PKT session connection state
---@param session_id integer PKT session
---@param fw string firmware version
@@ -384,7 +346,7 @@ function iocontrol.fp_pkt_rtt(session_id, rtt)
elseif rtt > WARN_RTT then
io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.yellow_hc)
else
io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.green)
io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.green_hc)
end
end
@@ -566,7 +528,7 @@ function iocontrol.update_facility_status(status)
local ctl_status = status[1]
if type(ctl_status) == "table" and #ctl_status == 16 then
if type(ctl_status) == "table" and #ctl_status == 17 then
fac.all_sys_ok = ctl_status[1]
fac.auto_ready = ctl_status[2]
@@ -607,19 +569,20 @@ function iocontrol.update_facility_status(status)
local group_map = ctl_status[14]
if (type(group_map) == "table") and (#group_map == fac.num_units) then
local names = { "Manual", "Primary", "Secondary", "Tertiary", "Backup" }
for i = 1, #group_map do
io.units[i].a_group = group_map[i]
io.units[i].unit_ps.publish("auto_group_id", group_map[i])
io.units[i].unit_ps.publish("auto_group", names[group_map[i] + 1])
io.units[i].unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[group_map[i] + 1])
end
end
fac.auto_current_waste_product = ctl_status[15]
fac.auto_pu_fallback_active = ctl_status[16]
fac.auto_sps_disabled = ctl_status[17]
fac.ps.publish("current_waste_product", fac.auto_current_waste_product)
fac.ps.publish("pu_fallback_active", fac.auto_pu_fallback_active)
fac.ps.publish("sps_disabled_low_power", fac.auto_sps_disabled)
else
log.debug(log_header .. "control status not a table or length mismatch")
valid = false
@@ -636,10 +599,27 @@ function iocontrol.update_facility_status(status)
fac.rtu_count = rtu_statuses.count
-- power statistics
if type(rtu_statuses.power) == "table" then
fac.induction_ps_tbl[1].publish("avg_charge", rtu_statuses.power[1])
fac.induction_ps_tbl[1].publish("avg_inflow", rtu_statuses.power[2])
fac.induction_ps_tbl[1].publish("avg_outflow", rtu_statuses.power[3])
if type(rtu_statuses.power) == "table" and #rtu_statuses.power == 4 then
local data = fac.induction_data_tbl[1] ---@type imatrix_session_db
local ps = fac.induction_ps_tbl[1] ---@type psil
local chg = tonumber(rtu_statuses.power[1])
local in_f = tonumber(rtu_statuses.power[2])
local out_f = tonumber(rtu_statuses.power[3])
local eta = tonumber(rtu_statuses.power[4])
ps.publish("avg_charge", chg)
ps.publish("avg_inflow", in_f)
ps.publish("avg_outflow", out_f)
ps.publish("eta_ms", eta)
ps.publish("is_charging", in_f > out_f)
ps.publish("is_discharging", out_f > in_f)
if data and data.build then
local cap = util.joules_to_fe_rf(data.build.transfer_cap)
ps.publish("at_max_io", in_f >= cap or out_f >= cap)
end
else
log.debug(log_header .. "power statistics list not a table")
valid = false
@@ -850,6 +830,7 @@ function iocontrol.update_unit_statuses(statuses)
end
if #reactor_status == 0 then
unit.connected = false
unit.unit_ps.publish("computed_status", 1) -- disconnected
elseif #reactor_status == 3 then
local mek_status = reactor_status[1]
@@ -909,6 +890,8 @@ function iocontrol.update_unit_statuses(statuses)
unit.unit_ps.publish(key, val)
end
end
unit.connected = true
else
log.debug(log_header .. "reactor status length mismatch")
valid = false
@@ -923,7 +906,10 @@ function iocontrol.update_unit_statuses(statuses)
local boil_sum = 0
for id = 1, #unit.boiler_ps_tbl do
if rtu_statuses.boilers[i] == nil then
local connected = rtu_statuses.boilers[id] ~= nil
unit.rtu_hw.boilers[id].connected = connected
if not connected then
-- disconnected
unit.boiler_ps_tbl[id].publish("computed_status", 1)
end
@@ -935,6 +921,7 @@ function iocontrol.update_unit_statuses(statuses)
local ps = unit.boiler_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(boiler, data, ps)
unit.rtu_hw.boilers[id].faulted = rtu_faulted
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
@@ -966,7 +953,10 @@ function iocontrol.update_unit_statuses(statuses)
local flow_sum = 0
for id = 1, #unit.turbine_ps_tbl do
if rtu_statuses.turbines[i] == nil then
local connected = rtu_statuses.turbines[id] ~= nil
unit.rtu_hw.turbines[id].connected = connected
if not connected then
-- disconnected
unit.turbine_ps_tbl[id].publish("computed_status", 1)
end
@@ -978,6 +968,7 @@ function iocontrol.update_unit_statuses(statuses)
local ps = unit.turbine_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(turbine, data, ps)
unit.rtu_hw.turbines[id].faulted = rtu_faulted
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
@@ -1009,7 +1000,7 @@ function iocontrol.update_unit_statuses(statuses)
-- dynamic tank statuses
if type(rtu_statuses.tanks) == "table" then
for id = 1, #unit.tank_ps_tbl do
if rtu_statuses.tanks[i] == nil then
if rtu_statuses.tanks[id] == nil then
-- disconnected
unit.tank_ps_tbl[id].publish("computed_status", 1)
end
@@ -1048,12 +1039,14 @@ function iocontrol.update_unit_statuses(statuses)
-- solar neutron activator status info
if type(rtu_statuses.sna) == "table" then
unit.num_snas = rtu_statuses.sna[1] ---@type integer
unit.sna_prod_rate = rtu_statuses.sna[2] ---@type number
unit.sna_peak_rate = rtu_statuses.sna[3] ---@type number
unit.sna_peak_rate = rtu_statuses.sna[2] ---@type number
unit.sna_max_rate = rtu_statuses.sna[3] ---@type number
unit.sna_out_rate = rtu_statuses.sna[4] ---@type number
unit.unit_ps.publish("sna_count", unit.num_snas)
unit.unit_ps.publish("sna_prod_rate", unit.sna_prod_rate)
unit.unit_ps.publish("sna_peak_rate", unit.sna_peak_rate)
unit.unit_ps.publish("sna_max_rate", unit.sna_max_rate)
unit.unit_ps.publish("sna_out_rate", unit.sna_out_rate)
sna_count_sum = sna_count_sum + unit.num_snas
else
@@ -1150,9 +1143,11 @@ function iocontrol.update_unit_statuses(statuses)
local unit_state = status[5]
if type(unit_state) == "table" then
if #unit_state == 6 then
if #unit_state == 8 then
unit.waste_mode = unit_state[5]
unit.waste_product = unit_state[6]
unit.last_rate_change_ms = unit_state[7]
unit.turbine_flow_stable = unit_state[8]
unit.unit_ps.publish("U_StatusLine1", unit_state[1])
unit.unit_ps.publish("U_StatusLine2", unit_state[2])
@@ -1201,7 +1196,7 @@ function iocontrol.update_unit_statuses(statuses)
local u_spent_rate = waste_rate
local u_pu_rate = util.trinary(is_pu, waste_rate, 0.0)
local u_po_rate = util.trinary(not is_pu, math.min(waste_rate, unit.sna_prod_rate), 0.0)
local u_po_rate = unit.sna_out_rate
unit.unit_ps.publish("pu_rate", u_pu_rate)
unit.unit_ps.publish("po_rate", u_po_rate)
@@ -1209,14 +1204,15 @@ function iocontrol.update_unit_statuses(statuses)
unit.unit_ps.publish("sna_in", util.trinary(is_pu, 0, burn_rate))
if unit.waste_product == types.WASTE_PRODUCT.POLONIUM then
u_spent_rate = u_po_rate
unit.unit_ps.publish("po_pl_rate", u_po_rate)
unit.unit_ps.publish("po_am_rate", 0)
po_pl_rate = po_pl_rate + u_po_rate
elseif unit.waste_product == types.WASTE_PRODUCT.ANTI_MATTER then
u_spent_rate = 0
unit.unit_ps.publish("po_pl_rate", 0)
unit.unit_ps.publish("po_am_rate", u_po_rate)
po_am_rate = po_am_rate + u_po_rate
u_spent_rate = 0
else
unit.unit_ps.publish("po_pl_rate", 0)
unit.unit_ps.publish("po_am_rate", 0)

View File

@@ -7,141 +7,331 @@ local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local FAC_COMMAND = comms.FAC_COMMAND
local UNIT_COMMAND = comms.UNIT_COMMAND
local F_CMD = comms.FAC_COMMAND
local U_CMD = comms.UNIT_COMMAND
local PROCESS = types.PROCESS
local PRODUCT = types.WASTE_PRODUCT
local REQUEST_TIMEOUT_MS = 10000
---@class process_controller
local process = {}
local self = {
io = nil, ---@type ioctl
comms = nil, ---@type coord_comms
---@class coord_auto_config
config = {
mode = PROCESS.INACTIVE,
burn_target = 0.0,
charge_target = 0.0,
gen_target = 0.0,
limits = {},
waste_product = PRODUCT.PLUTONIUM,
pu_fallback = false
local pctl = {
io = nil, ---@type ioctl
comms = nil, ---@type coord_comms
---@class sys_control_states
control_states = {
---@class sys_auto_config
process = {
mode = PROCESS.INACTIVE,
burn_target = 0.0,
charge_target = 0.0,
gen_target = 0.0,
limits = {},
waste_product = PRODUCT.PLUTONIUM,
pu_fallback = false,
sps_low_power = false
},
waste_modes = {},
priority_groups = {}
},
commands = {
unit = {}, ---@type process_command_state[][]
fac = {} ---@type process_command_state[]
}
}
--------------------------
-- UNIT COMMAND CONTROL --
--------------------------
---@class process_command_state
---@field active boolean if this command is live
---@field timeout integer expiration time of this command request
---@field requestors table list of callbacks from the requestors
-- write auto process control to config file
local function _write_auto_config()
-- save config
settings.set("ControlStates", pctl.control_states)
local saved = settings.save("/coordinator.settings")
if not saved then
log.warning("process._write_auto_config(): failed to save coordinator settings file")
end
return saved
end
-- initialize the process controller
---@param iocontrol ioctl iocontrl system
---@param coord_comms coord_comms coordinator communications
function process.init(iocontrol, coord_comms)
self.io = iocontrol
self.comms = coord_comms
pctl.io = iocontrol
pctl.comms = coord_comms
for i = 1, self.io.facility.num_units do
self.config.limits[i] = 0.1
-- create command handling objects
for _, v in pairs(F_CMD) do pctl.commands.fac[v] = { active = false, timeout = 0, requestors = {} } end
for i = 1, pctl.io.facility.num_units do
pctl.commands.unit[i] = {}
for _, v in pairs(U_CMD) do pctl.commands.unit[i][v] = { active = false, timeout = 0, requestors = {} } end
end
-- load settings
if not settings.load("/coord.settings") then
log.error("process.init(): failed to load coordinator settings file")
local ctl_proc = pctl.control_states.process
for i = 1, pctl.io.facility.num_units do
ctl_proc.limits[i] = 0.1
end
local ctrl_states = settings.get("ControlStates", {})
local config = ctrl_states.process ---@type sys_auto_config
-- facility auto control configuration
local config = settings.get("PROCESS") ---@type coord_auto_config|nil
if type(config) == "table" then
self.config.mode = config.mode
self.config.burn_target = config.burn_target
self.config.charge_target = config.charge_target
self.config.gen_target = config.gen_target
self.config.limits = config.limits
self.config.waste_product = config.waste_product
self.config.pu_fallback = config.pu_fallback
ctl_proc.mode = config.mode
ctl_proc.burn_target = config.burn_target
ctl_proc.charge_target = config.charge_target
ctl_proc.gen_target = config.gen_target
ctl_proc.limits = config.limits
ctl_proc.waste_product = config.waste_product
ctl_proc.pu_fallback = config.pu_fallback
ctl_proc.sps_low_power = config.sps_low_power
self.io.facility.ps.publish("process_mode", self.config.mode)
self.io.facility.ps.publish("process_burn_target", self.config.burn_target)
self.io.facility.ps.publish("process_charge_target", self.config.charge_target)
self.io.facility.ps.publish("process_gen_target", self.config.gen_target)
self.io.facility.ps.publish("process_waste_product", self.config.waste_product)
self.io.facility.ps.publish("process_pu_fallback", self.config.pu_fallback)
pctl.io.facility.ps.publish("process_mode", ctl_proc.mode)
pctl.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
pctl.io.facility.ps.publish("process_charge_target", pctl.io.energy_convert_from_fe(ctl_proc.charge_target))
pctl.io.facility.ps.publish("process_gen_target", pctl.io.energy_convert_from_fe(ctl_proc.gen_target))
pctl.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
pctl.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback)
pctl.io.facility.ps.publish("process_sps_low_power", ctl_proc.sps_low_power)
for id = 1, math.min(#self.config.limits, self.io.facility.num_units) do
local unit = self.io.units[id] ---@type ioctl_unit
unit.unit_ps.publish("burn_limit", self.config.limits[id])
for id = 1, math.min(#ctl_proc.limits, pctl.io.facility.num_units) do
local unit = pctl.io.units[id] ---@type ioctl_unit
unit.unit_ps.publish("burn_limit", ctl_proc.limits[id])
end
log.info("PROCESS: loaded auto control settings from coord.settings")
log.info("PROCESS: loaded auto control settings")
-- notify supervisor of auto waste config
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, self.config.waste_product)
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, self.config.pu_fallback)
pctl.comms.send_fac_command(F_CMD.SET_WASTE_MODE, ctl_proc.waste_product)
pctl.comms.send_fac_command(F_CMD.SET_PU_FB, ctl_proc.pu_fallback)
pctl.comms.send_fac_command(F_CMD.SET_SPS_LP, ctl_proc.sps_low_power)
end
-- unit waste states
local waste_modes = settings.get("WASTE_MODES") ---@type table|nil
local waste_modes = ctrl_states.waste_modes ---@type table|nil
if type(waste_modes) == "table" then
for id, mode in pairs(waste_modes) do
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
pctl.control_states.waste_modes[id] = mode
pctl.comms.send_unit_command(U_CMD.SET_WASTE, id, mode)
end
log.info("PROCESS: loaded unit waste mode settings from coord.settings")
log.info("PROCESS: loaded unit waste mode settings")
end
-- unit priority groups
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil
local prio_groups = ctrl_states.priority_groups ---@type table|nil
if type(prio_groups) == "table" then
for id, group in pairs(prio_groups) do
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group)
pctl.control_states.priority_groups[id] = group
pctl.comms.send_unit_command(U_CMD.SET_GROUP, id, group)
end
log.info("PROCESS: loaded priority groups settings from coord.settings")
log.info("PROCESS: loaded priority groups settings")
end
end
-- facility SCRAM command
function process.fac_scram()
self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL)
log.debug("PROCESS: FAC SCRAM ALL")
-- create a handle to process control for usage of commands that get acknowledgements
function process.create_handle()
---@class process_handle
local handle = {}
-- add this handle to the requestors and activate the command if inactive
---@param cmd process_command_state
---@param ack function
local function request(cmd, ack)
local new = not cmd.active
if new then
cmd.active = true
cmd.timeout = util.time_ms() + REQUEST_TIMEOUT_MS
end
table.insert(cmd.requestors, ack)
return new
end
local function u_request(u_id, cmd_id, ack) return request(pctl.commands.unit[u_id][cmd_id], ack) end
local function f_request(cmd_id, ack) return request(pctl.commands.fac[cmd_id], ack) end
--#region Facility Commands
-- facility SCRAM command
function handle.fac_scram()
if f_request(F_CMD.SCRAM_ALL, handle.fac_ack.on_scram) then
pctl.comms.send_fac_command(F_CMD.SCRAM_ALL)
log.debug("PROCESS: FAC SCRAM ALL")
end
end
-- facility alarm acknowledge command
function handle.fac_ack_alarms()
if f_request(F_CMD.ACK_ALL_ALARMS, handle.fac_ack.on_ack_alarms) then
pctl.comms.send_fac_command(F_CMD.ACK_ALL_ALARMS)
log.debug("PROCESS: FAC ACK ALL ALARMS")
end
end
handle.fac_ack = {}
-- luacheck: no unused args
-- facility SCRAM ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function handle.fac_ack.on_scram(success) end
-- facility acknowledge all alarms ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function handle.fac_ack.on_ack_alarms(success) end
-- luacheck: unused args
--#endregion
--#region Unit Commands
-- start a reactor
---@param id integer unit ID
function handle.start(id)
if u_request(id, U_CMD.START, handle.unit_ack[id].on_start) then
pctl.io.units[id].control_state = true
pctl.comms.send_unit_command(U_CMD.START, id)
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
end
end
-- SCRAM reactor
---@param id integer unit ID
function handle.scram(id)
if u_request(id, U_CMD.SCRAM, handle.unit_ack[id].on_scram) then
pctl.io.units[id].control_state = false
pctl.comms.send_unit_command(U_CMD.SCRAM, id)
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
end
end
-- reset reactor protection system
---@param id integer unit ID
function handle.reset_rps(id)
if u_request(id, U_CMD.RESET_RPS, handle.unit_ack[id].on_rps_reset) then
pctl.comms.send_unit_command(U_CMD.RESET_RPS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
end
end
-- acknowledge all alarms
---@param id integer unit ID
function handle.ack_all_alarms(id)
if u_request(id, U_CMD.ACK_ALL_ALARMS, handle.unit_ack[id].on_ack_alarms) then
pctl.comms.send_unit_command(U_CMD.ACK_ALL_ALARMS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
end
end
-- unit command acknowledgement callbacks, indexed by unit ID
---@type process_unit_ack[]
handle.unit_ack = {}
for u = 1, pctl.io.facility.num_units do
handle.unit_ack[u] = {}
---@class process_unit_ack
local u_ack = handle.unit_ack[u]
-- luacheck: no unused args
-- unit start ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function u_ack.on_start(success) end
-- unit SCRAM ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function u_ack.on_scram(success) end
-- unit RPS reset ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function u_ack.on_rps_reset(success) end
-- unit acknowledge all alarms ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function u_ack.on_ack_alarms(success) end
-- luacheck: unused args
end
--#endregion
return handle
end
-- facility alarm acknowledge command
function process.fac_ack_alarms()
self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_ALARMS)
log.debug("PROCESS: FAC ACK ALL ALARMS")
-- clear outstanding process commands that have timed out
function process.clear_timed_out()
local now = util.time_ms()
local objs = { pctl.commands.fac, table.unpack(pctl.commands.unit) }
for _, obj in pairs(objs) do
-- cancel expired requests
for _, cmd in pairs(obj) do
if cmd.active and now > cmd.timeout then
cmd.active = false
cmd.requestors = {}
end
end
end
end
-- start reactor
---@param id integer unit ID
function process.start(id)
self.io.units[id].control_state = true
self.comms.send_unit_command(UNIT_COMMAND.START, id)
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
-- handle a command acknowledgement
---@param cmd_state process_command_state
---@param success boolean if the command was successful
local function cmd_ack(cmd_state, success)
if cmd_state.active then
cmd_state.active = false
-- call all acknowledge callback functions
for i = 1, #cmd_state.requestors do
cmd_state.requestors[i](success)
end
cmd_state.requestors = {}
end
end
-- SCRAM reactor
---@param id integer unit ID
function process.scram(id)
self.io.units[id].control_state = false
self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id)
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
-- handle a facility command acknowledgement
---@param command FAC_COMMAND command
---@param success boolean if the command was successful
function process.fac_ack(command, success)
cmd_ack(pctl.commands.fac[command], success)
end
-- reset reactor protection system
---@param id integer unit ID
function process.reset_rps(id)
self.comms.send_unit_command(UNIT_COMMAND.RESET_RPS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
-- handle a unit command acknowledgement
---@param unit integer unit ID
---@param command UNIT_COMMAND command
---@param success boolean if the command was successful
function process.unit_ack(unit, command, success)
cmd_ack(pctl.commands.unit[unit][command], success)
end
--#region One-Way Commands (no acknowledgements)
-- set burn rate
---@param id integer unit ID
---@param rate number burn rate
function process.set_rate(id, rate)
self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate)
pctl.comms.send_unit_command(U_CMD.SET_BURN, id, rate)
log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
end
@@ -150,36 +340,24 @@ end
---@param mode integer waste mode
function process.set_unit_waste(id, mode)
-- publish so that if it fails then it gets reset
self.io.units[id].unit_ps.publish("U_WasteMode", mode)
pctl.io.units[id].unit_ps.publish("U_WasteMode", mode)
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
pctl.comms.send_unit_command(U_CMD.SET_WASTE, id, mode)
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil
pctl.control_states.waste_modes[id] = mode
settings.set("ControlStates", pctl.control_states)
if type(waste_mode) ~= "table" then waste_mode = {} end
waste_mode[id] = mode
settings.set("WASTE_MODES", waste_mode)
if not settings.save("/coord.settings") then
if not settings.save("/coordinator.settings") then
log.error("process.set_unit_waste(): failed to save coordinator settings file")
end
end
-- acknowledge all alarms
---@param id integer unit ID
function process.ack_all_alarms(id)
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALL_ALARMS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
end
-- acknowledge an alarm
---@param id integer unit ID
---@param alarm integer alarm ID
function process.ack_alarm(id, alarm)
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALARM, id, alarm)
pctl.comms.send_unit_command(U_CMD.ACK_ALARM, id, alarm)
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
end
@@ -187,7 +365,7 @@ end
---@param id integer unit ID
---@param alarm integer alarm ID
function process.reset_alarm(id, alarm)
self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm)
pctl.comms.send_unit_command(U_CMD.RESET_ALARM, id, alarm)
log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
end
@@ -195,77 +373,68 @@ end
---@param unit_id integer unit ID
---@param group_id integer|0 group ID or 0 for independent
function process.set_group(unit_id, group_id)
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, unit_id, group_id)
pctl.comms.send_unit_command(U_CMD.SET_GROUP, unit_id, group_id)
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil
pctl.control_states.priority_groups[unit_id] = group_id
settings.set("ControlStates", pctl.control_states)
if type(prio_groups) ~= "table" then prio_groups = {} end
prio_groups[unit_id] = group_id
settings.set("PRIORITY_GROUPS", prio_groups)
if not settings.save("/coord.settings") then
if not settings.save("/coordinator.settings") then
log.error("process.set_group(): failed to save coordinator settings file")
end
end
--#endregion
--------------------------
-- AUTO PROCESS CONTROL --
--------------------------
-- write auto process control to config file
local function _write_auto_config()
-- attempt to load settings
if not settings.load("/coord.settings") then
log.warning("process._write_auto_config(): failed to load coordinator settings file")
end
-- save config
settings.set("PROCESS", self.config)
local saved = settings.save("/coord.settings")
if not saved then
log.warning("process._write_auto_config(): failed to save coordinator settings file")
end
return not not saved
-- start automatic process control
function process.start_auto()
pctl.comms.send_auto_start(pctl.control_states.process)
log.debug("PROCESS: START AUTO CTL")
end
-- stop automatic process control
function process.stop_auto()
self.comms.send_fac_command(FAC_COMMAND.STOP)
pctl.comms.send_fac_command(F_CMD.STOP)
log.debug("PROCESS: STOP AUTO CTL")
end
-- start automatic process control
function process.start_auto()
self.comms.send_auto_start(self.config)
log.debug("PROCESS: START AUTO CTL")
end
-- set automatic process control waste mode
---@param product WASTE_PRODUCT waste product for auto control
function process.set_process_waste(product)
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, product)
pctl.comms.send_fac_command(F_CMD.SET_WASTE_MODE, product)
log.debug(util.c("PROCESS: SET WASTE ", product))
-- update config table and save
self.config.waste_product = product
pctl.control_states.process.waste_product = product
_write_auto_config()
end
-- set automatic process control plutonium fallback
---@param enabled boolean whether to enable plutonium fallback
function process.set_pu_fallback(enabled)
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, enabled)
pctl.comms.send_fac_command(F_CMD.SET_PU_FB, enabled)
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
-- update config table and save
self.config.pu_fallback = enabled
pctl.control_states.process.pu_fallback = enabled
_write_auto_config()
end
-- set automatic process control SPS usage at low power
---@param enabled boolean whether to enable SPS usage at low power
function process.set_sps_low_power(enabled)
pctl.comms.send_fac_command(F_CMD.SET_SPS_LP, enabled)
log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled))
-- update config table and save
pctl.control_states.process.sps_low_power = enabled
_write_auto_config()
end
@@ -279,14 +448,15 @@ function process.save(mode, burn_target, charge_target, gen_target, limits)
log.debug("PROCESS: SAVE")
-- update config table
self.config.mode = mode
self.config.burn_target = burn_target
self.config.charge_target = charge_target
self.config.gen_target = gen_target
self.config.limits = limits
local ctl_proc = pctl.control_states.process
ctl_proc.mode = mode
ctl_proc.burn_target = burn_target
ctl_proc.charge_target = charge_target
ctl_proc.gen_target = gen_target
ctl_proc.limits = limits
-- save config
self.io.facility.save_cfg_ack(_write_auto_config())
pctl.io.facility.save_cfg_ack(_write_auto_config())
end
-- handle a start command acknowledgement
@@ -294,38 +464,46 @@ end
function process.start_ack_handle(response)
local ack = response[1]
self.config.mode = response[2]
self.config.burn_target = response[3]
self.config.charge_target = response[4]
self.config.gen_target = response[5]
local ctl_proc = pctl.control_states.process
ctl_proc.mode = response[2]
ctl_proc.burn_target = response[3]
ctl_proc.charge_target = response[4]
ctl_proc.gen_target = response[5]
for i = 1, math.min(#response[6], self.io.facility.num_units) do
self.config.limits[i] = response[6][i]
for i = 1, math.min(#response[6], pctl.io.facility.num_units) do
ctl_proc.limits[i] = response[6][i]
local unit = self.io.units[i] ---@type ioctl_unit
unit.unit_ps.publish("burn_limit", self.config.limits[i])
local unit = pctl.io.units[i] ---@type ioctl_unit
unit.unit_ps.publish("burn_limit", ctl_proc.limits[i])
end
self.io.facility.ps.publish("process_mode", self.config.mode)
self.io.facility.ps.publish("process_burn_target", self.config.burn_target)
self.io.facility.ps.publish("process_charge_target", self.config.charge_target)
self.io.facility.ps.publish("process_gen_target", self.config.gen_target)
pctl.io.facility.ps.publish("process_mode", ctl_proc.mode)
pctl.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
pctl.io.facility.ps.publish("process_charge_target", pctl.io.energy_convert_from_fe(ctl_proc.charge_target))
pctl.io.facility.ps.publish("process_gen_target", pctl.io.energy_convert_from_fe(ctl_proc.gen_target))
self.io.facility.start_ack(ack)
pctl.io.facility.start_ack(ack)
end
-- record waste product state after attempting to change it
---@param response WASTE_PRODUCT supervisor waste product state
-- record waste product settting after attempting to change it
---@param response WASTE_PRODUCT supervisor waste product settting
function process.waste_ack_handle(response)
self.config.waste_product = response
self.io.facility.ps.publish("process_waste_product", response)
pctl.control_states.process.waste_product = response
pctl.io.facility.ps.publish("process_waste_product", response)
end
-- record plutonium fallback state after attempting to change it
---@param response boolean supervisor plutonium fallback state
-- record plutonium fallback settting after attempting to change it
---@param response boolean supervisor plutonium fallback settting
function process.pu_fb_ack_handle(response)
self.config.pu_fallback = response
self.io.facility.ps.publish("process_pu_fallback", response)
pctl.control_states.process.pu_fallback = response
pctl.io.facility.ps.publish("process_pu_fallback", response)
end
-- record SPS low power settting after attempting to change it
---@param response boolean supervisor SPS low power settting
function process.sps_lp_ack_handle(response)
pctl.control_states.process.sps_low_power = response
pctl.io.facility.ps.publish("process_sps_low_power", response)
end
return process

View File

@@ -2,29 +2,33 @@
-- Graphics Rendering Control
--
local log = require("scada-common.log")
local util = require("scada-common.util")
local log = require("scada-common.log")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local pgi = require("coordinator.ui.pgi")
local style = require("coordinator.ui.style")
local pgi = require("coordinator.ui.pgi")
local flow_view = require("coordinator.ui.layout.flow_view")
local panel_view = require("coordinator.ui.layout.front_panel")
local main_view = require("coordinator.ui.layout.main_view")
local unit_view = require("coordinator.ui.layout.unit_view")
local flow_view = require("coordinator.ui.layout.flow_view")
local panel_view = require("coordinator.ui.layout.front_panel")
local main_view = require("coordinator.ui.layout.main_view")
local unit_view = require("coordinator.ui.layout.unit_view")
local core = require("graphics.core")
local flasher = require("graphics.flasher")
local core = require("graphics.core")
local flasher = require("graphics.flasher")
local DisplayBox = require("graphics.elements.displaybox")
local DisplayBox = require("graphics.elements.displaybox")
local log_render = coordinator.log_render
---@class coord_renderer
local renderer = {}
-- render engine
local engine = {
color_mode = 1, ---@type COLOR_MODE
monitors = nil, ---@type monitors_struct|nil
dmesg_window = nil, ---@type table|nil
ui_ready = false,
@@ -48,15 +52,34 @@ local function _init_display(monitor)
monitor.setCursorPos(1, 1)
-- set overridden colors
for i = 1, #style.colors do
monitor.setPaletteColor(style.colors[i].c, style.colors[i].hex)
for i = 1, #style.theme.colors do
monitor.setPaletteColor(style.theme.colors[i].c, style.theme.colors[i].hex)
end
-- apply color mode
local c_mode_overrides = style.theme.color_modes[engine.color_mode]
for i = 1, #c_mode_overrides do
monitor.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
end
end
-- disable the flow view
---@param disable boolean
function renderer.legacy_disable_flow_view(disable)
engine.disable_flow_view = disable
-- print out that the monitor is too small
---@param monitor table monitor
local function _print_too_small(monitor)
monitor.setCursorPos(1, 1)
monitor.setBackgroundColor(colors.black)
monitor.setTextColor(colors.red)
monitor.clear()
monitor.write("monitor too small")
end
-- apply renderer configurations
---@param config crd_config
function renderer.configure(config)
style.set_themes(config.MainTheme, config.FrontPanelTheme, config.ColorMode)
engine.color_mode = config.ColorMode
engine.disable_flow_view = config.DisableFlowView
end
-- link to the monitor peripherals
@@ -65,15 +88,15 @@ function renderer.set_displays(monitors)
engine.monitors = monitors
-- report to front panel as connected
iocontrol.fp_monitor_state("main", engine.monitors.primary ~= nil)
iocontrol.fp_monitor_state("main", engine.monitors.main ~= nil)
iocontrol.fp_monitor_state("flow", engine.monitors.flow ~= nil)
for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end
end
-- init all displays in use by the renderer
function renderer.init_displays()
-- init primary and flow monitors
_init_display(engine.monitors.primary)
-- init main and flow monitors
_init_display(engine.monitors.main)
if not engine.disable_flow_view then _init_display(engine.monitors.flow) end
-- init unit displays
@@ -88,48 +111,21 @@ function renderer.init_displays()
term.setCursorPos(1, 1)
-- set overridden colors
for i = 1, #style.fp.colors do
term.setPaletteColor(style.fp.colors[i].c, style.fp.colors[i].hex)
end
end
-- check main display width
---@nodiscard
---@return boolean width_okay
function renderer.validate_main_display_width()
local w, _ = engine.monitors.primary.getSize()
return w == 164
end
-- check flow display width
---@nodiscard
---@return boolean width_okay
function renderer.validate_flow_display_width()
local w, _ = engine.monitors.flow.getSize()
return w == 164
end
-- check display sizes
---@nodiscard
---@return boolean valid all unit display dimensions OK
function renderer.validate_unit_display_sizes()
local valid = true
for id, monitor in ipairs(engine.monitors.unit_displays) do
local w, h = monitor.getSize()
if w ~= 79 or h ~= 52 then
log.warning(util.c("RENDERER: unit ", id, " display resolution not 79 wide by 52 tall: ", w, ", ", h))
valid = false
end
for i = 1, #style.fp_theme.colors do
term.setPaletteColor(style.fp_theme.colors[i].c, style.fp_theme.colors[i].hex)
end
return valid
-- apply color mode
local c_mode_overrides = style.fp_theme.color_modes[engine.color_mode]
for i = 1, #c_mode_overrides do
term.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
end
end
-- initialize the dmesg output window
function renderer.init_dmesg()
local disp_x, disp_y = engine.monitors.primary.getSize()
engine.dmesg_window = window.create(engine.monitors.primary, 1, 1, disp_x, disp_y)
local disp_w, disp_h = engine.monitors.main.getSize()
engine.dmesg_window = window.create(engine.monitors.main, 1, 1, disp_w, disp_h)
log.direct_dmesg(engine.dmesg_window)
end
@@ -176,9 +172,9 @@ function renderer.close_fp()
engine.fp_ready = false
-- restore colors
for i = 1, #style.colors do
local r, g, b = term.nativePaletteColor(style.colors[i].c)
term.setPaletteColor(style.colors[i].c, r, g, b)
for i = 1, #style.fp_theme.colors do
local r, g, b = term.nativePaletteColor(style.fp_theme.colors[i].c)
term.setPaletteColor(style.fp_theme.colors[i].c, r, g, b)
end
-- reset terminal
@@ -200,21 +196,24 @@ function renderer.try_start_ui()
status, msg = pcall(function ()
-- show main view on main monitor
if engine.monitors.primary ~= nil then
engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root}
if engine.monitors.main ~= nil then
engine.ui.main_display = DisplayBox{window=engine.monitors.main,fg_bg=style.root}
main_view(engine.ui.main_display)
util.nop()
end
-- show flow view on flow monitor
if engine.monitors.flow ~= nil then
engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root}
flow_view(engine.ui.flow_display)
util.nop()
end
-- show unit views on unit displays
for idx, display in pairs(engine.monitors.unit_displays) do
engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
unit_view(engine.ui.unit_displays[idx], idx)
util.nop()
end
end)
@@ -255,6 +254,11 @@ function renderer.close_ui()
-- clear unit monitors
for _, monitor in ipairs(engine.monitors.unit_displays) do monitor.clear() end
if not engine.disable_flow_view then
-- clear flow monitor
engine.monitors.flow.clear()
end
-- re-draw dmesg
engine.dmesg_window.setVisible(true)
engine.dmesg_window.redraw()
@@ -276,43 +280,43 @@ function renderer.ui_ready() return engine.ui_ready end
function renderer.handle_disconnect(device)
local is_used = false
if engine.monitors ~= nil then
if engine.monitors.primary == device then
if engine.ui.main_display ~= nil then
-- delete element tree and clear root UI elements
engine.ui.main_display.delete()
end
if not engine.monitors then return false end
is_used = true
engine.monitors.primary = nil
engine.ui.main_display = nil
if engine.monitors.main == device then
if engine.ui.main_display ~= nil then
-- delete element tree and clear root UI elements
engine.ui.main_display.delete()
end
iocontrol.fp_monitor_state("main", false)
elseif engine.monitors.flow == device then
if engine.ui.flow_display ~= nil then
-- delete element tree and clear root UI elements
engine.ui.flow_display.delete()
end
is_used = true
engine.monitors.main = nil
engine.ui.main_display = nil
is_used = true
engine.monitors.flow = nil
engine.ui.flow_display = nil
iocontrol.fp_monitor_state("main", false)
elseif engine.monitors.flow == device then
if engine.ui.flow_display ~= nil then
-- delete element tree and clear root UI elements
engine.ui.flow_display.delete()
end
iocontrol.fp_monitor_state("flow", false)
else
for idx, monitor in pairs(engine.monitors.unit_displays) do
if monitor == device then
if engine.ui.unit_displays[idx] ~= nil then
engine.ui.unit_displays[idx].delete()
end
is_used = true
engine.monitors.flow = nil
engine.ui.flow_display = nil
is_used = true
engine.monitors.unit_displays[idx] = nil
engine.ui.unit_displays[idx] = nil
iocontrol.fp_monitor_state(idx, false)
break
iocontrol.fp_monitor_state("flow", false)
else
for idx, monitor in pairs(engine.monitors.unit_displays) do
if monitor == device then
if engine.ui.unit_displays[idx] ~= nil then
engine.ui.unit_displays[idx].delete()
end
is_used = true
engine.monitors.unit_displays[idx] = nil
engine.ui.unit_displays[idx] = nil
iocontrol.fp_monitor_state(idx, false)
break
end
end
end
@@ -327,52 +331,29 @@ end
function renderer.handle_reconnect(name, device)
local is_used = false
if engine.monitors ~= nil then
if engine.monitors.primary_name == name then
is_used = true
_init_display(device)
engine.monitors.primary = device
if not engine.monitors then return false end
local disp_x, disp_y = engine.monitors.primary.getSize()
engine.dmesg_window.reposition(1, 1, disp_x, disp_y, engine.monitors.primary)
-- 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.ui_ready and (engine.ui.main_display == nil) then
engine.dmesg_window.setVisible(false)
if engine.monitors.main_name == name then
is_used = true
engine.monitors.main = device
engine.ui.main_display = DisplayBox{window=device,fg_bg=style.root}
main_view(engine.ui.main_display)
else
engine.dmesg_window.setVisible(true)
engine.dmesg_window.redraw()
end
renderer.handle_resize(name)
elseif engine.monitors.flow_name == name then
is_used = true
engine.monitors.flow = device
iocontrol.fp_monitor_state("main", true)
elseif engine.monitors.flow_name == name then
is_used = true
_init_display(device)
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
if engine.ui_ready and (engine.ui.flow_display == nil) then
engine.ui.flow_display = DisplayBox{window=device,fg_bg=style.root}
flow_view(engine.ui.flow_display)
end
iocontrol.fp_monitor_state("flow", true)
else
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
if monitor == name then
is_used = true
_init_display(device)
engine.monitors.unit_displays[idx] = device
if engine.ui_ready and (engine.ui.unit_displays[idx] == nil) then
engine.ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
unit_view(engine.ui.unit_displays[idx], idx)
end
iocontrol.fp_monitor_state(idx, true)
break
end
renderer.handle_resize(name)
break
end
end
end
@@ -380,6 +361,142 @@ function renderer.handle_reconnect(name, device)
return is_used
end
-- handle a monitor being resized<br>
-- returns if this monitor is assigned + if the assigned screen still fits
---@param name string monitor name
---@return boolean is_used, boolean is_ok
function renderer.handle_resize(name)
local is_used = false
local is_ok = true
local ui = engine.ui
if not engine.monitors then return false, false end
if engine.monitors.main_name == name and engine.monitors.main then
local device = engine.monitors.main ---@type table
-- this is necessary if the bottom left block was broken and on reconnect
_init_display(device)
is_used = true
-- resize dmesg window if needed, but don't make it thinner
local disp_w, disp_h = engine.monitors.main.getSize()
local dmsg_w, _ = engine.dmesg_window.getSize()
engine.dmesg_window.reposition(1, 1, math.max(disp_w, dmsg_w), disp_h, engine.monitors.main)
if ui.main_display then
ui.main_display.delete()
ui.main_display = nil
end
iocontrol.fp_monitor_state("main", true)
engine.dmesg_window.setVisible(not engine.ui_ready)
if engine.ui_ready then
local draw_start = util.time_ms()
local ok = pcall(function ()
ui.main_display = DisplayBox{window=device,fg_bg=style.root}
main_view(ui.main_display)
end)
if ok then
log_render("main view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
else
if ui.main_display then
ui.main_display.delete()
ui.main_display = nil
end
_print_too_small(device)
iocontrol.fp_monitor_state("main", false)
is_ok = false
end
else engine.dmesg_window.redraw() end
elseif engine.monitors.flow_name == name and engine.monitors.flow then
local device = engine.monitors.flow ---@type table
-- this is necessary if the bottom left block was broken and on reconnect
_init_display(device)
is_used = true
if ui.flow_display then
ui.flow_display.delete()
ui.flow_display = nil
end
iocontrol.fp_monitor_state("flow", true)
if engine.ui_ready then
local draw_start = util.time_ms()
local ok = pcall(function ()
ui.flow_display = DisplayBox{window=device,fg_bg=style.root}
flow_view(ui.flow_display)
end)
if ok then
log_render("flow view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
else
if ui.flow_display then
ui.flow_display.delete()
ui.flow_display = nil
end
_print_too_small(device)
iocontrol.fp_monitor_state("flow", false)
is_ok = false
end
end
else
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
local device = engine.monitors.unit_displays[idx]
if monitor == name and device then
-- this is necessary if the bottom left block was broken and on reconnect
_init_display(device)
is_used = true
if ui.unit_displays[idx] then
ui.unit_displays[idx].delete()
ui.unit_displays[idx] = nil
end
iocontrol.fp_monitor_state(idx, true)
if engine.ui_ready then
local draw_start = util.time_ms()
local ok = pcall(function ()
ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
unit_view(ui.unit_displays[idx], idx)
end)
if ok then
log_render("unit " .. idx .. " view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
else
if ui.unit_displays[idx] then
ui.unit_displays[idx].delete()
ui.unit_displays[idx] = nil
end
_print_too_small(device)
iocontrol.fp_monitor_state(idx, false)
is_ok = false
end
end
break
end
end
end
return is_used, is_ok
end
-- handle a touch event
---@param event mouse_interaction|nil
@@ -388,16 +505,15 @@ 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.primary_name then
engine.ui.main_display.handle_mouse(event)
if event.monitor == engine.monitors.main_name then
if engine.ui.main_display then engine.ui.main_display.handle_mouse(event) end
elseif event.monitor == engine.monitors.flow_name then
engine.ui.flow_display.handle_mouse(event)
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
if event.monitor == monitor then
local layout = engine.ui.unit_displays[id] ---@type graphics_element
layout.handle_mouse(event)
break
local display = engine.ui.unit_displays[id]
if event.monitor == monitor and display then
if display then display.handle_mouse(event) end
end
end
end

View File

@@ -3,7 +3,6 @@ local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util")
local config = require("coordinator.config")
local iocontrol = require("coordinator.iocontrol")
local pocket = require("coordinator.session.pocket")
@@ -11,7 +10,8 @@ local pocket = require("coordinator.session.pocket")
local apisessions = {}
local self = {
nic = nil,
nic = nil, ---@type nic
config = nil, ---@type crd_config
next_id = 0,
sessions = {}
}
@@ -32,7 +32,7 @@ local function _api_handle_outq(session)
if msg ~= nil then
if msg.qtype == mqueue.TYPE.PACKET then
-- handle a packet to be sent
self.nic.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message)
self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message)
elseif msg.qtype == mqueue.TYPE.COMMAND then
-- handle instruction/notification
elseif msg.qtype == mqueue.TYPE.DATA then
@@ -59,7 +59,7 @@ local function _shutdown(session)
while session.out_queue.ready() do
local msg = session.out_queue.pop()
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then
self.nic.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message)
self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message)
end
end
@@ -69,9 +69,11 @@ end
-- PUBLIC FUNCTIONS --
-- initialize apisessions
---@param nic nic
function apisessions.init(nic)
---@param nic nic network interface
---@param config crd_config coordinator config
function apisessions.init(nic, config)
self.nic = nic
self.config = config
end
-- find a session by remote port
@@ -87,10 +89,11 @@ end
-- establish a new API session
---@nodiscard
---@param source_addr integer
---@param version string
---@param source_addr integer pocket computer ID
---@param i_seq_num integer initial (most recent) sequence number
---@param version string pocket version
---@return integer session_id
function apisessions.establish_session(source_addr, version)
function apisessions.establish_session(source_addr, i_seq_num, version)
---@class pkt_session_struct
local pkt_s = {
open = true,
@@ -103,7 +106,7 @@ function apisessions.establish_session(source_addr, version)
local id = self.next_id
pkt_s.instance = pocket.new_session(id, source_addr, pkt_s.in_queue, pkt_s.out_queue, config.API_TIMEOUT)
pkt_s.instance = pocket.new_session(id, source_addr, i_seq_num, pkt_s.in_queue, pkt_s.out_queue, self.config.API_Timeout)
table.insert(self.sessions, pkt_s)
local mt = {

View File

@@ -4,12 +4,15 @@ local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local pocket = {}
local PROTOCOL = comms.PROTOCOL
-- local CRDN_TYPE = comms.CRDN_TYPE
local CRDN_TYPE = comms.CRDN_TYPE
local MGMT_TYPE = comms.MGMT_TYPE
local FAC_COMMAND = comms.FAC_COMMAND
local UNIT_COMMAND = comms.UNIT_COMMAND
-- retry time constants in ms
-- local INITIAL_WAIT = 1500
@@ -32,19 +35,22 @@ local PERIODICS = {
---@nodiscard
---@param id integer session ID
---@param s_addr integer device source address
---@param i_seq_num integer initial sequence number
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
local log_header = "pkt_session(" .. id .. "): "
function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
local log_tag = "pkt_session(" .. id .. "): "
local self = {
-- connection properties
seq_num = 0,
r_seq_num = nil,
seq_num = i_seq_num + 2, -- next after the establish approval was sent
r_seq_num = i_seq_num + 1,
connected = true,
conn_watchdog = util.new_watchdog(timeout),
last_rtt = 0,
-- process accessor handle
proc_handle = process.create_handle(),
-- periodic messages
periodics = {
last_update = 0,
@@ -73,18 +79,18 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
end
-- send a CRDN packet
-----@param msg_type CRDN_TYPE
-----@param msg table
-- local function _send(msg_type, msg)
-- local s_pkt = comms.scada_packet()
-- local c_pkt = comms.crdn_packet()
---@param msg_type CRDN_TYPE
---@param msg table
local function _send(msg_type, msg)
local s_pkt = comms.scada_packet()
local c_pkt = comms.crdn_packet()
-- c_pkt.make(msg_type, msg)
-- s_pkt.make(self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable())
c_pkt.make(msg_type, msg)
s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable())
-- out_queue.push_packet(s_pkt)
-- self.seq_num = self.seq_num + 1
-- end
out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1
end
-- send a SCADA management packet
---@param msg_type MGMT_TYPE
@@ -100,17 +106,27 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
self.seq_num = self.seq_num + 1
end
-- link callback transmissions
self.proc_handle.fac_ack.on_scram = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.SCRAM_ALL, success }) end
self.proc_handle.fac_ack.on_ack_alarms = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.ACK_ALL_ALARMS, success }) end
for u = 1, iocontrol.get_db().facility.num_units do
self.proc_handle.unit_ack[u].on_start = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.START, u, success }) end
self.proc_handle.unit_ack[u].on_scram = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.SCRAM, u, success }) end
self.proc_handle.unit_ack[u].on_rps_reset = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.RESET_RPS, u, success }) end
self.proc_handle.unit_ack[u].on_ack_alarms = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.ACK_ALL_ALARMS, u, success }) end
end
-- handle a packet
---@param pkt mgmt_frame|crdn_frame
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return
else
self.r_seq_num = pkt.scada_frame.seq_num()
self.r_seq_num = pkt.scada_frame.seq_num() + 1
end
-- feed watchdog
@@ -120,10 +136,110 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
if pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
---@cast pkt crdn_frame
local db = iocontrol.get_db()
-- handle packet by type
if pkt.type == nil then
if pkt.type == CRDN_TYPE.FAC_CMD then
if pkt.length >= 1 then
local cmd = pkt.data[1]
if cmd == FAC_COMMAND.SCRAM_ALL then
log.info(log_tag .. "FAC SCRAM ALL")
self.proc_handle.fac_scram()
elseif cmd == FAC_COMMAND.STOP then
elseif cmd == FAC_COMMAND.START then
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
log.info(log_tag .. "FAC ACK ALL ALARMS")
self.proc_handle.fac_ack_alarms()
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
elseif cmd == FAC_COMMAND.SET_PU_FB then
elseif cmd == FAC_COMMAND.SET_SPS_LP then
else
log.debug(log_tag .. "CRDN facility command unknown")
end
else
log.debug(log_tag .. "CRDN facility command packet length mismatch")
end
elseif pkt.type == CRDN_TYPE.UNIT_CMD then
if pkt.length >= 2 then
-- get command and unit id
local cmd = pkt.data[1]
local uid = pkt.data[2]
-- continue if valid unit id
if util.is_int(uid) and uid > 0 and uid <= #db.units then
if cmd == UNIT_COMMAND.SCRAM then
log.info(util.c(log_tag, "UNIT[", uid, "] SCRAM"))
self.proc_handle.scram(uid)
elseif cmd == UNIT_COMMAND.START then
log.info(util.c(log_tag, "UNIT[", uid, "] START"))
self.proc_handle.start(uid)
elseif cmd == UNIT_COMMAND.RESET_RPS then
log.info(util.c(log_tag, "UNIT[", uid, "] RESET RPS"))
self.proc_handle.reset_rps(uid)
elseif cmd == UNIT_COMMAND.SET_BURN then
if pkt.length == 3 then
log.info(util.c(log_tag, "UNIT[", uid, "] SET BURN ", pkt.data[3]))
process.set_rate(uid, pkt.data[3])
else
log.debug(log_tag .. "CRDN unit command burn rate missing option")
end
elseif cmd == UNIT_COMMAND.SET_WASTE then
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
log.info(util.c(log_tag, "UNIT[", uid, "] ACK ALL ALARMS"))
self.proc_handle.ack_all_alarms(uid)
elseif cmd == UNIT_COMMAND.ACK_ALARM then
elseif cmd == UNIT_COMMAND.RESET_ALARM then
elseif cmd == UNIT_COMMAND.SET_GROUP then
else
log.debug(log_tag .. "CRDN unit command unknown")
end
else
log.debug(log_tag .. "CRDN unit command invalid")
end
else
log.debug(log_tag .. "CRDN unit command packet length mismatch")
end
elseif pkt.type == CRDN_TYPE.API_GET_FAC then
local fac = db.facility
local data = {
fac.all_sys_ok,
fac.rtu_count,
fac.radiation,
{ fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated },
{ fac.auto_current_waste_product, fac.auto_pu_fallback_active },
util.table_len(fac.tank_data_tbl),
fac.induction_data_tbl[1] ~= nil,
fac.sps_data_tbl[1] ~= nil,
}
_send(CRDN_TYPE.API_GET_FAC, 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]] ---@type ioctl_unit
if u then
local data = {
u.unit_id,
u.connected,
u.rtu_hw,
u.a_group,
u.alarms,
u.annunciator,
u.reactor_data,
u.boiler_data_tbl,
u.turbine_data_tbl,
u.tank_data_tbl,
u.last_rate_change_ms,
u.turbine_flow_stable
}
_send(CRDN_TYPE.API_GET_UNIT, data)
end
end
else
log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type)
log.debug(log_tag .. "handler received unsupported CRDN packet type " .. pkt.type)
end
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
---@cast pkt mgmt_frame
@@ -136,7 +252,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then
log.warning(log_header .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
log.warning(log_tag .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
end
-- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms")
@@ -144,13 +260,17 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
iocontrol.fp_pkt_rtt(id, self.last_rtt)
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
end
elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session
_close()
elseif pkt.type == MGMT_TYPE.ESTABLISH then
-- something is wrong, kill the session
_close()
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end
end
end
@@ -175,7 +295,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
function public.close()
_close()
_send_mgmt(MGMT_TYPE.CLOSE, {})
log.info(log_header .. "session closed by server")
log.info(log_tag .. "session closed by server")
end
-- iterate the session
@@ -206,14 +326,14 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
-- max 100ms spent processing queue
if util.time() - handle_start > 100 then
log.warning(log_header .. "exceeded 100ms queue process limit")
log.warning(log_tag .. "exceeded 100ms queue process limit")
break
end
end
-- exit if connection was closed
if not self.connected then
log.info(log_header .. "session closed by remote host")
log.info(log_tag .. "session closed by remote host")
return self.connected
end

View File

@@ -7,59 +7,92 @@ require("/initenv").init_env()
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 tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core")
local config = require("coordinator.config")
local configure = require("coordinator.configure")
local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol")
local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder")
local threads = require("coordinator.threads")
local apisessions = require("coordinator.session.apisessions")
local COORDINATOR_VERSION = "v1.5.8"
local COORDINATOR_VERSION = "v1.0.17"
local CHUNK_LOAD_DELAY_S = 30.0
local println = util.println
local println = util.println
local println_ts = util.println_ts
local log_graphics = coordinator.log_graphics
local log_sys = coordinator.log_sys
local log_boot = coordinator.log_boot
local log_comms = coordinator.log_comms
local log_render = coordinator.log_render
local log_sys = coordinator.log_sys
local log_boot = coordinator.log_boot
local log_comms = coordinator.log_comms
local log_crypto = coordinator.log_crypto
----------------------------------------
-- config validation
-- get configuration
----------------------------------------
local cfv = util.new_validator()
-- mount connected devices (required for monitor setup)
ppm.mount_all()
cfv.assert_channel(config.SVR_CHANNEL)
cfv.assert_channel(config.CRD_CHANNEL)
cfv.assert_channel(config.PKT_CHANNEL)
cfv.assert_type_int(config.TRUSTED_RANGE)
cfv.assert_type_num(config.SV_TIMEOUT)
cfv.assert_min(config.SV_TIMEOUT, 2)
cfv.assert_type_num(config.API_TIMEOUT)
cfv.assert_min(config.API_TIMEOUT, 2)
cfv.assert_type_int(config.NUM_UNITS)
cfv.assert_type_num(config.SOUNDER_VOLUME)
cfv.assert_type_bool(config.TIME_24_HOUR)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
local wait_on_load = true
local loaded, monitors = coordinator.load_config()
assert(cfv.valid(), "bad config file: missing/invalid fields")
-- if the computer just started, its chunk may have just loaded (...or the user rebooted)
-- if monitor config failed, maybe an adjacent chunk containing all or part of a monitor has not loaded yet, so keep trying
while wait_on_load and loaded == 2 and os.clock() < CHUNK_LOAD_DELAY_S do
term.clear()
term.setCursorPos(1, 1)
println("There was a monitor configuration problem at boot.\n")
println("Startup will keep trying every 2s in case of chunk load delays.\n")
println(util.sprintf("The configurator will be started in %ds if all attempts fail.\n", math.max(0, CHUNK_LOAD_DELAY_S - os.clock())))
println("(click to skip to the configurator)")
local timer_id = util.start_timer(2)
while true do
local event, param1 = util.pull_event()
if event == "timer" and param1 == timer_id then
-- remount and re-attempt
ppm.mount_all()
loaded, monitors = coordinator.load_config()
break
elseif event == "mouse_click" or event == "terminate" then
wait_on_load = false
break
end
end
end
if loaded ~= 0 then
-- try to reconfigure (user action)
local success, error = configure.configure(loaded, monitors)
if success then
loaded, monitors = coordinator.load_config()
if loaded ~= 0 then
println(util.trinary(loaded == 2, "monitor configuration invalid", "failed to load a valid configuration") .. ", please reconfigure")
return
end
else
println("configuration error: " .. error)
return
end
end
-- passed checks, good now
---@cast monitors monitors_struct
local config = coordinator.config
----------------------------------------
-- log init
----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
log.init(config.LogPath, config.LogMode, config.LogDebug)
log.info("========================================")
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
@@ -67,6 +100,7 @@ log.info("========================================")
println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<")
crash.set_env("coordinator", COORDINATOR_VERSION)
crash.dbg_log_env()
----------------------------------------
-- main application
@@ -77,54 +111,73 @@ local function main()
-- system startup
----------------------------------------
-- mount connected devices
ppm.mount_all()
-- 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)
-- setup monitors
local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS, config.DISABLE_FLOW_VIEW == true)
if not configured or monitors == nil then
println("startup> monitor setup failed")
log.fatal("monitor configuration failed")
return
end
-- init renderer
renderer.legacy_disable_flow_view(config.DISABLE_FLOW_VIEW == true)
renderer.configure(config)
renderer.set_displays(monitors)
renderer.init_displays()
if not renderer.validate_main_display_width() then
println("startup> main display must be 8 blocks wide")
log.fatal("main display not wide enough")
return
elseif (config.DISABLE_FLOW_VIEW ~= true) and not renderer.validate_flow_display_width() then
println("startup> flow display must be 8 blocks wide")
log.fatal("flow display not wide enough")
return
elseif not renderer.validate_unit_display_sizes() then
println("startup> one or more unit display dimensions incorrect; they must be 4x4 blocks")
log.fatal("unit display dimensions incorrect")
return
end
renderer.init_dmesg()
-- lets get started!
log.info("monitors ready, dmesg output incoming...")
log_graphics("displays connected and reset")
log_render("displays connected and reset")
log_sys("system start on " .. os.date("%c"))
log_boot("starting " .. COORDINATOR_VERSION)
----------------------------------------
-- memory allocation
----------------------------------------
-- shared memory across threads
---@class crd_shared_memory
local __shared_memory = {
-- time and date format for display
date_format = util.trinary(config.Time24Hour, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y"),
-- coordinator system state flags
---@class crd_state
crd_state = {
fp_ok = false,
ui_ok = true, -- default true, used to abort on fail
link_fail = false,
shutdown = false
},
-- core coordinator devices
crd_dev = {
modem = ppm.get_wireless_modem(),
speaker = ppm.get_device("speaker")
},
-- system objects
crd_sys = {
nic = nil, ---@type nic
coord_comms = nil, ---@type coord_comms
conn_watchdog = nil ---@type watchdog
},
-- message queues
q = {
mq_render = mqueue.new()
}
}
local smem_dev = __shared_memory.crd_dev
local smem_sys = __shared_memory.crd_sys
local crd_state = __shared_memory.crd_state
----------------------------------------
-- setup alarm sounder subsystem
----------------------------------------
local speaker = ppm.get_device("speaker")
if speaker == nil then
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")
@@ -132,7 +185,7 @@ local function main()
else
local sounder_start = util.time_ms()
log_boot("annunciator alarm speaker connected")
sounder.init(speaker, config.SOUNDER_VOLUME)
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)
@@ -143,14 +196,13 @@ local function main()
----------------------------------------
-- message authentication init
if type(config.AUTH_KEY) == "string" then
local init_time = network.init_mac(config.AUTH_KEY)
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
local init_time = network.init_mac(config.AuthKey)
log_crypto("HMAC init took " .. init_time .. "ms")
end
-- get the communications modem
local modem = ppm.get_wireless_modem()
if modem == nil then
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")
@@ -161,239 +213,54 @@ local function main()
end
-- create connection watchdog
local conn_watchdog = util.new_watchdog(config.SV_TIMEOUT)
conn_watchdog.cancel()
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
local nic = network.nic(modem)
local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, config.NUM_UNITS, config.CRD_CHANNEL,
config.SVR_CHANNEL, config.PKT_CHANNEL, config.TRUSTED_RANGE, conn_watchdog)
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")
-- base loop clock (2Hz, 10 ticks)
local MAIN_CLOCK = 0.5
local loop_clock = util.new_clock(MAIN_CLOCK)
----------------------------------------
-- start front panel & UI start function
-- start front panel
----------------------------------------
log_graphics("starting front panel UI...")
log_render("starting front panel UI...")
local fp_ok, fp_message = renderer.try_start_fp()
if not fp_ok then
log_graphics(util.c("front panel UI error: ", fp_message))
local fp_message
crd_state.fp_ok, fp_message = renderer.try_start_fp()
if not crd_state.fp_ok then
log_render(util.c("front panel UI error: ", fp_message))
println_ts("front panel UI creation failed")
log.fatal(util.c("front panel GUI render failed with error ", fp_message))
return
else log_graphics("front panel ready") end
-- start up the main UI
---@return boolean ui_ok started ok
local function start_main_ui()
log_graphics("starting main UI...")
local draw_start = util.time_ms()
local ui_ok, ui_message = renderer.try_start_ui()
if not ui_ok then
log_graphics(util.c("main UI error: ", ui_message))
log.fatal(util.c("main GUI render failed with error ", ui_message))
else
log_graphics("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
end
return ui_ok
end
else log_render("front panel ready") end
----------------------------------------
-- main event loop
-- start system
----------------------------------------
local link_failed = false
local ui_ok = true
local date_format = util.trinary(config.TIME_24_HOUR, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y")
-- init threads
local main_thread = threads.thread__main(__shared_memory)
local render_thread = threads.thread__render(__shared_memory)
-- start clock
loop_clock.start()
log.info("startup> completed")
log_sys("system started successfully")
-- main event loop
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event
if event == "peripheral_detach" then
local type, device = ppm.handle_unmount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
-- we only really care if this is our wireless modem
-- if it is another modem, handle other peripheral losses separately
if nic.is_modem(device) then
nic.disconnect()
log_sys("comms modem disconnected")
local other_modem = ppm.get_wireless_modem()
if other_modem then
log_sys("found another wireless modem, using it for comms")
nic.connect(other_modem)
else
-- close out main UI
renderer.close_ui()
-- alert user to status
log_sys("awaiting comms modem reconnect...")
iocontrol.fp_has_modem(false)
end
else
log_sys("non-comms modem disconnected")
end
elseif type == "monitor" then
if renderer.handle_disconnect(device) then
log_sys("lost a configured monitor")
else
log_sys("lost an unused monitor")
end
elseif type == "speaker" then
log_sys("lost alarm sounder speaker")
iocontrol.fp_has_speaker(false)
end
end
elseif event == "peripheral" then
local type, device = ppm.mount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
if device.isWireless() and not nic.is_connected() then
-- reconnected modem
log_sys("comms modem reconnected")
nic.connect(device)
iocontrol.fp_has_modem(true)
elseif device.isWireless() then
log.info("unused wireless modem reconnected")
else
log_sys("wired modem reconnected")
end
elseif type == "monitor" then
if renderer.handle_reconnect(param1, device) then
log_sys(util.c("configured monitor ", param1, " reconnected"))
else
log_sys(util.c("unused monitor ", param1, " connected"))
end
elseif type == "speaker" then
log_sys("alarm sounder speaker reconnected")
sounder.reconnect(device)
iocontrol.fp_has_speaker(true)
end
end
elseif event == "timer" then
if loop_clock.is_clock(param1) then
-- main loop tick
-- toggle heartbeat
iocontrol.heartbeat()
-- maintain connection
if nic.is_connected() then
local ok, start_ui = coord_comms.try_connect()
if not ok then
link_failed = true
log_sys("supervisor connection failed, shutting down...")
log.fatal("failed to connect to supervisor")
break
elseif start_ui then
log_sys("supervisor connected, proceeding to main UI start")
ui_ok = start_main_ui()
if not ui_ok then break end
end
end
-- iterate sessions
apisessions.iterate_all()
-- free any closed sessions
apisessions.free_all_closed()
-- update date and time string for main display
if coord_comms.is_linked() then
iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format))
end
loop_clock.start()
elseif conn_watchdog.is_timer(param1) then
-- supervisor watchdog timeout
log_comms("supervisor server timeout")
-- close connection, main UI, and stop sounder
coord_comms.close()
renderer.close_ui()
sounder.stop()
else
-- a non-clock/main watchdog timer event
-- check API watchdogs
apisessions.check_all_watchdogs(param1)
-- notify timer callback dispatcher
tcd.handle(param1)
end
elseif event == "modem_message" then
-- got a packet
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
-- handle then check if it was a disconnect
if coord_comms.handle_packet(packet) then
log_comms("supervisor closed connection")
-- close connection, main UI, and stop sounder
coord_comms.close()
renderer.close_ui()
sounder.stop()
end
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
-- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
elseif event == "speaker_audio_empty" then
-- handle speaker buffer emptied
sounder.continue()
end
-- check for termination request
if event == "terminate" or ppm.should_terminate() then
-- handle supervisor connection
coord_comms.try_connect(true)
if coord_comms.is_linked() then
log_comms("terminate requested, closing supervisor connection...")
else link_failed = true end
coord_comms.close()
log_comms("supervisor connection closed")
-- handle API sessions
log_comms("closing api sessions...")
apisessions.close_all()
log_comms("api sessions closed")
break
end
end
-- run threads
parallel.waitForAll(main_thread.p_exec, render_thread.p_exec)
renderer.close_ui()
renderer.close_fp()
sounder.stop()
log_sys("system shutdown")
if link_failed then println_ts("failed to connect to supervisor") end
if not ui_ok then println_ts("main UI creation failed") end
if crd_state.link_fail then println_ts("failed to connect to supervisor") end
if not crd_state.ui_ok then println_ts("main UI creation failed") end
-- close on error exit (such as UI error)
if coord_comms.is_linked() then coord_comms.close() end
if smem_sys.coord_comms.is_linked() then smem_sys.coord_comms.close() end
println_ts("exited")
log.info("exited")

367
coordinator/threads.lua Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -29,16 +29,6 @@ local cpair = core.cpair
local border = core.border
local bw_fg_bg = style.bw_fg_bg
local lu_cpair = style.lu_colors
local hzd_fg_bg = style.hzd_fg_bg
local dis_colors = style.dis_colors
local gry_wht = style.gray_white
local ind_grn = style.ind_grn
local ind_yel = style.ind_yel
local ind_red = style.ind_red
local ind_wht = style.ind_wht
local period = core.flasher.PERIOD
@@ -47,25 +37,40 @@ local period = core.flasher.PERIOD
---@param x integer top left x
---@param y integer top left y
local function new_view(root, x, y)
local s_hi_box = style.theme.highlight_box
local s_field = style.theme.field_box
local lu_cpair = style.lu_colors
local hzd_fg_bg = style.hzd_fg_bg
local dis_colors = style.dis_colors
local arrow_fg_bg = cpair(style.theme.label, s_hi_box.bkg)
local ind_grn = style.ind_grn
local ind_yel = style.ind_yel
local ind_red = style.ind_red
local ind_wht = style.ind_wht
assert(root.get_height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
local black = cpair(colors.black, colors.black)
local blk_brn = cpair(colors.black, colors.brown)
local blk_pur = cpair(colors.black, colors.purple)
local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units
local db = iocontrol.get_db()
local facility = db.facility
local units = db.units
local main = Div{parent=root,width=128,height=24,x=x,y=y}
local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,fg_bg=hzd_fg_bg}
local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=db.process.fac_scram,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=db.process.fac_ack_alarms,fg_bg=hzd_fg_bg}
facility.scram_ack = scram.on_response
facility.ack_alarms_ack = ack_a.on_response
db.process.fac_ack.on_scram = scram.on_response
db.process.fac_ack.on_ack_alarms = ack_a.on_response
local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=ind_grn}
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=style.ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd}
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=ind_grn}
local sps = IndicatorLight{parent=main,label="SPS Connected",colors=ind_grn}
@@ -102,12 +107,12 @@ local function new_view(root, x, y)
fac_rad_h.register(facility.ps, "as_radiation", fac_rad_h.update)
gen_fault.register(facility.ps, "as_gen_fault", gen_fault.update)
TextBox{parent=main,y=23,text="Radiation",height=1,width=13,fg_bg=style.label}
local radiation = RadIndicator{parent=main,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
TextBox{parent=main,y=23,text="Radiation",width=13,fg_bg=style.label}
local radiation = RadIndicator{parent=main,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=s_field}
radiation.register(facility.ps, "radiation", radiation.update)
TextBox{parent=main,x=15,y=23,text="Linked RTUs",height=1,width=11,fg_bg=style.label}
local rtu_count = DataIndicator{parent=main,x=15,y=24,label="",format="%11d",value=0,lu_colors=lu_cpair,width=11,fg_bg=bw_fg_bg}
TextBox{parent=main,x=15,y=23,text="Linked RTUs",width=11,fg_bg=style.label}
local rtu_count = DataIndicator{parent=main,x=15,y=24,label="",format="%11d",value=0,lu_colors=lu_cpair,width=11,fg_bg=s_field}
rtu_count.register(facility.ps, "rtu_count", rtu_count.update)
---------------------
@@ -125,9 +130,9 @@ local function new_view(root, x, y)
local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=blk_pur}
TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2}
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=gry_wht}
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
TextBox{parent=burn_target,x=18,y=2,text="mB/t"}
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=s_hi_box}
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=burn_target,x=18,y=2,text="mB/t",fg_bg=style.theme.label_fg}
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
b_target.register(facility.ps, "process_burn_target", b_target.set_value)
@@ -136,24 +141,24 @@ local function new_view(root, x, y)
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=blk_pur}
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=gry_wht}
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
TextBox{parent=chg_target,x=18,y=2,text="MFE"}
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=s_hi_box}
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=chg_target,x=18,y=2,text="M"..db.energy_label,fg_bg=style.theme.label_fg}
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="M"..db.energy_label,commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
c_target.register(facility.ps, "process_charge_target", c_target.set_value)
cur_charge.register(facility.induction_ps_tbl[1], "energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end)
cur_charge.register(facility.induction_ps_tbl[1], "avg_charge", function (fe) cur_charge.update(db.energy_convert_from_fe(fe) / 1000000) end)
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur}
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=gry_wht}
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
TextBox{parent=gen_target,x=18,y=2,text="kFE/t"}
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=s_hi_box}
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=gen_target,x=18,y=2,text="k"..db.energy_label.."/t",fg_bg=style.theme.label_fg}
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="k"..db.energy_label.."/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
g_target.register(facility.ps, "process_gen_target", g_target.set_value)
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end)
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(db.energy_convert(j) / 1000)) end)
-----------------
-- unit limits --
@@ -165,17 +170,17 @@ local function new_view(root, x, y)
for i = 1, 4 do
local unit
local tag_fg_bg = gry_wht
local lim_fg_bg = style.lg_white
local ctl_fg = colors.lightGray
local cur_fg_bg = style.lg_white
local cur_lu = colors.lightGray
local tag_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
local lim_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
local label_fg = style.theme.disabled_fg
local cur_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
local cur_lu = style.theme.disabled
if i <= facility.num_units then
unit = units[i] ---@type ioctl_unit
tag_fg_bg = cpair(colors.black,colors.lightBlue)
lim_fg_bg = bw_fg_bg
ctl_fg = colors.gray
tag_fg_bg = cpair(colors.black, colors.lightBlue)
lim_fg_bg = s_hi_box
label_fg = style.theme.label_fg
cur_fg_bg = blk_brn
cur_lu = colors.black
end
@@ -185,9 +190,9 @@ local function new_view(root, x, y)
local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2}
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(ctl_fg,colors.white)}
local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=lim_fg_bg}
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1}
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=s_hi_box}
local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled,fg_bg=lim_fg_bg}
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,fg_bg=label_fg}
local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(cur_lu,cur_lu),width=14,fg_bg=cur_fg_bg}
@@ -209,14 +214,14 @@ local function new_view(root, x, y)
local stat_div = Div{parent=proc,width=22,height=24,x=57,y=6}
for i = 1, 4 do
local tag_fg_bg = gry_wht
local ind_fg_bg = style.lg_white
local ind_off = colors.lightGray
local tag_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
local ind_fg_bg = cpair(style.theme.disabled, s_hi_box.bkg)
local ind_off = style.theme.disabled
if i <= facility.num_units then
tag_fg_bg = cpair(colors.black, colors.cyan)
ind_fg_bg = bw_fg_bg
ind_off = colors.gray
ind_fg_bg = cpair(style.theme.text, s_hi_box.bkg)
ind_off = style.ind_hi_box_bg
end
local _y = ((i - 1) * 5) + 1
@@ -225,8 +230,8 @@ local function new_view(root, x, y)
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Status",width=7,height=2}
local lights = Div{parent=stat_div,x=9,y=_y,width=14,height=4,fg_bg=ind_fg_bg}
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(colors.green,ind_off)}
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(colors.red,ind_off),flash=true,period=period.BLINK_250_MS}
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(ind_grn.fgd,ind_off)}
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(ind_red.fgd,ind_off),flash=true,period=period.BLINK_250_MS}
if i <= facility.num_units then
local unit = units[i] ---@type ioctl_unit
@@ -241,25 +246,28 @@ 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(colors.gray,colors.white),select_color=colors.purple}
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
mode.register(facility.ps, "process_mode", mode.set_value)
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=gry_wht}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.white)}
stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value)
stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value)
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=gry_wht}
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=s_hi_box}
-- save the automatic process control configuration without starting
local function _save_cfg()
local limits = {}
for i = 1, #rate_limits do limits[i] = rate_limits[i].get_value() end
process.save(mode.get_value(), b_target.get_value(), c_target.get_value(), g_target.get_value(), limits)
process.save(mode.get_value(), b_target.get_value(),
db.energy_convert_to_fe(c_target.get_value()),
db.energy_convert_to_fe(g_target.get_value()),
limits)
end
-- start automatic control after saving process control settings
@@ -317,7 +325,7 @@ local function new_view(root, x, y)
for i = 1, facility.num_units do
local unit = units[i] ---@type ioctl_unit
TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8,height=1}
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}
@@ -327,40 +335,36 @@ local function new_view(root, x, y)
local waste_sel = Div{parent=proc,width=21,height=24,x=81,y=1}
TextBox{parent=waste_sel,text=" ",width=21,height=1,x=1,y=1,fg_bg=blk_brn}
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.brown)}
local cutout_fg_bg = cpair(style.theme.bg, colors.brown)
TextBox{parent=waste_sel,text=" ",width=21,x=1,y=1,fg_bg=cutout_fg_bg}
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,x=1,y=2,fg_bg=cutout_fg_bg}
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17}
status.register(facility.ps, "current_waste_product", status.update)
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.gray,colors.white),select_color=colors.brown}
local pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.green,colors.black)}
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}
waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value)
pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value)
local fb_active = IndicatorLight{parent=rect,x=2,y=9,label="Fallback Active",colors=ind_wht}
local fb_active = IndicatorLight{parent=rect,x=2,y=7,label="Fallback Active",colors=ind_wht}
local sps_disabled = IndicatorLight{parent=rect,x=2,y=8,label="SPS Disabled LC",colors=ind_yel}
fb_active.register(facility.ps, "pu_fallback_active", fb_active.update)
sps_disabled.register(facility.ps, "sps_disabled_low_power", sps_disabled.update)
TextBox{parent=rect,x=2,y=11,text="Plutonium Rate",height=1,width=17,fg_bg=style.label}
local pu_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
local pu_fallback = Checkbox{parent=rect,x=2,y=10,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.brown,style.theme.checkbox_bg)}
TextBox{parent=rect,x=2,y=14,text="Polonium Rate",height=1,width=17,fg_bg=style.label}
local po_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
TextBox{parent=rect,x=2,y=12,height=3,text="Switch to Pu when SNAs cannot keep up with waste.",fg_bg=style.label}
TextBox{parent=rect,x=2,y=17,text="Antimatter Rate",height=1,width=17,fg_bg=style.label}
local am_rate = DataIndicator{parent=rect,x=2,label="",unit="\xb5B/t",format="%12d",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
local lc_sps = Checkbox{parent=rect,x=2,y=16,label="Low Charge SPS",callback=process.set_sps_low_power,box_fg_bg=cpair(colors.brown,style.theme.checkbox_bg)}
pu_rate.register(facility.ps, "pu_rate", pu_rate.update)
po_rate.register(facility.ps, "po_rate", po_rate.update)
am_rate.register(facility.ps, "am_rate", am_rate.update)
TextBox{parent=rect,x=2,y=18,height=3,text="Use SPS at low charge, otherwise switches to Po.",fg_bg=style.label}
local sna_count = DataIndicator{parent=rect,x=2,y=20,label="Linked SNAs:",format="%4d",value=0,lu_colors=lu_cpair,width=17}
sna_count.register(facility.ps, "sna_count", sna_count.update)
pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value)
lc_sps.register(facility.ps, "process_sps_low_power", lc_sps.set_value)
end
return new_view

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
--
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
@@ -28,31 +29,41 @@ local PushButton = require("graphics.elements.controls.push_button")
local RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
local AUTO_GROUP = types.AUTO_GROUP
local ALIGN = core.ALIGN
local cpair = core.cpair
local border = core.border
local bw_fg_bg = style.bw_fg_bg
local lu_cpair = style.lu_colors
local hzd_fg_bg = style.hzd_fg_bg
local dis_colors = style.dis_colors
local gry_wht = style.gray_white
local ind_grn = style.ind_grn
local ind_yel = style.ind_yel
local ind_red = style.ind_red
local ind_wht = style.ind_wht
local period = core.flasher.PERIOD
-- create a unit view
---@param parent graphics_element parent
---@param id integer
local function init(parent, id)
local unit = iocontrol.get_db().units[id] ---@type ioctl_unit
local f_ps = iocontrol.get_db().facility.ps
local s_hi_box = style.theme.highlight_box
local s_hi_bright = style.theme.highlight_box_bright
local s_field = style.theme.field_box
local hc_text = style.hc_text
local lu_cpair = style.lu_colors
local hzd_fg_bg = style.hzd_fg_bg
local dis_colors = style.dis_colors
local arrow_fg_bg = cpair(style.theme.label, s_hi_box.bkg)
local ind_bkg = style.ind_bkg
local ind_grn = style.ind_grn
local ind_yel = style.ind_yel
local ind_red = style.ind_red
local ind_wht = style.ind_wht
local db = iocontrol.get_db()
local unit = db.units[id] ---@type ioctl_unit
local f_ps = db.facility.ps
local main = Div{parent=parent,x=1,y=1}
@@ -62,7 +73,7 @@ local function init(parent, id)
local b_ps = unit.boiler_ps_tbl
local t_ps = unit.turbine_ps_tbl
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,fg_bg=style.theme.header}
-----------------------------
-- main stats and core map --
@@ -72,22 +83,22 @@ local function init(parent, id)
core_map.register(u_ps, "temp", core_map.update)
core_map.register(u_ps, "size", function (s) core_map.resize(s[1], s[2]) end)
TextBox{parent=main,x=12,y=22,text="Heating Rate",height=1,width=12,fg_bg=style.label}
local heating_r = DataIndicator{parent=main,x=12,label="",format="%14.0f",value=0,unit="mB/t",commas=true,lu_colors=lu_cpair,width=19,fg_bg=bw_fg_bg}
TextBox{parent=main,x=12,y=22,text="Heating Rate",width=12,fg_bg=style.label}
local heating_r = DataIndicator{parent=main,x=12,label="",format="%14.0f",value=0,unit="mB/t",commas=true,lu_colors=lu_cpair,width=19,fg_bg=s_field}
heating_r.register(u_ps, "heating_rate", heating_r.update)
TextBox{parent=main,x=12,y=25,text="Commanded Burn Rate",height=1,width=19,fg_bg=style.label}
local burn_r = DataIndicator{parent=main,x=12,label="",format="%14.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=19,fg_bg=bw_fg_bg}
TextBox{parent=main,x=12,y=25,text="Commanded Burn Rate",width=19,fg_bg=style.label}
local burn_r = DataIndicator{parent=main,x=12,label="",format="%14.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=19,fg_bg=s_field}
burn_r.register(u_ps, "burn_rate", burn_r.update)
TextBox{parent=main,text="F",x=2,y=22,width=1,height=1,fg_bg=style.label}
TextBox{parent=main,text="C",x=4,y=22,width=1,height=1,fg_bg=style.label}
TextBox{parent=main,text="\x1a",x=6,y=24,width=1,height=1,fg_bg=style.label}
TextBox{parent=main,text="\x1a",x=6,y=25,width=1,height=1,fg_bg=style.label}
TextBox{parent=main,text="H",x=8,y=22,width=1,height=1,fg_bg=style.label}
TextBox{parent=main,text="W",x=10,y=22,width=1,height=1,fg_bg=style.label}
TextBox{parent=main,text="F",x=2,y=22,width=1,fg_bg=style.label}
TextBox{parent=main,text="C",x=4,y=22,width=1,fg_bg=style.label}
TextBox{parent=main,text="\x1a",x=6,y=24,width=1,fg_bg=style.label}
TextBox{parent=main,text="\x1a",x=6,y=25,width=1,fg_bg=style.label}
TextBox{parent=main,text="H",x=8,y=22,width=1,fg_bg=style.label}
TextBox{parent=main,text="W",x=10,y=22,width=1,fg_bg=style.label}
local fuel = VerticalBar{parent=main,x=2,y=23,fg_bg=cpair(colors.black,colors.gray),height=4,width=1}
local fuel = VerticalBar{parent=main,x=2,y=23,fg_bg=cpair(style.theme.fuel_color,colors.gray),height=4,width=1}
local ccool = VerticalBar{parent=main,x=4,y=23,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1}
local hcool = VerticalBar{parent=main,x=8,y=23,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
local waste = VerticalBar{parent=main,x=10,y=23,fg_bg=cpair(colors.brown,colors.gray),height=4,width=1}
@@ -113,20 +124,21 @@ local function init(parent, id)
end
end)
TextBox{parent=main,x=32,y=22,text="Core Temp",height=1,width=9,fg_bg=style.label}
local core_temp = DataIndicator{parent=main,x=32,label="",format="%11.2f",value=0,unit="K",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
core_temp.register(u_ps, "temp", core_temp.update)
TextBox{parent=main,x=32,y=22,text="Core Temp",width=9,fg_bg=style.label}
local fmt = util.trinary(string.len(db.temp_label) == 2, "%10.2f", "%11.2f")
local core_temp = DataIndicator{parent=main,x=32,label="",format=fmt,value=0,commas=true,unit=db.temp_label,lu_colors=lu_cpair,width=13,fg_bg=s_field}
core_temp.register(u_ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end)
TextBox{parent=main,x=32,y=25,text="Burn Rate",height=1,width=9,fg_bg=style.label}
local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
TextBox{parent=main,x=32,y=25,text="Burn Rate",width=9,fg_bg=style.label}
local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=s_field}
act_burn_r.register(u_ps, "act_burn_rate", act_burn_r.update)
TextBox{parent=main,x=32,y=28,text="Damage",height=1,width=6,fg_bg=style.label}
local damage_p = DataIndicator{parent=main,x=32,label="",format="%11.0f",value=0,unit="%",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
TextBox{parent=main,x=32,y=28,text="Damage",width=6,fg_bg=style.label}
local damage_p = DataIndicator{parent=main,x=32,label="",format="%11.0f",value=0,unit="%",lu_colors=lu_cpair,width=13,fg_bg=s_field}
damage_p.register(u_ps, "damage", damage_p.update)
TextBox{parent=main,x=32,y=31,text="Radiation",height=1,width=21,fg_bg=style.label}
local radiation = RadIndicator{parent=main,x=32,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
TextBox{parent=main,x=32,y=31,text="Radiation",width=21,fg_bg=style.label}
local radiation = RadIndicator{parent=main,x=32,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=s_field}
radiation.register(u_ps, "radiation", radiation.update)
-------------------
@@ -134,8 +146,8 @@ local function init(parent, id)
-------------------
local u_stat = Rectangle{parent=main,border=border(1,colors.gray,true),thin=true,width=33,height=4,x=46,y=3,fg_bg=bw_fg_bg}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=ALIGN.CENTER,fg_bg=gry_wht}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,alignment=ALIGN.CENTER,fg_bg=gry_wht}
stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value)
stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value)
@@ -149,9 +161,9 @@ local function init(parent, id)
local annunciator = Div{parent=main,width=23,height=18,x=22,y=3}
-- connectivity
local plc_online = IndicatorLight{parent=annunciator,label="PLC Online",colors=cpair(colors.green,colors.red)}
local plc_online = IndicatorLight{parent=annunciator,label="PLC Online",colors=cpair(ind_grn.fgd,ind_red.fgd)}
local plc_hbeat = IndicatorLight{parent=annunciator,label="PLC Heartbeat",colors=ind_wht}
local rad_mon = TriIndicatorLight{parent=annunciator,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
local rad_mon = TriIndicatorLight{parent=annunciator,label="Radiation Monitor",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd}
plc_online.register(u_ps, "PLCOnline", plc_online.update)
plc_hbeat.register(u_ps, "PLCHeartbeat", plc_hbeat.update)
@@ -173,7 +185,7 @@ local function init(parent, id)
local rad_wrn = IndicatorLight{parent=annunciator,label="Radiation Warning",colors=ind_yel}
local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=ind_red}
local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=ind_yel}
local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=ind_yel}
local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=ind_yel}
local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=ind_red}
local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=ind_yel}
local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=ind_yel}
@@ -195,7 +207,7 @@ local function init(parent, id)
-- RPS annunciator panel
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=8}
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,x=46,y=8}
local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9}
local rps_annunc = Div{parent=rps,width=31,height=10,x=2,y=1}
@@ -208,7 +220,7 @@ local function init(parent, id)
local rps_loc = IndicatorLight{parent=rps_annunc,label="Coolant Level Low Low",colors=ind_yel}
local rps_flt = IndicatorLight{parent=rps_annunc,label="PPM Fault",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
local rps_tmo = IndicatorLight{parent=rps_annunc,label="Connection Timeout",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
local rps_sfl = IndicatorLight{parent=rps_annunc,label="System Failure",colors=cpair(colors.orange,colors.gray),flash=true,period=period.BLINK_500_MS}
local rps_sfl = IndicatorLight{parent=rps_annunc,label="System Failure",colors=ind_red,flash=true,period=period.BLINK_500_MS}
rps_trp.register(u_ps, "rps_tripped", rps_trp.update)
rps_dmg.register(u_ps, "high_dmg", rps_dmg.update)
@@ -223,13 +235,13 @@ local function init(parent, id)
-- cooling annunciator panel
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=22}
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,x=46,y=22}
local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23}
local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1}
local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7}
local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=ind_yel}
local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=colors.gray,c2=colors.white,c3=colors.green}
local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=ind_bkg,c2=ind_wht.fgd,c3=ind_grn.fgd}
local c_cfm = IndicatorLight{parent=rcs_annunc,label="Coolant Feed Mismatch",colors=ind_yel}
local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=ind_yel}
local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=ind_yel}
@@ -252,14 +264,14 @@ local function init(parent, id)
-- boiler annunciator panel(s)
if available_space > 0 then _add_space() end
if unit.num_boilers > 0 then
TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
b1_wll.register(b_ps[1], "WasterLevelLow", b1_wll.update)
if available_space > 0 then _add_space() end
TextBox{parent=rcs_tags,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
TextBox{parent=rcs_tags,x=1,text="B1",width=2,fg_bg=hc_text}
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
b1_wll.register(b_ps[1], "WaterLevelLow", b1_wll.update)
TextBox{parent=rcs_tags,text="B1",width=2,fg_bg=hc_text}
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
b1_hr.register(b_ps[1], "HeatingRateLow", b1_hr.update)
end
@@ -271,11 +283,11 @@ local function init(parent, id)
_add_space()
end
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
TextBox{parent=rcs_tags,text="B2",width=2,fg_bg=hc_text}
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
b2_wll.register(b_ps[2], "WasterLevelLow", b2_wll.update)
b2_wll.register(b_ps[2], "WaterLevelLow", b2_wll.update)
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
TextBox{parent=rcs_tags,text="B2",width=2,fg_bg=hc_text}
local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
b2_hr.register(b_ps[2], "HeatingRateLow", b2_hr.update)
end
@@ -284,19 +296,19 @@ local function init(parent, id)
if available_space > 1 then _add_space() end
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
t1_sdo.register(t_ps[1], "SteamDumpOpen", t1_sdo.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
t1_tos.register(t_ps[1], "TurbineOverSpeed", t1_tos.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
t1_gtrp.register(t_ps[1], "GeneratorTrip", t1_gtrp.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
t1_trp.register(t_ps[1], "TurbineTrip", t1_trp.update)
@@ -305,19 +317,19 @@ local function init(parent, id)
_add_space()
end
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
t2_sdo.register(t_ps[2], "SteamDumpOpen", t2_sdo.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
t2_tos.register(t_ps[2], "TurbineOverSpeed", t2_tos.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
t2_gtrp.register(t_ps[2], "GeneratorTrip", t2_gtrp.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
t2_trp.register(t_ps[2], "TurbineTrip", t2_trp.update)
end
@@ -325,30 +337,32 @@ local function init(parent, id)
if unit.num_turbines > 2 then
if available_space > 3 then _add_space() end
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
t3_sdo.register(t_ps[3], "SteamDumpOpen", t3_sdo.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
t3_tos.register(t_ps[3], "TurbineOverSpeed", t3_tos.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
t3_gtrp.register(t_ps[3], "GeneratorTrip", t3_gtrp.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update)
end
util.nop()
----------------------
-- reactor controls --
----------------------
local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=gry_wht}
local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
TextBox{parent=burn_control,x=9,y=2,text="mB/t"}
local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=s_hi_box}
local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=burn_control,x=9,y=2,text="mB/t",fg_bg=style.theme.label_fg}
local set_burn = function () unit.set_burn(burn_rate.get_value()) end
local set_burn_btn = PushButton{parent=burn_control,x=14,y=2,text="SET",min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=style.wh_gray,dis_fg_bg=dis_colors,callback=set_burn}
@@ -361,16 +375,16 @@ local function init(parent, id)
local scram = HazardButton{parent=main,x=2,y=32,text="SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=unit.scram,fg_bg=hzd_fg_bg}
local reset = HazardButton{parent=main,x=22,y=32,text="RESET",accent=colors.red,dis_colors=dis_colors,callback=unit.reset_rps,fg_bg=hzd_fg_bg}
unit.start_ack = start.on_response
unit.scram_ack = scram.on_response
unit.reset_rps_ack = reset.on_response
unit.ack_alarms_ack = ack_a.on_response
db.process.unit_ack[id].on_start = start.on_response
db.process.unit_ack[id].on_scram = scram.on_response
db.process.unit_ack[id].on_rps_reset = reset.on_response
db.process.unit_ack[id].on_ack_alarms = ack_a.on_response
local function start_button_en_check()
if (unit.reactor_data ~= nil) and (unit.reactor_data.mek_status ~= nil) then
local can_start = (not unit.reactor_data.mek_status.status) and
(not unit.reactor_data.rps_tripped) and
(unit.a_group == 0)
(unit.a_group == AUTO_GROUP.MANUAL)
if can_start then start.enable() else start.disable() end
end
end
@@ -382,7 +396,7 @@ local function init(parent, id)
reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=48}
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=ALIGN.CENTER,width=33,x=46,y=48}
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1}
@@ -394,22 +408,22 @@ local function init(parent, id)
-- alarm management --
----------------------
local alarm_panel = Div{parent=main,x=2,y=36,width=29,height=16,fg_bg=bw_fg_bg}
local alarm_panel = Div{parent=main,x=2,y=36,width=29,height=16,fg_bg=s_hi_bright}
local a_brc = AlarmLight{parent=alarm_panel,x=6,y=2,label="Containment Breach",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
local a_rad = AlarmLight{parent=alarm_panel,x=6,label="Containment Radiation",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
local a_dmg = AlarmLight{parent=alarm_panel,x=6,label="Critical Damage",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
local a_brc = AlarmLight{parent=alarm_panel,x=6,y=2,label="Containment Breach",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
local a_rad = AlarmLight{parent=alarm_panel,x=6,label="Containment Radiation",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
local a_dmg = AlarmLight{parent=alarm_panel,x=6,label="Critical Damage",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
alarm_panel.line_break()
local a_rcl = AlarmLight{parent=alarm_panel,x=6,label="Reactor Lost",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
local a_rcd = AlarmLight{parent=alarm_panel,x=6,label="Reactor Damage",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
local a_rot = AlarmLight{parent=alarm_panel,x=6,label="Reactor Over Temp",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
local a_rht = AlarmLight{parent=alarm_panel,x=6,label="Reactor High Temp",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS}
local a_rwl = AlarmLight{parent=alarm_panel,x=6,label="Reactor Waste Leak",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
local a_rwh = AlarmLight{parent=alarm_panel,x=6,label="Reactor Waste High",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS}
local a_rcl = AlarmLight{parent=alarm_panel,x=6,label="Reactor Lost",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
local a_rcd = AlarmLight{parent=alarm_panel,x=6,label="Reactor Damage",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
local a_rot = AlarmLight{parent=alarm_panel,x=6,label="Reactor Over Temp",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
local a_rht = AlarmLight{parent=alarm_panel,x=6,label="Reactor High Temp",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_500_MS}
local a_rwl = AlarmLight{parent=alarm_panel,x=6,label="Reactor Waste Leak",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
local a_rwh = AlarmLight{parent=alarm_panel,x=6,label="Reactor Waste High",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_500_MS}
alarm_panel.line_break()
local a_rps = AlarmLight{parent=alarm_panel,x=6,label="RPS Transient",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS}
local a_clt = AlarmLight{parent=alarm_panel,x=6,label="RCS Transient",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS}
local a_tbt = AlarmLight{parent=alarm_panel,x=6,label="Turbine Trip",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
local a_rps = AlarmLight{parent=alarm_panel,x=6,label="RPS Transient",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_500_MS}
local a_clt = AlarmLight{parent=alarm_panel,x=6,label="RCS Transient",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_500_MS}
local a_tbt = AlarmLight{parent=alarm_panel,x=6,label="Turbine Trip",c1=ind_bkg,c2=ind_red.fgd,c3=ind_grn.fgd,flash=true,period=period.BLINK_250_MS}
a_brc.register(u_ps, "Alarm_1", a_brc.update)
a_rad.register(u_ps, "Alarm_2", a_rad.update)
@@ -462,21 +476,19 @@ local function init(parent, id)
-- color tags
TextBox{parent=alarm_panel,x=5,y=13,text="\x95",width=1,height=1,fg_bg=cpair(colors.white,colors.cyan)}
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,height=1,fg_bg=cpair(colors.white,colors.blue)}
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,height=1,fg_bg=cpair(colors.white,colors.blue)}
TextBox{parent=alarm_panel,x=5,y=13,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.cyan)}
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)}
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)}
--------------------------------
-- automatic control settings --
--------------------------------
TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,height=1,x=32,y=36}
TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,x=32,y=36}
local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37}
local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1}
local ctl_opts = { "Manual", "Primary", "Secondary", "Tertiary", "Backup" }
local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.gray,colors.white),select_color=colors.purple}
local group = RadioButton{parent=auto_div,options=types.AUTO_GROUP_NAMES,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end)
@@ -487,8 +499,8 @@ local function init(parent, id)
auto_div.line_break()
TextBox{parent=auto_div,text="Prio. Group",height=1,width=11,fg_bg=style.label}
local auto_grp = TextBox{parent=auto_div,text="Manual",height=1,width=11,fg_bg=bw_fg_bg}
TextBox{parent=auto_div,text="Prio. Group",width=11,fg_bg=style.label}
local auto_grp = TextBox{parent=auto_div,text="Manual",width=11,fg_bg=s_field}
auto_grp.register(u_ps, "auto_group", auto_grp.set_value)
@@ -511,10 +523,10 @@ local function init(parent, id)
-- enable/disable controls based on group assignment (start button is separate)
burn_rate.register(u_ps, "auto_group_id", function (gid)
if gid == 0 then burn_rate.enable() else burn_rate.disable() end
if gid == AUTO_GROUP.MANUAL then burn_rate.enable() else burn_rate.disable() end
end)
set_burn_btn.register(u_ps, "auto_group_id", function (gid)
if gid == 0 then set_burn_btn.enable() else set_burn_btn.disable() end
if gid == AUTO_GROUP.MANUAL then set_burn_btn.enable() else set_burn_btn.disable() end
end)
-- can't change group if auto is engaged regardless of if this unit is part of auto control

View File

@@ -24,17 +24,12 @@ local ALIGN = core.ALIGN
local sprintf = util.sprintf
local border = core.border
local cpair = core.cpair
local pipe = core.pipe
local wh_gray = style.wh_gray
local bw_fg_bg = style.bw_fg_bg
local text_c = style.text_colors
local lu_c = style.lu_colors
local lg_gray = style.lg_gray
local ind_grn = style.ind_grn
local ind_wht = style.ind_wht
-- make a new unit flow window
---@param parent graphics_element parent
---@param x integer top left x
@@ -42,6 +37,15 @@ local ind_wht = style.ind_wht
---@param wide boolean whether to render wide version
---@param unit ioctl_unit unit database entry
local function make(parent, x, y, wide, unit)
local s_field = style.theme.field_box
local text_c = style.text_colors
local lu_c = style.lu_colors
local lu_c_d = style.lu_colors_dark
local ind_grn = style.ind_grn
local ind_wht = style.ind_wht
local height = 16
local v_start = 1 + ((unit.unit_id - 1) * 5)
@@ -69,10 +73,10 @@ local function make(parent, x, y, wide, unit)
------------------
local reactor = Rectangle{parent=root,x=1,y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=reactor,y=1,text="FISSION REACTOR",alignment=ALIGN.CENTER,height=1}
TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=ALIGN.CENTER,height=1}
TextBox{parent=reactor,y=1,text="FISSION REACTOR",alignment=ALIGN.CENTER}
TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=ALIGN.CENTER}
TextBox{parent=root,x=19,y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
TextBox{parent=root,x=3,y=5,text="\x19",width=1,height=1,fg_bg=lg_gray}
TextBox{parent=root,x=3,y=5,text="\x19",width=1,fg_bg=lg_gray}
local rc_pipes = {}
@@ -99,43 +103,43 @@ local function make(parent, x, y, wide, unit)
table.insert(rc_pipes, pipe(_wide(92, 78), py, _wide(104, 83), py, colors.white, true))
end
PipeNetwork{parent=root,x=20,y=1,pipes=rc_pipes,bg=colors.lightGray}
PipeNetwork{parent=root,x=20,y=1,pipes=rc_pipes,bg=style.theme.bg}
if unit.num_boilers > 0 then
local cc_rate = DataIndicator{parent=root,x=_wide(25,22),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
local hc_rate = DataIndicator{parent=root,x=_wide(25,22),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
local cc_rate = DataIndicator{parent=root,x=_wide(25,22),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
local hc_rate = DataIndicator{parent=root,x=_wide(25,22),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
cc_rate.register(unit.unit_ps, "boiler_boil_sum", function (sum) cc_rate.update(sum * 10) end)
hc_rate.register(unit.unit_ps, "heating_rate", hc_rate.update)
local boiler = Rectangle{parent=root,x=_wide(47,40),y=1,border=border(1, colors.gray, true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=boiler,y=1,text="THERMO-ELECTRIC",alignment=ALIGN.CENTER,height=1}
TextBox{parent=boiler,y=3,text=util.trinary(unit.num_boilers>1,"BOILERS","BOILER"),alignment=ALIGN.CENTER,height=1}
local boiler = Rectangle{parent=root,x=_wide(47,40),y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=boiler,y=1,text="THERMO-ELECTRIC",alignment=ALIGN.CENTER}
TextBox{parent=boiler,y=3,text=util.trinary(unit.num_boilers>1,"BOILERS","BOILER"),alignment=ALIGN.CENTER}
TextBox{parent=root,x=_wide(47,40),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
TextBox{parent=root,x=_wide(65,58),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
local wt_rate = DataIndicator{parent=root,x=_wide(71,61),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
local st_rate = DataIndicator{parent=root,x=_wide(71,61),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
local wt_rate = DataIndicator{parent=root,x=_wide(71,61),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
local st_rate = DataIndicator{parent=root,x=_wide(71,61),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
wt_rate.register(unit.unit_ps, "turbine_flow_sum", wt_rate.update)
st_rate.register(unit.unit_ps, "boiler_boil_sum", st_rate.update)
else
local wt_rate = DataIndicator{parent=root,x=28,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
local st_rate = DataIndicator{parent=root,x=28,y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
local wt_rate = DataIndicator{parent=root,x=28,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
local st_rate = DataIndicator{parent=root,x=28,y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=s_field}
wt_rate.register(unit.unit_ps, "turbine_flow_sum", wt_rate.update)
st_rate.register(unit.unit_ps, "heating_rate", st_rate.update)
end
local turbine = Rectangle{parent=root,x=_wide(93,79),y=1,border=border(1, colors.gray, true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=turbine,y=1,text="STEAM TURBINE",alignment=ALIGN.CENTER,height=1}
TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=ALIGN.CENTER,height=1}
local turbine = Rectangle{parent=root,x=_wide(93,79),y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=turbine,y=1,text="STEAM TURBINE",alignment=ALIGN.CENTER}
TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=ALIGN.CENTER}
TextBox{parent=root,x=_wide(93,79),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
for i = 1, unit.num_turbines do
local ry = 1 + (2 * (i - 1)) + prv_yo
TextBox{parent=root,x=_wide(125,103),y=ry,text="\x10\x11\x7f",fg_bg=text_c,width=3,height=1}
local state = TriIndicatorLight{parent=root,x=_wide(129,107),y=ry,label=v_names[i+4],c1=colors.gray,c2=colors.yellow,c3=colors.red}
TextBox{parent=root,x=_wide(125,103),y=ry,text="\x10\x11\x7f",fg_bg=text_c,width=3}
local state = TriIndicatorLight{parent=root,x=_wide(129,107),y=ry,label=v_names[i+4],c1=style.ind_bkg,c2=style.ind_yel.fgd,c3=style.ind_red.fgd}
state.register(unit.turbine_ps_tbl[i], "SteamDumpOpen", state.update)
end
@@ -145,6 +149,8 @@ local function make(parent, x, y, wide, unit)
local waste = Div{parent=root,x=3,y=6}
local waste_c = style.theme.fuel_color
local waste_pipes = {
pipe(0, 0, _wide(19, 16), 1, colors.brown, true),
pipe(_wide(14, 13), 1, _wide(19, 17), 5, colors.brown, true),
@@ -158,15 +164,15 @@ local function make(parent, x, y, wide, unit)
pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.cyan, true),
pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.cyan, true),
pipe(_wide(108, 94), 1, _wide(132, 110), 6, colors.black, true, true),
pipe(_wide(108, 94), 4, _wide(111, 95), 1, colors.black, true, true),
pipe(_wide(132, 110), 6, _wide(130, 108), 6, colors.black, true, true)
pipe(_wide(108, 94), 1, _wide(132, 110), 6, waste_c, true, true),
pipe(_wide(108, 94), 4, _wide(111, 95), 1, waste_c, true, true),
pipe(_wide(132, 110), 6, _wide(130, 108), 6, waste_c, true, true)
}
PipeNetwork{parent=waste,x=1,y=1,pipes=waste_pipes,bg=colors.lightGray}
PipeNetwork{parent=waste,x=1,y=1,pipes=waste_pipes,bg=style.theme.bg}
local function _valve(vx, vy, n)
TextBox{parent=waste,x=vx,y=vy,text="\x10\x11",fg_bg=text_c,width=2,height=1}
TextBox{parent=waste,x=vx,y=vy,text="\x10\x11",fg_bg=text_c,width=2}
local conn = IndicatorLight{parent=waste,x=vx-3,y=vy+1,label=v_names[n],colors=ind_grn}
local open = IndicatorLight{parent=waste,x=vx-3,y=vy+2,label="OPEN",colors=ind_wht}
conn.register(unit.unit_ps, util.c("V_", v_fields[n], "_conn"), conn.update)
@@ -175,16 +181,16 @@ local function make(parent, x, y, wide, unit)
local function _machine(mx, my, name)
local l = string.len(name) + 2
TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=ALIGN.CENTER,fg_bg=lg_gray,width=l,height=1}
TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=ALIGN.CENTER,fg_bg=wh_gray,width=l,height=1}
TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=ALIGN.CENTER,fg_bg=cpair(style.theme.bg,style.theme.header.bkg),width=l}
TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=ALIGN.CENTER,fg_bg=style.theme.header,width=l}
end
local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg}
local pu_rate = DataIndicator{parent=waste,x=_wide(82,70),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
local po_rate = DataIndicator{parent=waste,x=_wide(52,45),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
local popl_rate = DataIndicator{parent=waste,x=_wide(82,70),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
local poam_rate = DataIndicator{parent=waste,x=_wide(82,70),y=10,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
local spent_rate = DataIndicator{parent=waste,x=_wide(117,99),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
local pu_rate = DataIndicator{parent=waste,x=_wide(82,70),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=s_field}
local po_rate = DataIndicator{parent=waste,x=_wide(52,45),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
local popl_rate = DataIndicator{parent=waste,x=_wide(82,70),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
local poam_rate = DataIndicator{parent=waste,x=_wide(82,70),y=10,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
local spent_rate = DataIndicator{parent=waste,x=_wide(117,98),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%8.3f",value=0,width=13,fg_bg=s_field}
waste_rate.register(unit.unit_ps, "act_burn_rate", waste_rate.update)
pu_rate.register(unit.unit_ps, "pu_rate", pu_rate.update)
@@ -203,18 +209,18 @@ local function make(parent, x, y, wide, unit)
_machine(_wide(97, 83), 4, "PRC [Po] \x1a");
_machine(_wide(116, 94), 6, "SPENT WASTE \x1b")
TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=bw_fg_bg}
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_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn}
local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c,label="CNT",unit="",format="%2d",value=0,width=7}
local sna_pk = DataIndicator{parent=sna_po,y=3,lu_colors=lu_c,label="PEAK",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max = DataIndicator{parent=sna_po,lu_colors=lu_c,label="MAX",unit="mB/t",format="%8.2f",value=0,width=17}
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c,label="IN",unit="mB/t",format="%9.2f",value=0,width=17}
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}
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_prod_rate", sna_max.update)
sna_max.register(unit.unit_ps, "sna_max_rate", sna_max.update)
sna_in.register(unit.unit_ps, "sna_in", sna_in.update)
return root

View File

@@ -44,7 +44,7 @@ local function make(parent, x, y, unit)
local root = Div{parent=parent,x=x,y=y,width=80,height=height}
-- unit header message
TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,fg_bg=style.theme.header}
-------------
-- REACTOR --
@@ -66,7 +66,7 @@ local function make(parent, x, y, unit)
table.insert(coolant_pipes, pipe(2, 0, 11, 11, colors.orange))
end
PipeNetwork{parent=root,x=4,y=10,pipes=coolant_pipes,bg=colors.lightGray}
PipeNetwork{parent=root,x=4,y=10,pipes=coolant_pipes,bg=style.theme.bg}
end
-------------
@@ -164,10 +164,10 @@ local function make(parent, x, y, unit)
table.insert(steam_pipes_b, pipe(0, 18, 2, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
end
PipeNetwork{parent=root,x=47,y=11,pipes=steam_pipes_a,bg=colors.lightGray}
PipeNetwork{parent=root,x=47,y=11,pipes=steam_pipes_a,bg=style.theme.bg}
end
PipeNetwork{parent=root,x=54,y=3,pipes=steam_pipes_b,bg=colors.lightGray}
PipeNetwork{parent=root,x=54,y=3,pipes=steam_pipes_b,bg=style.theme.bg}
return root
end

View File

@@ -1,52 +0,0 @@
local completion = require("cc.completion")
local util = require("scada-common.util")
local print = util.print
local dialog = {}
-- ask the user yes or no
---@nodiscard
---@param question string
---@param default boolean
---@return boolean|nil
function dialog.ask_y_n(question, default)
print(question)
if default == true then
print(" (Y/n)? ")
else
print(" (y/N)? ")
end
local response = read(nil, nil)
if response == "" then
return default
elseif response == "Y" or response == "y" then
return true
elseif response == "N" or response == "n" then
return false
else
return nil
end
end
-- ask the user for an input within a set of options
---@nodiscard
---@param options table
---@param cancel string
---@return boolean|string|nil
function dialog.ask_options(options, cancel)
print("> ")
local response = read(nil, nil, function(text) return completion.choice(text, options) end)
if response == cancel then return false end
if util.table_contains(options, response) then
return response
else return nil end
end
return dialog

View File

@@ -32,13 +32,16 @@ local border = core.border
local pipe = core.pipe
local wh_gray = style.wh_gray
local bw_fg_bg = style.bw_fg_bg
local text_col = style.text_colors
local lu_col = style.lu_colors
-- create new flow view
---@param main graphics_element main displaybox
local function init(main)
local s_hi_bright = style.theme.highlight_box_bright
local s_field = style.theme.field_box
local text_col = style.text_colors
local lu_col = style.lu_colors
local lu_c_d = style.lu_colors_dark
local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units
@@ -46,9 +49,9 @@ local function init(main)
local tank_list = facility.tank_list
-- window header message
local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
-- max length example: "01:23:45 AM - Wednesday, September 28 2022"
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,fg_bg=style.theme.header}
datetime.register(facility.ps, "date_time", datetime.set_value)
@@ -240,16 +243,17 @@ local function init(main)
local flow_x = 3
if #water_pipes > 0 then
flow_x = 25
PipeNetwork{parent=main,x=2,y=3,pipes=water_pipes,bg=colors.lightGray}
PipeNetwork{parent=main,x=2,y=3,pipes=water_pipes,bg=style.theme.bg}
end
for i = 1, facility.num_units do
local y_offset = y_ofs(i)
unit_flow(main, flow_x, 5 + y_offset, #water_pipes == 0, units[i])
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.cyan, true, true))
util.nop()
end
PipeNetwork{parent=main,x=139,y=15,pipes=po_pipes,bg=colors.lightGray}
PipeNetwork{parent=main,x=139,y=15,pipes=po_pipes,bg=style.theme.bg}
-----------------
-- tank valves --
@@ -261,7 +265,7 @@ local function init(main)
if tank_defs[i] > 0 then
local vy = 3 + y_ofs(i)
TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=text_col,width=2,height=1}
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 open = IndicatorLight{parent=main,x=9,y=vy+2,label="OPEN",colors=style.ind_wht}
@@ -288,21 +292,21 @@ local function init(main)
local tank = Div{parent=main,x=3,y=7+y_offset,width=20,height=14}
TextBox{parent=tank,text=" ",height=1,x=1,y=1,fg_bg=style.lg_gray}
TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=ALIGN.CENTER,height=1,fg_bg=style.wh_gray}
TextBox{parent=tank,text=" ",x=1,y=1,fg_bg=style.lg_gray}
TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=ALIGN.CENTER,fg_bg=style.wh_gray}
local tank_box = Rectangle{parent=tank,border=border(1,colors.gray,true),width=20,height=12}
local status = StateIndicator{parent=tank_box,x=3,y=1,states=style.dtank.states,value=1,min_width=14}
TextBox{parent=tank_box,x=2,y=3,text="Fill",height=1,width=10,fg_bg=style.label}
TextBox{parent=tank_box,x=2,y=3,text="Fill",width=10,fg_bg=style.label}
local tank_pcnt = DataIndicator{parent=tank_box,x=10,y=3,label="",format="%5.2f",value=100,unit="%",lu_colors=lu_col,width=8,fg_bg=text_col}
local tank_amnt = DataIndicator{parent=tank_box,x=2,label="",format="%13d",value=0,commas=true,unit="mB",lu_colors=lu_col,width=16,fg_bg=bw_fg_bg}
local tank_amnt = DataIndicator{parent=tank_box,x=2,label="",format="%13d",value=0,commas=true,unit="mB",lu_colors=lu_col,width=16,fg_bg=s_field}
TextBox{parent=tank_box,x=2,y=6,text="Water Level",height=1,width=11,fg_bg=style.label}
TextBox{parent=tank_box,x=2,y=6,text="Water Level",width=11,fg_bg=style.label}
local level = HorizontalBar{parent=tank_box,x=2,y=7,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=16}
TextBox{parent=tank_box,x=2,y=9,text="In/Out Mode",height=1,width=11,fg_bg=style.label}
TextBox{parent=tank_box,x=2,y=9,text="In/Out Mode",width=11,fg_bg=style.label}
local can_fill = IndicatorLight{parent=tank_box,x=2,y=10,label="FILL",colors=style.ind_wht}
local can_empty = IndicatorLight{parent=tank_box,x=10,y=10,label="EMPTY",colors=style.ind_wht}
@@ -332,14 +336,16 @@ local function init(main)
end
end
util.nop()
---------
-- SPS --
---------
local sps = Div{parent=main,x=140,y=3,height=12}
TextBox{parent=sps,text=" ",width=24,height=1,x=1,y=1,fg_bg=style.lg_gray}
TextBox{parent=sps,text="SPS",alignment=ALIGN.CENTER,width=24,height=1,fg_bg=wh_gray}
TextBox{parent=sps,text=" ",width=24,x=1,y=1,fg_bg=style.lg_gray}
TextBox{parent=sps,text="SPS",alignment=ALIGN.CENTER,width=24,fg_bg=wh_gray}
local sps_box = Rectangle{parent=sps,border=border(1,colors.gray,true),width=24,height=10}
@@ -347,13 +353,13 @@ local function init(main)
status.register(facility.sps_ps_tbl[1], "computed_status", status.update)
TextBox{parent=sps_box,x=2,y=3,text="Input Rate",height=1,width=10,fg_bg=style.label}
local sps_in = DataIndicator{parent=sps_box,x=2,label="",format="%15.3f",value=0,unit="mB/t",lu_colors=lu_col,width=20,fg_bg=bw_fg_bg}
TextBox{parent=sps_box,x=2,y=3,text="Input Rate",width=10,fg_bg=style.label}
local sps_in = DataIndicator{parent=sps_box,x=2,label="",format="%15.2f",value=0,unit="mB/t",lu_colors=lu_col,width=20,fg_bg=s_field}
sps_in.register(facility.ps, "po_am_rate", sps_in.update)
TextBox{parent=sps_box,x=2,y=6,text="Production Rate",height=1,width=15,fg_bg=style.label}
local sps_rate = DataIndicator{parent=sps_box,x=2,label="",format="%15d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=20,fg_bg=bw_fg_bg}
TextBox{parent=sps_box,x=2,y=6,text="Production Rate",width=15,fg_bg=style.label}
local sps_rate = DataIndicator{parent=sps_box,x=2,label="",format="%15d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=20,fg_bg=s_field}
sps_rate.register(facility.sps_ps_tbl[1], "process_rate", function (r) sps_rate.update(r * 1000) end)
@@ -361,25 +367,25 @@ local function init(main)
-- statistics --
----------------
TextBox{parent=main,x=145,y=16,text="RAW WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
local raw_waste = Rectangle{parent=main,x=145,y=17,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg}
local sum_raw_waste = DataIndicator{parent=raw_waste,lu_colors=lu_col,label="SUM",unit="mB/t",format="%8.2f",value=0,width=17}
TextBox{parent=main,x=145,y=16,text="RAW WASTE",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
local raw_waste = Rectangle{parent=main,x=145,y=17,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=s_hi_bright}
local sum_raw_waste = DataIndicator{parent=raw_waste,lu_colors=lu_c_d,label="SUM",unit="mB/t",format="%8.2f",value=0,width=17}
sum_raw_waste.register(facility.ps, "burn_sum", sum_raw_waste.update)
TextBox{parent=main,x=145,y=21,text="PROC. WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=bw_fg_bg}
local pu = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17}
local po = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Po",unit="mB/t",format="%9.3f",value=0,width=17}
local popl = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="PoPl",unit="mB/t",format="%7.3f",value=0,width=17}
TextBox{parent=main,x=145,y=21,text="PROC. WASTE",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=s_hi_bright}
local pu = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17}
local po = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="Po",unit="mB/t",format="%9.2f",value=0,width=17}
local popl = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="PoPl",unit="mB/t",format="%7.2f",value=0,width=17}
pu.register(facility.ps, "pu_rate", pu.update)
po.register(facility.ps, "po_rate", po.update)
popl.register(facility.ps, "po_pl_rate", popl.update)
TextBox{parent=main,x=145,y=28,text="SPENT WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
local sp_waste = Rectangle{parent=main,x=145,y=29,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg}
local sum_sp_waste = DataIndicator{parent=sp_waste,lu_colors=lu_col,label="SUM",unit="mB/t",format="%8.3f",value=0,width=17}
TextBox{parent=main,x=145,y=28,text="SPENT WASTE",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
local sp_waste = Rectangle{parent=main,x=145,y=29,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=s_hi_bright}
local sum_sp_waste = DataIndicator{parent=sp_waste,lu_colors=lu_c_d,label="SUM",unit="mB/t",format="%8.3f",value=0,width=17}
sum_sp_waste.register(facility.ps, "spent_waste_rate", sum_sp_waste.update)
end

View File

@@ -22,8 +22,11 @@ local TextBox = require("graphics.elements.textbox")
local TabBar = require("graphics.elements.controls.tabbar")
local LED = require("graphics.elements.indicators.led")
local LEDPair = require("graphics.elements.indicators.ledpair")
local RGBLED = require("graphics.elements.indicators.ledrgb")
local LINK_STATE = types.PANEL_LINK_STATE
local ALIGN = core.ALIGN
local cpair = core.cpair
@@ -36,7 +39,7 @@ local led_grn = style.led_grn
local function init(panel, num_units)
local ps = iocontrol.get_db().fp.ps
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,height=1,fg_bg=style.fp.header}
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}
@@ -56,19 +59,58 @@ local function init(panel, num_units)
heartbeat.register(ps, "heartbeat", heartbeat.update)
local modem = LED{parent=system,label="MODEM",colors=led_grn}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
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}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(ps, "link_state", network.update)
else
local nt_lnk = LEDPair{parent=system,label="NT LINKED",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
local nt_ver = LEDPair{parent=system,label="NT VERSION",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
nt_lnk.register(ps, "link_state", function (state)
local value = 2
if state == LINK_STATE.DISCONNECTED then
value = 1
elseif state == LINK_STATE.LINKED then
value = 3
end
nt_lnk.update(value)
end)
nt_ver.register(ps, "link_state", function (state)
local value = 3
if state == LINK_STATE.BAD_VERSION then
value = 2
elseif state == LINK_STATE.DISCONNECTED then
value = 1
end
nt_ver.update(value)
end)
end
system.line_break()
modem.register(ps, "has_modem", modem.update)
network.register(ps, "link_state", network.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,height=1,text=comp_id,fg_bg=style.fp_label}
TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=style.fp.disabled_fg}
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
@@ -89,9 +131,9 @@ local function init(panel, num_units)
-- about footer
--
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp_label}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1}
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"}
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)
@@ -103,7 +145,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,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
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 _ = Div{parent=api_list,height=1,hidden=true} -- padding
-- assemble page panes
@@ -113,11 +155,11 @@ local function init(panel, num_units)
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
local tabs = {
{ name = "CRD", color = style.fp_text },
{ name = "API", color = style.fp_text },
{ name = "CRD", color = style.fp.text },
{ name = "API", color = style.fp.text },
}
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=style.bw_fg_bg}
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=style.fp_theme.highlight_box_bright}
-- link pocket API list management to PGI
pgi.link_elements(api_list, pkt_entry)

View File

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

View File

@@ -2,79 +2,141 @@
-- Graphics Style Options
--
local core = require("graphics.core")
local util = require("scada-common.util")
local core = require("graphics.core")
local themes = require("graphics.themes")
---@class crd_style
local style = {}
local cpair = core.cpair
-- GLOBAL --
-- add color mappings for front panel
colors.ivory = colors.pink
colors.yellow_hc = colors.purple
colors.red_off = colors.brown
colors.yellow_off = colors.magenta
colors.green_off = colors.lime
-- front panel styling
style.fp = {}
style.fp_theme = themes.sandstone
style.fp = themes.get_fp_style(style.fp_theme)
style.fp.root = cpair(colors.black, colors.ivory)
style.fp.header = cpair(colors.black, colors.lightGray)
style.fp.colors = {
{ c = colors.red, hex = 0xdf4949 }, -- RED ON
{ c = colors.orange, hex = 0xffb659 },
{ c = colors.yellow, hex = 0xf9fb53 }, -- YELLOW ON
{ c = colors.lime, hex = 0x16665a }, -- GREEN OFF
{ c = colors.green, hex = 0x6be551 }, -- GREEN ON
{ c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff },
{ c = colors.purple, hex = 0xb156ee }, -- YELLOW HIGH CONTRAST
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
-- { c = colors.white, hex = 0xdcd9ca },
{ c = colors.lightGray, hex = 0xb1b8b3 },
{ c = colors.gray, hex = 0x575757 },
-- { c = colors.black, hex = 0x191919 },
{ c = colors.brown, hex = 0x672223 } -- RED OFF
}
style.led_grn = cpair(colors.green, colors.green_off)
-- main GUI styling
style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray)
style.label = cpair(colors.gray, colors.lightGray)
---@class theme
local smooth_stone = {
text = colors.black,
text_inv = colors.white,
label = colors.gray,
label_dark = colors.gray,
disabled = colors.lightGray,
bg = colors.lightGray,
checkbox_bg = colors.black,
accent_light = colors.white,
accent_dark = colors.gray,
style.colors = {
{ c = colors.red, hex = 0xdf4949 },
{ c = colors.orange, hex = 0xffb659 },
{ c = colors.yellow, hex = 0xfffc79 },
{ c = colors.lime, hex = 0x80ff80 },
{ c = colors.green, hex = 0x4aee8a },
{ c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff },
{ c = colors.purple, hex = 0xb156ee },
{ c = colors.pink, hex = 0xf26ba2 },
{ c = colors.magenta, hex = 0xf9488a },
-- { c = colors.white, hex = 0xf0f0f0 },
{ c = colors.lightGray, hex = 0xcacaca },
{ c = colors.gray, hex = 0x575757 },
-- { c = colors.black, hex = 0x191919 },
-- { c = colors.brown, hex = 0x7f664c }
fuel_color = colors.black,
header = cpair(colors.white, colors.gray),
text_fg = cpair(colors.black, colors._INHERIT),
label_fg = cpair(colors.gray, colors._INHERIT),
disabled_fg = cpair(colors.lightGray, colors._INHERIT),
highlight_box = cpair(colors.black, colors.white),
highlight_box_bright = cpair(colors.black, colors.white),
field_box = cpair(colors.black, colors.white),
colors = themes.smooth_stone.colors,
-- color re-mappings for assistive modes
color_modes = themes.smooth_stone.color_modes
}
---@type theme
local deepslate = {
text = colors.white,
text_inv = colors.black,
label = colors.lightGray,
label_dark = colors.gray,
disabled = colors.gray,
bg = colors.black,
checkbox_bg = colors.gray,
accent_light = colors.gray,
accent_dark = colors.lightGray,
fuel_color = colors.lightGray,
header = cpair(colors.white, colors.gray),
text_fg = cpair(colors.white, colors._INHERIT),
label_fg = cpair(colors.lightGray, colors._INHERIT),
disabled_fg = cpair(colors.gray, colors._INHERIT),
highlight_box = cpair(colors.white, colors.gray),
highlight_box_bright = cpair(colors.black, colors.lightGray),
field_box = cpair(colors.white, colors.gray),
colors = themes.deepslate.colors,
-- color re-mappings for assistive modes
color_modes = themes.deepslate.color_modes
}
style.theme = smooth_stone
-- set themes per configurations
---@param main UI_THEME main UI theme
---@param fp FP_THEME front panel theme
---@param color_mode COLOR_MODE the color mode to use
function style.set_themes(main, fp, color_mode)
local colorblind = color_mode ~= themes.COLOR_MODE.STANDARD and color_mode ~= themes.COLOR_MODE.STD_ON_BLACK
local gray_ind_off = color_mode == themes.COLOR_MODE.STANDARD or color_mode == themes.COLOR_MODE.BLUE_IND
style.ind_bkg = colors.gray
style.fp_ind_bkg = util.trinary(gray_ind_off, colors.gray, colors.black)
style.ind_hi_box_bg = util.trinary(gray_ind_off, colors.gray, colors.black)
if main == themes.UI_THEME.SMOOTH_STONE then
style.theme = smooth_stone
style.ind_bkg = util.trinary(gray_ind_off, colors.gray, colors.black)
elseif main == themes.UI_THEME.DEEPSLATE then
style.theme = deepslate
style.ind_hi_box_bg = util.trinary(gray_ind_off, colors.lightGray, colors.black)
end
style.colorblind = colorblind
style.root = cpair(style.theme.text, style.theme.bg)
style.label = cpair(style.theme.label, style.theme.bg)
-- high contrast text (also tags)
style.hc_text = cpair(style.theme.text, style.theme.text_inv)
-- text on default background
style.text_colors = cpair(style.theme.text, style.theme.bg)
-- label & unit colors
style.lu_colors = cpair(style.theme.label, style.theme.label)
-- label & unit colors (darker if set)
style.lu_colors_dark = cpair(style.theme.label_dark, style.theme.label_dark)
style.ind_grn = cpair(util.trinary(colorblind, colors.blue, colors.green), style.ind_bkg)
style.ind_yel = cpair(colors.yellow, style.ind_bkg)
style.ind_red = cpair(colors.red, style.ind_bkg)
style.ind_wht = cpair(colors.white, style.ind_bkg)
if fp == themes.FP_THEME.SANDSTONE then
style.fp_theme = themes.sandstone
elseif fp == themes.FP_THEME.BASALT then
style.fp_theme = themes.basalt
end
style.fp = themes.get_fp_style(style.fp_theme)
end
-- COMMON COLOR PAIRS --
style.wh_gray = cpair(colors.white, colors.gray)
style.bw_fg_bg = cpair(colors.black, colors.white)
style.text_colors = cpair(colors.black, colors.lightGray)
style.lu_colors = cpair(colors.gray, colors.gray)
style.hzd_fg_bg = style.wh_gray
style.dis_colors = cpair(colors.white, colors.lightGray)
@@ -82,15 +144,6 @@ style.lg_gray = cpair(colors.lightGray, colors.gray)
style.lg_white = cpair(colors.lightGray, colors.white)
style.gray_white = cpair(colors.gray, colors.white)
style.ind_grn = cpair(colors.green, colors.gray)
style.ind_yel = cpair(colors.yellow, colors.gray)
style.ind_red = cpair(colors.red, colors.gray)
style.ind_wht = style.wh_gray
style.fp_text = cpair(colors.black, colors.ivory)
style.fp_label = cpair(colors.lightGray, colors.ivory)
style.led_grn = cpair(colors.green, colors.green_off)
-- UI COMPONENTS --
style.reactor = {

View File

@@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
local core = {}
core.version = "2.0.7"
core.version = "2.3.4"
core.flasher = flasher
core.events = events
@@ -61,6 +61,9 @@ end
---@field blit_fgd string
---@field blit_bkg string
-- add inherited flag, 3 isn't a pure color so it wouldn't be used
colors._INHERIT = 3
-- create a new color pair definition
---@nodiscard
---@param a color
@@ -120,15 +123,17 @@ end
-- Interactive Field Manager
---@param e graphics_base
---@param max_len any
---@param fg_bg any
---@param dis_fg_bg any
function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
---@param e graphics_base element
---@param max_len any max value length
---@param fg_bg any enabled fg/bg
---@param dis_fg_bg any disabled fg/bg
---@param align_right any true to align content right while unfocused
function core.new_ifield(e, max_len, fg_bg, dis_fg_bg, align_right)
local self = {
frame_start = 1,
visible_text = e.value,
cursor_pos = string.len(e.value) + 1,
align_offset = 0,
selected_all = false
}
@@ -183,7 +188,12 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
e.w_write(string.rep(" ", e.frame.w))
e.w_set_cur(1, 1)
local function _write()
local function _write(align_r)
if align_r and string.len(self.visible_text) <=e.frame.w then
self.align_offset = (e.frame.w - string.len(self.visible_text))
e.w_set_cur((e.frame.w - string.len(self.visible_text)) + 1, 1)
end
if self.censor then
e.w_write(string.rep(self.censor, string.len(self.visible_text)))
else
@@ -223,15 +233,27 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
self.selected_all = false
-- write text without cursor
_write()
_write(align_right)
end
end
-- move cursor to x
-- get an x value to pass to move_cursor taking into account right alignment offset present when unfocused
---@param x integer
function public.get_cursor_align_shift(x)
return math.max(0, x - self.align_offset)
end
-- move cursor to x
---@param x integer x position or 0 to jump to the end
function public.move_cursor(x)
self.selected_all = false
self.cursor_pos = math.min(x, string.len(self.visible_text) + 1)
if x <= 0 then
self.cursor_pos = string.len(self.visible_text) + 1
else
self.cursor_pos = math.min(x, string.len(self.visible_text) + 1)
end
public.show()
end

View File

@@ -2,6 +2,7 @@
-- Generic Graphics Element
--
-- local log = require("scada-common.log")
local util = require("scada-common.util")
local core = require("graphics.core")
@@ -49,9 +50,11 @@ local element = {}
---|indicator_light_args
---|power_indicator_args
---|rad_indicator_args
---|signal_bar_args
---|state_indicator_args
---|tristate_indicator_light_args
---|vbar_args
---|app_multipane_args
---|colormap_args
---|displaybox_args
---|div_args
@@ -80,9 +83,10 @@ end
-- a base graphics element, should not be created on its own
---@nodiscard
---@param args graphics_args arguments
---@param constraint? function apply a dimensional constraint based on proposed dimensions function(frame) -> width, height
---@param child_offset_x? integer mouse event offset x
---@param child_offset_y? integer mouse event offset y
function element.new(args, child_offset_x, child_offset_y)
function element.new(args, constraint, child_offset_x, child_offset_y)
local self = {
id = nil, ---@type element_id|nil
is_root = args.parent == nil,
@@ -196,6 +200,9 @@ function element.new(args, child_offset_x, child_offset_y)
---@param offset_y integer y offset for mouse events
---@param next_y integer next line if no y was provided
function protected.prepare_template(offset_x, offset_y, next_y)
-- don't auto incrememnt y if inheriting height, that would cause an assertion
next_y = util.trinary(args.height == nil and constraint == nil, 1, next_y)
-- record offsets in case there is a reposition
self.offset_x = offset_x
self.offset_y = offset_y
@@ -221,6 +228,13 @@ function element.new(args, child_offset_x, child_offset_y)
local w, h = self.p_window.getSize()
f.w = math.min(f.w, w - (f.x - 1))
f.h = math.min(f.h, h - (f.y - 1))
if type(constraint) == "function" then
-- constrain per provided constraint function (can only get smaller than available space)
w, h = constraint(f)
f.w = math.min(f.w, w)
f.h = math.min(f.h, h)
end
end
-- check frame
@@ -234,11 +248,24 @@ function element.new(args, child_offset_x, child_offset_y)
-- init colors
if args.fg_bg ~= nil then
protected.fg_bg = args.fg_bg
elseif args.parent ~= nil then
protected.fg_bg = args.parent.get_fg_bg()
protected.fg_bg = core.cpair(args.fg_bg.fgd, args.fg_bg.bkg)
end
if args.parent ~= nil then
local p_fg_bg = args.parent.get_fg_bg()
if args.fg_bg == nil then
protected.fg_bg = core.cpair(p_fg_bg.fgd, p_fg_bg.bkg)
else
if protected.fg_bg.fgd == colors._INHERIT then protected.fg_bg = core.cpair(p_fg_bg.fgd, protected.fg_bg.bkg) end
if protected.fg_bg.bkg == colors._INHERIT then protected.fg_bg = core.cpair(protected.fg_bg.fgd, p_fg_bg.bkg) end
end
end
-- check colors
element.assert(protected.fg_bg.fgd ~= colors._INHERIT, "could not determine foreground color to inherit")
element.assert(protected.fg_bg.bkg ~= colors._INHERIT, "could not determine background color to inherit")
-- set colors
protected.window.setBackgroundColor(protected.fg_bg.bkg)
protected.window.setTextColor(protected.fg_bg.fgd)
@@ -477,7 +504,10 @@ function element.new(args, child_offset_x, child_offset_y)
if args.parent ~= nil then
-- remove self from parent
-- log.debug("removing " .. self.id .. " from parent")
args.parent.__remove_child(self.id)
else
-- log.debug("no parent for " .. self.id .. " on delete attempt")
end
end
@@ -544,6 +574,15 @@ function element.new(args, child_offset_x, child_offset_y)
---@return graphics_element
function public.get_child(id) return protected.children[protected.child_id_map[id]].get() end
-- get all children
---@nodiscard
---@return table children table of graphics_element objects
function public.get_children()
local list = {}
for k, v in pairs(protected.children) do list[k] = v.get() end
return list
end
-- remove a child element
---@param id element_id
function public.remove(id)
@@ -843,9 +882,12 @@ function element.new(args, child_offset_x, child_offset_y)
-- re-draw this element and all its children
function public.redraw()
local bg, fg = protected.window.getBackgroundColor(), protected.window.getTextColor()
protected.window.setBackgroundColor(protected.fg_bg.bkg)
protected.window.setTextColor(protected.fg_bg.fgd)
protected.window.clear()
protected.window.setBackgroundColor(bg)
protected.window.setTextColor(fg)
protected.redraw()
for _, child in pairs(protected.children) do child.get().redraw() end
end

View File

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

View File

@@ -30,7 +30,7 @@ local function app_button(args)
element.assert(type(args.app_fg_bg) == "table", "app_fg_bg is a required field")
args.height = 4
args.width = 5
args.width = 7
-- create new graphics element base object
local e = element.new(args)
@@ -46,7 +46,7 @@ local function app_button(args)
end
-- draw icon
e.w_set_cur(1, 1)
e.w_set_cur(2, 1)
e.w_set_fgd(fgd)
e.w_set_bkg(bkg)
e.w_write("\x9f\x83\x83\x83")
@@ -55,16 +55,16 @@ local function app_button(args)
e.w_write("\x90")
e.w_set_fgd(fgd)
e.w_set_bkg(bkg)
e.w_set_cur(1, 2)
e.w_set_cur(2, 2)
e.w_write("\x95 ")
e.w_set_fgd(bkg)
e.w_set_bkg(fgd)
e.w_write("\x95")
e.w_set_cur(1, 3)
e.w_set_cur(2, 3)
e.w_write("\x82\x8f\x8f\x8f\x81")
-- write the icon text
e.w_set_cur(3, 2)
e.w_set_cur(4, 2)
e.w_set_fgd(fgd)
e.w_set_bkg(bkg)
e.w_write(args.text)

View File

@@ -24,7 +24,7 @@ local function checkbox(args)
args.can_focus = true
args.height = 1
args.width = 3 + string.len(args.label)
args.width = 2 + string.len(args.label)
-- create new graphics element base object
local e = element.new(args)

View File

@@ -10,6 +10,7 @@ local element = require("graphics.element")
---@field accent color accent color for hazard border
---@field dis_colors? cpair text color and border color when disabled
---@field callback function function to call on touch
---@field timeout? integer override for the default 1.5 second timeout, in seconds
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
@@ -28,6 +29,8 @@ local function hazard_button(args)
args.height = 3
args.width = string.len(args.text) + 4
local timeout = args.timeout or 1.5
-- create new graphics element base object
local e = element.new(args)
@@ -138,23 +141,21 @@ local function hazard_button(args)
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled then
if core.events.was_clicked(event.type) then
-- change text color to indicate clicked
e.w_set_fgd(args.accent)
e.w_set_cur(3, 2)
e.w_write(args.text)
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then
-- change text color to indicate clicked
e.w_set_fgd(args.accent)
e.w_set_cur(3, 2)
e.w_write(args.text)
-- abort any other callbacks
tcd.abort(on_timeout)
tcd.abort(on_success)
tcd.abort(on_failure)
-- abort any other callbacks
tcd.abort(on_timeout)
tcd.abort(on_success)
tcd.abort(on_failure)
-- 1.5 second timeout
tcd.dispatch(1.5, on_timeout)
-- operation timeout animation
tcd.dispatch(timeout, on_timeout)
args.callback()
end
args.callback()
end
end

View File

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

View File

@@ -90,7 +90,8 @@ local function radio_button(args)
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled and core.events.was_clicked(event.type) and (event.initial.y == event.current.y) then
if e.enabled and core.events.was_clicked(event.type) and
(event.initial.y == event.current.y) and e.in_frame_bounds(event.current.x, event.current.y) then
-- determine what was pressed
if args.options[event.current.y] ~= nil then
e.value = event.current.y

View File

@@ -8,13 +8,7 @@ local element = require("graphics.element")
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class sidebar_tab
---@field char string character identifier
---@field color cpair tab colors (fg/bg)
---@class sidebar_args
---@field tabs table sidebar tab options
---@field callback function function to call on tab change
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
@@ -27,21 +21,16 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@param args sidebar_args
---@return graphics_element element, element_id id
local function sidebar(args)
element.assert(type(args.tabs) == "table", "tabs is a required field")
element.assert(#args.tabs > 0, "at least one tab is required")
element.assert(type(args.callback) == "function", "callback is a required field")
args.width = 3
-- create new graphics element base object
local e = element.new(args)
element.assert(e.frame.h >= (#args.tabs * 3), "height insufficent to display all tabs")
-- default to 1st tab
e.value = 1
local was_pressed = false
local tabs = {}
-- show the button state
---@param pressed? boolean if the currently selected tab should appear as actively pressed
@@ -51,10 +40,18 @@ local function sidebar(args)
was_pressed = pressed
pressed_idx = pressed_idx or e.value
for i = 1, #args.tabs do
local tab = args.tabs[i] ---@type sidebar_tab
-- clear
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
for y = 1, e.frame.h do
e.w_set_cur(1, y)
e.w_write(" ")
end
local y = ((i - 1) * 3) + 1
-- draw tabs
for i = 1, #tabs do
local tab = tabs[i] ---@type sidebar_tab
local y = tab.y_start
e.w_set_cur(1, y)
@@ -66,13 +63,29 @@ local function sidebar(args)
e.w_set_bkg(tab.color.bkg)
end
e.w_write(" ")
e.w_set_cur(1, y + 1)
if e.value == i then
e.w_write(" " .. tab.char .. "\x10")
else e.w_write(" " .. tab.char .. " ") end
e.w_set_cur(1, y + 2)
e.w_write(" ")
if tab.tall then
e.w_write(" ")
e.w_set_cur(1, y + 1)
end
e.w_write(tab.label)
if tab.tall then
e.w_set_cur(1, y + 2)
e.w_write(" ")
end
end
end
-- determine which tab was pressed
---@param y integer y coordinate
local function find_tab(y)
for i = 1, #tabs do
local tab = tabs[i] ---@type sidebar_tab
if y >= tab.y_start and y <= tab.y_end then
return i
end
end
end
@@ -81,23 +94,25 @@ local function sidebar(args)
function e.handle_mouse(event)
-- determine what was pressed
if e.enabled then
local cur_idx = math.ceil(event.current.y / 3)
local ini_idx = math.ceil(event.initial.y / 3)
local cur_idx = find_tab(event.current.y)
local ini_idx = find_tab(event.initial.y)
local tab = tabs[cur_idx]
if args.tabs[cur_idx] ~= nil then
-- handle press if a callback was provided
if tab ~= nil and type(tab.callback) == "function" then
if event.type == MOUSE_CLICK.TAP then
e.value = cur_idx
draw(true)
-- show as unpressed in 0.25 seconds
tcd.dispatch(0.25, function () draw(false) end)
args.callback(e.value)
tab.callback()
elseif event.type == MOUSE_CLICK.DOWN then
draw(true, cur_idx)
elseif event.type == MOUSE_CLICK.UP then
if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then
e.value = cur_idx
draw(false)
args.callback(e.value)
tab.callback()
else draw(false) end
end
elseif event.type == MOUSE_CLICK.UP then
@@ -113,6 +128,35 @@ local function sidebar(args)
draw(false)
end
-- update the sidebar navigation options
---@param items table sidebar entries
function e.on_update(items)
local next_y = 1
tabs = {}
for i = 1, #items do
local item = items[i]
local height = util.trinary(item.tall, 3, 1)
---@class sidebar_tab
local entry = {
y_start = next_y, ---@type integer
y_end = next_y + height - 1, ---@type integer
tall = item.tall, ---@type boolean
label = item.label, ---@type string
color = item.color, ---@type cpair
callback = item.callback ---@type function|nil
}
next_y = next_y + height
tabs[i] = entry
end
draw()
end
-- element redraw
e.redraw = draw

View File

@@ -127,20 +127,19 @@ local function spinbox(args)
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
-- only handle if on an increment or decrement arrow
if e.enabled and core.events.was_clicked(event.type) and
(event.current.x ~= dec_point_x) and (event.current.y ~= 2) then
if event.current.x == event.initial.x and event.current.y == event.initial.y then
local idx = util.trinary(event.current.x > dec_point_x, event.current.x - 1, event.current.x)
if digits[idx] ~= nil then
if event.current.y == 1 then
digits[idx] = digits[idx] + 1
elseif event.current.y == 3 then
digits[idx] = digits[idx] - 1
end
update_value()
show_num()
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) and
(event.current.x ~= dec_point_x) and (event.current.y ~= 2) and
(event.current.x == event.initial.x) and (event.current.y == event.initial.y) then
local idx = util.trinary(event.current.x > dec_point_x, event.current.x - 1, event.current.x)
if digits[idx] ~= nil then
if event.current.y == 1 then
digits[idx] = digits[idx] + 1
elseif event.current.y == 3 then
digits[idx] = digits[idx] - 1
end
update_value()
show_num()
end
end
end

View File

@@ -58,14 +58,14 @@ local function switch_button(args)
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled and core.events.was_clicked(event.type) then
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then
e.value = not e.value
e.redraw()
args.callback(e.value)
end
end
-- set the value
-- set the value (does not call the callback)
---@param val boolean new value
function e.set_value(val)
e.value = val

View File

@@ -98,7 +98,7 @@ local function tabbar(args)
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
-- determine what was pressed
if e.enabled and core.events.was_clicked(event.type) then
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then
-- a button may have been pressed, which one was it?
local tab_ini = which_tab(event.initial.x)
local tab_cur = which_tab(event.current.x)

View File

@@ -1,5 +1,7 @@
-- Numeric Value Entry Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
@@ -8,11 +10,14 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class number_field_args
---@field default? number default value, defaults to 0
---@field min? number minimum, forced on unfocus
---@field max? number maximum, forced on unfocus
---@field max_digits? integer maximum number of digits, defaults to width
---@field min? number minimum, enforced on unfocus
---@field max? number maximum, enforced on unfocus
---@field max_chars? integer maximum number of characters, defaults to width
---@field max_int_digits? integer maximum number of integer digits, enforced on unfocus
---@field max_frac_digits? integer maximum number of fractional digits, enforced on unfocus
---@field allow_decimal? boolean true to allow decimals
---@field allow_negative? boolean true to allow negative numbers
---@field align_right? boolean true to align right while unfocused
---@field dis_fg_bg? cpair foreground/background colors when disabled
---@field parent graphics_element
---@field id? string element id
@@ -26,6 +31,9 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@param args number_field_args
---@return graphics_element element, element_id id
local function number_field(args)
element.assert(args.max_int_digits == nil or (util.is_int(args.max_int_digits) and args.max_int_digits > 0), "max_int_digits must be an integer greater than zero if supplied")
element.assert(args.max_frac_digits == nil or (util.is_int(args.max_frac_digits) and args.max_frac_digits > 0), "max_frac_digits must be an integer greater than zero if supplied")
args.height = 1
args.can_focus = true
@@ -34,24 +42,30 @@ local function number_field(args)
local has_decimal = false
args.max_digits = args.max_digits or e.frame.w
args.max_chars = args.max_chars or e.frame.w
-- set initial value
e.value = "" .. (args.default or 0)
-- make an interactive field manager
local ifield = core.new_ifield(e, args.max_digits, args.fg_bg, args.dis_fg_bg)
local ifield = core.new_ifield(e, args.max_chars, args.fg_bg, args.dis_fg_bg, args.align_right)
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
-- only handle if on an increment or decrement arrow
if e.enabled then
if e.enabled and e.in_frame_bounds(event.current.x, event.current.y) then
if core.events.was_clicked(event.type) then
local x = event.current.x
if not e.is_focused() then
x = ifield.get_cursor_align_shift(x)
end
e.take_focus()
if event.type == MOUSE_CLICK.UP then
ifield.move_cursor(event.current.x)
ifield.move_cursor(x)
end
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
ifield.select_all()
@@ -62,7 +76,7 @@ local function number_field(args)
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == KEY_CLICK.CHAR and string.len(e.value) < args.max_digits then
if event.type == KEY_CLICK.CHAR and string.len(e.value) < args.max_chars then
if tonumber(event.name) then
if e.value == 0 then e.value = "" end
ifield.try_insert_char(event.name)
@@ -127,6 +141,37 @@ local function number_field(args)
local min = tonumber(args.min)
if type(val) == "number" then
if args.max_int_digits or args.max_frac_digits then
local str = e.value
local ceil = false
if string.find(str, "-") then str = string.sub(e.value, 2) end
local parts = util.strtok(str, ".")
if parts[1] and args.max_int_digits then
if string.len(parts[1]) > args.max_int_digits then
parts[1] = string.rep("9", args.max_int_digits)
ceil = true
end
end
if args.allow_decimal and args.max_frac_digits then
if ceil then
parts[2] = string.rep("9", args.max_frac_digits)
elseif parts[2] and (string.len(parts[2]) > args.max_frac_digits) then
-- add a half of the highest precision fractional value in order to round using floor
local scaled = math.fmod(val, 1) * (10 ^ (args.max_frac_digits))
local value = math.floor(scaled + 0.5)
local unscaled = value * (10 ^ (-args.max_frac_digits))
parts[2] = string.sub(tostring(unscaled), 3) -- remove starting "0."
end
end
if parts[2] then parts[2] = "." .. parts[2] else parts[2] = "" end
val = tonumber((parts[1] or "") .. parts[2])
end
if type(args.max) == "number" and val > max then
e.value = "" .. max
ifield.nav_start()

View File

@@ -41,7 +41,7 @@ local function text_field(args)
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
-- only handle if on an increment or decrement arrow
if e.enabled then
if e.enabled and e.in_frame_bounds(event.current.x, event.current.y) then
if core.events.was_clicked(event.type) then
e.take_focus()

View File

@@ -9,7 +9,7 @@ local element = require("graphics.element")
---@class icon_indicator_args
---@field label string indicator label
---@field states table state color and symbol table
---@field value? integer default state, defaults to 1
---@field value? integer|boolean default state, defaults to 1 (true = 2, false = 1)
---@field min_label_width? integer label length if omitted
---@field parent graphics_element
---@field id? string element id
@@ -33,6 +33,7 @@ local function icon(args)
local e = element.new(args)
e.value = args.value or 1
if e.value == true then e.value = 2 end
-- state blit strings
local state_blit_cmds = {}
@@ -47,8 +48,11 @@ local function icon(args)
end
-- on state change
---@param new_state integer indicator state
---@param new_state integer|boolean indicator state
function e.on_update(new_state)
new_state = new_state or 1
if new_state == true then new_state = 2 end
local blit_cmd = state_blit_cmds[new_state]
e.value = new_state
e.w_set_cur(1, 1)
@@ -56,7 +60,7 @@ local function icon(args)
end
-- set indicator state
---@param val integer indicator state
---@param val integer|boolean indicator state
function e.set_value(val) e.on_update(val) end
-- element redraw

View File

@@ -6,6 +6,7 @@ local element = require("graphics.element")
---@class power_indicator_args
---@field label string indicator label
---@field unit string energy unit
---@field format string power format override (lua string format)
---@field rate boolean? whether to append /t to the end (power per tick)
---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
@@ -23,6 +24,8 @@ local element = require("graphics.element")
---@param args power_indicator_args
---@return graphics_element element, element_id id
local function power(args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.unit) == "string", "unit is a required field")
element.assert(type(args.value) == "number", "value is a required field")
element.assert(util.is_int(args.width), "width is a required field")
@@ -40,7 +43,7 @@ local function power(args)
function e.on_update(value)
e.value = value
local data_str, unit = util.power_format(value, false, args.format)
local data_str, unit = util.power_format(value, args.unit, false, args.format)
-- write data
e.w_set_cur(data_start, 1)
@@ -53,14 +56,13 @@ local function power(args)
end
-- append per tick if rate is set
-- add space to FE so we don't end up with FEE (after having kFE for example)
if args.rate == true then
unit = unit .. "/t"
if unit == "FE/t" then unit = "FE/t " end
else
if unit == "FE" then unit = "FE " end
end
-- add space to unit so we don't end up with something like FEE after having kFE
unit = util.strminw(unit, 5)
e.w_write(" " .. unit)
end

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ local function rectangle(args)
end
-- create new graphics element base object
local e = element.new(args, offset_x, offset_y)
local e = element.new(args, nil, offset_x, offset_y)
-- create content window for child elements
e.content_window = window.create(e.window, 1 + offset_x, 1 + offset_y, e.frame.w - (2 * offset_x), e.frame.h - (2 * offset_y))

View File

@@ -10,12 +10,13 @@ local ALIGN = core.ALIGN
---@class textbox_args
---@field text string text to show
---@field alignment? ALIGN text alignment, left by default
---@field anchor? boolean true to use this as an anchor, making it focusable
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field height? integer minimum necessary height for wrapped text if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
@@ -26,8 +27,22 @@ local ALIGN = core.ALIGN
local function textbox(args)
element.assert(type(args.text) == "string", "text is a required field")
if args.anchor == true then args.can_focus = true end
-- provide a constraint condition to element creation to prevent an pointlessly tall text box
---@param frame graphics_frame
local function constrain(frame)
local new_height = math.max(1, #util.strwrap(args.text, frame.w))
if args.height then
new_height = math.max(frame.h, new_height)
end
return frame.w, new_height
end
-- create new graphics element base object
local e = element.new(args)
local e = element.new(args, constrain)
e.value = args.text
@@ -42,6 +57,9 @@ local function textbox(args)
for i = 1, #lines do
if i > e.frame.h then break end
-- trim leading/trailing whitespace
lines[i] = util.trim(lines[i])
local len = string.len(lines[i])
-- use cursor position to align this line

418
graphics/themes.lua Normal file
View File

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

View File

@@ -1,27 +0,0 @@
local config = {}
-- supervisor comms channel
config.SVR_CHANNEL = 16240
-- coordinator comms channel
config.CRD_CHANNEL = 16243
-- pocket comms channel
config.PKT_CHANNEL = 16244
-- max trusted modem message distance (0 to disable check)
config.TRUSTED_RANGE = 0
-- time in seconds (>= 2) before assuming a remote device is no longer active
config.COMMS_TIMEOUT = 5
-- facility authentication key (do NOT use one of your passwords)
-- this enables verifying that messages are authentic
-- all devices on the same network must use the same key
-- config.AUTH_KEY = "SCADAfacility123"
-- log path
config.LOG_PATH = "/log.txt"
-- log mode
-- 0 = APPEND (adds to existing file on start)
-- 1 = NEW (replaces existing file on start)
config.LOG_MODE = 0
-- true to log verbose debug messages
config.LOG_DEBUG = false
return config

607
pocket/configure.lua Normal file
View File

@@ -0,0 +1,607 @@
--
-- Configuration GUI
--
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local core = require("graphics.core")
local themes = require("graphics.themes")
local DisplayBox = require("graphics.elements.displaybox")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local CheckBox = require("graphics.elements.controls.checkbox")
local PushButton = require("graphics.elements.controls.push_button")
local RadioButton = require("graphics.elements.controls.radio_button")
local NumberField = require("graphics.elements.form.number_field")
local TextField = require("graphics.elements.form.text_field")
local println = util.println
local tri = util.trinary
local cpair = core.cpair
local CENTER = core.ALIGN.CENTER
local RIGHT = core.ALIGN.RIGHT
-- 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" } }
}
---@class pkt_configurator
local configurator = {}
local style = {}
style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray)
style.colors = themes.smooth_stone.colors
local bw_fg_bg = cpair(colors.black, colors.white)
local g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
local nav_fg_bg = bw_fg_bg
local btn_act_fg_bg = cpair(colors.white, colors.gray)
local dis_fg_bg = cpair(colors.lightGray,colors.white)
local tool_ctl = {
ask_config = false,
has_config = false,
viewing_config = false,
importing_legacy = false,
view_cfg = nil, ---@type graphics_element
settings_apply = nil, ---@type graphics_element
set_networked = nil, ---@type function
bundled_emcool = nil, ---@type function
gen_summary = nil, ---@type function
show_current_cfg = nil, ---@type function
load_legacy = nil, ---@type function
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type graphics_element
auth_key_textbox = nil, ---@type graphics_element
auth_key_value = ""
}
---@class pkt_config
local tmp_cfg = {
TempScale = 1,
EnergyScale = 1,
SVR_Channel = nil, ---@type integer
CRD_Channel = nil, ---@type integer
PKT_Channel = nil, ---@type integer
ConnTimeout = nil, ---@type number
TrustedRange = nil, ---@type number
AuthKey = nil, ---@type string|nil
LogMode = 0,
LogPath = "",
LogDebug = false,
}
---@class pkt_config
local ini_cfg = {}
---@class pkt_config
local settings_cfg = {}
-- all settings fields, their nice names, and their default values
local fields = {
{ "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN },
{ "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE },
{ "SVR_Channel", "SVR Channel", 16240 },
{ "CRD_Channel", "CRD Channel", 16243 },
{ "PKT_Channel", "PKT Channel", 16244 },
{ "ConnTimeout", "Connection Timeout", 5 },
{ "TrustedRange", "Trusted Range", 0 },
{ "AuthKey", "Facility Auth Key" , ""},
{ "LogMode", "Log Mode", log.MODE.APPEND },
{ "LogPath", "Log Path", "/log.txt" },
{ "LogDebug", "Log Debug Messages", false }
}
-- load data from the settings file
---@param target pkt_config
---@param raw boolean? true to not use default values
local function load_settings(target, raw)
for _, v in pairs(fields) do settings.unset(v[1]) end
local loaded = settings.load("/pocket.settings")
for _, v in pairs(fields) do target[v[1]] = settings.get(v[1], tri(raw, nil, v[3])) end
return loaded
end
-- create the config view
---@param display graphics_element
local function config_view(display)
---@diagnostic disable-next-line: undefined-field
local function exit() os.queueEvent("terminate") end
TextBox{parent=display,y=1,text="Pocket Configurator",alignment=CENTER,fg_bg=style.header}
local root_pane_div = Div{parent=display,x=1,y=2}
local main_page = Div{parent=root_pane_div,x=1,y=1}
local ui_cfg = Div{parent=root_pane_div,x=1,y=1}
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
local summary = Div{parent=root_pane_div,x=1,y=1}
local changelog = Div{parent=root_pane_div,x=1,y=1}
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,ui_cfg,net_cfg,log_cfg,summary,changelog}}
-- Main Page
local y_start = 7
TextBox{parent=main_page,x=2,y=2,height=4,text="Welcome to the Pocket configurator! Please select one of the following options."}
if tool_ctl.ask_config then
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Please configure before starting up.",fg_bg=cpair(colors.red,colors.lightGray)}
y_start = y_start + 3
end
local function view_config()
tool_ctl.viewing_config = true
tool_ctl.gen_summary(settings_cfg)
tool_ctl.settings_apply.hide(true)
main_pane.set_value(5)
end
if fs.exists("/pocket/config.lua") then
PushButton{parent=main_page,x=2,y=y_start,min_width=22,text="Import Legacy Config",callback=function()tool_ctl.load_legacy()end,fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=btn_act_fg_bg}
y_start = y_start + 2
end
PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure Device",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg}
if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end
PushButton{parent=main_page,x=2,y=18,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
PushButton{parent=main_page,x=14,y=18,min_width=12,text="Change Log",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#region Pocket UI
local ui_c_1 = Div{parent=ui_cfg,x=2,y=4,width=24}
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=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,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}
local function submit_ui_opts()
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}
--#endregion
--#region Network
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=24}
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=24}
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=24}
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=24}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=net_c_1,x=1,y=1,text="Set network channels."}
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the named channels must be the same within a particular SCADA network.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=10,width=19,text="Coordinator Channel"}
local crd_chan = NumberField{parent=net_c_1,x=1,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=9,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"}
local pkt_chan = NumberField{parent=net_c_1,x=1,y=13,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=9,y=13,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
local chan_err = TextBox{parent=net_c_1,x=1,y=14,width=24,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_channels()
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then
tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c
net_pane.set_value(2)
chan_err.hide(true)
else chan_err.show() end
end
PushButton{parent=net_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=19,y=15,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,text="Set connection timeout."}
TextBox{parent=net_c_2,x=1,y=3,height=7,text="You generally should not need to modify this. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=11,width=19,text="Connection Timeout"}
local timeout = NumberField{parent=net_c_2,x=1,y=12,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=9,y=12,height=2,text="seconds\n(default 5)",fg_bg=g_lg_fg_bg}
local ct_err = TextBox{parent=net_c_2,x=1,y=14,width=24,text="Please set timeout.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_timeouts()
local timeout_val = tonumber(timeout.get_value())
if timeout_val ~= nil then
tmp_cfg.ConnTimeout = timeout_val
net_pane.set_value(3)
ct_err.hide(true)
else ct_err.show() end
end
PushButton{parent=net_c_2,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=19,y=15,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,text="Set the trusted range."}
TextBox{parent=net_c_3,x=1,y=3,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many blocks away.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=8,height=4,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
local range = NumberField{parent=net_c_3,x=1,y=13,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local tr_err = TextBox{parent=net_c_3,x=1,y=14,width=24,text="Set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_tr()
local range_val = tonumber(range.get_value())
if range_val ~= nil then
tmp_cfg.TrustedRange = range_val
net_pane.set_value(4)
tr_err.hide(true)
else tr_err.show() end
end
PushButton{parent=net_c_3,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=19,y=15,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_4,x=1,y=1,height=4,text="Optionally, set the facility authentication key. Do NOT use one of your passwords."}
TextBox{parent=net_c_4,x=1,y=6,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=1,y=12,text="Facility Auth Key"}
local key, _, censor = TextField{parent=net_c_4,x=1,y=13,max_len=64,value=ini_cfg.AuthKey,width=24,height=1,fg_bg=bw_fg_bg}
local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
-- declare back first so tabbing makes sense visually
PushButton{parent=net_c_4,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local hide_key = CheckBox{parent=net_c_4,x=8,y=15,label="Hide Key",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
hide_key.set_value(true)
censor_key(true)
local key_err = TextBox{parent=net_c_4,x=1,y=14,width=24,text="Length must be > 7.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_auth()
local v = key.get_value()
if string.len(v) == 0 or string.len(v) >= 8 then
tmp_cfg.AuthKey = key.get_value()
main_pane.set_value(4)
key_err.hide(true)
else key_err.show() end
end
PushButton{parent=net_c_4,x=19,y=15,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Logging
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=24}
TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
TextBox{parent=log_c_1,x=1,y=1,text="Configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=24,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
TextBox{parent=log_c_1,x=3,y=11,height=4,text="This results in much larger log files. Use only as needed.",fg_bg=g_lg_fg_bg}
local path_err = TextBox{parent=log_c_1,x=1,y=14,width=24,text="Provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_log()
if path.get_value() ~= "" then
path_err.hide(true)
tmp_cfg.LogMode = mode.get_value() - 1
tmp_cfg.LogPath = path.get_value()
tmp_cfg.LogDebug = en_dbg.get_value()
tool_ctl.gen_summary(tmp_cfg)
tool_ctl.viewing_config = false
tool_ctl.importing_legacy = false
tool_ctl.settings_apply.show()
main_pane.set_value(5)
else path_err.show() end
end
PushButton{parent=log_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=log_c_1,x=19,y=15,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Summary and Saving
local sum_c_1 = Div{parent=summary,x=2,y=4,width=24}
local sum_c_2 = Div{parent=summary,x=2,y=4,width=24}
local sum_c_3 = Div{parent=summary,x=2,y=4,width=24}
local sum_c_4 = Div{parent=summary,x=2,y=4,width=24}
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=11,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function back_from_summary()
if tool_ctl.viewing_config or tool_ctl.importing_legacy then
main_pane.set_value(1)
tool_ctl.viewing_config = false
tool_ctl.importing_legacy = false
tool_ctl.settings_apply.show()
else
main_pane.set_value(4)
end
end
---@param element graphics_element
---@param data any
local function try_set(element, data)
if data ~= nil then element.set_value(data) end
end
local function save_and_continue()
for _, field in ipairs(fields) do
local k, v = field[1], tmp_cfg[field[1]]
if v == nil then settings.unset(k) else settings.set(k, v) end
end
if settings.save("/pocket.settings") then
load_settings(settings_cfg, true)
load_settings(ini_cfg)
try_set(temp_scale, ini_cfg.TempScale)
try_set(energy_scale, ini_cfg.EnergyScale)
try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(crd_chan, ini_cfg.CRD_Channel)
try_set(pkt_chan, ini_cfg.PKT_Channel)
try_set(timeout, ini_cfg.ConnTimeout)
try_set(range, ini_cfg.TrustedRange)
try_set(key, ini_cfg.AuthKey)
try_set(mode, ini_cfg.LogMode)
try_set(path, ini_cfg.LogPath)
try_set(en_dbg, ini_cfg.LogDebug)
tool_ctl.view_cfg.enable()
if tool_ctl.importing_legacy then
tool_ctl.importing_legacy = false
sum_pane.set_value(3)
else
sum_pane.set_value(2)
end
else
sum_pane.set_value(4)
end
end
PushButton{parent=sum_c_1,x=1,y=15,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=1,y=13,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg}
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=18,y=15,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"}
local function go_home()
main_pane.set_value(1)
net_pane.set_value(1)
sum_pane.set_value(1)
end
PushButton{parent=sum_c_2,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_2,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=sum_c_3,x=1,y=1,height=4,text="The old config.lua file will now be deleted, then the configurator will exit."}
local function delete_legacy()
fs.delete("/pocket/config.lua")
exit()
end
PushButton{parent=sum_c_3,x=1,y=15,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_3,x=19,y=15,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=sum_c_4,x=1,y=1,height=8,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
PushButton{parent=sum_c_4,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_4,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
--#endregion
-- Config Change Log
local cl = Div{parent=changelog,x=2,y=4,width=24}
TextBox{parent=changelog,x=1,y=2,text=" Config Change Log",fg_bg=bw_fg_bg}
local c_log = ListBox{parent=cl,x=1,y=1,height=13,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
for _, change in ipairs(changes) do
TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
for _, v in ipairs(change[2]) do
local e = Div{parent=c_log,height=#util.strwrap(v,21)}
TextBox{parent=e,y=1,x=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
end
end
PushButton{parent=cl,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
-- set tool functions now that we have the elements
-- load a legacy config file
function tool_ctl.load_legacy()
local config = require("pocket.config")
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
tmp_cfg.CRD_Channel = config.CRD_CHANNEL
tmp_cfg.PKT_Channel = config.PKT_CHANNEL
tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT
tmp_cfg.TrustedRange = config.TRUSTED_RANGE
tmp_cfg.AuthKey = config.AUTH_KEY or ""
tmp_cfg.LogMode = config.LOG_MODE
tmp_cfg.LogPath = config.LOG_PATH
tmp_cfg.LogDebug = config.LOG_DEBUG or false
tool_ctl.gen_summary(tmp_cfg)
sum_pane.set_value(1)
main_pane.set_value(5)
tool_ctl.importing_legacy = true
end
-- expose the auth key on the summary page
function tool_ctl.show_auth_key()
tool_ctl.show_key_btn.disable()
tool_ctl.auth_key_textbox.set_value(tool_ctl.auth_key_value)
end
-- generate the summary list
---@param cfg pkt_config
function tool_ctl.gen_summary(cfg)
setting_list.remove_all()
local alternate = false
local inner_width = setting_list.get_width() - 1
tool_ctl.show_key_btn.enable()
tool_ctl.auth_key_value = cfg.AuthKey or "" -- to show auth key
for i = 1, #fields do
local f = fields[i]
local height = 1
local label_w = string.len(f[2])
local val_max_w = (inner_width - label_w) - 1
local raw = cfg[f[1]]
local val = util.strval(raw)
if f[1] == "AuthKey" then
val = string.rep("*", string.len(val))
elseif f[1] == "LogMode" then
val = util.trinary(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "TempScale" then
val = util.strval(types.TEMP_SCALE_NAMES[raw])
elseif f[1] == "EnergyScale" then
val = util.strval(types.ENERGY_SCALE_NAMES[raw])
end
if val == "nil" then val = "<not set>" end
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if string.len(val) > val_max_w then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end
local line = Div{parent=setting_list,height=height,fg_bg=c}
TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)}
local textbox
if height > 1 then
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end
if f[1] == "AuthKey" then tool_ctl.auth_key_textbox = textbox end
end
end
end
-- reset terminal screen
local function reset_term()
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
end
-- run the pcoket configurator
---@param ask_config? boolean indicate if this is being called by the startup app due to an invalid configuration
function configurator.configure(ask_config)
tool_ctl.ask_config = ask_config == true
load_settings(settings_cfg, true)
tool_ctl.has_config = load_settings(ini_cfg)
reset_term()
-- set overridden colors
for i = 1, #style.colors do
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
end
local status, error = pcall(function ()
local display = DisplayBox{window=term.current(),fg_bg=style.root}
config_view(display)
while true do
local event, param1, param2, param3 = util.pull_event()
-- handle event
if event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
local m_e = core.events.new_mouse_event(event, param1, param2, param3)
if m_e then display.handle_mouse(m_e) end
elseif event == "char" or event == "key" or event == "key_up" then
local k_e = core.events.new_key_event(event, param1, param2)
if k_e then display.handle_key(k_e) end
elseif event == "paste" then
display.handle_paste(param1)
end
if event == "terminate" then return end
end
end)
-- restore colors
for i = 1, #style.colors do
local r, g, b = term.nativePaletteColor(style.colors[i].c)
term.setPaletteColor(style.colors[i].c, r, g, b)
end
reset_term()
if not status then
println("configurator error: " .. error)
end
return status, error
end
return configurator

View File

@@ -2,19 +2,27 @@
-- I/O Control for Pocket Integration with Supervisor & Coordinator
--
local psil = require("scada-common.psil")
local const = require("scada-common.constants")
local psil = require("scada-common.psil")
local types = require("scada-common.types")
local util = require("scada-common.util")
local types = require("scada-common.types")
local process = require("pocket.process")
local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE
local ENERGY_SCALE = types.ENERGY_SCALE
local ENERGY_UNITS = types.ENERGY_SCALE_UNITS
local TEMP_SCALE = types.TEMP_SCALE
local TEMP_UNITS = types.TEMP_SCALE_UNITS
---@todo nominal trip time is ping (0ms to 10ms usually)
local WARN_TT = 40
local HIGH_TT = 80
local iocontrol = {}
---@class pocket_ioctl
local io = {
ps = psil.create()
}
---@enum POCKET_LINK_STATE
local LINK_STATE = {
UNLINKED = 0,
@@ -23,23 +31,36 @@ local LINK_STATE = {
LINKED = 3
}
---@enum NAV_PAGE
local NAV_PAGE = {
HOME = 1,
UNITS = 2,
REACTORS = 3,
BOILERS = 4,
TURBINES = 5,
DIAG = 6,
D_ALARMS = 7
iocontrol.LINK_STATE = LINK_STATE
---@class pocket_ioctl
local io = {
version = "unknown",
ps = psil.create()
}
iocontrol.LINK_STATE = LINK_STATE
iocontrol.NAV_PAGE = NAV_PAGE
-- luacheck: no unused args
-- placeholder acknowledge function for type hinting
---@param success boolean
---@diagnostic disable-next-line: unused-local
local function __generic_ack(success) end
-- luacheck: unused args
local config = nil ---@type pkt_config
local comms = nil ---@type pocket_comms
-- initialize facility-independent components of pocket iocontrol
---@param comms pocket_comms
function iocontrol.init_core(comms)
---@param pkt_comms pocket_comms
---@param nav pocket_nav
---@param cfg pkt_config
function iocontrol.init_core(pkt_comms, nav, cfg)
comms = pkt_comms
config = cfg
io.nav = nav
---@class pocket_ioctl_diag
io.diag = {}
@@ -77,28 +98,835 @@ function iocontrol.init_core(comms)
tone_indicators = {} -- indicators to update from supervisor tone states
}
---@class pocket_nav
io.nav = {
page = NAV_PAGE.HOME, ---@type NAV_PAGE
sub_pages = { NAV_PAGE.HOME, NAV_PAGE.UNITS, NAV_PAGE.REACTORS, NAV_PAGE.BOILERS, NAV_PAGE.TURBINES, NAV_PAGE.DIAG },
tasks = {}
-- API access
---@class pocket_ioctl_api
io.api = {
get_unit = function (unit) comms.api__get_unit(unit) end
}
-- add a task to be performed periodically while on a given page
---@param page NAV_PAGE page to add task to
---@param task function function to execute
function io.nav.register_task(page, task)
if io.nav.tasks[page] == nil then io.nav.tasks[page] = {} end
table.insert(io.nav.tasks[page], task)
end
end
-- initialize facility-dependent components of pocket iocontrol
function iocontrol.init_fac() end
---@param conf facility_conf facility configuration
function iocontrol.init_fac(conf)
local temp_scale, energy_scale = config.TempScale, config.EnergyScale
io.temp_label = TEMP_UNITS[temp_scale]
io.energy_label = ENERGY_UNITS[energy_scale]
-- temperature unit label and conversion function (from Kelvin)
if temp_scale == TEMP_SCALE.CELSIUS then
io.temp_convert = function (t) return t - 273.15 end
elseif temp_scale == TEMP_SCALE.FAHRENHEIT then
io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end
elseif temp_scale == TEMP_SCALE.RANKINE then
io.temp_convert = function (t) return 1.8 * t end
else
io.temp_label = "K"
io.temp_convert = function (t) return t end
end
-- energy unit label and conversion function (from Joules unless otherwise specified)
if energy_scale == ENERGY_SCALE.FE or energy_scale == ENERGY_SCALE.RF then
io.energy_convert = util.joules_to_fe_rf
io.energy_convert_from_fe = function (t) return t end
io.energy_convert_to_fe = function (t) return t end
else
io.energy_label = "J"
io.energy_convert = function (t) return t end
io.energy_convert_from_fe = util.fe_rf_to_joules
io.energy_convert_to_fe = util.joules_to_fe_rf
end
-- facility data structure
---@class pioctl_facility
io.facility = {
num_units = conf.num_units,
tank_mode = conf.cooling.fac_tank_mode,
tank_defs = conf.cooling.fac_tank_defs,
all_sys_ok = false,
rtu_count = 0,
auto_ready = false,
auto_active = false,
auto_ramping = false,
auto_saturated = false,
auto_scram = false,
---@type ascram_status
ascram_status = {
matrix_dc = false,
matrix_fill = false,
crit_alarm = false,
radiation = false,
gen_fault = false
},
---@type WASTE_PRODUCT
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
auto_pu_fallback_active = false,
radiation = types.new_zero_radiation_reading(),
start_ack = __generic_ack,
stop_ack = __generic_ack,
scram_ack = __generic_ack,
ack_alarms_ack = __generic_ack,
ps = psil.create(),
induction_ps_tbl = {},
induction_data_tbl = {},
sps_ps_tbl = {},
sps_data_tbl = {},
tank_ps_tbl = {},
tank_data_tbl = {},
env_d_ps = psil.create(),
env_d_data = {}
}
-- create induction and SPS tables (currently only 1 of each is supported)
table.insert(io.facility.induction_ps_tbl, psil.create())
table.insert(io.facility.induction_data_tbl, {})
table.insert(io.facility.sps_ps_tbl, psil.create())
table.insert(io.facility.sps_data_tbl, {})
-- determine tank information
if io.facility.tank_mode == 0 then
io.facility.tank_defs = {}
-- on facility tank mode 0, setup tank defs to match unit tank option
for i = 1, conf.num_units do
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TankConnection, 1, 0)
end
io.facility.tank_list = { table.unpack(io.facility.tank_defs) }
else
-- decode the layout of tanks from the connections definitions
local tank_mode = io.facility.tank_mode
local tank_defs = io.facility.tank_defs
local tank_list = { table.unpack(tank_defs) }
local function calc_fdef(start_idx, end_idx)
local first = 4
for i = start_idx, end_idx do
if io.facility.tank_defs[i] == 2 then
if i < first then first = i end
end
end
return first
end
if tank_mode == 1 then
-- (1) 1 total facility tank (A A A A)
local first_fdef = calc_fdef(1, #tank_defs)
for i = 1, #tank_defs do
if i > first_fdef and tank_defs[i] == 2 then
tank_list[i] = 0
end
end
elseif tank_mode == 2 then
-- (2) 2 total facility tanks (A A A B)
local first_fdef = calc_fdef(1, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 3 then
-- (3) 2 total facility tanks (A A B B)
for _, a in pairs({ 1, 3 }) do
local b = a + 1
if (tank_defs[a] == 2) and (tank_defs[b] == 2) then
tank_list[b] = 0
end
end
elseif tank_mode == 4 then
-- (4) 2 total facility tanks (A B B B)
local first_fdef = calc_fdef(2, #tank_defs)
for i = 1, #tank_defs do
if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 5 then
-- (5) 3 total facility tanks (A A B C)
local first_fdef = calc_fdef(1, math.min(2, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 6 then
-- (6) 3 total facility tanks (A B B C)
local first_fdef = calc_fdef(2, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 7 then
-- (7) 3 total facility tanks (A B C C)
local first_fdef = calc_fdef(3, #tank_defs)
for i = 1, #tank_defs do
if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
end
io.facility.tank_list = tank_list
end
-- create facility tank tables
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 = {}
for i = 1, conf.num_units do
---@class pioctl_unit
local entry = {
unit_id = i,
connected = false,
rtu_hw = {},
num_boilers = 0,
num_turbines = 0,
num_snas = 0,
has_tank = conf.cooling.r_cool[i].TankConnection,
control_state = false,
burn_rate_cmd = 0.0,
radiation = types.new_zero_radiation_reading(),
sna_peak_rate = 0.0,
sna_max_rate = 0.0,
sna_out_rate = 0.0,
waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
waste_product = types.WASTE_PRODUCT.PLUTONIUM,
last_rate_change_ms = 0,
turbine_flow_stable = false,
-- auto control group
a_group = types.AUTO_GROUP.MANUAL,
start = function () process.start(i) end,
scram = function () process.scram(i) end,
reset_rps = function () process.reset_rps(i) end,
ack_alarms = function () process.ack_all_alarms(i) end,
set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate
start_ack = __generic_ack,
scram_ack = __generic_ack,
reset_rps_ack = __generic_ack,
ack_alarms_ack = __generic_ack,
---@type alarms
alarms = { ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE },
annunciator = {}, ---@type annunciator
unit_ps = psil.create(),
reactor_data = {}, ---@type reactor_db
boiler_ps_tbl = {},
boiler_data_tbl = {},
turbine_ps_tbl = {},
turbine_data_tbl = {},
tank_ps_tbl = {},
tank_data_tbl = {}
}
-- on other facility modes, overwrite unit TANK option with facility tank defs
if io.facility.tank_mode ~= 0 then
entry.has_tank = conf.cooling.fac_tank_defs[i] > 0
end
-- create boiler tables
for _ = 1, conf.cooling.r_cool[i].BoilerCount do
table.insert(entry.boiler_ps_tbl, psil.create())
table.insert(entry.boiler_data_tbl, {})
end
-- create turbine tables
for _ = 1, conf.cooling.r_cool[i].TurbineCount do
table.insert(entry.turbine_ps_tbl, psil.create())
table.insert(entry.turbine_data_tbl, {})
end
-- create tank tables
if io.facility.tank_defs[i] == 1 then
table.insert(entry.tank_ps_tbl, psil.create())
table.insert(entry.tank_data_tbl, {})
end
entry.num_boilers = #entry.boiler_data_tbl
entry.num_turbines = #entry.turbine_data_tbl
table.insert(io.units, entry)
end
-- pass IO control here since it can't be require'd due to a require loop
process.init(io, comms)
end
-- set network link state
---@param state POCKET_LINK_STATE
function iocontrol.report_link_state(state) io.ps.publish("link_state", state) end
---@param sv_addr integer|false|nil supervisor address if linked, nil if unchanged, false if unlinked
---@param api_addr integer|false|nil coordinator address if linked, nil if unchanged, false if unlinked
function iocontrol.report_link_state(state, sv_addr, api_addr)
io.ps.publish("link_state", state)
if state == LINK_STATE.API_LINK_ONLY or state == LINK_STATE.UNLINKED then
io.ps.publish("svr_conn_quality", 0)
end
if state == LINK_STATE.SV_LINK_ONLY or state == LINK_STATE.UNLINKED then
io.ps.publish("crd_conn_quality", 0)
end
if sv_addr then
io.ps.publish("sv_addr", util.c(sv_addr, ":", config.SVR_Channel))
elseif sv_addr == false then
io.ps.publish("sv_addr", "unknown (not linked)")
end
if api_addr then
io.ps.publish("api_addr", util.c(api_addr, ":", config.CRD_Channel))
elseif api_addr == false then
io.ps.publish("api_addr", "unknown (not linked)")
end
end
-- show the reason the supervisor connection isn't linking
function iocontrol.report_svr_link_error(msg) io.ps.publish("svr_link_msg", msg) end
-- show the reason the coordinator api connection isn't linking
function iocontrol.report_crd_link_error(msg) io.ps.publish("api_link_msg", msg) end
-- determine supervisor connection quality (trip time)
---@param trip_time integer
function iocontrol.report_svr_tt(trip_time)
local state = 3
if trip_time > HIGH_TT then
state = 1
elseif trip_time > WARN_TT then
state = 2
end
io.ps.publish("svr_conn_quality", state)
end
-- determine coordinator connection quality (trip time)
---@param trip_time integer
function iocontrol.report_crd_tt(trip_time)
local state = 3
if trip_time > HIGH_TT then
state = 1
elseif trip_time > WARN_TT then
state = 2
end
io.ps.publish("crd_conn_quality", state)
end
-- populate facility data from API_GET_FAC
---@param data table
---@return boolean valid
function iocontrol.record_facility_data(data)
local valid = true
local fac = io.facility
fac.all_sys_ok = data[1]
fac.rtu_count = data[2]
fac.radiation = data[3]
-- auto control
if type(data[4]) == "table" and #data[4] == 4 then
fac.auto_ready = data[4][1]
fac.auto_active = data[4][2]
fac.auto_ramping = data[4][3]
fac.auto_saturated = data[4][4]
end
-- waste
if type(data[5]) == "table" and #data[5] == 2 then
fac.auto_current_waste_product = data[5][1]
fac.auto_pu_fallback_active = data[5][2]
end
fac.num_tanks = data[6]
fac.has_imatrix = data[7]
fac.has_sps = data[8]
return valid
end
local function tripped(state) return state == ALARM_STATE.TRIPPED or state == ALARM_STATE.ACKED end
local function _record_multiblock_status(faulted, data, ps)
ps.publish("formed", data.formed)
ps.publish("faulted", faulted)
for key, val in pairs(data.state) do ps.publish(key, val) end
for key, val in pairs(data.tanks) do ps.publish(key, val) end
end
-- update unit status data from API_GET_UNIT
---@param data table
function iocontrol.record_unit_data(data)
local unit = io.units[data[1]] ---@type pioctl_unit
unit.connected = data[2]
unit.rtu_hw = data[3]
unit.a_group = data[4]
unit.alarms = data[5]
unit.unit_ps.publish("auto_group_id", unit.a_group)
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
--#region Annunciator
unit.annunciator = data[6]
local rcs_disconn, rcs_warn, rcs_hazard = false, false, false
for key, val in pairs(unit.annunciator) do
if key == "BoilerOnline" or key == "TurbineOnline" then
local every = true
-- split up online arrays
for id = 1, #val do
every = every and val[id]
if key == "BoilerOnline" then
unit.boiler_ps_tbl[id].publish(key, val[id])
else
unit.turbine_ps_tbl[id].publish(key, val[id])
end
end
if not every then rcs_disconn = true end
unit.unit_ps.publish("U_" .. key, every)
elseif key == "HeatingRateLow" or key == "WaterLevelLow" then
-- split up array for all boilers
local any = false
for id = 1, #val do
any = any or val[id]
unit.boiler_ps_tbl[id].publish(key, val[id])
end
if key == "HeatingRateLow" and any then
rcs_warn = true
elseif key == "WaterLevelLow" and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
elseif key == "SteamDumpOpen" or key == "TurbineOverSpeed" or key == "GeneratorTrip" or key == "TurbineTrip" then
-- split up array for all turbines
local any = false
for id = 1, #val do
any = any or val[id]
unit.turbine_ps_tbl[id].publish(key, val[id])
end
if key == "GeneratorTrip" and any then
rcs_warn = true
elseif (key == "TurbineOverSpeed" or key == "TurbineTrip") and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
else
-- non-table fields
unit.unit_ps.publish(key, val)
end
end
local anc = unit.annunciator
rcs_hazard = rcs_hazard or anc.RCPTrip
rcs_warn = rcs_warn or anc.RCSFlowLow or anc.CoolantLevelLow or anc.RCSFault or anc.MaxWaterReturnFeed or
anc.CoolantFeedMismatch or anc.BoilRateMismatch or anc.SteamFeedMismatch
local rcs_status = 4
if rcs_hazard then
rcs_status = 2
elseif rcs_warn then
rcs_status = 3
elseif rcs_disconn then
rcs_status = 1
end
unit.unit_ps.publish("U_RCS", rcs_status)
--#endregion
--#region Reactor Data
unit.reactor_data = data[7]
local control_status = 1
local reactor_status = 1
local reactor_state = 1
local rps_status = 1
if unit.connected then
-- update RPS status
if unit.reactor_data.rps_tripped then
control_status = 2
if unit.reactor_data.rps_trip_cause == "manual" then
reactor_state = 4 -- disabled
rps_status = 3
else
reactor_state = 6 -- SCRAM
rps_status = 2
end
else
rps_status = 4
reactor_state = 4
end
-- update reactor/control status
if unit.reactor_data.mek_status.status then
reactor_status = 4
reactor_state = 5 -- running
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
else
if unit.reactor_data.no_reactor then
reactor_status = 2
reactor_state = 3 -- faulted
elseif not unit.reactor_data.formed then
reactor_status = 3
reactor_state = 2 -- not formed
elseif unit.reactor_data.rps_status.force_dis then
reactor_status = 3
reactor_state = 7 -- force disabled
else
reactor_status = 4
end
end
for key, val in pairs(unit.reactor_data) do
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
unit.unit_ps.publish(key, val)
end
end
if type(unit.reactor_data.rps_status) == "table" then
for key, val in pairs(unit.reactor_data.rps_status) do
unit.unit_ps.publish(key, val)
end
end
if type(unit.reactor_data.mek_status) == "table" then
for key, val in pairs(unit.reactor_data.mek_status) do
unit.unit_ps.publish(key, val)
end
end
end
unit.unit_ps.publish("U_ControlStatus", control_status)
unit.unit_ps.publish("U_ReactorStatus", reactor_status)
unit.unit_ps.publish("U_ReactorStateStatus", reactor_state)
unit.unit_ps.publish("U_RPS", rps_status)
--#endregion
--#region RTU Devices
unit.boiler_data_tbl = data[8]
for id = 1, #unit.boiler_data_tbl do
local boiler = unit.boiler_data_tbl[id] ---@type boilerv_session_db
local ps = unit.boiler_ps_tbl[id] ---@type psil
local boiler_status = 1
local computed_status = 1
if unit.rtu_hw.boilers[id].connected then
if unit.rtu_hw.boilers[id].faulted then
boiler_status = 3
computed_status = 3
elseif boiler.formed then
boiler_status = 4
if boiler.state.boil_rate > 0 then
computed_status = 5
else
computed_status = 4
end
else
boiler_status = 2
computed_status = 2
end
_record_multiblock_status(unit.rtu_hw.boilers[id].faulted, boiler, ps)
end
ps.publish("BoilerStatus", boiler_status)
ps.publish("BoilerStateStatus", computed_status)
end
unit.turbine_data_tbl = data[9]
for id = 1, #unit.turbine_data_tbl do
local turbine = unit.turbine_data_tbl[id] ---@type turbinev_session_db
local ps = unit.turbine_ps_tbl[id] ---@type psil
local turbine_status = 1
local computed_status = 1
if unit.rtu_hw.turbines[id].connected then
if unit.rtu_hw.turbines[id].faulted then
turbine_status = 3
computed_status = 3
elseif turbine.formed then
turbine_status = 4
if turbine.tanks.energy_fill >= 0.99 then
computed_status = 6
elseif turbine.state.flow_rate < 100 then
computed_status = 4
else
computed_status = 5
end
else
turbine_status = 2
computed_status = 2
end
_record_multiblock_status(unit.rtu_hw.turbines[id].faulted, turbine, ps)
end
ps.publish("TurbineStatus", turbine_status)
ps.publish("TurbineStateStatus", computed_status)
end
unit.tank_data_tbl = data[10]
unit.last_rate_change_ms = data[11]
unit.turbine_flow_stable = data[12]
--#endregion
--#region Status Information Display
local ecam = {} -- aviation reference :)
-- local function red(text) return { text = text, color = colors.red } end
local function white(text) return { text = text, color = colors.white } end
local function blue(text) return { text = text, color = colors.blue } end
-- unit.reactor_data.rps_status = {
-- high_dmg = false,
-- high_temp = false,
-- low_cool = false,
-- ex_waste = false,
-- ex_hcool = false,
-- no_fuel = false,
-- fault = false,
-- timeout = false,
-- manual = false,
-- automatic = false,
-- sys_fail = false,
-- force_dis = false
-- }
-- if unit.reactor_data.rps_status then
-- for k, v in pairs(unit.alarms) do
-- unit.alarms[k] = ALARM_STATE.TRIPPED
-- end
-- end
if tripped(unit.alarms[ALARM.ContainmentBreach]) then
local items = { white("REACTOR MELTDOWN"), blue("DON HAZMAT SUIT") }
table.insert(ecam, { color = colors.red, text = "CONTAINMENT BREACH", help = "ContainmentBreach", items = items })
end
if tripped(unit.alarms[ALARM.ContainmentRadiation]) then
local items = {
white("RADIATION DETECTED"),
blue("DON HAZMAT SUIT"),
blue("RESOLVE LEAK"),
blue("AWAIT SAFE LEVELS")
}
table.insert(ecam, { color = colors.red, text = "RADIATION LEAK", help = "ContainmentRadiation", items = items })
end
if tripped(unit.alarms[ALARM.CriticalDamage]) then
local items = { white("MELTDOWN IMMINENT"), blue("EVACUATE") }
table.insert(ecam, { color = colors.red, text = "RCT DAMAGE CRITICAL", help = "CriticalDamage", items = items })
end
if tripped(unit.alarms[ALARM.ReactorLost]) then
local items = { white("REACTOR OFF-LINE"), blue("CHECK PLC") }
table.insert(ecam, { color = colors.red, text = "REACTOR CONN LOST", help = "ReactorLost", items = items })
end
if tripped(unit.alarms[ALARM.ReactorDamage]) then
local items = { white("REACTOR DAMAGED"), blue("CHECK RCS"), blue("AWAIT DMG REDUCED") }
table.insert(ecam, { color = colors.red, text = "REACTOR DAMAGE", help = "ReactorDamage", items = items })
end
if tripped(unit.alarms[ALARM.ReactorOverTemp]) then
local items = { white("DAMAGING TEMP"), blue("CHECK RCS"), blue("AWAIT COOLDOWN") }
table.insert(ecam, { color = colors.red, text = "REACTOR OVER TEMP", help = "ReactorOverTemp", items = items })
end
if tripped(unit.alarms[ALARM.ReactorHighTemp]) then
local items = { white("OVER EXPECTED TEMP"), blue("CHECK RCS") }
table.insert(ecam, { color = colors.yellow, text = "REACTOR HIGH TEMP", help = "ReactorHighTemp", items = items})
end
if tripped(unit.alarms[ALARM.ReactorWasteLeak]) then
local items = { white("AT WASTE CAPACITY"), blue("CHECK WASTE OUTPUT"), blue("KEEP RCT DISABLED") }
table.insert(ecam, { color = colors.red, text = "REACTOR WASTE LEAK", help = "ReactorWasteLeak", items = items})
end
if tripped(unit.alarms[ALARM.ReactorHighWaste]) then
local items = { blue("CHECK WASTE OUTPUT") }
table.insert(ecam, { color = colors.yellow, text = "REACTOR WASTE HIGH", help = "ReactorHighWaste", items = items})
end
if tripped(unit.alarms[ALARM.RPSTransient]) then
local items = {}
local stat = unit.reactor_data.rps_status
-- for k, _ in pairs(stat) do stat[k] = true end
local function insert(cond, key, text, color) if cond[key] then table.insert(items, { text = text, help = key, color = color }) end end
table.insert(items, white("REACTOR SCRAMMED"))
insert(stat, "high_dmg", "HIGH DAMAGE", colors.red)
insert(stat, "high_temp", "HIGH TEMPERATURE", colors.red)
insert(stat, "low_cool", "CRIT LOW COOLANT")
insert(stat, "ex_waste", "EXCESS WASTE")
insert(stat, "ex_hcool", "EXCESS HEATED COOL")
insert(stat, "no_fuel", "NO FUEL")
insert(stat, "fault", "HARDWARE FAULT")
insert(stat, "timeout", "SUPERVISOR DISCONN")
insert(stat, "manual", "MANUAL SCRAM", colors.white)
insert(stat, "automatic", "AUTOMATIC SCRAM")
insert(stat, "sys_fail", "NOT FORMED", colors.red)
insert(stat, "force_dis", "FORCE DISABLED", colors.red)
table.insert(items, blue("RESOLVE PROBLEM"))
table.insert(items, blue("RESET RPS"))
table.insert(ecam, { color = colors.yellow, text = "RPS TRANSIENT", help = "RPSTransient", items = items})
end
if tripped(unit.alarms[ALARM.RCSTransient]) then
local items = {}
local annunc = unit.annunciator
-- for k, v in pairs(annunc) do
-- if type(v) == "boolean" then annunc[k] = true end
-- if type(v) == "table" then
-- for a, _ in pairs(v) do
-- v[a] = true
-- end
-- end
-- end
local function insert(cond, key, text, color)
if cond == true or (type(cond) == "table" and cond[key]) then table.insert(items, { text = text, help = key, color = color }) end
end
table.insert(items, white("COOLANT PROBLEM"))
insert(annunc, "RCPTrip", "RCP TRIP", colors.red)
insert(annunc, "CoolantLevelLow", "LOW COOLANT")
if unit.num_boilers == 0 then
if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then
insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH")
end
if unit.turbine_flow_stable then
insert(annunc, "RCSFlowLow", "RCS FLOW LOW")
insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH")
insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH")
end
else
if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then
insert(annunc, "RCSFlowLow", "RCS FLOW LOW")
insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH")
insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH")
end
if unit.turbine_flow_stable then
insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH")
end
end
insert(annunc, "MaxWaterReturnFeed", "MAX WTR RTRN FEED")
for k, v in ipairs(annunc.WaterLevelLow) do insert(v, "WaterLevelLow", "BOILER " .. k .. " WTR LOW", colors.red) end
for k, v in ipairs(annunc.HeatingRateLow) do insert(v, "HeatingRateLow", "BOILER " .. k .. " HEAT RATE") end
for k, v in ipairs(annunc.TurbineOverSpeed) do insert(v, "TurbineOverSpeed", "TURBINE " .. k .. " OVERSPD", colors.red) end
for k, v in ipairs(annunc.GeneratorTrip) do insert(v, "GeneratorTrip", "TURBINE " .. k .. " GEN TRIP") end
table.insert(items, blue("CHECK COOLING SYS"))
table.insert(ecam, { color = colors.yellow, text = "RCS TRANSIENT", help = "RCSTransient", items = items})
end
if tripped(unit.alarms[ALARM.TurbineTrip]) then
local items = {}
for k, v in ipairs(unit.annunciator.TurbineTrip) do
if v then table.insert(items, { text = "TURBINE " .. k .. " TRIP", help = "TurbineTrip" }) end
end
table.insert(items, blue("CHECK ENERGY OUT"))
table.insert(ecam, { color = colors.red, text = "TURBINE TRIP", help = "TurbineTripAlarm", items = items})
end
if not (tripped(unit.alarms[ALARM.ReactorLost]) or unit.connected) then
local items = { blue("CHECK PLC") }
table.insert(ecam, { color = colors.yellow, text = "REACTOR OFF-LINE", items = items })
end
for k, v in ipairs(unit.annunciator.BoilerOnline) do
if not v then
local items = { blue("CHECK RTU") }
table.insert(ecam, { color = colors.yellow, text = "BOILER " .. k .. " OFF-LINE", items = items})
end
end
for k, v in ipairs(unit.annunciator.TurbineOnline) do
if not v then
local items = { blue("CHECK RTU") }
table.insert(ecam, { color = colors.yellow, text = "TURBINE " .. k .. " OFF-LINE", items = items})
end
end
-- if no alarms, put some basic status messages in
if #ecam == 0 then
table.insert(ecam, { color = colors.green, text = "REACTOR " .. util.trinary(unit.reactor_data.mek_status.status, "NOMINAL", "IDLE"), items = {}})
local plural = util.trinary(unit.num_turbines > 1, "S", "")
table.insert(ecam, { color = colors.green, text = "TURBINE" .. plural .. util.trinary(unit.turbine_flow_stable, " STABLE", " STABILIZING"), items = {}})
end
unit.unit_ps.publish("U_ECAM", textutils.serialize(ecam))
--#endregion
end
-- get the IO controller database
function iocontrol.get_db() return io end

View File

@@ -8,47 +8,395 @@ local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE
local CRDN_TYPE = comms.CRDN_TYPE
local UNIT_COMMAND = comms.UNIT_COMMAND
local FAC_COMMAND = comms.FAC_COMMAND
local LINK_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
local config = {}
pocket.config = config
-- load the pocket configuration
function pocket.load_config()
if not settings.load("/pocket.settings") then return false end
config.TempScale = settings.get("TempScale")
config.EnergyScale = settings.get("EnergyScale")
config.SVR_Channel = settings.get("SVR_Channel")
config.CRD_Channel = settings.get("CRD_Channel")
config.PKT_Channel = settings.get("PKT_Channel")
config.ConnTimeout = settings.get("ConnTimeout")
config.TrustedRange = settings.get("TrustedRange")
config.AuthKey = settings.get("AuthKey")
config.LogMode = settings.get("LogMode")
config.LogPath = settings.get("LogPath")
config.LogDebug = settings.get("LogDebug")
local cfv = util.new_validator()
cfv.assert_type_int(config.TempScale)
cfv.assert_range(config.TempScale, 1, 4)
cfv.assert_type_int(config.EnergyScale)
cfv.assert_range(config.EnergyScale, 1, 3)
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.CRD_Channel)
cfv.assert_channel(config.PKT_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)
if type(config.AuthKey) == "string" then
local len = string.len(config.AuthKey)
cfv.assert(len == 0 or len >= 8)
end
cfv.assert_type_int(config.LogMode)
cfv.assert_range(config.LogMode, 0, 1)
cfv.assert_type_str(config.LogPath)
cfv.assert_type_bool(config.LogDebug)
return cfv.valid()
end
---@enum POCKET_APP_ID
local APP_ID = {
ROOT = 1,
LOADER = 2,
-- main app pages
UNITS = 3,
CONTROL = 4,
GUIDE = 5,
ABOUT = 6,
-- diag app page
ALARMS = 7,
-- other
DUMMY = 8,
NUM_APPS = 8
}
pocket.APP_ID = APP_ID
---@class nav_tree_page
---@field _p nav_tree_page|nil page's parent
---@field _c table page's children
---@field nav_to function function to navigate to this page
---@field switcher function|nil function to switch between children
---@field tasks table tasks to run while viewing this page
-- initialize the page navigation system
---@param smem pkt_shared_memory
function pocket.init_nav(smem)
local self = {
pane = nil, ---@type graphics_element
sidebar = nil, ---@type graphics_element
apps = {},
containers = {},
help_map = {},
help_return = nil,
loader_return = nil,
cur_app = APP_ID.ROOT
}
self.cur_page = self.root
---@class pocket_nav
local nav = {}
-- set the root pane element to switch between apps with
---@param root_pane graphics_element
function nav.set_pane(root_pane) self.pane = root_pane end
-- link sidebar element
---@param sidebar graphics_element
function nav.set_sidebar(sidebar) self.sidebar = sidebar end
-- register an app
---@param app_id POCKET_APP_ID app ID
---@param container graphics_element element that contains this app (usually a Div)
---@param pane? graphics_element multipane if this is a simple paned app, then nav_to must be a number
---@param require_sv? boolean true to specifiy if this app should be unloaded when the supervisor connection is lost
---@param require_api? boolean true to specifiy if this app should be unloaded when the api connection is lost
function nav.register_app(app_id, container, pane, require_sv, require_api)
---@class pocket_app
local app = {
loaded = false,
cur_page = nil, ---@type nav_tree_page
pane = pane,
paned_pages = {},
sidebar_items = {}
}
app.load = function () app.loaded = true end
app.unload = function () app.loaded = false end
-- check which connections this requires (for unload)
---@return boolean requires_sv, boolean requires_api
function app.check_requires() return require_sv or false, require_api or false end
-- check if any connection is required (for load)
function app.requires_conn() return require_sv or require_api or false end
-- delayed set of the pane if it wasn't ready at the start
---@param root_pane graphics_element multipane
function app.set_root_pane(root_pane)
app.pane = root_pane
end
-- configure the sidebar
---@param items table
function app.set_sidebar(items)
app.sidebar_items = items
if self.sidebar then self.sidebar.update(items) end
end
-- function to run on initial load into memory
---@param on_load function callback
function app.set_load(on_load)
app.load = function ()
on_load()
app.loaded = true
end
end
-- function to run to close out the app
---@param on_unload function callback
function app.set_unload(on_unload)
app.unload = function ()
on_unload()
app.loaded = false
end
end
-- if a pane was provided, this will switch between numbered pages
---@param idx integer page index
function app.switcher(idx)
if app.paned_pages[idx] then
app.paned_pages[idx].nav_to()
end
end
-- create a new page entry in the app's page navigation tree
---@param parent nav_tree_page|nil a parent page or nil to set this as the root
---@param nav_to function|integer function to navigate to this page or pane index
---@return nav_tree_page new_page this new page
function app.new_page(parent, nav_to)
---@type nav_tree_page
local page = { _p = parent, _c = {}, nav_to = function () end, switcher = function () end, tasks = {} }
if parent == nil and app.cur_page == nil then
app.cur_page = page
end
if type(nav_to) == "number" then
app.paned_pages[nav_to] = page
function page.nav_to()
app.cur_page = page
if app.pane then app.pane.set_value(nav_to) end
end
else
function page.nav_to()
app.cur_page = page
nav_to()
end
end
-- switch between children
---@param id integer child ID
function page.switcher(id) if page._c[id] then page._c[id].nav_to() end end
if parent ~= nil then
table.insert(page._p._c, page)
end
return page
end
-- delete paned pages and clear the current page
function app.delete_pages()
app.paned_pages = {}
app.cur_page = nil
end
-- get the currently active page
function app.get_current_page() return app.cur_page end
-- attempt to navigate up the tree
---@return boolean success true if successfully navigated up
function app.nav_up()
local parent = app.cur_page._p
if parent then parent.nav_to() end
return parent ~= nil
end
self.apps[app_id] = app
self.containers[app_id] = container
return app
end
-- open an app
---@param app_id POCKET_APP_ID
function nav.open_app(app_id)
-- 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] ---@type pocket_app
if app then
if app.requires_conn() and not smem.pkt_sys.pocket_comms.is_linked() then
-- 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) end
self.cur_app = app_id
self.pane.set_value(app_id)
if #app.sidebar_items > 0 then
self.sidebar.update(app.sidebar_items)
end
else
log.debug("tried to open unknown app")
end
end
-- open the app that was blocked on connecting
function nav.on_loader_connected()
if self.loader_return then
nav.open_app(self.loader_return)
end
end
-- load a given app
---@param app_id POCKET_APP_ID
function nav.load_app(app_id)
self.apps[app_id].load()
end
-- unload api-dependent apps
function nav.unload_api()
for id, app in pairs(self.apps) do
local _, api = app.check_requires()
if app.loaded and api then
if id == self.cur_app then nav.open_app(APP_ID.ROOT) end
app.unload()
end
end
end
-- unload supervisor-dependent apps
function nav.unload_sv()
for id, app in pairs(self.apps) do
local sv, _ = app.check_requires()
if app.loaded and sv then
if id == self.cur_app then nav.open_app(APP_ID.ROOT) end
app.unload()
end
end
end
-- get a list of the app containers (usually Div elements)
function nav.get_containers() return self.containers end
-- get the currently active page
---@return nav_tree_page
function nav.get_current_page()
return self.apps[self.cur_app].get_current_page()
end
-- attempt to navigate up within the active app, otherwise open home page<br>
-- except, this will go back to a prior app if leaving the help app after open_help was used
function nav.nav_up()
-- return out of help if opened with open_help
if self.help_return then
nav.open_app(self.help_return)
self.help_return = nil
return
end
local app = self.apps[self.cur_app] ---@type pocket_app
log.debug("attempting app nav up for app " .. self.cur_app)
if not app.nav_up() then
log.debug("internal app nav up failed, going to home screen")
nav.open_app(APP_ID.ROOT)
end
end
-- open the help app, to show the reference for a key
function nav.open_help(key)
self.help_return = self.cur_app
nav.open_app(APP_ID.GUIDE)
local load = self.help_map[key]
if load then load() end
end
-- link the help map from the guide app
function nav.link_help(map) self.help_map = map end
return nav
end
-- pocket coordinator + supervisor communications
---@nodiscard
---@param version string pocket version
---@param nic nic network interface device
---@param pkt_channel integer pocket comms channel
---@param svr_channel integer supervisor access channel
---@param crd_channel integer coordinator access channel
---@param range integer trusted device connection range
---@param sv_watchdog watchdog
---@param api_watchdog watchdog
function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range, sv_watchdog, api_watchdog)
---@param nav pocket_nav
function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
local self = {
sv = {
linked = false,
addr = comms.BROADCAST,
seq_num = 0,
r_seq_num = nil, ---@type nil|integer
seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, ---@type nil|integer
last_est_ack = ESTABLISH_ACK.ALLOW
},
api = {
linked = false,
addr = comms.BROADCAST,
seq_num = 0,
r_seq_num = nil, ---@type nil|integer
seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, ---@type nil|integer
last_est_ack = ESTABLISH_ACK.ALLOW
},
establish_delay_counter = 0
}
comms.set_trusted_range(range)
comms.set_trusted_range(config.TrustedRange)
-- PRIVATE FUNCTIONS --
-- configure network channels
nic.closeAll()
nic.open(pkt_channel)
nic.open(config.PKT_Channel)
-- send a management packet to the supervisor
---@param msg_type MGMT_TYPE
@@ -60,7 +408,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
pkt.make(msg_type, msg)
s_pkt.make(self.sv.addr, self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
nic.transmit(svr_channel, pkt_channel, s_pkt)
nic.transmit(config.SVR_Channel, config.PKT_Channel, s_pkt)
self.sv.seq_num = self.sv.seq_num + 1
end
@@ -74,18 +422,34 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
pkt.make(msg_type, msg)
s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
nic.transmit(crd_channel, pkt_channel, s_pkt)
nic.transmit(config.CRD_Channel, config.PKT_Channel, s_pkt)
self.api.seq_num = self.api.seq_num + 1
end
-- send an API packet to the coordinator
---@param msg_type CRDN_TYPE
---@param msg table
local function _send_api(msg_type, msg)
local s_pkt = comms.scada_packet()
local pkt = comms.crdn_packet()
pkt.make(msg_type, msg)
s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_CRDN, pkt.raw_sendable())
nic.transmit(config.CRD_Channel, config.PKT_Channel, s_pkt)
self.api.seq_num = self.api.seq_num + 1
end
-- attempt supervisor connection establishment
local function _send_sv_establish()
self.sv.r_seq_num = nil
_send_sv(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
end
-- attempt coordinator API connection establishment
local function _send_api_establish()
_send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
self.api.r_seq_num = nil
_send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT, comms.api_version })
end
-- keep alive ack to supervisor
@@ -108,6 +472,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
-- close connection to the supervisor
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
@@ -117,6 +482,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
-- 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
@@ -132,7 +498,11 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
-- attempt to re-link if any of the dependent links aren't active
function public.link_update()
if not self.sv.linked then
iocontrol.report_link_state(util.trinary(self.api.linked, LINK_STATE.API_LINK_ONLY, LINK_STATE.UNLINKED))
if self.api.linked then
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY, false, nil)
else
iocontrol.report_link_state(LINK_STATE.UNLINKED, false, false)
end
if self.establish_delay_counter <= 0 then
_send_sv_establish()
@@ -141,7 +511,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
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)
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, nil, false)
if self.establish_delay_counter <= 0 then
_send_api_establish()
@@ -149,9 +519,6 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
else
self.establish_delay_counter = self.establish_delay_counter - 1
end
else
-- linked, all good!
iocontrol.report_link_state(LINK_STATE.LINKED)
end
end
@@ -174,6 +541,26 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) 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
end
-- send a facility command
---@param cmd FAC_COMMAND command
---@param option any? optional option options for the optional options (like waste mode)
function public.send_fac_command(cmd, option)
_send_api(CRDN_TYPE.FAC_CMD, { cmd, option })
end
-- send a unit command
---@param cmd UNIT_COMMAND command
---@param unit integer unit ID
---@param option any? optional option options for the optional options (like burn rate)
function public.send_unit_command(cmd, unit, option)
_send_api(CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
end
-- parse a packet
---@param side string
---@param sender integer
@@ -206,6 +593,25 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
return pkt
end
---@param packet mgmt_frame|crdn_frame
---@param length integer
---@param max integer?
---@return boolean
local function _check_length(packet, length, max)
local ok = util.trinary(max == nil, packet.length == length, packet.length >= length and packet.length <= (max or 0))
if not ok then
local fmt = "[comms] RX_PACKET{r_chan=%d,proto=%d,type=%d}: packet length mismatch -> expect %d != actual %d"
log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type, length, packet.length))
end
return ok
end
---@param packet mgmt_frame|crdn_frame
local function _fail_type(packet)
local fmt = "[comms] RX_PACKET{r_chan=%d,proto=%d,type=%d}: unrecognized packet type"
log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type))
end
-- handle a packet
---@param packet mgmt_frame|crdn_frame|nil
function public.handle_packet(packet)
@@ -217,32 +623,91 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
local protocol = packet.scada_frame.protocol()
local src_addr = packet.scada_frame.src_addr()
if l_chan ~= pkt_channel then
if l_chan ~= config.PKT_Channel then
log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == crd_channel then
elseif r_chan == config.CRD_Channel then
-- check sequence number
if self.api.r_seq_num == nil then
self.api.r_seq_num = packet.scada_frame.seq_num()
elseif self.connected and ((self.api.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
log.warning("sequence out-of-order (API): last = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
self.api.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.api.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order (API): next = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
elseif self.api.linked and (src_addr ~= self.api.addr) then
log.debug("received packet from unknown computer " .. src_addr .. " while linked (API expected " .. self.api.addr ..
"); channel in use by another system?")
"); channel in use by another system?")
return
else
self.api.r_seq_num = packet.scada_frame.seq_num()
self.api.r_seq_num = packet.scada_frame.seq_num() + 1
end
-- feed watchdog on valid sequence number
api_watchdog.feed()
if protocol == PROTOCOL.SCADA_MGMT then
if protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
if self.api.linked then
if packet.type == CRDN_TYPE.FAC_CMD then
-- facility command acknowledgement
if packet.length >= 2 then
local cmd = packet.data[1]
local ack = packet.data[2] == true
if cmd == FAC_COMMAND.SCRAM_ALL then
iocontrol.get_db().facility.scram_ack(ack)
elseif cmd == FAC_COMMAND.STOP then
elseif cmd == FAC_COMMAND.START then
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
iocontrol.get_db().facility.ack_alarms_ack(ack)
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
elseif cmd == FAC_COMMAND.SET_PU_FB then
elseif cmd == FAC_COMMAND.SET_SPS_LP then
else
log.debug(util.c("received facility command ack with unknown command ", cmd))
end
else
log.debug("SCADA_CRDN facility command ack packet length mismatch")
end
elseif packet.type == CRDN_TYPE.UNIT_CMD then
-- unit command acknowledgement
if packet.length == 3 then
local cmd = packet.data[1]
local unit_id = packet.data[2]
local ack = packet.data[3] == true
local unit = iocontrol.get_db().units[unit_id] ---@type pioctl_unit
if unit ~= nil then
if cmd == UNIT_COMMAND.SCRAM then
unit.scram_ack(ack)
elseif cmd == UNIT_COMMAND.START then
unit.start_ack(ack)
elseif cmd == UNIT_COMMAND.RESET_RPS then
unit.reset_rps_ack(ack)
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
unit.ack_alarms_ack(ack)
else
log.debug(util.c("received unsupported unit command ack for command ", cmd))
end
end
end
elseif packet.type == CRDN_TYPE.API_GET_FAC then
if _check_length(packet, 11) then
iocontrol.record_facility_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_UNIT then
if _check_length(packet, 12) and type(packet.data[1]) == "number" and iocontrol.get_db().units[packet.data[1]] then
iocontrol.record_unit_data(packet.data)
end
else _fail_type(packet) end
else
log.debug("discarding coordinator SCADA_CRDN packet before linked")
end
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
if self.api.linked then
if packet.type == MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back
if packet.length == 1 then
if _check_length(packet, 1) then
local timestamp = packet.data[1]
local trip_time = util.time() - timestamp
@@ -250,57 +715,80 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
log.warning("pocket coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
end
-- log.debug("pocket coordinator RTT = " .. trip_time .. "ms")
-- log.debug("pocket coordinator TT = " .. trip_time .. "ms")
_send_api_keep_alive_ack(timestamp)
else
log.debug("coordinator SCADA keep alive packet length mismatch")
iocontrol.report_crd_tt(trip_time)
end
elseif packet.type == MGMT_TYPE.CLOSE then
-- handle session close
api_watchdog.cancel()
nav.unload_api()
self.api.linked = false
self.api.r_seq_num = nil
self.api.addr = comms.BROADCAST
log.info("coordinator server connection closed by remote host")
else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator")
end
else _fail_type(packet) end
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- connection with coordinator established
if packet.length == 1 then
if _check_length(packet, 1, 2) then
local est_ack = packet.data[1]
if est_ack == ESTABLISH_ACK.ALLOW then
log.info("coordinator connection established")
self.establish_delay_counter = 0
self.api.linked = true
self.api.addr = src_addr
if packet.length == 2 then
local fac_config = packet.data[2]
if self.sv.linked then
iocontrol.report_link_state(LINK_STATE.LINKED)
if type(fac_config) == "table" and #fac_config == 2 then
-- get configuration
local conf = { num_units = fac_config[1], cooling = fac_config[2] }
iocontrol.init_fac(conf)
log.info("coordinator connection established")
self.establish_delay_counter = 0
self.api.linked = true
self.api.addr = src_addr
iocontrol.report_crd_link_error("")
if self.sv.linked then
iocontrol.report_link_state(LINK_STATE.LINKED, nil, self.api.addr)
else
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY, nil, self.api.addr)
end
else
log.debug("invalid facility configuration table received from coordinator, establish failed")
end
else
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY)
end
elseif est_ack == ESTABLISH_ACK.DENY then
if self.api.last_est_ack ~= est_ack then
log.info("coordinator connection denied")
end
elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.api.last_est_ack ~= est_ack then
log.info("coordinator connection denied due to collision")
end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.api.last_est_ack ~= est_ack then
log.info("coordinator comms version mismatch")
log.debug("received coordinator establish allow without facility configuration")
end
else
log.debug("coordinator SCADA_MGMT establish packet reply unsupported")
if self.api.last_est_ack ~= est_ack then
if est_ack == ESTABLISH_ACK.DENY then
log.info("coordinator connection denied")
iocontrol.report_crd_link_error("denied")
elseif est_ack == ESTABLISH_ACK.COLLISION then
log.info("coordinator connection denied due to collision")
iocontrol.report_crd_link_error("collision")
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
log.info("coordinator comms version mismatch")
iocontrol.report_crd_link_error("comms version mismatch")
elseif est_ack == ESTABLISH_ACK.BAD_API_VERSION then
log.info("coordinator api version mismatch")
iocontrol.report_crd_link_error("API version mismatch")
else
log.debug("coordinator SCADA_MGMT establish packet reply unsupported")
iocontrol.report_crd_link_error("unknown reply")
end
end
-- unlink
self.api.addr = comms.BROADCAST
self.api.linked = false
end
self.api.last_est_ack = est_ack
else
log.debug("coordinator SCADA_MGMT establish packet length mismatch")
end
else
log.debug("discarding coordinator non-link SCADA_MGMT packet before linked")
@@ -308,19 +796,19 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
else
log.debug("illegal packet type " .. protocol .. " from coordinator", true)
end
elseif r_chan == svr_channel then
elseif r_chan == config.SVR_Channel then
-- check sequence number
if self.sv.r_seq_num == nil then
self.sv.r_seq_num = packet.scada_frame.seq_num()
elseif self.connected and ((self.sv.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
log.warning("sequence out-of-order (SVR): last = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
self.sv.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.sv.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order (SVR): next = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
elseif self.sv.linked and (src_addr ~= self.sv.addr) then
log.debug("received packet from unknown computer " .. src_addr .. " while linked (SVR expected " .. self.sv.addr ..
"); channel in use by another system?")
return
else
self.sv.r_seq_num = packet.scada_frame.seq_num()
self.sv.r_seq_num = packet.scada_frame.seq_num() + 1
end
-- feed watchdog on valid sequence number
@@ -332,7 +820,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
if self.sv.linked then
if packet.type == MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back
if packet.length == 1 then
if _check_length(packet, 1) then
local timestamp = packet.data[1]
local trip_time = util.time() - timestamp
@@ -340,26 +828,25 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
log.warning("pocket supervisor KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
end
-- log.debug("pocket supervisor RTT = " .. trip_time .. "ms")
-- log.debug("pocket supervisor TT = " .. trip_time .. "ms")
_send_sv_keep_alive_ack(timestamp)
else
log.debug("supervisor SCADA keep alive packet length mismatch")
iocontrol.report_svr_tt(trip_time)
end
elseif packet.type == MGMT_TYPE.CLOSE then
-- handle session close
sv_watchdog.cancel()
nav.unload_sv()
self.sv.linked = false
self.sv.r_seq_num = nil
self.sv.addr = comms.BROADCAST
log.info("supervisor server connection closed by remote host")
elseif packet.type == MGMT_TYPE.DIAG_TONE_GET then
if packet.length == 8 then
if _check_length(packet, 8) then
for i = 1, #packet.data do
diag.tone_test.tone_indicators[i].update(packet.data[i] == true)
end
else
log.debug("supervisor SCADA diag alarm states packet length mismatch")
end
elseif packet.type == MGMT_TYPE.DIAG_TONE_SET then
if packet.length == 1 and packet.data[1] == false then
@@ -398,12 +885,10 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
else
log.debug("supervisor SCADA diag alarm set packet length/type mismatch")
end
else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor")
end
else _fail_type(packet) end
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- connection with supervisor established
if packet.length == 1 then
if _check_length(packet, 1) then
local est_ack = packet.data[1]
if est_ack == ESTABLISH_ACK.ALLOW then
@@ -412,37 +897,41 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
self.sv.linked = true
self.sv.addr = src_addr
iocontrol.report_svr_link_error("")
if self.api.linked then
iocontrol.report_link_state(LINK_STATE.LINKED)
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, nil)
else
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY)
end
elseif est_ack == ESTABLISH_ACK.DENY then
if self.sv.last_est_ack ~= est_ack then
log.info("supervisor connection denied")
end
elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.sv.last_est_ack ~= est_ack then
log.info("supervisor connection denied due to collision")
end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.sv.last_est_ack ~= est_ack then
log.info("supervisor comms version mismatch")
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, self.sv.addr, nil)
end
else
log.debug("supervisor SCADA_MGMT establish packet reply unsupported")
if self.sv.last_est_ack ~= est_ack then
if est_ack == ESTABLISH_ACK.DENY then
log.info("supervisor connection denied")
iocontrol.report_svr_link_error("denied")
elseif est_ack == ESTABLISH_ACK.COLLISION then
log.info("supervisor connection denied due to collision")
iocontrol.report_svr_link_error("collision")
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
log.info("supervisor comms version mismatch")
iocontrol.report_svr_link_error("comms version mismatch")
else
log.debug("supervisor SCADA_MGMT establish packet reply unsupported")
iocontrol.report_svr_link_error("unknown reply")
end
end
-- unlink
self.sv.addr = comms.BROADCAST
self.sv.linked = false
end
self.sv.last_est_ack = est_ack
else
log.debug("supervisor SCADA_MGMT establish packet length mismatch")
end
else
log.debug("discarding supervisor non-link SCADA_MGMT packet before linked")
end
else
log.debug("illegal packet type " .. protocol .. " from supervisor", true)
end
else _fail_type(packet) end
else
log.debug("received packet from unconfigured channel " .. r_chan, true)
end
@@ -457,8 +946,11 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
---@nodiscard
function public.is_api_linked() return self.api.linked end
-- check if we are still linked with the supervisor and coordinator
---@nodiscard
function public.is_linked() return self.sv.linked and self.api.linked end
return public
end
return pocket

94
pocket/process.lua Normal file
View File

@@ -0,0 +1,94 @@
--
-- Process Control Management
--
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local util = require("scada-common.util")
local FAC_COMMAND = comms.FAC_COMMAND
local UNIT_COMMAND = comms.UNIT_COMMAND
---@class pocket_process_controller
local process = {}
local self = {
io = nil, ---@type ioctl
comms = nil ---@type pocket_comms
}
-- initialize the process controller
---@param iocontrol pocket_ioctl iocontrl system
---@param pocket_comms pocket_comms pocket communications
function process.init(iocontrol, pocket_comms)
self.io = iocontrol
self.comms = pocket_comms
end
-- facility SCRAM command
function process.fac_scram()
self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL)
log.debug("PROCESS: FAC SCRAM ALL")
end
-- facility alarm acknowledge command
function process.fac_ack_alarms()
self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_ALARMS)
log.debug("PROCESS: FAC ACK ALL ALARMS")
end
-- start reactor
---@param id integer unit ID
function process.start(id)
self.io.units[id].control_state = true
self.comms.send_unit_command(UNIT_COMMAND.START, id)
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
end
-- SCRAM reactor
---@param id integer unit ID
function process.scram(id)
self.io.units[id].control_state = false
self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id)
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
end
-- reset reactor protection system
---@param id integer unit ID
function process.reset_rps(id)
self.comms.send_unit_command(UNIT_COMMAND.RESET_RPS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
end
-- set burn rate
---@param id integer unit ID
---@param rate number burn rate
function process.set_rate(id, rate)
self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate)
log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
end
-- acknowledge all alarms
---@param id integer unit ID
function process.ack_all_alarms(id)
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALL_ALARMS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
end
-- acknowledge an alarm
---@param id integer unit ID
---@param alarm integer alarm ID
function process.ack_alarm(id, alarm)
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALARM, id, alarm)
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
end
-- reset an alarm
---@param id integer unit ID
---@param alarm integer alarm ID
function process.reset_alarm(id, alarm)
self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm)
log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
end
return process

View File

@@ -92,4 +92,20 @@ function renderer.handle_mouse(event)
end
end
-- handle a keyboard event
---@param event key_interaction|nil
function renderer.handle_key(event)
if ui.display ~= nil and event ~= nil then
ui.display.handle_key(event)
end
end
-- handle a paste event
---@param text string
function renderer.handle_paste(text)
if ui.display ~= nil then
ui.display.handle_paste(text)
end
end
return renderer

View File

@@ -2,55 +2,67 @@
-- SCADA System Access on a Pocket Computer
--
---@diagnostic disable-next-line: undefined-global
local _is_pocket_env = pocket or periphemu -- luacheck: ignore pocket
require("/initenv").init_env()
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 tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core")
local config = require("pocket.config")
local configure = require("pocket.configure")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer")
local threads = require("pocket.threads")
local POCKET_VERSION = "v0.6.3-alpha"
local POCKET_VERSION = "v0.12.1-alpha"
local println = util.println
local println_ts = util.println_ts
-- check environment (allows Pocket or CraftOS-PC)
if not _is_pocket_env then
println("You can only use this application on a pocket computer.")
return
end
----------------------------------------
-- config validation
-- get configuration
----------------------------------------
local cfv = util.new_validator()
if not pocket.load_config() then
-- try to reconfigure (user action)
local success, error = configure.configure(true)
if success then
if not pocket.load_config() then
println("failed to load a valid configuration, please reconfigure")
return
end
else
println("configuration error: " .. error)
return
end
end
cfv.assert_channel(config.SVR_CHANNEL)
cfv.assert_channel(config.CRD_CHANNEL)
cfv.assert_channel(config.PKT_CHANNEL)
cfv.assert_type_int(config.TRUSTED_RANGE)
cfv.assert_type_num(config.COMMS_TIMEOUT)
cfv.assert_min(config.COMMS_TIMEOUT, 2)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
assert(cfv.valid(), "bad config file: missing/invalid fields")
local config = pocket.config
----------------------------------------
-- log init
----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
log.init(config.LogPath, config.LogMode, config.LogDebug)
log.info("========================================")
log.info("BOOTING pocket.startup " .. POCKET_VERSION)
log.info("========================================")
crash.set_env("pocket", POCKET_VERSION)
crash.dbg_log_env()
----------------------------------------
-- main application
@@ -64,128 +76,116 @@ local function main()
-- mount connected devices
ppm.mount_all()
-- record version for GUI
iocontrol.get_db().version = POCKET_VERSION
----------------------------------------
-- setup communications & clocks
-- memory allocation
----------------------------------------
-- shared memory across threads
---@class pkt_shared_memory
local __shared_memory = {
-- pocket system state flags
---@class pkt_state
pkt_state = {
ui_ok = false,
ui_error = nil,
shutdown = false
},
-- core pocket devices
pkt_dev = {
modem = ppm.get_wireless_modem()
},
-- system objects
pkt_sys = {
nic = nil, ---@type nic
pocket_comms = nil, ---@type pocket_comms
sv_wd = nil, ---@type watchdog
api_wd = nil, ---@type watchdog
nav = nil ---@type pocket_nav
},
-- message queues
q = {
mq_render = mqueue.new()
}
}
local smem_dev = __shared_memory.pkt_dev
local smem_sys = __shared_memory.pkt_sys
local pkt_state = __shared_memory.pkt_state
----------------------------------------
-- setup system
----------------------------------------
smem_sys.nav = pocket.init_nav(__shared_memory)
-- message authentication init
if type(config.AUTH_KEY) == "string" then
network.init_mac(config.AUTH_KEY)
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
network.init_mac(config.AuthKey)
end
iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED)
-- get the communications modem
local modem = ppm.get_wireless_modem()
if modem == nil then
if smem_dev.modem == nil then
println("startup> wireless modem not found: please craft the pocket computer with a wireless modem")
log.fatal("startup> no wireless modem on startup")
return
end
-- create connection watchdogs
local conn_wd = {
sv = util.new_watchdog(config.COMMS_TIMEOUT),
api = util.new_watchdog(config.COMMS_TIMEOUT)
}
conn_wd.sv.cancel()
conn_wd.api.cancel()
smem_sys.sv_wd = util.new_watchdog(config.ConnTimeout)
smem_sys.sv_wd.cancel()
smem_sys.api_wd = util.new_watchdog(config.ConnTimeout)
smem_sys.api_wd.cancel()
log.debug("startup> conn watchdogs created")
-- create network interface then setup comms
local nic = network.nic(modem)
local pocket_comms = pocket.comms(POCKET_VERSION, nic, config.PKT_CHANNEL, config.SVR_CHANNEL,
config.CRD_CHANNEL, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api)
smem_sys.nic = network.nic(smem_dev.modem)
smem_sys.pocket_comms = pocket.comms(POCKET_VERSION, smem_sys.nic, smem_sys.sv_wd, smem_sys.api_wd, smem_sys.nav)
log.debug("startup> comms init")
-- base loop clock (2Hz, 10 ticks)
local MAIN_CLOCK = 0.5
local loop_clock = util.new_clock(MAIN_CLOCK)
-- init I/O control
iocontrol.init_core(pocket_comms)
iocontrol.init_core(smem_sys.pocket_comms, smem_sys.nav, config)
----------------------------------------
-- start the UI
----------------------------------------
local ui_ok, message = renderer.try_start_ui()
if not ui_ok then
println(util.c("UI error: ", message))
log.error(util.c("startup> GUI render failed with error ", message))
else
-- start clock
loop_clock.start()
local ui_message
pkt_state.ui_ok, ui_message = renderer.try_start_ui()
if not pkt_state.ui_ok then
println(util.c("UI error: ", ui_message))
log.error(util.c("startup> GUI render failed with error ", ui_message))
end
----------------------------------------
-- main event loop
-- start system
----------------------------------------
if ui_ok then
-- start connection watchdogs
conn_wd.sv.feed()
conn_wd.api.feed()
log.debug("startup> conn watchdog started")
if pkt_state.ui_ok then
-- init threads
local main_thread = threads.thread__main(__shared_memory)
local render_thread = threads.thread__render(__shared_memory)
local io_db = iocontrol.get_db()
local nav = io_db.nav
log.info("startup> completed")
-- main event loop
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event
if event == "timer" then
if loop_clock.is_clock(param1) then
-- main loop tick
-- relink if necessary
pocket_comms.link_update()
-- update any tasks for the active page
if (type(nav.tasks[nav.page]) == "table") then
for i = 1, #nav.tasks[nav.page] do
nav.tasks[nav.page][i]()
end
end
loop_clock.start()
elseif conn_wd.sv.is_timer(param1) then
-- supervisor watchdog timeout
log.info("supervisor server timeout")
pocket_comms.close_sv()
elseif conn_wd.api.is_timer(param1) then
-- coordinator watchdog timeout
log.info("coordinator api server timeout")
pocket_comms.close_api()
else
-- a non-clock/main watchdog timer event
-- notify timer callback dispatcher
tcd.handle(param1)
end
elseif event == "modem_message" then
-- got a packet
local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
pocket_comms.handle_packet(packet)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
event == "double_click" then
-- handle a monitor touch event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
end
-- check for termination request
if event == "terminate" or ppm.should_terminate() then
log.info("terminate requested, closing server connections...")
pocket_comms.close()
log.info("connections closed")
break
end
end
-- run threads
parallel.waitForAll(main_thread.p_exec, render_thread.p_exec)
renderer.close_ui()
if not pkt_state.ui_ok then
println(util.c("UI crashed with error: ", pkt_state.ui_error))
end
else
println_ts("UI creation failed")
end
println_ts("exited")

219
pocket/threads.lua Normal file
View File

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

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

@@ -0,0 +1,233 @@
--
-- Unit Control Page
--
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local process = require("pocket.process")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local WaitingAnim = require("graphics.elements.animations.waiting")
local HazardButton = require("graphics.elements.controls.hazard_button")
local PushButton = require("graphics.elements.controls.push_button")
local NumberField = require("graphics.elements.form.number_field")
local DataIndicator = require("graphics.elements.indicators.data")
local IconIndicator = require("graphics.elements.indicators.icon")
local AUTO_GROUP = types.AUTO_GROUP
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
local lu_col = style.label_unit_pair
local text_fg = style.text_fg
local mode_states = style.icon_states.mode_states
local hzd_fg_bg = cpair(colors.white, colors.gray)
local dis_colors = cpair(colors.white, colors.lightGray)
-- new unit control page view
---@param root graphics_element 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.CONTROL, frame, nil, false, true)
local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.green,colors._INHERIT)}
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end } })
local btn_fg_bg = cpair(colors.green, colors.black)
local btn_active = cpair(colors.white, colors.black)
local page_div = nil ---@type nil|graphics_element
-- set sidebar to display unit-specific fields based on a specified unit
local function set_sidebar()
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end },
{ label = "FAC", color = core.cpair(colors.black, colors.orange), callback = function () app.switcher(db.facility.num_units + 1) end }
}
for i = 1, db.facility.num_units do
table.insert(list, { label = "U-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = function () app.switcher(i) end })
end
app.set_sidebar(list)
end
-- load the app (create the elements)
local function load()
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {}
local active_unit = 1
-- create all page divs
for _ = 1, db.facility.num_units + 1 do
local div = Div{parent=page_div}
table.insert(panes, div)
end
-- previous unit
local function prev(x)
active_unit = util.trinary(x == 1, db.facility.num_units, x - 1)
app.switcher(active_unit)
end
-- next unit
local function next(x)
active_unit = util.trinary(x == db.facility.num_units, 1, x + 1)
app.switcher(active_unit)
end
for i = 1, db.facility.num_units do
local u_pane = panes[i]
local u_div = Div{parent=u_pane,x=2,width=main.get_width()-2}
local unit = db.units[i] ---@type pioctl_unit
local u_ps = unit.unit_ps
-- 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_unit(i)
last_update = util.time_ms()
end
end
local u_page = app.new_page(nil, i)
u_page.tasks = { update }
TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,alignment=ALIGN.CENTER}
PushButton{parent=u_div,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()prev(i)end}
PushButton{parent=u_div,x=21,y=1,text=">",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()next(i)end}
local rate = DataIndicator{parent=u_div,y=3,lu_colors=lu_col,label="Burn",unit="mB/t",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
local temp = DataIndicator{parent=u_div,lu_colors=lu_col,label="Temp",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
local ctrl = IconIndicator{parent=u_div,x=1,y=6,label="Control State",states=mode_states}
rate.register(u_ps, "act_burn_rate", rate.update)
temp.register(u_ps, "temp", function (t) temp.update(db.temp_convert(t)) end)
ctrl.register(u_ps, "U_ControlStatus", ctrl.update)
u_div.line_break()
TextBox{parent=u_div,y=8,text="CMD",width=4,fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=u_div,x=14,y=8,text="mB/t",width=4,fg_bg=cpair(colors.lightGray,colors.black)}
local burn_cmd = NumberField{parent=u_div,x=5,y=8,width=8,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=cpair(colors.white,colors.gray),dis_fg_bg=cpair(colors.gray,colors.lightGray)}
local set_burn = function () unit.set_burn(burn_cmd.get_value()) end
local set_burn_btn = PushButton{parent=u_div,x=19,y=8,text="SET",min_width=5,fg_bg=cpair(colors.green,colors.black),active_fg_bg=cpair(colors.white,colors.black),dis_fg_bg=cpair(colors.gray,colors.black),callback=set_burn}
-- enable/disable controls based on group assignment (start button is separate)
burn_cmd.register(u_ps, "auto_group_id", function (gid)
if gid == AUTO_GROUP.MANUAL then burn_cmd.enable() else burn_cmd.disable() end
end)
set_burn_btn.register(u_ps, "auto_group_id", function (gid)
if gid == AUTO_GROUP.MANUAL then set_burn_btn.enable() else set_burn_btn.disable() end
end)
burn_cmd.register(u_ps, "burn_rate", burn_cmd.set_value)
burn_cmd.register(u_ps, "max_burn", burn_cmd.set_max)
local start = HazardButton{parent=u_div,x=2,y=11,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=unit.start,timeout=3,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=u_div,x=12,y=11,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=unit.ack_alarms,timeout=3,fg_bg=hzd_fg_bg}
local scram = HazardButton{parent=u_div,x=2,y=15,text="SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=unit.scram,timeout=3,fg_bg=hzd_fg_bg}
local reset = HazardButton{parent=u_div,x=12,y=15,text="RESET",accent=colors.red,dis_colors=dis_colors,callback=unit.reset_rps,timeout=3,fg_bg=hzd_fg_bg}
unit.start_ack = start.on_response
unit.ack_alarms_ack = ack_a.on_response
unit.scram_ack = scram.on_response
unit.reset_rps_ack = reset.on_response
local function start_button_en_check()
if (unit.reactor_data ~= nil) and (unit.reactor_data.mek_status ~= nil) then
local can_start = (not unit.reactor_data.mek_status.status) and
(not unit.reactor_data.rps_tripped) and
(unit.a_group == AUTO_GROUP.MANUAL)
if can_start then start.enable() else start.disable() end
end
end
start.register(u_ps, "status", start_button_en_check)
start.register(u_ps, "rps_tripped", start_button_en_check)
start.register(u_ps, "auto_group_id", start_button_en_check)
start.register(u_ps, "AutoControl", start_button_en_check)
reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
util.nop()
end
-- facility controls
local f_pane = panes[db.facility.num_units + 1]
local f_div = Div{parent=f_pane,x=2,width=main.get_width()-2}
app.new_page(nil, db.facility.num_units + 1)
TextBox{parent=f_div,y=1,text="Facility Commands",alignment=ALIGN.CENTER}
local scram = HazardButton{parent=f_div,x=5,y=6,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,timeout=3,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=f_div,x=7,y=11,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,timeout=3,fg_bg=hzd_fg_bg}
db.facility.scram_ack = scram.on_response
db.facility.ack_alarms_ack = ack_a.on_response
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)
set_sidebar()
-- done, show the app
load_pane.set_value(2)
end
-- delete the elements and switch back to the loading screen
local function unload()
if page_div then
page_div.delete()
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end } })
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,58 +1,41 @@
--
-- Diagnostic Apps
--
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.light")
local App = require("graphics.elements.controls.app")
local Checkbox = require("graphics.elements.controls.checkbox")
local PushButton = require("graphics.elements.controls.push_button")
local SwitchButton = require("graphics.elements.controls.switch_button")
local ALIGN = core.ALIGN
local cpair = core.cpair
local NAV_PAGE = iocontrol.NAV_PAGE
local APP_ID = pocket.APP_ID
local ALIGN = core.ALIGN
-- new diagnostics page view
-- create diagnostic app pages
---@param root graphics_element parent
local function new_view(root)
local function create_pages(root)
local db = iocontrol.get_db()
local main = Div{parent=root,x=1,y=1}
local diag_home = Div{parent=main,x=1,y=1}
TextBox{parent=diag_home,text="Diagnostic Apps",x=1,y=2,height=1,alignment=ALIGN.CENTER}
local alarm_test = Div{parent=main,x=1,y=1}
local panes = { diag_home, alarm_test }
local page_pane = MultiPane{parent=main,x=1,y=1,panes=panes}
local function navigate_diag()
page_pane.set_value(1)
db.nav.page = NAV_PAGE.DIAG
db.nav.sub_pages[NAV_PAGE.DIAG] = NAV_PAGE.DIAG
end
local function navigate_alarm()
page_pane.set_value(2)
db.nav.page = NAV_PAGE.D_ALARMS
db.nav.sub_pages[NAV_PAGE.DIAG] = NAV_PAGE.D_ALARMS
end
------------------------
-- Alarm Testing Page --
------------------------
db.nav.register_task(NAV_PAGE.D_ALARMS, db.diag.tone_test.get_tone_states)
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
@@ -63,15 +46,13 @@ local function new_view(root)
local audio = Div{parent=alarm_test,x=1,y=1}
TextBox{parent=audio,y=1,text="Alarm Sounder Tests",height=1,alignment=ALIGN.CENTER}
TextBox{parent=audio,y=1,text="Alarm Sounder Tests",alignment=ALIGN.CENTER}
ttest.ready_warn = TextBox{parent=audio,y=2,text="",height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
PushButton{parent=audio,x=13,y=18,text="\x11 BACK",min_width=8,fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=c_wht_gray,callback=navigate_diag}
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",height=1,alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
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}
@@ -94,7 +75,7 @@ local function new_view(root)
local alarms = Div{parent=audio,x=11,y=3,height=15,fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=alarms,text="Alarms (\x13)",height=1,alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
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}
@@ -121,7 +102,7 @@ local function new_view(root)
local states = Div{parent=audio,x=2,y=14,height=5,width=8}
TextBox{parent=states,text="States",height=1,alignment=ALIGN.CENTER}
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}
@@ -132,16 +113,6 @@ local function new_view(root)
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 }
--------------
-- App List --
--------------
App{parent=diag_home,x=3,y=4,text="\x0f",title="Alarm",callback=navigate_alarm,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
App{parent=diag_home,x=10,y=4,text="\x1e",title="LoopT",callback=function()end,app_fg_bg=cpair(colors.black,colors.cyan)}
App{parent=diag_home,x=17,y=4,text="@",title="Comps",callback=function()end,app_fg_bg=cpair(colors.black,colors.orange)}
return main
end
return new_view
return create_pages

View File

@@ -0,0 +1,29 @@
--
-- 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 graphics_element parent
local function create_pages(root)
local db = iocontrol.get_db()
local main = Div{parent=root,x=1,y=1}
db.nav.register_app(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

280
pocket/ui/apps/guide.lua Normal file
View File

@@ -0,0 +1,280 @@
--
-- System Guide
--
local util = require("scada-common.util")
local log = require("scada-common.log")
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")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local WaitingAnim = require("graphics.elements.animations.waiting")
local PushButton = require("graphics.elements.controls.push_button")
local TextField = require("graphics.elements.form.text_field")
local ALIGN = core.ALIGN
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 graphics_element 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.GUIDE, frame)
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)}
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 = function () db.nav.open_app(APP_ID.ROOT) end }})
local page_div = nil ---@type nil|graphics_element
-- load the app (create the elements)
local function load()
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end },
{ label = " \x14 ", color = core.cpair(colors.black, colors.cyan), callback = function () app.switcher(1) end },
{ label = "__?", color = core.cpair(colors.black, colors.lightGray), callback = function () app.switcher(2) end }
}
app.set_sidebar(list)
page_div = Div{parent=main,y=2}
local p_width = page_div.get_width() - 2
local main_page = app.new_page(nil, 1)
local search_page = app.new_page(main_page, 2)
local use_page = app.new_page(main_page, 3)
local uis_page = app.new_page(main_page, 4)
local fps_page = app.new_page(main_page, 5)
local gls_page = app.new_page(main_page, 6)
local lnk_page = app.new_page(main_page, 7)
local home = Div{parent=page_div,x=2}
local search = Div{parent=page_div,x=2}
local use = Div{parent=page_div,x=2,width=p_width}
local uis = Div{parent=page_div,x=2,width=p_width}
local fps = Div{parent=page_div,x=2,width=p_width}
local gls = Div{parent=page_div,x=2,width=p_width}
local lnk = Div{parent=page_div,x=2,width=p_width}
local panes = { home, search, use, uis, fps, gls, lnk }
local doc_map = {}
local search_db = {}
---@class _guide_section_constructor_data
local sect_construct_data = { app, page_div, panes, doc_map, search_db, btn_fg_bg, btn_active }
TextBox{parent=home,y=1,text="cc-mek-scada Guide",alignment=ALIGN.CENTER}
PushButton{parent=home,y=3,text="Search >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=search_page.nav_to}
PushButton{parent=home,y=5,text="System Usage >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=use_page.nav_to}
PushButton{parent=home,text="Operator UIs >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=uis_page.nav_to}
PushButton{parent=home,text="Front Panels >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fps_page.nav_to}
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}
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)}
local func_ref = {}
PushButton{parent=search,x=20,y=3,text="GO",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()func_ref.run_search()end}
local search_results = ListBox{parent=search,x=1,y=5,scroll_height=200,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
function func_ref.run_search()
local query = string.lower(query_field.get_value())
local s_results = { {}, {}, {}, {} }
search_results.remove_all()
if string.len(query) < 2 then
TextBox{parent=search_results,text="Search requires at least 2 characters."}
return
end
local start = util.time_ms()
for _, entry in ipairs(search_db) do
local s_start, s_end = string.find(entry[1], query, 1, true)
if s_start == nil then
elseif s_start == 1 then
if s_end == string.len(entry[1]) then
-- best match: full match
table.insert(s_results[1], entry)
else
-- very good match, start of key
table.insert(s_results[2], entry)
end
elseif string.sub(query, s_start - 1, s_start) == " " then
-- start of word, good match
table.insert(s_results[3], entry)
else
-- basic match in content
table.insert(s_results[4], entry)
end
end
local empty = true
for tier = 1, 4 do
for idx = 1, #s_results[tier] do
local entry = s_results[tier][idx]
TextBox{parent=search_results,text=entry[3].." >",fg_bg=cpair(colors.gray,colors.black)}
PushButton{parent=search_results,text=entry[2],fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=entry[4]}
empty = false
end
end
log.debug("App.Guide: search for \"" .. query .. "\" completed in " .. (util.time_ms() - start) .. "ms")
if empty then
TextBox{parent=search_results,text="No results found."}
end
end
TextBox{parent=search_results,text="Click 'GO' to search..."}
util.nop()
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()
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}
local annunc_page = app.new_page(uis_page, #panes + 1)
local annunc_div = Div{parent=page_div,x=2}
table.insert(panes, annunc_div)
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()
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 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,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()
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}
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)
local fp_rtu_page = guide_section(sect_construct_data, fps_page, "RTU Gateway", docs.fp.rtu_gw, 100)
local fp_supervisor_page = guide_section(sect_construct_data, fps_page, "Supervisor", docs.fp.supervisor, 160)
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()
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)
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}
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}
lnk.line_break()
TextBox{parent=lnk,text="GitHub",fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=lnk,text="https://github.com/MikaylaFischler/cc-mek-scada"}
lnk.line_break()
TextBox{parent=lnk,text="Wiki",fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=lnk,text="https://github.com/MikaylaFischler/cc-mek-scada/wiki"}
lnk.line_break()
TextBox{parent=lnk,text="Discord",fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=lnk,text="discord.gg/R9NSCkhcwt"}
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)
-- link help resources
db.nav.link_help(doc_map)
-- 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 = function () db.nav.open_app(APP_ID.ROOT) end } })
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

49
pocket/ui/apps/loader.lua Normal file
View File

@@ -0,0 +1,49 @@
--
-- Loading Screen App
--
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local conn_waiting = require("pocket.ui.components.conn_waiting")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local APP_ID = pocket.APP_ID
local LINK_STATE = iocontrol.LINK_STATE
-- create the connecting to SV & API page
---@param root graphics_element parent
local function create_pages(root)
local db = iocontrol.get_db()
local main = Div{parent=root,x=1,y=1}
db.nav.register_app(APP_ID.LOADER, main).new_page(nil, function () end)
local conn_sv_wait = conn_waiting(main, 6, false)
local conn_api_wait = conn_waiting(main, 6, true)
local main_pane = Div{parent=main,x=1,y=2}
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
root_pane.set_value(1)
elseif state == LINK_STATE.SV_LINK_ONLY then
root_pane.set_value(2)
else
root_pane.set_value(3)
db.nav.on_loader_connected()
end
end)
TextBox{parent=main_pane,text="Connected!",x=1,y=6,alignment=core.ALIGN.CENTER}
end
return create_pages

146
pocket/ui/apps/sys_apps.lua Normal file
View File

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

399
pocket/ui/apps/unit.lua Normal file
View File

@@ -0,0 +1,399 @@
--
-- Unit Overview Page
--
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local style = require("pocket.ui.style")
local boiler = require("pocket.ui.pages.unit_boiler")
local reactor = require("pocket.ui.pages.unit_reactor")
local turbine = require("pocket.ui.pages.unit_turbine")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local WaitingAnim = require("graphics.elements.animations.waiting")
local PushButton = require("graphics.elements.controls.push_button")
local DataIndicator = require("graphics.elements.indicators.data")
local IconIndicator = require("graphics.elements.indicators.icon")
local ALIGN = core.ALIGN
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 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 emc_ind_s = {
{ color = cpair(colors.black, colors.gray), symbol = "-" },
{ color = cpair(colors.black, colors.white), symbol = "\x07" },
{ color = cpair(colors.black, colors.green), symbol = "+" }
}
-- new unit page view
---@param root graphics_element 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.UNITS, 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 = function () db.nav.open_app(APP_ID.ROOT) end } })
local btn_fg_bg = cpair(colors.yellow, colors.black)
local btn_active = cpair(colors.white, colors.black)
local nav_links = {}
local page_div = nil ---@type nil|graphics_element
-- set sidebar to display unit-specific fields based on a specified unit
local function set_sidebar(id)
local unit = db.units[id] ---@type pioctl_unit
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end },
{ label = "U-" .. id, color = core.cpair(colors.black, colors.yellow), callback = function () app.switcher(id) end },
{ label = " \x13 ", color = core.cpair(colors.black, colors.red), callback = nav_links[id].alarm },
{ label = "RPS", tall = true, color = core.cpair(colors.black, colors.cyan), callback = nav_links[id].rps },
{ label = " R ", color = core.cpair(colors.black, colors.lightGray), callback = nav_links[id].reactor },
{ label = "RCS", tall = true, color = core.cpair(colors.black, colors.blue), callback = nav_links[id].rcs },
}
for i = 1, unit.num_boilers do
table.insert(list, { label = "B-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = nav_links[id].boiler[i] })
end
for i = 1, unit.num_turbines do
table.insert(list, { label = "T-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = nav_links[id].turbine[i] })
end
app.set_sidebar(list)
end
-- load the app (create the elements)
local function load()
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {}
local active_unit = 1
-- create all page divs
for _ = 1, db.facility.num_units do
local div = Div{parent=page_div}
table.insert(panes, div)
table.insert(nav_links, {})
end
-- previous unit
local function prev(x)
active_unit = util.trinary(x == 1, db.facility.num_units, x - 1)
app.switcher(active_unit)
set_sidebar(active_unit)
end
-- next unit
local function next(x)
active_unit = util.trinary(x == db.facility.num_units, 1, x + 1)
app.switcher(active_unit)
set_sidebar(active_unit)
end
for i = 1, db.facility.num_units do
local u_pane = panes[i]
local u_div = Div{parent=u_pane,x=2,width=main.get_width()-2}
local unit = db.units[i] ---@type pioctl_unit
local u_ps = unit.unit_ps
-- 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_unit(i)
last_update = util.time_ms()
end
end
--#region Main Unit Overview
local u_page = app.new_page(nil, i)
u_page.tasks = { update }
TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,alignment=ALIGN.CENTER}
PushButton{parent=u_div,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()prev(i)end}
PushButton{parent=u_div,x=21,y=1,text=">",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()next(i)end}
local type = util.trinary(unit.num_boilers > 0, "Sodium Cooled Reactor", "Boiling Water Reactor")
TextBox{parent=u_div,y=3,text=type,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.black)}
local rate = DataIndicator{parent=u_div,y=5,lu_colors=lu_col,label="Burn",unit="mB/t",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
local temp = DataIndicator{parent=u_div,lu_colors=lu_col,label="Temp",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
local ctrl = IconIndicator{parent=u_div,x=1,y=8,label="Control State",states=mode_states}
rate.register(u_ps, "act_burn_rate", rate.update)
temp.register(u_ps, "temp", function (t) temp.update(db.temp_convert(t)) end)
ctrl.register(u_ps, "U_ControlStatus", ctrl.update)
u_div.line_break()
local rct = IconIndicator{parent=u_div,x=1,label="Fission Reactor",states=basic_states}
local rps = IconIndicator{parent=u_div,x=1,label="Protection System",states=basic_states}
rct.register(u_ps, "U_ReactorStatus", rct.update)
rps.register(u_ps, "U_RPS", rps.update)
u_div.line_break()
local rcs = IconIndicator{parent=u_div,x=1,label="Coolant System",states=basic_states}
rcs.register(u_ps, "U_RCS", rcs.update)
for b = 1, unit.num_boilers do
local blr = IconIndicator{parent=u_div,x=1,label="Boiler "..b,states=basic_states}
blr.register(unit.boiler_ps_tbl[b], "BoilerStatus", blr.update)
end
for t = 1, unit.num_turbines do
local tbn = IconIndicator{parent=u_div,x=1,label="Turbine "..t,states=basic_states}
tbn.register(unit.turbine_ps_tbl[t], "TurbineStatus", tbn.update)
end
--#endregion
util.nop()
--#region Alarms Tab
local alm_div = Div{parent=page_div}
table.insert(panes, alm_div)
local alm_page = app.new_page(u_page, #panes)
alm_page.tasks = { update }
nav_links[i].alarm = alm_page.nav_to
TextBox{parent=alm_div,y=1,text="Status Info Display",alignment=ALIGN.CENTER}
local ecam_disp = ListBox{parent=alm_div,x=2,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
ecam_disp.register(u_ps, "U_ECAM", function (data)
local ecam = textutils.unserialize(data)
ecam_disp.remove_all()
for _, entry in ipairs(ecam) do
local div = Div{parent=ecam_disp,height=1+#entry.items,fg_bg=cpair(entry.color,colors.black)}
local text = TextBox{parent=div,text=entry.text}
if entry.help then
PushButton{parent=div,x=21,y=text.get_y(),text="?",callback=function()db.nav.open_help(entry.help)end,fg_bg=cpair(colors.gray,colors.black)}
end
for _, item in ipairs(entry.items) do
local fg_bg = nil
if item.color then fg_bg = cpair(item.color, colors.black) end
text = TextBox{parent=div,x=3,text=item.text,fg_bg=fg_bg}
if item.help then
PushButton{parent=div,x=21,y=text.get_y(),text="?",callback=function()db.nav.open_help(item.help)end,fg_bg=cpair(colors.gray,colors.black)}
end
end
ecam_disp.line_break()
end
end)
--#endregion
--#region RPS Tab
local rps_pane = Div{parent=page_div}
local rps_div = Div{parent=rps_pane,x=2,width=main.get_width()-2}
table.insert(panes, rps_div)
local rps_page = app.new_page(u_page, #panes)
rps_page.tasks = { update }
nav_links[i].rps = rps_page.nav_to
TextBox{parent=rps_div,y=1,text="Protection System",alignment=ALIGN.CENTER}
local r_trip = IconIndicator{parent=rps_div,y=3,label="RPS Trip",states=basic_states}
r_trip.register(u_ps, "U_RPS", r_trip.update)
local r_mscrm = IconIndicator{parent=rps_div,y=5,label="Manual SCRAM",states=red_ind_s}
local r_ascrm = IconIndicator{parent=rps_div,label="Automatic SCRAM",states=red_ind_s}
local rps_tmo = IconIndicator{parent=rps_div,label="Timeout",states=yel_ind_s}
local rps_flt = IconIndicator{parent=rps_div,label="PPM Fault",states=yel_ind_s}
local rps_sfl = IconIndicator{parent=rps_div,label="Not Formed",states=red_ind_s}
r_mscrm.register(u_ps, "manual", r_mscrm.update)
r_ascrm.register(u_ps, "automatic", r_ascrm.update)
rps_tmo.register(u_ps, "timeout", rps_tmo.update)
rps_flt.register(u_ps, "fault", rps_flt.update)
rps_sfl.register(u_ps, "sys_fail", rps_sfl.update)
rps_div.line_break()
local rps_dmg = IconIndicator{parent=rps_div,label="Reactor Damage Hi",states=red_ind_s}
local rps_tmp = IconIndicator{parent=rps_div,label="Temp. Critical",states=red_ind_s}
local rps_nof = IconIndicator{parent=rps_div,label="Fuel Level Lo",states=yel_ind_s}
local rps_exw = IconIndicator{parent=rps_div,label="Waste Level Hi",states=yel_ind_s}
local rps_loc = IconIndicator{parent=rps_div,label="Coolant Lo Lo",states=yel_ind_s}
local rps_exh = IconIndicator{parent=rps_div,label="Heated Coolant Hi",states=yel_ind_s}
rps_dmg.register(u_ps, "high_dmg", rps_dmg.update)
rps_tmp.register(u_ps, "high_temp", rps_tmp.update)
rps_nof.register(u_ps, "no_fuel", rps_nof.update)
rps_exw.register(u_ps, "ex_waste", rps_exw.update)
rps_loc.register(u_ps, "low_cool", rps_loc.update)
rps_exh.register(u_ps, "ex_hcool", rps_exh.update)
--#endregion
--#region Reactor Tab
nav_links[i].reactor = reactor(app, u_page, panes, page_div, u_ps, update)
--#endregion
--#region RCS Tab
local rcs_pane = Div{parent=page_div}
local rcs_div = Div{parent=rcs_pane,x=2,width=main.get_width()-2}
table.insert(panes, rcs_pane)
local rcs_page = app.new_page(u_page, #panes)
rcs_page.tasks = { update }
nav_links[i].rcs = rcs_page.nav_to
TextBox{parent=rcs_div,y=1,text="Coolant System",alignment=ALIGN.CENTER}
local r_rtrip = IconIndicator{parent=rcs_div,y=3,label="RCP Trip",states=red_ind_s}
local r_cflow = IconIndicator{parent=rcs_div,label="RCS Flow Lo",states=yel_ind_s}
local r_clow = IconIndicator{parent=rcs_div,label="Coolant Level Lo",states=yel_ind_s}
r_rtrip.register(u_ps, "RCPTrip", r_rtrip.update)
r_cflow.register(u_ps, "RCSFlowLow", r_cflow.update)
r_clow.register(u_ps, "CoolantLevelLow", r_clow.update)
local c_flt = IconIndicator{parent=rcs_div,label="RCS HW Fault",states=yel_ind_s}
local c_emg = IconIndicator{parent=rcs_div,label="Emergency Coolant",states=emc_ind_s}
local c_mwrf = IconIndicator{parent=rcs_div,label="Max Water Return",states=yel_ind_s}
c_flt.register(u_ps, "RCSFault", c_flt.update)
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}
c_cfm.register(u_ps, "CoolantFeedMismatch", c_cfm.update)
c_brm.register(u_ps, "BoilRateMismatch", c_brm.update)
c_sfm.register(u_ps, "SteamFeedMismatch", c_sfm.update)
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}
local hrl = IconIndicator{parent=rcs_div,label="Heating Rate Lo",states=yel_ind_s}
wll.register(u_ps, "U_WaterLevelLow", wll.update)
hrl.register(u_ps, "U_HeatingRateLow", hrl.update)
end
local tospd = IconIndicator{parent=rcs_div,label="TRB Over Speed",states=red_ind_s}
local gtrip = IconIndicator{parent=rcs_div,label="Generator Trip",states=yel_ind_s}
local ttrip = IconIndicator{parent=rcs_div,label="Turbine Trip",states=red_ind_s}
tospd.register(u_ps, "U_TurbineOverSpeed", tospd.update)
gtrip.register(u_ps, "U_GeneratorTrip", gtrip.update)
ttrip.register(u_ps, "U_TurbineTrip", ttrip.update)
--#endregion
--#region Boiler Tabs
local blr_pane = Div{parent=page_div}
nav_links[i].boiler = {}
for b_id = 1, unit.num_boilers do
local ps = unit.boiler_ps_tbl[b_id]
nav_links[i].boiler[b_id] = boiler(app, u_page, panes, blr_pane, b_id, ps, update)
end
--#endregion
--#region Turbine Tabs
local tbn_pane = Div{parent=page_div}
nav_links[i].turbine = {}
for t_id = 1, unit.num_turbines do
local ps = unit.turbine_ps_tbl[t_id]
nav_links[i].turbine[t_id] = turbine(app, u_page, panes, tbn_pane, i, t_id, ps, update)
end
--#endregion
util.nop()
end
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)
set_sidebar(active_unit)
-- 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 = function () db.nav.open_app(APP_ID.ROOT) end } })
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

@@ -2,6 +2,8 @@
-- Connection Waiting Spinner
--
local iocontrol = require("pocket.iocontrol")
local style = require("pocket.ui.style")
local core = require("graphics.core")
@@ -23,16 +25,20 @@ local function init(parent, y, is_api)
local root = Div{parent=parent,x=1,y=1}
-- bounding box div
local box = Div{parent=root,x=1,y=y,height=5}
local box = Div{parent=root,x=1,y=y,height=12}
local waiting_x = math.floor(parent.get_width() / 2) - 1
local msg = TextBox{parent=box,x=3,y=11,width=box.get_width()-4,height=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.red,style.root.bkg)}
if is_api then
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)}
TextBox{parent=box,text="Connecting to API",alignment=ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)}
TextBox{parent=box,y=5,text="Connecting to API",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,style.root.bkg)}
msg.register(iocontrol.get_db().ps, "api_link_msg", msg.set_value)
else
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.green,style.root.bkg)}
TextBox{parent=box,text="Connecting to Supervisor",alignment=ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)}
TextBox{parent=box,y=5,text="Connecting to Supervisor",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,style.root.bkg)}
msg.register(iocontrol.get_db().ps, "svr_link_msg", msg.set_value)
end
return root

285
pocket/ui/docs.lua Normal file
View File

@@ -0,0 +1,285 @@
local const = require("scada-common.constants")
local docs = {}
---@enum DOC_ITEM_TYPE
local DOC_ITEM_TYPE = {
SECTION = 1,
SUBSECTION = 2,
TEXT = 3,
LIST = 4
}
---@enum DOC_LIST_TYPE
local DOC_LIST_TYPE = {
BULLET = 1,
NUMBERED = 2,
INDICATOR = 3,
LED = 4
}
docs.DOC_ITEM_TYPE = DOC_ITEM_TYPE
docs.DOC_LIST_TYPE = DOC_LIST_TYPE
local target
local function sect(name)
---@class pocket_doc_sect
local item = { type = DOC_ITEM_TYPE.SECTION, name = name }
table.insert(target, item)
end
---@param key string item identifier for linking
---@param name string item name for display
---@param text_a string text body, or the subtitle/note if text_b is specified
---@param text_b? string text body if subtitle/note was specified
local function doc(key, name, text_a, text_b)
if text_b == nil then
text_b = text_a
---@diagnostic disable-next-line: cast-local-type
text_a = nil
end
---@class pocket_doc_subsect
local item = { type = DOC_ITEM_TYPE.SUBSECTION, key = key, name = name, subtitle = text_a, body = text_b }
table.insert(target, item)
end
local function text(body)
---@class pocket_doc_text
local item = { type = DOC_ITEM_TYPE.TEXT, 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
local function list(type, items, colors)
---@class pocket_doc_list
local list_def = { type = DOC_ITEM_TYPE.LIST, list_type = type, items = items, colors = 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
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("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.")
doc("ReactorHighTemp", "Reactor High Temp", "Reactor temperature is above expected operating levels and may exceed maximum safe temperature soon.")
doc("ReactorWasteLeak", "Reactor Waste Leak", "The reactor is full of spent waste so it will now emit radiation if additional waste is generated.")
doc("ReactorHighWaste", "Reactor High Waste", "Reactor waste levels are high and may leak soon.")
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.")
docs.annunc = {
unit = {
main_section = {}, rps_section = {}, rcs_section = {}
},
facility = {
main_section = {}
}
}
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("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")
doc("ReactorSCRAM", "Reactor SCRAM", "On if the reactor protection system is holding the reactor SCRAM'd.")
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("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.")
doc("ReactorHighDeltaT", "Reactor High Delta T", "On if the reactor temperature is climbing rapidly. This can occur when a reactor is starting up, but it is a concern if it happens while the burn rate is not increasing.")
doc("FuelInputRateLow", "Fuel Input Rate Low", "On if the fissile fuel levels in the reactor are dropping or very low. Ensure a steady supply of fuel is entering the reactor.")
doc("WasteLineOcclusion", "Waste Line Occlusion", "Waste levels in the reactor are increasing. Ensure your waste processing system is operating at a sufficient rate for your burn rate.")
doc("HighStartupRate", "Startup Rate High", "This is a rough calculation of if your burn rate is high enough to cause a loss of coolant on startup. A burn rate above this is likely to cause that, but it could occur at even higher or even lower rates depending on your setup (such as pipes, water supplies, and boiler tanks).")
target = docs.annunc.unit.rps_section
doc("rps_tripped", "RPS Trip", "Indicates if the reactor protection system has caused a SCRAM.")
doc("manual", "Manual Reactor SCRAM", "Indicates if the operator (you) tripped the RPS by pressing SCRAM.")
doc("automatic", "Auto Reactor SCRAM", "Indicates if the automatic control system tripped the RPS.")
doc("high_dmg", "Damage Level High", "Indicates if the RPS tripped due to significant reactor damage. Await damage levels to lower.")
doc("ex_waste", "Excess Waste", "Indicates if the RPS tripped due to very high waste levels. Ensure waste processing system is keeping up.")
doc("ex_hcool", "Excess Heated Coolant", "Indicates if the RPS tripped due to very high heated coolant levels. Check that the cooling system is able to keep up with heated coolant flow.")
doc("high_temp", "Temperature High", "Indicates if the RPS tripped due to reaching damaging temperatures. Await damage levels to lower.")
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("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
doc("RCSFault", "RCS Hardware Fault", "Indicates if one or more of the RCS devices have a peripheral fault. Check that your machines are formed. If this persists, try rebooting affected RTUs.")
doc("EmergencyCoolant", "Emergency Coolant", "Off if no emergency coolant redstone is configured, white when it is configured but not in use, and green/blue when it is activated. This is based on an RTU having a redstone emergency coolant output configured for this unit.")
doc("CoolantFeedMismatch", "Coolant Feed Mismatch", "The coolant system is accumulating heated coolant or losing cooled coolant, likely due to one of the machines not keeping up with the needs of the reactor. The flow monitor can help figure out where the problem is.")
doc("BoilRateMismatch", "Boil Rate Mismatch", "The total heating rate of the reactor exceed the tolerance from the steam input rate of the turbines OR for sodium setups, the boiler boil rates exceed the tolerance from the steam input rate of the turbines. The flow monitor can help figure out where the problem is.")
doc("SteamFeedMismatch", "Steam Feed Mismatch", "There is an above tolerance difference between turbine flow and steam input rates or the reactor/boilers are gaining steam or losing water. The flow monitor can help figure out where the problem is.")
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("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.")
target = docs.annunc.facility.main_section
sect("Connectivity")
doc("all_sys_ok", "Unit Systems Online", "All unit systems (reactors, boilers, and turbines) are connected.")
doc("rad_computed_status", "Radiation Monitor", "At least one facility radiation monitor is connected")
doc("im_computed_status", "Induction Matrix", "The induction matrix is connected.")
doc("sps_computed_status", "SPS Connected", "Indicates if the super-critical phase shifter is connected.")
sect("Automatic Control")
doc("auto_ready", "Configured Units Ready", "All units assigned to automatic control are ready to run automatic control.")
doc("auto_active", "Process Active", "Automatic process control is active.")
doc("auto_ramping", "Process Ramping", "Automatic process control is performing an initial ramp-up of the reactors for later PID control (generation and charge mode).")
doc("auto_saturated", "Min/Max Burn Rate", "Auto control has either commanded 0 mB/t or the maximum total burn rate available (from assigned units).")
sect("Automatic SCRAM")
doc("auto_scram", "Automatic SCRAM", "Automatic control system SCRAM'ed the assigned reactors due to a safety hazard, shown by the below indicators.")
doc("as_matrix_dc", "Matrix Disconnected", "Automatic SCRAM occurred due to loss of induction matrix connection.")
doc("as_matrix_fill", "Matrix Charge High", "Automatic SCRAM occurred due to induction matrix charge exceeding acceptable limit.")
doc("as_crit_alarm", "Unit Critical Alarm", "Automatic SCRAM occurred due to critical level unit alarm(s).")
doc("as_radiation", "Facility Radiation High", "Automatic SCRAM occurred due to high facility radiation levels.")
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 = {}
}
--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.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", "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.")
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("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")
doc("fp_rplc_reactor", "REACTOR", "This indicates the status of the connected reactor peripheral.")
list(DOC_LIST_TYPE.LED, { "disconnected", "unformed", "ok" }, { colors.red, colors.yellow, colors.green })
doc("fp_nt_collision", "NT COLLISION", "(color accessibility modes only)", "This indicates the Reactor PLC unit ID is a duplicate of another already connected Reactor PLC.")
sect("Co-Routine States")
doc("fp_rplc_rt_main", "RT MAIN", "This lights up as long as the device's main loop co-routine is running, which it should be as long as STATUS is green.")
doc("fp_rplc_rt_rps", "RT RPS", "This should always be lit up if a reactor is connected as it indicates the RPS co-routine is running, otherwise safety checks will not be running.")
doc("fp_rplc_rt_ctx", "RT COMMS TX", "This should always be lit if the Reactor PLC is not running in standalone mode, as it indicates the communications transmission co-routine is running.")
doc("fp_rplc_rt_crx", "RT COMMS RX", "This should always be lit if the Reactor PLC is not running in standalone mode, as it indicates the communications receiver/handler co-routine is running.")
doc("fp_rplc_rt_spctl", "RT SPCTL", "This should always be lit if the Reactor PLC is not running in standalone mode, as it indicates the process setpoint controller co-routine is running.")
sect("Status")
doc("fp_rct_active", "RCT ACTIVE", "The reactor is active (running).")
doc("fp_emer_cool", "EMER COOLANT", "This is only present if PLC-controlled emergency coolant is configured on that device. When lit, it indicates that it has been activated.")
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_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.")
doc("fp_rps_temp", "HI TEMP", "The RPS tripped due to high reactor temperature (>=" .. const.RPS_LIMITS.MAX_DAMAGE_TEMPERATURE .. "K).")
doc("fp_rps_fuel", "LO FUEL", "The RPS tripped due to having no fuel.")
doc("fp_rps_waste", "HI WASTE", "The RPS tripped due to having high levels of waste (>" .. (const.RPS_LIMITS.MAX_WASTE_FILL * 100) .. "%).")
doc("fp_rps_ccool", "LO CCOOLANT", "The RPS tripped due to having low levels of cooled coolant (<" .. (const.RPS_LIMITS.MIN_COOLANT_FILL * 100) .. "%).")
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("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.")
sect("Device List")
doc("fp_rtu_rt", "RT", "In each RTU entry row, an RT light indicates if the co-routine for that RTU unit is running. This is never lit for redstone units.")
doc("fp_rtu_rt", "Device Status", "In each RTU entry row, the light to the left of the device name indicates its peripheral status.")
list(DOC_LIST_TYPE.LED, { "disconnected", "faulted", "unformed", "ok" }, { colors.red, colors.orange, colors.yellow, colors.green })
text("Note that disconnected devices lack detailed information and will not be modifiable in configuration until re-connected.")
doc("fp_rtu_rt", "Device Assignment", "In each RTU entry row, the device identification is to the right of the status light. This begins with the device type and its index followed by its assignment after the \x1a, which is a unit or the facility (FACIL). Unit 1's 3rd turbine would show up as 'TURBINE 3 \x1a UNIT 1'.")
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.")
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'.")
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.")
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.")
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.")
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_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.")
docs.glossary = {
abbvs = {}, terms = {}
}
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_FP", "FP", "Front Panel. See Terminology.")
doc("G_Hi", "Hi", "High.")
doc("G_Lo", "Lo", "Low.")
doc("G_PID", "PID", "A Proportional Integral Derivative closed-loop controller.")
doc("G_PKT", "PKT", "Pocket. Abbreviation for the pocket computer.")
doc("G_PLC", "PLC", "Programmable Logic Controller. A device that not only reports data and controls outputs, but can also make decisions on its own.")
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_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.")
doc("G_SVR", "SVR", "Supervisor. Abbreviation for the supervisory computer.")
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_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.")
doc("G_LowLow", "Low Low", "Very Low.")
doc("G_Nominal", "Nominal", "Normal operation. Everything operating as intended.")
doc("G_Ringback", "Ringback", "An indication that an alarm had gone off but is no longer having its trip condition(s) met. This is to make you are aware that it occurred.")
doc("G_SCRAM", "SCRAM", "[Emergency] shut-down of a reactor by stopping the fission. In Mekanism and here, it isn't always for an emergency.")
doc("G_Transient", "Transient", "A temporary change in state from normal operation. Coolant levels dropping or core temperature rising above nominal values are examples of transients.")
doc("G_Trip", "Trip", "A checked condition had occurred, see 'Tripped'.")
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.")
return docs

View File

@@ -2,122 +2,86 @@
-- Pocket GUI Root
--
local iocontrol = require("pocket.iocontrol")
local util = require("scada-common.util")
local style = require("pocket.ui.style")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local conn_waiting = require("pocket.ui.components.conn_waiting")
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 guide_app = require("pocket.ui.apps.guide")
local loader_app = require("pocket.ui.apps.loader")
local sys_apps = require("pocket.ui.apps.sys_apps")
local unit_app = require("pocket.ui.apps.unit")
local boiler_page = require("pocket.ui.pages.boiler_page")
local diag_page = require("pocket.ui.pages.diag_page")
local home_page = require("pocket.ui.pages.home_page")
local reactor_page = require("pocket.ui.pages.reactor_page")
local turbine_page = require("pocket.ui.pages.turbine_page")
local unit_page = require("pocket.ui.pages.unit_page")
local home_page = require("pocket.ui.pages.home_page")
local core = require("graphics.core")
local style = require("pocket.ui.style")
local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local core = require("graphics.core")
local Sidebar = require("graphics.elements.controls.sidebar")
local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local LINK_STATE = iocontrol.LINK_STATE
local NAV_PAGE = iocontrol.NAV_PAGE
local WaitingAnim = require("graphics.elements.animations.waiting")
local PushButton = require("graphics.elements.controls.push_button")
local Sidebar = require("graphics.elements.controls.sidebar")
local SignalBar = require("graphics.elements.indicators.signal")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
-- create new main view
---@param main graphics_element main displaybox
local function init(main)
local nav = iocontrol.get_db().nav
local ps = iocontrol.get_db().ps
local db = iocontrol.get_db()
-- window header message
TextBox{parent=main,y=1,text="",alignment=ALIGN.LEFT,height=1,fg_bg=style.header}
-- window header message and connection status
TextBox{parent=main,y=1,text="EARLY ACCESS ALPHA 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)}
--
-- root panel panes (connection screens + main screen)
--
db.ps.subscribe("svr_conn_quality", svr_conn.set_value)
db.ps.subscribe("crd_conn_quality", crd_conn.set_value)
local root_pane_div = Div{parent=main,x=1,y=2}
local conn_sv_wait = conn_waiting(root_pane_div, 6, false)
local conn_api_wait = conn_waiting(root_pane_div, 6, true)
local start_pane = Div{parent=main,x=1,y=2}
local main_pane = Div{parent=main,x=1,y=2}
local root_panes = { conn_sv_wait, conn_api_wait, main_pane }
local root_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes=root_panes}
WaitingAnim{parent=start_pane,x=12,y=7,fg_bg=cpair(colors.lightBlue,style.root.bkg)}
TextBox{parent=start_pane,y=11,text="starting up...",alignment=ALIGN.CENTER,fg_bg=cpair(colors.lightGray,style.root.bkg)}
root_pane.register(ps, "link_state", function (state)
if state == LINK_STATE.UNLINKED or state == LINK_STATE.API_LINK_ONLY then
root_pane.set_value(1)
elseif state == LINK_STATE.SV_LINK_ONLY then
root_pane.set_value(2)
else
root_pane.set_value(3)
end
end)
--
-- main page panel panes & sidebar
--
local root_pane = MultiPane{parent=main,x=1,y=2,panes={start_pane,main_pane}}
local page_div = Div{parent=main_pane,x=4,y=1}
local sidebar_tabs = {
{
char = "#",
color = cpair(colors.black,colors.green)
},
{
char = "U",
color = cpair(colors.black,colors.yellow)
},
{
char = "R",
color = cpair(colors.black,colors.cyan)
},
{
char = "B",
color = cpair(colors.black,colors.lightGray)
},
{
char = "T",
color = cpair(colors.black,colors.white)
},
{
char = "D",
color = cpair(colors.black,colors.orange)
}
}
-- create all the apps & pages
home_page(page_div)
unit_app(page_div)
control_app(page_div)
guide_app(page_div)
loader_app(page_div)
sys_apps(page_div)
diag_apps(page_div)
dummy_app(page_div)
local panes = { home_page(page_div), unit_page(page_div), reactor_page(page_div), boiler_page(page_div), turbine_page(page_div), diag_page(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")
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
db.nav.set_pane(MultiPane{parent=page_div,x=1,y=1,panes=db.nav.get_containers()})
db.nav.set_sidebar(Sidebar{parent=main_pane,x=1,y=1,height=18,fg_bg=cpair(colors.white,colors.gray)})
local function navigate_sidebar(page)
if page == 1 then
nav.page = nav.sub_pages[NAV_PAGE.HOME]
elseif page == 2 then
nav.page = nav.sub_pages[NAV_PAGE.UNITS]
elseif page == 3 then
nav.page = nav.sub_pages[NAV_PAGE.REACTORS]
elseif page == 4 then
nav.page = nav.sub_pages[NAV_PAGE.BOILERS]
elseif page == 5 then
nav.page = nav.sub_pages[NAV_PAGE.TURBINES]
elseif page == 6 then
nav.page = nav.sub_pages[NAV_PAGE.DIAG]
end
PushButton{parent=main_pane,x=1,y=19,text="\x1b",min_width=3,fg_bg=cpair(colors.white,colors.gray),active_fg_bg=cpair(colors.gray,colors.black),callback=db.nav.nav_up}
page_pane.set_value(page)
end
db.nav.open_app(APP_ID.ROOT)
Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=navigate_sidebar}
-- done with initial render, lets go!
root_pane.set_value(2)
end
return init

View File

@@ -1,22 +0,0 @@
-- local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
-- local cpair = core.cpair
local ALIGN = core.ALIGN
-- new boiler page view
---@param root graphics_element parent
local function new_view(root)
local main = Div{parent=root,x=1,y=1}
TextBox{parent=main,text="BOILERS",x=1,y=1,height=1,alignment=ALIGN.CENTER}
return main
end
return new_view

View File

@@ -0,0 +1,147 @@
local log = require("scada-common.log")
local util = require("scada-common.util")
local docs = require("pocket.ui.docs")
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.push_button")
local IndicatorLight = require("graphics.elements.indicators.light")
local LED = require("graphics.elements.indicators.led")
local ALIGN = core.ALIGN
local cpair = core.cpair
local DOC_TYPE = docs.DOC_ITEM_TYPE
local LIST_TYPE = docs.DOC_LIST_TYPE
-- new guide documentation section
---@param data _guide_section_constructor_data
---@param base_page nav_tree_page
---@param title string
---@param items table
---@param scroll_height integer
---@return nav_tree_page
return function (data, base_page, title, items, scroll_height)
local app, page_div, panes, doc_map, search_db, btn_fg_bg, btn_active = table.unpack(data)
local section_page = app.new_page(base_page, #panes + 1)
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}
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}
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)}
local sect_id = 1
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
if item.type == DOC_TYPE.SECTION then
---@cast item pocket_doc_sect
local title_text = sect_id.."."
local title_offs = string.len(title_text) + 2
local sect_title = Div{parent=def_list,height=1}
TextBox{parent=sect_title,x=1,text=title_text,fg_bg=cpair(colors.lightGray,colors.black)}
local anchor = TextBox{parent=sect_title,x=title_offs,y=1,text=item.name,anchor=true,fg_bg=cpair(colors.green,colors.black)}
page_end = Div{parent=def_list,height=1,can_focus=true}
local function view()
page_end.focus()
view_page.nav_to()
anchor.focus()
end
if #name_list.get_children() > 0 then
local _ = Div{parent=name_list,height=1}
end
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}
sect_id = sect_id + 1
elseif item.type == DOC_TYPE.SUBSECTION then
---@cast item pocket_doc_subsect
local anchor = TextBox{parent=def_list,text=item.name,anchor=true,fg_bg=cpair(colors.blue,colors.black)}
if item.subtitle then
TextBox{parent=def_list,text=item.subtitle,fg_bg=cpair(colors.gray,colors.black)}
end
TextBox{parent=def_list,text=item.body}
page_end = Div{parent=def_list,height=1,can_focus=true}
local function view()
page_end.focus()
view_page.nav_to()
anchor.focus()
end
doc_map[item.key] = view
table.insert(search_db, { string.lower(item.name), item.name, title, view })
local name_entry = Div{parent=name_list,height=#util.strwrap(item.name,name_list.get_width()-3)}
TextBox{parent=name_entry,x=1,text="\x10",fg_bg=cpair(colors.gray,colors.black)}
PushButton{parent=name_entry,x=3,y=1,text=item.name,alignment=ALIGN.LEFT,fg_bg=cpair(colors.blue,colors.black),active_fg_bg=btn_active,callback=view}
elseif item.type == DOC_TYPE.TEXT then
---@cast item pocket_doc_text
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
local container = Div{parent=def_list,height=#item.items}
if item.list_type == LIST_TYPE.BULLET then
for _, li in ipairs(item.items) do
TextBox{parent=container,x=2,text="\x07 "..li}
end
elseif item.list_type == LIST_TYPE.NUMBERED then
local width = string.len("" .. #item.items)
for idx, li in ipairs(item.items) do
TextBox{parent=container,x=2,text=util.sprintf("%" .. width .. "d. %s", idx, li)}
end
elseif item.list_type == LIST_TYPE.INDICATOR then
for idx, li in ipairs(item.items) do
local _ = IndicatorLight{parent=container,x=2,label=li,colors=cpair(colors.black,item.colors[idx])}
end
elseif item.list_type == LIST_TYPE.LED then
for idx, li in ipairs(item.items) do
local _ = LED{parent=container,x=2,label=li,colors=cpair(colors.black,item.colors[idx])}
end
end
page_end = Div{parent=def_list,height=1,can_focus=true}
end
if i % 12 == 0 then util.nop() end
end
log.debug("guide section " .. title .. " generated with final height ".. page_end.get_y())
util.nop()
return section_page
end

View File

@@ -1,21 +1,64 @@
local core = require("graphics.core")
--
-- Main Home Page
--
local Div = require("graphics.elements.div")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local App = require("graphics.elements.controls.app")
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
-- new home page view
---@param root graphics_element parent
local function new_view(root)
local main = Div{parent=root,x=1,y=1}
local db = iocontrol.get_db()
App{parent=main,x=3,y=2,text="\x17",title="PRC",callback=function()end,app_fg_bg=cpair(colors.black,colors.purple)}
App{parent=main,x=10,y=2,text="\x15",title="CTL",callback=function()end,app_fg_bg=cpair(colors.black,colors.green)}
App{parent=main,x=17,y=2,text="\x08",title="DEV",callback=function()end,app_fg_bg=cpair(colors.black,colors.lightGray)}
App{parent=main,x=3,y=7,text="\x7f",title="Waste",callback=function()end,app_fg_bg=cpair(colors.black,colors.brown)}
App{parent=main,x=10,y=7,text="\xb6",title="Guide",callback=function()end,app_fg_bg=cpair(colors.black,colors.cyan)}
local main = Div{parent=root,x=1,y=1,height=19}
local app = db.nav.register_app(APP_ID.ROOT, main)
local apps_1 = Div{parent=main,x=1,y=1,height=15}
local apps_2 = Div{parent=main,x=1,y=1,height=15}
local panes = { apps_1, apps_2 }
local app_pane = AppMultiPane{parent=main,x=1,y=1,height=18,panes=panes,active_color=colors.lightGray,nav_colors=cpair(colors.lightGray,colors.gray),scroll_nav=true,drag_nav=true,callback=app.switcher}
app.set_root_pane(app_pane)
app.new_page(app.new_page(nil, 1), 2)
local function open(id) db.nav.open_app(id) end
app.set_sidebar({
{ label = " #\x10", tall = true, color = core.cpair(colors.black, colors.green), callback = function () open(APP_ID.ROOT) end }
})
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=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.DUMMY)end,app_fg_bg=cpair(colors.black,colors.purple),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=9,y=7,text="\x7f",title="Waste",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.brown),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=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}
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}
return main
end

View File

@@ -1,22 +0,0 @@
-- local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
-- local cpair = core.cpair
local ALIGN = core.ALIGN
-- new reactor page view
---@param root graphics_element parent
local function new_view(root)
local main = Div{parent=root,x=1,y=1}
TextBox{parent=main,text="REACTOR",x=1,y=1,height=1,alignment=ALIGN.CENTER}
return main
end
return new_view

View File

@@ -1,22 +0,0 @@
-- local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
-- local cpair = core.cpair
local ALIGN = core.ALIGN
-- new turbine page view
---@param root graphics_element parent
local function new_view(root)
local main = Div{parent=root,x=1,y=1}
TextBox{parent=main,text="TURBINES",x=1,y=1,height=1,alignment=ALIGN.CENTER}
return main
end
return new_view

View File

@@ -0,0 +1,131 @@
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local PushButton = require("graphics.elements.controls.push_button")
local DataIndicator = require("graphics.elements.indicators.data")
local StateIndicator = require("graphics.elements.indicators.state")
local IconIndicator = require("graphics.elements.indicators.icon")
local VerticalBar = require("graphics.elements.indicators.vbar")
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 red_ind_s = style.icon_states.red_ind_s
local yel_ind_s = style.icon_states.yel_ind_s
-- create a boiler view in the unit app
---@param app pocket_app
---@param u_page nav_tree_page
---@param panes table
---@param blr_pane graphics_element
---@param b_id integer boiler ID
---@param ps psil
---@param update function
return function (app, u_page, panes, blr_pane, b_id, ps, update)
local db = iocontrol.get_db()
local blr_div = Div{parent=blr_pane,x=2,width=blr_pane.get_width()-2}
table.insert(panes, blr_div)
local blr_page = app.new_page(u_page, #panes)
blr_page.tasks = { update }
TextBox{parent=blr_div,y=1,text="BLR #"..b_id,width=8}
local status = StateIndicator{parent=blr_div,x=10,y=1,states=style.boiler.states,value=1,min_width=12}
status.register(ps, "BoilerStateStatus", status.update)
local hcool = VerticalBar{parent=blr_div,x=1,y=4,fg_bg=cpair(colors.orange,colors.gray),height=5,width=1}
local water = VerticalBar{parent=blr_div,x=3,y=4,fg_bg=cpair(colors.blue,colors.gray),height=5,width=1}
local steam = VerticalBar{parent=blr_div,x=19,y=4,fg_bg=cpair(colors.white,colors.gray),height=5,width=1}
local ccool = VerticalBar{parent=blr_div,x=21,y=4,fg_bg=cpair(colors.lightBlue,colors.gray),height=5,width=1}
TextBox{parent=blr_div,text="H",x=1,y=3,width=1,fg_bg=label}
TextBox{parent=blr_div,text="W",x=3,y=3,width=1,fg_bg=label}
TextBox{parent=blr_div,text="S",x=19,y=3,width=1,fg_bg=label}
TextBox{parent=blr_div,text="C",x=21,y=3,width=1,fg_bg=label}
hcool.register(ps, "hcool_fill", hcool.update)
water.register(ps, "water_fill", water.update)
steam.register(ps, "steam_fill", steam.update)
ccool.register(ps, "ccool_fill", ccool.update)
TextBox{parent=blr_div,text="Temperature",x=5,y=5,width=13,fg_bg=label}
local t_prec = util.trinary(db.temp_label == types.TEMP_SCALE_UNITS[types.TEMP_SCALE.KELVIN], 11, 10)
local temp = DataIndicator{parent=blr_div,x=5,y=6,lu_colors=lu_col,label="",unit=db.temp_label,format="%"..t_prec..".2f",value=0,commas=true,width=13,fg_bg=text_fg}
temp.register(ps, "temperature", function (t) temp.update(db.temp_convert(t)) end)
local b_wll = IconIndicator{parent=blr_div,y=10,label="Water Level Lo",states=red_ind_s}
local b_hr = IconIndicator{parent=blr_div,label="Heating Rate Lo",states=yel_ind_s}
b_wll.register(ps, "WaterLevelLow", b_wll.update)
b_hr.register(ps, "HeatingRateLow", b_hr.update)
TextBox{parent=blr_div,text="Boil Rate",x=1,y=13,width=12,fg_bg=label}
local boil_r = DataIndicator{parent=blr_div,x=6,y=14,lu_colors=lu_col,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=text_fg}
boil_r.register(ps, "boil_rate", boil_r.update)
local blr_ext_div = Div{parent=blr_pane,x=2,width=blr_pane.get_width()-2}
table.insert(panes, blr_ext_div)
local blr_ext_page = app.new_page(blr_page, #panes)
blr_ext_page.tasks = { update }
PushButton{parent=blr_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=blr_ext_page.nav_to}
PushButton{parent=blr_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=blr_page.nav_to}
TextBox{parent=blr_ext_div,y=1,text="More Boiler Info",alignment=ALIGN.CENTER}
local function update_amount(indicator)
return function (x) indicator.update(x.amount) end
end
TextBox{parent=blr_ext_div,text="Hot Coolant",x=1,y=3,width=12,fg_bg=label}
local heated_p = DataIndicator{parent=blr_ext_div,x=14,y=3,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
local hcool_amnt = DataIndicator{parent=blr_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}
heated_p.register(ps, "hcool_fill", function (x) heated_p.update(x * 100) end)
hcool_amnt.register(ps, "hcool", update_amount(hcool_amnt))
TextBox{parent=blr_ext_div,text="Water Tank",x=1,y=6,width=9,fg_bg=label}
local fuel_p = DataIndicator{parent=blr_ext_div,x=14,y=6,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
local fuel_amnt = DataIndicator{parent=blr_ext_div,x=1,y=7,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
fuel_p.register(ps, "water_fill", function (x) fuel_p.update(x * 100) end)
fuel_amnt.register(ps, "water", update_amount(fuel_amnt))
TextBox{parent=blr_ext_div,text="Steam Tank",x=1,y=9,width=10,fg_bg=label}
local steam_p = DataIndicator{parent=blr_ext_div,x=14,y=9,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
local steam_amnt = DataIndicator{parent=blr_ext_div,x=1,y=10,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
steam_p.register(ps, "steam_fill", function (x) steam_p.update(x * 100) end)
steam_amnt.register(ps, "steam", update_amount(steam_amnt))
TextBox{parent=blr_ext_div,text="Cool Coolant",x=1,y=12,width=12,fg_bg=label}
local cooled_p = DataIndicator{parent=blr_ext_div,x=14,y=12,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
local ccool_amnt = DataIndicator{parent=blr_ext_div,x=1,y=13,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
cooled_p.register(ps, "ccool_fill", function (x) cooled_p.update(x * 100) end)
ccool_amnt.register(ps, "ccool", update_amount(ccool_amnt))
TextBox{parent=blr_ext_div,text="Env. Loss",x=1,y=15,width=9,fg_bg=label}
local env_loss = DataIndicator{parent=blr_ext_div,x=11,y=15,lu_colors=lu_col,label="",unit="",format="%11.8f",value=0,width=11,fg_bg=text_fg}
env_loss.register(ps, "env_loss", env_loss.update)
return blr_page.nav_to
end

View File

@@ -1,22 +0,0 @@
-- local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
-- local cpair = core.cpair
local ALIGN = core.ALIGN
-- new unit page view
---@param root graphics_element parent
local function new_view(root)
local main = Div{parent=root,x=1,y=1}
TextBox{parent=main,text="UNITS",x=1,y=1,height=1,alignment=ALIGN.CENTER}
return main
end
return new_view

View File

@@ -0,0 +1,158 @@
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local PushButton = require("graphics.elements.controls.push_button")
local DataIndicator = require("graphics.elements.indicators.data")
local StateIndicator = require("graphics.elements.indicators.state")
local IconIndicator = require("graphics.elements.indicators.icon")
local VerticalBar = require("graphics.elements.indicators.vbar")
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 red_ind_s = style.icon_states.red_ind_s
local yel_ind_s = style.icon_states.yel_ind_s
-- create a reactor view in the unit app
---@param app pocket_app
---@param u_page nav_tree_page
---@param panes table
---@param page_div graphics_element
---@param u_ps psil
---@param update function
return function (app, u_page, panes, page_div, u_ps, update)
local db = iocontrol.get_db()
local rct_pane = Div{parent=page_div}
local rct_div = Div{parent=rct_pane,x=2,width=page_div.get_width()-2}
table.insert(panes, rct_div)
local rct_page = app.new_page(u_page, #panes)
rct_page.tasks = { update }
TextBox{parent=rct_div,y=1,text="Reactor",width=8}
local status = StateIndicator{parent=rct_div,x=10,y=1,states=style.reactor.states,value=1,min_width=12}
status.register(u_ps, "U_ReactorStateStatus", status.update)
local fuel = VerticalBar{parent=rct_div,x=1,y=4,fg_bg=cpair(colors.lightGray,colors.gray),height=5,width=1}
local ccool = VerticalBar{parent=rct_div,x=3,y=4,fg_bg=cpair(colors.blue,colors.gray),height=5,width=1}
local hcool = VerticalBar{parent=rct_div,x=19,y=4,fg_bg=cpair(colors.white,colors.gray),height=5,width=1}
local waste = VerticalBar{parent=rct_div,x=21,y=4,fg_bg=cpair(colors.brown,colors.gray),height=5,width=1}
TextBox{parent=rct_div,text="F",x=1,y=3,width=1,fg_bg=label}
TextBox{parent=rct_div,text="C",x=3,y=3,width=1,fg_bg=label}
TextBox{parent=rct_div,text="H",x=19,y=3,width=1,fg_bg=label}
TextBox{parent=rct_div,text="W",x=21,y=3,width=1,fg_bg=label}
fuel.register(u_ps, "fuel_fill", fuel.update)
ccool.register(u_ps, "ccool_fill", ccool.update)
hcool.register(u_ps, "hcool_fill", hcool.update)
waste.register(u_ps, "waste_fill", waste.update)
ccool.register(u_ps, "ccool_type", function (type)
if type == types.FLUID.SODIUM then
ccool.recolor(cpair(colors.lightBlue, colors.gray))
else
ccool.recolor(cpair(colors.blue, colors.gray))
end
end)
hcool.register(u_ps, "hcool_type", function (type)
if type == types.FLUID.SUPERHEATED_SODIUM then
hcool.recolor(cpair(colors.orange, colors.gray))
else
hcool.recolor(cpair(colors.white, colors.gray))
end
end)
TextBox{parent=rct_div,text="Burn Rate",x=5,y=4,width=13,fg_bg=label}
local burn_rate = DataIndicator{parent=rct_div,x=5,y=5,lu_colors=lu_col,label="",unit="mB/t",format="%8.2f",value=0,commas=true,width=13,fg_bg=text_fg}
TextBox{parent=rct_div,text="Temperature",x=5,y=6,width=13,fg_bg=label}
local t_prec = util.trinary(db.temp_label == types.TEMP_SCALE_UNITS[types.TEMP_SCALE.KELVIN], 11, 10)
local core_temp = DataIndicator{parent=rct_div,x=5,y=7,lu_colors=lu_col,label="",unit=db.temp_label,format="%"..t_prec..".2f",value=0,commas=true,width=13,fg_bg=text_fg}
burn_rate.register(u_ps, "act_burn_rate", burn_rate.update)
core_temp.register(u_ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end)
local r_temp = IconIndicator{parent=rct_div,y=10,label="Reactor Temp. Hi",states=red_ind_s}
local r_rhdt = IconIndicator{parent=rct_div,label="Hi Delta Temp.",states=yel_ind_s}
local r_firl = IconIndicator{parent=rct_div,label="Fuel Rate Lo",states=yel_ind_s}
local r_wloc = IconIndicator{parent=rct_div,label="Waste Line Occl.",states=yel_ind_s}
local r_hsrt = IconIndicator{parent=rct_div,label="Hi Startup Rate",states=yel_ind_s}
r_temp.register(u_ps, "ReactorTempHigh", r_temp.update)
r_rhdt.register(u_ps, "ReactorHighDeltaT", r_rhdt.update)
r_firl.register(u_ps, "FuelInputRateLow", r_firl.update)
r_wloc.register(u_ps, "WasteLineOcclusion", r_wloc.update)
r_hsrt.register(u_ps, "HighStartupRate", r_hsrt.update)
TextBox{parent=rct_div,text="HR",x=1,y=16,width=4,fg_bg=label}
local heating_r = DataIndicator{parent=rct_div,x=6,y=16,lu_colors=lu_col,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=text_fg}
TextBox{parent=rct_div,text="DMG",x=1,y=17,width=4,fg_bg=label}
local damage_p = DataIndicator{parent=rct_div,x=6,y=17,lu_colors=lu_col,label="",unit="%",format="%11.2f",value=0,width=16,fg_bg=text_fg}
heating_r.register(u_ps, "heating_rate", heating_r.update)
damage_p.register(u_ps, "damage", damage_p.update)
local rct_ext_div = Div{parent=rct_pane,x=2,width=page_div.get_width()-2}
table.insert(panes, rct_ext_div)
local rct_ext_page = app.new_page(rct_page, #panes)
rct_ext_page.tasks = { update }
PushButton{parent=rct_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=rct_ext_page.nav_to}
PushButton{parent=rct_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=rct_page.nav_to}
TextBox{parent=rct_ext_div,y=1,text="More Reactor Info",alignment=ALIGN.CENTER}
TextBox{parent=rct_ext_div,text="Fuel Tank",x=1,y=3,width=9,fg_bg=label}
local fuel_p = DataIndicator{parent=rct_ext_div,x=14,y=3,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
local fuel_amnt = DataIndicator{parent=rct_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}
fuel_p.register(u_ps, "fuel_fill", function (x) fuel_p.update(x * 100) end)
fuel_amnt.register(u_ps, "fuel", fuel_amnt.update)
TextBox{parent=rct_ext_div,text="Cool Coolant",x=1,y=6,width=12,fg_bg=label}
local cooled_p = DataIndicator{parent=rct_ext_div,x=14,y=6,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
local ccool_amnt = DataIndicator{parent=rct_ext_div,x=1,y=7,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
cooled_p.register(u_ps, "ccool_fill", function (x) cooled_p.update(x * 100) end)
ccool_amnt.register(u_ps, "ccool_amnt", ccool_amnt.update)
TextBox{parent=rct_ext_div,text="Hot Coolant",x=1,y=9,width=12,fg_bg=label}
local heated_p = DataIndicator{parent=rct_ext_div,x=14,y=9,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
local hcool_amnt = DataIndicator{parent=rct_ext_div,x=1,y=10,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
heated_p.register(u_ps, "hcool_fill", function (x) heated_p.update(x * 100) end)
hcool_amnt.register(u_ps, "hcool_amnt", hcool_amnt.update)
TextBox{parent=rct_ext_div,text="Waste Tank",x=1,y=12,width=10,fg_bg=label}
local waste_p = DataIndicator{parent=rct_ext_div,x=14,y=12,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
local waste_amnt = DataIndicator{parent=rct_ext_div,x=1,y=13,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
waste_p.register(u_ps, "waste_fill", function (x) waste_p.update(x * 100) end)
waste_amnt.register(u_ps, "waste", waste_amnt.update)
TextBox{parent=rct_ext_div,text="Boil Eff.",x=1,y=15,width=9,fg_bg=label}
TextBox{parent=rct_ext_div,text="Env. Loss",x=1,y=16,width=9,fg_bg=label}
local boil_eff = DataIndicator{parent=rct_ext_div,x=11,y=15,lu_colors=lu_col,label="",unit="%",format="%9.2f",value=0,width=11,fg_bg=text_fg}
local env_loss = DataIndicator{parent=rct_ext_div,x=11,y=16,lu_colors=lu_col,label="",unit="",format="%11.8f",value=0,width=11,fg_bg=text_fg}
boil_eff.register(u_ps, "boil_eff", function (x) boil_eff.update(x * 100) end)
env_loss.register(u_ps, "env_loss", env_loss.update)
return rct_page.nav_to
end

View File

@@ -0,0 +1,116 @@
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local PushButton = require("graphics.elements.controls.push_button")
local DataIndicator = require("graphics.elements.indicators.data")
local IconIndicator = require("graphics.elements.indicators.icon")
local PowerIndicator = require("graphics.elements.indicators.power")
local StateIndicator = require("graphics.elements.indicators.state")
local VerticalBar = require("graphics.elements.indicators.vbar")
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 tri_ind_s = style.icon_states.tri_ind_s
local red_ind_s = style.icon_states.red_ind_s
local yel_ind_s = style.icon_states.yel_ind_s
-- create a turbine view in the unit app
---@param app pocket_app
---@param u_page nav_tree_page
---@param panes table
---@param tbn_pane graphics_element
---@param u_id integer unit ID
---@param t_id integer turbine ID
---@param ps psil
---@param update function
return function (app, u_page, panes, tbn_pane, u_id, t_id, ps, update)
local db = iocontrol.get_db()
local tbn_div = Div{parent=tbn_pane,x=2,width=tbn_pane.get_width()-2}
table.insert(panes, tbn_div)
local tbn_page = app.new_page(u_page, #panes)
tbn_page.tasks = { update }
TextBox{parent=tbn_div,y=1,text="TRBN #"..t_id,width=8}
local status = StateIndicator{parent=tbn_div,x=10,y=1,states=style.turbine.states,value=1,min_width=12}
status.register(ps, "TurbineStateStatus", status.update)
local steam = VerticalBar{parent=tbn_div,x=1,y=4,fg_bg=cpair(colors.white,colors.gray),height=5,width=1}
local ccool = VerticalBar{parent=tbn_div,x=21,y=4,fg_bg=cpair(colors.green,colors.gray),height=5,width=1}
TextBox{parent=tbn_div,text="S",x=1,y=3,width=1,fg_bg=label}
TextBox{parent=tbn_div,text="E",x=21,y=3,width=1,fg_bg=label}
steam.register(ps, "steam_fill", steam.update)
ccool.register(ps, "energy_fill", ccool.update)
TextBox{parent=tbn_div,text="Production",x=3,y=3,width=17,fg_bg=label}
local prod_rate = PowerIndicator{parent=tbn_div,x=3,y=4,lu_colors=lu_col,label="",unit=db.energy_label,format="%11.2f",value=0,rate=true,width=17,fg_bg=text_fg}
TextBox{parent=tbn_div,text="Flow Rate",x=3,y=5,width=17,fg_bg=label}
local flow_rate = DataIndicator{parent=tbn_div,x=3,y=6,lu_colors=lu_col,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=17,fg_bg=text_fg}
TextBox{parent=tbn_div,text="Steam Input Rate",x=3,y=7,width=17,fg_bg=label}
local input_rate = DataIndicator{parent=tbn_div,x=3,y=8,lu_colors=lu_col,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=17,fg_bg=text_fg}
prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(db.energy_convert(val)) end)
flow_rate.register(ps, "flow_rate", flow_rate.update)
input_rate.register(ps, "steam_input_rate", input_rate.update)
local t_sdo = IconIndicator{parent=tbn_div,y=10,label="Steam Dumping",states=tri_ind_s}
local t_tos = IconIndicator{parent=tbn_div,label="Over Speed",states=red_ind_s}
local t_gtrp = IconIndicator{parent=tbn_div,label="Generator Trip",states=yel_ind_s}
local t_trp = IconIndicator{parent=tbn_div,label="Turbine Trip",states=red_ind_s}
t_sdo.register(ps, "SteamDumpOpen", t_sdo.update)
t_tos.register(ps, "TurbineOverSpeed", t_tos.update)
t_gtrp.register(ps, "GeneratorTrip", t_gtrp.update)
t_trp.register(ps, "TurbineTrip", t_trp.update)
local tbn_ext_div = Div{parent=tbn_pane,x=2,width=tbn_pane.get_width()-2}
table.insert(panes, tbn_ext_div)
local tbn_ext_page = app.new_page(tbn_page, #panes)
tbn_ext_page.tasks = { update }
PushButton{parent=tbn_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=tbn_ext_page.nav_to}
PushButton{parent=tbn_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=tbn_page.nav_to}
TextBox{parent=tbn_ext_div,y=1,text="More Turbine Info",alignment=ALIGN.CENTER}
TextBox{parent=tbn_ext_div,text="Steam Tank",x=1,y=3,width=10,fg_bg=label}
local steam_p = DataIndicator{parent=tbn_ext_div,x=14,y=3,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
local steam_amnt = DataIndicator{parent=tbn_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}
steam_p.register(ps, "steam_fill", function (x) steam_p.update(x * 100) end)
steam_amnt.register(ps, "steam", function (x) steam_amnt.update(x.amount) end)
TextBox{parent=tbn_ext_div,text="Energy Fill",x=1,y=6,width=12,fg_bg=label}
local charge_p = DataIndicator{parent=tbn_ext_div,x=14,y=6,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
local charge_amnt = PowerIndicator{parent=tbn_ext_div,x=1,y=7,lu_colors=lu_col,label="",unit=db.energy_label,format="%17.4f",value=0,width=21,fg_bg=text_fg}
charge_p.register(ps, "energy_fill", function (x) charge_p.update(x * 100) end)
charge_amnt.register(ps, "energy", function (val) charge_amnt.update(db.energy_convert(val)) end)
TextBox{parent=tbn_ext_div,text="Rotation Rate",x=1,y=9,width=13,fg_bg=label}
local rotation = DataIndicator{parent=tbn_ext_div,x=1,y=10,lu_colors=lu_col,label="",unit="",format="%21.12f",value=0,width=21,fg_bg=text_fg}
rotation.register(ps, "steam", function ()
local ok, result = pcall(function () return util.turbine_rotation(db.units[u_id].turbine_data_tbl[t_id]) end)
if ok then rotation.update(result) end
end)
return tbn_page.nav_to
end

View File

@@ -12,7 +12,9 @@ local cpair = core.cpair
style.root = cpair(colors.white, colors.black)
style.header = cpair(colors.white, colors.gray)
style.label = cpair(colors.gray, colors.lightGray)
style.text_fg = cpair(colors.white, colors._INHERIT)
style.label = cpair(colors.lightGray, colors.black)
style.label_unit_pair = cpair(colors.lightGray, colors.lightGray)
style.colors = {
{ c = colors.red, hex = 0xdf4949 },
@@ -33,6 +35,46 @@ style.colors = {
-- { c = colors.brown, hex = 0x7f664c }
}
local states = {}
states.basic_states = {
{ color = cpair(colors.black, colors.lightGray), symbol = "\x07" },
{ color = cpair(colors.black, colors.red), symbol = "-" },
{ color = cpair(colors.black, colors.yellow), symbol = "\x1e" },
{ color = cpair(colors.black, colors.green), symbol = "+" }
}
states.mode_states = {
{ color = cpair(colors.black, colors.lightGray), symbol = "\x07" },
{ color = cpair(colors.black, colors.red), symbol = "-" },
{ color = cpair(colors.black, colors.green), symbol = "+" },
{ color = cpair(colors.black, colors.purple), symbol = "A" }
}
states.emc_ind_s = {
{ color = cpair(colors.black, colors.gray), symbol = "-" },
{ color = cpair(colors.black, colors.white), symbol = "\x07" },
{ color = cpair(colors.black, colors.green), symbol = "+" }
}
states.tri_ind_s = {
{ color = cpair(colors.black, colors.lightGray), symbol = "+" },
{ color = cpair(colors.black, colors.yellow), symbol = "\x1e" },
{ color = cpair(colors.black, colors.red), symbol = "-" }
}
states.red_ind_s = {
{ color = cpair(colors.black, colors.lightGray), symbol = "+" },
{ color = cpair(colors.black, colors.red), symbol = "-" }
}
states.yel_ind_s = {
{ color = cpair(colors.black, colors.lightGray), symbol = "+" },
{ color = cpair(colors.black, colors.yellow), symbol = "-" }
}
style.icon_states = states
-- MAIN LAYOUT --
style.reactor = {
@@ -40,7 +82,7 @@ style.reactor = {
states = {
{
color = cpair(colors.black, colors.yellow),
text = "PLC OFF-LINE"
text = "OFF-LINE"
},
{
color = cpair(colors.black, colors.orange),
@@ -64,7 +106,7 @@ style.reactor = {
},
{
color = cpair(colors.black, colors.red),
text = "FORCE DISABLED"
text = "FORCE DSBL"
}
}
}

View File

@@ -0,0 +1,239 @@
local comms = require("scada-common.comms")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local plc = require("reactor-plc.plc")
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.push_button")
local tri = util.trinary
local cpair = core.cpair
local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE
local self = {
nic = nil, ---@type nic
net_listen = false,
sv_addr = comms.BROADCAST,
sv_seq_num = util.time_ms() * 10,
self_check_pass = true,
settings = nil, ---@type plc_config
run_test_btn = nil, ---@type graphics_element
sc_log = nil, ---@type graphics_element
self_check_msg = nil ---@type function
}
-- report successful completion of the check
local function check_complete()
TextBox{parent=self.sc_log,text="> all tests passed!",fg_bg=cpair(colors.blue,colors._INHERIT)}
TextBox{parent=self.sc_log,text=""}
local more = Div{parent=self.sc_log,height=3,fg_bg=cpair(colors.gray,colors._INHERIT)}
TextBox{parent=more,text="if you still have a problem:"}
TextBox{parent=more,text="- check the wiki on GitHub"}
TextBox{parent=more,text="- ask for help on GitHub discussions or Discord"}
end
-- send a management packet to the supervisor
---@param msg_type MGMT_TYPE
---@param msg table
local function send_sv(msg_type, msg)
local s_pkt = comms.scada_packet()
local pkt = comms.mgmt_packet()
pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.sv_seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
self.nic.transmit(self.settings.SVR_Channel, self.settings.PLC_Channel, s_pkt)
self.sv_seq_num = self.sv_seq_num + 1
end
-- handle an establish message from the supervisor
---@param packet mgmt_frame
local function handle_packet(packet)
local error_msg = nil
if packet.scada_frame.local_channel() ~= self.settings.PLC_Channel then
error_msg = "error: unknown receive channel"
elseif packet.scada_frame.remote_channel() == self.settings.SVR_Channel and packet.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
if packet.type == MGMT_TYPE.ESTABLISH then
if packet.length == 1 then
local est_ack = packet.data[1]
if est_ack== ESTABLISH_ACK.ALLOW then
self.self_check_msg(nil, true, "")
self.sv_addr = packet.scada_frame.src_addr()
send_sv(MGMT_TYPE.CLOSE, {})
if self.self_check_pass then check_complete() end
elseif est_ack == ESTABLISH_ACK.DENY then
error_msg = "error: supervisor connection denied"
elseif est_ack == ESTABLISH_ACK.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 ...)"
else
error_msg = "error: invalid reply from supervisor"
end
else
error_msg = "error: invalid reply length from supervisor"
end
else
error_msg = "error: didn't get an establish reply from supervisor"
end
end
self.net_listen = false
self.run_test_btn.enable()
if error_msg then
self.self_check_msg(nil, false, error_msg)
end
end
-- handle supervisor connection failure
local function handle_timeout()
self.net_listen = false
self.run_test_btn.enable()
self.self_check_msg(nil, false, "make sure your supervisor is running, your channels are correct, trusted ranges are set properly (if enabled), facility keys match (if set), and if you are using wireless modems rather than ender modems, that your devices are close together in the same dimension")
end
-- 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 modem = ppm.get_wireless_modem()
local reactor = ppm.get_fission_reactor()
local valid_cfg = plc.validate_config(self.settings)
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
self.self_check_msg(nil, reactor and reactor.isFormed(), "ensure the fission reactor multiblock is formed")
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...")
-- init mac as needed
if self.settings.AuthKey and string.len(self.settings.AuthKey) >= 8 then
network.init_mac(self.settings.AuthKey)
else
network.deinit_mac()
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()
end
end
-- exit self check back home
---@param main_pane graphics_element
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 graphics_element
---@param settings_cfg plc_config
---@param check_sys graphics_element
---@param style table
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=" Reactor PLC Self-Check",fg_bg=bw_fg_bg}
self.sc_log = ListBox{parent=sc,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local last_check = { nil, nil }
function self.self_check_msg(msg, success, fail_msg)
if type(msg) == "string" then
last_check[1] = Div{parent=self.sc_log,height=1}
local e = TextBox{parent=last_check[1],text=msg,fg_bg=bw_fg_bg}
last_check[2] = e.get_x()+string.len(msg)
end
if type(fail_msg) == "string" then
TextBox{parent=last_check[1],x=last_check[2],y=1,text=tri(success,"PASS","FAIL"),fg_bg=tri(success,cpair(colors.green,colors._INHERIT),cpair(colors.red,colors._INHERIT))}
if not success then
local fail = Div{parent=self.sc_log,height=#util.strwrap(fail_msg, 46)}
TextBox{parent=fail,x=3,text=fail_msg,fg_bg=cpair(colors.gray,colors.white)}
end
self.self_check_pass = self.self_check_pass and success
end
end
PushButton{parent=sc,x=1,y=14,text="\x1b Back",callback=function()exit_self_check(main_pane)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.run_test_btn = PushButton{parent=sc,x=40,y=14,min_width=10,text="Run Test",callback=function()self_check()end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
end
-- handle incoming modem messages
---@param side string
---@param sender integer
---@param reply_to integer
---@param message any
---@param distance integer
function check.receive_sv(side, sender, reply_to, message, distance)
if self.nic ~= nil and self.net_listen then
local s_pkt = self.nic.receive(side, sender, reply_to, message, distance)
if s_pkt and s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then
tcd.abort(handle_timeout)
handle_packet(mgmt_pkt.get())
end
end
end
end
return check

View File

@@ -0,0 +1,620 @@
local log = require("scada-common.log")
local rsio = require("scada-common.rsio")
local util = require("scada-common.util")
local core = require("graphics.core")
local themes = require("graphics.themes")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local CheckBox = require("graphics.elements.controls.checkbox")
local PushButton = require("graphics.elements.controls.push_button")
local Radio2D = require("graphics.elements.controls.radio_2d")
local RadioButton = require("graphics.elements.controls.radio_button")
local NumberField = require("graphics.elements.form.number_field")
local TextField = require("graphics.elements.form.text_field")
local IndLight = require("graphics.elements.indicators.light")
local cpair = core.cpair
local RIGHT = core.ALIGN.RIGHT
local self = {
importing_legacy = false,
set_networked = nil, ---@type function
bundled_emcool = nil, ---@type function
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type graphics_element
auth_key_textbox = nil, ---@type graphics_element
auth_key_value = ""
}
local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" }
local side_options_map = { "top", "bottom", "left", "right", "front", "back" }
local color_options = { "Red", "Orange", "Yellow", "Lime", "Green", "Cyan", "Light Blue", "Blue", "Purple", "Magenta", "Pink", "White", "Light Gray", "Gray", "Black", "Brown" }
local color_options_map = { colors.red, colors.orange, colors.yellow, colors.lime, colors.green, colors.cyan, colors.lightBlue, colors.blue, colors.purple, colors.magenta, colors.pink, colors.white, colors.lightGray, colors.gray, colors.black, colors.brown }
-- convert text representation to index
---@param side string
local function side_to_idx(side)
for k, v in ipairs(side_options_map) do
if v == side then return k end
end
end
-- convert color to index
---@param color color
local function color_to_idx(color)
for k, v in ipairs(color_options_map) do
if v == color then return k end
end
end
local system = {}
-- create the system configuration view
---@param tool_ctl _plc_cfg_tool_ctl
---@param main_pane graphics_element
---@param cfg_sys table
---@param divs table
---@param style table
---@param exit function
function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
---@type plc_config, plc_config, plc_config, table, function
local settings_cfg, ini_cfg, tmp_cfg, fields, load_settings = table.unpack(cfg_sys)
---@type graphics_element, graphics_element, graphics_element, graphics_element, graphics_element
local plc_cfg, net_cfg, log_cfg, clr_cfg, summary = table.unpack(divs)
local bw_fg_bg = style.bw_fg_bg
local g_lg_fg_bg = style.g_lg_fg_bg
local nav_fg_bg = style.nav_fg_bg
local btn_act_fg_bg = style.btn_act_fg_bg
local btn_dis_fg_bg = style.btn_dis_fg_bg
--#region PLC
local plc_c_1 = Div{parent=plc_cfg,x=2,y=4,width=49}
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_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4}}
TextBox{parent=plc_cfg,x=1,y=2,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)}
TextBox{parent=plc_c_1,x=1,y=1,text="Would you like to set this PLC as networked?"}
TextBox{parent=plc_c_1,x=1,y=3,height=4,text="If you have a supervisor, select the box. You will later be prompted to select the network configuration. If you instead want to use this as a standalone safety system, don't select the box.",fg_bg=g_lg_fg_bg}
local networked = CheckBox{parent=plc_c_1,x=1,y=8,label="Networked",default=ini_cfg.Networked,box_fg_bg=cpair(colors.orange,colors.black)}
local function submit_networked()
self.set_networked(networked.get_value())
plc_pane.set_value(2)
end
PushButton{parent=plc_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=plc_c_1,x=44,y=14,text="Next \x1a",callback=submit_networked,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=plc_c_2,x=1,y=1,text="Please enter the reactor unit ID for this PLC."}
TextBox{parent=plc_c_2,x=1,y=3,height=3,text="If this is a networked PLC, currently only IDs 1 through 4 are acceptable.",fg_bg=g_lg_fg_bg}
TextBox{parent=plc_c_2,x=1,y=6,text="Unit #"}
local u_id = NumberField{parent=plc_c_2,x=7,y=6,width=5,max_chars=3,default=ini_cfg.UnitID,min=1,fg_bg=bw_fg_bg}
local u_id_err = TextBox{parent=plc_c_2,x=8,y=14,width=35,text="Please set a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
function self.set_networked(enable)
tmp_cfg.Networked = enable
if enable then u_id.set_max(4) else u_id.set_max(999) end
end
local function submit_id()
local unit_id = tonumber(u_id.get_value())
if unit_id ~= nil then
u_id_err.hide(true)
tmp_cfg.UnitID = unit_id
plc_pane.set_value(3)
else u_id_err.show() end
end
PushButton{parent=plc_c_2,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=plc_c_2,x=44,y=14,text="Next \x1a",callback=submit_id,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=plc_c_3,x=1,y=1,height=4,text="When networked, the supervisor takes care of emergency coolant via RTUs. However, you can configure independent emergency coolant via the PLC."}
TextBox{parent=plc_c_3,x=1,y=6,height=5,text="This independent control can be used with or without a supervisor. To configure, you would next select the interface of the redstone output connected to one or more mekanism pipes.",fg_bg=g_lg_fg_bg}
local en_em_cool = CheckBox{parent=plc_c_3,x=1,y=11,label="Enable PLC Emergency Coolant Control",default=ini_cfg.EmerCoolEnable,box_fg_bg=cpair(colors.orange,colors.black)}
local function next_from_plc()
if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(4) end
end
local function submit_en_emcool()
tmp_cfg.EmerCoolEnable = en_em_cool.get_value()
if tmp_cfg.EmerCoolEnable then plc_pane.set_value(4) else next_from_plc() end
end
PushButton{parent=plc_c_3,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=plc_c_3,x=44,y=14,text="Next \x1a",callback=submit_en_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=plc_c_4,x=1,y=1,text="Emergency Coolant Redstone Output Side"}
local side = Radio2D{parent=plc_c_4,x=1,y=2,rows=2,columns=3,default=side_to_idx(ini_cfg.EmerCoolSide),options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.orange}
TextBox{parent=plc_c_4,x=1,y=5,text="Bundled Redstone Configuration"}
local bundled = CheckBox{parent=plc_c_4,x=1,y=6,label="Is Bundled?",default=ini_cfg.EmerCoolColor~=nil,box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)self.bundled_emcool(v)end}
local color = Radio2D{parent=plc_c_4,x=1,y=8,rows=4,columns=4,default=color_to_idx(ini_cfg.EmerCoolColor),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}
if ini_cfg.EmerCoolColor == nil then color.disable() end
function self.bundled_emcool(en) if en then color.enable() else color.disable() end end
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)
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=44,y=14,text="Next \x1a",callback=submit_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Network
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=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_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}}
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=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 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 submit_channels()
local svr_c = tonumber(svr_chan.get_value())
local plc_c = tonumber(plc_chan.get_value())
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)
chan_err.hide(true)
elseif svr_c == nil then
chan_err.set_value("Please set the supervisor channel.")
chan_err.show()
else
chan_err.set_value("Please set the PLC channel.")
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}
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_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}
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 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()
else
p2_err.set_value("Please set the trusted range.")
p2_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_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 compution (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, _, censor = 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}
local function censor_key(enable) censor(util.trinary(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}
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 function submit_auth()
local v = key.get_value()
if string.len(v) == 0 or string.len(v) >= 8 then
tmp_cfg.AuthKey = key.get_value()
main_pane.set_value(4)
key_err.hide(true)
else key_err.show() end
end
PushButton{parent=net_c_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}
--#endregion
--#region Logging
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49}
TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg}
local path_err = TextBox{parent=log_c_1,x=8,y=14,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_log()
if path.get_value() ~= "" then
path_err.hide(true)
tmp_cfg.LogMode = mode.get_value() - 1
tmp_cfg.LogPath = path.get_value()
tmp_cfg.LogDebug = en_dbg.get_value()
tool_ctl.color_apply.hide(true)
tool_ctl.color_next.show()
main_pane.set_value(5)
else path_err.show() end
end
local function back_from_log()
if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(2) end
end
PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=back_from_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Color Options
local clr_c_1 = Div{parent=clr_cfg,x=2,y=4,width=49}
local clr_c_2 = Div{parent=clr_cfg,x=2,y=4,width=49}
local clr_c_3 = Div{parent=clr_cfg,x=2,y=4,width=49}
local clr_c_4 = Div{parent=clr_cfg,x=2,y=4,width=49}
local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}}
TextBox{parent=clr_cfg,x=1,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)}
TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color theme for the front panel."}
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}
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."}
TextBox{parent=clr_c_2,x=21,y=7,text="Preview"}
local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)}
_ = IndLight{parent=clr_c_2,x=21,y=9,label="Warning",colors=cpair(colors.black,colors.yellow)}
_ = IndLight{parent=clr_c_2,x=21,y=10,label="Bad",colors=cpair(colors.black,colors.red)}
local b_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.black,colors.black),hidden=true}
local g_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.gray,colors.gray),hidden=true}
local function recolor(value)
local c = themes.smooth_stone.color_modes[value]
if value == themes.COLOR_MODE.STANDARD or value == themes.COLOR_MODE.BLUE_IND then
b_off.hide()
g_off.show()
else
g_off.hide()
b_off.show()
end
if #c == 0 then
for i = 1, #style.colors do term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end
else
term.setPaletteColor(colors.green, c[1].hex)
term.setPaletteColor(colors.yellow, c[2].hex)
term.setPaletteColor(colors.red, c[3].hex)
end
end
TextBox{parent=clr_c_2,x=1,y=7,width=10,text="Color Mode"}
local c_mode = RadioButton{parent=clr_c_2,x=1,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_2,x=21,y=13,height=2,width=18,text="Note: exact color varies by theme.",fg_bg=g_lg_fg_bg}
PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local function back_from_colors()
main_pane.set_value(util.trinary(tool_ctl.jumped_to_color, 1, 4))
tool_ctl.jumped_to_color = false
recolor(1)
end
local function show_access()
clr_pane.set_value(2)
recolor(c_mode.get_value())
end
local function submit_colors()
tmp_cfg.FrontPanelTheme = fp_theme.get_value()
tmp_cfg.ColorMode = c_mode.get_value()
if tool_ctl.jumped_to_color then
settings.set("FrontPanelTheme", tmp_cfg.FrontPanelTheme)
settings.set("ColorMode", tmp_cfg.ColorMode)
if settings.save("/reactor-plc.settings") then
load_settings(settings_cfg, true)
load_settings(ini_cfg)
clr_pane.set_value(3)
else
clr_pane.set_value(4)
end
else
tool_ctl.gen_summary(tmp_cfg)
tool_ctl.viewing_config = false
self.importing_legacy = false
tool_ctl.settings_apply.show()
main_pane.set_value(6)
end
end
PushButton{parent=clr_c_1,x=1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=clr_c_1,x=8,y=14,min_width=15,text="Accessibility",callback=show_access,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
tool_ctl.color_next = PushButton{parent=clr_c_1,x=44,y=14,text="Next \x1a",callback=submit_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
tool_ctl.color_apply = PushButton{parent=clr_c_1,x=43,y=14,min_width=7,text="Apply",callback=submit_colors,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
tool_ctl.color_apply.hide(true)
local function c_go_home()
main_pane.set_value(1)
clr_pane.set_value(1)
end
TextBox{parent=clr_c_3,x=1,y=1,text="Settings saved!"}
PushButton{parent=clr_c_3,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
PushButton{parent=clr_c_3,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=clr_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
PushButton{parent=clr_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
PushButton{parent=clr_c_4,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Summary and Saving
local sum_c_1 = Div{parent=summary,x=2,y=4,width=49}
local sum_c_2 = Div{parent=summary,x=2,y=4,width=49}
local sum_c_3 = Div{parent=summary,x=2,y=4,width=49}
local sum_c_4 = Div{parent=summary,x=2,y=4,width=49}
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function back_from_settings()
if tool_ctl.viewing_config or self.importing_legacy then
main_pane.set_value(1)
tool_ctl.viewing_config = false
self.importing_legacy = false
tool_ctl.settings_apply.show()
else
main_pane.set_value(5)
end
end
---@param element graphics_element
---@param data any
local function try_set(element, data)
if data ~= nil then element.set_value(data) end
end
local function save_and_continue()
for _, field in ipairs(fields) do
local k, v = field[1], tmp_cfg[field[1]]
if v == nil then settings.unset(k) else settings.set(k, v) end
end
if settings.save("/reactor-plc.settings") then
load_settings(settings_cfg, true)
load_settings(ini_cfg)
try_set(networked, ini_cfg.Networked)
try_set(u_id, ini_cfg.UnitID)
try_set(en_em_cool, ini_cfg.EmerCoolEnable)
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(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(key, ini_cfg.AuthKey)
try_set(mode, ini_cfg.LogMode)
try_set(path, ini_cfg.LogPath)
try_set(en_dbg, ini_cfg.LogDebug)
try_set(fp_theme, ini_cfg.FrontPanelTheme)
try_set(c_mode, ini_cfg.ColorMode)
tool_ctl.view_cfg.enable()
tool_ctl.color_cfg.enable()
if self.importing_legacy then
self.importing_legacy = false
sum_pane.set_value(3)
else
sum_pane.set_value(2)
end
else
sum_pane.set_value(4)
end
end
PushButton{parent=sum_c_1,x=1,y=14,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"}
TextBox{parent=sum_c_2,x=1,y=3,text="Tip: you can run a Self-Check from the configurator home screen to make sure everything is going to work right!"}
local function go_home()
main_pane.set_value(1)
plc_pane.set_value(1)
net_pane.set_value(1)
clr_pane.set_value(1)
sum_pane.set_value(1)
end
PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=sum_c_3,x=1,y=1,height=2,text="The old config.lua file will now be deleted, then the configurator will exit."}
local function delete_legacy()
fs.delete("/reactor-plc/config.lua")
exit()
end
PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=sum_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
--#endregion
--#region Tool Functions
-- load a legacy config file
function tool_ctl.load_legacy()
local config = require("reactor-plc.config")
tmp_cfg.Networked = config.NETWORKED
tmp_cfg.UnitID = config.REACTOR_ID
tmp_cfg.EmerCoolEnable = type(config.EMERGENCY_COOL) == "table"
if tmp_cfg.EmerCoolEnable then
tmp_cfg.EmerCoolSide = config.EMERGENCY_COOL.side
tmp_cfg.EmerCoolColor = config.EMERGENCY_COOL.color
else
tmp_cfg.EmerCoolSide = nil
tmp_cfg.EmerCoolColor = nil
end
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
tmp_cfg.PLC_Channel = config.PLC_CHANNEL
tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT
tmp_cfg.TrustedRange = config.TRUSTED_RANGE
tmp_cfg.AuthKey = config.AUTH_KEY or ""
tmp_cfg.LogMode = config.LOG_MODE
tmp_cfg.LogPath = config.LOG_PATH
tmp_cfg.LogDebug = config.LOG_DEBUG or false
tool_ctl.gen_summary(tmp_cfg)
sum_pane.set_value(1)
main_pane.set_value(6)
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 plc_config
function tool_ctl.gen_summary(cfg)
setting_list.remove_all()
local alternate = false
local inner_width = setting_list.get_width() - 1
if cfg.AuthKey then self.show_key_btn.enable() else self.show_key_btn.disable() end
self.auth_key_value = cfg.AuthKey or "" -- to show auth key
for i = 1, #fields do
local f = fields[i]
local height = 1
local label_w = string.len(f[2])
local val_max_w = (inner_width - label_w) + 1
local raw = cfg[f[1]]
local val = util.strval(raw)
if f[1] == "AuthKey" 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] == "EmerCoolColor" and raw ~= nil then val = rsio.color_name(raw)
elseif f[1] == "FrontPanelTheme" then
val = util.strval(themes.fp_theme_name(raw))
elseif f[1] == "ColorMode" then
val = util.strval(themes.color_mode_name(raw))
end
if val == "nil" then val = "<not set>" end
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if string.len(val) > val_max_w then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end
local line = Div{parent=setting_list,height=height,fg_bg=c}
TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)}
local textbox
if height > 1 then
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end
if f[1] == "AuthKey" then self.auth_key_textbox = textbox end
end
end
--#endregion
end
return system

View File

@@ -2,39 +2,37 @@
-- Configuration GUI
--
local log = require("scada-common.log")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local rsio = require("scada-common.rsio")
local log = require("scada-common.log")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core")
local check = require("reactor-plc.config.check")
local system = require("reactor-plc.config.system")
local DisplayBox = require("graphics.elements.displaybox")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local core = require("graphics.core")
local themes = require("graphics.themes")
local CheckBox = require("graphics.elements.controls.checkbox")
local PushButton = require("graphics.elements.controls.push_button")
local Radio2D = require("graphics.elements.controls.radio_2d")
local RadioButton = require("graphics.elements.controls.radio_button")
local DisplayBox = require("graphics.elements.displaybox")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local NumberField = require("graphics.elements.form.number_field")
local TextField = require("graphics.elements.form.text_field")
local PushButton = require("graphics.elements.controls.push_button")
local println = util.println
local tri = util.trinary
local cpair = core.cpair
local LEFT = core.ALIGN.LEFT
local CENTER = core.ALIGN.CENTER
local RIGHT = core.ALIGN.RIGHT
-- changes to the config data/format to let the user know
local changes = {
{"v1.6.2", { "AuthKey minimum length is now 8 (if set)" } }
{ "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" } }
}
---@class plc_configurator
@@ -42,49 +40,32 @@ local configurator = {}
local style = {}
style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray)
style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray)
style.colors = {
{ c = colors.red, hex = 0xdf4949 },
{ c = colors.orange, hex = 0xffb659 },
{ c = colors.yellow, hex = 0xfffc79 },
{ c = colors.lime, hex = 0x80ff80 },
{ c = colors.green, hex = 0x4aee8a },
{ c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff },
{ c = colors.purple, hex = 0xb156ee },
{ c = colors.pink, hex = 0xf26ba2 },
{ c = colors.magenta, hex = 0xf9488a },
{ c = colors.lightGray, hex = 0xcacaca },
{ c = colors.gray, hex = 0x575757 }
}
style.colors = themes.smooth_stone.colors
local bw_fg_bg = cpair(colors.black, colors.white)
local g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
local nav_fg_bg = bw_fg_bg
local btn_act_fg_bg = cpair(colors.white, colors.gray)
style.bw_fg_bg = cpair(colors.black, colors.white)
style.g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
style.nav_fg_bg = style.bw_fg_bg
style.btn_act_fg_bg = cpair(colors.white, colors.gray)
style.btn_dis_fg_bg = cpair(colors.lightGray, colors.white)
---@class _plc_cfg_tool_ctl
local tool_ctl = {
ask_config = false,
has_config = false,
viewing_config = false,
importing_legacy = false,
jumped_to_color = false,
view_cfg = nil, ---@type graphics_element
color_cfg = nil, ---@type graphics_element
color_next = nil, ---@type graphics_element
color_apply = nil, ---@type graphics_element
settings_apply = nil, ---@type graphics_element
set_networked = nil, ---@type function
bundled_emcool = nil, ---@type function
gen_summary = nil, ---@type function
show_current_cfg = nil, ---@type function
load_legacy = nil, ---@type function
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type graphics_element
auth_key_textbox = nil, ---@type graphics_element
auth_key_value = ""
}
---@class plc_config
@@ -92,16 +73,18 @@ local tmp_cfg = {
Networked = false,
UnitID = 0,
EmerCoolEnable = false,
EmerCoolSide = nil,
EmerCoolColor = nil,
SVR_Channel = nil,
PLC_Channel = nil,
ConnTimeout = nil,
TrustedRange = nil,
AuthKey = nil,
EmerCoolSide = nil, ---@type string|nil
EmerCoolColor = nil, ---@type color|nil
SVR_Channel = nil, ---@type integer
PLC_Channel = nil, ---@type integer
ConnTimeout = nil, ---@type number
TrustedRange = nil, ---@type number
AuthKey = nil, ---@type string|nil
LogMode = 0,
LogPath = "",
LogDebug = false,
FrontPanelTheme = 1,
ColorMode = 1
}
---@class plc_config
@@ -123,30 +106,11 @@ local fields = {
{ "AuthKey", "Facility Auth Key" , ""},
{ "LogMode", "Log Mode", log.MODE.APPEND },
{ "LogPath", "Log Path", "/log.txt" },
{ "LogDebug","Log Debug Messages", false }
{ "LogDebug", "Log Debug Messages", false },
{ "FrontPanelTheme", "Front Panel Theme", themes.FP_THEME.SANDSTONE },
{ "ColorMode", "Color Mode", themes.COLOR_MODE.STANDARD }
}
local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" }
local side_options_map = { "top", "bottom", "left", "right", "front", "back" }
local color_options = { "Red", "Orange", "Yellow", "Lime", "Green", "Cyan", "Light Blue", "Blue", "Purple", "Magenta", "Pink", "White", "Light Gray", "Gray", "Black", "Brown" }
local color_options_map = { colors.red, colors.orange, colors.yellow, colors.lime, colors.green, colors.cyan, colors.lightBlue, colors.blue, colors.purple, colors.magenta, colors.pink, colors.white, colors.lightGray, colors.gray, colors.black, colors.brown }
-- convert text representation to index
---@param side string
local function side_to_idx(side)
for k, v in ipairs(side_options_map) do
if v == side then return k end
end
end
-- convert color to index
---@param color color
local function color_to_idx(color)
for k, v in ipairs(color_options_map) do
if v == color then return k end
end
end
-- load data from the settings file
---@param target plc_config
---@param raw boolean? true to not use default values
@@ -163,11 +127,16 @@ end
-- create the config view
---@param display graphics_element
local function config_view(display)
local bw_fg_bg = style.bw_fg_bg
local g_lg_fg_bg = style.g_lg_fg_bg
local nav_fg_bg = style.nav_fg_bg
local btn_act_fg_bg = style.btn_act_fg_bg
local btn_dis_fg_bg = style.btn_dis_fg_bg
---@diagnostic disable-next-line: undefined-field
local function exit() os.queueEvent("terminate") end
TextBox{parent=display,y=1,text="Reactor PLC Configurator",alignment=CENTER,height=1,fg_bg=style.header}
TextBox{parent=display,y=1,text="Reactor PLC Configurator",alignment=CENTER,fg_bg=style.header}
local root_pane_div = Div{parent=display,x=1,y=2}
@@ -175,19 +144,21 @@ local function config_view(display)
local plc_cfg = Div{parent=root_pane_div,x=1,y=1}
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
local clr_cfg = Div{parent=root_pane_div,x=1,y=1}
local summary = Div{parent=root_pane_div,x=1,y=1}
local changelog = Div{parent=root_pane_div,x=1,y=1}
local 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,plc_cfg,net_cfg,log_cfg,summary,changelog}}
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,plc_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog,check_sys}}
-- MAIN PAGE
--#region Main Page
local y_start = 5
TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the Reactor PLC configurator! Please select one of the following options."}
if tool_ctl.ask_config then
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device has no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device had no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
y_start = y_start + 5
end
@@ -195,7 +166,7 @@ local function config_view(display)
tool_ctl.viewing_config = true
tool_ctl.gen_summary(settings_cfg)
tool_ctl.settings_apply.hide(true)
main_pane.set_value(5)
main_pane.set_value(6)
end
if fs.exists("/reactor-plc/config.lua") then
@@ -204,436 +175,62 @@ local function config_view(display)
end
PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure System",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end
local function jump_color()
tool_ctl.jumped_to_color = true
tool_ctl.color_next.hide(true)
tool_ctl.color_apply.show()
main_pane.set_value(5)
end
PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=main_page,x=10,y=17,min_width=12,text="Self-Check",callback=function()main_pane.set_value(8)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
-- PLC CONFIG
local plc_c_1 = Div{parent=plc_cfg,x=2,y=4,width=49}
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_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4}}
TextBox{parent=plc_cfg,x=1,y=2,height=1,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)}
TextBox{parent=plc_c_1,x=1,y=1,height=1,text="Would you like to set this PLC as networked?"}
TextBox{parent=plc_c_1,x=1,y=3,height=4,text="If you have a supervisor, select the box. You will later be prompted to select the network configuration. If you instead want to use this as a standalone safety system, don't select the box.",fg_bg=g_lg_fg_bg}
local networked = CheckBox{parent=plc_c_1,x=1,y=8,label="Networked",default=ini_cfg.Networked,box_fg_bg=cpair(colors.orange,colors.black)}
local function submit_networked()
tool_ctl.set_networked(networked.get_value())
plc_pane.set_value(2)
if not tool_ctl.has_config then
tool_ctl.view_cfg.disable()
tool_ctl.color_cfg.disable()
end
PushButton{parent=plc_c_1,x=1,y=14,min_width=6,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=plc_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_networked,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
TextBox{parent=plc_c_2,x=1,y=1,height=1,text="Please enter the reactor unit ID for this PLC."}
TextBox{parent=plc_c_2,x=1,y=3,height=3,text="If this is a networked PLC, currently only IDs 1 through 4 are acceptable.",fg_bg=g_lg_fg_bg}
--#region System Configuration
TextBox{parent=plc_c_2,x=1,y=6,height=1,text="Unit #"}
local u_id = NumberField{parent=plc_c_2,x=7,y=6,width=5,max_digits=3,default=ini_cfg.UnitID,min=1,fg_bg=bw_fg_bg}
local settings = { settings_cfg, ini_cfg, tmp_cfg, fields, load_settings }
local divs = { plc_cfg, net_cfg, log_cfg, clr_cfg, summary }
local u_id_err = TextBox{parent=plc_c_2,x=8,y=14,height=1,width=35,text="Please set a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
system.create(tool_ctl, main_pane, settings, divs, style, exit)
local function submit_id()
local unit_id = tonumber(u_id.get_value())
if unit_id ~= nil then
u_id_err.hide(true)
tmp_cfg.UnitID = unit_id
plc_pane.set_value(3)
else u_id_err.show() end
end
--#endregion
PushButton{parent=plc_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=plc_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_id,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=plc_c_3,x=1,y=1,height=4,text="When networked, the supervisor takes care of emergency coolant via RTUs. However, you can configure independent emergency coolant via the PLC. "}
TextBox{parent=plc_c_3,x=1,y=6,height=5,text="This independent control can be used with or without a supervisor. To configure, you would next select the interface of the redstone output connected to one or more mekanism pipes.",fg_bg=g_lg_fg_bg}
local en_em_cool = CheckBox{parent=plc_c_3,x=1,y=11,label="Enable PLC Emergency Coolant Control",default=ini_cfg.EmerCoolEnable,box_fg_bg=cpair(colors.orange,colors.black)}
local function next_from_plc()
if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(4) end
end
local function submit_en_emcool()
tmp_cfg.EmerCoolEnable = en_em_cool.get_value()
if tmp_cfg.EmerCoolEnable then plc_pane.set_value(4) else next_from_plc() end
end
PushButton{parent=plc_c_3,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=plc_c_3,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_en_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=plc_c_4,x=1,y=1,height=1,text="Emergency Coolant Redstone Output Side"}
local side = Radio2D{parent=plc_c_4,x=1,y=2,rows=2,columns=3,default=side_to_idx(ini_cfg.EmerCoolSide),options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.orange}
TextBox{parent=plc_c_4,x=1,y=5,height=1,text="Bundled Redstone Configuration"}
local bundled = CheckBox{parent=plc_c_4,x=1,y=6,label="Is Bundled?",default=ini_cfg.EmerCoolColor~=nil,box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)tool_ctl.bundled_emcool(v)end}
local color = Radio2D{parent=plc_c_4,x=1,y=8,rows=4,columns=4,default=color_to_idx(ini_cfg.EmerCoolColor),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}
if ini_cfg.EmerCoolColor == nil then color.disable() end
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)
next_from_plc()
end
PushButton{parent=plc_c_4,x=1,y=14,min_width=6,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=44,y=14,min_width=6,text="Next \x1a",callback=submit_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
-- NET CONFIG
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_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}}
TextBox{parent=net_cfg,x=1,y=2,height=1,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=net_c_1,x=1,y=1,height=1,text="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=8,height=1,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,height=1,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 chan_err = TextBox{parent=net_c_1,x=8,y=14,height=1,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_channels()
local svr_c = tonumber(svr_chan.get_value())
local plc_c = tonumber(plc_chan.get_value())
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)
chan_err.hide(true)
elseif svr_c == nil then
chan_err.set_value("Please set the supervisor channel.")
chan_err.show()
else
chan_err.set_value("Please set the PLC channel.")
chan_err.show()
end
end
PushButton{parent=net_c_1,x=1,y=14,min_width=6,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,min_width=6,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,height=1,text="Connection Timeout"}
local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,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_2,x=1,y=8,height=1,text="Trusted Range"}
local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_digits=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}
local p2_err = TextBox{parent=net_c_2,x=8,y=14,height=1,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()
else
p2_err.set_value("Please set the trusted range.")
p2_err.show()
end
end
PushButton{parent=net_c_2,x=1,y=14,min_width=6,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,min_width=6,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 compution (can slow things down).",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=11,height=1,text="Facility Auth Key"}
local key, _, censor = 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}
local function censor_key(enable) censor(util.trinary(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}
hide_key.set_value(true)
censor_key(true)
local key_err = TextBox{parent=net_c_3,x=8,y=14,height=1,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_auth()
local v = key.get_value()
if string.len(v) == 0 or string.len(v) >= 8 then
tmp_cfg.AuthKey = key.get_value()
main_pane.set_value(4)
key_err.hide(true)
else key_err.show() end
end
PushButton{parent=net_c_3,x=1,y=14,min_width=6,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,min_width=6,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
-- LOG CONFIG
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49}
TextBox{parent=log_cfg,x=1,y=2,height=1,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
TextBox{parent=log_c_1,x=1,y=1,height=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,height=1,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
TextBox{parent=log_c_1,x=1,y=7,height=1,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg}
local path_err = TextBox{parent=log_c_1,x=8,y=14,height=1,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_log()
if path.get_value() ~= "" then
path_err.hide(true)
tmp_cfg.LogMode = mode.get_value() - 1
tmp_cfg.LogPath = path.get_value()
tmp_cfg.LogDebug = en_dbg.get_value()
tool_ctl.gen_summary(tmp_cfg)
tool_ctl.viewing_config = false
tool_ctl.importing_legacy = false
tool_ctl.settings_apply.show()
main_pane.set_value(5)
else path_err.show() end
end
local function back_from_log()
if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(2) end
end
PushButton{parent=log_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=back_from_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=log_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
-- SUMMARY OF CHANGES
local sum_c_1 = Div{parent=summary,x=2,y=4,width=49}
local sum_c_2 = Div{parent=summary,x=2,y=4,width=49}
local sum_c_3 = Div{parent=summary,x=2,y=4,width=49}
local sum_c_4 = Div{parent=summary,x=2,y=4,width=49}
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
TextBox{parent=summary,x=1,y=2,height=1,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=51,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function back_from_settings()
if tool_ctl.viewing_config or tool_ctl.importing_legacy then
main_pane.set_value(1)
tool_ctl.viewing_config = false
tool_ctl.importing_legacy = false
tool_ctl.settings_apply.show()
else
main_pane.set_value(4)
end
end
---@param element graphics_element
---@param data any
local function try_set(element, data)
if data ~= nil then element.set_value(data) end
end
local function save_and_continue()
for k, v in pairs(tmp_cfg) do settings.set(k, v) end
if settings.save("reactor-plc.settings") then
load_settings(settings_cfg, true)
load_settings(ini_cfg)
try_set(networked, ini_cfg.Networked)
try_set(u_id, ini_cfg.UnitID)
try_set(en_em_cool, ini_cfg.EmerCoolEnable)
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(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(key, ini_cfg.AuthKey)
try_set(mode, ini_cfg.LogMode)
try_set(path, ini_cfg.LogPath)
try_set(en_dbg, ini_cfg.LogDebug)
tool_ctl.view_cfg.enable()
if tool_ctl.importing_legacy then
tool_ctl.importing_legacy = false
sum_pane.set_value(3)
else
sum_pane.set_value(2)
end
else
sum_pane.set_value(4)
end
end
PushButton{parent=sum_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
TextBox{parent=sum_c_2,x=1,y=1,height=1,text="Settings saved!"}
local function go_home()
main_pane.set_value(1)
plc_pane.set_value(1)
net_pane.set_value(1)
sum_pane.set_value(1)
end
PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=sum_c_3,x=1,y=1,height=2,text="The old config.lua file will now be deleted, then the configurator will exit."}
local function delete_legacy()
fs.delete("/reactor-plc/config.lua")
exit()
end
PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=sum_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
-- CONFIG CHANGE LOG
--#region Config Change Log
local cl = Div{parent=changelog,x=2,y=4,width=49}
TextBox{parent=changelog,x=1,y=2,height=1,text=" Config Change Log",fg_bg=bw_fg_bg}
TextBox{parent=changelog,x=1,y=2,text=" Config Change Log",fg_bg=bw_fg_bg}
local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=51,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
for _, change in ipairs(changes) do
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg}
TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
for _, v in ipairs(change[2]) do
local e = Div{parent=c_log,height=#util.strwrap(v,46)}
TextBox{parent=e,y=1,x=1,text="- ",height=1,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=e,y=1,x=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
end
end
PushButton{parent=cl,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
-- set tool functions now that we have the elements
--#endregion
function tool_ctl.set_networked(enable)
tmp_cfg.Networked = enable
if enable then u_id.set_max(4) else u_id.set_max(999) end
end
--#region Self-Check
function tool_ctl.bundled_emcool(en) if en then color.enable() else color.disable() end end
check.create(main_pane, settings_cfg, check_sys, style)
-- load a legacy config file
function tool_ctl.load_legacy()
local config = require("reactor-plc.config")
tmp_cfg.Networked = config.NETWORKED
tmp_cfg.UnitID = config.REACTOR_ID
tmp_cfg.EmerCoolEnable = type(config.EMERGENCY_COOL) == "table"
if tmp_cfg.EmerCoolEnable then
tmp_cfg.EmerCoolSide = config.EMERGENCY_COOL.side
tmp_cfg.EmerCoolColor = config.EMERGENCY_COOL.color
else
tmp_cfg.EmerCoolSide = nil
tmp_cfg.EmerCoolColor = nil
end
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
tmp_cfg.PLC_Channel = config.PLC_CHANNEL
tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT
tmp_cfg.TrustedRange = config.TRUSTED_RANGE
tmp_cfg.AuthKey = config.AUTH_KEY or ""
tmp_cfg.LogMode = config.LOG_MODE
tmp_cfg.LogPath = config.LOG_PATH
tmp_cfg.LogDebug = config.LOG_DEBUG or false
tool_ctl.gen_summary(tmp_cfg)
sum_pane.set_value(1)
main_pane.set_value(5)
tool_ctl.importing_legacy = true
end
-- expose the auth key on the summary page
function tool_ctl.show_auth_key()
tool_ctl.show_key_btn.disable()
tool_ctl.auth_key_textbox.set_value(tool_ctl.auth_key_value)
end
-- generate the summary list
---@param cfg plc_config
function tool_ctl.gen_summary(cfg)
setting_list.remove_all()
local alternate = false
local inner_width = setting_list.get_width() - 1
tool_ctl.show_key_btn.enable()
tool_ctl.auth_key_value = cfg.AuthKey or "" -- to show auth key
for i = 1, #fields do
local f = fields[i]
local height = 1
local label_w = string.len(f[2])
local val_max_w = (inner_width - label_w) + 1
local raw = cfg[f[1]]
local val = util.strval(raw)
if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) end
if f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end
if f[1] == "EmerCoolColor" and raw ~= nil then val = rsio.color_name(raw) end
if val == "nil" then val = "<not set>" end
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if string.len(val) > val_max_w then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end
local line = Div{parent=setting_list,height=height,fg_bg=c}
TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)}
local textbox
if height > 1 then
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end
if f[1] == "AuthKey" then tool_ctl.auth_key_textbox = textbox end
end
end
--#endregion
end
-- reset terminal screen
@@ -645,7 +242,7 @@ local function reset_term()
end
-- run the reactor PLC configurator
---@param ask_config? boolean indicate if this is being called by the PLC startup app due to an invalid configuration
---@param ask_config? boolean indicate if this is being called by the startup app due to an invalid configuration
function configurator.configure(ask_config)
tool_ctl.ask_config = ask_config == true
@@ -664,7 +261,7 @@ function configurator.configure(ask_config)
config_view(display)
while true do
local event, param1, param2, param3 = util.pull_event()
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event
if event == "timer" then
@@ -677,6 +274,8 @@ 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)
end
if event == "terminate" then return end

View File

@@ -70,9 +70,9 @@ function databus.tx_link_state(state)
end
-- transmit reactor enable state across the bus
---@param active boolean reactor active
---@param active any reactor active
function databus.tx_reactor_state(active)
databus.ps.publish("reactor_active", active)
databus.ps.publish("reactor_active", active == true)
end
-- transmit RPS data across the bus

View File

@@ -23,6 +23,8 @@ local LED = require("graphics.elements.indicators.led")
local LEDPair = require("graphics.elements.indicators.ledpair")
local RGBLED = require("graphics.elements.indicators.ledrgb")
local LINK_STATE = types.PANEL_LINK_STATE
local ALIGN = core.ALIGN
local cpair = core.cpair
@@ -34,8 +36,12 @@ local ind_red = style.ind_red
-- create new front panel view
---@param panel graphics_element main displaybox
local function init(panel)
local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
header.register(databus.ps, "unit_id", function (id) header.set_value(util.c("REACTOR PLC - UNIT ", id)) end)
local s_hi_box = style.theme.highlight_box
local disabled_fg = style.fp.disabled_fg
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)
--
-- system indicators
@@ -52,13 +58,47 @@ local function init(panel)
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}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,style.ind_bkg}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(databus.ps, "link_state", network.update)
else
local nt_lnk = LEDPair{parent=system,label="NT LINKED",off=style.ind_bkg,c1=colors.red,c2=colors.green}
local nt_ver = LEDPair{parent=system,label="NT VERSION",off=style.ind_bkg,c1=colors.red,c2=colors.green}
local nt_col = LED{parent=system,label="NT COLLISION",colors=ind_red}
nt_lnk.register(databus.ps, "link_state", function (state)
local value = 2
if state == LINK_STATE.DISCONNECTED then
value = 1
elseif state == LINK_STATE.LINKED then
value = 3
end
nt_lnk.update(value)
end)
nt_ver.register(databus.ps, "link_state", function (state)
local value = 3
if state == LINK_STATE.BAD_VERSION then
value = 2
elseif state == LINK_STATE.DISCONNECTED then
value = 1
end
nt_ver.update(value)
end)
nt_col.register(databus.ps, "link_state", function (state) nt_col.update(state == LINK_STATE.COLLISION) end)
end
system.line_break()
reactor.register(databus.ps, "reactor_dev_state", reactor.update)
modem.register(databus.ps, "has_modem", modem.update)
network.register(databus.ps, "link_state", network.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}
@@ -75,7 +115,7 @@ local function init(panel)
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=5,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)}
TextBox{parent=system,x=9,y=5,width=6,text=comp_id,fg_bg=disabled_fg}
--
-- status & controls
@@ -91,12 +131,12 @@ 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,colors.lightGray,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)}
local status_trip = Div{parent=status_trip_rct,width=18,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
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 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,colors.white,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)}
local controls = Div{parent=controls_rct,width=15,height=1,fg_bg=cpair(colors.black,colors.white)}
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)}
@@ -107,9 +147,9 @@ local function init(panel)
-- about footer
--
local about = Rectangle{parent=panel,width=32,height=3,x=2,y=16,border=border(1,colors.ivory),thin=true,fg_bg=cpair(colors.black,colors.white)}
local fw_v = TextBox{parent=about,x=2,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=17,y=1,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1}
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)
@@ -118,7 +158,7 @@ local function init(panel)
-- rps list
--
local rps = Rectangle{parent=panel,width=16,height=16,x=36,y=3,border=border(1,colors.lightGray),thin=true,fg_bg=cpair(colors.black,colors.lightGray)}
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_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

@@ -2,46 +2,40 @@
-- Graphics Style Options
--
local core = require("graphics.core")
local core = require("graphics.core")
local themes = require("graphics.themes")
---@class plc_style
local style = {}
local cpair = core.cpair
-- GLOBAL --
-- remap global colors
colors.ivory = colors.pink
colors.yellow_hc = colors.purple
colors.red_off = colors.brown
colors.yellow_off = colors.magenta
colors.green_off = colors.lime
style.root = cpair(colors.black, colors.ivory)
style.header = cpair(colors.black, colors.lightGray)
style.colors = {
{ c = colors.red, hex = 0xdf4949 }, -- RED ON
{ c = colors.orange, hex = 0xffb659 },
{ c = colors.yellow, hex = 0xf9fb53 }, -- YELLOW ON
{ c = colors.lime, hex = 0x16665a }, -- GREEN OFF
{ c = colors.green, hex = 0x6be551 }, -- GREEN ON
{ c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff },
{ c = colors.purple, hex = 0xb156ee }, -- YELLOW HIGH CONTRAST
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
-- { c = colors.white, hex = 0xdcd9ca },
{ c = colors.lightGray, hex = 0xb1b8b3 },
{ c = colors.gray, hex = 0x575757 },
-- { c = colors.black, hex = 0x191919 },
{ c = colors.brown, hex = 0x672223 } -- RED OFF
}
-- COMMON COLOR PAIRS --
style.theme = themes.sandstone
style.fp = themes.get_fp_style(style.theme)
style.colorblind = false
style.ind_grn = cpair(colors.green, colors.green_off)
style.ind_red = cpair(colors.red, colors.red_off)
-- set theme per configuration
---@param fp FP_THEME front panel theme
---@param color_mode COLOR_MODE the color mode to use
function style.set_theme(fp, color_mode)
if fp == themes.FP_THEME.SANDSTONE then
style.theme = themes.sandstone
elseif fp == themes.FP_THEME.BASALT then
style.theme = themes.basalt
end
style.fp = themes.get_fp_style(style.theme)
style.colorblind = color_mode ~= themes.COLOR_MODE.STANDARD and color_mode ~= themes.COLOR_MODE.STD_ON_BLACK
if color_mode == themes.COLOR_MODE.STANDARD or color_mode == themes.COLOR_MODE.BLUE_IND then
style.ind_bkg = colors.gray
else
style.ind_bkg = colors.black
end
end
return style

View File

@@ -6,6 +6,8 @@ local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local themes = require("graphics.themes")
local databus = require("reactor-plc.databus")
local plc = {}
@@ -23,8 +25,8 @@ 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
local PCALL_SCRAM_MSG = "pcall: Scram requires the reactor to be active."
local PCALL_START_MSG = "pcall: Reactor is already active."
local PCALL_SCRAM_MSG = "Scram requires the reactor to be active."
local PCALL_START_MSG = "Reactor is already active."
---@type plc_config
local config = {}
@@ -37,47 +39,65 @@ function plc.load_config()
config.Networked = settings.get("Networked")
config.UnitID = settings.get("UnitID")
config.EmerCoolEnable = settings.get("EmerCoolEnable")
config.EmerCoolSide = settings.get("EmerCoolSide")
config.EmerCoolColor = settings.get("EmerCoolColor")
config.SVR_Channel = settings.get("SVR_Channel")
config.PLC_Channel = settings.get("PLC_Channel")
config.ConnTimeout = settings.get("ConnTimeout")
config.TrustedRange = settings.get("TrustedRange")
config.AuthKey = settings.get("AuthKey")
config.LogMode = settings.get("LogMode")
config.LogPath = settings.get("LogPath")
config.LogDebug = settings.get("LogDebug")
config.FrontPanelTheme = settings.get("FrontPanelTheme")
config.ColorMode = settings.get("ColorMode")
return plc.validate_config(config)
end
-- validate a PLC configuration
---@param cfg plc_config
function plc.validate_config(cfg)
local cfv = util.new_validator()
cfv.assert_type_bool(config.Networked)
cfv.assert_type_int(config.UnitID)
cfv.assert_type_bool(config.EmerCoolEnable)
cfv.assert_type_bool(cfg.Networked)
cfv.assert_type_int(cfg.UnitID)
cfv.assert_type_bool(cfg.EmerCoolEnable)
if config.Networked == true then
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.PLC_Channel)
cfv.assert_type_int(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)
if cfg.Networked == true then
cfv.assert_channel(cfg.SVR_Channel)
cfv.assert_channel(cfg.PLC_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)
cfv.assert_eq(len == 0 or len >= 8, true)
if type(cfg.AuthKey) == "string" then
local len = string.len(cfg.AuthKey)
cfv.assert(len == 0 or len >= 8)
end
end
cfv.assert_type_int(config.LogMode)
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(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)
-- check emergency coolant configuration if enabled
if config.EmerCoolEnable then
cfv.assert_eq(rsio.is_valid_side(config.EmerCoolSide), true)
cfv.assert_eq(config.EmerCoolColor == nil or rsio.is_color(config.EmerCoolColor), true)
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)
end
return cfv.valid()
@@ -125,6 +145,21 @@ function plc.rps_init(reactor, is_formed)
end
end
-- check if the result of a peripheral call was OK, handle the failure if not
---@nodiscard
---@param result any PPM function call result
---@return boolean succeeded if the result is OK, false if it was a PPM failure
local function _check_and_handle_ppm_call(result)
if result == ppm.ACCESS_FAULT then
_set_fault()
-- if undefined, then the reactor isn't formed
if reactor.__p_last_fault() == ppm.UNDEFINED_FIELD then self.formed = false end
else return true end
return false
end
-- set emergency coolant control (if configured)
---@param state boolean true to enable emergency coolant, false to disable
local function _set_emer_cool(state)
@@ -163,25 +198,20 @@ function plc.rps_init(reactor, is_formed)
-- check if the reactor is formed
local function _is_formed()
local formed = reactor.isFormed()
if formed == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
else
if _check_and_handle_ppm_call(formed) then
self.formed = formed
end
if not self.state[state_keys.sys_fail] then
self.state[state_keys.sys_fail] = not formed
end
-- always update, since some ppm failures constitute not being formed
if not self.state[state_keys.sys_fail] then
self.state[state_keys.sys_fail] = not self.formed
end
end
-- check if the reactor is force disabled
local function _is_force_disabled()
local disabled = reactor.isForceDisabled()
if disabled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
else
if _check_and_handle_ppm_call(disabled) then
self.force_disabled = disabled
if not self.state[state_keys.force_disabled] then
@@ -193,22 +223,16 @@ function plc.rps_init(reactor, is_formed)
-- check for high damage
local function _high_damage()
local damage_percent = reactor.getDamagePercent()
if damage_percent == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.high_dmg] then
if _check_and_handle_ppm_call(damage_percent) and not self.state[state_keys.high_dmg] then
self.state[state_keys.high_dmg] = damage_percent >= RPS_LIMITS.MAX_DAMAGE_PERCENT
end
end
-- check if the reactor is at a critically high temperature
local function _high_temp()
-- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200
-- mekanism: MAX_DAMAGE_TEMPERATURE = 1200K
local temp = reactor.getTemperature()
if temp == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.high_temp] then
if _check_and_handle_ppm_call(temp) and not self.state[state_keys.high_temp] then
self.state[state_keys.high_temp] = temp >= RPS_LIMITS.MAX_DAMAGE_TEMPERATURE
end
end
@@ -216,10 +240,7 @@ function plc.rps_init(reactor, is_formed)
-- check if there is very low coolant
local function _low_coolant()
local coolant_filled = reactor.getCoolantFilledPercentage()
if coolant_filled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.low_coolant] then
if _check_and_handle_ppm_call(coolant_filled) and not self.state[state_keys.low_coolant] then
self.state[state_keys.low_coolant] = coolant_filled < RPS_LIMITS.MIN_COOLANT_FILL
end
end
@@ -227,10 +248,7 @@ function plc.rps_init(reactor, is_formed)
-- check for excess waste (>80% filled)
local function _excess_waste()
local w_filled = reactor.getWasteFilledPercentage()
if w_filled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.ex_waste] then
if _check_and_handle_ppm_call(w_filled) and not self.state[state_keys.ex_waste] then
self.state[state_keys.ex_waste] = w_filled > RPS_LIMITS.MAX_WASTE_FILL
end
end
@@ -238,10 +256,7 @@ function plc.rps_init(reactor, is_formed)
-- check for heated coolant backup (>95% filled)
local function _excess_heated_coolant()
local hc_filled = reactor.getHeatedCoolantFilledPercentage()
if hc_filled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.ex_hcoolant] then
if _check_and_handle_ppm_call(hc_filled) and not self.state[state_keys.ex_hcoolant] then
self.state[state_keys.ex_hcoolant] = hc_filled > RPS_LIMITS.MAX_HEATED_COLLANT_FILL
end
end
@@ -249,10 +264,7 @@ function plc.rps_init(reactor, is_formed)
-- check if there is no fuel
local function _insufficient_fuel()
local fuel = reactor.getFuelFilledPercentage()
if fuel == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.no_fuel] then
if _check_and_handle_ppm_call(fuel) and not self.state[state_keys.no_fuel] then
self.state[state_keys.no_fuel] = fuel <= RPS_LIMITS.NO_FUEL_FILL
end
end
@@ -301,7 +313,7 @@ function plc.rps_init(reactor, is_formed)
log.info("RPS: reactor SCRAM")
reactor.scram()
if reactor.__p_is_faulted() and (reactor.__p_last_fault() ~= PCALL_SCRAM_MSG) then
if reactor.__p_is_faulted() and not string.find(reactor.__p_last_fault(), PCALL_SCRAM_MSG) then
log.error("RPS: failed reactor SCRAM")
return false
else
@@ -319,7 +331,7 @@ function plc.rps_init(reactor, is_formed)
log.info("RPS: reactor start")
reactor.activate()
if reactor.__p_is_faulted() and (reactor.__p_last_fault() ~= PCALL_START_MSG) then
if reactor.__p_is_faulted() and not string.find(reactor.__p_last_fault(), PCALL_START_MSG) then
log.error("RPS: failed reactor start")
else
self.reactor_enabled = true
@@ -473,13 +485,22 @@ function plc.rps_init(reactor, is_formed)
self.tripped = false
self.trip_cause = RPS_TRIP_CAUSE.OK
for i = 1, #self.state do
self.state[i] = false
end
for i = 1, #self.state do self.state[i] = false end
if not quiet then log.info("RPS: reset") end
end
-- partial RPS reset that only clears fault and sys_fail
function public.reset_formed()
self.tripped = false
self.trip_cause = RPS_TRIP_CAUSE.OK
self.state[state_keys.fault] = false
self.state[state_keys.sys_fail] = false
log.info("RPS: partial reset on formed")
end
-- reset the automatic and timeout trip flags, then clear trip if that was the trip cause
function public.auto_reset()
self.state[state_keys.automatic] = false
@@ -509,8 +530,8 @@ end
function plc.comms(version, nic, reactor, rps, conn_watchdog)
local self = {
sv_addr = comms.BROADCAST,
seq_num = 0,
r_seq_num = nil,
seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, ---@type nil|integer
scrammed = false,
linked = false,
last_est_ack = ESTABLISH_ACK.ALLOW,
@@ -556,33 +577,17 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
self.seq_num = self.seq_num + 1
end
-- variable reactor status information, excluding heating rate
-- dynamic reactor status information, excluding heating rate
---@return table data_table, boolean faulted
local function _reactor_status()
local function _get_reactor_status()
local fuel = nil
local waste = nil
local coolant = nil
local hcoolant = nil
local data_table = {
false, -- getStatus
0, -- getBurnRate
0, -- getActualBurnRate
0, -- getTemperature
0, -- getDamagePercent
0, -- getBoilEfficiency
0, -- getEnvironmentalLoss
0, -- fuel_amnt
0, -- getFuelFilledPercentage
0, -- waste_amnt
0, -- getWasteFilledPercentage
"", -- coolant_name
0, -- coolant_amnt
0, -- getCoolantFilledPercentage
"", -- hcoolant_name
0, -- hcoolant_amnt
0 -- getHeatedCoolantFilledPercentage
}
local data_table = {}
reactor.__p_disable_afc()
local tasks = {
function () data_table[1] = reactor.getStatus() end,
@@ -622,30 +627,32 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
data_table[16] = hcoolant.amount
end
reactor.__p_enable_afc()
return data_table, reactor.__p_is_faulted()
end
-- update the status cache if changed
---@return boolean changed
local function _update_status_cache()
local status, faulted = _reactor_status()
local status, faulted = _get_reactor_status()
local changed = false
if self.status_cache ~= nil then
if not faulted then
if not faulted then
if self.status_cache ~= nil then
for i = 1, #status do
if status[i] ~= self.status_cache[i] then
changed = true
break
end
end
else
changed = true
end
else
changed = true
end
if changed and not faulted then
self.status_cache = status
if changed then
self.status_cache = status
end
end
return changed
@@ -664,9 +671,11 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
_send(msg_type, { status })
end
-- send structure properties (these should not change, server will cache these)
-- send static structure properties, cached by server
local function _send_struct()
local mek_data = { false, 0, 0, 0, types.new_zero_coordinate(), types.new_zero_coordinate(), 0, 0, 0, 0, 0, 0, 0, 0 }
local mek_data = {}
reactor.__p_disable_afc()
local tasks = {
function () mek_data[1] = reactor.getLength() end,
@@ -690,6 +699,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
_send(RPLC_TYPE.MEK_STRUCT, mek_data)
self.resend_build = false
end
reactor.__p_enable_afc()
end
-- PUBLIC FUNCTIONS --
@@ -724,6 +735,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
-- attempt to establish link with supervisor
function public.send_link_req()
self.r_seq_num = nil
_send_mgmt(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PLC, config.UnitID })
end
@@ -820,16 +832,16 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
if l_chan == config.PLC_Channel then
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num()
elseif self.linked and ((self.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
self.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
elseif self.linked and (src_addr ~= self.sv_addr) then
log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr ..
"); channel in use by another system?")
return
else
self.r_seq_num = packet.scada_frame.seq_num()
self.r_seq_num = packet.scada_frame.seq_num() + 1
end
-- feed the watchdog first so it doesn't uhh...eat our packets :)
@@ -1015,10 +1027,9 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
println_ts("linked!")
log.info("supervisor establish request approved, linked to SV (CID#" .. src_addr .. ")")
-- link + reset remote sequence number and cache
-- link + reset cache
self.sv_addr = src_addr
self.linked = true
self.r_seq_num = nil
self.status_cache = nil
if plc_state.reactor_formed then _send_struct() end

View File

@@ -18,11 +18,16 @@ local ui = {
}
-- try to start the UI
---@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()
function renderer.try_start_ui(theme, color_mode)
local status, msg = true, nil
if ui.display == nil then
-- set theme
style.set_theme(theme, color_mode)
-- reset terminal
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
@@ -30,13 +35,19 @@ function renderer.try_start_ui()
term.setCursorPos(1, 1)
-- set overridden colors
for i = 1, #style.colors do
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
for i = 1, #style.theme.colors do
term.setPaletteColor(style.theme.colors[i].c, style.theme.colors[i].hex)
end
-- apply color mode
local c_mode_overrides = style.theme.color_modes[color_mode]
for i = 1, #c_mode_overrides do
term.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
end
-- init front panel view
status, msg = pcall(function ()
ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
ui.display = DisplayBox{window=term.current(),fg_bg=style.fp.root}
panel_view(ui.display)
end)
@@ -64,9 +75,9 @@ function renderer.close_ui()
ui.display = nil
-- restore colors
for i = 1, #style.colors do
local r, g, b = term.nativePaletteColor(style.colors[i].c)
term.setPaletteColor(style.colors[i].c, r, g, b)
for i = 1, #style.theme.colors do
local r, g, b = term.nativePaletteColor(style.theme.colors[i].c)
term.setPaletteColor(style.theme.colors[i].c, r, g, b)
end
-- reset terminal

View File

@@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "v1.6.5"
local R_PLC_VERSION = "v1.8.8"
local println = util.println
local println_ts = util.println_ts
@@ -31,9 +31,13 @@ if not plc.load_config() then
-- try to reconfigure (user action)
local success, error = configure.configure(true)
if success then
assert(plc.load_config(), "failed to load valid reactor PLC configuration")
if not plc.load_config() then
println("failed to load a valid configuration, please reconfigure")
return
end
else
assert(success, "reactor PLC configuration error: " .. error)
println("configuration error: " .. error)
return
end
end
@@ -51,6 +55,7 @@ log.info("========================================")
println(">> Reactor PLC " .. R_PLC_VERSION .. " <<")
crash.set_env("reactor-plc", R_PLC_VERSION)
crash.dbg_log_env()
----------------------------------------
-- main application
@@ -131,13 +136,13 @@ local function main()
-- 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");
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 not formed");
println("init> fission reactor is not formed")
log.warning("init> reactor logic adapter present, but reactor is not formed")
plc_state.degraded = true
@@ -172,8 +177,9 @@ local function main()
-- front panel time!
if not renderer.ui_ready() then
local message
plc_state.fp_ok, message = renderer.try_start_ui()
plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode)
-- ...or not
if not plc_state.fp_ok then
println_ts(util.c("UI error: ", message))
println("init> running without front panel")

View File

@@ -71,77 +71,49 @@ function threads.thread__main(smem, init)
-- blink heartbeat indicator
databus.heartbeat()
-- core clock tick
if networked then
-- start next clock timer
loop_clock.start()
-- start next clock timer
loop_clock.start()
-- send updated data
if nic.is_connected() then
if plc_comms.is_linked() then
smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS)
-- send updated data
if networked and nic.is_connected() then
if plc_comms.is_linked() then
smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS)
else
if ticks_to_update == 0 then
plc_comms.send_link_req()
ticks_to_update = LINK_TICKS
else
if ticks_to_update == 0 then
plc_comms.send_link_req()
ticks_to_update = LINK_TICKS
else
ticks_to_update = ticks_to_update - 1
end
ticks_to_update = ticks_to_update - 1
end
end
end
-- are we now formed after waiting to be formed?
-- check for formed state change
if (not plc_state.reactor_formed) and rps.is_formed() then
-- push a connect event and unmount it from the PPM
local iface = ppm.get_iface(plc_dev.reactor)
if iface then
log.info("unmounting and remounting unformed reactor")
ppm.unmount(plc_dev.reactor)
-- reactor now formed
plc_state.reactor_formed = true
local type, device = ppm.mount(iface)
println_ts("reactor is now formed.")
log.info("reactor is now formed")
if type == "fissionReactorLogicAdapter" and device ~= nil then
-- reconnect reactor
plc_dev.reactor = device
-- SCRAM newly formed reactor
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
-- 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
println_ts("reactor reconnected.")
log.info("reactor reconnected")
-- SCRAM newly connected 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
plc_state.degraded = false
end
rps.reconnect_reactor(plc_dev.reactor)
if networked then
plc_comms.reconnect_reactor(plc_dev.reactor)
end
-- reset RPS for newly connected reactor
-- without this, is_formed will be out of date and cause it to think its no longer formed again
rps.reset()
else
-- fully lost the reactor now :(
println_ts("reactor lost (failed reconnect)!")
log.error("reactor lost (failed reconnect)")
plc_state.no_reactor = true
plc_state.degraded = true
end
else
log.error("failed to get interface of previously connected reactor", true)
-- determine if we are still in a degraded state
if (not networked) or nic.is_connected() then
plc_state.degraded = false
end
elseif not rps.is_formed() then
-- 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
-- reactor no longer formed
println_ts("reactor is no longer formed.")
log.info("reactor is no longer formed")
plc_state.reactor_formed = false
plc_state.degraded = true
end
-- update indicators
@@ -173,7 +145,8 @@ function threads.thread__main(smem, init)
plc_state.degraded = true
elseif networked and type == "modem" then
-- we only care if this is our wireless modem
if nic.is_modem(device) then
-- 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!")
@@ -193,7 +166,7 @@ function threads.thread__main(smem, init)
end
end
else
log.warning("non-comms modem disconnected")
log.warning("a modem was disconnected")
end
end
end
@@ -230,12 +203,13 @@ function threads.thread__main(smem, init)
plc_comms.reconnect_reactor(plc_dev.reactor)
end
-- reset RPS for newly connected reactor
-- without this, is_formed will be out of date and cause it to think its no longer formed again
rps.reset()
-- 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
if device.isWireless() and not nic.is_connected() then
-- 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
@@ -368,9 +342,9 @@ function threads.thread__rps(smem)
end
end
-- if we are in standalone mode, continuously reset RPS
-- 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 then rps.reset(true) end
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
@@ -662,8 +636,9 @@ function threads.thread__setpoint_control(smem)
if (type(cur_burn_rate) == "number") and (setpoints.burn_rate ~= cur_burn_rate) and rps.is_active() then
last_burn_sp = setpoints.burn_rate
-- update without ramp if <= 2.5 mB/t change
running = math.abs(setpoints.burn_rate - cur_burn_rate) > 2.5
-- update without ramp if <= 2.5 mB/t increase
-- no need to ramp down, as the ramp up poses the safety risks
running = (setpoints.burn_rate - cur_burn_rate) > 2.5
if running then
log.debug(util.c("SPCTL: starting burn rate ramp from ", cur_burn_rate, " mB/t to ", setpoints.burn_rate, " mB/t"))

File diff suppressed because it is too large Load Diff

View File

@@ -7,60 +7,51 @@ local boilerv_rtu = {}
---@param boiler table
---@return rtu_device interface, boolean faulted
function boilerv_rtu.new(boiler)
local unit = rtu.init_unit()
-- disable auto fault clearing
boiler.__p_clear_fault()
boiler.__p_disable_afc()
local unit = rtu.init_unit(boiler)
-- discrete inputs --
unit.connect_di(boiler.isFormed)
unit.connect_di("isFormed")
-- coils --
-- none
-- input registers --
-- multiblock properties
unit.connect_input_reg(boiler.getLength)
unit.connect_input_reg(boiler.getWidth)
unit.connect_input_reg(boiler.getHeight)
unit.connect_input_reg(boiler.getMinPos)
unit.connect_input_reg(boiler.getMaxPos)
unit.connect_input_reg("getLength")
unit.connect_input_reg("getWidth")
unit.connect_input_reg("getHeight")
unit.connect_input_reg("getMinPos")
unit.connect_input_reg("getMaxPos")
-- build properties
unit.connect_input_reg(boiler.getBoilCapacity)
unit.connect_input_reg(boiler.getSteamCapacity)
unit.connect_input_reg(boiler.getWaterCapacity)
unit.connect_input_reg(boiler.getHeatedCoolantCapacity)
unit.connect_input_reg(boiler.getCooledCoolantCapacity)
unit.connect_input_reg(boiler.getSuperheaters)
unit.connect_input_reg(boiler.getMaxBoilRate)
unit.connect_input_reg("getBoilCapacity")
unit.connect_input_reg("getSteamCapacity")
unit.connect_input_reg("getWaterCapacity")
unit.connect_input_reg("getHeatedCoolantCapacity")
unit.connect_input_reg("getCooledCoolantCapacity")
unit.connect_input_reg("getSuperheaters")
unit.connect_input_reg("getMaxBoilRate")
-- current state
unit.connect_input_reg(boiler.getTemperature)
unit.connect_input_reg(boiler.getBoilRate)
unit.connect_input_reg(boiler.getEnvironmentalLoss)
unit.connect_input_reg("getTemperature")
unit.connect_input_reg("getBoilRate")
unit.connect_input_reg("getEnvironmentalLoss")
-- tanks
unit.connect_input_reg(boiler.getSteam)
unit.connect_input_reg(boiler.getSteamNeeded)
unit.connect_input_reg(boiler.getSteamFilledPercentage)
unit.connect_input_reg(boiler.getWater)
unit.connect_input_reg(boiler.getWaterNeeded)
unit.connect_input_reg(boiler.getWaterFilledPercentage)
unit.connect_input_reg(boiler.getHeatedCoolant)
unit.connect_input_reg(boiler.getHeatedCoolantNeeded)
unit.connect_input_reg(boiler.getHeatedCoolantFilledPercentage)
unit.connect_input_reg(boiler.getCooledCoolant)
unit.connect_input_reg(boiler.getCooledCoolantNeeded)
unit.connect_input_reg(boiler.getCooledCoolantFilledPercentage)
unit.connect_input_reg("getSteam")
unit.connect_input_reg("getSteamNeeded")
unit.connect_input_reg("getSteamFilledPercentage")
unit.connect_input_reg("getWater")
unit.connect_input_reg("getWaterNeeded")
unit.connect_input_reg("getWaterFilledPercentage")
unit.connect_input_reg("getHeatedCoolant")
unit.connect_input_reg("getHeatedCoolantNeeded")
unit.connect_input_reg("getHeatedCoolantFilledPercentage")
unit.connect_input_reg("getCooledCoolant")
unit.connect_input_reg("getCooledCoolantNeeded")
unit.connect_input_reg("getCooledCoolantFilledPercentage")
-- holding registers --
-- none
-- check if any calls faulted
local faulted = boiler.__p_is_faulted()
boiler.__p_clear_fault()
boiler.__p_enable_afc()
return unit.interface(), faulted
return unit.interface(), false
end
return boilerv_rtu

View File

@@ -7,14 +7,10 @@ local dynamicv_rtu = {}
---@param dynamic_tank table
---@return rtu_device interface, boolean faulted
function dynamicv_rtu.new(dynamic_tank)
local unit = rtu.init_unit()
-- disable auto fault clearing
dynamic_tank.__p_clear_fault()
dynamic_tank.__p_disable_afc()
local unit = rtu.init_unit(dynamic_tank)
-- discrete inputs --
unit.connect_di(dynamic_tank.isFormed)
unit.connect_di("isFormed")
-- coils --
unit.connect_coil(function () dynamic_tank.incrementContainerEditMode() end, function () end)
@@ -22,27 +18,22 @@ function dynamicv_rtu.new(dynamic_tank)
-- input registers --
-- multiblock properties
unit.connect_input_reg(dynamic_tank.getLength)
unit.connect_input_reg(dynamic_tank.getWidth)
unit.connect_input_reg(dynamic_tank.getHeight)
unit.connect_input_reg(dynamic_tank.getMinPos)
unit.connect_input_reg(dynamic_tank.getMaxPos)
unit.connect_input_reg("getLength")
unit.connect_input_reg("getWidth")
unit.connect_input_reg("getHeight")
unit.connect_input_reg("getMinPos")
unit.connect_input_reg("getMaxPos")
-- build properties
unit.connect_input_reg(dynamic_tank.getTankCapacity)
unit.connect_input_reg(dynamic_tank.getChemicalTankCapacity)
unit.connect_input_reg("getTankCapacity")
unit.connect_input_reg("getChemicalTankCapacity")
-- tanks/containers
unit.connect_input_reg(dynamic_tank.getStored)
unit.connect_input_reg(dynamic_tank.getFilledPercentage)
unit.connect_input_reg("getStored")
unit.connect_input_reg("getFilledPercentage")
-- holding registers --
unit.connect_holding_reg(dynamic_tank.getContainerEditMode, dynamic_tank.setContainerEditMode)
unit.connect_holding_reg("getContainerEditMode", "setContainerEditMode")
-- check if any calls faulted
local faulted = dynamic_tank.__p_is_faulted()
dynamic_tank.__p_clear_fault()
dynamic_tank.__p_enable_afc()
return unit.interface(), faulted
return unit.interface(), false
end
return dynamicv_rtu

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