Compare commits

..

275 Commits

Author SHA1 Message Date
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
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
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 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
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
130 changed files with 10082 additions and 4316 deletions

View File

@ -26,4 +26,4 @@ jobs:
# --no-max-line-length = Disable warnings for long line lengths # --no-max-line-length = Disable warnings for long line lengths
# --exclude-files ... = Exclude lockbox library (external) and config files # --exclude-files ... = Exclude lockbox library (external) and config files
# --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os' # --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os'
args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http keys parallel periphemu peripheral read rs settings shell term textutils window args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* --globals os _HOST bit colors fs http keys parallel periphemu peripheral read rs settings shell term textutils window

View File

@ -46,7 +46,7 @@ jobs:
- name: Generate manifest and shields for main branch - name: Generate manifest and shields for main branch
id: manifest-main id: manifest-main
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }} if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
run: python imgen.py shields run: python build/imgen.py shields
- name: Save main's manifest - name: Save main's manifest
if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }} if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/main run: mv install_manifest.json deploy/manifests/main
@ -61,7 +61,7 @@ jobs:
- name: Generate manifest for devel - name: Generate manifest for devel
id: manifest-devel id: manifest-devel
if: ${{ (success() || failure()) && steps.checkout-devel.outcome == 'success' }} if: ${{ (success() || failure()) && steps.checkout-devel.outcome == 'success' }}
run: python imgen.py run: python build/imgen.py
- name: Save devel's manifest - name: Save devel's manifest
if: ${{ (success() || failure()) && steps.manifest-devel.outcome == 'success' }} if: ${{ (success() || failure()) && steps.manifest-devel.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/devel run: mv install_manifest.json deploy/manifests/devel

2
.gitignore vendored
View File

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

View File

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

View File

@ -6,9 +6,9 @@ Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fi
![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=main&label=main)
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=devel&label=devel) ![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 ## Released Component Versions
@ -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: You can install this on a ComputerCraft computer using either:
* `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua` * `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua`
* `pastebin get sqUN6VUb ccmsi.lua` * `pastebin get sqUN6VUb ccmsi.lua`
* Off-line (when HTTP is disabled) installation via [release bundles](https://github.com/MikaylaFischler/cc-mek-scada/wiki/Alternative-Installation-Strategies#release-bundles)
## Contributing
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) ## [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. > 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()

229
ccmsi.lua
View File

@ -15,15 +15,63 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]-- ]]--
local function println(message) print(tostring(message)) end local CCMSI_VERSION = "v1.17"
local function print(message) term.write(tostring(message)) end
local CCMSI_VERSION = "v1.14"
local install_dir = "/.install-cache" local install_dir = "/.install-cache"
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/" local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/" local repo_path = "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 opts = { ... }
local mode, app, target local mode, app, target
local install_manifest = manifest_path.."main/install_manifest.json" local install_manifest = manifest_path.."main/install_manifest.json"
@ -120,8 +168,29 @@ local function write_install_manifest(manifest, dependencies)
imfile.close() imfile.close()
end end
-- try at most 3 times to download a file from the repository and write into w_path base directory
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 -- recursively build a tree out of the file manifest
local function gen_tree(manifest) local function gen_tree(manifest, log)
local function _tree_add(tree, split) local function _tree_add(tree, split)
if #split > 1 then if #split > 1 then
local name = table.remove(split, 1) local name = table.remove(split, 1)
@ -131,13 +200,14 @@ local function gen_tree(manifest)
return nil return nil
end end
local list, tree = {}, {} local list, tree = { log }, {}
-- make a list of each and every file -- make a list of each and every file
for _, files in pairs(manifest.files) do for i = 1, #files do table.insert(list, files[i]) end end for _, files in pairs(manifest.files) do for i = 1, #files do table.insert(list, files[i]) end end
for i = 1, #list do for i = 1, #list do
local split = {} local split = {}
---@diagnostic disable-next-line: discard-returns
string.gsub(list[i], "([^/]+)", function(c) split[#split + 1] = c end) string.gsub(list[i], "([^/]+)", function(c) split[#split + 1] = c end)
if #split == 1 then table.insert(tree, list[i]) if #split == 1 then table.insert(tree, list[i])
else table.insert(tree, _tree_add(tree, split)) end else table.insert(tree, _tree_add(tree, split)) end
@ -159,7 +229,7 @@ local function _clean_dir(dir, tree)
if fs.isDir(path) then if fs.isDir(path) then
_clean_dir(path, tree[val]) _clean_dir(path, tree[val])
if #fs.list(path) == 0 then fs.delete(path);println("deleted "..path) end if #fs.list(path) == 0 then fs.delete(path);println("deleted "..path) end
elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then ---@todo remove config.lua on full release
fs.delete(path) fs.delete(path)
println("deleted "..path) println("deleted "..path)
end end
@ -168,11 +238,16 @@ end
-- go through app/common directories to delete unused files -- go through app/common directories to delete unused files
local function clean(manifest) local function clean(manifest)
local tree = gen_tree(manifest) local log = nil
if fs.exists(app..".settings") and settings.load(app..".settings") then
log = settings.get("LogPath")
if log:sub(1, 1) == "/" then log = log:sub(2) end
end
local tree = gen_tree(manifest, log)
table.insert(tree, "install_manifest.json") table.insert(tree, "install_manifest.json")
table.insert(tree, "ccmsi.lua") table.insert(tree, "ccmsi.lua")
table.insert(tree, "log.txt") ---@fixme fix after migration to settings files?
local ls = fs.list("/") local ls = fs.list("/")
for _, val in pairs(ls) do for _, val in pairs(ls) do
@ -192,10 +267,27 @@ end
-- get and validate command line options -- get and validate command line options
println("-- CC Mekanism SCADA Installer "..CCMSI_VERSION.." --") if _is_pkt_env then println("- SCADA Installer "..CCMSI_VERSION.." -")
else println("-- CC Mekanism SCADA Installer "..CCMSI_VERSION.." --") end
if #opts == 0 or opts[1] == "help" then if #opts == 0 or opts[1] == "help" then
println("usage: ccmsi <mode> <app> <branch>") println("usage: ccmsi <mode> <app> <branch>")
if _is_pkt_env then
yellow();println("<mode>");lgray()
println(" check - check latest")
println(" install - fresh install")
println(" update - update app")
println(" uninstall - remove app")
yellow();println("<app>");lgray()
println(" reactor-plc")
println(" rtu")
println(" supervisor")
println(" coordinator")
println(" pocket")
println(" installer (update only)")
yellow();println("<branch>");lgray();
println(" main (default) | devel");white()
else
println("<mode>") println("<mode>")
lgray() lgray()
println(" check - check latest versions available") println(" check - check latest versions available")
@ -214,6 +306,7 @@ if #opts == 0 or opts[1] == "help" then
println(" installer - ccmsi installer (update only)") println(" installer - ccmsi installer (update only)")
white();println("<branch>") white();println("<branch>")
lgray();println(" main (default) | devel");white() lgray();println(" main (default) | devel");white()
end
return return
else else
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" }) mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
@ -244,7 +337,6 @@ else
end end
-- run selected mode -- run selected mode
if mode == "check" then if mode == "check" then
local ok, manifest = get_remote_manifest() local ok, manifest = get_remote_manifest()
if not ok then return end if not ok then return end
@ -260,20 +352,22 @@ if mode == "check" then
-- list all versions -- list all versions
for key, value in pairs(manifest.versions) do for key, value in pairs(manifest.versions) do
term.setTextColor(colors.purple) term.setTextColor(colors.purple)
print(string.format("%-14s", "["..key.."]")) local tag = string.format("%-14s", "["..key.."]")
if not _is_pkt_env then print(tag) end
if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then
if _is_pkt_env then println(tag) end
blue();print(local_manifest.versions[key]) blue();print(local_manifest.versions[key])
if value ~= local_manifest.versions[key] then if value ~= local_manifest.versions[key] then
white();print(" (") white();print(" (")
cyan();print(value);white();println(" available)") cyan();print(value);white();println(" available)")
else green();println(" (up to date)") end else green();println(" (up to date)") end
else elseif not _is_pkt_env then
lgray();print("not installed");white();print(" (latest ") lgray();print("not installed");white();print(" (latest ")
cyan();print(value);white();println(")") cyan();print(value);white();println(")")
end end
end end
if manifest.versions.installer ~= local_manifest.versions.installer then if manifest.versions.installer ~= local_manifest.versions.installer and not _is_pkt_env then
yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white() yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white()
end end
elseif mode == "install" or mode == "update" then elseif mode == "install" or mode == "update" then
@ -318,7 +412,7 @@ elseif mode == "install" or mode == "update" then
local dl, err = http.get(repo_path.."ccmsi.lua") local dl, err = http.get(repo_path.."ccmsi.lua")
if dl == nil then if dl == nil then
red();println("HTTP Error "..err) red();println("HTTP Error: "..err)
println("Installer download failed.");white() println("Installer download failed.");white()
else else
local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location
@ -355,9 +449,6 @@ elseif mode == "install" or mode == "update" then
ver.graphics.changed = show_pkg_change("graphics", ver.graphics) ver.graphics.changed = show_pkg_change("graphics", ver.graphics)
ver.lockbox.changed = show_pkg_change("lockbox", ver.lockbox) ver.lockbox.changed = show_pkg_change("lockbox", ver.lockbox)
-- ask for confirmation
if not ask_y_n("Continue", false) then return end
-------------------------- --------------------------
-- START INSTALL/UPDATE -- -- START INSTALL/UPDATE --
-------------------------- --------------------------
@ -372,11 +463,32 @@ elseif mode == "install" or mode == "update" then
table.insert(dependencies, app) 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 for _, dependency in pairs(dependencies) do
local size = size_list[dependency] local size = size_list[dependency]
space_required = space_required + size space_required = space_required + size
any_change = any_change or not unchanged(dependency)
end 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 -- check space constraints
if space_available < space_required then if space_available < space_required then
single_file_mode = true single_file_mode = true
@ -392,16 +504,6 @@ elseif mode == "install" or mode == "update" then
local success = true 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 not single_file_mode then
if fs.exists(install_dir) then fs.delete(install_dir);fs.makeDir(install_dir) end if fs.exists(install_dir) then fs.delete(install_dir);fs.makeDir(install_dir) end
@ -416,19 +518,14 @@ elseif mode == "install" or mode == "update" then
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do for _, file in pairs(files) do
println("GET "..file) println("GET "..file)
local dl, err = http.get(repo_path..file) if not http_get_file(file, install_dir.."/") then
red();println("failed to download "..file)
if dl == nil then
red();println("HTTP Error "..err)
success = false success = false
break break
else
local handle = fs.open(install_dir.."/"..file, "w")
handle.write(dl.readAll())
handle.close()
end end
end end
end end
if not success then break end
end end
-- copy in downloaded files (installation) -- copy in downloaded files (installation)
@ -478,19 +575,14 @@ elseif mode == "install" or mode == "update" then
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do for _, file in pairs(files) do
println("GET "..file) println("GET "..file)
local dl, err = http.get(repo_path..file) if not http_get_file(file, "/") then
red();println("failed to download "..file)
if dl == nil then
red();println("HTTP Error "..err)
success = false success = false
break break
else
local handle = fs.open("/"..file, "w")
handle.write(dl.readAll())
handle.close()
end end
end end
end end
if not success then break end
end end
if success then if success then
@ -534,36 +626,8 @@ elseif mode == "uninstall" then
table.insert(dependencies, app) 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 -- delete all installed files
lgray()
for _, dependency in pairs(dependencies) do for _, dependency in pairs(dependencies) do
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do for _, file in pairs(files) do
@ -582,8 +646,23 @@ elseif mode == "uninstall" then
end end
end end
if fs.exists(legacy_config_file) then -- delete log file
fs.delete(legacy_config_file);println("deleted "..legacy_config_file) local log_deleted = false
local settings_file = app..".settings"
if fs.exists(settings_file) and settings.load(settings_file) then
local log = settings.get("LogPath")
if log ~= nil then
log_deleted = true
if fs.exists(log) then
fs.delete(log)
println("deleted log file "..log)
end
end
end
if not log_deleted then
red();println("Failed to delete log file (it may not exist).");lgray()
end end
if fs.exists(settings_file) then if fs.exists(settings_file) then

View File

@ -7,10 +7,11 @@ local log = require("scada-common.log")
local network = require("scada-common.network") local network = require("scada-common.network")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd") local tcd = require("scada-common.tcd")
local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local themes = require("graphics.themes")
local core = require("graphics.core") local core = require("graphics.core")
local themes = require("graphics.themes")
local DisplayBox = require("graphics.elements.displaybox") local DisplayBox = require("graphics.elements.displaybox")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.div")
@ -44,7 +45,9 @@ local RIGHT = core.ALIGN.RIGHT
-- changes to the config data/format to let the user know -- changes to the config data/format to let the user know
local changes = { local changes = {
{ "v1.2.4", { "Added temperature scale options" } }, { "v1.2.4", { "Added temperature scale options" } },
{ "v1.2.12", { "Added main UI theme", "Added front panel UI theme", "Added color accessibility modes" } } { "v1.2.12", { "Added main UI theme", "Added front panel UI theme", "Added color accessibility modes" } },
{ "v1.3.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
{ "v1.5.1", { "Added energy scale options" } }
} }
---@class crd_configurator ---@class crd_configurator
@ -68,7 +71,7 @@ local tool_ctl = {
nic = nil, ---@type nic nic = nil, ---@type nic
net_listen = false, net_listen = false,
sv_addr = comms.BROADCAST, sv_addr = comms.BROADCAST,
sv_seq_num = 0, sv_seq_num = util.time_ms() * 10,
sv_cool_conf = nil, ---@type table list of boiler & turbine counts sv_cool_conf = nil, ---@type table list of boiler & turbine counts
show_sv_cfg = nil, ---@type function show_sv_cfg = nil, ---@type function
@ -117,6 +120,7 @@ local tmp_cfg = {
SpeakerVolume = 1.0, SpeakerVolume = 1.0,
Time24Hour = true, Time24Hour = true,
TempScale = 1, TempScale = 1,
EnergyScale = 1,
DisableFlowView = false, DisableFlowView = false,
MainDisplay = nil, ---@type string MainDisplay = nil, ---@type string
FlowDisplay = nil, ---@type string FlowDisplay = nil, ---@type string
@ -149,7 +153,8 @@ local fields = {
{ "UnitDisplays", "Unit Monitors", {} }, { "UnitDisplays", "Unit Monitors", {} },
{ "SpeakerVolume", "Speaker Volume", 1.0 }, { "SpeakerVolume", "Speaker Volume", 1.0 },
{ "Time24Hour", "Use 24-hour Time Format", true }, { "Time24Hour", "Use 24-hour Time Format", true },
{ "TempScale", "Temperature Scale", 1 }, { "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN },
{ "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE },
{ "DisableFlowView", "Disable Flow Monitor (legacy, discouraged)", false }, { "DisableFlowView", "Disable Flow Monitor (legacy, discouraged)", false },
{ "SVR_Channel", "SVR Channel", 16240 }, { "SVR_Channel", "SVR Channel", 16240 },
{ "CRD_Channel", "CRD Channel", 16243 }, { "CRD_Channel", "CRD Channel", 16243 },
@ -303,7 +308,7 @@ local function config_view(display)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local function exit() os.queueEvent("terminate") end local function exit() os.queueEvent("terminate") end
TextBox{parent=display,y=1,text="Coordinator Configurator",alignment=CENTER,height=1,fg_bg=style.header} TextBox{parent=display,y=1,text="Coordinator Configurator",alignment=CENTER,fg_bg=style.header}
local root_pane_div = Div{parent=display,x=1,y=2} local root_pane_div = Div{parent=display,x=1,y=2}
@ -331,7 +336,7 @@ local function config_view(display)
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text=msg,fg_bg=cpair(colors.red,colors.lightGray)} TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text=msg,fg_bg=cpair(colors.red,colors.lightGray)}
y_start = y_start + 5 y_start = y_start + 5
elseif tool_ctl.start_fail > 0 then elseif tool_ctl.start_fail > 0 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 y_start = y_start + 5
end end
@ -358,7 +363,7 @@ local function config_view(display)
end 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=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(10)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(10)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
if not tool_ctl.has_config then if not tool_ctl.has_config then
@ -375,24 +380,24 @@ local function config_view(display)
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}} local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
TextBox{parent=net_cfg,x=1,y=2,height=1,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} 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,height=1,text="Please set the network channels below."} TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."}
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 3 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 3 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=8,height=1,width=18,text="Supervisor Channel"} TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_1,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg} local svr_chan = NumberField{parent=net_c_1,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_1,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=10,height=1,width=19,text="Coordinator Channel"} TextBox{parent=net_c_1,x=1,y=10,width=19,text="Coordinator Channel"}
local crd_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg} local crd_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=10,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_1,x=29,y=10,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=12,height=1,width=14,text="Pocket Channel"} TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"}
local pkt_chan = NumberField{parent=net_c_1,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg} local pkt_chan = NumberField{parent=net_c_1,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
local chan_err = TextBox{parent=net_c_1,x=8,y=14,height=1,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_channels() local function submit_channels()
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value()) local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
@ -406,18 +411,18 @@ local function config_view(display)
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=net_c_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,height=1,text="Please set the connection timeouts below."} TextBox{parent=net_c_2,x=1,y=1,text="Please set the connection timeouts below."}
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=8,height=1,width=19,text="Supervisor Timeout"} TextBox{parent=net_c_2,x=1,y=8,width=19,text="Supervisor Timeout"}
local svr_timeout = NumberField{parent=net_c_2,x=20,y=8,width=7,default=ini_cfg.SVR_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} local svr_timeout = NumberField{parent=net_c_2,x=20,y=8,width=7,default=ini_cfg.SVR_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=1,y=10,height=1,width=14,text="Pocket Timeout"} TextBox{parent=net_c_2,x=1,y=10,width=14,text="Pocket Timeout"}
local api_timeout = NumberField{parent=net_c_2,x=20,y=10,width=7,default=ini_cfg.API_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} local api_timeout = NumberField{parent=net_c_2,x=20,y=10,width=7,default=ini_cfg.API_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=28,y=8,height=4,width=7,text="seconds\n\nseconds",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_2,x=28,y=8,height=4,width=7,text="seconds\n\nseconds",fg_bg=g_lg_fg_bg}
local ct_err = TextBox{parent=net_c_2,x=8,y=14,height=1,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local ct_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_timeouts() local function submit_timeouts()
local svr_cto, api_cto = tonumber(svr_timeout.get_value()), tonumber(api_timeout.get_value()) local svr_cto, api_cto = tonumber(svr_timeout.get_value()), tonumber(api_timeout.get_value())
@ -431,13 +436,13 @@ local function config_view(display)
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=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,height=1,text="Please set the trusted range below."} TextBox{parent=net_c_3,x=1,y=1,text="Please set the trusted range below."}
TextBox{parent=net_c_3,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_3,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_3,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg} local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local tr_err = TextBox{parent=net_c_3,x=8,y=14,height=1,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local tr_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_tr() local function submit_tr()
local range_val = tonumber(range.get_value()) local range_val = tonumber(range.get_value())
@ -455,7 +460,7 @@ local function config_view(display)
TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."} TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
TextBox{parent=net_c_4,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra compution (can slow things down).",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_4,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra compution (can slow things down).",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=1,y=11,height=1,text="Facility Auth Key"} TextBox{parent=net_c_4,x=1,y=11,text="Facility Auth Key"}
local key, _, censor = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg} local key, _, censor = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
@ -465,7 +470,7 @@ local function config_view(display)
hide_key.set_value(true) hide_key.set_value(true)
censor_key(true) censor_key(true)
local key_err = TextBox{parent=net_c_4,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 key_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_auth() local function submit_auth()
local v = key.get_value() local v = key.get_value()
@ -503,12 +508,12 @@ local function config_view(display)
local fac_pane = MultiPane{parent=mon_cfg,x=1,y=4,panes={fac_c_1,fac_c_2,fac_c_3}} local fac_pane = MultiPane{parent=mon_cfg,x=1,y=4,panes={fac_c_1,fac_c_2,fac_c_3}}
TextBox{parent=fac_cfg,x=1,y=2,height=1,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)} TextBox{parent=fac_cfg,x=1,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)}
TextBox{parent=fac_c_1,x=1,y=1,height=4,text="This tool can attempt to connect to your supervisor computer. This would load facility information in order to get the unit count and aid monitor setup."} TextBox{parent=fac_c_1,x=1,y=1,height=4,text="This tool can attempt to connect to your supervisor computer. This would load facility information in order to get the unit count and aid monitor setup."}
TextBox{parent=fac_c_1,x=1,y=6,height=2,text="The supervisor startup app must be running and fully configured on your supervisor computer."} TextBox{parent=fac_c_1,x=1,y=6,height=2,text="The supervisor startup app must be running and fully configured on your supervisor computer."}
tool_ctl.sv_conn_status = TextBox{parent=fac_c_1,x=11,y=9,height=1,text=""} tool_ctl.sv_conn_status = TextBox{parent=fac_c_1,x=11,y=9,text=""}
tool_ctl.sv_conn_detail = TextBox{parent=fac_c_1,x=1,y=11,height=2,text=""} tool_ctl.sv_conn_detail = TextBox{parent=fac_c_1,x=1,y=11,height=2,text=""}
tool_ctl.sv_conn_button = PushButton{parent=fac_c_1,x=1,y=9,text="Connect",min_width=9,callback=function()tool_ctl.sv_connect()end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg} tool_ctl.sv_conn_button = PushButton{parent=fac_c_1,x=1,y=9,text="Connect",min_width=9,callback=function()tool_ctl.sv_connect()end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg}
@ -532,11 +537,11 @@ local function config_view(display)
TextBox{parent=fac_c_2,x=1,y=1,height=3,text="Please enter the number of reactors you have, also referred to as reactor units or 'units' for short. A maximum of 4 is currently supported."} TextBox{parent=fac_c_2,x=1,y=1,height=3,text="Please enter the number of reactors you have, also referred to as reactor units or 'units' for short. A maximum of 4 is currently supported."}
local num_units = NumberField{parent=fac_c_2,x=1,y=5,width=5,max_chars=2,default=ini_cfg.UnitCount,min=1,max=4,fg_bg=bw_fg_bg} local num_units = NumberField{parent=fac_c_2,x=1,y=5,width=5,max_chars=2,default=ini_cfg.UnitCount,min=1,max=4,fg_bg=bw_fg_bg}
TextBox{parent=fac_c_2,x=7,y=5,height=1,text="reactors"} TextBox{parent=fac_c_2,x=7,y=5,text="reactors"}
TextBox{parent=fac_c_2,x=1,y=7,height=3,text="This will decide how many monitors you need. If this does not match the supervisor's number of reactor units, the coordinator will not connect.",fg_bg=g_lg_fg_bg} TextBox{parent=fac_c_2,x=1,y=7,height=3,text="This will decide how many monitors you need. If this does not match the supervisor's number of reactor units, the coordinator will not connect.",fg_bg=g_lg_fg_bg}
TextBox{parent=fac_c_2,x=1,y=10,height=3,text="Since you skipped supervisor sync, the main monitor minimum height can't be determined precisely. It is marked with * on the next page.",fg_bg=g_lg_fg_bg} TextBox{parent=fac_c_2,x=1,y=10,height=3,text="Since you skipped supervisor sync, the main monitor minimum height can't be determined precisely. It is marked with * on the next page.",fg_bg=g_lg_fg_bg}
local nu_error = TextBox{parent=fac_c_2,x=8,y=14,height=1,width=35,text="Please set the number of reactors.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local nu_error = TextBox{parent=fac_c_2,x=8,y=14,width=35,text="Please set the number of reactors.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_num_units() local function submit_num_units()
local count = tonumber(num_units.get_value()) local count = tonumber(num_units.get_value())
@ -569,7 +574,7 @@ local function config_view(display)
local mon_pane = MultiPane{parent=mon_cfg,x=1,y=4,panes={mon_c_1,mon_c_2,mon_c_3,mon_c_4}} local mon_pane = MultiPane{parent=mon_cfg,x=1,y=4,panes={mon_c_1,mon_c_2,mon_c_3,mon_c_4}}
TextBox{parent=mon_cfg,x=1,y=2,height=1,text=" Monitor Configuration",fg_bg=cpair(colors.black,colors.blue)} TextBox{parent=mon_cfg,x=1,y=2,text=" Monitor Configuration",fg_bg=cpair(colors.black,colors.blue)}
TextBox{parent=mon_c_1,x=1,y=1,height=5,text="Your configuration requires the following monitors. The main and flow monitors' heights are dependent on your unit count and cooling setup. If you manually entered the unit count, a * will be shown on potentially inaccurate calculations."} TextBox{parent=mon_c_1,x=1,y=1,height=5,text="Your configuration requires the following monitors. The main and flow monitors' heights are dependent on your unit count and cooling setup. If you manually entered the unit count, a * will be shown on potentially inaccurate calculations."}
local mon_reqs = ListBox{parent=mon_c_1,x=1,y=7,height=6,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local mon_reqs = ListBox{parent=mon_c_1,x=1,y=7,height=6,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
@ -590,7 +595,7 @@ local function config_view(display)
local mon_list = ListBox{parent=mon_c_2,x=1,y=6,height=7,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local mon_list = ListBox{parent=mon_c_2,x=1,y=6,height=7,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local assign_err = TextBox{parent=mon_c_2,x=8,y=14,height=1,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local assign_err = TextBox{parent=mon_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_monitors() local function submit_monitors()
if tmp_cfg.MainDisplay == nil then if tmp_cfg.MainDisplay == nil then
@ -650,13 +655,13 @@ local function config_view(display)
if value == "0" or value == nil then mon_unit.set_value(0) end if value == "0" or value == nil then mon_unit.set_value(0) end
end end
TextBox{parent=mon_c_3,x=1,y=6,width=10,height=1,text="Assignment"} TextBox{parent=mon_c_3,x=1,y=6,width=10,text="Assignment"}
local mon_assign = RadioButton{parent=mon_c_3,x=1,y=7,default=1,options={"Main Monitor","Flow Monitor","Unit Monitor"},callback=on_assign_mon,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.blue} local mon_assign = RadioButton{parent=mon_c_3,x=1,y=7,default=1,options={"Main Monitor","Flow Monitor","Unit Monitor"},callback=on_assign_mon,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.blue}
mon_unit_l = TextBox{parent=mon_c_3,x=18,y=6,width=7,height=1,text="Unit ID"} mon_unit_l = TextBox{parent=mon_c_3,x=18,y=6,width=7,text="Unit ID"}
mon_unit = NumberField{parent=mon_c_3,x=18,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg} mon_unit = NumberField{parent=mon_c_3,x=18,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
local mon_u_err = TextBox{parent=mon_c_3,x=8,y=14,height=1,width=35,text="Please provide a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local mon_u_err = TextBox{parent=mon_c_3,x=8,y=14,width=35,text="Please provide a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
-- purge all assignments for a given monitor -- purge all assignments for a given monitor
---@param iface string ---@param iface string
@ -718,7 +723,7 @@ local function config_view(display)
local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49} local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49}
TextBox{parent=spkr_cfg,x=1,y=2,height=1,text=" Speaker Configuration",fg_bg=cpair(colors.black,colors.cyan)} TextBox{parent=spkr_cfg,x=1,y=2,text=" Speaker Configuration",fg_bg=cpair(colors.black,colors.cyan)}
TextBox{parent=spkr_c,x=1,y=1,height=2,text="The coordinator uses a speaker to play alarm sounds."} TextBox{parent=spkr_c,x=1,y=1,height=2,text="The coordinator uses a speaker to play alarm sounds."}
TextBox{parent=spkr_c,x=1,y=4,height=3,text="You can change the speaker audio volume from the default. The range is 0.0 to 3.0, where 1.0 is standard volume."} TextBox{parent=spkr_c,x=1,y=4,height=3,text="You can change the speaker audio volume from the default. The range is 0.0 to 3.0, where 1.0 is standard volume."}
@ -727,7 +732,7 @@ local function config_view(display)
TextBox{parent=spkr_c,x=1,y=10,height=3,text="Note: alarm sine waves are at half scale so that multiple will be required to reach full scale.",fg_bg=g_lg_fg_bg} TextBox{parent=spkr_c,x=1,y=10,height=3,text="Note: alarm sine waves are at half scale so that multiple will be required to reach full scale.",fg_bg=g_lg_fg_bg}
local s_vol_err = TextBox{parent=spkr_c,x=8,y=14,height=1,width=35,text="Please set a volume.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local s_vol_err = TextBox{parent=spkr_c,x=8,y=14,width=35,text="Please set a volume.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_vol() local function submit_vol()
local vol = tonumber(s_vol.get_value()) local vol = tonumber(s_vol.get_value())
@ -747,19 +752,23 @@ local function config_view(display)
local crd_c_1 = Div{parent=crd_cfg,x=2,y=4,width=49} local crd_c_1 = Div{parent=crd_cfg,x=2,y=4,width=49}
TextBox{parent=crd_cfg,x=1,y=2,height=1,text=" Coordinator UI Configuration",fg_bg=cpair(colors.black,colors.lime)} TextBox{parent=crd_cfg,x=1,y=2,text=" Coordinator UI Configuration",fg_bg=cpair(colors.black,colors.lime)}
TextBox{parent=crd_c_1,x=1,y=1,height=3,text="Configure the UI interface options below if you wish to customize formats."} TextBox{parent=crd_c_1,x=1,y=1,height=3,text="Configure the UI interface options below if you wish to customize formats."}
TextBox{parent=crd_c_1,x=1,y=4,height=1,text="Clock Time Format"} TextBox{parent=crd_c_1,x=1,y=4,text="Clock Time Format"}
local clock_fmt = RadioButton{parent=crd_c_1,x=1,y=5,default=util.trinary(ini_cfg.Time24Hour,1,2),options={"24-Hour","12-Hour"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} local clock_fmt = RadioButton{parent=crd_c_1,x=1,y=5,default=util.trinary(ini_cfg.Time24Hour,1,2),options={"24-Hour","12-Hour"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=crd_c_1,x=1,y=8,height=1,text="Temperature Scale"} TextBox{parent=crd_c_1,x=1,y=8,text="Temperature Scale"}
local temp_scale = RadioButton{parent=crd_c_1,x=1,y=9,default=ini_cfg.TempScale,options={"Kelvin","Celsius","Fahrenheit","Rankine"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} local temp_scale = RadioButton{parent=crd_c_1,x=1,y=9,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=crd_c_1,x=24,y=8,text="Energy Scale"}
local energy_scale = RadioButton{parent=crd_c_1,x=24,y=9,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
local function submit_ui_opts() local function submit_ui_opts()
tmp_cfg.Time24Hour = clock_fmt.get_value() == 1 tmp_cfg.Time24Hour = clock_fmt.get_value() == 1
tmp_cfg.TempScale = temp_scale.get_value() tmp_cfg.TempScale = temp_scale.get_value()
tmp_cfg.EnergyScale = energy_scale.get_value()
main_pane.set_value(7) main_pane.set_value(7)
end end
@ -772,20 +781,20 @@ local function config_view(display)
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49} 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_cfg,x=1,y=2,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=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,height=1,text="Log File Mode"} TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},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"} 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 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)} 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} 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 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() local function submit_log()
if path.get_value() ~= "" then if path.get_value() ~= "" then
@ -813,23 +822,38 @@ local function config_view(display)
local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}} 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,height=1,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)} TextBox{parent=clr_cfg,x=1,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)}
TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color themes for the different UI displays."} TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color themes for the different UI displays."}
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg} TextBox{parent=clr_c_1,x=1,y=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,height=1,text="Main UI Theme"} TextBox{parent=clr_c_1,x=1,y=7,text="Main UI Theme"}
local main_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.MainTheme,options=themes.UI_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} local main_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.MainTheme,options=themes.UI_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_1,x=18,y=7,height=1,text="Front Panel Theme"} TextBox{parent=clr_c_1,x=18,y=7,text="Front Panel Theme"}
local fp_theme = RadioButton{parent=clr_c_1,x=18,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} local fp_theme = RadioButton{parent=clr_c_1,x=18,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,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="By default, this project uses green/red heavily to distinguish ok and not, with some indicators also using multiple colors. By selecting a color blindness below, blues will be used instead of greens on indicators and multi-color indicators will be split up as space permits."} 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 function recolor(value)
local c = themes.smooth_stone.color_modes[value] local c = themes.smooth_stone.color_modes[value]
if value == 1 then 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 for i = 1, #style.colors do term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end
else else
term.setPaletteColor(colors.green, c[1].hex) term.setPaletteColor(colors.green, c[1].hex)
@ -838,15 +862,10 @@ local function config_view(display)
end end
end end
TextBox{parent=clr_c_2,x=1,y=8,height=1,text="Color Mode"} 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=9,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} 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=20,y=8,height=1,text="Preview"} 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}
local _ = IndLight{parent=clr_c_2,x=20,y=9,label="Good",colors=cpair(colors.black,colors.green)}
_ = IndLight{parent=clr_c_2,x=20,y=10,label="Warning",colors=cpair(colors.black,colors.yellow)}
_ = IndLight{parent=clr_c_2,x=20,y=11,label="Bad",colors=cpair(colors.black,colors.red)}
TextBox{parent=clr_c_2,x=1,y=14,height=6,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} 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}
@ -899,7 +918,7 @@ local function config_view(display)
clr_pane.set_value(1) clr_pane.set_value(1)
end end
TextBox{parent=clr_c_3,x=1,y=1,height=1,text="Settings saved!"} 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=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} 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}
@ -918,7 +937,7 @@ local function config_view(display)
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}} 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)} 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 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)}
@ -940,7 +959,10 @@ local function config_view(display)
end end
local function save_and_continue() local function save_and_continue()
for k, v in pairs(tmp_cfg) do settings.set(k, v) end for _, field in ipairs(fields) do
local k, v = field[1], tmp_cfg[field[1]]
if v == nil then settings.unset(k) else settings.set(k, v) end
end
if settings.save("/coordinator.settings") then if settings.save("/coordinator.settings") then
load_settings(settings_cfg, true) load_settings(settings_cfg, true)
@ -957,6 +979,8 @@ local function config_view(display)
try_set(dis_flow_view, ini_cfg.DisableFlowView) try_set(dis_flow_view, ini_cfg.DisableFlowView)
try_set(s_vol, ini_cfg.SpeakerVolume) try_set(s_vol, ini_cfg.SpeakerVolume)
try_set(clock_fmt, util.trinary(ini_cfg.Time24Hour, 1, 2)) try_set(clock_fmt, util.trinary(ini_cfg.Time24Hour, 1, 2))
try_set(temp_scale, ini_cfg.TempScale)
try_set(energy_scale, ini_cfg.EnergyScale)
try_set(mode, ini_cfg.LogMode) try_set(mode, ini_cfg.LogMode)
try_set(path, ini_cfg.LogPath) try_set(path, ini_cfg.LogPath)
try_set(en_dbg, ini_cfg.LogDebug) try_set(en_dbg, ini_cfg.LogDebug)
@ -986,7 +1010,7 @@ local function config_view(display)
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=dis_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=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} 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!"} TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"}
local function go_home() local function go_home()
main_pane.set_value(1) main_pane.set_value(1)
@ -1021,15 +1045,15 @@ local function config_view(display)
local cl = Div{parent=changelog,x=2,y=4,width=49} 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=49,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 for _, change in ipairs(changes) do
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg} TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
for _, v in ipairs(change[2]) do for _, v in ipairs(change[2]) do
local e = Div{parent=c_log,height=#util.strwrap(v,46)} 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)} TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
end end
end end
@ -1110,7 +1134,6 @@ local function config_view(display)
tool_ctl.nic.open(tmp_cfg.CRD_Channel) tool_ctl.nic.open(tmp_cfg.CRD_Channel)
tool_ctl.sv_addr = comms.BROADCAST tool_ctl.sv_addr = comms.BROADCAST
tool_ctl.sv_seq_num = 0
tool_ctl.net_listen = true tool_ctl.net_listen = true
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.CRD }) send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.CRD })
@ -1125,12 +1148,12 @@ local function config_view(display)
fac_config_list.remove_all() fac_config_list.remove_all()
local str = util.sprintf("Facility has %d reactor unit%s:", #conf, util.trinary(#conf==1,"","s")) local str = util.sprintf("Facility has %d reactor unit%s:", #conf, util.trinary(#conf==1,"","s"))
TextBox{parent=fac_config_list,height=1,text=str,fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=fac_config_list,text=str,fg_bg=cpair(colors.gray,colors.white)}
for i = 1, #conf do for i = 1, #conf do
local num_b, num_t = conf[i][1], conf[i][2] local num_b, num_t = conf[i][1], conf[i][2]
str = util.sprintf("\x07 Unit %d has %d boiler%s and %d turbine%s", i, num_b, util.trinary(num_b == 1, "", "s"), num_t, util.trinary(num_t == 1, "", "s")) str = util.sprintf("\x07 Unit %d has %d boiler%s and %d turbine%s", i, num_b, util.trinary(num_b == 1, "", "s"), num_t, util.trinary(num_t == 1, "", "s"))
TextBox{parent=fac_config_list,height=1,text=str,fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=fac_config_list,text=str,fg_bg=cpair(colors.gray,colors.white)}
end end
end end
@ -1166,13 +1189,13 @@ local function config_view(display)
mon_reqs.remove_all() mon_reqs.remove_all()
TextBox{parent=mon_reqs,x=1,y=1,height=1,text="\x1a "..tmp_cfg.UnitCount.." Unit View Monitor"..util.trinary(plural,"s","")} TextBox{parent=mon_reqs,x=1,y=1,text="\x1a "..tmp_cfg.UnitCount.." Unit View Monitor"..util.trinary(plural,"s","")}
TextBox{parent=mon_reqs,x=1,y=1,height=1,text=" "..util.trinary(plural,"each ","").."must be 4 blocks wide by 4 tall",fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=mon_reqs,x=1,y=1,text=" "..util.trinary(plural,"each ","").."must be 4 blocks wide by 4 tall",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=mon_reqs,x=1,y=1,height=1,text="\x1a 1 Main View Monitor"} TextBox{parent=mon_reqs,x=1,y=1,text="\x1a 1 Main View Monitor"}
TextBox{parent=mon_reqs,x=1,y=1,height=1,text=" must be 8 blocks wide by "..m_at_least..tool_ctl.main_mon_h..asterisk.." tall",fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=mon_reqs,x=1,y=1,text=" must be 8 blocks wide by "..m_at_least..tool_ctl.main_mon_h..asterisk.." tall",fg_bg=cpair(colors.gray,colors.white)}
if not tmp_cfg.DisableFlowView then if not tmp_cfg.DisableFlowView then
TextBox{parent=mon_reqs,x=1,y=1,height=1,text="\x1a 1 Flow View Monitor"} TextBox{parent=mon_reqs,x=1,y=1,text="\x1a 1 Flow View Monitor"}
TextBox{parent=mon_reqs,x=1,y=1,height=1,text=" must be 8 blocks wide by "..f_at_least..tool_ctl.flow_mon_h.." tall",fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=mon_reqs,x=1,y=1,text=" must be 8 blocks wide by "..f_at_least..tool_ctl.flow_mon_h.." tall",fg_bg=cpair(colors.gray,colors.white)}
end end
end end
@ -1235,6 +1258,10 @@ local function config_view(display)
function tool_ctl.gen_mon_list() function tool_ctl.gen_mon_list()
mon_list.remove_all() mon_list.remove_all()
local missing = { main = tmp_cfg.MainDisplay ~= nil, flow = tmp_cfg.FlowDisplay ~= nil, unit = {} }
for i = 1, tmp_cfg.UnitCount do missing.unit[i] = tmp_cfg.UnitDisplays[i] ~= nil end
-- list connected monitors
local monitors = ppm.get_monitor_list() local monitors = ppm.get_monitor_list()
for iface, device in pairs(monitors) do for iface, device in pairs(monitors) do
local dev = device.dev local dev = device.dev
@ -1254,11 +1281,14 @@ local function config_view(display)
if tmp_cfg.MainDisplay == iface then if tmp_cfg.MainDisplay == iface then
assignment = "Main" assignment = "Main"
missing.main = false
elseif tmp_cfg.FlowDisplay == iface then elseif tmp_cfg.FlowDisplay == iface then
assignment = "Flow" assignment = "Flow"
missing.flow = false
else else
for i = 1, tmp_cfg.UnitCount do for i = 1, tmp_cfg.UnitCount do
if tmp_cfg.UnitDisplays[i] == iface then if tmp_cfg.UnitDisplays[i] == iface then
missing.unit[i] = false
assignment = "Unit " .. i assignment = "Unit " .. i
break break
end end
@ -1267,8 +1297,8 @@ local function config_view(display)
local line = Div{parent=mon_list,x=1,y=1,height=1} local line = Div{parent=mon_list,x=1,y=1,height=1}
TextBox{parent=line,x=1,y=1,width=6,height=1,text=assignment,fg_bg=cpair(util.trinary(assignment=="Unused",colors.red,colors.blue),colors.white)} TextBox{parent=line,x=1,y=1,width=6,text=assignment,fg_bg=cpair(util.trinary(assignment=="Unused",colors.red,colors.blue),colors.white)}
TextBox{parent=line,x=8,y=1,height=1,text=iface} TextBox{parent=line,x=8,y=1,text=iface}
local w, h = ppm.monitor_block_size(dev.getSize()) local w, h = ppm.monitor_block_size(dev.getSize())
@ -1277,12 +1307,37 @@ local function config_view(display)
tool_ctl.gen_mon_list() tool_ctl.gen_mon_list()
end end
TextBox{parent=line,x=33,y=1,width=4,height=1,text=w.."x"..h,fg_bg=cpair(colors.black,colors.white)} TextBox{parent=line,x=33,y=1,width=4,text=w.."x"..h,fg_bg=cpair(colors.black,colors.white)}
PushButton{parent=line,x=37,y=1,min_width=5,height=1,text="SET",callback=function()tool_ctl.edit_monitor(iface,device)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} PushButton{parent=line,x=37,y=1,min_width=5,height=1,text="SET",callback=function()tool_ctl.edit_monitor(iface,device)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
local unset = PushButton{parent=line,x=42,y=1,min_width=7,height=1,text="UNSET",callback=unset_mon,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.black,colors.gray)} local unset = PushButton{parent=line,x=42,y=1,min_width=7,height=1,text="UNSET",callback=unset_mon,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.black,colors.gray)}
if assignment == "Unused" then unset.disable() end if assignment == "Unused" then unset.disable() end
end end
local dc_list = {} -- disconnected monitor list
if missing.main then table.insert(dc_list, { "Main", tmp_cfg.MainDisplay }) end
if missing.flow then table.insert(dc_list, { "Flow", tmp_cfg.FlowDisplay }) end
for i = 1, tmp_cfg.UnitCount do
if missing.unit[i] then table.insert(dc_list, { "Unit " .. i, tmp_cfg.UnitDisplays[i] }) end
end
-- add monitors that are assigned but not connected
for i = 1, #dc_list do
local line = Div{parent=mon_list,x=1,y=1,height=1}
TextBox{parent=line,x=1,y=1,width=6,text=dc_list[i][1],fg_bg=cpair(colors.blue,colors.white)}
TextBox{parent=line,x=8,y=1,text="disconnected",fg_bg=cpair(colors.red,colors.white)}
local function unset_mon()
purge_assignments(dc_list[i][2])
tool_ctl.gen_mon_list()
end
TextBox{parent=line,x=33,y=1,width=4,text="?x?",fg_bg=cpair(colors.black,colors.white)}
PushButton{parent=line,x=37,y=1,min_width=5,height=1,text="SET",callback=function()end,dis_fg_bg=cpair(colors.black,colors.gray)}.disable()
PushButton{parent=line,x=42,y=1,min_width=7,height=1,text="UNSET",callback=unset_mon,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.black,colors.gray)}
end
end end
-- expose the auth key on the summary page -- expose the auth key on the summary page
@ -1313,7 +1368,9 @@ local function config_view(display)
if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) 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] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "TempScale" then elseif f[1] == "TempScale" then
if raw == 1 then val = "Kelvin" elseif raw == 2 then val = "Celsius" elseif raw == 3 then val = "Fahrenheit" elseif raw == 4 then val = "Rankine" end val = util.strval(types.TEMP_SCALE_NAMES[raw])
elseif f[1] == "EnergyScale" then
val = util.strval(types.ENERGY_SCALE_NAMES[raw])
elseif f[1] == "MainTheme" then elseif f[1] == "MainTheme" then
val = util.strval(themes.ui_theme_name(raw)) val = util.strval(themes.ui_theme_name(raw))
elseif f[1] == "FrontPanelTheme" then elseif f[1] == "FrontPanelTheme" then
@ -1406,9 +1463,11 @@ function configurator.configure(start_code, message)
elseif event == "paste" then elseif event == "paste" then
display.handle_paste(param1) display.handle_paste(param1)
elseif event == "peripheral_detach" then elseif event == "peripheral_detach" then
---@diagnostic disable-next-line: discard-returns
ppm.handle_unmount(param1) ppm.handle_unmount(param1)
tool_ctl.gen_mon_list() tool_ctl.gen_mon_list()
elseif event == "peripheral" then elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1) ppm.mount(param1)
tool_ctl.gen_mon_list() tool_ctl.gen_mon_list()
elseif event == "monitor_resize" then elseif event == "monitor_resize" then

View File

@ -4,6 +4,8 @@ local ppm = require("scada-common.ppm")
local util = require("scada-common.util") local util = require("scada-common.util")
local types = require("scada-common.types") local types = require("scada-common.types")
local themes = require("graphics.themes")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process") local process = require("coordinator.process")
@ -36,6 +38,7 @@ function coordinator.load_config()
config.SpeakerVolume = settings.get("SpeakerVolume") config.SpeakerVolume = settings.get("SpeakerVolume")
config.Time24Hour = settings.get("Time24Hour") config.Time24Hour = settings.get("Time24Hour")
config.TempScale = settings.get("TempScale") config.TempScale = settings.get("TempScale")
config.EnergyScale = settings.get("EnergyScale")
config.DisableFlowView = settings.get("DisableFlowView") config.DisableFlowView = settings.get("DisableFlowView")
config.MainDisplay = settings.get("MainDisplay") config.MainDisplay = settings.get("MainDisplay")
@ -65,6 +68,8 @@ function coordinator.load_config()
cfv.assert_type_bool(config.Time24Hour) cfv.assert_type_bool(config.Time24Hour)
cfv.assert_type_int(config.TempScale) cfv.assert_type_int(config.TempScale)
cfv.assert_range(config.TempScale, 1, 4) cfv.assert_range(config.TempScale, 1, 4)
cfv.assert_type_int(config.EnergyScale)
cfv.assert_range(config.EnergyScale, 1, 3)
cfv.assert_type_bool(config.DisableFlowView) cfv.assert_type_bool(config.DisableFlowView)
cfv.assert_type_table(config.UnitDisplays) cfv.assert_type_table(config.UnitDisplays)
@ -87,7 +92,7 @@ function coordinator.load_config()
if type(config.AuthKey) == "string" then if type(config.AuthKey) == "string" then
local len = string.len(config.AuthKey) local len = string.len(config.AuthKey)
cfv.assert_eq(len == 0 or len >= 8, true) cfv.assert(len == 0 or len >= 8)
end end
cfv.assert_type_int(config.LogMode) cfv.assert_type_int(config.LogMode)
@ -100,7 +105,7 @@ function coordinator.load_config()
cfv.assert_type_int(config.FrontPanelTheme) cfv.assert_type_int(config.FrontPanelTheme)
cfv.assert_range(config.FrontPanelTheme, 1, 2) cfv.assert_range(config.FrontPanelTheme, 1, 2)
cfv.assert_type_int(config.ColorMode) cfv.assert_type_int(config.ColorMode)
cfv.assert_range(config.ColorMode, 1, 4) cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
-- Monitor Setup -- Monitor Setup
@ -190,7 +195,7 @@ end
---@return function? update, function? done ---@return function? update, function? done
local function log_dmesg(message, dmesg_tag, working) local function log_dmesg(message, dmesg_tag, working)
local colors = { local colors = {
GRAPHICS = colors.green, RENDER = colors.green,
SYSTEM = colors.cyan, SYSTEM = colors.cyan,
BOOT = colors.blue, BOOT = colors.blue,
COMMS = colors.purple, COMMS = colors.purple,
@ -204,7 +209,7 @@ local function log_dmesg(message, dmesg_tag, working)
end end
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_sys(message) log_dmesg(message, "SYSTEM") end
function coordinator.log_boot(message) log_dmesg(message, "BOOT") end function coordinator.log_boot(message) log_dmesg(message, "BOOT") end
function coordinator.log_comms(message) log_dmesg(message, "COMMS") end function coordinator.log_comms(message) log_dmesg(message, "COMMS") end
@ -230,8 +235,8 @@ function coordinator.comms(version, nic, sv_watchdog)
local self = { local self = {
sv_linked = false, sv_linked = false,
sv_addr = comms.BROADCAST, sv_addr = comms.BROADCAST,
sv_seq_num = 0, sv_seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
sv_r_seq_num = nil, sv_r_seq_num = nil, ---@type nil|integer
sv_config_err = false, sv_config_err = false,
last_est_ack = ESTABLISH_ACK.ALLOW, last_est_ack = ESTABLISH_ACK.ALLOW,
last_api_est_acks = {}, last_api_est_acks = {},
@ -277,11 +282,12 @@ function coordinator.comms(version, nic, sv_watchdog)
-- send an API establish request response -- send an API establish request response
---@param packet scada_packet ---@param packet scada_packet
---@param ack ESTABLISH_ACK ---@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 s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_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()) s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt) nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt)
@ -345,6 +351,7 @@ function coordinator.comms(version, nic, sv_watchdog)
ok = false ok = false
elseif self.sv_config_err then elseif self.sv_config_err then
self.est_task_done(false)
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs") coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
ok = false ok = false
elseif (os.clock() - self.est_last) > 1.0 then elseif (os.clock() - self.est_last) > 1.0 then
@ -468,10 +475,11 @@ function coordinator.comms(version, nic, sv_watchdog)
elseif packet.type == MGMT_TYPE.ESTABLISH then elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session -- establish a new session
-- validate packet and continue -- validate packet and continue
if packet.length == 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then if packet.length == 4 then
local comms_v = packet.data[1] local comms_v = util.strval(packet.data[1])
local firmware_v = packet.data[2] local firmware_v = util.strval(packet.data[2])
local dev_type = packet.data[3] local dev_type = packet.data[3]
local api_v = util.strval(packet.data[4])
if comms_v ~= comms.version then if comms_v ~= comms.version then
if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_VERSION then if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_VERSION then
@ -479,12 +487,19 @@ function coordinator.comms(version, nic, sv_watchdog)
end end
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) _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 elseif dev_type == DEVICE_TYPE.PKT then
-- pocket linking request -- pocket linking request
local id = apisessions.establish_session(src_addr, firmware_v) local id = apisessions.establish_session(src_addr, packet.scada_frame.seq_num(), firmware_v)
coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id)) coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id))
_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 else
log.debug(util.c("API_ESTABLISH: illegal establish packet for device ", dev_type, " on pocket channel")) 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) _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
@ -503,15 +518,15 @@ function coordinator.comms(version, nic, sv_watchdog)
elseif r_chan == config.SVR_Channel then elseif r_chan == config.SVR_Channel then
-- check sequence number -- check sequence number
if self.sv_r_seq_num == nil then if self.sv_r_seq_num == nil then
self.sv_r_seq_num = packet.scada_frame.seq_num() self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.sv_linked and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then elseif self.sv_r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) log.warning("sequence out-of-order: next = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return false return false
elseif self.sv_linked and src_addr ~= self.sv_addr then elseif self.sv_linked and src_addr ~= self.sv_addr then
log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?") log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?")
return false return false
else else
self.sv_r_seq_num = packet.scada_frame.seq_num() self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
end end
-- feed watchdog on valid sequence number -- feed watchdog on valid sequence number
@ -690,11 +705,10 @@ function coordinator.comms(version, nic, sv_watchdog)
if conf.num_units == config.UnitCount then if conf.num_units == config.UnitCount then
-- init io controller -- init io controller
iocontrol.init(conf, public, config.TempScale) iocontrol.init(conf, public, config.TempScale, config.EnergyScale)
self.sv_addr = src_addr self.sv_addr = src_addr
self.sv_linked = true self.sv_linked = true
self.sv_r_seq_num = nil
self.sv_config_err = false self.sv_config_err = false
iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED) iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED)

View File

@ -15,6 +15,11 @@ local pgi = require("coordinator.ui.pgi")
local ALARM_STATE = types.ALARM_STATE local ALARM_STATE = types.ALARM_STATE
local PROCESS = types.PROCESS 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 -- 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 WARN_RTT = 1000 -- 2x as long as expected w/ 0 ping
local HIGH_RTT = 1500 -- 3.33x as long as expected w/ 0 ping local HIGH_RTT = 1500 -- 3.33x as long as expected w/ 0 ping
@ -47,29 +52,44 @@ end
-- initialize the coordinator IO controller -- initialize the coordinator IO controller
---@param conf facility_conf configuration ---@param conf facility_conf configuration
---@param comms coord_comms comms reference ---@param comms coord_comms comms reference
---@param temp_scale integer temperature unit (1 = K, 2 = C, 3 = F, 4 = R) ---@param temp_scale TEMP_SCALE temperature unit
function iocontrol.init(conf, comms, temp_scale) ---@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) -- temperature unit label and conversion function (from Kelvin)
if temp_scale == 2 then if temp_scale == TEMP_SCALE.CELSIUS then
io.temp_label = "\xb0C"
io.temp_convert = function (t) return t - 273.15 end io.temp_convert = function (t) return t - 273.15 end
elseif temp_scale == 3 then elseif temp_scale == TEMP_SCALE.FAHRENHEIT then
io.temp_label = "\xb0F"
io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end
elseif temp_scale == 4 then elseif temp_scale == TEMP_SCALE.RANKINE then
io.temp_label = "\xb0R"
io.temp_convert = function (t) return 1.8 * t end io.temp_convert = function (t) return 1.8 * t end
else else
io.temp_label = "K" io.temp_label = "K"
io.temp_convert = function (t) return t end io.temp_convert = function (t) return t end
end end
-- energy unit label and conversion function (from Joules unless otherwise specified)
if energy_scale == ENERGY_SCALE.FE or energy_scale == ENERGY_SCALE.RF then
io.energy_convert = util.joules_to_fe_rf
io.energy_convert_from_fe = function (t) return t end
io.energy_convert_to_fe = function (t) return t end
else
io.energy_label = "J"
io.energy_convert = function (t) return t end
io.energy_convert_from_fe = util.fe_rf_to_joules
io.energy_convert_to_fe = util.joules_to_fe_rf
end
-- facility data structure -- facility data structure
---@class ioctl_facility ---@class ioctl_facility
io.facility = { io.facility = {
conf = conf,
num_units = conf.num_units, num_units = conf.num_units,
tank_mode = conf.cooling.fac_tank_mode, tank_mode = conf.cooling.fac_tank_mode,
tank_defs = conf.cooling.fac_tank_defs, tank_defs = conf.cooling.fac_tank_defs,
tank_list = conf.cooling.fac_tank_list,
all_sys_ok = false, all_sys_ok = false,
rtu_count = 0, rtu_count = 0,
@ -91,6 +111,7 @@ function iocontrol.init(conf, comms, temp_scale)
---@type WASTE_PRODUCT ---@type WASTE_PRODUCT
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM, auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
auto_pu_fallback_active = false, auto_pu_fallback_active = false,
auto_sps_disabled = false,
radiation = types.new_zero_radiation_reading(), radiation = types.new_zero_radiation_reading(),
@ -123,92 +144,6 @@ function iocontrol.init(conf, comms, temp_scale)
table.insert(io.facility.sps_ps_tbl, psil.create()) table.insert(io.facility.sps_ps_tbl, psil.create())
table.insert(io.facility.sps_data_tbl, {}) table.insert(io.facility.sps_data_tbl, {})
-- determine tank information
if io.facility.tank_mode == 0 then
io.facility.tank_defs = {}
-- on facility tank mode 0, setup tank defs to match unit tank option
for i = 1, conf.num_units do
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TankConnection, 1, 0)
end
io.facility.tank_list = { table.unpack(io.facility.tank_defs) }
else
-- decode the layout of tanks from the connections definitions
local tank_mode = io.facility.tank_mode
local tank_defs = io.facility.tank_defs
local tank_list = { table.unpack(tank_defs) }
local function calc_fdef(start_idx, end_idx)
local first = 4
for i = start_idx, end_idx do
if io.facility.tank_defs[i] == 2 then
if i < first then first = i end
end
end
return first
end
if tank_mode == 1 then
-- (1) 1 total facility tank (A A A A)
local first_fdef = calc_fdef(1, #tank_defs)
for i = 1, #tank_defs do
if i > first_fdef and tank_defs[i] == 2 then
tank_list[i] = 0
end
end
elseif tank_mode == 2 then
-- (2) 2 total facility tanks (A A A B)
local first_fdef = calc_fdef(1, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 3 then
-- (3) 2 total facility tanks (A A B B)
for _, a in pairs({ 1, 3 }) do
local b = a + 1
if (tank_defs[a] == 2) and (tank_defs[b] == 2) then
tank_list[b] = 0
end
end
elseif tank_mode == 4 then
-- (4) 2 total facility tanks (A B B B)
local first_fdef = calc_fdef(2, #tank_defs)
for i = 1, #tank_defs do
if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 5 then
-- (5) 3 total facility tanks (A A B C)
local first_fdef = calc_fdef(1, math.min(2, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 6 then
-- (6) 3 total facility tanks (A B B C)
local first_fdef = calc_fdef(2, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 7 then
-- (7) 3 total facility tanks (A B C C)
local first_fdef = calc_fdef(3, #tank_defs)
for i = 1, #tank_defs do
if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
end
io.facility.tank_list = tank_list
end
-- create facility tank tables -- create facility tank tables
for i = 1, #io.facility.tank_list do for i = 1, #io.facility.tank_list do
if io.facility.tank_list[i] == 2 then if io.facility.tank_list[i] == 2 then
@ -226,6 +161,8 @@ function iocontrol.init(conf, comms, temp_scale)
---@class ioctl_unit ---@class ioctl_unit
local entry = { local entry = {
unit_id = i, unit_id = i,
connected = false,
rtu_hw = { boilers = {}, turbines = {} },
num_boilers = 0, num_boilers = 0,
num_turbines = 0, num_turbines = 0,
@ -243,6 +180,9 @@ function iocontrol.init(conf, comms, temp_scale)
waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM, waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
waste_product = types.WASTE_PRODUCT.PLUTONIUM, waste_product = types.WASTE_PRODUCT.PLUTONIUM,
last_rate_change_ms = 0,
turbine_flow_stable = false,
-- auto control group -- auto control group
a_group = 0, a_group = 0,
@ -279,18 +219,18 @@ function iocontrol.init(conf, comms, temp_scale)
---@type alarms ---@type alarms
alarms = { alarms = {
ALARM_STATE.INACTIVE, -- containment breach ALARM_STATE.INACTIVE, -- containment breach
ALARM_STATE.INACTIVE, -- containment radiation ALARM_STATE.INACTIVE, -- containment radiation
ALARM_STATE.INACTIVE, -- reactor lost ALARM_STATE.INACTIVE, -- reactor lost
ALARM_STATE.INACTIVE, -- damage critical ALARM_STATE.INACTIVE, -- damage critical
ALARM_STATE.INACTIVE, -- reactor taking damage ALARM_STATE.INACTIVE, -- reactor taking damage
ALARM_STATE.INACTIVE, -- reactor over temperature ALARM_STATE.INACTIVE, -- reactor over temperature
ALARM_STATE.INACTIVE, -- reactor high temperature ALARM_STATE.INACTIVE, -- reactor high temperature
ALARM_STATE.INACTIVE, -- waste leak ALARM_STATE.INACTIVE, -- waste leak
ALARM_STATE.INACTIVE, -- waste level high ALARM_STATE.INACTIVE, -- waste level high
ALARM_STATE.INACTIVE, -- RPS transient ALARM_STATE.INACTIVE, -- RPS transient
ALARM_STATE.INACTIVE, -- RCS transient ALARM_STATE.INACTIVE, -- RCS transient
ALARM_STATE.INACTIVE -- turbine trip ALARM_STATE.INACTIVE -- turbine trip
}, },
annunciator = {}, ---@type annunciator annunciator = {}, ---@type annunciator
@ -317,12 +257,14 @@ function iocontrol.init(conf, comms, temp_scale)
for _ = 1, conf.cooling.r_cool[i].BoilerCount do for _ = 1, conf.cooling.r_cool[i].BoilerCount do
table.insert(entry.boiler_ps_tbl, psil.create()) table.insert(entry.boiler_ps_tbl, psil.create())
table.insert(entry.boiler_data_tbl, {}) table.insert(entry.boiler_data_tbl, {})
table.insert(entry.rtu_hw.boilers, { connected = false, faulted = false })
end end
-- create turbine tables -- create turbine tables
for _ = 1, conf.cooling.r_cool[i].TurbineCount do for _ = 1, conf.cooling.r_cool[i].TurbineCount do
table.insert(entry.turbine_ps_tbl, psil.create()) table.insert(entry.turbine_ps_tbl, psil.create())
table.insert(entry.turbine_data_tbl, {}) table.insert(entry.turbine_data_tbl, {})
table.insert(entry.rtu_hw.turbines, { connected = false, faulted = false })
end end
-- create tank tables -- create tank tables
@ -376,6 +318,13 @@ function iocontrol.fp_monitor_state(id, connected)
end end
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 -- report PKT firmware version and PKT session connection state
---@param session_id integer PKT session ---@param session_id integer PKT session
---@param fw string firmware version ---@param fw string firmware version
@ -585,7 +534,7 @@ function iocontrol.update_facility_status(status)
local ctl_status = status[1] 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.all_sys_ok = ctl_status[1]
fac.auto_ready = ctl_status[2] fac.auto_ready = ctl_status[2]
@ -636,9 +585,11 @@ function iocontrol.update_facility_status(status)
fac.auto_current_waste_product = ctl_status[15] fac.auto_current_waste_product = ctl_status[15]
fac.auto_pu_fallback_active = ctl_status[16] 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("current_waste_product", fac.auto_current_waste_product)
fac.ps.publish("pu_fallback_active", fac.auto_pu_fallback_active) fac.ps.publish("pu_fallback_active", fac.auto_pu_fallback_active)
fac.ps.publish("sps_disabled_low_power", fac.auto_sps_disabled)
else else
log.debug(log_header .. "control status not a table or length mismatch") log.debug(log_header .. "control status not a table or length mismatch")
valid = false valid = false
@ -655,10 +606,27 @@ function iocontrol.update_facility_status(status)
fac.rtu_count = rtu_statuses.count fac.rtu_count = rtu_statuses.count
-- power statistics -- power statistics
if type(rtu_statuses.power) == "table" then if type(rtu_statuses.power) == "table" and #rtu_statuses.power == 4 then
fac.induction_ps_tbl[1].publish("avg_charge", rtu_statuses.power[1]) local data = fac.induction_data_tbl[1] ---@type imatrix_session_db
fac.induction_ps_tbl[1].publish("avg_inflow", rtu_statuses.power[2]) local ps = fac.induction_ps_tbl[1] ---@type psil
fac.induction_ps_tbl[1].publish("avg_outflow", rtu_statuses.power[3])
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 else
log.debug(log_header .. "power statistics list not a table") log.debug(log_header .. "power statistics list not a table")
valid = false valid = false
@ -869,6 +837,7 @@ function iocontrol.update_unit_statuses(statuses)
end end
if #reactor_status == 0 then if #reactor_status == 0 then
unit.connected = false
unit.unit_ps.publish("computed_status", 1) -- disconnected unit.unit_ps.publish("computed_status", 1) -- disconnected
elseif #reactor_status == 3 then elseif #reactor_status == 3 then
local mek_status = reactor_status[1] local mek_status = reactor_status[1]
@ -928,6 +897,8 @@ function iocontrol.update_unit_statuses(statuses)
unit.unit_ps.publish(key, val) unit.unit_ps.publish(key, val)
end end
end end
unit.connected = true
else else
log.debug(log_header .. "reactor status length mismatch") log.debug(log_header .. "reactor status length mismatch")
valid = false valid = false
@ -942,7 +913,10 @@ function iocontrol.update_unit_statuses(statuses)
local boil_sum = 0 local boil_sum = 0
for id = 1, #unit.boiler_ps_tbl do for id = 1, #unit.boiler_ps_tbl do
if rtu_statuses.boilers[id] == nil then local connected = rtu_statuses.boilers[id] ~= nil
unit.rtu_hw.boilers[id].connected = connected
if not connected then
-- disconnected -- disconnected
unit.boiler_ps_tbl[id].publish("computed_status", 1) unit.boiler_ps_tbl[id].publish("computed_status", 1)
end end
@ -954,6 +928,7 @@ function iocontrol.update_unit_statuses(statuses)
local ps = unit.boiler_ps_tbl[id] ---@type psil local ps = unit.boiler_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(boiler, data, ps) local rtu_faulted = _record_multiblock_status(boiler, data, ps)
unit.rtu_hw.boilers[id].faulted = rtu_faulted
if rtu_faulted then if rtu_faulted then
ps.publish("computed_status", 3) -- faulted ps.publish("computed_status", 3) -- faulted
@ -985,7 +960,10 @@ function iocontrol.update_unit_statuses(statuses)
local flow_sum = 0 local flow_sum = 0
for id = 1, #unit.turbine_ps_tbl do for id = 1, #unit.turbine_ps_tbl do
if rtu_statuses.turbines[id] == nil then local connected = rtu_statuses.turbines[id] ~= nil
unit.rtu_hw.turbines[id].connected = connected
if not connected then
-- disconnected -- disconnected
unit.turbine_ps_tbl[id].publish("computed_status", 1) unit.turbine_ps_tbl[id].publish("computed_status", 1)
end end
@ -997,6 +975,7 @@ function iocontrol.update_unit_statuses(statuses)
local ps = unit.turbine_ps_tbl[id] ---@type psil local ps = unit.turbine_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(turbine, data, ps) local rtu_faulted = _record_multiblock_status(turbine, data, ps)
unit.rtu_hw.turbines[id].faulted = rtu_faulted
if rtu_faulted then if rtu_faulted then
ps.publish("computed_status", 3) -- faulted ps.publish("computed_status", 3) -- faulted
@ -1171,9 +1150,11 @@ function iocontrol.update_unit_statuses(statuses)
local unit_state = status[5] local unit_state = status[5]
if type(unit_state) == "table" then if type(unit_state) == "table" then
if #unit_state == 6 then if #unit_state == 8 then
unit.waste_mode = unit_state[5] unit.waste_mode = unit_state[5]
unit.waste_product = unit_state[6] 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_StatusLine1", unit_state[1])
unit.unit_ps.publish("U_StatusLine2", unit_state[2]) unit.unit_ps.publish("U_StatusLine2", unit_state[2])

View File

@ -29,7 +29,8 @@ local self = {
gen_target = 0.0, gen_target = 0.0,
limits = {}, limits = {},
waste_product = PRODUCT.PLUTONIUM, waste_product = PRODUCT.PLUTONIUM,
pu_fallback = false pu_fallback = false,
sps_low_power = false
}, },
waste_modes = {}, waste_modes = {},
priority_groups = {} priority_groups = {}
@ -65,13 +66,15 @@ function process.init(iocontrol, coord_comms)
ctl_proc.limits = config.limits ctl_proc.limits = config.limits
ctl_proc.waste_product = config.waste_product ctl_proc.waste_product = config.waste_product
ctl_proc.pu_fallback = config.pu_fallback ctl_proc.pu_fallback = config.pu_fallback
ctl_proc.sps_low_power = config.sps_low_power
self.io.facility.ps.publish("process_mode", ctl_proc.mode) self.io.facility.ps.publish("process_mode", ctl_proc.mode)
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target) self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target) self.io.facility.ps.publish("process_charge_target", self.io.energy_convert_from_fe(ctl_proc.charge_target))
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target) self.io.facility.ps.publish("process_gen_target", self.io.energy_convert_from_fe(ctl_proc.gen_target))
self.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product) self.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
self.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback) self.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback)
self.io.facility.ps.publish("process_sps_low_power", ctl_proc.sps_low_power)
for id = 1, math.min(#ctl_proc.limits, self.io.facility.num_units) do for id = 1, math.min(#ctl_proc.limits, self.io.facility.num_units) do
local unit = self.io.units[id] ---@type ioctl_unit local unit = self.io.units[id] ---@type ioctl_unit
@ -83,6 +86,7 @@ function process.init(iocontrol, coord_comms)
-- notify supervisor of auto waste config -- notify supervisor of auto waste config
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, ctl_proc.waste_product) self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, ctl_proc.waste_product)
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, ctl_proc.pu_fallback) self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, ctl_proc.pu_fallback)
self.comms.send_fac_command(FAC_COMMAND.SET_SPS_LP, ctl_proc.sps_low_power)
end end
-- unit waste states -- unit waste states
@ -259,6 +263,18 @@ function process.set_pu_fallback(enabled)
_write_auto_config() _write_auto_config()
end end
-- set automatic process control SPS usage at low power
---@param enabled boolean whether to enable SPS usage at low power
function process.set_sps_low_power(enabled)
self.comms.send_fac_command(FAC_COMMAND.SET_SPS_LP, enabled)
log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled))
-- update config table and save
self.control_states.process.sps_low_power = enabled
_write_auto_config()
end
-- save process control settings -- save process control settings
---@param mode PROCESS control mode ---@param mode PROCESS control mode
---@param burn_target number burn rate target ---@param burn_target number burn rate target
@ -300,8 +316,8 @@ function process.start_ack_handle(response)
self.io.facility.ps.publish("process_mode", ctl_proc.mode) self.io.facility.ps.publish("process_mode", ctl_proc.mode)
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target) self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target) self.io.facility.ps.publish("process_charge_target", self.io.energy_convert_from_fe(ctl_proc.charge_target))
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target) self.io.facility.ps.publish("process_gen_target", self.io.energy_convert_from_fe(ctl_proc.gen_target))
self.io.facility.start_ack(ack) self.io.facility.start_ack(ack)
end end

View File

@ -2,22 +2,26 @@
-- Graphics Rendering Control -- Graphics Rendering Control
-- --
local log = require("scada-common.log") 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 style = require("coordinator.ui.style")
local pgi = require("coordinator.ui.pgi") local pgi = require("coordinator.ui.pgi")
local flow_view = require("coordinator.ui.layout.flow_view") local flow_view = require("coordinator.ui.layout.flow_view")
local panel_view = require("coordinator.ui.layout.front_panel") local panel_view = require("coordinator.ui.layout.front_panel")
local main_view = require("coordinator.ui.layout.main_view") local main_view = require("coordinator.ui.layout.main_view")
local unit_view = require("coordinator.ui.layout.unit_view") local unit_view = require("coordinator.ui.layout.unit_view")
local core = require("graphics.core") local core = require("graphics.core")
local flasher = require("graphics.flasher") local flasher = require("graphics.flasher")
local DisplayBox = require("graphics.elements.displaybox") local DisplayBox = require("graphics.elements.displaybox")
local log_render = coordinator.log_render
---@class coord_renderer ---@class coord_renderer
local renderer = {} local renderer = {}
@ -195,18 +199,21 @@ function renderer.try_start_ui()
if engine.monitors.main ~= nil then if engine.monitors.main ~= nil then
engine.ui.main_display = DisplayBox{window=engine.monitors.main,fg_bg=style.root} engine.ui.main_display = DisplayBox{window=engine.monitors.main,fg_bg=style.root}
main_view(engine.ui.main_display) main_view(engine.ui.main_display)
util.nop()
end end
-- show flow view on flow monitor -- show flow view on flow monitor
if engine.monitors.flow ~= nil then if engine.monitors.flow ~= nil then
engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root} engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root}
flow_view(engine.ui.flow_display) flow_view(engine.ui.flow_display)
util.nop()
end end
-- show unit views on unit displays -- show unit views on unit displays
for idx, display in pairs(engine.monitors.unit_displays) do for idx, display in pairs(engine.monitors.unit_displays) do
engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root} engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
unit_view(engine.ui.unit_displays[idx], idx) unit_view(engine.ui.unit_displays[idx], idx)
util.nop()
end end
end) end)
@ -247,6 +254,11 @@ function renderer.close_ui()
-- clear unit monitors -- clear unit monitors
for _, monitor in ipairs(engine.monitors.unit_displays) do monitor.clear() end 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 -- re-draw dmesg
engine.dmesg_window.setVisible(true) engine.dmesg_window.setVisible(true)
engine.dmesg_window.redraw() engine.dmesg_window.redraw()
@ -383,12 +395,15 @@ function renderer.handle_resize(name)
engine.dmesg_window.setVisible(not engine.ui_ready) engine.dmesg_window.setVisible(not engine.ui_ready)
if engine.ui_ready then if engine.ui_ready then
local draw_start = util.time_ms()
local ok = pcall(function () local ok = pcall(function ()
ui.main_display = DisplayBox{window=device,fg_bg=style.root} ui.main_display = DisplayBox{window=device,fg_bg=style.root}
main_view(ui.main_display) main_view(ui.main_display)
end) end)
if not ok then if ok then
log_render("main view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
else
if ui.main_display then if ui.main_display then
ui.main_display.delete() ui.main_display.delete()
ui.main_display = nil ui.main_display = nil
@ -416,14 +431,15 @@ function renderer.handle_resize(name)
iocontrol.fp_monitor_state("flow", true) iocontrol.fp_monitor_state("flow", true)
if engine.ui_ready then if engine.ui_ready then
engine.dmesg_window.setVisible(false) local draw_start = util.time_ms()
local ok = pcall(function () local ok = pcall(function ()
ui.flow_display = DisplayBox{window=device,fg_bg=style.root} ui.flow_display = DisplayBox{window=device,fg_bg=style.root}
flow_view(ui.flow_display) flow_view(ui.flow_display)
end) end)
if not ok then if ok then
log_render("flow view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
else
if ui.flow_display then if ui.flow_display then
ui.flow_display.delete() ui.flow_display.delete()
ui.flow_display = nil ui.flow_display = nil
@ -453,14 +469,15 @@ function renderer.handle_resize(name)
iocontrol.fp_monitor_state(idx, true) iocontrol.fp_monitor_state(idx, true)
if engine.ui_ready then if engine.ui_ready then
engine.dmesg_window.setVisible(false) local draw_start = util.time_ms()
local ok = pcall(function () local ok = pcall(function ()
ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root} ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
unit_view(ui.unit_displays[idx], idx) unit_view(ui.unit_displays[idx], idx)
end) end)
if not ok then 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 if ui.unit_displays[idx] then
ui.unit_displays[idx].delete() ui.unit_displays[idx].delete()
ui.unit_displays[idx] = nil ui.unit_displays[idx] = nil

View File

@ -89,10 +89,11 @@ end
-- establish a new API session -- establish a new API session
---@nodiscard ---@nodiscard
---@param source_addr integer ---@param source_addr integer pocket computer ID
---@param version string ---@param i_seq_num integer initial (most recent) sequence number
---@param version string pocket version
---@return integer session_id ---@return integer session_id
function apisessions.establish_session(source_addr, version) function apisessions.establish_session(source_addr, i_seq_num, version)
---@class pkt_session_struct ---@class pkt_session_struct
local pkt_s = { local pkt_s = {
open = true, open = true,
@ -105,7 +106,7 @@ function apisessions.establish_session(source_addr, version)
local id = self.next_id local id = self.next_id
pkt_s.instance = pocket.new_session(id, source_addr, pkt_s.in_queue, pkt_s.out_queue, self.config.API_Timeout) pkt_s.instance = pocket.new_session(id, source_addr, i_seq_num, pkt_s.in_queue, pkt_s.out_queue, self.config.API_Timeout)
table.insert(self.sessions, pkt_s) table.insert(self.sessions, pkt_s)
local mt = { local mt = {

View File

@ -8,7 +8,7 @@ local iocontrol = require("coordinator.iocontrol")
local pocket = {} local pocket = {}
local PROTOCOL = comms.PROTOCOL local PROTOCOL = comms.PROTOCOL
-- local CRDN_TYPE = comms.CRDN_TYPE local CRDN_TYPE = comms.CRDN_TYPE
local MGMT_TYPE = comms.MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
-- retry time constants in ms -- retry time constants in ms
@ -32,16 +32,17 @@ local PERIODICS = {
---@nodiscard ---@nodiscard
---@param id integer session ID ---@param id integer session ID
---@param s_addr integer device source address ---@param s_addr integer device source address
---@param i_seq_num integer initial sequence number
---@param in_queue mqueue in message queue ---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue ---@param out_queue mqueue out message queue
---@param timeout number communications timeout ---@param timeout number communications timeout
function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
local log_header = "pkt_session(" .. id .. "): " local log_header = "pkt_session(" .. id .. "): "
local self = { local self = {
-- connection properties -- connection properties
seq_num = 0, seq_num = i_seq_num + 2, -- next after the establish approval was sent
r_seq_num = nil, r_seq_num = i_seq_num + 1,
connected = true, connected = true,
conn_watchdog = util.new_watchdog(timeout), conn_watchdog = util.new_watchdog(timeout),
last_rtt = 0, last_rtt = 0,
@ -73,18 +74,18 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
end end
-- send a CRDN packet -- send a CRDN packet
-----@param msg_type CRDN_TYPE ---@param msg_type CRDN_TYPE
-----@param msg table ---@param msg table
-- local function _send(msg_type, msg) local function _send(msg_type, msg)
-- local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
-- local c_pkt = comms.crdn_packet() local c_pkt = comms.crdn_packet()
-- c_pkt.make(msg_type, msg) c_pkt.make(msg_type, msg)
-- s_pkt.make(self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable()) s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable())
-- out_queue.push_packet(s_pkt) out_queue.push_packet(s_pkt)
-- self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
-- end end
-- send a SCADA management packet -- send a SCADA management packet
---@param msg_type MGMT_TYPE ---@param msg_type MGMT_TYPE
@ -104,13 +105,11 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
---@param pkt mgmt_frame|crdn_frame ---@param pkt mgmt_frame|crdn_frame
local function _handle_packet(pkt) local function _handle_packet(pkt)
-- check sequence number -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num ~= pkt.scada_frame.seq_num() then
self.r_seq_num = pkt.scada_frame.seq_num() log.warning(log_header .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return return
else else
self.r_seq_num = pkt.scada_frame.seq_num() self.r_seq_num = pkt.scada_frame.seq_num() + 1
end end
-- feed watchdog -- feed watchdog
@ -120,8 +119,46 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
if pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then if pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
---@cast pkt crdn_frame ---@cast pkt crdn_frame
local db = iocontrol.get_db()
-- handle packet by type -- handle packet by type
if pkt.type == nil then if 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.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 else
log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type) log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type)
end end
@ -149,6 +186,10 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
elseif pkt.type == MGMT_TYPE.CLOSE then elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session -- close the session
_close() _close()
elseif pkt.type == MGMT_TYPE.ESTABLISH then
-- something is wrong, kill the session
_close()
log.warning(log_header .. "terminated session due to an unexpected ESTABLISH packet")
else else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end end

View File

@ -7,32 +7,29 @@ require("/initenv").init_env()
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local network = require("scada-common.network") local network = require("scada-common.network")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core")
local configure = require("coordinator.configure") local configure = require("coordinator.configure")
local coordinator = require("coordinator.coordinator") local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local renderer = require("coordinator.renderer") local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder") local sounder = require("coordinator.sounder")
local threads = require("coordinator.threads")
local apisessions = require("coordinator.session.apisessions") local COORDINATOR_VERSION = "v1.5.6"
local COORDINATOR_VERSION = "v1.3.0"
local CHUNK_LOAD_DELAY_S = 30.0 local CHUNK_LOAD_DELAY_S = 30.0
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
local log_graphics = coordinator.log_graphics local log_render = coordinator.log_render
local log_sys = coordinator.log_sys local log_sys = coordinator.log_sys
local log_boot = coordinator.log_boot local log_boot = coordinator.log_boot
local log_comms = coordinator.log_comms local log_comms = coordinator.log_comms
local log_crypto = coordinator.log_crypto local log_crypto = coordinator.log_crypto
---------------------------------------- ----------------------------------------
@ -103,6 +100,7 @@ log.info("========================================")
println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<") println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<")
crash.set_env("coordinator", COORDINATOR_VERSION) crash.set_env("coordinator", COORDINATOR_VERSION)
crash.dbg_log_env()
---------------------------------------- ----------------------------------------
-- main application -- main application
@ -128,16 +126,58 @@ local function main()
-- lets get started! -- lets get started!
log.info("monitors ready, dmesg output incoming...") 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_sys("system start on " .. os.date("%c"))
log_boot("starting " .. COORDINATOR_VERSION) 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 -- setup alarm sounder subsystem
---------------------------------------- ----------------------------------------
local speaker = ppm.get_device("speaker") if smem_dev.speaker == nil then
if speaker == nil then
log_boot("annunciator alarm speaker not found") log_boot("annunciator alarm speaker not found")
println("startup> speaker not found") println("startup> speaker not found")
log.fatal("no annunciator alarm speaker found") log.fatal("no annunciator alarm speaker found")
@ -145,7 +185,7 @@ local function main()
else else
local sounder_start = util.time_ms() local sounder_start = util.time_ms()
log_boot("annunciator alarm speaker connected") log_boot("annunciator alarm speaker connected")
sounder.init(speaker, config.SpeakerVolume) sounder.init(smem_dev.speaker, config.SpeakerVolume)
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms") log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
log_sys("annunciator alarm configured") log_sys("annunciator alarm configured")
iocontrol.fp_has_speaker(true) iocontrol.fp_has_speaker(true)
@ -162,8 +202,7 @@ local function main()
end end
-- get the communications modem -- get the communications modem
local modem = ppm.get_wireless_modem() if smem_dev.modem == nil then
if modem == nil then
log_comms("wireless modem not found") log_comms("wireless modem not found")
println("startup> wireless modem not found") println("startup> wireless modem not found")
log.fatal("no wireless modem on startup") log.fatal("no wireless modem on startup")
@ -174,243 +213,54 @@ local function main()
end end
-- create connection watchdog -- create connection watchdog
local conn_watchdog = util.new_watchdog(config.SVR_Timeout) smem_sys.conn_watchdog = util.new_watchdog(config.SVR_Timeout)
conn_watchdog.cancel() smem_sys.conn_watchdog.cancel()
log.debug("startup> conn watchdog created") log.debug("startup> conn watchdog created")
-- create network interface then setup comms -- create network interface then setup comms
local nic = network.nic(modem) smem_sys.nic = network.nic(smem_dev.modem)
local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, conn_watchdog) smem_sys.coord_comms = coordinator.comms(COORDINATOR_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
log.debug("startup> comms init") log.debug("startup> comms init")
log_comms("comms initialized") 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() local fp_message
if not fp_ok then crd_state.fp_ok, fp_message = renderer.try_start_fp()
log_graphics(util.c("front panel UI error: ", fp_message)) if not crd_state.fp_ok then
log_render(util.c("front panel UI error: ", fp_message))
println_ts("front panel UI creation failed") println_ts("front panel UI creation failed")
log.fatal(util.c("front panel GUI render failed with error ", fp_message)) log.fatal(util.c("front panel GUI render failed with error ", fp_message))
return return
else log_graphics("front panel ready") end else log_render("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
---------------------------------------- ----------------------------------------
-- main event loop -- start system
---------------------------------------- ----------------------------------------
local link_failed = false -- init threads
local ui_ok = true local main_thread = threads.thread__main(__shared_memory)
local date_format = util.trinary(config.Time24Hour, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y") local render_thread = threads.thread__render(__shared_memory)
-- start clock log.info("startup> completed")
loop_clock.start()
log_sys("system started successfully") -- run threads
parallel.waitForAll(main_thread.p_exec, render_thread.p_exec)
-- 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 == "monitor_resize" then
local is_used, is_ok = renderer.handle_resize(param1)
if is_used then
log_sys(util.c("configured monitor ", param1, " resized, ", util.trinary(is_ok, "display still fits", "display no longer fits")))
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
renderer.close_ui() renderer.close_ui()
renderer.close_fp() renderer.close_fp()
sounder.stop() sounder.stop()
log_sys("system shutdown") log_sys("system shutdown")
if link_failed then println_ts("failed to connect to supervisor") end if crd_state.link_fail then println_ts("failed to connect to supervisor") end
if not ui_ok then println_ts("main UI creation failed") end if not crd_state.ui_ok then println_ts("main UI creation failed") end
-- close on error exit (such as UI error) -- 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") println_ts("exited")
log.info("exited") log.info("exited")

363
coordinator/threads.lua Normal file
View File

@ -0,0 +1,363 @@
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 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()
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,7 +1,7 @@
local style = require("coordinator.ui.style")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local core = require("graphics.core") local core = require("graphics.core")
local Rectangle = require("graphics.elements.rectangle") local Rectangle = require("graphics.elements.rectangle")
@ -35,10 +35,10 @@ local function new_view(root, x, y, ps)
temp.register(ps, "temperature", function (t) temp.update(db.temp_convert(t)) end) temp.register(ps, "temperature", function (t) temp.update(db.temp_convert(t)) end)
boil_r.register(ps, "boil_rate", boil_r.update) boil_r.register(ps, "boil_rate", boil_r.update)
TextBox{parent=boiler,text="H",x=2,y=5,height=1,width=1,fg_bg=text_fg} TextBox{parent=boiler,text="H",x=2,y=5,width=1,fg_bg=text_fg}
TextBox{parent=boiler,text="W",x=3,y=5,height=1,width=1,fg_bg=text_fg} TextBox{parent=boiler,text="W",x=3,y=5,width=1,fg_bg=text_fg}
TextBox{parent=boiler,text="S",x=27,y=5,height=1,width=1,fg_bg=text_fg} TextBox{parent=boiler,text="S",x=27,y=5,width=1,fg_bg=text_fg}
TextBox{parent=boiler,text="C",x=28,y=5,height=1,width=1,fg_bg=text_fg} TextBox{parent=boiler,text="C",x=28,y=5,width=1,fg_bg=text_fg}
local hcool = VerticalBar{parent=boiler,x=2,y=1,fg_bg=cpair(colors.orange,colors.gray),height=4,width=1} local hcool = VerticalBar{parent=boiler,x=2,y=1,fg_bg=cpair(colors.orange,colors.gray),height=4,width=1}
local water = VerticalBar{parent=boiler,x=3,y=1,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1} local water = VerticalBar{parent=boiler,x=3,y=1,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1}

View File

@ -1,5 +1,7 @@
local util = require("scada-common.util") local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
local core = require("graphics.core") local core = require("graphics.core")
@ -9,6 +11,7 @@ local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light")
local PowerIndicator = require("graphics.elements.indicators.power") local PowerIndicator = require("graphics.elements.indicators.power")
local StateIndicator = require("graphics.elements.indicators.state") local StateIndicator = require("graphics.elements.indicators.state")
local VerticalBar = require("graphics.elements.indicators.vbar") local VerticalBar = require("graphics.elements.indicators.vbar")
@ -26,60 +29,69 @@ local ALIGN = core.ALIGN
---@param ps psil ps interface ---@param ps psil ps interface
---@param id number? matrix ID ---@param id number? matrix ID
local function new_view(root, x, y, data, ps, id) local function new_view(root, x, y, data, ps, id)
local label_fg = style.theme.label_fg
local text_fg = style.theme.text_fg local text_fg = style.theme.text_fg
local lu_col = style.lu_colors local lu_col = style.lu_colors
local ind_yel = style.ind_yel
local ind_wht = style.ind_wht
local db = iocontrol.get_db()
local title = "INDUCTION MATRIX" local title = "INDUCTION MATRIX"
if type(id) == "number" then title = title .. id end if type(id) == "number" then title = title .. id end
local matrix = Div{parent=root,fg_bg=style.root,width=33,height=24,x=x,y=y} local matrix = Div{parent=root,fg_bg=style.root,width=33,height=24,x=x,y=y}
local cutout_fg_bg = cpair(style.theme.bg, colors.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,height=1,x=1,y=1,fg_bg=cutout_fg_bg} TextBox{parent=matrix,text=" ",width=33,x=1,y=1,fg_bg=cutout_fg_bg}
TextBox{parent=matrix,text=title,alignment=ALIGN.CENTER,width=33,height=1,x=1,y=2,fg_bg=cutout_fg_bg} TextBox{parent=matrix,text=title,alignment=ALIGN.CENTER,width=33,x=1,y=2,fg_bg=cutout_fg_bg}
local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3} local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14} local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
local 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} 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 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} 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 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} 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 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} 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 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} 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_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} 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 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} local trans_cap = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Max I/O: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
status.register(ps, "computed_status", status.update) status.register(ps, "computed_status", status.update)
energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end) capacity.register(ps, "max_energy", function (val) capacity.update(db.energy_convert(val)) end)
capacity.register(ps, "max_energy", function (val) capacity.update(util.joules_to_fe(val)) end) energy.register(ps, "energy", function (val) energy.update(db.energy_convert(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)
avg_chg.register(ps, "avg_charge", avg_chg.update) avg_chg.register(ps, "avg_charge", avg_chg.update)
input.register(ps, "last_input", function (val) input.update(db.energy_convert(val)) end)
avg_in.register(ps, "avg_inflow", avg_in.update) avg_in.register(ps, "avg_inflow", avg_in.update)
output.register(ps, "last_output", function (val) output.update(db.energy_convert(val)) end)
avg_out.register(ps, "avg_outflow", avg_out.update) avg_out.register(ps, "avg_outflow", avg_out.update)
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(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} local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill: ",format="%7.2f",unit="%",value=0,width=20,fg_bg=text_fg}
local cells = DataIndicator{parent=rect,x=11,y=13,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg}
local cells = DataIndicator{parent=rect,x=11,y=14,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}
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}
TextBox{parent=rect,text="Transfer Capacity",x=11,y=17,height=1,width=17,fg_bg=style.theme.label_fg}
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}
fill.register(ps, "energy_fill", function (val) fill.update(val * 100) end)
cells.register(ps, "cells", cells.update) cells.register(ps, "cells", cells.update)
providers.register(ps, "providers", providers.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 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 in_cap = VerticalBar{parent=rect,x=7,y=12,fg_bg=cpair(colors.red,colors.gray),height=7,width=1}
local out_cap = VerticalBar{parent=rect,x=9,y=12,fg_bg=cpair(colors.blue,colors.gray),height=7,width=1} local out_cap = VerticalBar{parent=rect,x=9,y=12,fg_bg=cpair(colors.blue,colors.gray),height=7,width=1}
TextBox{parent=rect,text="FILL",x=2,y=20,height=1,width=4,fg_bg=text_fg} TextBox{parent=rect,text="FILL I/O",x=2,y=20,width=8,fg_bg=label_fg}
TextBox{parent=rect,text="I/O",x=7,y=20,height=1,width=3,fg_bg=text_fg}
local function calc_saturation(val) local function calc_saturation(val)
if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then
@ -90,6 +102,49 @@ local function new_view(root, x, y, data, ps, id)
charge.register(ps, "energy_fill", charge.update) charge.register(ps, "energy_fill", charge.update)
in_cap.register(ps, "last_input", function (val) in_cap.update(calc_saturation(val)) end) in_cap.register(ps, "last_input", function (val) in_cap.update(calc_saturation(val)) end)
out_cap.register(ps, "last_output", function (val) out_cap.update(calc_saturation(val)) end) out_cap.register(ps, "last_output", function (val) out_cap.update(calc_saturation(val)) end)
local eta = TextBox{parent=rect,x=11,y=20,width=20,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 end
return new_view return new_view

View File

@ -34,18 +34,18 @@ local function init(parent, id)
local ps_prefix = "pkt_" .. id .. "_" local ps_prefix = "pkt_" .. id .. "_"
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=s_hi_box} TextBox{parent=entry,x=1,y=1,text="",width=8,fg_bg=s_hi_box}
local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,height=1,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)} local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)}
TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=s_hi_box} TextBox{parent=entry,x=1,y=3,text="",width=8,fg_bg=s_hi_box}
pkt_addr.register(ps, ps_prefix .. "addr", pkt_addr.set_value) pkt_addr.register(ps, ps_prefix .. "addr", pkt_addr.set_value)
TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1} TextBox{parent=entry,x=10,y=2,text="FW:",width=3}
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=label_fg} local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,fg_bg=label_fg}
pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value) pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value)
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1} TextBox{parent=entry,x=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} 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,height=1,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", pkt_rtt.update)
pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor) pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor)

View File

@ -56,8 +56,10 @@ local function new_view(root, x, y)
local blk_brn = cpair(colors.black, colors.brown) local blk_brn = cpair(colors.black, colors.brown)
local blk_pur = cpair(colors.black, colors.purple) local blk_pur = cpair(colors.black, colors.purple)
local facility = iocontrol.get_db().facility local db = iocontrol.get_db()
local units = iocontrol.get_db().units
local facility = db.facility
local units = db.units
local main = Div{parent=root,width=128,height=24,x=x,y=y} local main = Div{parent=root,width=128,height=24,x=x,y=y}
@ -105,11 +107,11 @@ local function new_view(root, x, y)
fac_rad_h.register(facility.ps, "as_radiation", fac_rad_h.update) fac_rad_h.register(facility.ps, "as_radiation", fac_rad_h.update)
gen_fault.register(facility.ps, "as_gen_fault", gen_fault.update) gen_fault.register(facility.ps, "as_gen_fault", gen_fault.update)
TextBox{parent=main,y=23,text="Radiation",height=1,width=13,fg_bg=style.label} TextBox{parent=main,y=23,text="Radiation",width=13,fg_bg=style.label}
local radiation = RadIndicator{parent=main,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=s_field} local radiation = RadIndicator{parent=main,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=s_field}
radiation.register(facility.ps, "radiation", radiation.update) radiation.register(facility.ps, "radiation", radiation.update)
TextBox{parent=main,x=15,y=23,text="Linked RTUs",height=1,width=11,fg_bg=style.label} TextBox{parent=main,x=15,y=23,text="Linked RTUs",width=11,fg_bg=style.label}
local rtu_count = DataIndicator{parent=main,x=15,y=24,label="",format="%11d",value=0,lu_colors=lu_cpair,width=11,fg_bg=s_field} local rtu_count = DataIndicator{parent=main,x=15,y=24,label="",format="%11d",value=0,lu_colors=lu_cpair,width=11,fg_bg=s_field}
rtu_count.register(facility.ps, "rtu_count", rtu_count.update) rtu_count.register(facility.ps, "rtu_count", rtu_count.update)
@ -141,22 +143,22 @@ local function new_view(root, x, y)
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=s_hi_box} local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=s_hi_box}
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled} local c_target = 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="MFE",fg_bg=style.theme.label_fg} TextBox{parent=chg_target,x=18,y=2,text="M"..db.energy_label,fg_bg=style.theme.label_fg}
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=black,width=23,fg_bg=blk_brn} local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="M"..db.energy_label,commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
c_target.register(facility.ps, "process_charge_target", c_target.set_value) c_target.register(facility.ps, "process_charge_target", c_target.set_value)
cur_charge.register(facility.induction_ps_tbl[1], "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} local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur}
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2} TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=s_hi_box} local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=s_hi_box}
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled} local g_target = 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="kFE/t",fg_bg=style.theme.label_fg} TextBox{parent=gen_target,x=18,y=2,text="k"..db.energy_label.."/t",fg_bg=style.theme.label_fg}
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn} local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="k"..db.energy_label.."/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
g_target.register(facility.ps, "process_gen_target", g_target.set_value) g_target.register(facility.ps, "process_gen_target", g_target.set_value)
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end) cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(db.energy_convert(j) / 1000)) end)
----------------- -----------------
-- unit limits -- -- unit limits --
@ -190,7 +192,7 @@ local function new_view(root, x, y)
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=s_hi_box} local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=s_hi_box}
local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled,fg_bg=lim_fg_bg} local lim = 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,height=1,fg_bg=label_fg} TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,fg_bg=label_fg}
local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(cur_lu,cur_lu),width=14,fg_bg=cur_fg_bg} local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(cur_lu,cur_lu),width=14,fg_bg=cur_fg_bg}
@ -249,8 +251,8 @@ local function new_view(root, x, y)
mode.register(facility.ps, "process_mode", mode.set_value) mode.register(facility.ps, "process_mode", mode.set_value)
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg} local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg} local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.white)} local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.white)}
stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value) stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value)
stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value) stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value)
@ -262,7 +264,10 @@ local function new_view(root, x, y)
local limits = {} local limits = {}
for i = 1, #rate_limits do limits[i] = rate_limits[i].get_value() end for i = 1, #rate_limits do limits[i] = rate_limits[i].get_value() end
process.save(mode.get_value(), b_target.get_value(), c_target.get_value(), g_target.get_value(), limits) process.save(mode.get_value(), b_target.get_value(),
db.energy_convert_to_fe(c_target.get_value()),
db.energy_convert_to_fe(g_target.get_value()),
limits)
end end
-- start automatic control after saving process control settings -- start automatic control after saving process control settings
@ -320,7 +325,7 @@ local function new_view(root, x, y)
for i = 1, facility.num_units do for i = 1, facility.num_units do
local unit = units[i] ---@type ioctl_unit local unit = units[i] ---@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 a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=ind_wht}
local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.waste.states_abbrv,value=1,min_width=6} local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.waste.states_abbrv,value=1,min_width=6}
@ -332,8 +337,8 @@ local function new_view(root, x, y)
local cutout_fg_bg = cpair(style.theme.bg, colors.brown) local cutout_fg_bg = cpair(style.theme.bg, colors.brown)
TextBox{parent=waste_sel,text=" ",width=21,height=1,x=1,y=1,fg_bg=cutout_fg_bg} TextBox{parent=waste_sel,text=" ",width=21,x=1,y=1,fg_bg=cutout_fg_bg}
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,height=1,x=1,y=2,fg_bg=cutout_fg_bg} TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,x=1,y=2,fg_bg=cutout_fg_bg}
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3} local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17} local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17}
@ -341,31 +346,25 @@ local function new_view(root, x, y)
status.register(facility.ps, "current_waste_product", status.update) status.register(facility.ps, "current_waste_product", status.update)
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.brown} local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),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,style.theme.checkbox_bg)}
waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value) 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) 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_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)}
local pu_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=s_field,width=17}
TextBox{parent=rect,x=2,y=14,text="Polonium Rate",height=1,width=17,fg_bg=style.label} TextBox{parent=rect,x=2,y=12,height=3,text="Switch to Pu when SNAs cannot keep up with waste.",fg_bg=style.label}
local po_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=s_field,width=17}
TextBox{parent=rect,x=2,y=17,text="Antimatter Rate",height=1,width=17,fg_bg=style.label} local lc_sps = Checkbox{parent=rect,x=2,y=16,label="Low Charge SPS",callback=process.set_sps_low_power,box_fg_bg=cpair(colors.brown,style.theme.checkbox_bg)}
local am_rate = DataIndicator{parent=rect,x=2,label="",unit="\xb5B/t",format="%12d",value=0,lu_colors=lu_cpair,fg_bg=s_field,width=17}
pu_rate.register(facility.ps, "pu_rate", pu_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}
po_rate.register(facility.ps, "po_rate", po_rate.update)
am_rate.register(facility.ps, "am_rate", am_rate.update)
local sna_count = DataIndicator{parent=rect,x=2,y=20,label="Linked SNAs:",format="%4d",value=0,lu_colors=lu_cpair,width=17} 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)
sna_count.register(facility.ps, "sna_count", sna_count.update)
end end
return new_view return new_view

View File

@ -41,10 +41,10 @@ local function new_view(root, x, y, ps)
local reactor_fills = Rectangle{parent=root,border=border(1, colors.gray, true),width=24,height=7,x=(x + 29),y=y} local reactor_fills = Rectangle{parent=root,border=border(1, colors.gray, true),width=24,height=7,x=(x + 29),y=y}
TextBox{parent=reactor_fills,text="FUEL",x=2,y=1,height=1,fg_bg=text_fg} TextBox{parent=reactor_fills,text="FUEL",x=2,y=1,fg_bg=text_fg}
TextBox{parent=reactor_fills,text="COOL",x=2,y=2,height=1,fg_bg=text_fg} TextBox{parent=reactor_fills,text="COOL",x=2,y=2,fg_bg=text_fg}
TextBox{parent=reactor_fills,text="HCOOL",x=2,y=4,height=1,fg_bg=text_fg} TextBox{parent=reactor_fills,text="HCOOL",x=2,y=4,fg_bg=text_fg}
TextBox{parent=reactor_fills,text="WASTE",x=2,y=5,height=1,fg_bg=text_fg} TextBox{parent=reactor_fills,text="WASTE",x=2,y=5,fg_bg=text_fg}
local fuel = HorizontalBar{parent=reactor_fills,x=8,y=1,show_percent=true,bar_fg_bg=cpair(style.theme.fuel_color,colors.gray),height=1,width=14} local fuel = HorizontalBar{parent=reactor_fills,x=8,y=1,show_percent=true,bar_fg_bg=cpair(style.theme.fuel_color,colors.gray),height=1,width=14}
local ccool = HorizontalBar{parent=reactor_fills,x=8,y=2,show_percent=true,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=14} local ccool = HorizontalBar{parent=reactor_fills,x=8,y=2,show_percent=true,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=14}

View File

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

View File

@ -71,7 +71,7 @@ local function init(parent, id)
local b_ps = unit.boiler_ps_tbl local b_ps = unit.boiler_ps_tbl
local t_ps = unit.turbine_ps_tbl local t_ps = unit.turbine_ps_tbl
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,height=1,fg_bg=style.theme.header} TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,fg_bg=style.theme.header}
----------------------------- -----------------------------
-- main stats and core map -- -- main stats and core map --
@ -81,20 +81,20 @@ local function init(parent, id)
core_map.register(u_ps, "temp", core_map.update) core_map.register(u_ps, "temp", core_map.update)
core_map.register(u_ps, "size", function (s) core_map.resize(s[1], s[2]) end) core_map.register(u_ps, "size", function (s) core_map.resize(s[1], s[2]) end)
TextBox{parent=main,x=12,y=22,text="Heating Rate",height=1,width=12,fg_bg=style.label} TextBox{parent=main,x=12,y=22,text="Heating Rate",width=12,fg_bg=style.label}
local heating_r = DataIndicator{parent=main,x=12,label="",format="%14.0f",value=0,unit="mB/t",commas=true,lu_colors=lu_cpair,width=19,fg_bg=s_field} local heating_r = DataIndicator{parent=main,x=12,label="",format="%14.0f",value=0,unit="mB/t",commas=true,lu_colors=lu_cpair,width=19,fg_bg=s_field}
heating_r.register(u_ps, "heating_rate", heating_r.update) heating_r.register(u_ps, "heating_rate", heating_r.update)
TextBox{parent=main,x=12,y=25,text="Commanded Burn Rate",height=1,width=19,fg_bg=style.label} TextBox{parent=main,x=12,y=25,text="Commanded Burn Rate",width=19,fg_bg=style.label}
local burn_r = DataIndicator{parent=main,x=12,label="",format="%14.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=19,fg_bg=s_field} local burn_r = DataIndicator{parent=main,x=12,label="",format="%14.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=19,fg_bg=s_field}
burn_r.register(u_ps, "burn_rate", burn_r.update) burn_r.register(u_ps, "burn_rate", burn_r.update)
TextBox{parent=main,text="F",x=2,y=22,width=1,height=1,fg_bg=style.label} TextBox{parent=main,text="F",x=2,y=22,width=1,fg_bg=style.label}
TextBox{parent=main,text="C",x=4,y=22,width=1,height=1,fg_bg=style.label} TextBox{parent=main,text="C",x=4,y=22,width=1,fg_bg=style.label}
TextBox{parent=main,text="\x1a",x=6,y=24,width=1,height=1,fg_bg=style.label} TextBox{parent=main,text="\x1a",x=6,y=24,width=1,fg_bg=style.label}
TextBox{parent=main,text="\x1a",x=6,y=25,width=1,height=1,fg_bg=style.label} TextBox{parent=main,text="\x1a",x=6,y=25,width=1,fg_bg=style.label}
TextBox{parent=main,text="H",x=8,y=22,width=1,height=1,fg_bg=style.label} TextBox{parent=main,text="H",x=8,y=22,width=1,fg_bg=style.label}
TextBox{parent=main,text="W",x=10,y=22,width=1,height=1,fg_bg=style.label} TextBox{parent=main,text="W",x=10,y=22,width=1,fg_bg=style.label}
local fuel = VerticalBar{parent=main,x=2,y=23,fg_bg=cpair(style.theme.fuel_color,colors.gray),height=4,width=1} local fuel = VerticalBar{parent=main,x=2,y=23,fg_bg=cpair(style.theme.fuel_color,colors.gray),height=4,width=1}
local ccool = VerticalBar{parent=main,x=4,y=23,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1} local ccool = VerticalBar{parent=main,x=4,y=23,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1}
@ -122,20 +122,20 @@ local function init(parent, id)
end end
end) end)
TextBox{parent=main,x=32,y=22,text="Core Temp",height=1,width=9,fg_bg=style.label} TextBox{parent=main,x=32,y=22,text="Core Temp",width=9,fg_bg=style.label}
local fmt = util.trinary(string.len(db.temp_label) == 2, "%10.2f", "%11.2f") local fmt = util.trinary(string.len(db.temp_label) == 2, "%10.2f", "%11.2f")
local core_temp = DataIndicator{parent=main,x=32,label="",format=fmt,value=0,commas=true,unit=db.temp_label,lu_colors=lu_cpair,width=13,fg_bg=s_field} local core_temp = DataIndicator{parent=main,x=32,label="",format=fmt,value=0,commas=true,unit=db.temp_label,lu_colors=lu_cpair,width=13,fg_bg=s_field}
core_temp.register(u_ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end) core_temp.register(u_ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end)
TextBox{parent=main,x=32,y=25,text="Burn Rate",height=1,width=9,fg_bg=style.label} TextBox{parent=main,x=32,y=25,text="Burn Rate",width=9,fg_bg=style.label}
local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=s_field} local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=s_field}
act_burn_r.register(u_ps, "act_burn_rate", act_burn_r.update) act_burn_r.register(u_ps, "act_burn_rate", act_burn_r.update)
TextBox{parent=main,x=32,y=28,text="Damage",height=1,width=6,fg_bg=style.label} TextBox{parent=main,x=32,y=28,text="Damage",width=6,fg_bg=style.label}
local damage_p = DataIndicator{parent=main,x=32,label="",format="%11.0f",value=0,unit="%",lu_colors=lu_cpair,width=13,fg_bg=s_field} local damage_p = DataIndicator{parent=main,x=32,label="",format="%11.0f",value=0,unit="%",lu_colors=lu_cpair,width=13,fg_bg=s_field}
damage_p.register(u_ps, "damage", damage_p.update) damage_p.register(u_ps, "damage", damage_p.update)
TextBox{parent=main,x=32,y=31,text="Radiation",height=1,width=21,fg_bg=style.label} TextBox{parent=main,x=32,y=31,text="Radiation",width=21,fg_bg=style.label}
local radiation = RadIndicator{parent=main,x=32,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=s_field} local radiation = RadIndicator{parent=main,x=32,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=s_field}
radiation.register(u_ps, "radiation", radiation.update) radiation.register(u_ps, "radiation", radiation.update)
@ -144,8 +144,8 @@ local function init(parent, id)
------------------- -------------------
local u_stat = Rectangle{parent=main,border=border(1,colors.gray,true),thin=true,width=33,height=4,x=46,y=3,fg_bg=bw_fg_bg} local u_stat = Rectangle{parent=main,border=border(1,colors.gray,true),thin=true,width=33,height=4,x=46,y=3,fg_bg=bw_fg_bg}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg} local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=ALIGN.CENTER,fg_bg=gry_wht} local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,alignment=ALIGN.CENTER,fg_bg=gry_wht}
stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value) stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value)
stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value) stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value)
@ -183,7 +183,7 @@ local function init(parent, id)
local rad_wrn = IndicatorLight{parent=annunciator,label="Radiation Warning",colors=ind_yel} 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_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_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_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_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=ind_yel}
local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=ind_yel} local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=ind_yel}
@ -205,7 +205,7 @@ local function init(parent, id)
-- RPS annunciator panel -- RPS annunciator panel
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=8} TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,x=46,y=8}
local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9} local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9}
local rps_annunc = Div{parent=rps,width=31,height=10,x=2,y=1} local rps_annunc = Div{parent=rps,width=31,height=10,x=2,y=1}
@ -233,7 +233,7 @@ local function init(parent, id)
-- cooling annunciator panel -- cooling annunciator panel
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=22} TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,x=46,y=22}
local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23} local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23}
local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1} local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1}
local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7} local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7}
@ -265,11 +265,11 @@ local function init(parent, id)
if unit.num_boilers > 0 then if unit.num_boilers > 0 then
if available_space > 0 then _add_space() end if available_space > 0 then _add_space() end
TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,x=1,text="B1",width=2,fg_bg=hc_text}
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red} local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
b1_wll.register(b_ps[1], "WaterLevelLow", b1_wll.update) b1_wll.register(b_ps[1], "WaterLevelLow", b1_wll.update)
TextBox{parent=rcs_tags,text="B1",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="B1",width=2,fg_bg=hc_text}
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel} local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
b1_hr.register(b_ps[1], "HeatingRateLow", b1_hr.update) b1_hr.register(b_ps[1], "HeatingRateLow", b1_hr.update)
end end
@ -281,11 +281,11 @@ local function init(parent, id)
_add_space() _add_space()
end end
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="B2",width=2,fg_bg=hc_text}
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red} local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
b2_wll.register(b_ps[2], "WaterLevelLow", b2_wll.update) b2_wll.register(b_ps[2], "WaterLevelLow", b2_wll.update)
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="B2",width=2,fg_bg=hc_text}
local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel} local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
b2_hr.register(b_ps[2], "HeatingRateLow", b2_hr.update) b2_hr.register(b_ps[2], "HeatingRateLow", b2_hr.update)
end end
@ -294,19 +294,19 @@ local function init(parent, id)
if available_space > 1 then _add_space() end if available_space > 1 then _add_space() end
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd} local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
t1_sdo.register(t_ps[1], "SteamDumpOpen", t1_sdo.update) t1_sdo.register(t_ps[1], "SteamDumpOpen", t1_sdo.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red} local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
t1_tos.register(t_ps[1], "TurbineOverSpeed", t1_tos.update) t1_tos.register(t_ps[1], "TurbineOverSpeed", t1_tos.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS} local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
t1_gtrp.register(t_ps[1], "GeneratorTrip", t1_gtrp.update) t1_gtrp.register(t_ps[1], "GeneratorTrip", t1_gtrp.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T1",width=2,fg_bg=hc_text}
local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS} local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
t1_trp.register(t_ps[1], "TurbineTrip", t1_trp.update) t1_trp.register(t_ps[1], "TurbineTrip", t1_trp.update)
@ -315,19 +315,19 @@ local function init(parent, id)
_add_space() _add_space()
end end
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd} local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
t2_sdo.register(t_ps[2], "SteamDumpOpen", t2_sdo.update) t2_sdo.register(t_ps[2], "SteamDumpOpen", t2_sdo.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red} local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
t2_tos.register(t_ps[2], "TurbineOverSpeed", t2_tos.update) t2_tos.register(t_ps[2], "TurbineOverSpeed", t2_tos.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS} local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
t2_gtrp.register(t_ps[2], "GeneratorTrip", t2_gtrp.update) t2_gtrp.register(t_ps[2], "GeneratorTrip", t2_gtrp.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T2",width=2,fg_bg=hc_text}
local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS} local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
t2_trp.register(t_ps[2], "TurbineTrip", t2_trp.update) t2_trp.register(t_ps[2], "TurbineTrip", t2_trp.update)
end end
@ -335,23 +335,25 @@ local function init(parent, id)
if unit.num_turbines > 2 then if unit.num_turbines > 2 then
if available_space > 3 then _add_space() end if available_space > 3 then _add_space() end
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd} local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=ind_bkg,c2=ind_yel.fgd,c3=ind_red.fgd}
t3_sdo.register(t_ps[3], "SteamDumpOpen", t3_sdo.update) t3_sdo.register(t_ps[3], "SteamDumpOpen", t3_sdo.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red} local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
t3_tos.register(t_ps[3], "TurbineOverSpeed", t3_tos.update) t3_tos.register(t_ps[3], "TurbineOverSpeed", t3_tos.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS} local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
t3_gtrp.register(t_ps[3], "GeneratorTrip", t3_gtrp.update) t3_gtrp.register(t_ps[3], "GeneratorTrip", t3_gtrp.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=hc_text} TextBox{parent=rcs_tags,text="T3",width=2,fg_bg=hc_text}
local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS} local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update) t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update)
end end
util.nop()
---------------------- ----------------------
-- reactor controls -- -- reactor controls --
---------------------- ----------------------
@ -392,7 +394,7 @@ local function init(parent, id)
reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end) reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=48} TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=ALIGN.CENTER,width=33,x=46,y=48}
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49} local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1} local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1}
@ -472,15 +474,15 @@ local function init(parent, id)
-- color tags -- color tags
TextBox{parent=alarm_panel,x=5,y=13,text="\x95",width=1,height=1,fg_bg=cpair(s_hi_bright.bkg,colors.cyan)} TextBox{parent=alarm_panel,x=5,y=13,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.cyan)}
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,height=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)} TextBox{parent=alarm_panel,x=5,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)}
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,height=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)} TextBox{parent=alarm_panel,x=5,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)}
-------------------------------- --------------------------------
-- automatic control settings -- -- automatic control settings --
-------------------------------- --------------------------------
TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,height=1,x=32,y=36} TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,x=32,y=36}
local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37} local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37}
local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1} local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1}
@ -497,8 +499,8 @@ local function init(parent, id)
auto_div.line_break() auto_div.line_break()
TextBox{parent=auto_div,text="Prio. Group",height=1,width=11,fg_bg=style.label} TextBox{parent=auto_div,text="Prio. Group",width=11,fg_bg=style.label}
local auto_grp = TextBox{parent=auto_div,text="Manual",height=1,width=11,fg_bg=s_field} local auto_grp = TextBox{parent=auto_div,text="Manual",width=11,fg_bg=s_field}
auto_grp.register(u_ps, "auto_group", auto_grp.set_value) auto_grp.register(u_ps, "auto_group", auto_grp.set_value)

View File

@ -73,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} local reactor = Rectangle{parent=root,x=1,y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=reactor,y=1,text="FISSION REACTOR",alignment=ALIGN.CENTER,height=1} TextBox{parent=reactor,y=1,text="FISSION REACTOR",alignment=ALIGN.CENTER}
TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=ALIGN.CENTER,height=1} TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=ALIGN.CENTER}
TextBox{parent=root,x=19,y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray} TextBox{parent=root,x=19,y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
TextBox{parent=root,x=3,y=5,text="\x19",width=1,height=1,fg_bg=lg_gray} TextBox{parent=root,x=3,y=5,text="\x19",width=1,fg_bg=lg_gray}
local rc_pipes = {} local rc_pipes = {}
@ -113,8 +113,8 @@ local function make(parent, x, y, wide, unit)
hc_rate.register(unit.unit_ps, "heating_rate", hc_rate.update) hc_rate.register(unit.unit_ps, "heating_rate", hc_rate.update)
local boiler = Rectangle{parent=root,x=_wide(47,40),y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray} local boiler = Rectangle{parent=root,x=_wide(47,40),y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=boiler,y=1,text="THERMO-ELECTRIC",alignment=ALIGN.CENTER,height=1} TextBox{parent=boiler,y=1,text="THERMO-ELECTRIC",alignment=ALIGN.CENTER}
TextBox{parent=boiler,y=3,text=util.trinary(unit.num_boilers>1,"BOILERS","BOILER"),alignment=ALIGN.CENTER,height=1} TextBox{parent=boiler,y=3,text=util.trinary(unit.num_boilers>1,"BOILERS","BOILER"),alignment=ALIGN.CENTER}
TextBox{parent=root,x=_wide(47,40),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray} TextBox{parent=root,x=_wide(47,40),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
TextBox{parent=root,x=_wide(65,58),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray} TextBox{parent=root,x=_wide(65,58),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
@ -132,13 +132,13 @@ local function make(parent, x, y, wide, unit)
end end
local turbine = Rectangle{parent=root,x=_wide(93,79),y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray} local turbine = Rectangle{parent=root,x=_wide(93,79),y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=turbine,y=1,text="STEAM TURBINE",alignment=ALIGN.CENTER,height=1} TextBox{parent=turbine,y=1,text="STEAM TURBINE",alignment=ALIGN.CENTER}
TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=ALIGN.CENTER,height=1} TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=ALIGN.CENTER}
TextBox{parent=root,x=_wide(93,79),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray} TextBox{parent=root,x=_wide(93,79),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
for i = 1, unit.num_turbines do for i = 1, unit.num_turbines do
local ry = 1 + (2 * (i - 1)) + prv_yo local ry = 1 + (2 * (i - 1)) + prv_yo
TextBox{parent=root,x=_wide(125,103),y=ry,text="\x10\x11\x7f",fg_bg=text_c,width=3,height=1} TextBox{parent=root,x=_wide(125,103),y=ry,text="\x10\x11\x7f",fg_bg=text_c,width=3}
local state = TriIndicatorLight{parent=root,x=_wide(129,107),y=ry,label=v_names[i+4],c1=style.ind_bkg,c2=style.ind_yel.fgd,c3=style.ind_red.fgd} local state = TriIndicatorLight{parent=root,x=_wide(129,107),y=ry,label=v_names[i+4],c1=style.ind_bkg,c2=style.ind_yel.fgd,c3=style.ind_red.fgd}
state.register(unit.turbine_ps_tbl[i], "SteamDumpOpen", state.update) state.register(unit.turbine_ps_tbl[i], "SteamDumpOpen", state.update)
end end
@ -172,7 +172,7 @@ local function make(parent, x, y, wide, unit)
PipeNetwork{parent=waste,x=1,y=1,pipes=waste_pipes,bg=style.theme.bg} PipeNetwork{parent=waste,x=1,y=1,pipes=waste_pipes,bg=style.theme.bg}
local function _valve(vx, vy, n) local function _valve(vx, vy, n)
TextBox{parent=waste,x=vx,y=vy,text="\x10\x11",fg_bg=text_c,width=2,height=1} TextBox{parent=waste,x=vx,y=vy,text="\x10\x11",fg_bg=text_c,width=2}
local conn = IndicatorLight{parent=waste,x=vx-3,y=vy+1,label=v_names[n],colors=ind_grn} local conn = IndicatorLight{parent=waste,x=vx-3,y=vy+1,label=v_names[n],colors=ind_grn}
local open = IndicatorLight{parent=waste,x=vx-3,y=vy+2,label="OPEN",colors=ind_wht} local open = IndicatorLight{parent=waste,x=vx-3,y=vy+2,label="OPEN",colors=ind_wht}
conn.register(unit.unit_ps, util.c("V_", v_fields[n], "_conn"), conn.update) conn.register(unit.unit_ps, util.c("V_", v_fields[n], "_conn"), conn.update)
@ -181,8 +181,8 @@ local function make(parent, x, y, wide, unit)
local function _machine(mx, my, name) local function _machine(mx, my, name)
local l = string.len(name) + 2 local l = string.len(name) + 2
TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=ALIGN.CENTER,fg_bg=cpair(style.theme.bg,style.theme.header.bkg),width=l,height=1} TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=ALIGN.CENTER,fg_bg=cpair(style.theme.bg,style.theme.header.bkg),width=l}
TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=ALIGN.CENTER,fg_bg=style.theme.header,width=l,height=1} TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=ALIGN.CENTER,fg_bg=style.theme.header,width=l}
end end
local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field} local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
@ -209,7 +209,7 @@ local function make(parent, x, y, wide, unit)
_machine(_wide(97, 83), 4, "PRC [Po] \x1a"); _machine(_wide(97, 83), 4, "PRC [Po] \x1a");
_machine(_wide(116, 94), 6, "SPENT WASTE \x1b") _machine(_wide(116, 94), 6, "SPENT WASTE \x1b")
TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=style.theme.highlight_box_bright} local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=style.theme.highlight_box_bright}
local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn} local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn}
local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c_d,label="CNT",unit="",format="%2d",value=0,width=7} local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c_d,label="CNT",unit="",format="%2d",value=0,width=7}

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} local root = Div{parent=parent,x=x,y=y,width=80,height=height}
-- unit header message -- unit header message
TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,height=1,fg_bg=style.theme.header} TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,fg_bg=style.theme.header}
------------- -------------
-- REACTOR -- -- REACTOR --

View File

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

View File

@ -39,7 +39,7 @@ local led_grn = style.led_grn
local function init(panel, num_units) local function init(panel, num_units)
local ps = iocontrol.get_db().fp.ps local ps = iocontrol.get_db().fp.ps
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,height=1,fg_bg=style.fp_theme.header} TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,fg_bg=style.fp_theme.header}
local page_div = Div{parent=panel,x=1,y=3} local page_div = Div{parent=panel,x=1,y=3}
@ -61,12 +61,12 @@ local function init(panel, num_units)
local modem = LED{parent=system,label="MODEM",colors=led_grn} local modem = LED{parent=system,label="MODEM",colors=led_grn}
if not style.colorblind then if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}} 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.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(ps, "link_state", network.update) network.register(ps, "link_state", network.update)
else else
local nt_lnk = LEDPair{parent=system,label="NT LINKED",off=colors.red_off,c1=colors.red,c2=colors.green} 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=colors.red_off,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) nt_lnk.register(ps, "link_state", function (state)
local value = 2 local value = 2
@ -100,9 +100,17 @@ local function init(panel, num_units)
local speaker = LED{parent=system,label="SPEAKER",colors=led_grn} local speaker = LED{parent=system,label="SPEAKER",colors=led_grn}
speaker.register(ps, "has_speaker", speaker.update) 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 ---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID()) local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=style.fp.disabled_fg} TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=style.fp.disabled_fg}
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2} local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
@ -124,8 +132,8 @@ local function init(panel, num_units)
-- --
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg} local about = Div{parent=main_page,width=15,height=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",alignment=ALIGN.LEFT,height=1} 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",alignment=ALIGN.LEFT,height=1} 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) fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)

View File

@ -2,6 +2,8 @@
-- Main SCADA Coordinator GUI -- Main SCADA Coordinator GUI
-- --
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
@ -27,10 +29,10 @@ local function init(main)
local units = iocontrol.get_db().units local units = iocontrol.get_db().units
-- window header message -- window header message
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=ALIGN.CENTER,height=1,fg_bg=s_header} local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=ALIGN.CENTER,fg_bg=s_header}
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=style.lg_white,width=12,fg_bg=s_header} local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=style.lg_white,width=12,fg_bg=s_header}
-- max length example: "01:23:45 AM - Wednesday, September 28 2022" -- max length example: "01:23:45 AM - Wednesday, September 28 2022"
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=s_header} local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,fg_bg=s_header}
ping.register(facility.ps, "sv_ping", ping.update) ping.register(facility.ps, "sv_ping", ping.update)
datetime.register(facility.ps, "date_time", datetime.set_value) datetime.register(facility.ps, "date_time", datetime.set_value)
@ -53,6 +55,8 @@ local function init(main)
cnc_y_start = cnc_y_start + row_1_height + 1 cnc_y_start = cnc_y_start + row_1_height + 1
util.nop()
if facility.num_units >= 3 then if facility.num_units >= 3 then
-- base offset 3, spacing 1, max height of units 1 and 2 -- base offset 3, spacing 1, max height of units 1 and 2
local row_2_offset = cnc_y_start local row_2_offset = cnc_y_start
@ -64,6 +68,8 @@ local function init(main)
uo_4 = unit_overview(main, 84, row_2_offset, units[4]) 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) cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.get_height() + 1)
end end
util.nop()
end end
-- command & control -- command & control
@ -73,12 +79,14 @@ local function init(main)
assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)") assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)")
TextBox{parent=main,y=cnc_bottom_align_start,text=string.rep("\x8c", header.get_width()),alignment=ALIGN.CENTER,height=1,fg_bg=style.lg_gray} TextBox{parent=main,y=cnc_bottom_align_start,text=string.rep("\x8c", header.get_width()),alignment=ALIGN.CENTER,fg_bg=style.lg_gray}
cnc_bottom_align_start = cnc_bottom_align_start + 2 cnc_bottom_align_start = cnc_bottom_align_start + 2
process_ctl(main, 2, cnc_bottom_align_start) 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]) imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1])
end end

View File

@ -88,17 +88,19 @@ style.theme = smooth_stone
---@param fp FP_THEME front panel theme ---@param fp FP_THEME front panel theme
---@param color_mode COLOR_MODE the color mode to use ---@param color_mode COLOR_MODE the color mode to use
function style.set_themes(main, fp, color_mode) function style.set_themes(main, fp, color_mode)
local colorblind = color_mode ~= themes.COLOR_MODE.STANDARD 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.ind_bkg = colors.gray
style.ind_hi_box_bg = util.trinary(colorblind, colors.black, 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 if main == themes.UI_THEME.SMOOTH_STONE then
style.theme = smooth_stone style.theme = smooth_stone
style.ind_bkg = util.trinary(colorblind, colors.black, colors.gray) style.ind_bkg = util.trinary(gray_ind_off, colors.gray, colors.black)
elseif main == themes.UI_THEME.DEEPSLATE then elseif main == themes.UI_THEME.DEEPSLATE then
style.theme = deepslate style.theme = deepslate
style.ind_hi_box_bg = util.trinary(colorblind, colors.black, colors.lightGray) style.ind_hi_box_bg = util.trinary(gray_ind_off, colors.lightGray, colors.black)
end end
style.colorblind = colorblind style.colorblind = colorblind

View File

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

View File

@ -2,6 +2,7 @@
-- Generic Graphics Element -- Generic Graphics Element
-- --
-- local log = require("scada-common.log")
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
@ -49,9 +50,11 @@ local element = {}
---|indicator_light_args ---|indicator_light_args
---|power_indicator_args ---|power_indicator_args
---|rad_indicator_args ---|rad_indicator_args
---|signal_bar_args
---|state_indicator_args ---|state_indicator_args
---|tristate_indicator_light_args ---|tristate_indicator_light_args
---|vbar_args ---|vbar_args
---|app_multipane_args
---|colormap_args ---|colormap_args
---|displaybox_args ---|displaybox_args
---|div_args ---|div_args
@ -80,9 +83,10 @@ end
-- a base graphics element, should not be created on its own -- a base graphics element, should not be created on its own
---@nodiscard ---@nodiscard
---@param args graphics_args arguments ---@param args graphics_args arguments
---@param constraint? function apply a dimensional constraint based on proposed dimensions function(frame) -> width, height
---@param child_offset_x? integer mouse event offset x ---@param child_offset_x? integer mouse event offset x
---@param child_offset_y? integer mouse event offset y ---@param child_offset_y? integer mouse event offset y
function element.new(args, child_offset_x, child_offset_y) function element.new(args, constraint, child_offset_x, child_offset_y)
local self = { local self = {
id = nil, ---@type element_id|nil id = nil, ---@type element_id|nil
is_root = args.parent == nil, is_root = args.parent == nil,
@ -196,6 +200,9 @@ function element.new(args, child_offset_x, child_offset_y)
---@param offset_y integer y offset for mouse events ---@param offset_y integer y offset for mouse events
---@param next_y integer next line if no y was provided ---@param next_y integer next line if no y was provided
function protected.prepare_template(offset_x, offset_y, next_y) function protected.prepare_template(offset_x, offset_y, next_y)
-- don't auto incrememnt y if inheriting height, that would cause an assertion
next_y = util.trinary(args.height == nil and constraint == nil, 1, next_y)
-- record offsets in case there is a reposition -- record offsets in case there is a reposition
self.offset_x = offset_x self.offset_x = offset_x
self.offset_y = offset_y 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() local w, h = self.p_window.getSize()
f.w = math.min(f.w, w - (f.x - 1)) f.w = math.min(f.w, w - (f.x - 1))
f.h = math.min(f.h, h - (f.y - 1)) f.h = math.min(f.h, h - (f.y - 1))
if type(constraint) == "function" then
-- constrain per provided constraint function (can only get smaller than available space)
w, h = constraint(f)
f.w = math.min(f.w, w)
f.h = math.min(f.h, h)
end
end end
-- check frame -- check frame
@ -490,7 +504,10 @@ function element.new(args, child_offset_x, child_offset_y)
if args.parent ~= nil then if args.parent ~= nil then
-- remove self from parent -- remove self from parent
-- log.debug("removing " .. self.id .. " from parent")
args.parent.__remove_child(self.id) args.parent.__remove_child(self.id)
else
-- log.debug("no parent for " .. self.id .. " on delete attempt")
end end
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") element.assert(type(args.app_fg_bg) == "table", "app_fg_bg is a required field")
args.height = 4 args.height = 4
args.width = 5 args.width = 7
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
@ -46,7 +46,7 @@ local function app_button(args)
end end
-- draw icon -- draw icon
e.w_set_cur(1, 1) e.w_set_cur(2, 1)
e.w_set_fgd(fgd) e.w_set_fgd(fgd)
e.w_set_bkg(bkg) e.w_set_bkg(bkg)
e.w_write("\x9f\x83\x83\x83") e.w_write("\x9f\x83\x83\x83")
@ -55,16 +55,16 @@ local function app_button(args)
e.w_write("\x90") e.w_write("\x90")
e.w_set_fgd(fgd) e.w_set_fgd(fgd)
e.w_set_bkg(bkg) e.w_set_bkg(bkg)
e.w_set_cur(1, 2) e.w_set_cur(2, 2)
e.w_write("\x95 ") e.w_write("\x95 ")
e.w_set_fgd(bkg) e.w_set_fgd(bkg)
e.w_set_bkg(fgd) e.w_set_bkg(fgd)
e.w_write("\x95") e.w_write("\x95")
e.w_set_cur(1, 3) e.w_set_cur(2, 3)
e.w_write("\x82\x8f\x8f\x8f\x81") e.w_write("\x82\x8f\x8f\x8f\x81")
-- write the icon text -- write the icon text
e.w_set_cur(3, 2) e.w_set_cur(4, 2)
e.w_set_fgd(fgd) e.w_set_fgd(fgd)
e.w_set_bkg(bkg) e.w_set_bkg(bkg)
e.w_write(args.text) e.w_write(args.text)

View File

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

View File

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

View File

@ -65,7 +65,7 @@ local function switch_button(args)
end end
end end
-- set the value -- set the value (does not call the callback)
---@param val boolean new value ---@param val boolean new value
function e.set_value(val) function e.set_value(val)
e.value = val e.value = val

View File

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

View File

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

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

View File

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

View File

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

View File

@ -54,14 +54,21 @@ themes.COLOR_MODE = {
STANDARD = 1, STANDARD = 1,
DEUTERANOPIA = 2, DEUTERANOPIA = 2,
PROTANOPIA = 3, PROTANOPIA = 3,
TRITANOPIA = 4 TRITANOPIA = 4,
BLUE_IND = 5,
STD_ON_BLACK = 6,
BLUE_ON_BLACK = 7,
NUM_MODES = 8
} }
themes.COLOR_MODE_NAMES = { themes.COLOR_MODE_NAMES = {
"Standard", "Standard",
"Deuteranopia", "Deuteranopia",
"Protanopia", "Protanopia",
"Tritanopia" "Tritanopia",
"Blue for 'Good'",
"Standard + Black",
"Blue + Black"
} }
-- attempts to get the string name of a color mode -- attempts to get the string name of a color mode
@ -72,7 +79,10 @@ function themes.color_mode_name(id)
if id == themes.COLOR_MODE.STANDARD or if id == themes.COLOR_MODE.STANDARD or
id == themes.COLOR_MODE.DEUTERANOPIA or id == themes.COLOR_MODE.DEUTERANOPIA or
id == themes.COLOR_MODE.PROTANOPIA or id == themes.COLOR_MODE.PROTANOPIA or
id == themes.COLOR_MODE.TRITANOPIA then 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] return themes.COLOR_MODE_NAMES[id]
else return nil end else return nil end
end end
@ -147,6 +157,26 @@ themes.sandstone = {
{ c = colors.yellow_off, hex = 0x141414 }, { c = colors.yellow_off, hex = 0x141414 },
{ c = colors.red, hex = 0xff0000 }, { c = colors.red, hex = 0xff0000 },
{ c = colors.red_off, hex = 0x141414 } { 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 }
} }
} }
} }
@ -180,8 +210,8 @@ themes.basalt = {
{ c = colors.white, hex = 0xbfbfbf }, { c = colors.white, hex = 0xbfbfbf },
{ c = colors.lightGray, hex = 0x848794 }, { c = colors.lightGray, hex = 0x848794 },
{ c = colors.gray, hex = 0x5c5f68 }, { c = colors.gray, hex = 0x5c5f68 },
{ c = colors.black, hex = 0x262626 }, { c = colors.black, hex = 0x333333 },
{ c = colors.red_off, hex = 0x653839 } { c = colors.red_off, hex = 0x512d2d }
}, },
color_modes = { color_modes = {
@ -216,6 +246,26 @@ themes.basalt = {
{ c = colors.yellow_off, hex = 0x333333 }, { c = colors.yellow_off, hex = 0x333333 },
{ c = colors.red, hex = 0xdf4949 }, { c = colors.red, hex = 0xdf4949 },
{ c = colors.red_off, hex = 0x333333 } { 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 }
} }
} }
} }
@ -272,19 +322,33 @@ themes.smooth_stone = {
{ {
{ c = colors.blue, hex = 0x1081ff }, { c = colors.blue, hex = 0x1081ff },
{ c = colors.yellow, hex = 0xf7c311 }, { c = colors.yellow, hex = 0xf7c311 },
{ c = colors.red, hex = 0xfb5615 }, { c = colors.red, hex = 0xfb5615 }
}, },
-- protanopia -- protanopia
{ {
{ c = colors.blue, hex = 0x1081ff }, { c = colors.blue, hex = 0x1081ff },
{ c = colors.yellow, hex = 0xf5e633 }, { c = colors.yellow, hex = 0xf5e633 },
{ c = colors.red, hex = 0xff521a }, { c = colors.red, hex = 0xff521a }
}, },
-- tritanopia -- tritanopia
{ {
{ c = colors.blue, hex = 0x40cbd7 }, { c = colors.blue, hex = 0x40cbd7 },
{ c = colors.yellow, hex = 0xffbc00 }, { c = colors.yellow, hex = 0xffbc00 },
{ c = colors.red, hex = 0xff0000 }, { 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 }
} }
} }
} }
@ -318,19 +382,33 @@ themes.deepslate = {
{ {
{ c = colors.blue, hex = 0x65aeff }, { c = colors.blue, hex = 0x65aeff },
{ c = colors.yellow, hex = 0xf7c311 }, { c = colors.yellow, hex = 0xf7c311 },
{ c = colors.red, hex = 0xfb5615 }, { c = colors.red, hex = 0xfb5615 }
}, },
-- protanopia -- protanopia
{ {
{ c = colors.blue, hex = 0x65aeff }, { c = colors.blue, hex = 0x65aeff },
{ c = colors.yellow, hex = 0xf5e633 }, { c = colors.yellow, hex = 0xf5e633 },
{ c = colors.red, hex = 0xff8058 }, { c = colors.red, hex = 0xff8058 }
}, },
-- tritanopia -- tritanopia
{ {
{ c = colors.blue, hex = 0x00ecff }, { c = colors.blue, hex = 0x00ecff },
{ c = colors.yellow, hex = 0xffbc00 }, { c = colors.yellow, hex = 0xffbc00 },
{ c = colors.red, hex = 0xdf4949 }, { 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 }
} }
} }
} }

View File

@ -3,7 +3,7 @@
-- --
local log = require("scada-common.log") local log = require("scada-common.log")
local tcd = require("scada-common.tcd") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
@ -32,7 +32,10 @@ local CENTER = core.ALIGN.CENTER
local RIGHT = core.ALIGN.RIGHT local RIGHT = core.ALIGN.RIGHT
-- changes to the config data/format to let the user know -- changes to the config data/format to let the user know
local changes = {} local changes = {
{ "v0.9.2", { "Added temperature scale options" } },
{ "v0.11.3", { "Added energy scale options" } }
}
---@class pkt_configurator ---@class pkt_configurator
local configurator = {} local configurator = {}
@ -73,6 +76,8 @@ local tool_ctl = {
---@class pkt_config ---@class pkt_config
local tmp_cfg = { local tmp_cfg = {
TempScale = 1,
EnergyScale = 1,
SVR_Channel = nil, ---@type integer SVR_Channel = nil, ---@type integer
CRD_Channel = nil, ---@type integer CRD_Channel = nil, ---@type integer
PKT_Channel = nil, ---@type integer PKT_Channel = nil, ---@type integer
@ -91,6 +96,8 @@ local settings_cfg = {}
-- all settings fields, their nice names, and their default values -- all settings fields, their nice names, and their default values
local fields = { local fields = {
{ "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN },
{ "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE },
{ "SVR_Channel", "SVR Channel", 16240 }, { "SVR_Channel", "SVR Channel", 16240 },
{ "CRD_Channel", "CRD Channel", 16243 }, { "CRD_Channel", "CRD Channel", 16243 },
{ "PKT_Channel", "PKT Channel", 16244 }, { "PKT_Channel", "PKT Channel", 16244 },
@ -121,17 +128,18 @@ local function config_view(display)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local function exit() os.queueEvent("terminate") end local function exit() os.queueEvent("terminate") end
TextBox{parent=display,y=1,text="Pocket Configurator",alignment=CENTER,height=1,fg_bg=style.header} TextBox{parent=display,y=1,text="Pocket Configurator",alignment=CENTER,fg_bg=style.header}
local root_pane_div = Div{parent=display,x=1,y=2} local root_pane_div = Div{parent=display,x=1,y=2}
local main_page = Div{parent=root_pane_div,x=1,y=1} local main_page = Div{parent=root_pane_div,x=1,y=1}
local ui_cfg = Div{parent=root_pane_div,x=1,y=1}
local net_cfg = Div{parent=root_pane_div,x=1,y=1} local net_cfg = Div{parent=root_pane_div,x=1,y=1}
local log_cfg = Div{parent=root_pane_div,x=1,y=1} local log_cfg = Div{parent=root_pane_div,x=1,y=1}
local summary = Div{parent=root_pane_div,x=1,y=1} local summary = Div{parent=root_pane_div,x=1,y=1}
local changelog = Div{parent=root_pane_div,x=1,y=1} local changelog = Div{parent=root_pane_div,x=1,y=1}
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,net_cfg,log_cfg,summary,changelog}} local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,ui_cfg,net_cfg,log_cfg,summary,changelog}}
-- Main Page -- Main Page
@ -148,7 +156,7 @@ local function config_view(display)
tool_ctl.viewing_config = true tool_ctl.viewing_config = true
tool_ctl.gen_summary(settings_cfg) tool_ctl.gen_summary(settings_cfg)
tool_ctl.settings_apply.hide(true) tool_ctl.settings_apply.hide(true)
main_pane.set_value(4) main_pane.set_value(5)
end end
if fs.exists("/pocket/config.lua") then if fs.exists("/pocket/config.lua") then
@ -162,7 +170,32 @@ local function config_view(display)
if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end
PushButton{parent=main_page,x=2,y=18,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} 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(5)end,fg_bg=nav_fg_bg,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 --#region Network
@ -173,24 +206,24 @@ local function config_view(display)
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}} local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
TextBox{parent=net_cfg,x=1,y=2,height=1,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} 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,height=1,text="Set network channels."} 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=3,height=4,text="Each of the named channels must be the same within a particular SCADA network.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=8,height=1,width=18,text="Supervisor Channel"} 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} 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=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=10,height=1,width=19,text="Coordinator Channel"} 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} 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=9,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=12,height=1,width=14,text="Pocket Channel"} 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} 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} TextBox{parent=net_c_1,x=9,y=13,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
local chan_err = TextBox{parent=net_c_1,x=1,y=14,height=1,width=24,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local 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 function submit_channels()
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value()) local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
@ -201,18 +234,18 @@ local function config_view(display)
else chan_err.show() end else chan_err.show() end
end end
PushButton{parent=net_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=net_c_1,x=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} PushButton{parent=net_c_1,x=19,y=15,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,height=1,text="Set connection timeout."} TextBox{parent=net_c_2,x=1,y=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=3,height=7,text="You generally should not need to modify this. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=11,height=1,width=19,text="Connection Timeout"} 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} 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} TextBox{parent=net_c_2,x=9,y=12,height=2,text="seconds\n(default 5)",fg_bg=g_lg_fg_bg}
local ct_err = TextBox{parent=net_c_2,x=1,y=14,height=1,width=24,text="Please set timeout.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local 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 function submit_timeouts()
local timeout_val = tonumber(timeout.get_value()) local timeout_val = tonumber(timeout.get_value())
@ -226,13 +259,13 @@ local function config_view(display)
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=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} PushButton{parent=net_c_2,x=19,y=15,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,height=1,text="Set the trusted range."} TextBox{parent=net_c_3,x=1,y=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=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} 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 range = NumberField{parent=net_c_3,x=1,y=13,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local tr_err = TextBox{parent=net_c_3,x=1,y=14,height=1,width=24,text="Set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local 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 function submit_tr()
local range_val = tonumber(range.get_value()) local range_val = tonumber(range.get_value())
@ -249,7 +282,7 @@ local function config_view(display)
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=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=6,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=1,y=12,height=1,text="Facility Auth Key"} 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 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 local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
@ -262,13 +295,13 @@ local function config_view(display)
hide_key.set_value(true) hide_key.set_value(true)
censor_key(true) censor_key(true)
local key_err = TextBox{parent=net_c_4,x=1,y=14,height=1,width=24,text="Length must be > 7.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local 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 function submit_auth()
local v = key.get_value() local v = key.get_value()
if string.len(v) == 0 or string.len(v) >= 8 then if string.len(v) == 0 or string.len(v) >= 8 then
tmp_cfg.AuthKey = key.get_value() tmp_cfg.AuthKey = key.get_value()
main_pane.set_value(3) main_pane.set_value(4)
key_err.hide(true) key_err.hide(true)
else key_err.show() end else key_err.show() end
end end
@ -281,20 +314,20 @@ local function config_view(display)
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=24} local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=24}
TextBox{parent=log_cfg,x=1,y=2,height=1,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)} TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
TextBox{parent=log_c_1,x=1,y=1,height=1,text="Configure logging below."} TextBox{parent=log_c_1,x=1,y=1,text="Configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,height=1,text="Log File Mode"} TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},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"} 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 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)} 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} TextBox{parent=log_c_1,x=3,y=11,height=4,text="This results in much larger log files. Use only as needed.",fg_bg=g_lg_fg_bg}
local path_err = TextBox{parent=log_c_1,x=1,y=14,height=1,width=24,text="Provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local 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() local function submit_log()
if path.get_value() ~= "" then if path.get_value() ~= "" then
@ -306,11 +339,11 @@ local function config_view(display)
tool_ctl.viewing_config = false tool_ctl.viewing_config = false
tool_ctl.importing_legacy = false tool_ctl.importing_legacy = false
tool_ctl.settings_apply.show() tool_ctl.settings_apply.show()
main_pane.set_value(4) main_pane.set_value(5)
else path_err.show() end else path_err.show() end
end end
PushButton{parent=log_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=log_c_1,x=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} 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 --#endregion
@ -324,7 +357,7 @@ local function config_view(display)
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}} 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)} 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 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)}
@ -335,7 +368,7 @@ local function config_view(display)
tool_ctl.importing_legacy = false tool_ctl.importing_legacy = false
tool_ctl.settings_apply.show() tool_ctl.settings_apply.show()
else else
main_pane.set_value(3) main_pane.set_value(4)
end end
end end
@ -346,12 +379,17 @@ local function config_view(display)
end end
local function save_and_continue() local function save_and_continue()
for k, v in pairs(tmp_cfg) do settings.set(k, v) end 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 if settings.save("/pocket.settings") then
load_settings(settings_cfg, true) load_settings(settings_cfg, true)
load_settings(ini_cfg) 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(svr_chan, ini_cfg.SVR_Channel)
try_set(crd_chan, ini_cfg.CRD_Channel) try_set(crd_chan, ini_cfg.CRD_Channel)
try_set(pkt_chan, ini_cfg.PKT_Channel) try_set(pkt_chan, ini_cfg.PKT_Channel)
@ -379,7 +417,7 @@ local function config_view(display)
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.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} tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=18,y=15,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
TextBox{parent=sum_c_2,x=1,y=1,height=1,text="Settings saved!"} TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"}
local function go_home() local function go_home()
main_pane.set_value(1) main_pane.set_value(1)
@ -410,15 +448,15 @@ local function config_view(display)
local cl = Div{parent=changelog,x=2,y=4,width=24} local cl = Div{parent=changelog,x=2,y=4,width=24}
TextBox{parent=changelog,x=1,y=2,height=1,text=" Config Change Log",fg_bg=bw_fg_bg} TextBox{parent=changelog,x=1,y=2,text=" Config Change Log",fg_bg=bw_fg_bg}
local c_log = ListBox{parent=cl,x=1,y=1,height=13,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local c_log = ListBox{parent=cl,x=1,y=1,height=13,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
for _, change in ipairs(changes) do for _, change in ipairs(changes) do
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg} TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
for _, v in ipairs(change[2]) do for _, v in ipairs(change[2]) do
local e = Div{parent=c_log,height=#util.strwrap(v,21)} local e = Div{parent=c_log,height=#util.strwrap(v,21)}
TextBox{parent=e,y=1,x=1,text="- ",height=1,fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=e,y=1,x=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
end end
end end
@ -444,7 +482,7 @@ local function config_view(display)
tool_ctl.gen_summary(tmp_cfg) tool_ctl.gen_summary(tmp_cfg)
sum_pane.set_value(1) sum_pane.set_value(1)
main_pane.set_value(4) main_pane.set_value(5)
tool_ctl.importing_legacy = true tool_ctl.importing_legacy = true
end end
@ -473,8 +511,15 @@ local function config_view(display)
local raw = cfg[f[1]] local raw = cfg[f[1]]
local val = util.strval(raw) local val = util.strval(raw)
if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) if f[1] == "AuthKey" then
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end 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 if val == "nil" then val = "<not set>" end
@ -532,9 +577,7 @@ function configurator.configure(ask_config)
local event, param1, param2, param3 = util.pull_event() local event, param1, param2, param3 = util.pull_event()
-- handle event -- handle event
if event == "timer" then if event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
tcd.handle(param1)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
local m_e = core.events.new_mouse_event(event, param1, param2, param3) local m_e = core.events.new_mouse_event(event, param1, param2, param3)
if m_e then display.handle_mouse(m_e) end if m_e then display.handle_mouse(m_e) end
elseif event == "char" or event == "key" or event == "key_up" then elseif event == "char" or event == "key" or event == "key_up" then

View File

@ -2,19 +2,26 @@
-- I/O Control for Pocket Integration with Supervisor & Coordinator -- I/O Control for Pocket Integration with Supervisor & Coordinator
-- --
local psil = require("scada-common.psil") local const = require("scada-common.constants")
-- local log = require("scada-common.log")
local psil = require("scada-common.psil")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util")
local ALARM = types.ALARM 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 = {} local iocontrol = {}
---@class pocket_ioctl
local io = {
ps = psil.create()
}
---@enum POCKET_LINK_STATE ---@enum POCKET_LINK_STATE
local LINK_STATE = { local LINK_STATE = {
UNLINKED = 0, UNLINKED = 0,
@ -23,23 +30,25 @@ local LINK_STATE = {
LINKED = 3 LINKED = 3
} }
---@enum NAV_PAGE iocontrol.LINK_STATE = LINK_STATE
local NAV_PAGE = {
HOME = 1, ---@class pocket_ioctl
UNITS = 2, local io = {
REACTORS = 3, version = "unknown",
BOILERS = 4, ps = psil.create()
TURBINES = 5,
DIAG = 6,
D_ALARMS = 7
} }
iocontrol.LINK_STATE = LINK_STATE local config = nil ---@type pkt_config
iocontrol.NAV_PAGE = NAV_PAGE
-- initialize facility-independent components of pocket iocontrol -- initialize facility-independent components of pocket iocontrol
---@param comms pocket_comms ---@param comms pocket_comms
function iocontrol.init_core(comms) ---@param nav pocket_nav
---@param cfg pkt_config
function iocontrol.init_core(comms, nav, cfg)
config = cfg
io.nav = nav
---@class pocket_ioctl_diag ---@class pocket_ioctl_diag
io.diag = {} io.diag = {}
@ -77,28 +86,812 @@ function iocontrol.init_core(comms)
tone_indicators = {} -- indicators to update from supervisor tone states tone_indicators = {} -- indicators to update from supervisor tone states
} }
---@class pocket_nav -- API access
io.nav = { ---@class pocket_ioctl_api
page = NAV_PAGE.HOME, ---@type NAV_PAGE io.api = {
sub_pages = { NAV_PAGE.HOME, NAV_PAGE.UNITS, NAV_PAGE.REACTORS, NAV_PAGE.BOILERS, NAV_PAGE.TURBINES, NAV_PAGE.DIAG }, get_unit = function (unit) comms.api__get_unit(unit) end
tasks = {}
} }
-- 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 end
-- initialize facility-dependent components of pocket iocontrol -- 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(),
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 = 0,
---@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
end
-- set network link state -- set network link state
---@param state POCKET_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.alarms = data[4]
--#region Annunciator
unit.annunciator = data[5]
local rcs_disconn, rcs_warn, rcs_hazard = false, false, false
for key, val in pairs(unit.annunciator) do
if key == "BoilerOnline" or key == "TurbineOnline" then
local every = true
-- split up online arrays
for id = 1, #val do
every = every and val[id]
if key == "BoilerOnline" then
unit.boiler_ps_tbl[id].publish(key, val[id])
else
unit.turbine_ps_tbl[id].publish(key, val[id])
end
end
if not every then rcs_disconn = true end
unit.unit_ps.publish("U_" .. key, every)
elseif key == "HeatingRateLow" or key == "WaterLevelLow" then
-- split up array for all boilers
local any = false
for id = 1, #val do
any = any or val[id]
unit.boiler_ps_tbl[id].publish(key, val[id])
end
if key == "HeatingRateLow" and any then
rcs_warn = true
elseif key == "WaterLevelLow" and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
elseif key == "SteamDumpOpen" or key == "TurbineOverSpeed" or key == "GeneratorTrip" or key == "TurbineTrip" then
-- split up array for all turbines
local any = false
for id = 1, #val do
any = any or val[id]
unit.turbine_ps_tbl[id].publish(key, val[id])
end
if key == "GeneratorTrip" and any then
rcs_warn = true
elseif (key == "TurbineOverSpeed" or key == "TurbineTrip") and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
else
-- non-table fields
unit.unit_ps.publish(key, val)
end
end
local anc = unit.annunciator
rcs_hazard = rcs_hazard or anc.RCPTrip
rcs_warn = rcs_warn or anc.RCSFlowLow or anc.CoolantLevelLow or anc.RCSFault or anc.MaxWaterReturnFeed or
anc.CoolantFeedMismatch or anc.BoilRateMismatch or anc.SteamFeedMismatch
local rcs_status = 4
if rcs_hazard then
rcs_status = 2
elseif rcs_warn then
rcs_status = 3
elseif rcs_disconn then
rcs_status = 1
end
unit.unit_ps.publish("U_RCS", rcs_status)
--#endregion
--#region Reactor Data
unit.reactor_data = data[6]
local control_status = 1
local reactor_status = 1
local 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[7]
for id = 1, #unit.boiler_data_tbl do
local boiler = unit.boiler_data_tbl[id] ---@type boilerv_session_db
local ps = unit.boiler_ps_tbl[id] ---@type psil
local boiler_status = 1
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[8]
for id = 1, #unit.turbine_data_tbl do
local turbine = unit.turbine_data_tbl[id] ---@type turbinev_session_db
local ps = unit.turbine_ps_tbl[id] ---@type psil
local turbine_status = 1
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[9]
unit.last_rate_change_ms = data[10]
unit.turbine_flow_stable = data[11]
--#endregion
--#region Status Information Display
local ecam = {} -- aviation reference :) back to VATSIM I go...
-- 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 -- get the IO controller database
function iocontrol.get_db() return io end function iocontrol.get_db() return io end

View File

@ -8,11 +8,24 @@ local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
local CRDN_TYPE = comms.CRDN_TYPE
local LINK_STATE = iocontrol.LINK_STATE local LINK_STATE = iocontrol.LINK_STATE
local pocket = {} 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 ---@type pkt_config
local config = {} local config = {}
@ -22,6 +35,9 @@ pocket.config = config
function pocket.load_config() function pocket.load_config()
if not settings.load("/pocket.settings") then return false end if not settings.load("/pocket.settings") then return false end
config.TempScale = settings.get("TempScale")
config.EnergyScale = settings.get("EnergyScale")
config.SVR_Channel = settings.get("SVR_Channel") config.SVR_Channel = settings.get("SVR_Channel")
config.CRD_Channel = settings.get("CRD_Channel") config.CRD_Channel = settings.get("CRD_Channel")
config.PKT_Channel = settings.get("PKT_Channel") config.PKT_Channel = settings.get("PKT_Channel")
@ -35,6 +51,11 @@ function pocket.load_config()
local cfv = util.new_validator() 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.SVR_Channel)
cfv.assert_channel(config.CRD_Channel) cfv.assert_channel(config.CRD_Channel)
cfv.assert_channel(config.PKT_Channel) cfv.assert_channel(config.PKT_Channel)
@ -57,26 +78,310 @@ function pocket.load_config()
return cfv.valid() return cfv.valid()
end end
---@enum POCKET_APP_ID
local APP_ID = {
ROOT = 1,
LOADER = 2,
-- main app pages
UNITS = 3,
GUIDE = 4,
ABOUT = 5,
-- diag app page
ALARMS = 6,
-- other
DUMMY = 7,
NUM_APPS = 7
}
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 -- pocket coordinator + supervisor communications
---@nodiscard ---@nodiscard
---@param version string pocket version ---@param version string pocket version
---@param nic nic network interface device ---@param nic nic network interface device
---@param sv_watchdog watchdog ---@param sv_watchdog watchdog
---@param api_watchdog watchdog ---@param api_watchdog watchdog
function pocket.comms(version, nic, sv_watchdog, api_watchdog) ---@param nav pocket_nav
function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
local self = { local self = {
sv = { sv = {
linked = false, linked = false,
addr = comms.BROADCAST, addr = comms.BROADCAST,
seq_num = 0, seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, ---@type nil|integer r_seq_num = nil, ---@type nil|integer
last_est_ack = ESTABLISH_ACK.ALLOW last_est_ack = ESTABLISH_ACK.ALLOW
}, },
api = { api = {
linked = false, linked = false,
addr = comms.BROADCAST, addr = comms.BROADCAST,
seq_num = 0, seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, ---@type nil|integer r_seq_num = nil, ---@type nil|integer
last_est_ack = ESTABLISH_ACK.ALLOW last_est_ack = ESTABLISH_ACK.ALLOW
}, },
establish_delay_counter = 0 establish_delay_counter = 0
@ -118,6 +423,20 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
self.api.seq_num = self.api.seq_num + 1 self.api.seq_num = self.api.seq_num + 1
end 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 -- attempt supervisor connection establishment
local function _send_sv_establish() local function _send_sv_establish()
_send_sv(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT }) _send_sv(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
@ -125,7 +444,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
-- attempt coordinator API connection establishment -- attempt coordinator API connection establishment
local function _send_api_establish() local function _send_api_establish()
_send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT }) _send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT, comms.api_version })
end end
-- keep alive ack to supervisor -- keep alive ack to supervisor
@ -148,6 +467,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
-- close connection to the supervisor -- close connection to the supervisor
function public.close_sv() function public.close_sv()
sv_watchdog.cancel() sv_watchdog.cancel()
nav.unload_sv()
self.sv.linked = false self.sv.linked = false
self.sv.r_seq_num = nil self.sv.r_seq_num = nil
self.sv.addr = comms.BROADCAST self.sv.addr = comms.BROADCAST
@ -157,6 +477,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
-- close connection to coordinator API server -- close connection to coordinator API server
function public.close_api() function public.close_api()
api_watchdog.cancel() api_watchdog.cancel()
nav.unload_api()
self.api.linked = false self.api.linked = false
self.api.r_seq_num = nil self.api.r_seq_num = nil
self.api.addr = comms.BROADCAST self.api.addr = comms.BROADCAST
@ -172,7 +493,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
-- attempt to re-link if any of the dependent links aren't active -- attempt to re-link if any of the dependent links aren't active
function public.link_update() function public.link_update()
if not self.sv.linked then if not self.sv.linked then
iocontrol.report_link_state(util.trinary(self.api.linked, LINK_STATE.API_LINK_ONLY, LINK_STATE.UNLINKED)) if self.api.linked then
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY, false, nil)
else
iocontrol.report_link_state(LINK_STATE.UNLINKED, false, false)
end
if self.establish_delay_counter <= 0 then if self.establish_delay_counter <= 0 then
_send_sv_establish() _send_sv_establish()
@ -181,7 +506,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
self.establish_delay_counter = self.establish_delay_counter - 1 self.establish_delay_counter = self.establish_delay_counter - 1
end end
elseif not self.api.linked then elseif not self.api.linked then
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY) iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, nil, false)
if self.establish_delay_counter <= 0 then if self.establish_delay_counter <= 0 then
_send_api_establish() _send_api_establish()
@ -189,9 +514,6 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
else else
self.establish_delay_counter = self.establish_delay_counter - 1 self.establish_delay_counter = self.establish_delay_counter - 1
end end
else
-- linked, all good!
iocontrol.report_link_state(LINK_STATE.LINKED)
end end
end end
@ -214,6 +536,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end
end end
-- 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
-- parse a packet -- parse a packet
---@param side string ---@param side string
---@param sender integer ---@param sender integer
@ -246,6 +573,25 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
return pkt return pkt
end 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.scada_frame.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 -- handle a packet
---@param packet mgmt_frame|crdn_frame|nil ---@param packet mgmt_frame|crdn_frame|nil
function public.handle_packet(packet) function public.handle_packet(packet)
@ -262,27 +608,42 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
elseif r_chan == config.CRD_Channel then elseif r_chan == config.CRD_Channel then
-- check sequence number -- check sequence number
if self.api.r_seq_num == nil then if self.api.r_seq_num == nil then
self.api.r_seq_num = packet.scada_frame.seq_num() self.api.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.connected and ((self.api.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then elseif self.api.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order (API): last = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) log.warning("sequence out-of-order (API): next = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return return
elseif self.api.linked and (src_addr ~= self.api.addr) then elseif self.api.linked and (src_addr ~= self.api.addr) then
log.debug("received packet from unknown computer " .. src_addr .. " while linked (API expected " .. self.api.addr .. log.debug("received packet from unknown computer " .. src_addr .. " while linked (API expected " .. self.api.addr ..
"); channel in use by another system?") "); channel in use by another system?")
return return
else else
self.api.r_seq_num = packet.scada_frame.seq_num() self.api.r_seq_num = packet.scada_frame.seq_num() + 1
end end
-- feed watchdog on valid sequence number -- feed watchdog on valid sequence number
api_watchdog.feed() 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.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, 11) 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 ---@cast packet mgmt_frame
if self.api.linked then if self.api.linked then
if packet.type == MGMT_TYPE.KEEP_ALIVE then if packet.type == MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back -- keep alive request received, echo back
if packet.length == 1 then if _check_length(packet, 1) then
local timestamp = packet.data[1] local timestamp = packet.data[1]
local trip_time = util.time() - timestamp local trip_time = util.time() - timestamp
@ -290,57 +651,80 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
log.warning("pocket coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)") log.warning("pocket coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
end end
-- log.debug("pocket coordinator RTT = " .. trip_time .. "ms") -- log.debug("pocket coordinator TT = " .. trip_time .. "ms")
_send_api_keep_alive_ack(timestamp) _send_api_keep_alive_ack(timestamp)
else
log.debug("coordinator SCADA keep alive packet length mismatch") iocontrol.report_crd_tt(trip_time)
end end
elseif packet.type == MGMT_TYPE.CLOSE then elseif packet.type == MGMT_TYPE.CLOSE then
-- handle session close -- handle session close
api_watchdog.cancel() api_watchdog.cancel()
nav.unload_api()
self.api.linked = false self.api.linked = false
self.api.r_seq_num = nil self.api.r_seq_num = nil
self.api.addr = comms.BROADCAST self.api.addr = comms.BROADCAST
log.info("coordinator server connection closed by remote host") log.info("coordinator server connection closed by remote host")
else else _fail_type(packet) end
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator")
end
elseif packet.type == MGMT_TYPE.ESTABLISH then elseif packet.type == MGMT_TYPE.ESTABLISH then
-- connection with coordinator established -- connection with coordinator established
if packet.length == 1 then if _check_length(packet, 1, 2) then
local est_ack = packet.data[1] local est_ack = packet.data[1]
if est_ack == ESTABLISH_ACK.ALLOW then if est_ack == ESTABLISH_ACK.ALLOW then
log.info("coordinator connection established") if packet.length == 2 then
self.establish_delay_counter = 0 local fac_config = packet.data[2]
self.api.linked = true
self.api.addr = src_addr
if self.sv.linked then if type(fac_config) == "table" and #fac_config == 2 then
iocontrol.report_link_state(LINK_STATE.LINKED) -- 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 else
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY) log.debug("received coordinator establish allow without facility configuration")
end
elseif est_ack == ESTABLISH_ACK.DENY then
if self.api.last_est_ack ~= est_ack then
log.info("coordinator connection denied")
end
elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.api.last_est_ack ~= est_ack then
log.info("coordinator connection denied due to collision")
end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.api.last_est_ack ~= est_ack then
log.info("coordinator comms version mismatch")
end end
else else
log.debug("coordinator SCADA_MGMT establish packet reply unsupported") if self.api.last_est_ack ~= est_ack then
if est_ack == ESTABLISH_ACK.DENY then
log.info("coordinator connection denied")
iocontrol.report_crd_link_error("denied")
elseif est_ack == ESTABLISH_ACK.COLLISION then
log.info("coordinator connection denied due to collision")
iocontrol.report_crd_link_error("collision")
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
log.info("coordinator comms version mismatch")
iocontrol.report_crd_link_error("comms version mismatch")
elseif est_ack == ESTABLISH_ACK.BAD_API_VERSION then
log.info("coordinator api version mismatch")
iocontrol.report_crd_link_error("API version mismatch")
else
log.debug("coordinator SCADA_MGMT establish packet reply unsupported")
iocontrol.report_crd_link_error("unknown reply")
end
end
-- unlink
self.api.addr = comms.BROADCAST
self.api.linked = false
end end
self.api.last_est_ack = est_ack self.api.last_est_ack = est_ack
else
log.debug("coordinator SCADA_MGMT establish packet length mismatch")
end end
else else
log.debug("discarding coordinator non-link SCADA_MGMT packet before linked") log.debug("discarding coordinator non-link SCADA_MGMT packet before linked")
@ -351,16 +735,16 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
elseif r_chan == config.SVR_Channel then elseif r_chan == config.SVR_Channel then
-- check sequence number -- check sequence number
if self.sv.r_seq_num == nil then if self.sv.r_seq_num == nil then
self.sv.r_seq_num = packet.scada_frame.seq_num() self.sv.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.connected and ((self.sv.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then elseif self.sv.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order (SVR): last = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) log.warning("sequence out-of-order (SVR): next = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return return
elseif self.sv.linked and (src_addr ~= self.sv.addr) then elseif self.sv.linked and (src_addr ~= self.sv.addr) then
log.debug("received packet from unknown computer " .. src_addr .. " while linked (SVR expected " .. self.sv.addr .. log.debug("received packet from unknown computer " .. src_addr .. " while linked (SVR expected " .. self.sv.addr ..
"); channel in use by another system?") "); channel in use by another system?")
return return
else else
self.sv.r_seq_num = packet.scada_frame.seq_num() self.sv.r_seq_num = packet.scada_frame.seq_num() + 1
end end
-- feed watchdog on valid sequence number -- feed watchdog on valid sequence number
@ -372,7 +756,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
if self.sv.linked then if self.sv.linked then
if packet.type == MGMT_TYPE.KEEP_ALIVE then if packet.type == MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back -- keep alive request received, echo back
if packet.length == 1 then if _check_length(packet, 1) then
local timestamp = packet.data[1] local timestamp = packet.data[1]
local trip_time = util.time() - timestamp local trip_time = util.time() - timestamp
@ -380,26 +764,25 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
log.warning("pocket supervisor KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)") log.warning("pocket supervisor KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
end end
-- log.debug("pocket supervisor RTT = " .. trip_time .. "ms") -- log.debug("pocket supervisor TT = " .. trip_time .. "ms")
_send_sv_keep_alive_ack(timestamp) _send_sv_keep_alive_ack(timestamp)
else
log.debug("supervisor SCADA keep alive packet length mismatch") iocontrol.report_svr_tt(trip_time)
end end
elseif packet.type == MGMT_TYPE.CLOSE then elseif packet.type == MGMT_TYPE.CLOSE then
-- handle session close -- handle session close
sv_watchdog.cancel() sv_watchdog.cancel()
nav.unload_sv()
self.sv.linked = false self.sv.linked = false
self.sv.r_seq_num = nil self.sv.r_seq_num = nil
self.sv.addr = comms.BROADCAST self.sv.addr = comms.BROADCAST
log.info("supervisor server connection closed by remote host") log.info("supervisor server connection closed by remote host")
elseif packet.type == MGMT_TYPE.DIAG_TONE_GET then 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 for i = 1, #packet.data do
diag.tone_test.tone_indicators[i].update(packet.data[i] == true) diag.tone_test.tone_indicators[i].update(packet.data[i] == true)
end end
else
log.debug("supervisor SCADA diag alarm states packet length mismatch")
end end
elseif packet.type == MGMT_TYPE.DIAG_TONE_SET then elseif packet.type == MGMT_TYPE.DIAG_TONE_SET then
if packet.length == 1 and packet.data[1] == false then if packet.length == 1 and packet.data[1] == false then
@ -438,12 +821,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
else else
log.debug("supervisor SCADA diag alarm set packet length/type mismatch") log.debug("supervisor SCADA diag alarm set packet length/type mismatch")
end end
else else _fail_type(packet) end
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor")
end
elseif packet.type == MGMT_TYPE.ESTABLISH then elseif packet.type == MGMT_TYPE.ESTABLISH then
-- connection with supervisor established -- connection with supervisor established
if packet.length == 1 then if _check_length(packet, 1) then
local est_ack = packet.data[1] local est_ack = packet.data[1]
if est_ack == ESTABLISH_ACK.ALLOW then if est_ack == ESTABLISH_ACK.ALLOW then
@ -452,37 +833,41 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
self.sv.linked = true self.sv.linked = true
self.sv.addr = src_addr self.sv.addr = src_addr
iocontrol.report_svr_link_error("")
if self.api.linked then if self.api.linked then
iocontrol.report_link_state(LINK_STATE.LINKED) iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, nil)
else else
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY) iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, self.sv.addr, nil)
end
elseif est_ack == ESTABLISH_ACK.DENY then
if self.sv.last_est_ack ~= est_ack then
log.info("supervisor connection denied")
end
elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.sv.last_est_ack ~= est_ack then
log.info("supervisor connection denied due to collision")
end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.sv.last_est_ack ~= est_ack then
log.info("supervisor comms version mismatch")
end end
else else
log.debug("supervisor SCADA_MGMT establish packet reply unsupported") if self.sv.last_est_ack ~= est_ack then
if est_ack == ESTABLISH_ACK.DENY then
log.info("supervisor connection denied")
iocontrol.report_svr_link_error("denied")
elseif est_ack == ESTABLISH_ACK.COLLISION then
log.info("supervisor connection denied due to collision")
iocontrol.report_svr_link_error("collision")
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
log.info("supervisor comms version mismatch")
iocontrol.report_svr_link_error("comms version mismatch")
else
log.debug("supervisor SCADA_MGMT establish packet reply unsupported")
iocontrol.report_svr_link_error("unknown reply")
end
end
-- unlink
self.sv.addr = comms.BROADCAST
self.sv.linked = false
end end
self.sv.last_est_ack = est_ack self.sv.last_est_ack = est_ack
else
log.debug("supervisor SCADA_MGMT establish packet length mismatch")
end end
else else
log.debug("discarding supervisor non-link SCADA_MGMT packet before linked") log.debug("discarding supervisor non-link SCADA_MGMT packet before linked")
end end
else else _fail_type(packet) end
log.debug("illegal packet type " .. protocol .. " from supervisor", true)
end
else else
log.debug("received packet from unconfigured channel " .. r_chan, true) log.debug("received packet from unconfigured channel " .. r_chan, true)
end end
@ -497,8 +882,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
---@nodiscard ---@nodiscard
function public.is_api_linked() return self.api.linked end function public.is_api_linked() return self.api.linked end
-- check if we are still linked with the supervisor and coordinator
---@nodiscard
function public.is_linked() return self.sv.linked and self.api.linked end
return public return public
end end
return pocket return pocket

View File

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

View File

@ -2,27 +2,35 @@
-- SCADA System Access on a Pocket Computer -- 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() require("/initenv").init_env()
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local network = require("scada-common.network") local network = require("scada-common.network")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core")
local configure = require("pocket.configure") local configure = require("pocket.configure")
local iocontrol = require("pocket.iocontrol") local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket") local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer") local renderer = require("pocket.renderer")
local threads = require("pocket.threads")
local POCKET_VERSION = "v0.7.2-alpha" local POCKET_VERSION = "v0.11.8-alpha"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
-- check environment (allows Pocket or CraftOS-PC)
if not _is_pocket_env then
println("You can only use this application on a pocket computer.")
return
end
---------------------------------------- ----------------------------------------
-- get configuration -- get configuration
---------------------------------------- ----------------------------------------
@ -54,6 +62,7 @@ log.info("BOOTING pocket.startup " .. POCKET_VERSION)
log.info("========================================") log.info("========================================")
crash.set_env("pocket", POCKET_VERSION) crash.set_env("pocket", POCKET_VERSION)
crash.dbg_log_env()
---------------------------------------- ----------------------------------------
-- main application -- main application
@ -67,10 +76,55 @@ local function main()
-- mount connected devices -- mount connected devices
ppm.mount_all() 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 -- message authentication init
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
network.init_mac(config.AuthKey) network.init_mac(config.AuthKey)
@ -79,115 +133,59 @@ local function main()
iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED) iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED)
-- get the communications modem -- get the communications modem
local modem = ppm.get_wireless_modem() if smem_dev.modem == nil then
if modem == nil then
println("startup> wireless modem not found: please craft the pocket computer with a wireless modem") println("startup> wireless modem not found: please craft the pocket computer with a wireless modem")
log.fatal("startup> no wireless modem on startup") log.fatal("startup> no wireless modem on startup")
return return
end end
-- create connection watchdogs -- create connection watchdogs
local conn_wd = { smem_sys.sv_wd = util.new_watchdog(config.ConnTimeout)
sv = util.new_watchdog(config.ConnTimeout), smem_sys.sv_wd.cancel()
api = util.new_watchdog(config.ConnTimeout) smem_sys.api_wd = util.new_watchdog(config.ConnTimeout)
} smem_sys.api_wd.cancel()
conn_wd.sv.cancel()
conn_wd.api.cancel()
log.debug("startup> conn watchdogs created") log.debug("startup> conn watchdogs created")
-- create network interface then setup comms -- create network interface then setup comms
local nic = network.nic(modem) smem_sys.nic = network.nic(smem_dev.modem)
local pocket_comms = pocket.comms(POCKET_VERSION, nic, conn_wd.sv, conn_wd.api) smem_sys.pocket_comms = pocket.comms(POCKET_VERSION, smem_sys.nic, smem_sys.sv_wd, smem_sys.api_wd, smem_sys.nav)
log.debug("startup> comms init") log.debug("startup> comms init")
-- base loop clock (2Hz, 10 ticks)
local MAIN_CLOCK = 0.5
local loop_clock = util.new_clock(MAIN_CLOCK)
-- init I/O control -- init I/O control
iocontrol.init_core(pocket_comms) iocontrol.init_core(smem_sys.pocket_comms, smem_sys.nav, config)
---------------------------------------- ----------------------------------------
-- start the UI -- start the UI
---------------------------------------- ----------------------------------------
local ui_ok, message = renderer.try_start_ui() local ui_message
if not ui_ok then pkt_state.ui_ok, ui_message = renderer.try_start_ui()
println(util.c("UI error: ", message)) if not pkt_state.ui_ok then
log.error(util.c("startup> GUI render failed with error ", message)) println(util.c("UI error: ", ui_message))
else log.error(util.c("startup> GUI render failed with error ", ui_message))
-- start clock
loop_clock.start()
end end
---------------------------------------- ----------------------------------------
-- main event loop -- start system
---------------------------------------- ----------------------------------------
if ui_ok then if pkt_state.ui_ok then
-- start connection watchdogs -- init threads
conn_wd.sv.feed() local main_thread = threads.thread__main(__shared_memory)
conn_wd.api.feed() local render_thread = threads.thread__render(__shared_memory)
log.debug("startup> conn watchdog started")
local io_db = iocontrol.get_db() log.info("startup> completed")
local nav = io_db.nav
-- main event loop -- run threads
while true do parallel.waitForAll(main_thread.p_exec, render_thread.p_exec)
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event
if event == "timer" then
if loop_clock.is_clock(param1) then
-- main loop tick
-- relink if necessary
pocket_comms.link_update()
-- update any tasks for the active page
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
renderer.close_ui() renderer.close_ui()
if not pkt_state.ui_ok then
println(util.c("UI crashed with error: ", pkt_state.ui_error))
end
else
println_ts("UI creation failed")
end end
println_ts("exited") println_ts("exited")

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

View File

@ -1,58 +1,41 @@
--
-- Diagnostic Apps
--
local iocontrol = require("pocket.iocontrol") local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local core = require("graphics.core") local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.textbox")
local IndicatorLight = require("graphics.elements.indicators.light") local IndicatorLight = require("graphics.elements.indicators.light")
local App = require("graphics.elements.controls.app")
local Checkbox = require("graphics.elements.controls.checkbox") local Checkbox = require("graphics.elements.controls.checkbox")
local PushButton = require("graphics.elements.controls.push_button") local PushButton = require("graphics.elements.controls.push_button")
local SwitchButton = require("graphics.elements.controls.switch_button") local SwitchButton = require("graphics.elements.controls.switch_button")
local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
local NAV_PAGE = iocontrol.NAV_PAGE local APP_ID = pocket.APP_ID
local ALIGN = core.ALIGN -- create diagnostic app pages
-- new diagnostics page view
---@param root graphics_element parent ---@param root graphics_element parent
local function new_view(root) local function create_pages(root)
local db = iocontrol.get_db() 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 -- -- 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 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} 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)} ttest.ready_warn = TextBox{parent=audio,y=2,text="",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}
local tones = Div{parent=audio,x=2,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)} 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 = {} local test_btns = {}
test_btns[1] = SwitchButton{parent=tones,text="TEST 1",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_1} test_btns[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)} 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 = {} local alarm_btns = {}
alarm_btns[1] = Checkbox{parent=alarms,label="BREACH",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_breach} alarm_btns[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} 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_1 = IndicatorLight{parent=states,label="1",colors=c_blue_gray}
local t_2 = IndicatorLight{parent=states,label="2",colors=c_blue_gray} local t_2 = IndicatorLight{parent=states,label="2",colors=c_blue_gray}
local t_3 = IndicatorLight{parent=states,label="3",colors=c_blue_gray} local t_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} 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 } 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 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

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

@ -0,0 +1,248 @@
--
-- System Guide
--
local util = require("scada-common.util")
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 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 panes = { home, search, use, uis, fps, gls }
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}
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) < 3 then
TextBox{parent=search_results,text="Search requires at least 3 characters."}
return
end
for _, entry in ipairs(search_db) do
local s_start, _ = string.find(entry[1], query, 1, true)
if s_start == nil then
elseif s_start == 1 then
-- best match, start of key
table.insert(s_results[1], entry)
elseif string.sub(query, s_start - 1, s_start) == " " then
-- start of word, good match
table.insert(s_results[2], entry)
else
-- basic match in content
table.insert(s_results[3], entry)
end
end
local empty = true
for tier = 1, 3 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
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.unit.fac_section, 100)
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}
PushButton{parent=fps,y=3,text="Common Items >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=fps,text="Reactor PLC >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=fps,text="RTU Gateway >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=fps,text="Supervisor >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=fps,text="Coordinator >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,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, 130)
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}
-- 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 -- Connection Waiting Spinner
-- --
local iocontrol = require("pocket.iocontrol")
local style = require("pocket.ui.style") local style = require("pocket.ui.style")
local core = require("graphics.core") 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} local root = Div{parent=parent,x=1,y=1}
-- bounding box div -- 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 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 if is_api then
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)} 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 else
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.green,style.root.bkg)} 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 end
return root return root

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

@ -0,0 +1,136 @@
local docs = {}
local target
local function doc(key, name, desc)
---@class pocket_doc_item
local item = { key = key, name = name, desc = desc }
table.insert(target, item)
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 = {}, fac_section = {}
}
}
target = docs.annunc.unit.main_section
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.")
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.unit.fac_section
doc("?", "Unit Systems Online", "All unit systems (reactors, boilers, and turbines) are connected.")
doc("?", "Radiation Monitor", "At least one facility radiation monitor is connected")
doc("?", "Induction Matrix", "The induction matrix is connected.")
doc("?", "SPS Connected", "Indicates if the super-critical phase shifter is connected.")
doc("?", "Configured Units Ready", "All units assigned to automatic control are ready to run automatic control.")
doc("?", "Process Active", "Automatic process control is active.")
doc("?", "Process Ramping", "Automatic process control is performing an initial ramp-up of the reactors for later PID control (generation and charge mode).")
doc("?", "Min/Max Burn Rate", "Auto control has either commanded 0 mB/t or the maximum total burn rate available (from assigned units).")
doc("?", "Automatic SCRAM", "Automatic control system SCRAM'ed the assigned reactors due to a safety hazard, shown by the below indicators.")
doc("?", "Matrix Disconnected", "Automatic SCRAM occurred due to loss of induction matrix connection.")
doc("?", "Matrix Charge High", "Automatic SCRAM occurred due to induction matrix charge exceeding acceptable limit.")
doc("?", "Unit Critical Alarm", "Automatic SCRAM occurred due to critical level unit alarm(s).")
doc("?", "Facility Radiation High", "Automatic SCRAM occurred due to high facility radiation levels.")
doc("?", "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.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 Derivitave 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", "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,84 @@
-- Pocket GUI Root -- 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 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 home_page = require("pocket.ui.pages.home_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 core = require("graphics.core") local style = require("pocket.ui.style")
local Div = require("graphics.elements.div") local core = require("graphics.core")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
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 WaitingAnim = require("graphics.elements.animations.waiting")
local NAV_PAGE = iocontrol.NAV_PAGE
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 ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
local APP_ID = pocket.APP_ID
-- create new main view -- create new main view
---@param main graphics_element main displaybox ---@param main graphics_element main displaybox
local function init(main) local function init(main)
local nav = iocontrol.get_db().nav local db = iocontrol.get_db()
local ps = iocontrol.get_db().ps
-- window header message -- window header message and connection status
TextBox{parent=main,y=1,text="",alignment=ALIGN.LEFT,height=1,fg_bg=style.header} 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)}
-- db.ps.subscribe("svr_conn_quality", svr_conn.set_value)
-- root panel panes (connection screens + main screen) db.ps.subscribe("crd_conn_quality", crd_conn.set_value)
--
local root_pane_div = Div{parent=main,x=1,y=2} local start_pane = 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 main_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) local root_pane = MultiPane{parent=main,x=1,y=2,panes={start_pane,main_pane}}
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 page_div = Div{parent=main_pane,x=4,y=1} local page_div = Div{parent=main_pane,x=4,y=1}
local sidebar_tabs = { -- create all the apps & pages
{ home_page(page_div)
char = "#", unit_app(page_div)
color = cpair(colors.black,colors.green) guide_app(page_div)
}, loader_app(page_div)
{ sys_apps(page_div)
char = "U", diag_apps(page_div)
color = cpair(colors.black,colors.yellow) dummy_app(page_div)
},
{
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)
}
}
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) 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}
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
page_pane.set_value(page) db.nav.open_app(APP_ID.ROOT)
end
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 end
return init 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,68 @@
local log = require("scada-common.log")
local util = require("scada-common.util")
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 ALIGN = core.ALIGN
local cpair = core.cpair
-- 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=30,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 _end
for i = 1, #items do
local item = items[i] ---@type pocket_doc_item
local anchor = TextBox{parent=def_list,text=item.name,anchor=true,fg_bg=cpair(colors.blue,colors.black)}
TextBox{parent=def_list,text=item.desc}
_end = Div{parent=def_list,height=1,can_focus=true}
local function view()
_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 })
PushButton{parent=name_list,text=item.name,fg_bg=cpair(colors.blue,colors.black),active_fg_bg=btn_active,callback=view}
if i % 12 == 0 then util.nop() end
end
log.debug("guide section " .. title .. " generated with final height ".. _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 cpair = core.cpair
local APP_ID = pocket.APP_ID
-- new home page view -- new home page view
---@param root graphics_element parent ---@param root graphics_element parent
local function new_view(root) 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)} local main = Div{parent=root,x=1,y=1,height=19}
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)} local app = db.nav.register_app(APP_ID.ROOT, main)
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 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.DUMMY)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 return main
end 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.root = cpair(colors.white, colors.black)
style.header = cpair(colors.white, colors.gray) 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 = { style.colors = {
{ c = colors.red, hex = 0xdf4949 }, { c = colors.red, hex = 0xdf4949 },
@ -33,6 +35,46 @@ style.colors = {
-- { c = colors.brown, hex = 0x7f664c } -- { 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 -- -- MAIN LAYOUT --
style.reactor = { style.reactor = {
@ -40,7 +82,7 @@ style.reactor = {
states = { states = {
{ {
color = cpair(colors.black, colors.yellow), color = cpair(colors.black, colors.yellow),
text = "PLC OFF-LINE" text = "OFF-LINE"
}, },
{ {
color = cpair(colors.black, colors.orange), color = cpair(colors.black, colors.orange),
@ -64,7 +106,7 @@ style.reactor = {
}, },
{ {
color = cpair(colors.black, colors.red), 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,621 @@
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 LEFT = core.ALIGN.LEFT
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,alignment=LEFT}
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,44 +2,37 @@
-- Configuration GUI -- Configuration GUI
-- --
local log = require("scada-common.log") local log = require("scada-common.log")
local rsio = require("scada-common.rsio") local tcd = require("scada-common.tcd")
local tcd = require("scada-common.tcd") local util = require("scada-common.util")
local util = require("scada-common.util")
local core = require("graphics.core") local check = require("reactor-plc.config.check")
local themes = require("graphics.themes") local system = require("reactor-plc.config.system")
local DisplayBox = require("graphics.elements.displaybox") local core = require("graphics.core")
local Div = require("graphics.elements.div") local themes = require("graphics.themes")
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 DisplayBox = require("graphics.elements.displaybox")
local PushButton = require("graphics.elements.controls.push_button") local Div = require("graphics.elements.div")
local Radio2D = require("graphics.elements.controls.radio_2d") local ListBox = require("graphics.elements.listbox")
local RadioButton = require("graphics.elements.controls.radio_button") local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local NumberField = require("graphics.elements.form.number_field") local PushButton = require("graphics.elements.controls.push_button")
local TextField = require("graphics.elements.form.text_field")
local IndLight = require("graphics.elements.indicators.light")
local println = util.println local println = util.println
local tri = util.trinary local tri = util.trinary
local cpair = core.cpair local cpair = core.cpair
local LEFT = core.ALIGN.LEFT
local CENTER = core.ALIGN.CENTER local CENTER = core.ALIGN.CENTER
local RIGHT = core.ALIGN.RIGHT
-- changes to the config data/format to let the user know -- changes to the config data/format to let the user know
local changes = { local changes = {
{ "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.8", { "ConnTimeout can now have a fractional part" } },
{ "v1.6.15", { "Added front panel UI theme", "Added color accessibility modes" } } { "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 ---@class plc_configurator
@ -47,22 +40,22 @@ local configurator = {}
local style = {} local style = {}
style.root = cpair(colors.black, colors.lightGray) style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray) style.header = cpair(colors.white, colors.gray)
style.colors = themes.smooth_stone.colors style.colors = themes.smooth_stone.colors
local bw_fg_bg = cpair(colors.black, colors.white) style.bw_fg_bg = cpair(colors.black, colors.white)
local g_lg_fg_bg = cpair(colors.gray, colors.lightGray) style.g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
local nav_fg_bg = bw_fg_bg style.nav_fg_bg = style.bw_fg_bg
local btn_act_fg_bg = cpair(colors.white, colors.gray) style.btn_act_fg_bg = cpair(colors.white, colors.gray)
style.btn_dis_fg_bg = cpair(colors.lightGray, colors.white)
---@class _plc_cfg_tool_ctl ---@class _plc_cfg_tool_ctl
local tool_ctl = { local tool_ctl = {
ask_config = false, ask_config = false,
has_config = false, has_config = false,
viewing_config = false, viewing_config = false,
importing_legacy = false,
jumped_to_color = false, jumped_to_color = false,
view_cfg = nil, ---@type graphics_element view_cfg = nil, ---@type graphics_element
@ -71,16 +64,8 @@ local tool_ctl = {
color_apply = nil, ---@type graphics_element color_apply = nil, ---@type graphics_element
settings_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 gen_summary = nil, ---@type function
show_current_cfg = nil, ---@type function
load_legacy = nil, ---@type function load_legacy = nil, ---@type function
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type graphics_element
auth_key_textbox = nil, ---@type graphics_element
auth_key_value = ""
} }
---@class plc_config ---@class plc_config
@ -126,27 +111,6 @@ local fields = {
{ "ColorMode", "Color Mode", themes.COLOR_MODE.STANDARD } { "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 -- load data from the settings file
---@param target plc_config ---@param target plc_config
---@param raw boolean? true to not use default values ---@param raw boolean? true to not use default values
@ -163,11 +127,16 @@ end
-- create the config view -- create the config view
---@param display graphics_element ---@param display graphics_element
local function config_view(display) local function config_view(display)
local bw_fg_bg = style.bw_fg_bg
local g_lg_fg_bg = style.g_lg_fg_bg
local nav_fg_bg = style.nav_fg_bg
local btn_act_fg_bg = style.btn_act_fg_bg
local btn_dis_fg_bg = style.btn_dis_fg_bg
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local function exit() os.queueEvent("terminate") end local function exit() os.queueEvent("terminate") end
TextBox{parent=display,y=1,text="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} local root_pane_div = Div{parent=display,x=1,y=2}
@ -178,17 +147,18 @@ local function config_view(display)
local clr_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 summary = Div{parent=root_pane_div,x=1,y=1}
local changelog = Div{parent=root_pane_div,x=1,y=1} local changelog = Div{parent=root_pane_div,x=1,y=1}
local 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,clr_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 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."} 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 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 y_start = y_start + 5
end end
@ -205,7 +175,7 @@ local function config_view(display)
end 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} 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}
local function jump_color() local function jump_color()
tool_ctl.jumped_to_color = true tool_ctl.jumped_to_color = true
@ -215,7 +185,8 @@ local function config_view(display)
end 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=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} 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} 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}
if not tool_ctl.has_config then if not tool_ctl.has_config then
@ -223,544 +194,43 @@ local function config_view(display)
tool_ctl.color_cfg.disable() tool_ctl.color_cfg.disable()
end end
--#region PLC --#endregion
local plc_c_1 = Div{parent=plc_cfg,x=2,y=4,width=49} --#region System Configuration
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}} local settings = { settings_cfg, ini_cfg, tmp_cfg, fields, load_settings }
local divs = { plc_cfg, net_cfg, log_cfg, clr_cfg, summary }
TextBox{parent=plc_cfg,x=1,y=2,height=1,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)} system.create(tool_ctl, main_pane, settings, divs, style, exit)
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)
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,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}
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_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,height=1,width=35,text="Please set a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
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,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,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 --#endregion
--#region Network --#region Config Change Log
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,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,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,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,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_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,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,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,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,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,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.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,height=1,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,height=1,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="By default, this project uses green/red heavily to distinguish ok and not, with some indicators also using multiple colors. By selecting a color blindness below, blues will be used instead of greens on indicators and multi-color indicators will be split up as space permits."}
local function recolor(value)
local c = themes.smooth_stone.color_modes[value]
if value == 1 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=8,height=1,text="Color Mode"}
local c_mode = RadioButton{parent=clr_c_2,x=1,y=9,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=20,y=8,height=1,text="Preview"}
local _ = IndLight{parent=clr_c_2,x=20,y=9,label="Good",colors=cpair(colors.black,colors.green)}
_ = IndLight{parent=clr_c_2,x=20,y=10,label="Warning",colors=cpair(colors.black,colors.yellow)}
_ = IndLight{parent=clr_c_2,x=20,y=11,label="Bad",colors=cpair(colors.black,colors.red)}
TextBox{parent=clr_c_2,x=1,y=14,height=6,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
tool_ctl.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,height=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,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=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 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(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 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)
try_set(fp_theme, ini_cfg.FrontPanelTheme)
try_set(c_mode, ini_cfg.ColorMode)
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,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)
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
-- Config Change Log
local cl = Div{parent=changelog,x=2,y=4,width=49} 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=49,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 for _, change in ipairs(changes) do
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg} TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
for _, v in ipairs(change[2]) do for _, v in ipairs(change[2]) do
local e = Div{parent=c_log,height=#util.strwrap(v,46)} 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)} TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
end end
end end
PushButton{parent=cl,x=1,y=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=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) --#region Self-Check
tmp_cfg.Networked = enable
if enable then u_id.set_max(4) else u_id.set_max(999) end
end
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 --#endregion
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)
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))
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,alignment=LEFT}
else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end
if f[1] == "AuthKey" then tool_ctl.auth_key_textbox = textbox end
end
end
end end
-- reset terminal screen -- reset terminal screen
@ -791,7 +261,7 @@ function configurator.configure(ask_config)
config_view(display) config_view(display)
while true do while true do
local event, param1, param2, param3 = util.pull_event() local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event -- handle event
if event == "timer" then if event == "timer" then
@ -804,6 +274,8 @@ function configurator.configure(ask_config)
if k_e then display.handle_key(k_e) end if k_e then display.handle_key(k_e) end
elseif event == "paste" then elseif event == "paste" then
display.handle_paste(param1) display.handle_paste(param1)
elseif event == "modem_message" then
check.receive_sv(param1, param2, param3, param4, param5)
end end
if event == "terminate" then return end if event == "terminate" then return end

View File

@ -40,7 +40,7 @@ local function init(panel)
local disabled_fg = style.fp.disabled_fg local disabled_fg = style.fp.disabled_fg
local header = TextBox{parent=panel,y=1,text="FISSION REACTOR PLC - UNIT ?",alignment=ALIGN.CENTER,height=1,fg_bg=style.theme.header} 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) header.register(databus.ps, "unit_id", function (id) header.set_value(util.c("FISSION REACTOR PLC - UNIT ", id)) end)
-- --
@ -60,12 +60,12 @@ local function init(panel)
local modem = LED{parent=system,label="MODEM",colors=ind_grn} local modem = LED{parent=system,label="MODEM",colors=ind_grn}
if not style.colorblind then if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}} 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.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(databus.ps, "link_state", network.update) network.register(databus.ps, "link_state", network.update)
else else
local nt_lnk = LEDPair{parent=system,label="NT LINKED",off=colors.red_off,c1=colors.red,c2=colors.green} 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=colors.red_off,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} local nt_col = LED{parent=system,label="NT COLLISION",colors=ind_red}
nt_lnk.register(databus.ps, "link_state", function (state) nt_lnk.register(databus.ps, "link_state", function (state)
@ -115,7 +115,7 @@ local function init(panel)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID()) local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=5,width=6,height=1,text=comp_id,fg_bg=disabled_fg} TextBox{parent=system,x=9,y=5,width=6,text=comp_id,fg_bg=disabled_fg}
-- --
-- status & controls -- status & controls
@ -148,8 +148,8 @@ local function init(panel)
-- --
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=disabled_fg} 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",alignment=ALIGN.LEFT,height=1} 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",alignment=ALIGN.LEFT,height=1} 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) 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) comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)

View File

@ -29,7 +29,13 @@ function style.set_theme(fp, color_mode)
style.fp = themes.get_fp_style(style.theme) style.fp = themes.get_fp_style(style.theme)
style.colorblind = color_mode ~= themes.COLOR_MODE.STANDARD 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 end
return style return style

View File

@ -6,6 +6,8 @@ local rsio = require("scada-common.rsio")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local themes = require("graphics.themes")
local databus = require("reactor-plc.databus") local databus = require("reactor-plc.databus")
local plc = {} 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 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 -- 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_SCRAM_MSG = "Scram requires the reactor to be active."
local PCALL_START_MSG = "pcall: Reactor is already active." local PCALL_START_MSG = "Reactor is already active."
---@type plc_config ---@type plc_config
local config = {} local config = {}
@ -55,41 +57,47 @@ function plc.load_config()
config.FrontPanelTheme = settings.get("FrontPanelTheme") config.FrontPanelTheme = settings.get("FrontPanelTheme")
config.ColorMode = settings.get("ColorMode") 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() local cfv = util.new_validator()
cfv.assert_type_bool(config.Networked) cfv.assert_type_bool(cfg.Networked)
cfv.assert_type_int(config.UnitID) cfv.assert_type_int(cfg.UnitID)
cfv.assert_type_bool(config.EmerCoolEnable) cfv.assert_type_bool(cfg.EmerCoolEnable)
if config.Networked == true then if cfg.Networked == true then
cfv.assert_channel(config.SVR_Channel) cfv.assert_channel(cfg.SVR_Channel)
cfv.assert_channel(config.PLC_Channel) cfv.assert_channel(cfg.PLC_Channel)
cfv.assert_type_num(config.ConnTimeout) cfv.assert_type_num(cfg.ConnTimeout)
cfv.assert_min(config.ConnTimeout, 2) cfv.assert_min(cfg.ConnTimeout, 2)
cfv.assert_type_num(config.TrustedRange) cfv.assert_type_num(cfg.TrustedRange)
cfv.assert_min(config.TrustedRange, 0) cfv.assert_min(cfg.TrustedRange, 0)
cfv.assert_type_str(config.AuthKey) cfv.assert_type_str(cfg.AuthKey)
if type(config.AuthKey) == "string" then if type(cfg.AuthKey) == "string" then
local len = string.len(config.AuthKey) local len = string.len(cfg.AuthKey)
cfv.assert_eq(len == 0 or len >= 8, true) cfv.assert(len == 0 or len >= 8)
end end
end end
cfv.assert_type_int(config.LogMode) cfv.assert_type_int(cfg.LogMode)
cfv.assert_range(config.LogMode, 0, 1) cfv.assert_range(cfg.LogMode, 0, 1)
cfv.assert_type_str(config.LogPath) cfv.assert_type_str(cfg.LogPath)
cfv.assert_type_bool(config.LogDebug) cfv.assert_type_bool(cfg.LogDebug)
cfv.assert_type_int(config.FrontPanelTheme) cfv.assert_type_int(cfg.FrontPanelTheme)
cfv.assert_range(config.FrontPanelTheme, 1, 2) cfv.assert_range(cfg.FrontPanelTheme, 1, 2)
cfv.assert_type_int(config.ColorMode) cfv.assert_type_int(cfg.ColorMode)
cfv.assert_range(config.ColorMode, 1, 4) cfv.assert_range(cfg.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
-- check emergency coolant configuration if enabled -- check emergency coolant configuration if enabled
if config.EmerCoolEnable then if cfg.EmerCoolEnable then
cfv.assert_eq(rsio.is_valid_side(config.EmerCoolSide), true) cfv.assert_eq(rsio.is_valid_side(cfg.EmerCoolSide), true)
cfv.assert_eq(config.EmerCoolColor == nil or rsio.is_color(config.EmerCoolColor), true) cfv.assert_eq(cfg.EmerCoolColor == nil or rsio.is_color(cfg.EmerCoolColor), true)
end end
return cfv.valid() return cfv.valid()
@ -144,9 +152,9 @@ function plc.rps_init(reactor, is_formed)
local function _check_and_handle_ppm_call(result) local function _check_and_handle_ppm_call(result)
if result == ppm.ACCESS_FAULT then if result == ppm.ACCESS_FAULT then
_set_fault() _set_fault()
elseif result == ppm.UNDEFINED_FIELD then
_set_fault() -- if undefined, then the reactor isn't formed
self.formed = false if reactor.__p_last_fault() == ppm.UNDEFINED_FIELD then self.formed = false end
else return true end else return true end
return false return false
@ -305,7 +313,7 @@ function plc.rps_init(reactor, is_formed)
log.info("RPS: reactor SCRAM") log.info("RPS: reactor SCRAM")
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") log.error("RPS: failed reactor SCRAM")
return false return false
else else
@ -323,7 +331,7 @@ function plc.rps_init(reactor, is_formed)
log.info("RPS: reactor start") log.info("RPS: reactor start")
reactor.activate() 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") log.error("RPS: failed reactor start")
else else
self.reactor_enabled = true self.reactor_enabled = true
@ -522,8 +530,8 @@ end
function plc.comms(version, nic, reactor, rps, conn_watchdog) function plc.comms(version, nic, reactor, rps, conn_watchdog)
local self = { local self = {
sv_addr = comms.BROADCAST, sv_addr = comms.BROADCAST,
seq_num = 0, seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, r_seq_num = nil, ---@type nil|integer
scrammed = false, scrammed = false,
linked = false, linked = false,
last_est_ack = ESTABLISH_ACK.ALLOW, last_est_ack = ESTABLISH_ACK.ALLOW,
@ -569,33 +577,17 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
end end
-- variable reactor status information, excluding heating rate -- dynamic reactor status information, excluding heating rate
---@return table data_table, boolean faulted ---@return table data_table, boolean faulted
local function _reactor_status() local function _get_reactor_status()
local fuel = nil local fuel = nil
local waste = nil local waste = nil
local coolant = nil local coolant = nil
local hcoolant = nil local hcoolant = nil
local data_table = { local data_table = {}
false, -- getStatus
0, -- getBurnRate reactor.__p_disable_afc()
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 tasks = { local tasks = {
function () data_table[1] = reactor.getStatus() end, function () data_table[1] = reactor.getStatus() end,
@ -635,30 +627,32 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
data_table[16] = hcoolant.amount data_table[16] = hcoolant.amount
end end
reactor.__p_enable_afc()
return data_table, reactor.__p_is_faulted() return data_table, reactor.__p_is_faulted()
end end
-- update the status cache if changed -- update the status cache if changed
---@return boolean changed ---@return boolean changed
local function _update_status_cache() local function _update_status_cache()
local status, faulted = _reactor_status() local status, faulted = _get_reactor_status()
local changed = false 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 for i = 1, #status do
if status[i] ~= self.status_cache[i] then if status[i] ~= self.status_cache[i] then
changed = true changed = true
break break
end end
end end
else
changed = true
end end
else
changed = true
end
if changed and not faulted then if changed then
self.status_cache = status self.status_cache = status
end
end end
return changed return changed
@ -677,9 +671,11 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
_send(msg_type, { status }) _send(msg_type, { status })
end end
-- send structure properties (these should not change, server will cache these) -- send static structure properties, cached by server
local function _send_struct() 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 = { local tasks = {
function () mek_data[1] = reactor.getLength() end, function () mek_data[1] = reactor.getLength() end,
@ -703,6 +699,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
_send(RPLC_TYPE.MEK_STRUCT, mek_data) _send(RPLC_TYPE.MEK_STRUCT, mek_data)
self.resend_build = false self.resend_build = false
end end
reactor.__p_enable_afc()
end end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
@ -833,16 +831,16 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
if l_chan == config.PLC_Channel then if l_chan == config.PLC_Channel then
-- check sequence number -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num() self.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.linked and ((self.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then elseif self.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) log.warning("sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return return
elseif self.linked and (src_addr ~= self.sv_addr) then 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 .. log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr ..
"); channel in use by another system?") "); channel in use by another system?")
return return
else else
self.r_seq_num = packet.scada_frame.seq_num() self.r_seq_num = packet.scada_frame.seq_num() + 1
end end
-- feed the watchdog first so it doesn't uhh...eat our packets :) -- feed the watchdog first so it doesn't uhh...eat our packets :)
@ -1028,10 +1026,9 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
println_ts("linked!") println_ts("linked!")
log.info("supervisor establish request approved, linked to SV (CID#" .. src_addr .. ")") 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.sv_addr = src_addr
self.linked = true self.linked = true
self.r_seq_num = nil
self.status_cache = nil self.status_cache = nil
if plc_state.reactor_formed then _send_struct() end if plc_state.reactor_formed then _send_struct() end

View File

@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer") local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads") local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "v1.7.0" local R_PLC_VERSION = "v1.8.6"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -55,6 +55,7 @@ log.info("========================================")
println(">> Reactor PLC " .. R_PLC_VERSION .. " <<") println(">> Reactor PLC " .. R_PLC_VERSION .. " <<")
crash.set_env("reactor-plc", R_PLC_VERSION) crash.set_env("reactor-plc", R_PLC_VERSION)
crash.dbg_log_env()
---------------------------------------- ----------------------------------------
-- main application -- main application
@ -144,13 +145,6 @@ local function main()
println("init> fission reactor is not formed") println("init> fission reactor is not formed")
log.warning("init> reactor logic adapter present, but reactor is not formed") log.warning("init> reactor logic adapter present, but reactor is not formed")
plc_state.degraded = true
plc_state.reactor_formed = false
elseif smem_dev.reactor.getStatus() == ppm.UNDEFINED_FIELD then
-- reactor formed after ppm.mount_all was called
println("init> fission reactor was not formed")
log.warning("init> reactor reported formed, but multiblock functions are not available")
plc_state.degraded = true plc_state.degraded = true
plc_state.reactor_formed = false plc_state.reactor_formed = false
end end
@ -185,6 +179,7 @@ local function main()
local message local message
plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode) plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode)
-- ...or not
if not plc_state.fp_ok then if not plc_state.fp_ok then
println_ts(util.c("UI error: ", message)) println_ts(util.c("UI error: ", message))
println("init> running without front panel") println("init> running without front panel")

View File

@ -71,76 +71,49 @@ function threads.thread__main(smem, init)
-- blink heartbeat indicator -- blink heartbeat indicator
databus.heartbeat() databus.heartbeat()
-- core clock tick -- start next clock timer
if networked then loop_clock.start()
-- start next clock timer
loop_clock.start()
-- send updated data -- send updated data
if nic.is_connected() then if networked and nic.is_connected() then
if plc_comms.is_linked() then if plc_comms.is_linked() then
smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS) 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 else
if ticks_to_update == 0 then ticks_to_update = ticks_to_update - 1
plc_comms.send_link_req()
ticks_to_update = LINK_TICKS
else
ticks_to_update = ticks_to_update - 1
end
end end
end 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 if (not plc_state.reactor_formed) and rps.is_formed() then
-- push a connect event and unmount it from the PPM -- reactor now formed
local iface = ppm.get_iface(plc_dev.reactor) plc_state.reactor_formed = true
if iface then
log.info("unmounting and remounting unformed reactor")
ppm.unmount(plc_dev.reactor)
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 -- SCRAM newly formed reactor
-- reconnect reactor smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
plc_dev.reactor = device
-- we need to assume formed here as we cannot check in this main loop -- determine if we are still in a degraded state
-- RPS will identify if it isn't and this will get set false later if (not networked) or nic.is_connected() then
plc_state.reactor_formed = true plc_state.degraded = false
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
-- partial reset of RPS, specific to becoming formed
rps.reset_formed()
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)
end 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 -- 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.reactor_formed = false
plc_state.degraded = true
end end
-- update indicators -- update indicators
@ -230,7 +203,8 @@ function threads.thread__main(smem, init)
plc_comms.reconnect_reactor(plc_dev.reactor) plc_comms.reconnect_reactor(plc_dev.reactor)
end end
-- partial reset of RPS, specific to becoming formed -- partial reset of RPS, specific to becoming formed/reconnected
-- without this, auto control can't resume on chunk load
rps.reset_formed() rps.reset_formed()
end end
elseif networked and type == "modem" then elseif networked and type == "modem" then
@ -368,9 +342,9 @@ function threads.thread__rps(smem)
end end
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 -- 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) -- check safety (SCRAM occurs if tripped)
if not plc_state.no_reactor then 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 if (type(cur_burn_rate) == "number") and (setpoints.burn_rate ~= cur_burn_rate) and rps.is_active() then
last_burn_sp = setpoints.burn_rate last_burn_sp = setpoints.burn_rate
-- update without ramp if <= 2.5 mB/t change -- update without ramp if <= 2.5 mB/t increase
running = math.abs(setpoints.burn_rate - cur_burn_rate) > 2.5 -- 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 if running then
log.debug(util.c("SPCTL: starting burn rate ramp from ", cur_burn_rate, " mB/t to ", setpoints.burn_rate, " mB/t")) log.debug(util.c("SPCTL: starting burn rate ramp from ", cur_burn_rate, " mB/t to ", setpoints.burn_rate, " mB/t"))

View File

@ -2,6 +2,7 @@
-- Configuration GUI -- Configuration GUI
-- --
local constants = require("scada-common.constants")
local log = require("scada-common.log") local log = require("scada-common.log")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio") local rsio = require("scada-common.rsio")
@ -33,51 +34,58 @@ local tri = util.trinary
local cpair = core.cpair local cpair = core.cpair
local IO = rsio.IO local IO = rsio.IO
local IO_LVL = rsio.IO_LVL
local IO_MODE = rsio.IO_MODE
local LEFT = core.ALIGN.LEFT local LEFT = core.ALIGN.LEFT
local CENTER = core.ALIGN.CENTER local CENTER = core.ALIGN.CENTER
local RIGHT = core.ALIGN.RIGHT local RIGHT = core.ALIGN.RIGHT
-- rsio port descriptions -- rsio port descriptions
local PORT_DESC = { local PORT_DESC_MAP = {
"Facility SCRAM", { IO.F_SCRAM, "Facility SCRAM" },
"Facility Acknowledge", { IO.F_ACK, "Facility Acknowledge" },
"Reactor SCRAM", { IO.R_SCRAM, "Reactor SCRAM" },
"Reactor RPS Reset", { IO.R_RESET, "Reactor RPS Reset" },
"Reactor Enable", { IO.R_ENABLE, "Reactor Enable" },
"Unit Acknowledge", { IO.U_ACK, "Unit Acknowledge" },
"Facility Alarm (high prio)", { IO.F_ALARM, "Facility Alarm (high prio)" },
"Facility Alarm (any)", { IO.F_ALARM_ANY, "Facility Alarm (any)" },
"Waste Plutonium Valve", { IO.F_MATRIX_LOW, "Induction Matrix < " .. (100 * constants.RS_THRESHOLDS.IMATRIX_CHARGE_LOW) .. "%" },
"Waste Polonium Valve", { IO.F_MATRIX_HIGH, "Induction Matrix > " .. (100 * constants.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH) .. "%" },
"Waste Po Pellets Valve", { IO.F_MATRIX_CHG, "Induction Matrix Charge %" },
"Waste Antimatter Valve", { IO.WASTE_PU, "Waste Plutonium Valve" },
"Reactor Active", { IO.WASTE_PO, "Waste Polonium Valve" },
"Reactor in Auto Control", { IO.WASTE_POPL, "Waste Po Pellets Valve" },
"RPS Tripped", { IO.WASTE_AM, "Waste Antimatter Valve" },
"RPS Auto SCRAM", { IO.R_ACTIVE, "Reactor Active" },
"RPS High Damage", { IO.R_AUTO_CTRL, "Reactor in Auto Control" },
"RPS High Temperature", { IO.R_SCRAMMED, "RPS Tripped" },
"RPS Low Coolant", { IO.R_AUTO_SCRAM, "RPS Auto SCRAM" },
"RPS Excess Heated Coolant", { IO.R_HIGH_DMG, "RPS High Damage" },
"RPS Excess Waste", { IO.R_HIGH_TEMP, "RPS High Temperature" },
"RPS Insufficient Fuel", { IO.R_LOW_COOLANT, "RPS Low Coolant" },
"RPS PLC Fault", { IO.R_EXCESS_HC, "RPS Excess Heated Coolant" },
"RPS Supervisor Timeout", { IO.R_EXCESS_WS, "RPS Excess Waste" },
"Unit Alarm", { IO.R_INSUFF_FUEL, "RPS Insufficient Fuel" },
"Unit Emergency Cool. Valve" { IO.R_PLC_FAULT, "RPS PLC Fault" },
{ IO.R_PLC_TIMEOUT, "RPS Supervisor Timeout" },
{ IO.U_ALARM, "Unit Alarm" },
{ IO.U_EMER_COOL, "Unit Emergency Cool. Valve" }
} }
-- designation (0 = facility, 1 = unit) -- designation (0 = facility, 1 = unit)
local PORT_DSGN = { [-1] = 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 } local PORT_DSGN = { [-1] = 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 }
assert(#PORT_DESC == rsio.NUM_PORTS) assert(#PORT_DESC_MAP == rsio.NUM_PORTS)
assert(#PORT_DSGN == rsio.NUM_PORTS) assert(#PORT_DSGN == rsio.NUM_PORTS)
-- changes to the config data/format to let the user know -- changes to the config data/format to let the user know
local changes = { local changes = {
{ "v1.7.9", { "ConnTimeout can now have a fractional part" } }, { "v1.7.9", { "ConnTimeout can now have a fractional part" } },
{ "v1.7.15", { "Added front panel UI theme", "Added color accessibility modes" } } { "v1.7.15", { "Added front panel UI theme", "Added color accessibility modes" } },
{ "v1.9.2", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
{ "v1.10.2", { "Re-organized peripheral configuration UI, resulting in some input fields being re-ordered" } }
} }
---@class rtu_rs_definition ---@class rtu_rs_definition
@ -151,7 +159,6 @@ local tool_ctl = {
p_idx = nil, ---@type graphics_element p_idx = nil, ---@type graphics_element
p_unit = nil, ---@type graphics_element p_unit = nil, ---@type graphics_element
p_assign_btn = nil, ---@type graphics_element p_assign_btn = nil, ---@type graphics_element
p_assign_end = nil, ---@type graphics_element
p_desc = nil, ---@type graphics_element p_desc = nil, ---@type graphics_element
p_desc_ext = nil, ---@type graphics_element p_desc_ext = nil, ---@type graphics_element
p_err = nil, ---@type graphics_element p_err = nil, ---@type graphics_element
@ -257,7 +264,7 @@ local function config_view(display)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local function exit() os.queueEvent("terminate") end local function exit() os.queueEvent("terminate") end
TextBox{parent=display,y=1,text="RTU Gateway Configurator",alignment=CENTER,height=1,fg_bg=style.header} TextBox{parent=display,y=1,text="RTU Gateway Configurator",alignment=CENTER,fg_bg=style.header}
local root_pane_div = Div{parent=display,x=1,y=2} local root_pane_div = Div{parent=display,x=1,y=2}
@ -278,7 +285,7 @@ local function config_view(display)
local y_start = 2 local y_start = 2
if tool_ctl.ask_config then 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 y_start = y_start + 5
else else
TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the RTU gateway configurator! Please select one of the following options."} TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the RTU gateway configurator! Please select one of the following options."}
@ -321,7 +328,7 @@ local function config_view(display)
end 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=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(7)end,fg_bg=nav_fg_bg,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(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
if not tool_ctl.has_config then if not tool_ctl.has_config then
@ -337,7 +344,7 @@ local function config_view(display)
local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49} local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49}
TextBox{parent=spkr_cfg,x=1,y=2,height=1,text=" Speaker Configuration",fg_bg=cpair(colors.black,colors.cyan)} TextBox{parent=spkr_cfg,x=1,y=2,text=" Speaker Configuration",fg_bg=cpair(colors.black,colors.cyan)}
TextBox{parent=spkr_c,x=1,y=1,height=2,text="Speakers can be connected to this RTU gateway without RTU unit configuration entries."} TextBox{parent=spkr_c,x=1,y=1,height=2,text="Speakers can be connected to this RTU gateway without RTU unit configuration entries."}
TextBox{parent=spkr_c,x=1,y=4,height=3,text="You can change the speaker audio volume from the default. The range is 0.0 to 3.0, where 1.0 is standard volume."} TextBox{parent=spkr_c,x=1,y=4,height=3,text="You can change the speaker audio volume from the default. The range is 0.0 to 3.0, where 1.0 is standard volume."}
@ -346,7 +353,7 @@ local function config_view(display)
TextBox{parent=spkr_c,x=1,y=10,height=3,text="Note: alarm sine waves are at half scale so that multiple will be required to reach full scale.",fg_bg=g_lg_fg_bg} TextBox{parent=spkr_c,x=1,y=10,height=3,text="Note: alarm sine waves are at half scale so that multiple will be required to reach full scale.",fg_bg=g_lg_fg_bg}
local s_vol_err = TextBox{parent=spkr_c,x=8,y=14,height=1,width=35,text="Please set a volume.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local s_vol_err = TextBox{parent=spkr_c,x=8,y=14,width=35,text="Please set a volume.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_vol() local function submit_vol()
local vol = tonumber(s_vol.get_value()) local vol = tonumber(s_vol.get_value())
@ -370,19 +377,19 @@ local function config_view(display)
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}} local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}}
TextBox{parent=net_cfg,x=1,y=2,height=1,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} 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,height=1,text="Please set the network channels below."} 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=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"} 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} 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=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="RTU Channel"} TextBox{parent=net_c_1,x=1,y=11,text="RTU Channel"}
local rtu_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg} local rtu_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=9,y=12,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_1,x=9,y=12,height=4,text="[RTU_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 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 function submit_channels()
local svr_c = tonumber(svr_chan.get_value()) local svr_c = tonumber(svr_chan.get_value())
@ -404,16 +411,16 @@ local function config_view(display)
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=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=net_c_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,height=1,text="Connection Timeout"} 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} 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=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=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"} 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} local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=1,y=10,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_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 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 function submit_ct_tr()
local timeout_val = tonumber(timeout.get_value()) local timeout_val = tonumber(timeout.get_value())
@ -438,17 +445,17 @@ local function config_view(display)
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=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=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"} 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 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 function censor_key(enable) censor(tri(enable, "*", nil)) end
local hide_key = CheckBox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key} local hide_key = CheckBox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
hide_key.set_value(true) hide_key.set_value(true)
censor_key(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 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 function submit_auth()
local v = key.get_value() local v = key.get_value()
@ -468,20 +475,20 @@ local function config_view(display)
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49} 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_cfg,x=1,y=2,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=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,height=1,text="Log File Mode"} TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},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"} 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 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)} 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} 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 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() local function submit_log()
if path.get_value() ~= "" then if path.get_value() ~= "" then
@ -509,20 +516,35 @@ local function config_view(display)
local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}} 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,height=1,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)} 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=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=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,height=1,text="Front Panel Theme"} TextBox{parent=clr_c_1,x=1,y=7,text="Front Panel Theme"}
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,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="By default, this project uses green/red heavily to distinguish ok and not, with some indicators also using multiple colors. By selecting a color blindness below, blues will be used instead of greens on indicators and multi-color indicators will be split up as space permits."} 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 function recolor(value)
local c = themes.smooth_stone.color_modes[value] local c = themes.smooth_stone.color_modes[value]
if value == 1 then 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 for i = 1, #style.colors do term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end
else else
term.setPaletteColor(colors.green, c[1].hex) term.setPaletteColor(colors.green, c[1].hex)
@ -531,20 +553,15 @@ local function config_view(display)
end end
end end
TextBox{parent=clr_c_2,x=1,y=8,height=1,text="Color Mode"} 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=9,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} 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=20,y=8,height=1,text="Preview"} 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}
local _ = IndLight{parent=clr_c_2,x=20,y=9,label="Good",colors=cpair(colors.black,colors.green)}
_ = IndLight{parent=clr_c_2,x=20,y=10,label="Warning",colors=cpair(colors.black,colors.yellow)}
_ = IndLight{parent=clr_c_2,x=20,y=11,label="Bad",colors=cpair(colors.black,colors.red)}
TextBox{parent=clr_c_2,x=1,y=14,height=6,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} 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() local function back_from_colors()
main_pane.set_value(util.trinary(tool_ctl.jumped_to_color, 1, 4)) main_pane.set_value(tri(tool_ctl.jumped_to_color, 1, 4))
tool_ctl.jumped_to_color = false tool_ctl.jumped_to_color = false
recolor(1) recolor(1)
end end
@ -586,7 +603,7 @@ local function config_view(display)
tool_ctl.color_apply.hide(true) tool_ctl.color_apply.hide(true)
TextBox{parent=clr_c_3,x=1,y=1,height=1,text="Settings saved!"} 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=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=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=clr_c_3,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
@ -608,7 +625,7 @@ local function config_view(display)
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4,sum_c_5,sum_c_6,sum_c_7}} local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4,sum_c_5,sum_c_6,sum_c_7}}
TextBox{parent=summary,x=1,y=2,height=1,text=" Summary",fg_bg=cpair(colors.black,colors.green)} 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 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)}
@ -633,8 +650,11 @@ local function config_view(display)
---@param exclude_conns boolean? true to exclude saving peripheral/redstone connections ---@param exclude_conns boolean? true to exclude saving peripheral/redstone connections
local function save_and_continue(exclude_conns) local function save_and_continue(exclude_conns)
for k, v in pairs(tmp_cfg) do for _, field in ipairs(fields) do
if not (exclude_conns and (k == "Peripherals" or k == "Redstone")) then settings.set(k, v) end local k, v = field[1], tmp_cfg[field[1]]
if not (exclude_conns and (k == "Peripherals" or k == "Redstone")) then
if v == nil then settings.unset(k) else settings.set(k, v) end
end
end end
-- always set these if missing -- always set these if missing
@ -681,13 +701,13 @@ local function config_view(display)
tool_ctl.settings_confirm = PushButton{parent=sum_c_1,x=41,y=14,min_width=9,text="Confirm",callback=function()sum_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} tool_ctl.settings_confirm = PushButton{parent=sum_c_1,x=41,y=14,min_width=9,text="Confirm",callback=function()sum_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
tool_ctl.settings_confirm.hide() tool_ctl.settings_confirm.hide()
TextBox{parent=sum_c_2,x=1,y=1,height=1,text="The following peripherals will be imported:"} TextBox{parent=sum_c_2,x=1,y=1,text="The following peripherals will be imported:"}
local peri_import_list = ListBox{parent=sum_c_2,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local peri_import_list = ListBox{parent=sum_c_2,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
PushButton{parent=sum_c_2,x=1,y=14,text="\x1b Back",callback=function()sum_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=sum_c_2,x=1,y=14,text="\x1b Back",callback=function()sum_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_2,x=41,y=14,min_width=9,text="Confirm",callback=function()sum_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} PushButton{parent=sum_c_2,x=41,y=14,min_width=9,text="Confirm",callback=function()sum_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
TextBox{parent=sum_c_3,x=1,y=1,height=1,text="The following redstone entries will be imported:"} TextBox{parent=sum_c_3,x=1,y=1,text="The following redstone entries will be imported:"}
local rs_import_list = ListBox{parent=sum_c_3,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local rs_import_list = ListBox{parent=sum_c_3,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
PushButton{parent=sum_c_3,x=1,y=14,text="\x1b Back",callback=function()sum_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=sum_c_3,x=1,y=14,text="\x1b Back",callback=function()sum_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
@ -703,7 +723,7 @@ local function config_view(display)
show_rs_conns() show_rs_conns()
end end
TextBox{parent=sum_c_4,x=1,y=1,height=1,text="Settings saved!"} TextBox{parent=sum_c_4,x=1,y=1,text="Settings saved!"}
TextBox{parent=sum_c_4,x=1,y=3,height=4,text="Remember to configure any peripherals or redstone that you have connected to this RTU gateway if you have not already done so, or if you have added, removed, or modified any of them."} TextBox{parent=sum_c_4,x=1,y=3,height=4,text="Remember to configure any peripherals or redstone that you have connected to this RTU gateway if you have not already done so, or if you have added, removed, or modified any of them."}
PushButton{parent=sum_c_4,x=1,y=8,min_width=24,text="Peripheral Connections",callback=jump_peri_conns,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg} PushButton{parent=sum_c_4,x=1,y=8,min_width=24,text="Peripheral Connections",callback=jump_peri_conns,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_4,x=1,y=10,min_width=22,text="Redstone Connections",callback=jump_rs_conns,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg} PushButton{parent=sum_c_4,x=1,y=10,min_width=22,text="Redstone Connections",callback=jump_rs_conns,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg}
@ -735,15 +755,15 @@ local function config_view(display)
local cl = Div{parent=changelog,x=2,y=4,width=49} 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=49,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 for _, change in ipairs(changes) do
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg} TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
for _, v in ipairs(change[2]) do for _, v in ipairs(change[2]) do
local e = Div{parent=c_log,height=#util.strwrap(v,46)} 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)} TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
end end
end end
@ -764,7 +784,7 @@ local function config_view(display)
local peri_pane = MultiPane{parent=peri_cfg,x=1,y=4,panes={peri_c_1,peri_c_2,peri_c_3,peri_c_4,peri_c_5,peri_c_6,peri_c_7}} local peri_pane = MultiPane{parent=peri_cfg,x=1,y=4,panes={peri_c_1,peri_c_2,peri_c_3,peri_c_4,peri_c_5,peri_c_6,peri_c_7}}
TextBox{parent=peri_cfg,x=1,y=2,height=1,text=" Peripheral Connections",fg_bg=cpair(colors.black,colors.purple)} TextBox{parent=peri_cfg,x=1,y=2,text=" Peripheral Connections",fg_bg=cpair(colors.black,colors.purple)}
local peri_list = ListBox{parent=peri_c_1,x=1,y=1,height=12,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local peri_list = ListBox{parent=peri_c_1,x=1,y=1,height=12,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
@ -790,7 +810,7 @@ local function config_view(display)
PushButton{parent=peri_c_1,x=35,y=14,min_width=7,text="Add +",callback=function()peri_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} PushButton{parent=peri_c_1,x=35,y=14,min_width=7,text="Add +",callback=function()peri_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
PushButton{parent=peri_c_1,x=43,y=14,min_width=7,text="Apply",callback=peri_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} PushButton{parent=peri_c_1,x=43,y=14,min_width=7,text="Apply",callback=peri_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
TextBox{parent=peri_c_2,x=1,y=1,height=1,text="Select one of the below devices to use."} TextBox{parent=peri_c_2,x=1,y=1,text="Select one of the below devices to use."}
tool_ctl.ppm_devs = ListBox{parent=peri_c_2,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} tool_ctl.ppm_devs = ListBox{parent=peri_c_2,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
@ -811,53 +831,35 @@ local function config_view(display)
tool_ctl.p_name_msg.set_value("Configuring peripheral on '" .. name .. "':") tool_ctl.p_name_msg.set_value("Configuring peripheral on '" .. name .. "':")
tool_ctl.p_desc_ext.set_value("") tool_ctl.p_desc_ext.set_value("")
local function reposition(prompt, idx_x, idx_max, unit_x, unit_y, desc_y)
tool_ctl.p_prompt.set_value(prompt)
tool_ctl.p_idx.reposition(idx_x, 4)
tool_ctl.p_idx.enable()
tool_ctl.p_idx.set_max(idx_max)
tool_ctl.p_idx.show()
tool_ctl.p_unit.reposition(unit_x, unit_y)
tool_ctl.p_unit.enable()
tool_ctl.p_unit.show()
tool_ctl.p_desc.reposition(1, desc_y)
end
if type == "boilerValve" then if type == "boilerValve" then
tool_ctl.p_prompt.set_value("This is the # boiler for reactor unit # .") reposition("This is reactor unit # 's # boiler.", 31, 2, 23, 4, 7)
tool_ctl.p_idx.show()
tool_ctl.p_idx.redraw()
tool_ctl.p_idx.enable()
tool_ctl.p_idx.set_max(2)
tool_ctl.p_unit.reposition(44, 4)
tool_ctl.p_unit.enable()
tool_ctl.p_unit.show()
tool_ctl.p_assign_btn.hide(true) tool_ctl.p_assign_btn.hide(true)
tool_ctl.p_assign_end.hide(true) tool_ctl.p_desc.set_value("Each unit can have at most 2 boilers. Boiler #1 shows up first on the main display, followed by boiler #2 below it. The numberings are per unit (unit 1 and unit 2 would both have a boiler #1 if each had one boiler) and can be split amongst multiple RTUs (one has #1, another has #2).")
tool_ctl.p_desc.reposition(1, 7)
tool_ctl.p_desc.set_value("Each unit can have at most 2 boilers. Boiler #1 shows up first on the main display, followed by boiler #2 below it. These numberings are independent of which RTU they are connected to. For example, one RTU can have boiler #1 and another can have #2, but both cannot have #1.")
elseif type == "turbineValve" then elseif type == "turbineValve" then
tool_ctl.p_prompt.set_value("This is the # turbine for reactor unit # .") reposition("This is reactor unit # 's # turbine.", 31, 3, 23, 4, 7)
tool_ctl.p_idx.show()
tool_ctl.p_idx.redraw()
tool_ctl.p_idx.enable()
tool_ctl.p_idx.set_max(3)
tool_ctl.p_unit.reposition(45, 4)
tool_ctl.p_unit.enable()
tool_ctl.p_unit.show()
tool_ctl.p_assign_btn.hide(true) tool_ctl.p_assign_btn.hide(true)
tool_ctl.p_assign_end.hide(true) tool_ctl.p_desc.set_value("Each unit can have at most 3 turbines. Turbine #1 shows up first on the main display, followed by #2 then #3 below it. The numberings are per unit (unit 1 and unit 2 would both have a turbine #1) and can be split amongst multiple RTUs (one has #1, another has #2).")
tool_ctl.p_desc.reposition(1, 7)
tool_ctl.p_desc.set_value("Each unit can have at most 3 turbines. Turbine #1 shows up first on the main display, followed by #2 then #3 below it. These numberings are independent of which RTU they are connected to. For example, one RTU can have turbine #1 and another can have #2, but both cannot have #1.")
elseif type == "solarNeutronActivator" then elseif type == "solarNeutronActivator" then
reposition("This SNA is for reactor unit # .", 46, 1, 31, 4, 7)
tool_ctl.p_idx.hide() tool_ctl.p_idx.hide()
tool_ctl.p_prompt.set_value("This SNA is for reactor unit # .")
tool_ctl.p_unit.reposition(31, 4)
tool_ctl.p_unit.enable()
tool_ctl.p_unit.show()
tool_ctl.p_assign_btn.hide(true) tool_ctl.p_assign_btn.hide(true)
tool_ctl.p_assign_end.hide(true)
tool_ctl.p_desc_ext.set_value("Before adding lots of SNAs: multiply the \"PEAK\" rate on the flow monitor (after connecting at least 1 SNA) by 10 to get the mB/t of waste that they can process. Enough SNAs to provide 2x to 3x of your max burn rate should be a good margin to catch up after night or cloudy weather. Too many devices (such as SNAs) on one RTU can cause lag.") tool_ctl.p_desc_ext.set_value("Before adding lots of SNAs: multiply the \"PEAK\" rate on the flow monitor (after connecting at least 1 SNA) by 10 to get the mB/t of waste that they can process. Enough SNAs to provide 2x to 3x of your max burn rate should be a good margin to catch up after night or cloudy weather. Too many devices (such as SNAs) on one RTU can cause lag.")
elseif type == "dynamicValve" then elseif type == "dynamicValve" then
tool_ctl.p_prompt.set_value("This is the # dynamic tank for...") reposition("This is the below system's # dynamic tank.", 29, 4, 17, 6, 8)
tool_ctl.p_assign_btn.show() tool_ctl.p_assign_btn.show()
tool_ctl.p_assign_btn.redraw() tool_ctl.p_assign_btn.redraw()
tool_ctl.p_assign_end.show()
tool_ctl.p_assign_end.redraw()
tool_ctl.p_idx.show()
tool_ctl.p_idx.redraw()
tool_ctl.p_idx.set_max(4)
tool_ctl.p_unit.reposition(18, 6)
tool_ctl.p_unit.enable()
tool_ctl.p_unit.show()
if tool_ctl.p_assign_btn.get_value() == 1 then if tool_ctl.p_assign_btn.get_value() == 1 then
tool_ctl.p_idx.enable() tool_ctl.p_idx.enable()
@ -868,30 +870,19 @@ local function config_view(display)
tool_ctl.p_unit.enable() tool_ctl.p_unit.enable()
end end
tool_ctl.p_desc.reposition(1, 8)
tool_ctl.p_desc.set_value("Each reactor unit can have at most 1 tank and the facility can have at most 4. Each facility tank must have a unique # 1 through 4, regardless of where it is connected. Only a total of 4 tanks can be displayed on the flow monitor.") tool_ctl.p_desc.set_value("Each reactor unit can have at most 1 tank and the facility can have at most 4. Each facility tank must have a unique # 1 through 4, regardless of where it is connected. Only a total of 4 tanks can be displayed on the flow monitor.")
elseif type == "environmentDetector" then elseif type == "environmentDetector" then
tool_ctl.p_prompt.set_value("This is the # environment detector for...") reposition("This is the below system's # env. detector.", 29, 99, 17, 6, 8)
tool_ctl.p_assign_btn.show() tool_ctl.p_assign_btn.show()
tool_ctl.p_assign_btn.redraw() tool_ctl.p_assign_btn.redraw()
tool_ctl.p_assign_end.show()
tool_ctl.p_assign_end.redraw()
tool_ctl.p_idx.show()
tool_ctl.p_idx.redraw()
tool_ctl.p_idx.set_max(99)
tool_ctl.p_unit.reposition(18, 6)
tool_ctl.p_unit.enable()
tool_ctl.p_unit.show()
if tool_ctl.p_assign_btn.get_value() == 1 then tool_ctl.p_unit.disable() else tool_ctl.p_unit.enable() end if tool_ctl.p_assign_btn.get_value() == 1 then tool_ctl.p_unit.disable() else tool_ctl.p_unit.enable() end
tool_ctl.p_desc.reposition(1, 8)
tool_ctl.p_desc.set_value("You can connect more than one environment detector for a particular unit or the facility. In that case, the maximum radiation reading from those assigned to that particular unit or the facility will be used for alarms and display.") tool_ctl.p_desc.set_value("You can connect more than one environment detector for a particular unit or the facility. In that case, the maximum radiation reading from those assigned to that particular unit or the facility will be used for alarms and display.")
elseif type == "inductionPort" or type == "spsPort" then elseif type == "inductionPort" or type == "spsPort" then
local dev = util.trinary(type == "inductionPort", "induction matrix", "SPS") local dev = tri(type == "inductionPort", "induction matrix", "SPS")
tool_ctl.p_idx.hide(true) tool_ctl.p_idx.hide(true)
tool_ctl.p_unit.hide(true) tool_ctl.p_unit.hide(true)
tool_ctl.p_prompt.set_value("This is the " .. dev .. " for the facility.") tool_ctl.p_prompt.set_value("This is the " .. dev .. " for the facility.")
tool_ctl.p_assign_btn.hide(true) tool_ctl.p_assign_btn.hide(true)
tool_ctl.p_assign_end.hide(true)
tool_ctl.p_desc.reposition(1, 7) tool_ctl.p_desc.reposition(1, 7)
tool_ctl.p_desc.set_value("There can only be one of these devices per SCADA network, so it will be assigned as the sole " .. dev .. " for the facility. There must only be one of these across all the RTUs you have.") tool_ctl.p_desc.set_value("There can only be one of these devices per SCADA network, so it will be assigned as the sole " .. dev .. " for the facility. There must only be one of these across all the RTUs you have.")
else else
@ -912,13 +903,13 @@ local function config_view(display)
tool_ctl.ppm_devs.remove_all() tool_ctl.ppm_devs.remove_all()
for name, entry in pairs(mounts) do for name, entry in pairs(mounts) do
if util.table_contains(RTU_DEV_TYPES, entry.type) then if util.table_contains(RTU_DEV_TYPES, entry.type) then
local bkg = util.trinary(alternate, colors.white, colors.lightGray) local bkg = tri(alternate, colors.white, colors.lightGray)
---@cast entry ppm_entry ---@cast entry ppm_entry
local line = Div{parent=tool_ctl.ppm_devs,height=2,fg_bg=cpair(colors.black,bkg)} local line = Div{parent=tool_ctl.ppm_devs,height=2,fg_bg=cpair(colors.black,bkg)}
PushButton{parent=line,x=1,y=1,min_width=9,alignment=LEFT,height=1,text="> SELECT",callback=function()tool_ctl.peri_cfg_manual=false;new_peri(name,entry.type)end,fg_bg=cpair(colors.black,colors.purple),active_fg_bg=cpair(colors.white,colors.black)} PushButton{parent=line,x=1,y=1,min_width=9,alignment=LEFT,height=1,text="> SELECT",callback=function()tool_ctl.peri_cfg_manual=false;new_peri(name,entry.type)end,fg_bg=cpair(colors.black,colors.purple),active_fg_bg=cpair(colors.white,colors.black)}
TextBox{parent=line,x=11,y=1,height=1,text=name,fg_bg=cpair(colors.black,bkg)} TextBox{parent=line,x=11,y=1,text=name,fg_bg=cpair(colors.black,bkg)}
TextBox{parent=line,x=11,y=2,height=1,text=entry.type,fg_bg=cpair(colors.gray,bkg)} TextBox{parent=line,x=11,y=2,text=entry.type,fg_bg=cpair(colors.gray,bkg)}
alternate = not alternate alternate = not alternate
end end
@ -931,7 +922,7 @@ local function config_view(display)
TextBox{parent=peri_c_3,x=1,y=6,height=4,text="Peripheral Name"} TextBox{parent=peri_c_3,x=1,y=6,height=4,text="Peripheral Name"}
local p_name = TextField{parent=peri_c_3,x=1,y=7,width=49,height=1,max_len=128,fg_bg=bw_fg_bg} local p_name = TextField{parent=peri_c_3,x=1,y=7,width=49,height=1,max_len=128,fg_bg=bw_fg_bg}
local p_type = Radio2D{parent=peri_c_3,x=1,y=9,rows=4,columns=2,default=1,options=RTU_DEV_TYPES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple} local p_type = Radio2D{parent=peri_c_3,x=1,y=9,rows=4,columns=2,default=1,options=RTU_DEV_TYPES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple}
local man_p_err = TextBox{parent=peri_c_3,x=8,y=14,height=1,width=35,text="Please enter a peripheral name.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local man_p_err = TextBox{parent=peri_c_3,x=8,y=14,width=35,text="Please enter a peripheral name.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
man_p_err.hide(true) man_p_err.hide(true)
local function submit_manual_peri() local function submit_manual_peri()
@ -948,11 +939,10 @@ local function config_view(display)
tool_ctl.p_name_msg = TextBox{parent=peri_c_4,x=1,y=1,height=2,text=""} tool_ctl.p_name_msg = TextBox{parent=peri_c_4,x=1,y=1,height=2,text=""}
tool_ctl.p_prompt = TextBox{parent=peri_c_4,x=1,y=4,height=2,text=""} tool_ctl.p_prompt = TextBox{parent=peri_c_4,x=1,y=4,height=2,text=""}
tool_ctl.p_idx = NumberField{parent=peri_c_4,x=14,y=4,width=4,max_chars=2,min=1,max=2,default=1,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} tool_ctl.p_idx = NumberField{parent=peri_c_4,x=31,y=4,width=4,max_chars=2,min=1,max=2,default=1,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
tool_ctl.p_assign_btn = RadioButton{parent=peri_c_4,x=1,y=5,default=1,options={"the facility.","a unit. (unit #"},callback=function(v)tool_ctl.p_assign(v)end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple} tool_ctl.p_assign_btn = RadioButton{parent=peri_c_4,x=1,y=5,default=1,options={"the facility","reactor unit #"},callback=function(v)tool_ctl.p_assign(v)end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple}
tool_ctl.p_assign_end = TextBox{parent=peri_c_4,x=22,y=6,height=6,width=1,text=")"}
tool_ctl.p_unit = NumberField{parent=peri_c_4,x=44,y=4,width=4,max_chars=2,min=1,max=4,default=1,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} tool_ctl.p_unit = NumberField{parent=peri_c_4,x=23,y=4,width=4,max_chars=2,min=1,max=4,default=1,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
tool_ctl.p_unit.disable() tool_ctl.p_unit.disable()
function tool_ctl.p_assign(opt) function tool_ctl.p_assign(opt)
@ -971,7 +961,7 @@ local function config_view(display)
tool_ctl.p_desc = TextBox{parent=peri_c_4,x=1,y=7,height=6,text="",fg_bg=g_lg_fg_bg} tool_ctl.p_desc = TextBox{parent=peri_c_4,x=1,y=7,height=6,text="",fg_bg=g_lg_fg_bg}
tool_ctl.p_desc_ext = TextBox{parent=peri_c_4,x=1,y=6,height=7,text="",fg_bg=g_lg_fg_bg} tool_ctl.p_desc_ext = TextBox{parent=peri_c_4,x=1,y=6,height=7,text="",fg_bg=g_lg_fg_bg}
tool_ctl.p_err = TextBox{parent=peri_c_4,x=8,y=14,height=1,width=32,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} tool_ctl.p_err = TextBox{parent=peri_c_4,x=8,y=14,width=32,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
tool_ctl.p_err.hide(true) tool_ctl.p_err.hide(true)
local function back_from_peri_opts() local function back_from_peri_opts()
@ -1056,7 +1046,7 @@ local function config_view(display)
PushButton{parent=peri_c_4,x=1,y=14,text="\x1b Back",callback=back_from_peri_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=peri_c_4,x=1,y=14,text="\x1b Back",callback=back_from_peri_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=peri_c_4,x=41,y=14,min_width=9,text="Confirm",callback=save_peri_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} PushButton{parent=peri_c_4,x=41,y=14,min_width=9,text="Confirm",callback=save_peri_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
TextBox{parent=peri_c_5,x=1,y=1,height=1,text="Settings saved!"} TextBox{parent=peri_c_5,x=1,y=1,text="Settings saved!"}
PushButton{parent=peri_c_5,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=peri_c_5,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=peri_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=peri_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
@ -1074,12 +1064,13 @@ local function config_view(display)
local rs_c_4 = Div{parent=rs_cfg,x=2,y=4,width=49} local rs_c_4 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_5 = Div{parent=rs_cfg,x=2,y=4,width=49} local rs_c_5 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_6 = Div{parent=rs_cfg,x=2,y=4,width=49} local rs_c_6 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_7 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6}} local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7}}
TextBox{parent=rs_cfg,x=1,y=2,height=1,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)} TextBox{parent=rs_cfg,x=1,y=2,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)}
TextBox{parent=rs_c_1,x=1,y=1,height=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg} TextBox{parent=rs_c_1,x=1,y=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg}
local rs_list = ListBox{parent=rs_c_1,x=1,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local rs_list = ListBox{parent=rs_c_1,x=1,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function rs_revert() local function rs_revert()
@ -1107,7 +1098,7 @@ local function config_view(display)
TextBox{parent=rs_c_6,x=1,y=1,height=5,text="You already configured this input. There can only be one entry for each input.\n\nPlease select a different port."} TextBox{parent=rs_c_6,x=1,y=1,height=5,text="You already configured this input. There can only be one entry for each input.\n\nPlease select a different port."}
PushButton{parent=rs_c_6,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_6,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_2,x=1,y=1,height=1,text="Select one of the below ports to use."} TextBox{parent=rs_c_2,x=1,y=1,text="Select one of the below ports to use."}
local rs_ports = ListBox{parent=rs_c_2,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local rs_ports = ListBox{parent=rs_c_2,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
@ -1132,9 +1123,23 @@ local function config_view(display)
text = "You selected the ALL_WASTE shortcut." text = "You selected the ALL_WASTE shortcut."
else else
tool_ctl.rs_cfg_shortcut.hide(true) tool_ctl.rs_cfg_shortcut.hide(true)
tool_ctl.rs_cfg_side_l.set_value(util.trinary(rsio.get_io_dir(port) == rsio.IO_DIR.IN, "Input Side", "Output Side")) tool_ctl.rs_cfg_side_l.set_value(tri(rsio.get_io_dir(port) == rsio.IO_DIR.IN, "Input Side", "Output Side"))
tool_ctl.rs_cfg_color.show() tool_ctl.rs_cfg_color.show()
text = "You selected " .. rsio.to_string(port) .. " (for "
local io_type = "analog input "
local io_mode = rsio.get_io_mode(port)
local inv = tri(rsio.digital_is_active(port, IO_LVL.LOW) == true, "inverted ", "")
if io_mode == IO_MODE.DIGITAL_IN then
io_type = inv .. "digital input "
elseif io_mode == IO_MODE.DIGITAL_OUT then
io_type = inv .. "digital output "
elseif io_mode == IO_MODE.ANALOG_OUT then
io_type = "analog output "
end
text = "You selected the " .. io_type .. rsio.to_string(port) .. " (for "
if PORT_DSGN[port] == 1 then if PORT_DSGN[port] == 1 then
text = text .. "a unit)." text = text .. "a unit)."
tool_ctl.rs_cfg_unit_l.show() tool_ctl.rs_cfg_unit_l.show()
@ -1154,27 +1159,37 @@ local function config_view(display)
-- add entries to redstone option list -- add entries to redstone option list
local all_w_macro = Div{parent=rs_ports,height=1} local all_w_macro = Div{parent=rs_ports,height=1}
PushButton{parent=all_w_macro,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">ALL_WASTE",callback=function()new_rs(-1)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.black)} PushButton{parent=all_w_macro,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">ALL_WASTE",callback=function()new_rs(-1)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.black)}
TextBox{parent=all_w_macro,x=16,y=1,width=5,height=1,text="[n/a]",fg_bg=cpair(colors.lightGray,colors.white)} TextBox{parent=all_w_macro,x=16,y=1,width=5,text="[n/a]",fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=all_w_macro,x=22,y=1,height=1,text="Create all 4 waste entries",fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=all_w_macro,x=22,y=1,text="Create all 4 waste entries",fg_bg=cpair(colors.gray,colors.white)}
for i = 1, rsio.NUM_PORTS do for i = 1, rsio.NUM_PORTS do
local name = rsio.to_string(i) local p = PORT_DESC_MAP[i][1]
local io_dir = util.trinary(rsio.get_io_dir(i) == rsio.IO_DIR.IN, "[in]", "[out]") local name = rsio.to_string(p)
local btn_color = util.trinary(rsio.get_io_dir(i) == rsio.IO_DIR.IN, colors.yellow, colors.lightBlue) local io_dir = tri(rsio.get_io_dir(p) == rsio.IO_DIR.IN, "[in]", "[out]")
local btn_color = tri(rsio.get_io_dir(p) == rsio.IO_DIR.IN, colors.yellow, colors.lightBlue)
local entry = Div{parent=rs_ports,height=1} local entry = Div{parent=rs_ports,height=1}
PushButton{parent=entry,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">"..name,callback=function()new_rs(i)end,fg_bg=cpair(colors.black,btn_color),active_fg_bg=cpair(colors.white,colors.black)} PushButton{parent=entry,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">"..name,callback=function()new_rs(p)end,fg_bg=cpair(colors.black,btn_color),active_fg_bg=cpair(colors.white,colors.black)}
TextBox{parent=entry,x=16,y=1,width=5,height=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)} TextBox{parent=entry,x=16,y=1,width=5,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=entry,x=22,y=1,height=1,text=PORT_DESC[i],fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=entry,x=22,y=1,text=PORT_DESC_MAP[i][2],fg_bg=cpair(colors.gray,colors.white)}
end end
PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
tool_ctl.rs_cfg_selection = TextBox{parent=rs_c_3,x=1,y=1,height=1,text=""} tool_ctl.rs_cfg_selection = TextBox{parent=rs_c_3,x=1,y=1,height=2,text=""}
tool_ctl.rs_cfg_unit_l = TextBox{parent=rs_c_3,x=27,y=3,width=7,height=1,text="Unit ID"} PushButton{parent=rs_c_3,x=36,y=3,text="What's that?",min_width=14,callback=function()rs_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
tool_ctl.rs_cfg_unit = NumberField{parent=rs_c_3,x=27,y=4,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
tool_ctl.rs_cfg_side_l = TextBox{parent=rs_c_3,x=1,y=3,width=11,height=1,text="Output Side"} TextBox{parent=rs_c_7,x=1,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"}
local side = Radio2D{parent=rs_c_3,x=1,y=4,rows=2,columns=3,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red} TextBox{parent=rs_c_7,x=1,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"}
TextBox{parent=rs_c_7,x=1,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"}
PushButton{parent=rs_c_7,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
tool_ctl.rs_cfg_side_l = TextBox{parent=rs_c_3,x=1,y=4,width=11,text="Output Side"}
local side = Radio2D{parent=rs_c_3,x=1,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red}
tool_ctl.rs_cfg_unit_l = TextBox{parent=rs_c_3,x=25,y=7,width=7,text="Unit ID"}
tool_ctl.rs_cfg_unit = NumberField{parent=rs_c_3,x=33,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
local function set_bundled(bundled) local function set_bundled(bundled)
if bundled then tool_ctl.rs_cfg_color.enable() else tool_ctl.rs_cfg_color.disable() end if bundled then tool_ctl.rs_cfg_color.enable() else tool_ctl.rs_cfg_color.disable() end
@ -1187,7 +1202,7 @@ local function config_view(display)
tool_ctl.rs_cfg_color = Radio2D{parent=rs_c_3,x=1,y=9,rows=4,columns=4,default=1,options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg} tool_ctl.rs_cfg_color = Radio2D{parent=rs_c_3,x=1,y=9,rows=4,columns=4,default=1,options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
tool_ctl.rs_cfg_color.disable() tool_ctl.rs_cfg_color.disable()
local rs_err = TextBox{parent=rs_c_3,x=8,y=14,height=1,width=30,text="Unit ID must be within 1 to 4.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local rs_err = TextBox{parent=rs_c_3,x=8,y=14,width=30,text="Unit ID must be within 1 to 4.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
rs_err.hide(true) rs_err.hide(true)
local function back_from_rs_opts() local function back_from_rs_opts()
@ -1205,10 +1220,10 @@ local function config_view(display)
if port >= 0 then if port >= 0 then
---@type rtu_rs_definition ---@type rtu_rs_definition
local def = { local def = {
unit = util.trinary(PORT_DSGN[port] == 1, u, nil), unit = tri(PORT_DSGN[port] == 1, u, nil),
port = port, port = port,
side = side_options_map[side.get_value()], side = side_options_map[side.get_value()],
color = util.trinary(bundled.get_value(), color_options_map[tool_ctl.rs_cfg_color.get_value()], nil) color = tri(bundled.get_value(), color_options_map[tool_ctl.rs_cfg_color.get_value()], nil)
} }
if tool_ctl.rs_cfg_editing == false then if tool_ctl.rs_cfg_editing == false then
@ -1222,10 +1237,10 @@ local function config_view(display)
local default_colors = { colors.red, colors.orange, colors.yellow, colors.lime } local default_colors = { colors.red, colors.orange, colors.yellow, colors.lime }
for i = 0, 3 do for i = 0, 3 do
table.insert(tmp_cfg.Redstone, { table.insert(tmp_cfg.Redstone, {
unit = util.trinary(PORT_DSGN[IO.WASTE_PU + i] == 1, u, nil), unit = tri(PORT_DSGN[IO.WASTE_PU + i] == 1, u, nil),
port = IO.WASTE_PU + i, port = IO.WASTE_PU + i,
side = util.trinary(bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]), side = tri(bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]),
color = util.trinary(bundled.get_value(), default_colors[i + 1], nil) color = tri(bundled.get_value(), default_colors[i + 1], nil)
}) })
end end
end end
@ -1243,7 +1258,7 @@ local function config_view(display)
PushButton{parent=rs_c_3,x=1,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_3,x=1,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_3,x=41,y=14,min_width=9,text="Confirm",callback=save_rs_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_3,x=41,y=14,min_width=9,text="Confirm",callback=save_rs_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_4,x=1,y=1,height=1,text="Settings saved!"} TextBox{parent=rs_c_4,x=1,y=1,text="Settings saved!"}
PushButton{parent=rs_c_4,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_4,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_4,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_4,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
@ -1278,7 +1293,7 @@ local function config_view(display)
peri_import_list.remove_all() peri_import_list.remove_all()
for _, entry in ipairs(config.RTU_DEVICES) do for _, entry in ipairs(config.RTU_DEVICES) do
local for_facility = entry.for_reactor == 0 local for_facility = entry.for_reactor == 0
local ini_unit = util.trinary(for_facility, nil, entry.for_reactor) local ini_unit = tri(for_facility, nil, entry.for_reactor)
local def = { name = entry.name, unit = ini_unit, index = entry.index } local def = { name = entry.name, unit = ini_unit, index = entry.index }
local mount = mounts[def.name] ---@type ppm_entry|nil local mount = mounts[def.name] ---@type ppm_entry|nil
@ -1344,9 +1359,9 @@ local function config_view(display)
end end
local line = Div{parent=peri_import_list,height=3} local line = Div{parent=peri_import_list,height=3}
TextBox{parent=line,x=1,y=1,height=1,text="@ "..def.name,fg_bg=cpair(colors.black,colors.white)} TextBox{parent=line,x=1,y=1,text="@ "..def.name,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=1,y=2,height=1,text=status,fg_bg=cpair(color,colors.white)} TextBox{parent=line,x=1,y=2,text=status,fg_bg=cpair(color,colors.white)}
TextBox{parent=line,x=1,y=3,height=1,text=desc,fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=line,x=1,y=3,text=desc,fg_bg=cpair(colors.gray,colors.white)}
end end
rs_import_list.remove_all() rs_import_list.remove_all()
@ -1357,7 +1372,7 @@ local function config_view(display)
table.insert(tmp_cfg.Redstone, def) table.insert(tmp_cfg.Redstone, def)
local name = rsio.to_string(def.port) local name = rsio.to_string(def.port)
local io_dir = util.trinary(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b") local io_dir = tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
local conn = def.side local conn = def.side
local unit = "facility" local unit = "facility"
@ -1365,10 +1380,10 @@ local function config_view(display)
if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end
local line = Div{parent=rs_import_list,height=1} local line = Div{parent=rs_import_list,height=1}
TextBox{parent=line,x=1,y=1,width=1,height=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)} TextBox{parent=line,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=line,x=2,y=1,width=14,height=1,text=name} TextBox{parent=line,x=2,y=1,width=14,text=name}
TextBox{parent=line,x=18,y=1,width=string.len(conn),height=1,text=conn,fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=line,x=18,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=line,x=40,y=1,height=1,text=unit,fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=line,x=40,y=1,text=unit,fg_bg=cpair(colors.gray,colors.white)}
end end
end end
@ -1420,7 +1435,7 @@ local function config_view(display)
local val = util.strval(raw) local val = util.strval(raw)
if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) 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] == "LogMode" then val = tri(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "FrontPanelTheme" then elseif f[1] == "FrontPanelTheme" then
val = util.strval(themes.fp_theme_name(raw)) val = util.strval(themes.fp_theme_name(raw))
elseif f[1] == "ColorMode" then elseif f[1] == "ColorMode" then
@ -1429,7 +1444,7 @@ local function config_view(display)
if val == "nil" then val = "<not set>" end if val == "nil" then val = "<not set>" end
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white)) local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate alternate = not alternate
if string.len(val) > val_max_w then if string.len(val) > val_max_w then
@ -1503,9 +1518,9 @@ local function config_view(display)
end end
local entry = Div{parent=peri_list,height=3} local entry = Div{parent=peri_list,height=3}
TextBox{parent=entry,x=1,y=1,height=1,text="@ "..def.name,fg_bg=cpair(colors.black,colors.white)} TextBox{parent=entry,x=1,y=1,text="@ "..def.name,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=entry,x=1,y=2,height=1,text=" \x1a "..t_str,fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=entry,x=1,y=2,text=" \x1a "..t_str,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=entry,x=1,y=3,height=1,text=desc,fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=entry,x=1,y=3,text=desc,fg_bg=cpair(colors.gray,colors.white)}
local edit_btn = PushButton{parent=entry,x=41,y=2,min_width=8,height=1,text="EDIT",callback=function()edit_peri_entry(i,def,t or "")end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} local edit_btn = PushButton{parent=entry,x=41,y=2,min_width=8,height=1,text="EDIT",callback=function()edit_peri_entry(i,def,t or "")end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
PushButton{parent=entry,x=41,y=3,min_width=8,height=1,text="DELETE",callback=function()delete_peri_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} PushButton{parent=entry,x=41,y=3,min_width=8,height=1,text="DELETE",callback=function()delete_peri_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
@ -1543,7 +1558,7 @@ local function config_view(display)
end end
tool_ctl.rs_cfg_selection.set_value(text) tool_ctl.rs_cfg_selection.set_value(text)
tool_ctl.rs_cfg_side_l.set_value(util.trinary(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "Input Side", "Output Side")) tool_ctl.rs_cfg_side_l.set_value(tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "Input Side", "Output Side"))
side.set_value(side_to_idx(def.side)) side.set_value(side_to_idx(def.side))
bundled.set_value(def.color ~= nil) bundled.set_value(def.color ~= nil)
tool_ctl.rs_cfg_color.set_value(value) tool_ctl.rs_cfg_color.set_value(value)
@ -1564,17 +1579,17 @@ local function config_view(display)
local def = cfg.Redstone[i] ---@type rtu_rs_definition local def = cfg.Redstone[i] ---@type rtu_rs_definition
local name = rsio.to_string(def.port) local name = rsio.to_string(def.port)
local io_dir = util.trinary(rsio.get_io_mode(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b") local io_dir = tri(rsio.get_io_mode(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
local conn = def.side local conn = def.side
local unit = util.strval(def.unit or "F") local unit = util.strval(def.unit or "F")
if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end
local entry = Div{parent=rs_list,height=1} local entry = Div{parent=rs_list,height=1}
TextBox{parent=entry,x=1,y=1,width=1,height=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)} TextBox{parent=entry,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=entry,x=2,y=1,width=14,height=1,text=name} TextBox{parent=entry,x=2,y=1,width=14,text=name}
TextBox{parent=entry,x=16,y=1,width=string.len(conn),height=1,text=conn,fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=entry,x=16,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=entry,x=33,y=1,width=1,height=1,text=unit,fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=entry,x=33,y=1,width=1,text=unit,fg_bg=cpair(colors.gray,colors.white)}
PushButton{parent=entry,x=35,y=1,min_width=6,height=1,text="EDIT",callback=function()edit_rs_entry(i)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} PushButton{parent=entry,x=35,y=1,min_width=6,height=1,text="EDIT",callback=function()edit_rs_entry(i)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
PushButton{parent=entry,x=41,y=1,min_width=8,height=1,text="DELETE",callback=function()delete_rs_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} PushButton{parent=entry,x=41,y=1,min_width=8,height=1,text="DELETE",callback=function()delete_rs_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
end end
@ -1627,9 +1642,11 @@ function configurator.configure(ask_config)
elseif event == "paste" then elseif event == "paste" then
display.handle_paste(param1) display.handle_paste(param1)
elseif event == "peripheral_detach" then elseif event == "peripheral_detach" then
---@diagnostic disable-next-line: discard-returns
ppm.handle_unmount(param1) ppm.handle_unmount(param1)
tool_ctl.update_peri_list() tool_ctl.update_peri_list()
elseif event == "peripheral" then elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1) ppm.mount(param1)
tool_ctl.update_peri_list() tool_ctl.update_peri_list()
end end

View File

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

View File

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

View File

@ -9,45 +9,36 @@ local imatrix_rtu = {}
function imatrix_rtu.new(imatrix) function imatrix_rtu.new(imatrix)
local unit = rtu.init_unit(imatrix) local unit = rtu.init_unit(imatrix)
-- disable auto fault clearing
imatrix.__p_clear_fault()
imatrix.__p_disable_afc()
-- discrete inputs -- -- discrete inputs --
unit.connect_di(imatrix.isFormed) unit.connect_di("isFormed")
-- coils -- -- coils --
-- none -- none
-- input registers -- -- input registers --
-- multiblock properties -- multiblock properties
unit.connect_input_reg(imatrix.getLength) unit.connect_input_reg("getLength")
unit.connect_input_reg(imatrix.getWidth) unit.connect_input_reg("getWidth")
unit.connect_input_reg(imatrix.getHeight) unit.connect_input_reg("getHeight")
unit.connect_input_reg(imatrix.getMinPos) unit.connect_input_reg("getMinPos")
unit.connect_input_reg(imatrix.getMaxPos) unit.connect_input_reg("getMaxPos")
-- build properties -- build properties
unit.connect_input_reg(imatrix.getMaxEnergy) unit.connect_input_reg("getMaxEnergy")
unit.connect_input_reg(imatrix.getTransferCap) unit.connect_input_reg("getTransferCap")
unit.connect_input_reg(imatrix.getInstalledCells) unit.connect_input_reg("getInstalledCells")
unit.connect_input_reg(imatrix.getInstalledProviders) unit.connect_input_reg("getInstalledProviders")
-- I/O rates -- I/O rates
unit.connect_input_reg(imatrix.getLastInput) unit.connect_input_reg("getLastInput")
unit.connect_input_reg(imatrix.getLastOutput) unit.connect_input_reg("getLastOutput")
-- tanks -- tanks
unit.connect_input_reg(imatrix.getEnergy) unit.connect_input_reg("getEnergy")
unit.connect_input_reg(imatrix.getEnergyNeeded) unit.connect_input_reg("getEnergyNeeded")
unit.connect_input_reg(imatrix.getEnergyFilledPercentage) unit.connect_input_reg("getEnergyFilledPercentage")
-- holding registers -- -- holding registers --
-- none -- none
-- check if any calls faulted return unit.interface(), false
local faulted = imatrix.__p_is_faulted()
imatrix.__p_clear_fault()
imatrix.__p_enable_afc()
return unit.interface(), faulted
end end
return imatrix_rtu return imatrix_rtu

View File

@ -9,50 +9,41 @@ local sps_rtu = {}
function sps_rtu.new(sps) function sps_rtu.new(sps)
local unit = rtu.init_unit(sps) local unit = rtu.init_unit(sps)
-- disable auto fault clearing
sps.__p_clear_fault()
sps.__p_disable_afc()
-- discrete inputs -- -- discrete inputs --
unit.connect_di(sps.isFormed) unit.connect_di("isFormed")
-- coils -- -- coils --
-- none -- none
-- input registers -- -- input registers --
-- multiblock properties -- multiblock properties
unit.connect_input_reg(sps.getLength) unit.connect_input_reg("getLength")
unit.connect_input_reg(sps.getWidth) unit.connect_input_reg("getWidth")
unit.connect_input_reg(sps.getHeight) unit.connect_input_reg("getHeight")
unit.connect_input_reg(sps.getMinPos) unit.connect_input_reg("getMinPos")
unit.connect_input_reg(sps.getMaxPos) unit.connect_input_reg("getMaxPos")
-- build properties -- build properties
unit.connect_input_reg(sps.getCoils) unit.connect_input_reg("getCoils")
unit.connect_input_reg(sps.getInputCapacity) unit.connect_input_reg("getInputCapacity")
unit.connect_input_reg(sps.getOutputCapacity) unit.connect_input_reg("getOutputCapacity")
unit.connect_input_reg(sps.getMaxEnergy) unit.connect_input_reg("getMaxEnergy")
-- current state -- current state
unit.connect_input_reg(sps.getProcessRate) unit.connect_input_reg("getProcessRate")
-- tanks -- tanks
unit.connect_input_reg(sps.getInput) unit.connect_input_reg("getInput")
unit.connect_input_reg(sps.getInputNeeded) unit.connect_input_reg("getInputNeeded")
unit.connect_input_reg(sps.getInputFilledPercentage) unit.connect_input_reg("getInputFilledPercentage")
unit.connect_input_reg(sps.getOutput) unit.connect_input_reg("getOutput")
unit.connect_input_reg(sps.getOutputNeeded) unit.connect_input_reg("getOutputNeeded")
unit.connect_input_reg(sps.getOutputFilledPercentage) unit.connect_input_reg("getOutputFilledPercentage")
unit.connect_input_reg(sps.getEnergy) unit.connect_input_reg("getEnergy")
unit.connect_input_reg(sps.getEnergyNeeded) unit.connect_input_reg("getEnergyNeeded")
unit.connect_input_reg(sps.getEnergyFilledPercentage) unit.connect_input_reg("getEnergyFilledPercentage")
-- holding registers -- -- holding registers --
-- none -- none
-- check if any calls faulted return unit.interface(), false
local faulted = sps.__p_is_faulted()
sps.__p_clear_fault()
sps.__p_enable_afc()
return unit.interface(), faulted
end end
return sps_rtu return sps_rtu

View File

@ -9,12 +9,8 @@ local turbinev_rtu = {}
function turbinev_rtu.new(turbine) function turbinev_rtu.new(turbine)
local unit = rtu.init_unit(turbine) local unit = rtu.init_unit(turbine)
-- disable auto fault clearing
turbine.__p_clear_fault()
turbine.__p_disable_afc()
-- discrete inputs -- -- discrete inputs --
unit.connect_di(turbine.isFormed) unit.connect_di("isFormed")
-- coils -- -- coils --
unit.connect_coil(function () turbine.incrementDumpingMode() end, function () end) unit.connect_coil(function () turbine.incrementDumpingMode() end, function () end)
@ -22,44 +18,39 @@ function turbinev_rtu.new(turbine)
-- input registers -- -- input registers --
-- multiblock properties -- multiblock properties
unit.connect_input_reg(turbine.getLength) unit.connect_input_reg("getLength")
unit.connect_input_reg(turbine.getWidth) unit.connect_input_reg("getWidth")
unit.connect_input_reg(turbine.getHeight) unit.connect_input_reg("getHeight")
unit.connect_input_reg(turbine.getMinPos) unit.connect_input_reg("getMinPos")
unit.connect_input_reg(turbine.getMaxPos) unit.connect_input_reg("getMaxPos")
-- build properties -- build properties
unit.connect_input_reg(turbine.getBlades) unit.connect_input_reg("getBlades")
unit.connect_input_reg(turbine.getCoils) unit.connect_input_reg("getCoils")
unit.connect_input_reg(turbine.getVents) unit.connect_input_reg("getVents")
unit.connect_input_reg(turbine.getDispersers) unit.connect_input_reg("getDispersers")
unit.connect_input_reg(turbine.getCondensers) unit.connect_input_reg("getCondensers")
unit.connect_input_reg(turbine.getSteamCapacity) unit.connect_input_reg("getSteamCapacity")
unit.connect_input_reg(turbine.getMaxEnergy) unit.connect_input_reg("getMaxEnergy")
unit.connect_input_reg(turbine.getMaxFlowRate) unit.connect_input_reg("getMaxFlowRate")
unit.connect_input_reg(turbine.getMaxProduction) unit.connect_input_reg("getMaxProduction")
unit.connect_input_reg(turbine.getMaxWaterOutput) unit.connect_input_reg("getMaxWaterOutput")
-- current state -- current state
unit.connect_input_reg(turbine.getFlowRate) unit.connect_input_reg("getFlowRate")
unit.connect_input_reg(turbine.getProductionRate) unit.connect_input_reg("getProductionRate")
unit.connect_input_reg(turbine.getLastSteamInputRate) unit.connect_input_reg("getLastSteamInputRate")
unit.connect_input_reg(turbine.getDumpingMode) unit.connect_input_reg("getDumpingMode")
-- tanks/containers -- tanks/containers
unit.connect_input_reg(turbine.getSteam) unit.connect_input_reg("getSteam")
unit.connect_input_reg(turbine.getSteamNeeded) unit.connect_input_reg("getSteamNeeded")
unit.connect_input_reg(turbine.getSteamFilledPercentage) unit.connect_input_reg("getSteamFilledPercentage")
unit.connect_input_reg(turbine.getEnergy) unit.connect_input_reg("getEnergy")
unit.connect_input_reg(turbine.getEnergyNeeded) unit.connect_input_reg("getEnergyNeeded")
unit.connect_input_reg(turbine.getEnergyFilledPercentage) unit.connect_input_reg("getEnergyFilledPercentage")
-- holding registers -- -- holding registers --
unit.connect_holding_reg(turbine.getDumpingMode, turbine.setDumpingMode) unit.connect_holding_reg("getDumpingMode", "setDumpingMode")
-- check if any calls faulted return unit.interface(), false
local faulted = turbine.__p_is_faulted()
turbine.__p_clear_fault()
turbine.__p_enable_afc()
return unit.interface(), faulted
end end
return turbinev_rtu return turbinev_rtu

View File

@ -35,7 +35,7 @@ local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC
local function init(panel, units) local function init(panel, units)
local disabled_fg = style.fp.disabled_fg local disabled_fg = style.fp.disabled_fg
TextBox{parent=panel,y=1,text="RTU GATEWAY",alignment=ALIGN.CENTER,height=1,fg_bg=style.theme.header} TextBox{parent=panel,y=1,text="RTU GATEWAY",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
-- --
-- system indicators -- system indicators
@ -53,12 +53,12 @@ local function init(panel, units)
local modem = LED{parent=system,label="MODEM",colors=ind_grn} local modem = LED{parent=system,label="MODEM",colors=ind_grn}
if not style.colorblind then if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}} 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.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(databus.ps, "link_state", network.update) network.register(databus.ps, "link_state", network.update)
else else
local nt_lnk = LEDPair{parent=system,label="NT LINKED",off=colors.red_off,c1=colors.red,c2=colors.green} 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=colors.red_off,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}
nt_lnk.register(databus.ps, "link_state", function (state) nt_lnk.register(databus.ps, "link_state", function (state)
local value = 2 local value = 2
@ -98,9 +98,9 @@ local function init(panel, units)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID()) local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=disabled_fg} TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=disabled_fg}
TextBox{parent=system,x=1,y=14,text="SPEAKERS",height=1,width=8,fg_bg=style.fp.text_fg} TextBox{parent=system,x=1,y=14,text="SPEAKERS",width=8,fg_bg=style.fp.text_fg}
local speaker_count = DataIndicator{parent=system,x=10,y=14,label="",format="%3d",value=0,width=3,fg_bg=style.theme.field_box} local speaker_count = DataIndicator{parent=system,x=10,y=14,label="",format="%3d",value=0,width=3,fg_bg=style.theme.field_box}
speaker_count.register(databus.ps, "speaker_count", speaker_count.update) speaker_count.register(databus.ps, "speaker_count", speaker_count.update)
@ -109,8 +109,8 @@ local function init(panel, units)
-- --
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=disabled_fg} 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",alignment=ALIGN.LEFT,height=1} 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",alignment=ALIGN.LEFT,height=1} 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) 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) comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@ -126,7 +126,7 @@ local function init(panel, units)
-- show routine statuses -- show routine statuses
for i = 1, list_length do for i = 1, list_length do
TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i),height=1} TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i)}
local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=ind_grn} local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=ind_grn}
rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update) rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update)
end end
@ -144,13 +144,13 @@ local function init(panel, units)
-- unit name identifier (type + index) -- unit name identifier (type + index)
local function get_name(t) return util.c(UNIT_TYPE_LABELS[t + 1], " ", util.trinary(util.is_int(unit.index), unit.index, "")) end local function get_name(t) return util.c(UNIT_TYPE_LABELS[t + 1], " ", util.trinary(util.is_int(unit.index), unit.index, "")) end
local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(unit.type),width=15,height=1} local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(unit.type),width=15}
name_box.register(databus.ps, "unit_type_" .. i, function (t) name_box.set_value(get_name(t)) end) name_box.register(databus.ps, "unit_type_" .. i, function (t) name_box.set_value(get_name(t)) end)
-- assignment (unit # or facility) -- assignment (unit # or facility)
local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor) local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor)
TextBox{parent=unit_hw_statuses,y=i,x=19,text=for_unit,height=1,fg_bg=disabled_fg} TextBox{parent=unit_hw_statuses,y=i,x=19,text=for_unit,fg_bg=disabled_fg}
end end
end end

View File

@ -28,7 +28,13 @@ function style.set_theme(fp, color_mode)
style.fp = themes.get_fp_style(style.theme) style.fp = themes.get_fp_style(style.theme)
style.colorblind = color_mode ~= themes.COLOR_MODE.STANDARD 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 end
return style return style

View File

@ -5,6 +5,8 @@ local log = require("scada-common.log")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local themes = require("graphics.themes")
local databus = require("rtu.databus") local databus = require("rtu.databus")
local modbus = require("rtu.modbus") local modbus = require("rtu.modbus")
@ -58,7 +60,7 @@ function rtu.load_config()
if type(config.AuthKey) == "string" then if type(config.AuthKey) == "string" then
local len = string.len(config.AuthKey) local len = string.len(config.AuthKey)
cfv.assert_eq(len == 0 or len >= 8, true) cfv.assert(len == 0 or len >= 8)
end end
cfv.assert_type_int(config.LogMode) cfv.assert_type_int(config.LogMode)
@ -69,7 +71,7 @@ function rtu.load_config()
cfv.assert_type_int(config.FrontPanelTheme) cfv.assert_type_int(config.FrontPanelTheme)
cfv.assert_range(config.FrontPanelTheme, 1, 2) cfv.assert_range(config.FrontPanelTheme, 1, 2)
cfv.assert_type_int(config.ColorMode) cfv.assert_type_int(config.ColorMode)
cfv.assert_range(config.ColorMode, 1, 4) cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
cfv.assert_type_table(config.Peripherals) cfv.assert_type_table(config.Peripherals)
cfv.assert_type_table(config.Redstone) cfv.assert_type_table(config.Redstone)
@ -92,6 +94,8 @@ function rtu.init_unit(device)
local insert = table.insert local insert = table.insert
local stub = function () log.warning("tried to call an RTU function stub") end
---@class rtu_device ---@class rtu_device
local public = {} local public = {}
@ -113,13 +117,26 @@ function rtu.init_unit(device)
return self.io_count_cache[1], self.io_count_cache[2], self.io_count_cache[3], self.io_count_cache[4] return self.io_count_cache[1], self.io_count_cache[2], self.io_count_cache[3], self.io_count_cache[4]
end end
-- pass a function through or generate one to call a function by name from the device
---@param f function|string function or device function name
local function _as_func(f)
if type(f) == "string" then
local name = f
if device then
f = function (...) return device[name](...) end
else f = stub end
end
return f
end
-- discrete inputs: single bit read-only -- discrete inputs: single bit read-only
-- connect discrete input -- connect discrete input
---@param f function ---@param f function|string function or function name
---@return integer count count of discrete inputs ---@return integer count count of discrete inputs
function protected.connect_di(f) function protected.connect_di(f)
insert(self.discrete_inputs, { read = f }) insert(self.discrete_inputs, { read = _as_func(f) })
_count_io() _count_io()
return #self.discrete_inputs return #self.discrete_inputs
end end
@ -135,11 +152,11 @@ function rtu.init_unit(device)
-- coils: single bit read-write -- coils: single bit read-write
-- connect coil -- connect coil
---@param f_read function ---@param f_read function|string function or function name
---@param f_write function ---@param f_write function|string function or function name
---@return integer count count of coils ---@return integer count count of coils
function protected.connect_coil(f_read, f_write) function protected.connect_coil(f_read, f_write)
insert(self.coils, { read = f_read, write = f_write }) insert(self.coils, { read = _as_func(f_read), write = _as_func(f_write) })
_count_io() _count_io()
return #self.coils return #self.coils
end end
@ -164,10 +181,10 @@ function rtu.init_unit(device)
-- input registers: multi-bit read-only -- input registers: multi-bit read-only
-- connect input register -- connect input register
---@param f function ---@param f function|string function or function name
---@return integer count count of input registers ---@return integer count count of input registers
function protected.connect_input_reg(f) function protected.connect_input_reg(f)
insert(self.input_regs, { read = f }) insert(self.input_regs, { read = _as_func(f) })
_count_io() _count_io()
return #self.input_regs return #self.input_regs
end end
@ -183,11 +200,11 @@ function rtu.init_unit(device)
-- holding registers: multi-bit read-write -- holding registers: multi-bit read-write
-- connect holding register -- connect holding register
---@param f_read function ---@param f_read function|string function or function name
---@param f_write function ---@param f_write function|string function or function name
---@return integer count count of holding registers ---@return integer count count of holding registers
function protected.connect_holding_reg(f_read, f_write) function protected.connect_holding_reg(f_read, f_write)
insert(self.holding_regs, { read = f_read, write = f_write }) insert(self.holding_regs, { read = _as_func(f_read), write = _as_func(f_write) })
_count_io() _count_io()
return #self.holding_regs return #self.holding_regs
end end
@ -267,8 +284,8 @@ end
function rtu.comms(version, nic, conn_watchdog) function rtu.comms(version, nic, conn_watchdog)
local self = { local self = {
sv_addr = comms.BROADCAST, sv_addr = comms.BROADCAST,
seq_num = 0, seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, r_seq_num = nil, ---@type nil|integer
txn_id = 0, txn_id = 0,
last_est_ack = ESTABLISH_ACK.ALLOW last_est_ack = ESTABLISH_ACK.ALLOW
} }
@ -425,16 +442,16 @@ function rtu.comms(version, nic, conn_watchdog)
if l_chan == config.RTU_Channel then if l_chan == config.RTU_Channel then
-- check sequence number -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num() self.r_seq_num = packet.scada_frame.seq_num() + 1
elseif rtu_state.linked and ((self.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then elseif self.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) log.warning("sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return return
elseif rtu_state.linked and (src_addr ~= self.sv_addr) then elseif rtu_state.linked and (src_addr ~= self.sv_addr) then
log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr .. log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr ..
"); channel in use by another system?") "); channel in use by another system?")
return return
else else
self.r_seq_num = packet.scada_frame.seq_num() self.r_seq_num = packet.scada_frame.seq_num() + 1
end end
-- feed watchdog on valid sequence number -- feed watchdog on valid sequence number
@ -539,7 +556,6 @@ function rtu.comms(version, nic, conn_watchdog)
-- establish allowed -- establish allowed
rtu_state.linked = true rtu_state.linked = true
self.sv_addr = packet.scada_frame.src_addr() self.sv_addr = packet.scada_frame.src_addr()
self.r_seq_num = nil
println_ts("supervisor connection established") println_ts("supervisor connection established")
log.info("supervisor connection established") log.info("supervisor connection established")
else else
@ -555,7 +571,9 @@ function rtu.comms(version, nic, conn_watchdog)
end end
end end
public.unlink(rtu_state) -- unlink
self.sv_addr = comms.BROADCAST
rtu_state.linked = false
end end
self.last_est_ack = est_ack self.last_est_ack = est_ack

View File

@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu") local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "v1.8.0" local RTU_VERSION = "v1.10.6"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
@ -71,6 +71,7 @@ log.info("========================================")
println(">> RTU GATEWAY " .. RTU_VERSION .. " <<") println(">> RTU GATEWAY " .. RTU_VERSION .. " <<")
crash.set_env("rtu", RTU_VERSION) crash.set_env("rtu", RTU_VERSION)
crash.dbg_log_env()
---------------------------------------- ----------------------------------------
-- main application -- main application
@ -92,14 +93,6 @@ local function main()
network.init_mac(config.AuthKey) network.init_mac(config.AuthKey)
end end
-- get modem
local modem = ppm.get_wireless_modem()
if modem == nil then
println("boot> wireless modem not found")
log.fatal("no wireless modem on startup")
return
end
-- generate alarm tones -- generate alarm tones
audio.generate_tones() audio.generate_tones()
@ -115,14 +108,15 @@ local function main()
-- RTU gateway devices (not RTU units) -- RTU gateway devices (not RTU units)
rtu_dev = { rtu_dev = {
modem = ppm.get_wireless_modem(),
sounders = {} sounders = {}
}, },
-- system objects -- system objects
rtu_sys = { rtu_sys = {
nic = network.nic(modem), nic = nil, ---@type nic
rtu_comms = nil, ---@type rtu_comms rtu_comms = nil, ---@type rtu_comms
conn_watchdog = nil, ---@type watchdog conn_watchdog = nil, ---@type watchdog
units = {} units = {}
}, },
@ -133,8 +127,9 @@ local function main()
} }
local smem_sys = __shared_memory.rtu_sys local smem_sys = __shared_memory.rtu_sys
local smem_dev = __shared_memory.rtu_dev
databus.tx_hw_modem(true) local rtu_state = __shared_memory.rtu_state
---------------------------------------- ----------------------------------------
-- interpret config and init units -- interpret config and init units
@ -342,7 +337,7 @@ local function main()
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then if formed == ppm.ACCESS_FAULT then
println_ts(util.c("sys_config> failed to check if '", name, "' is formed")) println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed boiler multiblock")) log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed boiler multiblock"))
return false return false
@ -357,7 +352,7 @@ local function main()
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then if formed == ppm.ACCESS_FAULT then
println_ts(util.c("sys_config> failed to check if '", name, "' is formed")) println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed turbine multiblock")) log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed turbine multiblock"))
return false return false
@ -377,7 +372,7 @@ local function main()
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then if formed == ppm.ACCESS_FAULT then
println_ts(util.c("sys_config> failed to check if '", name, "' is formed")) println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed dynamic tank multiblock")) log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed dynamic tank multiblock"))
return false return false
@ -391,7 +386,7 @@ local function main()
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then if formed == ppm.ACCESS_FAULT then
println_ts(util.c("sys_config> failed to check if '", name, "' is formed")) println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed induction matrix multiblock")) log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed induction matrix multiblock"))
return false return false
@ -405,7 +400,7 @@ local function main()
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then if formed == ppm.ACCESS_FAULT then
println_ts(util.c("sys_config> failed to check if '", name, "' is formed")) println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed SPS multiblock")) log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed SPS multiblock"))
return false return false
@ -471,7 +466,8 @@ local function main()
for_message = util.c("reactor ", for_reactor) for_message = util.c("reactor ", for_reactor)
end end
log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ") [", index, "] for ", for_message)) local index_str = util.trinary(index ~= nil, util.c(" [", index, "]"), "")
log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ")", index_str, " for ", for_message))
rtu_unit.uid = #units rtu_unit.uid = #units
@ -499,8 +495,6 @@ local function main()
-- start system -- start system
---------------------------------------- ----------------------------------------
local rtu_state = __shared_memory.rtu_state
log.debug("boot> running sys_config()") log.debug("boot> running sys_config()")
if sys_config() then if sys_config() then
@ -515,23 +509,33 @@ local function main()
log.info("startup> running in headless mode without front panel") log.info("startup> running in headless mode without front panel")
end end
-- check modem
if smem_dev.modem == nil then
println("startup> wireless modem not found")
log.fatal("no wireless modem on startup")
return
end
databus.tx_hw_modem(true)
-- find and setup all speakers -- find and setup all speakers
local speakers = ppm.get_all_devices("speaker") local speakers = ppm.get_all_devices("speaker")
for _, s in pairs(speakers) do for _, s in pairs(speakers) do
local sounder = rtu.init_sounder(s) local sounder = rtu.init_sounder(s)
table.insert(__shared_memory.rtu_dev.sounders, sounder) table.insert(smem_dev.sounders, sounder)
log.debug(util.c("startup> added speaker, attached as ", sounder.name)) log.debug(util.c("startup> added speaker, attached as ", sounder.name))
end end
databus.tx_hw_spkr_count(#__shared_memory.rtu_dev.sounders) databus.tx_hw_spkr_count(#smem_dev.sounders)
-- start connection watchdog -- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout) smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("startup> conn watchdog started") log.debug("startup> conn watchdog started")
-- setup comms -- setup comms
smem_sys.nic = network.nic(smem_dev.modem)
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, smem_sys.conn_watchdog) smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
log.debug("startup> comms init") log.debug("startup> comms init")

View File

@ -517,82 +517,23 @@ function threads.thread__unit_comms(smem, unit)
-- check if multiblock is still formed if this is a multiblock -- check if multiblock is still formed if this is a multiblock
if unit.is_multiblock and (util.time_ms() - last_f_check > 250) then if unit.is_multiblock and (util.time_ms() - last_f_check > 250) then
local is_formed = unit.device.isFormed()
last_f_check = util.time_ms() last_f_check = util.time_ms()
local is_formed = unit.device.isFormed()
if unit.formed == nil then if unit.formed == nil then
unit.formed = is_formed unit.formed = is_formed
if is_formed then unit.hw_state = UNIT_HW_STATE.OK end if is_formed then unit.hw_state = UNIT_HW_STATE.OK end
elseif not unit.formed then
unit.hw_state = UNIT_HW_STATE.UNFORMED
end end
if not unit.formed then unit.hw_state = UNIT_HW_STATE.UNFORMED end if (is_formed == true) and not unit.formed then
unit.hw_state = UNIT_HW_STATE.OK
if (not unit.formed) and is_formed then log.info(util.c(detail_name, " is now formed"))
-- newly re-formed rtu_comms.send_remounted(unit.uid)
local iface = ppm.get_iface(unit.device) elseif (is_formed == false) and unit.formed then
if iface then log.warning(util.c(detail_name, " is no longer formed"))
log.info(util.c("unmounting and remounting reformed RTU unit ", detail_name))
ppm.unmount(unit.device)
local type, device = ppm.mount(iface)
local faulted = false
if device ~= nil then
if type == "boilerValve" and unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
-- boiler multiblock
unit.device = device
unit.rtu, faulted = boilerv_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
elseif type == "turbineValve" and unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
-- turbine multiblock
unit.device = device
unit.rtu, faulted = turbinev_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
elseif type == "dynamicValve" and unit.type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
-- dynamic tank multiblock
unit.device = device
unit.rtu, faulted = dynamicv_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
elseif type == "inductionPort" and unit.type == RTU_UNIT_TYPE.IMATRIX then
-- induction matrix multiblock
unit.device = device
unit.rtu, faulted = imatrix_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
elseif type == "spsPort" and unit.type == RTU_UNIT_TYPE.SPS then
-- SPS multiblock
unit.device = device
unit.rtu, faulted = sps_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
else
log.error("illegal remount of non-multiblock RTU or type change attempted for " .. short_name, true)
end
if unit.formed and faulted then
-- something is still wrong = can't mark as formed yet
unit.formed = false
unit.hw_state = UNIT_HW_STATE.UNFORMED
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
else
unit.hw_state = UNIT_HW_STATE.OK
rtu_comms.send_remounted(unit.uid)
end
local type_name = types.rtu_type_to_string(unit.type)
log.info(util.c("reconnected the ", type_name, " on interface ", unit.name))
else
-- fully lost the peripheral now :(
log.error(util.c(unit.name, " lost (failed reconnect)"))
end
else
log.error("failed to get interface of previously connected RTU unit " .. detail_name, true)
end
end end
unit.formed = is_formed unit.formed = is_formed

View File

@ -16,8 +16,9 @@ local max_distance = nil
---@class comms ---@class comms
local comms = {} local comms = {}
-- protocol/data version (protocol/data independent changes tracked by util.lua version) -- protocol/data versions (protocol/data independent changes tracked by util.lua version)
comms.version = "2.4.5" comms.version = "3.0.0"
comms.api_version = "0.0.3"
---@enum PROTOCOL ---@enum PROTOCOL
local PROTOCOL = { local PROTOCOL = {
@ -64,7 +65,9 @@ local CRDN_TYPE = {
FAC_CMD = 3, -- faility command FAC_CMD = 3, -- faility command
UNIT_BUILDS = 4, -- build of each reactor unit (reactor + RTUs) UNIT_BUILDS = 4, -- build of each reactor unit (reactor + RTUs)
UNIT_STATUSES = 5, -- state of each of the reactor units UNIT_STATUSES = 5, -- state of each of the reactor units
UNIT_CMD = 6 -- command a reactor unit UNIT_CMD = 6, -- command a reactor unit
API_GET_FAC = 7, -- API: get all the facility data
API_GET_UNIT = 8 -- API: get reactor unit data
} }
---@enum ESTABLISH_ACK ---@enum ESTABLISH_ACK
@ -72,7 +75,8 @@ local ESTABLISH_ACK = {
ALLOW = 0, -- link approved ALLOW = 0, -- link approved
DENY = 1, -- link denied DENY = 1, -- link denied
COLLISION = 2, -- link denied due to existing active link COLLISION = 2, -- link denied due to existing active link
BAD_VERSION = 3 -- link denied due to comms version mismatch BAD_VERSION = 3, -- link denied due to comms version mismatch
BAD_API_VERSION = 4 -- link denied due to api version mismatch
} }
---@enum DEVICE_TYPE device types for establish messages ---@enum DEVICE_TYPE device types for establish messages
@ -93,7 +97,8 @@ local FAC_COMMAND = {
START = 2, -- start automatic process control START = 2, -- start automatic process control
ACK_ALL_ALARMS = 3, -- acknowledge all alarms on all units ACK_ALL_ALARMS = 3, -- acknowledge all alarms on all units
SET_WASTE_MODE = 4, -- set automatic waste processing mode SET_WASTE_MODE = 4, -- set automatic waste processing mode
SET_PU_FB = 5 -- set plutonium fallback mode SET_PU_FB = 5, -- set plutonium fallback mode
SET_SPS_LP = 6 -- set SPS at low power mode
} }
---@enum UNIT_COMMAND ---@enum UNIT_COMMAND
@ -235,6 +240,8 @@ function comms.scada_packet()
---@nodiscard ---@nodiscard
function public.modem_event() return self.modem_msg_in end function public.modem_event() return self.modem_msg_in end
---@nodiscard ---@nodiscard
function public.raw_header() return { self.src_addr, self.dest_addr, self.seq_num, self.protocol } end
---@nodiscard
function public.raw_sendable() return self.raw end function public.raw_sendable() return self.raw end
---@nodiscard ---@nodiscard
@ -273,7 +280,7 @@ function comms.authd_packet()
src_addr = comms.BROADCAST, src_addr = comms.BROADCAST,
dest_addr = comms.BROADCAST, dest_addr = comms.BROADCAST,
mac = "", mac = "",
payload = "" payload = {}
} }
---@class authd_packet ---@class authd_packet
@ -281,14 +288,13 @@ function comms.authd_packet()
-- make an authenticated SCADA packet -- make an authenticated SCADA packet
---@param s_packet scada_packet scada packet to authenticate ---@param s_packet scada_packet scada packet to authenticate
---@param mac function message authentication function ---@param mac function message authentication hash function
function public.make(s_packet, mac) function public.make(s_packet, mac)
self.valid = true self.valid = true
self.src_addr = s_packet.src_addr() self.src_addr = s_packet.src_addr()
self.dest_addr = s_packet.dest_addr() self.dest_addr = s_packet.dest_addr()
self.payload = textutils.serialize(s_packet.raw_sendable(), { allow_repetitions = true, compact = true }) self.mac = mac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true }))
self.mac = mac(self.payload) self.raw = { self.src_addr, self.dest_addr, self.mac, s_packet.raw_sendable() }
self.raw = { self.src_addr, self.dest_addr, self.mac, self.payload }
end end
-- parse in a modem message as an authenticated SCADA packet -- parse in a modem message as an authenticated SCADA packet
@ -325,14 +331,14 @@ function comms.authd_packet()
self.src_addr = nil self.src_addr = nil
self.dest_addr = nil self.dest_addr = nil
self.mac = "" self.mac = ""
self.payload = "" self.payload = {}
end end
-- check if this packet is destined for this device -- check if this packet is destined for this device
local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == COMPUTER_ID) local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == COMPUTER_ID)
self.valid = is_destination and type(self.src_addr) == "number" and type(self.dest_addr) == "number" and self.valid = is_destination and type(self.src_addr) == "number" and type(self.dest_addr) == "number" and
type(self.mac) == "string" and type(self.payload) == "string" type(self.mac) == "string" and type(self.payload) == "table"
end end
end end

View File

@ -29,7 +29,7 @@ local annunc = {}
annunc.RCSFlowLow_H2O = -3.2 -- flow < -3.2 mB/s annunc.RCSFlowLow_H2O = -3.2 -- flow < -3.2 mB/s
annunc.RCSFlowLow_NA = -2.0 -- flow < -2.0 mB/s annunc.RCSFlowLow_NA = -2.0 -- flow < -2.0 mB/s
annunc.CoolantLevelLow = 0.4 -- fill < 40% annunc.CoolantLevelLow = 0.4 -- fill < 40%
annunc.ReactorTempHigh = 1000 -- temp > 1000K annunc.OpTempTolerance = 5 -- high temp if >= operational temp + X
annunc.ReactorHighDeltaT = 50 -- rate > 50K/s annunc.ReactorHighDeltaT = 50 -- rate > 50K/s
annunc.FuelLevelLow = 0.05 -- fill <= 5% annunc.FuelLevelLow = 0.05 -- fill <= 5%
annunc.WasteLevelHigh = 0.80 -- fill >= 80% annunc.WasteLevelHigh = 0.80 -- fill >= 80%
@ -52,7 +52,6 @@ local alarms = {}
-- unit alarms -- unit alarms
alarms.HIGH_TEMP = 1150 -- temp >= 1150K
alarms.HIGH_WASTE = 0.85 -- fill > 85% alarms.HIGH_WASTE = 0.85 -- fill > 85%
alarms.HIGH_RADIATION = 0.00005 -- 50 uSv/h, not yet damaging but this isn't good alarms.HIGH_RADIATION = 0.00005 -- 50 uSv/h, not yet damaging but this isn't good
@ -66,21 +65,48 @@ constants.ALARM_LIMITS = alarms
--#endregion --#endregion
--#region Supervisor Redstone Activation Thresholds
---@class _rs_threshold_constants
local rs = {}
rs.IMATRIX_CHARGE_LOW = 0.05 -- activation threshold (less than) for F_MATRIX_LOW
rs.IMATRIX_CHARGE_HIGH = 0.95 -- activation threshold (greater than) for F_MATRIX_HIGH
constants.RS_THRESHOLDS = rs
--#endregion
--#region Supervisor Constants --#region Supervisor Constants
-- milliseconds until turbine flow is assumed to be stable enough to enable coolant checks -- milliseconds until coolant flow is assumed to be stable enough to enable certain coolant checks
constants.FLOW_STABILITY_DELAY_MS = 15000 constants.FLOW_STABILITY_DELAY_MS = 10000
-- Notes on Radiation -- Notes on Radiation
-- - background radiation 0.0000001 Sv/h (99.99 nSv/h) -- - background radiation 0.0000001 Sv/h (99.99 nSv/h)
-- - "green tint" radiation 0.00001 Sv/h (10 uSv/h) -- - "green tint" radiation 0.00001 Sv/h (10 uSv/h)
-- - damaging radiation 0.00006 Sv/h (60 uSv/h) -- - damaging radiation 0.00006 Sv/h (60 uSv/h)
constants.LOW_RADIATION = 0.00001 constants.LOW_RADIATION = 0.00001
constants.HAZARD_RADIATION = 0.00006 constants.HAZARD_RADIATION = 0.00006
constants.HIGH_RADIATION = 0.001 constants.HIGH_RADIATION = 0.001
constants.VERY_HIGH_RADIATION = 0.1 constants.VERY_HIGH_RADIATION = 0.1
constants.SEVERE_RADIATION = 8.0 constants.SEVERE_RADIATION = 8.0
constants.EXTREME_RADIATION = 100.0 constants.EXTREME_RADIATION = 100.0
--#endregion
--#region Mekanism Configuration Constants
---@class _mek_constants
local mek = {}
mek.BASE_BOIL_TEMP = 373.15 -- mekanism: HeatUtils.BASE_BOIL_TEMP
mek.JOULES_PER_MB = 1000000 -- mekanism: energyPerFissionFuel
mek.TURBINE_GAS_PER_TANK = 64000 -- mekanism: turbineGasPerTank
mek.TURBINE_DISPERSER_FLOW = 1280 -- mekanism: turbineDisperserGasFlow
mek.TURBINE_VENT_FLOW = 32000 -- mekanism: turbineVentGasFlow
constants.mek = mek
--#endregion --#endregion

View File

@ -24,6 +24,21 @@ function crash.set_env(application, version)
ver = version ver = version
end end
-- log environment versions
---@param log_msg function log function to use
local function log_versions(log_msg)
log_msg(util.c("RUNTIME: ", _HOST))
log_msg(util.c("LUA VERSION: ", _VERSION))
log_msg(util.c("APPLICATION: ", app))
log_msg(util.c("FIRMWARE VERSION: ", ver))
log_msg(util.c("COMMS VERSION: ", comms.version))
if has_graphics then log_msg(util.c("GRAPHICS VERSION: ", core.version)) end
if has_lockbox then log_msg(util.c("LOCKBOX VERSION: ", lockbox.version)) end
end
-- when running with debug logs, log the useful information that the crash handler knows
function crash.dbg_log_env() log_versions(log.debug) end
-- handle a crash error -- handle a crash error
---@param error string error message ---@param error string error message
function crash.handler(error) function crash.handler(error)
@ -31,13 +46,7 @@ function crash.handler(error)
log.info("=====> FATAL SOFTWARE FAULT <=====") log.info("=====> FATAL SOFTWARE FAULT <=====")
log.fatal(error) log.fatal(error)
log.info("----------------------------------") log.info("----------------------------------")
log.info(util.c("RUNTIME: ", _HOST)) log_versions(log.info)
log.info(util.c("LUA VERSION: ", _VERSION))
log.info(util.c("APPLICATION: ", app))
log.info(util.c("FIRMWARE VERSION: ", ver))
log.info(util.c("COMMS VERSION: ", comms.version))
if has_graphics then log.info(util.c("GRAPHICS VERSION: ", core.version)) end
if has_lockbox then log.info(util.c("LOCKBOX VERSION: ", lockbox.version)) end
log.info("----------------------------------") log.info("----------------------------------")
log.info(debug.traceback("--- begin debug trace ---", 1)) log.info(debug.traceback("--- begin debug trace ---", 1))
log.info("--- end debug trace ---") log.info("--- end debug trace ---")

View File

@ -80,7 +80,7 @@ end
---@param modem table modem to use ---@param modem table modem to use
function network.nic(modem) function network.nic(modem)
local self = { local self = {
connected = true, -- used to avoid costly MAC calculations if modem isn't even present connected = true, -- used to avoid costly MAC calculations if modem isn't even present
channels = {} channels = {}
} }
@ -114,7 +114,7 @@ function network.nic(modem)
modem.open(channel) modem.open(channel)
end end
-- link all public functions except for transmit -- link all public functions except for transmit, open, and close
for key, func in pairs(modem) do for key, func in pairs(modem) do
if key ~= "transmit" and key ~= "open" and key ~= "close" and key ~= "closeAll" then public[key] = func end if key ~= "transmit" and key ~= "open" and key ~= "close" and key ~= "closeAll" then public[key] = func end
end end
@ -175,7 +175,7 @@ function network.nic(modem)
---@param packet scada_packet packet ---@param packet scada_packet packet
function public.transmit(dest_channel, local_channel, packet) function public.transmit(dest_channel, local_channel, packet)
if self.connected then if self.connected then
local tx_packet = packet ---@type authd_packet|scada_packet local tx_packet = packet ---@type authd_packet|scada_packet
if c_eng.hmac ~= nil then if c_eng.hmac ~= nil then
-- local start = util.time_ms() -- local start = util.time_ms()
@ -184,7 +184,7 @@ function network.nic(modem)
---@cast tx_packet authd_packet ---@cast tx_packet authd_packet
tx_packet.make(packet, compute_hmac) tx_packet.make(packet, compute_hmac)
-- log.debug("crypto.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms") -- log.debug("network.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms")
end end
modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable()) modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable())
@ -211,17 +211,18 @@ function network.nic(modem)
a_packet.receive(side, sender, reply_to, message, distance) a_packet.receive(side, sender, reply_to, message, distance)
if a_packet.is_valid() then if a_packet.is_valid() then
-- local start = util.time_ms() s_packet.receive(side, sender, reply_to, a_packet.data(), distance)
local packet_hmac = a_packet.mac()
local msg = a_packet.data()
local computed_hmac = compute_hmac(msg)
if packet_hmac == computed_hmac then if s_packet.is_valid() then
-- log.debug("crypto.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms") -- local start = util.time_ms()
s_packet.receive(side, sender, reply_to, textutils.unserialize(msg), distance) local computed_hmac = compute_hmac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true }))
s_packet.stamp_authenticated()
else if a_packet.mac() == computed_hmac then
-- log.debug("crypto.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms") -- log.debug("network.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms")
s_packet.stamp_authenticated()
else
-- log.debug("network.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
end
end end
end end
else else

View File

@ -51,11 +51,13 @@ local function peri_init(iface)
self.device = peripheral.wrap(iface) self.device = peripheral.wrap(iface)
end end
-- initialization process (re-map) -- create a protected version of a peripheral function call
---@nodiscard
for key, func in pairs(self.device) do ---@param key string function name
self.fault_counts[key] = 0 ---@param func function function
self.device[key] = function (...) ---@return function method protected version of the function
local function protect_peri_function(key, func)
return function (...)
local return_table = table.pack(pcall(func, ...)) local return_table = table.pack(pcall(func, ...))
local status = return_table[1] local status = return_table[1]
@ -85,20 +87,24 @@ local function peri_init(iface)
count_str = " [" .. self.fault_counts[key] .. " total faults]" count_str = " [" .. self.fault_counts[key] .. " total faults]"
end end
log.error(util.c("PPM: protected ", key, "() -> ", result, count_str)) log.error(util.c("PPM: [@", iface, "] protected ", key, "() -> ", result, count_str))
end end
self.fault_counts[key] = self.fault_counts[key] + 1 self.fault_counts[key] = self.fault_counts[key] + 1
if result == "Terminated" then if result == "Terminated" then ppm_sys.terminate = true end
ppm_sys.terminate = true
end
return ACCESS_FAULT return ACCESS_FAULT, result
end end
end end
end end
-- initialization process (re-map)
for key, func in pairs(self.device) do
self.fault_counts[key] = 0
self.device[key] = protect_peri_function(key, func)
end
-- fault management & monitoring functions -- fault management & monitoring functions
local function clear_fault() self.faulted = false end local function clear_fault() self.faulted = false end
@ -131,12 +137,23 @@ local function peri_init(iface)
local mt = { local mt = {
__index = function (_, key) __index = function (_, key)
-- try to find the function in case it was added (multiblock formed)
local funcs = peripheral.wrap(iface)
if (type(funcs) == "table") and (type(funcs[key]) == "function") then
-- add this function then return it
self.fault_counts[key] = 0
self.device[key] = protect_peri_function(key, funcs[key])
log.info(util.c("PPM: [@", iface, "] initialized previously undefined field ", key, "()"))
return self.device[key]
end
-- function still missing, return an undefined function handler
-- note: code should avoid storing functions for multiblocks and instead try to index them again
return (function () return (function ()
-- this will continuously be counting calls here as faults -- this will continuously be counting calls here as faults
-- unlike other functions, faults here can't be cleared as it is just not defined if self.fault_counts[key] == nil then self.fault_counts[key] = 0 end
if self.fault_counts[key] == nil then
self.fault_counts[key] = 0
end
-- function failed -- function failed
self.faulted = true self.faulted = true
@ -151,12 +168,12 @@ local function peri_init(iface)
count_str = " [" .. self.fault_counts[key] .. " total calls]" count_str = " [" .. self.fault_counts[key] .. " total calls]"
end end
log.error(util.c("PPM: caught undefined function ", key, "()", count_str)) log.error(util.c("PPM: [@", iface, "] caught undefined function ", key, "()", count_str))
end end
self.fault_counts[key] = self.fault_counts[key] + 1 self.fault_counts[key] = self.fault_counts[key] + 1
return UNDEFINED_FIELD return ACCESS_FAULT, UNDEFINED_FIELD
end) end)
end end
} }

View File

@ -52,6 +52,8 @@ local IO_PORT = {
-- facility -- facility
F_ALARM = 7, -- active high, facility-wide alarm (any high priority unit alarm) F_ALARM = 7, -- active high, facility-wide alarm (any high priority unit alarm)
F_ALARM_ANY = 8, -- active high, any alarm regardless of priority F_ALARM_ANY = 8, -- active high, any alarm regardless of priority
F_MATRIX_LOW = 27, -- active high, induction matrix charge low
F_MATRIX_HIGH = 28, -- active high, induction matrix charge high
-- waste -- waste
WASTE_PU = 9, -- active low, waste -> plutonium -> pellets route WASTE_PU = 9, -- active low, waste -> plutonium -> pellets route
@ -75,17 +77,27 @@ local IO_PORT = {
-- unit outputs -- unit outputs
U_ALARM = 25, -- active high, unit alarm U_ALARM = 25, -- active high, unit alarm
U_EMER_COOL = 26 -- active low, emergency coolant control U_EMER_COOL = 26, -- active low, emergency coolant control
-- analog outputs --
-- facility
F_MATRIX_CHG = 29 -- analog charge level of the induction matrix
} }
rsio.IO_LVL = IO_LVL rsio.IO_LVL = IO_LVL
rsio.IO_DIR = IO_DIR rsio.IO_DIR = IO_DIR
rsio.IO_MODE = IO_MODE rsio.IO_MODE = IO_MODE
rsio.IO = IO_PORT rsio.IO = IO_PORT
rsio.NUM_PORTS = IO_PORT.U_EMER_COOL
rsio.NUM_PORTS = 29
rsio.NUM_DIG_PORTS = 28
rsio.NUM_ANA_PORTS = 1
-- self checks -- self checks
assert(rsio.NUM_PORTS == (rsio.NUM_DIG_PORTS + rsio.NUM_ANA_PORTS), "port counts inconsistent")
local dup_chk = {} local dup_chk = {}
for _, v in pairs(IO_PORT) do for _, v in pairs(IO_PORT) do
assert(dup_chk[v] ~= true, "duplicate in port list") assert(dup_chk[v] ~= true, "duplicate in port list")
@ -96,64 +108,45 @@ assert(#dup_chk == rsio.NUM_PORTS, "port list malformed")
--#endregion --#endregion
--#region Utility Functions --#region Utility Functions and Attribute Tables
local PORT_NAMES = { local IO = IO_PORT
"F_SCRAM",
"F_ACK",
"R_SCRAM",
"R_RESET",
"R_ENABLE",
"U_ACK",
"F_ALARM",
"F_ALARM_ANY",
"WASTE_PU",
"WASTE_PO",
"WASTE_POPL",
"WASTE_AM",
"R_ACTIVE",
"R_AUTO_CTRL",
"R_SCRAMMED",
"R_AUTO_SCRAM",
"R_HIGH_DMG",
"R_HIGH_TEMP",
"R_LOW_COOLANT",
"R_EXCESS_HC",
"R_EXCESS_WS",
"R_INSUFF_FUEL",
"R_PLC_FAULT",
"R_PLC_TIMEOUT",
"U_ALARM",
"U_EMER_COOL"
}
-- list of all port names
local PORT_NAMES = {}
for k, v in pairs(IO) do PORT_NAMES[v] = k end
-- list of all port I/O modes
local MODES = { local MODES = {
IO_MODE.DIGITAL_IN, -- F_SCRAM [IO.F_SCRAM] = IO_MODE.DIGITAL_IN,
IO_MODE.DIGITAL_IN, -- F_ACK [IO.F_ACK] = IO_MODE.DIGITAL_IN,
IO_MODE.DIGITAL_IN, -- R_SCRAM [IO.R_SCRAM] = IO_MODE.DIGITAL_IN,
IO_MODE.DIGITAL_IN, -- R_RESET [IO.R_RESET] = IO_MODE.DIGITAL_IN,
IO_MODE.DIGITAL_IN, -- R_ENABLE [IO.R_ENABLE] = IO_MODE.DIGITAL_IN,
IO_MODE.DIGITAL_IN, -- U_ACK [IO.U_ACK] = IO_MODE.DIGITAL_IN,
IO_MODE.DIGITAL_OUT, -- F_ALARM [IO.F_ALARM] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- F_ALARM_ANY [IO.F_ALARM_ANY] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- WASTE_PU [IO.F_MATRIX_LOW] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- WASTE_PO [IO.F_MATRIX_HIGH] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- WASTE_POPL [IO.WASTE_PU] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- WASTE_AM [IO.WASTE_PO] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- R_ACTIVE [IO.WASTE_POPL] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- R_AUTO_CTRL [IO.WASTE_AM] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- R_SCRAMMED [IO.R_ACTIVE] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- R_AUTO_SCRAM [IO.R_AUTO_CTRL] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- R_HIGH_DMG [IO.R_SCRAMMED] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- R_HIGH_TEMP [IO.R_AUTO_SCRAM] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- R_LOW_COOLANT [IO.R_HIGH_DMG] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- R_EXCESS_HC [IO.R_HIGH_TEMP] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- R_EXCESS_WS [IO.R_LOW_COOLANT] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- R_INSUFF_FUEL [IO.R_EXCESS_HC] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- R_PLC_FAULT [IO.R_EXCESS_WS] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- R_PLC_TIMEOUT [IO.R_INSUFF_FUEL] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT, -- U_ALARM [IO.R_PLC_FAULT] = IO_MODE.DIGITAL_OUT,
IO_MODE.DIGITAL_OUT -- U_EMER_COOL [IO.R_PLC_TIMEOUT] = IO_MODE.DIGITAL_OUT,
[IO.U_ALARM] = IO_MODE.DIGITAL_OUT,
[IO.U_EMER_COOL] = IO_MODE.DIGITAL_OUT,
[IO.F_MATRIX_CHG] = IO_MODE.ANALOG_OUT
} }
assert(rsio.NUM_PORTS == #PORT_NAMES, "port names length incorrect") assert(rsio.NUM_PORTS == #PORT_NAMES, "port names length incorrect")
@ -179,74 +172,51 @@ local function _O_ACTIVE_LOW(active) if active then return IO_LVL.LOW else retur
-- I/O mappings to I/O function and I/O mode -- I/O mappings to I/O function and I/O mode
local RS_DIO_MAP = { local RS_DIO_MAP = {
-- F_SCRAM [IO.F_SCRAM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN },
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN }, [IO.F_ACK] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
-- F_ACK
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
-- R_SCRAM [IO.R_SCRAM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN },
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN }, [IO.R_RESET] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
-- R_RESET [IO.R_ENABLE] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
-- R_ENABLE
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
-- U_ACK [IO.U_ACK] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
-- F_ALARM [IO.F_ALARM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, [IO.F_ALARM_ANY] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- F_ALARM_ANY [IO.F_MATRIX_LOW] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, [IO.F_MATRIX_HIGH] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- WASTE_PU [IO.WASTE_PU] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, [IO.WASTE_PO] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
-- WASTE_PO [IO.WASTE_POPL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, [IO.WASTE_AM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
-- WASTE_POPL
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
-- WASTE_AM
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
-- R_ACTIVE [IO.R_ACTIVE] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, [IO.R_AUTO_CTRL] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_AUTO_CTRL [IO.R_SCRAMMED] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, [IO.R_AUTO_SCRAM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_SCRAMMED [IO.R_HIGH_DMG] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, [IO.R_HIGH_TEMP] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_AUTO_SCRAM [IO.R_LOW_COOLANT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, [IO.R_EXCESS_HC] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_HIGH_DMG [IO.R_EXCESS_WS] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, [IO.R_INSUFF_FUEL] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_HIGH_TEMP [IO.R_PLC_FAULT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, [IO.R_PLC_TIMEOUT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_LOW_COOLANT
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_EXCESS_HC
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_EXCESS_WS
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_INSUFF_FUEL
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_PLC_FAULT
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_PLC_TIMEOUT
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- U_ALARM [IO.U_ALARM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, [IO.U_EMER_COOL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }
-- U_EMER_COOL
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }
} }
assert(rsio.NUM_PORTS == #RS_DIO_MAP, "RS_DIO_MAP length incorrect") assert(rsio.NUM_DIG_PORTS == #RS_DIO_MAP, "RS_DIO_MAP length incorrect")
-- get the I/O direction of a port -- get the I/O direction of a port
---@nodiscard ---@nodiscard
---@param port IO_PORT ---@param port IO_PORT
---@return IO_DIR ---@return IO_DIR
function rsio.get_io_dir(port) function rsio.get_io_dir(port)
if rsio.is_valid_port(port) then return RS_DIO_MAP[port].mode if rsio.is_valid_port(port) then
return util.trinary(MODES[port] == IO_MODE.DIGITAL_OUT or MODES[port] == IO_MODE.ANALOG_OUT, IO_DIR.OUT, IO_DIR.IN)
else return IO_DIR.IN end else return IO_DIR.IN end
end end
@ -310,6 +280,13 @@ end
--#region Digital I/O --#region Digital I/O
-- check if a port is digital
---@nodiscard
---@param port IO_PORT
function rsio.is_digital(port)
return rsio.is_valid_port(port) and (MODES[port] == IO_MODE.DIGITAL_IN or MODES[port] == IO_MODE.DIGITAL_OUT)
end
-- get digital I/O level reading from a redstone boolean input value -- get digital I/O level reading from a redstone boolean input value
---@nodiscard ---@nodiscard
---@param rs_value boolean raw value from redstone ---@param rs_value boolean raw value from redstone
@ -330,7 +307,7 @@ function rsio.digital_write(level) return level == IO_LVL.HIGH end
---@param active boolean state to convert to logic level ---@param active boolean state to convert to logic level
---@return IO_LVL|false ---@return IO_LVL|false
function rsio.digital_write_active(port, active) function rsio.digital_write_active(port, active)
if (not util.is_int(port)) or (port < IO_PORT.F_ALARM) or (port > IO_PORT.U_EMER_COOL) then if not rsio.is_digital(port) then
return false return false
else else
return RS_DIO_MAP[port]._out(active) return RS_DIO_MAP[port]._out(active)
@ -343,9 +320,7 @@ end
---@param level IO_LVL logic level ---@param level IO_LVL logic level
---@return boolean|nil state true for active, false for inactive, or nil if invalid port or level provided ---@return boolean|nil state true for active, false for inactive, or nil if invalid port or level provided
function rsio.digital_is_active(port, level) function rsio.digital_is_active(port, level)
if not util.is_int(port) then if (not rsio.is_digital(port)) or level == IO_LVL.FLOATING or level == IO_LVL.DISCONNECT then
return nil
elseif level == IO_LVL.FLOATING or level == IO_LVL.DISCONNECT then
return nil return nil
else else
return RS_DIO_MAP[port]._in(level) return RS_DIO_MAP[port]._in(level)
@ -356,6 +331,13 @@ end
--#region Analog I/O --#region Analog I/O
-- check if a port is analog
---@nodiscard
---@param port IO_PORT
function rsio.is_analog(port)
return rsio.is_valid_port(port) and (MODES[port] == IO_MODE.ANALOG_IN or MODES[port] == IO_MODE.ANALOG_OUT)
end
-- read an analog value scaled from min to max -- read an analog value scaled from min to max
---@nodiscard ---@nodiscard
---@param rs_value number redstone reading (0 to 15) ---@param rs_value number redstone reading (0 to 15)
@ -372,7 +354,7 @@ end
---@param value number value to write (from min to max range) ---@param value number value to write (from min to max range)
---@param min number minimum of range ---@param min number minimum of range
---@param max number maximum of range ---@param max number maximum of range
---@return number rs_value scaled redstone reading (0 to 15) ---@return integer rs_value scaled redstone reading (0 to 15)
function rsio.analog_write(value, min, max) function rsio.analog_write(value, min, max)
local scaled_value = (value - min) / (max - min) local scaled_value = (value - min) / (max - min)
return math.floor(scaled_value * 15) return math.floor(scaled_value * 15)

View File

@ -5,7 +5,7 @@
---@class types ---@class types
local types = {} local types = {}
-- CLASSES -- --#region CLASSES
---@class tank_fluid ---@class tank_fluid
---@field name fluid ---@field name fluid
@ -67,12 +67,54 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
---@field reactor integer ---@field reactor integer
---@field rsio table|nil ---@field rsio table|nil
--#endregion
-- ALIASES -- -- ALIASES --
---@alias color integer ---@alias color integer
-- ENUMERATION TYPES -- --#region ENUMERATION TYPES
--#region
---@enum TEMP_SCALE
types.TEMP_SCALE = {
KELVIN = 1,
CELSIUS = 2,
FAHRENHEIT = 3,
RANKINE = 4
}
types.TEMP_SCALE_NAMES = {
"Kelvin",
"Celsius",
"Fahrenheit",
"Rankine"
}
types.TEMP_SCALE_UNITS = {
"K",
"\xb0C",
"\xb0F",
"\xb0R"
}
---@enum ENERGY_SCALE
types.ENERGY_SCALE = {
JOULES = 1,
FE = 2,
RF = 3
}
types.ENERGY_SCALE_NAMES = {
"Joules (J)",
"Forge Energy (FE)",
"Redstone Flux (RF)"
}
types.ENERGY_SCALE_UNITS = {
"J",
"FE",
"RF"
}
---@enum PANEL_LINK_STATE ---@enum PANEL_LINK_STATE
types.PANEL_LINK_STATE = { types.PANEL_LINK_STATE = {
@ -128,6 +170,15 @@ function types.rtu_type_to_string(utype)
end end
end end
---@enum RTU_ID_FAIL
types.RTU_ID_FAIL = {
OK = 0,
OUT_OF_RANGE = 1,
DUPLICATE = 2,
MAX_DEVICES = 3,
MISSING = 4
}
---@enum TRI_FAIL ---@enum TRI_FAIL
types.TRI_FAIL = { types.TRI_FAIL = {
OK = 1, OK = 1,
@ -249,8 +300,7 @@ types.ALARM_STATE_NAMES = {
--#endregion --#endregion
-- STRING TYPES -- --#region STRING TYPES
--#region
---@alias side ---@alias side
---|"top" ---|"top"
@ -364,8 +414,7 @@ types.DUMPING_MODE = {
--#endregion --#endregion
-- MODBUS -- --#region MODBUS
--#region
-- MODBUS function codes -- MODBUS function codes
---@enum MODBUS_FCODE ---@enum MODBUS_FCODE

View File

@ -4,6 +4,8 @@
local cc_strings = require("cc.strings") local cc_strings = require("cc.strings")
local const = require("scada-common.constants")
local math = math local math = math
local string = string local string = string
local table = table local table = table
@ -22,7 +24,7 @@ local t_pack = table.pack
local util = {} local util = {}
-- scada-common version -- scada-common version
util.version = "1.1.19" util.version = "1.4.3"
util.TICK_TIME_S = 0.05 util.TICK_TIME_S = 0.05
util.TICK_TIME_MS = 50 util.TICK_TIME_MS = 50
@ -108,12 +110,31 @@ function util.pad(str, n)
return t_concat{util.spaces(lpad), str, util.spaces(rpad)} return t_concat{util.spaces(lpad), str, util.spaces(rpad)}
end end
-- trim leading and trailing whitespace
---@nodiscard
---@param s string text
---@return string
function util.trim(s)
local str = s:gsub("^%s*(.-)%s*$", "%1")
return str
end
-- wrap a string into a table of lines -- wrap a string into a table of lines
---@nodiscard ---@nodiscard
---@param str string ---@param str string
---@param limit integer line limit ---@param limit integer line limit, must be greater than 0
---@return table lines ---@return table lines
function util.strwrap(str, limit) return cc_strings.wrap(str, limit) end function util.strwrap(str, limit)
assert(limit > 0, "util.strwrap() limit not greater than 0")
return cc_strings.wrap(str, limit)
end
-- make sure a string is at least 'width' long
---@nodiscard
---@param str string
---@param width integer minimum width
---@return string string
function util.strminw(str, width) return cc_strings.ensure_width(str, width) end
-- concatenation with built-in to string -- concatenation with built-in to string
---@nodiscard ---@nodiscard
@ -181,8 +202,7 @@ function util.round(x) return math.floor(x + 0.5) end
-- get a new moving average object -- get a new moving average object
---@nodiscard ---@nodiscard
---@param length integer history length ---@param length integer history length
---@param default number value to fill history with for first call to compute() function util.mov_avg(length)
function util.mov_avg(length, default)
local data = {} local data = {}
local index = 1 local index = 1
local last_t = 0 ---@type number|nil local last_t = 0 ---@type number|nil
@ -190,11 +210,15 @@ function util.mov_avg(length, default)
---@class moving_average ---@class moving_average
local public = {} local public = {}
-- reset all to a given value -- reset all to a given value, or clear all data if no value is given
---@param x number value ---@param x number? value
function public.reset(x) function public.reset(x)
index = 1
data = {} data = {}
for _ = 1, length do t_insert(data, x) end
if x then
for _ = 1, length do t_insert(data, x) end
end
end end
-- record a new value -- record a new value
@ -214,12 +238,15 @@ function util.mov_avg(length, default)
---@nodiscard ---@nodiscard
---@return number average ---@return number average
function public.compute() function public.compute()
local sum = 0 if #data == 0 then return 0 end
for i = 1, length do sum = sum + data[i] end
return sum / length
end
public.reset(default) local sum = 0
for i = 1, #data do
sum = sum + data[i]
end
return sum / #data
end
return public return public
end end
@ -362,72 +389,86 @@ end
--#endregion --#endregion
--#region MEKANISM POWER --#region MEKANISM MATH
-- convert Joules to FE -- convert Joules to FE (or RF)
---@nodiscard ---@nodiscard
---@param J number Joules ---@param J number Joules
---@return number FE Forge Energy ---@return number FE Forge Energy or Redstone Flux
function util.joules_to_fe(J) return (J * 0.4) end function util.joules_to_fe_rf(J) return (J * 0.4) end
-- convert FE to Joules -- convert FE (or RF) to Joules
---@nodiscard ---@nodiscard
---@param FE number Forge Energy ---@param FE number Forge Energy or Redstone Flux
---@return number J Joules ---@return number J Joules
function util.fe_to_joules(FE) return (FE * 2.5) end function util.fe_rf_to_joules(FE) return (FE * 2.5) end
local function kFE(fe) return fe / 1000.0 end -- format a power value into XXX.XX UNIT format<br>
local function MFE(fe) return fe / 1000000.0 end -- example for FE: FE, kFE, MFE, GFE, TFE, PFE, EFE, ZFE
local function GFE(fe) return fe / 1000000000.0 end
local function TFE(fe) return fe / 1000000000000.0 end
local function PFE(fe) return fe / 1000000000000000.0 end
local function EFE(fe) return fe / 1000000000000000000.0 end -- if you accomplish this please touch grass
local function ZFE(fe) return fe / 1000000000000000000000.0 end -- how & why did you do this?
-- format a power value into XXX.XX UNIT format (FE, kFE, MFE, GFE, TFE, PFE, EFE, ZFE)
---@nodiscard ---@nodiscard
---@param fe number forge energy value ---@param e number energy value
---@param label string energy scale label
---@param combine_label? boolean if a label should be included in the string itself ---@param combine_label? boolean if a label should be included in the string itself
---@param format? string format override ---@param format? string format override
---@return string str, string? unit ---@return string str, string unit
function util.power_format(fe, combine_label, format) function util.power_format(e, label, combine_label, format)
local unit, value local unit, value
if type(format) ~= "string" then format = "%.2f" end if type(format) ~= "string" then format = "%.2f" end
if fe < 1000.0 then if e < 1000.0 then
unit = "FE" unit = ""
value = fe value = e
elseif fe < 1000000.0 then elseif e < 1000000.0 then
unit = "kFE" unit = "k"
value = kFE(fe) value = e / 1000.0
elseif fe < 1000000000.0 then elseif e < 1000000000.0 then
unit = "MFE" unit = "M"
value = MFE(fe) value = e / 1000000.0
elseif fe < 1000000000000.0 then elseif e < 1000000000000.0 then
unit = "GFE" unit = "G"
value = GFE(fe) value = e / 1000000000.0
elseif fe < 1000000000000000.0 then elseif e < 1000000000000000.0 then
unit = "TFE" unit = "T"
value = TFE(fe) value = e / 1000000000000.0
elseif fe < 1000000000000000000.0 then elseif e < 1000000000000000000.0 then
unit = "PFE" unit = "P"
value = PFE(fe) value = e / 1000000000000000.0
elseif fe < 1000000000000000000000.0 then elseif e < 1000000000000000000000.0 then
unit = "EFE" -- if you accomplish this please touch grass
value = EFE(fe) unit = "E"
value = e / 1000000000000000000.0
else else
unit = "ZFE" -- how & why did you do this?
value = ZFE(fe) unit = "Z"
value = e / 1000000000000000000000.0
end end
unit = unit .. label
if combine_label then if combine_label then
return util.sprintf(util.c(format, " %s"), value, unit) return util.sprintf(util.c(format, " %s"), value, unit), unit
else else
return util.sprintf(format, value), unit return util.sprintf(format, value), unit
end end
end end
-- compute Mekanism's rotation rate for a turbine
---@nodiscard
---@param turbine turbinev_session_db turbine data
function util.turbine_rotation(turbine)
local build = turbine.build
local inner_vol = build.steam_cap / const.mek.TURBINE_GAS_PER_TANK
local disp_rate = (build.dispersers * const.mek.TURBINE_DISPERSER_FLOW) * inner_vol
local vent_rate = build.vents * const.mek.TURBINE_VENT_FLOW
local max_rate = math.min(disp_rate, vent_rate)
local flow = math.min(max_rate, turbine.tanks.steam.amount)
return (flow * (turbine.tanks.steam.amount / build.steam_cap)) / max_rate
end
--#endregion --#endregion
--#region UTILITY CLASSES --#region UTILITY CLASSES

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