Compare commits

...

172 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
43b44cc425 #465 first pass at minifier 2024-04-24 21:11:41 +00:00
102 changed files with 7117 additions and 3602 deletions

View File

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

View File

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

2
.gitignore vendored
View File

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

View File

@@ -1,6 +1,6 @@
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
of this software and associated documentation files (the "Software"), to deal

View File

@@ -45,6 +45,7 @@ v10.1+ is required due to the complete support of CC:Tweaked added in Mekanism v
You can install this on a ComputerCraft computer using either:
* `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua`
* `pastebin get sqUN6VUb ccmsi.lua`
* Off-line (when HTTP is disabled) installation via [release bundles](https://github.com/MikaylaFischler/cc-mek-scada/wiki/Alternative-Installation-Strategies#release-bundles)
## Contributing

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

View File

@@ -7,10 +7,11 @@ local log = require("scada-common.log")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local types = require("scada-common.types")
local util = require("scada-common.util")
local themes = require("graphics.themes")
local core = require("graphics.core")
local themes = require("graphics.themes")
local DisplayBox = require("graphics.elements.displaybox")
local Div = require("graphics.elements.div")
@@ -45,7 +46,8 @@ local RIGHT = core.ALIGN.RIGHT
local changes = {
{ "v1.2.4", { "Added temperature scale options" } },
{ "v1.2.12", { "Added main UI theme", "Added front panel UI theme", "Added color accessibility modes" } },
{ "v1.3.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } }
{ "v1.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
@@ -69,7 +71,7 @@ local tool_ctl = {
nic = nil, ---@type nic
net_listen = false,
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
show_sv_cfg = nil, ---@type function
@@ -118,6 +120,7 @@ local tmp_cfg = {
SpeakerVolume = 1.0,
Time24Hour = true,
TempScale = 1,
EnergyScale = 1,
DisableFlowView = false,
MainDisplay = nil, ---@type string
FlowDisplay = nil, ---@type string
@@ -150,7 +153,8 @@ local fields = {
{ "UnitDisplays", "Unit Monitors", {} },
{ "SpeakerVolume", "Speaker Volume", 1.0 },
{ "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 },
{ "SVR_Channel", "SVR Channel", 16240 },
{ "CRD_Channel", "CRD Channel", 16243 },
@@ -304,7 +308,7 @@ local function config_view(display)
---@diagnostic disable-next-line: undefined-field
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}
@@ -332,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)}
y_start = y_start + 5
elseif tool_ctl.start_fail > 0 then
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device has no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device had no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
y_start = y_start + 5
end
@@ -376,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}}
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=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}
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}
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}
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 svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
@@ -407,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=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=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}
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}
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 svr_cto, api_cto = tonumber(svr_timeout.get_value()), tonumber(api_timeout.get_value())
@@ -432,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=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=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local tr_err = TextBox{parent=net_c_3,x=8,y=14,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 range_val = tonumber(range.get_value())
@@ -456,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=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 function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
@@ -466,7 +470,7 @@ local function config_view(display)
hide_key.set_value(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 v = key.get_value()
@@ -504,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}}
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=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_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}
@@ -533,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."}
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=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 count = tonumber(num_units.get_value())
@@ -570,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}}
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."}
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)}
@@ -591,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 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()
if tmp_cfg.MainDisplay == nil then
@@ -651,13 +655,13 @@ local function config_view(display)
if value == "0" or value == nil then mon_unit.set_value(0) end
end
TextBox{parent=mon_c_3,x=1,y=6,width=10,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}
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}
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
---@param iface string
@@ -719,7 +723,7 @@ local function config_view(display)
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=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."}
@@ -728,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}
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 vol = tonumber(s_vol.get_value())
@@ -748,19 +752,23 @@ local function config_view(display)
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=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}
TextBox{parent=crd_c_1,x=1,y=8,height=1,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}
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=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()
tmp_cfg.Time24Hour = clock_fmt.get_value() == 1
tmp_cfg.TempScale = temp_scale.get_value()
tmp_cfg.EnergyScale = energy_scale.get_value()
main_pane.set_value(7)
end
@@ -773,20 +781,20 @@ local function config_view(display)
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}
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 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 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
@@ -814,20 +822,20 @@ 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}}
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=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}
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}
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,height=1,text="Preview"}
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)}
@@ -854,7 +862,7 @@ local function config_view(display)
end
end
TextBox{parent=clr_c_2,x=1,y=7,height=1,width=10,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=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}
@@ -910,7 +918,7 @@ local function config_view(display)
clr_pane.set_value(1)
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=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
@@ -929,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}}
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)}
@@ -951,7 +959,10 @@ local function config_view(display)
end
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
load_settings(settings_cfg, true)
@@ -968,6 +979,8 @@ local function config_view(display)
try_set(dis_flow_view, ini_cfg.DisableFlowView)
try_set(s_vol, ini_cfg.SpeakerVolume)
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(path, ini_cfg.LogPath)
try_set(en_dbg, ini_cfg.LogDebug)
@@ -997,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.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()
main_pane.set_value(1)
@@ -1032,15 +1045,15 @@ local function config_view(display)
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)}
for _, change in ipairs(changes) do
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg}
TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
for _, v in ipairs(change[2]) do
local e = Div{parent=c_log,height=#util.strwrap(v,46)}
TextBox{parent=e,y=1,x=1,text="- ",height=1,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=e,y=1,x=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
end
end
@@ -1121,7 +1134,6 @@ local function config_view(display)
tool_ctl.nic.open(tmp_cfg.CRD_Channel)
tool_ctl.sv_addr = comms.BROADCAST
tool_ctl.sv_seq_num = 0
tool_ctl.net_listen = true
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.CRD })
@@ -1136,12 +1148,12 @@ local function config_view(display)
fac_config_list.remove_all()
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
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"))
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
@@ -1177,13 +1189,13 @@ local function config_view(display)
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,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,height=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="\x1a "..tmp_cfg.UnitCount.." Unit View Monitor"..util.trinary(plural,"s","")}
TextBox{parent=mon_reqs,x=1,y=1,text=" "..util.trinary(plural,"each ","").."must be 4 blocks wide by 4 tall",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=mon_reqs,x=1,y=1,text="\x1a 1 Main View Monitor"}
TextBox{parent=mon_reqs,x=1,y=1,text=" must be 8 blocks wide by "..m_at_least..tool_ctl.main_mon_h..asterisk.." tall",fg_bg=cpair(colors.gray,colors.white)}
if not tmp_cfg.DisableFlowView then
TextBox{parent=mon_reqs,x=1,y=1,height=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="\x1a 1 Flow View Monitor"}
TextBox{parent=mon_reqs,x=1,y=1,text=" must be 8 blocks wide by "..f_at_least..tool_ctl.flow_mon_h.." tall",fg_bg=cpair(colors.gray,colors.white)}
end
end
@@ -1285,8 +1297,8 @@ local function config_view(display)
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=8,y=1,height=1,text=iface}
TextBox{parent=line,x=1,y=1,width=6,text=assignment,fg_bg=cpair(util.trinary(assignment=="Unused",colors.red,colors.blue),colors.white)}
TextBox{parent=line,x=8,y=1,text=iface}
local w, h = ppm.monitor_block_size(dev.getSize())
@@ -1295,7 +1307,7 @@ local function config_view(display)
tool_ctl.gen_mon_list()
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}
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)}
@@ -1314,15 +1326,15 @@ local function config_view(display)
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,height=1,text=dc_list[i][1],fg_bg=cpair(colors.blue,colors.white)}
TextBox{parent=line,x=8,y=1,height=1,text="disconnected",fg_bg=cpair(colors.red,colors.white)}
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,height=1,text="?x?",fg_bg=cpair(colors.black,colors.white)}
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
@@ -1356,7 +1368,9 @@ local function config_view(display)
if f[1] == "AuthKey" then val = string.rep("*", string.len(val))
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "TempScale" then
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
val = util.strval(themes.ui_theme_name(raw))
elseif f[1] == "FrontPanelTheme" then

View File

@@ -38,6 +38,7 @@ function coordinator.load_config()
config.SpeakerVolume = settings.get("SpeakerVolume")
config.Time24Hour = settings.get("Time24Hour")
config.TempScale = settings.get("TempScale")
config.EnergyScale = settings.get("EnergyScale")
config.DisableFlowView = settings.get("DisableFlowView")
config.MainDisplay = settings.get("MainDisplay")
@@ -67,6 +68,8 @@ function coordinator.load_config()
cfv.assert_type_bool(config.Time24Hour)
cfv.assert_type_int(config.TempScale)
cfv.assert_range(config.TempScale, 1, 4)
cfv.assert_type_int(config.EnergyScale)
cfv.assert_range(config.EnergyScale, 1, 3)
cfv.assert_type_bool(config.DisableFlowView)
cfv.assert_type_table(config.UnitDisplays)
@@ -232,8 +235,8 @@ function coordinator.comms(version, nic, sv_watchdog)
local self = {
sv_linked = false,
sv_addr = comms.BROADCAST,
sv_seq_num = 0,
sv_r_seq_num = nil,
sv_seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
sv_r_seq_num = nil, ---@type nil|integer
sv_config_err = false,
last_est_ack = ESTABLISH_ACK.ALLOW,
last_api_est_acks = {},
@@ -492,7 +495,7 @@ function coordinator.comms(version, nic, sv_watchdog)
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_API_VERSION)
elseif dev_type == DEVICE_TYPE.PKT then
-- pocket linking request
local id = apisessions.establish_session(src_addr, firmware_v)
local id = apisessions.establish_session(src_addr, packet.scada_frame.seq_num(), firmware_v)
coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id))
local conf = iocontrol.get_db().facility.conf
@@ -515,15 +518,15 @@ function coordinator.comms(version, nic, sv_watchdog)
elseif r_chan == config.SVR_Channel then
-- check sequence number
if self.sv_r_seq_num == nil then
self.sv_r_seq_num = packet.scada_frame.seq_num()
elseif self.sv_linked and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.sv_r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: next = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return false
elseif self.sv_linked and src_addr ~= self.sv_addr then
log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?")
return false
else
self.sv_r_seq_num = packet.scada_frame.seq_num()
self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
end
-- feed watchdog on valid sequence number
@@ -702,11 +705,10 @@ function coordinator.comms(version, nic, sv_watchdog)
if conf.num_units == config.UnitCount then
-- init io controller
iocontrol.init(conf, public, config.TempScale)
iocontrol.init(conf, public, config.TempScale, config.EnergyScale)
self.sv_addr = src_addr
self.sv_linked = true
self.sv_r_seq_num = nil
self.sv_config_err = false
iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED)

View File

@@ -15,6 +15,11 @@ local pgi = require("coordinator.ui.pgi")
local ALARM_STATE = types.ALARM_STATE
local PROCESS = types.PROCESS
local ENERGY_SCALE = types.ENERGY_SCALE
local ENERGY_UNITS = types.ENERGY_SCALE_UNITS
local TEMP_SCALE = types.TEMP_SCALE
local TEMP_UNITS = types.TEMP_SCALE_UNITS
-- nominal RTT is ping (0ms to 10ms usually) + 500ms for CRD main loop tick
local WARN_RTT = 1000 -- 2x as long as expected w/ 0 ping
local HIGH_RTT = 1500 -- 3.33x as long as expected w/ 0 ping
@@ -47,23 +52,36 @@ end
-- initialize the coordinator IO controller
---@param conf facility_conf configuration
---@param comms coord_comms comms reference
---@param temp_scale integer temperature unit (1 = K, 2 = C, 3 = F, 4 = R)
function iocontrol.init(conf, comms, temp_scale)
---@param temp_scale TEMP_SCALE temperature unit
---@param energy_scale ENERGY_SCALE energy unit
function iocontrol.init(conf, comms, temp_scale, energy_scale)
io.temp_label = TEMP_UNITS[temp_scale]
io.energy_label = ENERGY_UNITS[energy_scale]
-- temperature unit label and conversion function (from Kelvin)
if temp_scale == 2 then
io.temp_label = "\xb0C"
if temp_scale == TEMP_SCALE.CELSIUS then
io.temp_convert = function (t) return t - 273.15 end
elseif temp_scale == 3 then
io.temp_label = "\xb0F"
elseif temp_scale == TEMP_SCALE.FAHRENHEIT then
io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end
elseif temp_scale == 4 then
io.temp_label = "\xb0R"
elseif temp_scale == TEMP_SCALE.RANKINE then
io.temp_convert = function (t) return 1.8 * t end
else
io.temp_label = "K"
io.temp_convert = function (t) return t end
end
-- energy unit label and conversion function (from Joules unless otherwise specified)
if energy_scale == ENERGY_SCALE.FE or energy_scale == ENERGY_SCALE.RF then
io.energy_convert = util.joules_to_fe_rf
io.energy_convert_from_fe = function (t) return t end
io.energy_convert_to_fe = function (t) return t end
else
io.energy_label = "J"
io.energy_convert = function (t) return t end
io.energy_convert_from_fe = util.fe_rf_to_joules
io.energy_convert_to_fe = util.joules_to_fe_rf
end
-- facility data structure
---@class ioctl_facility
io.facility = {
@@ -71,6 +89,7 @@ function iocontrol.init(conf, comms, temp_scale)
num_units = conf.num_units,
tank_mode = conf.cooling.fac_tank_mode,
tank_defs = conf.cooling.fac_tank_defs,
tank_list = conf.cooling.fac_tank_list,
all_sys_ok = false,
rtu_count = 0,
@@ -125,92 +144,6 @@ function iocontrol.init(conf, comms, temp_scale)
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
@@ -247,6 +180,9 @@ function iocontrol.init(conf, comms, temp_scale)
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,
@@ -688,7 +624,7 @@ function iocontrol.update_facility_status(status)
ps.publish("is_discharging", out_f > in_f)
if data and data.build then
local cap = util.joules_to_fe(data.build.transfer_cap)
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
@@ -1214,9 +1150,11 @@ function iocontrol.update_unit_statuses(statuses)
local unit_state = status[5]
if type(unit_state) == "table" then
if #unit_state == 6 then
if #unit_state == 8 then
unit.waste_mode = unit_state[5]
unit.waste_product = unit_state[6]
unit.last_rate_change_ms = unit_state[7]
unit.turbine_flow_stable = unit_state[8]
unit.unit_ps.publish("U_StatusLine1", unit_state[1])
unit.unit_ps.publish("U_StatusLine2", unit_state[2])

View File

@@ -70,8 +70,8 @@ function process.init(iocontrol, coord_comms)
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_charge_target", ctl_proc.charge_target)
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_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", 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_pu_fallback", ctl_proc.pu_fallback)
self.io.facility.ps.publish("process_sps_low_power", ctl_proc.sps_low_power)
@@ -316,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_burn_target", ctl_proc.burn_target)
self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target)
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_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", self.io.energy_convert_from_fe(ctl_proc.gen_target))
self.io.facility.start_ack(ack)
end

View File

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

View File

@@ -32,16 +32,17 @@ local PERIODICS = {
---@nodiscard
---@param id integer session ID
---@param s_addr integer device source address
---@param i_seq_num integer initial sequence number
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
local log_header = "pkt_session(" .. id .. "): "
local self = {
-- connection properties
seq_num = 0,
r_seq_num = nil,
seq_num = i_seq_num + 2, -- next after the establish approval was sent
r_seq_num = i_seq_num + 1,
connected = true,
conn_watchdog = util.new_watchdog(timeout),
last_rtt = 0,
@@ -104,13 +105,11 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
---@param pkt mgmt_frame|crdn_frame
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return
else
self.r_seq_num = pkt.scada_frame.seq_num()
self.r_seq_num = pkt.scada_frame.seq_num() + 1
end
-- feed watchdog
@@ -152,7 +151,9 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
u.reactor_data,
u.boiler_data_tbl,
u.turbine_data_tbl,
u.tank_data_tbl
u.tank_data_tbl,
u.last_rate_change_ms,
u.turbine_flow_stable
}
_send(CRDN_TYPE.API_GET_UNIT, data)
@@ -185,6 +186,10 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session
_close()
elseif pkt.type == MGMT_TYPE.ESTABLISH then
-- something is wrong, kill the session
_close()
log.warning(log_header .. "terminated session due to an unexpected ESTABLISH packet")
else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end

View File

@@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder")
local threads = require("coordinator.threads")
local COORDINATOR_VERSION = "v1.4.6"
local COORDINATOR_VERSION = "v1.5.6"
local CHUNK_LOAD_DELAY_S = 30.0
@@ -151,8 +151,8 @@ local function main()
-- core coordinator devices
crd_dev = {
speaker = ppm.get_device("speaker"),
modem = ppm.get_wireless_modem()
modem = ppm.get_wireless_modem(),
speaker = ppm.get_device("speaker")
},
-- system objects

View File

@@ -244,7 +244,7 @@ function threads.thread__main(smem)
return public
end
-- coordinator renderer thread, tasked with long duration re-draws
-- coordinator renderer thread, tasked with long duration draws
---@nodiscard
---@param smem crd_shared_memory
function threads.thread__render(smem)

View File

@@ -1,7 +1,7 @@
local style = require("coordinator.ui.style")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local core = require("graphics.core")
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)
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="W",x=3,y=5,height=1,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="C",x=28,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,width=1,fg_bg=text_fg}
TextBox{parent=boiler,text="S",x=27,y=5,width=1,fg_bg=text_fg}
TextBox{parent=boiler,text="C",x=28,y=5,width=1,fg_bg=text_fg}
local hcool = VerticalBar{parent=boiler,x=2,y=1,fg_bg=cpair(colors.orange,colors.gray),height=4,width=1}
local water = VerticalBar{parent=boiler,x=3,y=1,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1}

View File

@@ -1,5 +1,7 @@
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local core = require("graphics.core")
@@ -34,6 +36,8 @@ local function new_view(root, x, y, data, ps, id)
local ind_yel = style.ind_yel
local ind_wht = style.ind_wht
local db = iocontrol.get_db()
local title = "INDUCTION MATRIX"
if type(id) == "number" then title = title .. id end
@@ -42,30 +46,30 @@ local function new_view(root, x, y, data, ps, id)
-- 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=title,alignment=ALIGN.CENTER,width=33,height=1,x=1,y=2,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,x=1,y=2,fg_bg=cutout_fg_bg}
local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
local capacity = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Capacity:",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: ",format="%8.2f",value=0,width=26,fg_bg=text_fg}
local avg_chg = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",value=0,width=26,fg_bg=text_fg}
local input = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Input: ",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:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local output = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Output: ",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:",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: ",format="%8.2f",rate=true,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 energy = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Energy: ",unit=db.energy_label,format="%8.2f",value=0,width=26,fg_bg=text_fg}
local avg_chg = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="\xb7Average:",unit=db.energy_label,format="%8.2f",value=0,width=26,fg_bg=text_fg}
local input = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Input: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local avg_in = PowerIndicator{parent=rect,x=7,y=7,lu_colors=lu_col,label="\xb7Average:",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local output = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Output: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local avg_out = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="\xb7Average:",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local trans_cap = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Max I/O: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
status.register(ps, "computed_status", status.update)
capacity.register(ps, "max_energy", function (val) capacity.update(util.joules_to_fe(val)) end)
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)
energy.register(ps, "energy", function (val) energy.update(db.energy_convert(val)) end)
avg_chg.register(ps, "avg_charge", avg_chg.update)
input.register(ps, "last_input", function (val) input.update(util.joules_to_fe(val)) end)
input.register(ps, "last_input", function (val) input.update(db.energy_convert(val)) end)
avg_in.register(ps, "avg_inflow", avg_in.update)
output.register(ps, "last_output", function (val) output.update(util.joules_to_fe(val)) end)
output.register(ps, "last_output", function (val) output.update(db.energy_convert(val)) end)
avg_out.register(ps, "avg_outflow", avg_out.update)
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end)
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(db.energy_convert(val)) end)
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill: ",format="%7.2f",unit="%",value=0,width=20,fg_bg=text_fg}
local cells = DataIndicator{parent=rect,x=11,y=13,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg}
@@ -87,7 +91,7 @@ local function new_view(root, x, y, data, ps, id)
local in_cap = VerticalBar{parent=rect,x=7,y=12,fg_bg=cpair(colors.red,colors.gray),height=7,width=1}
local out_cap = VerticalBar{parent=rect,x=9,y=12,fg_bg=cpair(colors.blue,colors.gray),height=7,width=1}
TextBox{parent=rect,text="FILL I/O",x=2,y=20,height=1,width=8,fg_bg=label_fg}
TextBox{parent=rect,text="FILL I/O",x=2,y=20,width=8,fg_bg=label_fg}
local function calc_saturation(val)
if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then
@@ -99,7 +103,7 @@ local function new_view(root, x, y, data, ps, id)
in_cap.register(ps, "last_input", function (val) in_cap.update(calc_saturation(val)) end)
out_cap.register(ps, "last_output", function (val) out_cap.update(calc_saturation(val)) end)
local eta = TextBox{parent=rect,x=11,y=20,width=20,height=1,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=style.theme.field_box}
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 ")

View File

@@ -34,18 +34,18 @@ local function init(parent, id)
local ps_prefix = "pkt_" .. id .. "_"
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,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)}
TextBox{parent=entry,x=1,y=3,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,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)}
TextBox{parent=entry,x=1,y=3,text="",width=8,fg_bg=s_hi_box}
pkt_addr.register(ps, ps_prefix .. "addr", pkt_addr.set_value)
TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1}
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=label_fg}
TextBox{parent=entry,x=10,y=2,text="FW:",width=3}
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,fg_bg=label_fg}
pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value)
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1}
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4}
local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,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_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_pur = cpair(colors.black, colors.purple)
local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units
local db = iocontrol.get_db()
local facility = db.facility
local units = db.units
local main = Div{parent=root,width=128,height=24,x=x,y=y}
@@ -105,11 +107,11 @@ local function new_view(root, x, y)
fac_rad_h.register(facility.ps, "as_radiation", fac_rad_h.update)
gen_fault.register(facility.ps, "as_gen_fault", gen_fault.update)
TextBox{parent=main,y=23,text="Radiation",height=1,width=13,fg_bg=style.label}
TextBox{parent=main,y=23,text="Radiation",width=13,fg_bg=style.label}
local radiation = RadIndicator{parent=main,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=s_field}
radiation.register(facility.ps, "radiation", radiation.update)
TextBox{parent=main,x=15,y=23,text="Linked RTUs",height=1,width=11,fg_bg=style.label}
TextBox{parent=main,x=15,y=23,text="Linked RTUs",width=11,fg_bg=style.label}
local rtu_count = DataIndicator{parent=main,x=15,y=24,label="",format="%11d",value=0,lu_colors=lu_cpair,width=11,fg_bg=s_field}
rtu_count.register(facility.ps, "rtu_count", rtu_count.update)
@@ -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 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}
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}
TextBox{parent=chg_target,x=18,y=2,text="M"..db.energy_label,fg_bg=style.theme.label_fg}
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="M"..db.energy_label,commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
c_target.register(facility.ps, "process_charge_target", c_target.set_value)
cur_charge.register(facility.induction_ps_tbl[1], "avg_charge", function (fe) cur_charge.update(fe / 1000000) end)
cur_charge.register(facility.induction_ps_tbl[1], "avg_charge", function (fe) cur_charge.update(db.energy_convert_from_fe(fe) / 1000000) end)
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur}
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=s_hi_box}
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=gen_target,x=18,y=2,text="kFE/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}
TextBox{parent=gen_target,x=18,y=2,text="k"..db.energy_label.."/t",fg_bg=style.theme.label_fg}
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="k"..db.energy_label.."/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
g_target.register(facility.ps, "process_gen_target", g_target.set_value)
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end)
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(db.energy_convert(j) / 1000)) end)
-----------------
-- unit limits --
@@ -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 = 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}
@@ -249,8 +251,8 @@ local function new_view(root, x, y)
mode.register(facility.ps, "process_mode", mode.set_value)
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.white)}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.white)}
stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value)
stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value)
@@ -262,7 +264,10 @@ local function new_view(root, x, y)
local limits = {}
for i = 1, #rate_limits do limits[i] = rate_limits[i].get_value() end
process.save(mode.get_value(), b_target.get_value(), c_target.get_value(), g_target.get_value(), limits)
process.save(mode.get_value(), b_target.get_value(),
db.energy_convert_to_fe(c_target.get_value()),
db.energy_convert_to_fe(g_target.get_value()),
limits)
end
-- start automatic control after saving process control settings
@@ -320,7 +325,7 @@ local function new_view(root, x, y)
for i = 1, facility.num_units do
local unit = units[i] ---@type ioctl_unit
TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8,height=1}
TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8}
local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=ind_wht}
local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.waste.states_abbrv,value=1,min_width=6}
@@ -332,8 +337,8 @@ local function new_view(root, x, y)
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="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,height=1,x=1,y=2,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,x=1,y=2,fg_bg=cutout_fg_bg}
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17}

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}
TextBox{parent=reactor_fills,text="FUEL",x=2,y=1,height=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="HCOOL",x=2,y=4,height=1,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="FUEL",x=2,y=1,fg_bg=text_fg}
TextBox{parent=reactor_fills,text="COOL",x=2,y=2,fg_bg=text_fg}
TextBox{parent=reactor_fills,text="HCOOL",x=2,y=4,fg_bg=text_fg}
TextBox{parent=reactor_fills,text="WASTE",x=2,y=5,fg_bg=text_fg}
local fuel = HorizontalBar{parent=reactor_fills,x=8,y=1,show_percent=true,bar_fg_bg=cpair(style.theme.fuel_color,colors.gray),height=1,width=14}
local ccool = HorizontalBar{parent=reactor_fills,x=8,y=2,show_percent=true,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=14}

View File

@@ -1,4 +1,4 @@
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
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 lu_col = style.lu_colors
local db = iocontrol.get_db()
local turbine = Rectangle{parent=root,border=border(1,colors.gray,true),width=23,height=7,x=x,y=y}
local status = StateIndicator{parent=turbine,x=7,y=1,states=style.turbine.states,value=1,min_width=12}
local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg}
local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",unit=db.energy_label,format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg}
local flow_rate = DataIndicator{parent=turbine,x=5,y=4,lu_colors=lu_col,label="",unit="mB/t",format="%10.0f",value=0,commas=true,width=16,fg_bg=text_fg}
status.register(ps, "computed_status", status.update)
prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end)
prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(db.energy_convert(val)) end)
flow_rate.register(ps, "steam_input_rate", flow_rate.update)
local steam = VerticalBar{parent=turbine,x=2,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
local energy = VerticalBar{parent=turbine,x=3,y=1,fg_bg=cpair(colors.green,colors.gray),height=4,width=1}
TextBox{parent=turbine,text="S",x=2,y=5,height=1,width=1,fg_bg=text_fg}
TextBox{parent=turbine,text="E",x=3,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,width=1,fg_bg=text_fg}
steam.register(ps, "steam_fill", steam.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 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 --
@@ -81,20 +81,20 @@ local function init(parent, id)
core_map.register(u_ps, "temp", core_map.update)
core_map.register(u_ps, "size", function (s) core_map.resize(s[1], s[2]) end)
TextBox{parent=main,x=12,y=22,text="Heating Rate",height=1,width=12,fg_bg=style.label}
TextBox{parent=main,x=12,y=22,text="Heating Rate",width=12,fg_bg=style.label}
local heating_r = DataIndicator{parent=main,x=12,label="",format="%14.0f",value=0,unit="mB/t",commas=true,lu_colors=lu_cpair,width=19,fg_bg=s_field}
heating_r.register(u_ps, "heating_rate", heating_r.update)
TextBox{parent=main,x=12,y=25,text="Commanded Burn Rate",height=1,width=19,fg_bg=style.label}
TextBox{parent=main,x=12,y=25,text="Commanded Burn Rate",width=19,fg_bg=style.label}
local burn_r = DataIndicator{parent=main,x=12,label="",format="%14.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=19,fg_bg=s_field}
burn_r.register(u_ps, "burn_rate", burn_r.update)
TextBox{parent=main,text="F",x=2,y=22,width=1,height=1,fg_bg=style.label}
TextBox{parent=main,text="C",x=4,y=22,width=1,height=1,fg_bg=style.label}
TextBox{parent=main,text="\x1a",x=6,y=24,width=1,height=1,fg_bg=style.label}
TextBox{parent=main,text="\x1a",x=6,y=25,width=1,height=1,fg_bg=style.label}
TextBox{parent=main,text="H",x=8,y=22,width=1,height=1,fg_bg=style.label}
TextBox{parent=main,text="W",x=10,y=22,width=1,height=1,fg_bg=style.label}
TextBox{parent=main,text="F",x=2,y=22,width=1,fg_bg=style.label}
TextBox{parent=main,text="C",x=4,y=22,width=1,fg_bg=style.label}
TextBox{parent=main,text="\x1a",x=6,y=24,width=1,fg_bg=style.label}
TextBox{parent=main,text="\x1a",x=6,y=25,width=1,fg_bg=style.label}
TextBox{parent=main,text="H",x=8,y=22,width=1,fg_bg=style.label}
TextBox{parent=main,text="W",x=10,y=22,width=1,fg_bg=style.label}
local fuel = VerticalBar{parent=main,x=2,y=23,fg_bg=cpair(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}
@@ -122,20 +122,20 @@ local function init(parent, id)
end
end)
TextBox{parent=main,x=32,y=22,text="Core Temp",height=1,width=9,fg_bg=style.label}
TextBox{parent=main,x=32,y=22,text="Core Temp",width=9,fg_bg=style.label}
local fmt = util.trinary(string.len(db.temp_label) == 2, "%10.2f", "%11.2f")
local core_temp = DataIndicator{parent=main,x=32,label="",format=fmt,value=0,commas=true,unit=db.temp_label,lu_colors=lu_cpair,width=13,fg_bg=s_field}
core_temp.register(u_ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end)
TextBox{parent=main,x=32,y=25,text="Burn Rate",height=1,width=9,fg_bg=style.label}
TextBox{parent=main,x=32,y=25,text="Burn Rate",width=9,fg_bg=style.label}
local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=s_field}
act_burn_r.register(u_ps, "act_burn_rate", act_burn_r.update)
TextBox{parent=main,x=32,y=28,text="Damage",height=1,width=6,fg_bg=style.label}
TextBox{parent=main,x=32,y=28,text="Damage",width=6,fg_bg=style.label}
local damage_p = DataIndicator{parent=main,x=32,label="",format="%11.0f",value=0,unit="%",lu_colors=lu_cpair,width=13,fg_bg=s_field}
damage_p.register(u_ps, "damage", damage_p.update)
TextBox{parent=main,x=32,y=31,text="Radiation",height=1,width=21,fg_bg=style.label}
TextBox{parent=main,x=32,y=31,text="Radiation",width=21,fg_bg=style.label}
local radiation = RadIndicator{parent=main,x=32,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=s_field}
radiation.register(u_ps, "radiation", radiation.update)
@@ -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 stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=ALIGN.CENTER,fg_bg=gry_wht}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,alignment=ALIGN.CENTER,fg_bg=gry_wht}
stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value)
stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value)
@@ -205,7 +205,7 @@ local function init(parent, id)
-- RPS annunciator panel
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=8}
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,x=46,y=8}
local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9}
local rps_annunc = Div{parent=rps,width=31,height=10,x=2,y=1}
@@ -233,7 +233,7 @@ local function init(parent, id)
-- cooling annunciator panel
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=22}
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,x=46,y=22}
local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23}
local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1}
local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7}
@@ -265,11 +265,11 @@ local function init(parent, id)
if unit.num_boilers > 0 then
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}
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}
b1_hr.register(b_ps[1], "HeatingRateLow", b1_hr.update)
end
@@ -281,11 +281,11 @@ local function init(parent, id)
_add_space()
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}
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}
b2_hr.register(b_ps[2], "HeatingRateLow", b2_hr.update)
end
@@ -294,19 +294,19 @@ local function init(parent, id)
if available_space > 1 then _add_space() end
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=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}
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}
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}
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}
t1_trp.register(t_ps[1], "TurbineTrip", t1_trp.update)
@@ -315,19 +315,19 @@ local function init(parent, id)
_add_space()
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}
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}
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}
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}
t2_trp.register(t_ps[2], "TurbineTrip", t2_trp.update)
end
@@ -335,19 +335,19 @@ local function init(parent, id)
if unit.num_turbines > 2 then
if available_space > 3 then _add_space() end
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=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}
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}
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}
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}
t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update)
end
@@ -394,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)
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=48}
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=ALIGN.CENTER,width=33,x=46,y=48}
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1}
@@ -474,15 +474,15 @@ local function init(parent, id)
-- 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,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,height=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)}
TextBox{parent=alarm_panel,x=5,y=13,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.cyan)}
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)}
TextBox{parent=alarm_panel,x=5,text="\x95",width=1,fg_bg=cpair(s_hi_bright.bkg,colors.blue)}
--------------------------------
-- automatic control settings --
--------------------------------
TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,height=1,x=32,y=36}
TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,x=32,y=36}
local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37}
local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1}
@@ -499,8 +499,8 @@ local function init(parent, id)
auto_div.line_break()
TextBox{parent=auto_div,text="Prio. Group",height=1,width=11,fg_bg=style.label}
local auto_grp = TextBox{parent=auto_div,text="Manual",height=1,width=11,fg_bg=s_field}
TextBox{parent=auto_div,text="Prio. Group",width=11,fg_bg=style.label}
local auto_grp = TextBox{parent=auto_div,text="Manual",width=11,fg_bg=s_field}
auto_grp.register(u_ps, "auto_group", auto_grp.set_value)

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

View File

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

View File

@@ -39,7 +39,7 @@ local led_grn = style.led_grn
local function init(panel, num_units)
local ps = iocontrol.get_db().fp.ps
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,height=1,fg_bg=style.fp_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}
@@ -110,7 +110,7 @@ local function init(panel, num_units)
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=style.fp.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}
@@ -132,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 fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)

View File

@@ -29,10 +29,10 @@ local function init(main)
local units = iocontrol.get_db().units
-- window header message
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=ALIGN.CENTER,height=1,fg_bg=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}
-- 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)
datetime.register(facility.ps, "date_time", datetime.set_value)
@@ -79,7 +79,7 @@ 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)")
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

View File

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

View File

@@ -2,6 +2,7 @@
-- Generic Graphics Element
--
-- local log = require("scada-common.log")
local util = require("scada-common.util")
local core = require("graphics.core")
@@ -82,9 +83,10 @@ end
-- a base graphics element, should not be created on its own
---@nodiscard
---@param args graphics_args arguments
---@param constraint? function apply a dimensional constraint based on proposed dimensions function(frame) -> width, height
---@param child_offset_x? integer mouse event offset x
---@param child_offset_y? integer mouse event offset y
function element.new(args, child_offset_x, child_offset_y)
function element.new(args, constraint, child_offset_x, child_offset_y)
local self = {
id = nil, ---@type element_id|nil
is_root = args.parent == nil,
@@ -199,7 +201,7 @@ function element.new(args, child_offset_x, child_offset_y)
---@param next_y integer next line if no y was provided
function protected.prepare_template(offset_x, offset_y, next_y)
-- don't auto incrememnt y if inheriting height, that would cause an assertion
next_y = util.trinary(args.height == nil, 1, next_y)
next_y = util.trinary(args.height == nil and constraint == nil, 1, next_y)
-- record offsets in case there is a reposition
self.offset_x = offset_x
@@ -226,6 +228,13 @@ function element.new(args, child_offset_x, child_offset_y)
local w, h = self.p_window.getSize()
f.w = math.min(f.w, w - (f.x - 1))
f.h = math.min(f.h, h - (f.y - 1))
if type(constraint) == "function" then
-- constrain per provided constraint function (can only get smaller than available space)
w, h = constraint(f)
f.w = math.min(f.w, w)
f.h = math.min(f.h, h)
end
end
-- check frame
@@ -495,7 +504,10 @@ function element.new(args, child_offset_x, child_offset_y)
if args.parent ~= nil then
-- remove self from parent
-- log.debug("removing " .. self.id .. " from parent")
args.parent.__remove_child(self.id)
else
-- log.debug("no parent for " .. self.id .. " on delete attempt")
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
--
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 core = require("graphics.core")
@@ -32,7 +32,10 @@ local CENTER = core.ALIGN.CENTER
local RIGHT = core.ALIGN.RIGHT
-- 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
local configurator = {}
@@ -73,6 +76,8 @@ local tool_ctl = {
---@class pkt_config
local tmp_cfg = {
TempScale = 1,
EnergyScale = 1,
SVR_Channel = nil, ---@type integer
CRD_Channel = nil, ---@type integer
PKT_Channel = nil, ---@type integer
@@ -91,6 +96,8 @@ local settings_cfg = {}
-- all settings fields, their nice names, and their default values
local fields = {
{ "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN },
{ "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE },
{ "SVR_Channel", "SVR Channel", 16240 },
{ "CRD_Channel", "CRD Channel", 16243 },
{ "PKT_Channel", "PKT Channel", 16244 },
@@ -121,17 +128,18 @@ local function config_view(display)
---@diagnostic disable-next-line: undefined-field
local function exit() os.queueEvent("terminate") end
TextBox{parent=display,y=1,text="Pocket Configurator",alignment=CENTER,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 main_page = Div{parent=root_pane_div,x=1,y=1}
local ui_cfg = Div{parent=root_pane_div,x=1,y=1}
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
local summary = Div{parent=root_pane_div,x=1,y=1}
local changelog = Div{parent=root_pane_div,x=1,y=1}
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,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
@@ -148,7 +156,7 @@ local function config_view(display)
tool_ctl.viewing_config = true
tool_ctl.gen_summary(settings_cfg)
tool_ctl.settings_apply.hide(true)
main_pane.set_value(4)
main_pane.set_value(5)
end
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
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
@@ -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}}
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=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}
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}
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}
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 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
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}
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=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}
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 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=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=8,height=4,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
local range = NumberField{parent=net_c_3,x=1,y=13,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local tr_err = TextBox{parent=net_c_3,x=1,y=14,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 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=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 function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
@@ -262,13 +295,13 @@ local function config_view(display)
hide_key.set_value(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 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(3)
main_pane.set_value(4)
key_err.hide(true)
else key_err.show() 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}
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}
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 en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
TextBox{parent=log_c_1,x=3,y=11,height=4,text="This results in much larger log files. Use only as needed.",fg_bg=g_lg_fg_bg}
local path_err = TextBox{parent=log_c_1,x=1,y=14,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()
if path.get_value() ~= "" then
@@ -306,11 +339,11 @@ local function config_view(display)
tool_ctl.viewing_config = false
tool_ctl.importing_legacy = false
tool_ctl.settings_apply.show()
main_pane.set_value(4)
main_pane.set_value(5)
else path_err.show() end
end
PushButton{parent=log_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(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}
--#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}}
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)}
@@ -335,7 +368,7 @@ local function config_view(display)
tool_ctl.importing_legacy = false
tool_ctl.settings_apply.show()
else
main_pane.set_value(3)
main_pane.set_value(4)
end
end
@@ -346,12 +379,17 @@ local function config_view(display)
end
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
load_settings(settings_cfg, true)
load_settings(ini_cfg)
try_set(temp_scale, ini_cfg.TempScale)
try_set(energy_scale, ini_cfg.EnergyScale)
try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(crd_chan, ini_cfg.CRD_Channel)
try_set(pkt_chan, ini_cfg.PKT_Channel)
@@ -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.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()
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}
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)}
for _, change in ipairs(changes) do
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg}
TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
for _, v in ipairs(change[2]) do
local e = Div{parent=c_log,height=#util.strwrap(v,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)}
end
end
@@ -444,7 +482,7 @@ local function config_view(display)
tool_ctl.gen_summary(tmp_cfg)
sum_pane.set_value(1)
main_pane.set_value(4)
main_pane.set_value(5)
tool_ctl.importing_legacy = true
end
@@ -473,8 +511,15 @@ local function config_view(display)
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") end
if f[1] == "AuthKey" then
val = string.rep("*", string.len(val))
elseif f[1] == "LogMode" then
val = util.trinary(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "TempScale" then
val = util.strval(types.TEMP_SCALE_NAMES[raw])
elseif f[1] == "EnergyScale" then
val = util.strval(types.ENERGY_SCALE_NAMES[raw])
end
if val == "nil" then val = "<not set>" end
@@ -532,9 +577,7 @@ function configurator.configure(ask_config)
local event, param1, param2, param3 = util.pull_event()
-- handle event
if event == "timer" then
tcd.handle(param1)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
if event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
local m_e = core.events.new_mouse_event(event, param1, param2, param3)
if m_e then display.handle_mouse(m_e) end
elseif event == "char" or event == "key" or event == "key_up" then

View File

@@ -2,7 +2,8 @@
-- I/O Control for Pocket Integration with Supervisor & Coordinator
--
local log = require("scada-common.log")
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 util = require("scada-common.util")
@@ -10,6 +11,11 @@ local util = require("scada-common.util")
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
@@ -26,200 +32,22 @@ local LINK_STATE = {
iocontrol.LINK_STATE = LINK_STATE
---@enum POCKET_APP_ID
local APP_ID = {
ROOT = 1,
-- main app page
UNITS = 2,
ABOUT = 3,
-- diag app page
ALARMS = 4,
-- other
DUMMY = 5,
NUM_APPS = 5
}
iocontrol.APP_ID = APP_ID
---@class pocket_ioctl
local io = {
version = "unknown",
ps = psil.create()
}
---@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
-- allocate the page navigation system
function iocontrol.alloc_nav()
local self = {
pane = nil, ---@type graphics_element
apps = {},
containers = {},
cur_app = APP_ID.ROOT
}
self.cur_page = self.root
---@class pocket_nav
io.nav = {}
-- set the root pane element to switch between apps with
---@param root_pane graphics_element
function io.nav.set_pane(root_pane)
self.pane = root_pane
end
function io.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
function io.nav.register_app(app_id, container, pane)
---@class pocket_app
local app = {
loaded = false,
load = nil,
cur_page = nil, ---@type nav_tree_page
pane = pane,
paned_pages = {},
sidebar_items = {}
}
app.load = function () app.loaded = true 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
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_on_load(on_load)
app.load = function ()
on_load()
app.loaded = true
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? 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
-- 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
-- get a list of the app containers (usually Div elements)
function io.nav.get_containers() return self.containers end
-- open a given app
---@param app_id POCKET_APP_ID
function io.nav.open_app(app_id)
local app = self.apps[app_id] ---@type pocket_app
if app then
if not app.loaded then app.load() 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
-- get the currently active page
---@return nav_tree_page
function io.nav.get_current_page()
return self.apps[self.cur_app].get_current_page()
end
-- attempt to navigate up
function io.nav.nav_up()
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")
io.nav.open_app(APP_ID.ROOT)
end
end
end
local config = nil ---@type pkt_config
-- initialize facility-independent components of pocket iocontrol
---@param comms pocket_comms
function iocontrol.init_core(comms)
iocontrol.alloc_nav()
---@param nav pocket_nav
---@param cfg pkt_config
function iocontrol.init_core(comms, nav, cfg)
config = cfg
io.nav = nav
---@class pocket_ioctl_diag
io.diag = {}
@@ -266,24 +94,36 @@ function iocontrol.init_core(comms)
end
-- initialize facility-dependent components of pocket iocontrol
---@param conf facility_conf configuration
---@param temp_scale 1|2|3|4 temperature unit (1 = K, 2 = C, 3 = F, 4 = R)
function iocontrol.init_fac(conf, temp_scale)
---@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 == 2 then
io.temp_label = "\xb0C"
if temp_scale == TEMP_SCALE.CELSIUS then
io.temp_convert = function (t) return t - 273.15 end
elseif temp_scale == 3 then
io.temp_label = "\xb0F"
elseif temp_scale == TEMP_SCALE.FAHRENHEIT then
io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end
elseif temp_scale == 4 then
io.temp_label = "\xb0R"
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 = {
@@ -454,6 +294,9 @@ function iocontrol.init_fac(conf, temp_scale)
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,
@@ -507,8 +350,8 @@ end
-- set network link state
---@param state POCKET_LINK_STATE
---@param sv_addr integer? supervisor address if linked
---@param api_addr integer? coordinator address if linked
---@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)
@@ -520,12 +363,25 @@ function iocontrol.report_link_state(state, sv_addr, api_addr)
io.ps.publish("crd_conn_quality", 0)
end
if state == LINK_STATE.LINKED then
io.ps.publish("sv_addr", sv_addr)
io.ps.publish("api_addr", api_addr)
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)
@@ -585,186 +441,456 @@ function iocontrol.record_facility_data(data)
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)
if type(data[1]) == "number" and io.units[data[1]] then
local unit = io.units[data[1]] ---@type pioctl_unit
local unit = io.units[data[1]] ---@type pioctl_unit
unit.connected = data[2]
unit.rtu_hw = data[3]
unit.alarms = data[4]
unit.connected = data[2]
unit.rtu_hw = data[3]
unit.alarms = data[4]
--#region Annunciator
--#region Annunciator
unit.annunciator = data[5]
unit.annunciator = data[5]
local rcs_disconn, rcs_warn, rcs_hazard = false, false, false
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
-- split up online arrays
local every = true
for id = 1, #val do
every = every and val[id]
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])
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]
else
unit.turbine_ps_tbl[id].publish(key, val[id])
end
end
if key == "GeneratorTrip" and any then
rcs_warn = true
elseif (key == "TurbineOverSpeed" or key == "TurbineTrip") and any then
rcs_hazard = true
end
if not every then rcs_disconn = true end
unit.unit_ps.publish("U_" .. key, any)
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
-- non-table fields
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
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 or anc.MaxWaterReturnFeed
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 rps_status = 1
if unit.connected then
-- update RPS status
if unit.reactor_data.rps_tripped then
control_status = 2
rps_status = util.trinary(unit.reactor_data.rps_trip_cause == "manual", 3, 2)
else rps_status = 4 end
-- update reactor/control status
if unit.reactor_data.mek_status.status then
reactor_status = 4
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
else
if unit.reactor_data.no_reactor then
reactor_status = 2
elseif not unit.reactor_data.formed or unit.reactor_data.rps_status.force_dis then
reactor_status = 3
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
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
unit.unit_ps.publish("U_ControlStatus", control_status)
unit.unit_ps.publish("U_ReactorStatus", reactor_status)
unit.unit_ps.publish("U_RPS", rps_status)
--#endregion
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
if unit.rtu_hw.boilers[id].connected then
if unit.rtu_hw.boilers[id].faulted then
boiler_status = 3
elseif boiler.formed then
boiler_status = 4
else
boiler_status = 2
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
ps.publish("BoilerStatus", boiler_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
if unit.rtu_hw.turbines[id].connected then
if unit.rtu_hw.turbines[id].faulted then
turbine_status = 3
elseif turbine.formed then
turbine_status = 4
else
turbine_status = 2
end
end
ps.publish("TurbineStatus", turbine_status)
end
unit.tank_data_tbl = data[9]
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

View File

@@ -14,6 +14,18 @@ local LINK_STATE = iocontrol.LINK_STATE
local pocket = {}
local MQ__RENDER_CMD = {
UNLOAD_SV_APPS = 1,
UNLOAD_API_APPS = 2
}
local MQ__RENDER_DATA = {
LOAD_APP = 1
}
pocket.MQ__RENDER_CMD = MQ__RENDER_CMD
pocket.MQ__RENDER_DATA = MQ__RENDER_DATA
---@type pkt_config
local config = {}
@@ -23,6 +35,9 @@ pocket.config = config
function pocket.load_config()
if not settings.load("/pocket.settings") then return false end
config.TempScale = settings.get("TempScale")
config.EnergyScale = settings.get("EnergyScale")
config.SVR_Channel = settings.get("SVR_Channel")
config.CRD_Channel = settings.get("CRD_Channel")
config.PKT_Channel = settings.get("PKT_Channel")
@@ -36,6 +51,11 @@ function pocket.load_config()
local cfv = util.new_validator()
cfv.assert_type_int(config.TempScale)
cfv.assert_range(config.TempScale, 1, 4)
cfv.assert_type_int(config.EnergyScale)
cfv.assert_range(config.EnergyScale, 1, 3)
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.CRD_Channel)
cfv.assert_channel(config.PKT_Channel)
@@ -58,26 +78,310 @@ function pocket.load_config()
return cfv.valid()
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
---@nodiscard
---@param version string pocket version
---@param nic nic network interface device
---@param sv_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 = {
sv = {
linked = false,
addr = comms.BROADCAST,
seq_num = 0,
r_seq_num = nil, ---@type nil|integer
seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, ---@type nil|integer
last_est_ack = ESTABLISH_ACK.ALLOW
},
api = {
linked = false,
addr = comms.BROADCAST,
seq_num = 0,
r_seq_num = nil, ---@type nil|integer
seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, ---@type nil|integer
last_est_ack = ESTABLISH_ACK.ALLOW
},
establish_delay_counter = 0
@@ -163,6 +467,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
-- close connection to the supervisor
function public.close_sv()
sv_watchdog.cancel()
nav.unload_sv()
self.sv.linked = false
self.sv.r_seq_num = nil
self.sv.addr = comms.BROADCAST
@@ -172,6 +477,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
-- close connection to coordinator API server
function public.close_api()
api_watchdog.cancel()
nav.unload_api()
self.api.linked = false
self.api.r_seq_num = nil
self.api.addr = comms.BROADCAST
@@ -187,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
function public.link_update()
if not self.sv.linked then
iocontrol.report_link_state(util.trinary(self.api.linked, LINK_STATE.API_LINK_ONLY, LINK_STATE.UNLINKED))
if self.api.linked then
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY, false, nil)
else
iocontrol.report_link_state(LINK_STATE.UNLINKED, false, false)
end
if self.establish_delay_counter <= 0 then
_send_sv_establish()
@@ -196,7 +506,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
self.establish_delay_counter = self.establish_delay_counter - 1
end
elseif not self.api.linked then
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY)
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, nil, false)
if self.establish_delay_counter <= 0 then
_send_api_establish()
@@ -204,9 +514,6 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
else
self.establish_delay_counter = self.establish_delay_counter - 1
end
else
-- linked, all good!
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
end
end
@@ -274,7 +581,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
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))
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
@@ -301,16 +608,16 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
elseif r_chan == config.CRD_Channel then
-- check sequence number
if self.api.r_seq_num == nil then
self.api.r_seq_num = packet.scada_frame.seq_num()
elseif self.connected and ((self.api.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
log.warning("sequence out-of-order (API): last = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
self.api.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.api.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order (API): next = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
elseif self.api.linked and (src_addr ~= self.api.addr) then
log.debug("received packet from unknown computer " .. src_addr .. " while linked (API expected " .. self.api.addr ..
"); channel in use by another system?")
return
else
self.api.r_seq_num = packet.scada_frame.seq_num()
self.api.r_seq_num = packet.scada_frame.seq_num() + 1
end
-- feed watchdog on valid sequence number
@@ -324,7 +631,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
iocontrol.record_facility_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_UNIT then
if _check_length(packet, 9) 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
@@ -353,6 +660,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
elseif packet.type == MGMT_TYPE.CLOSE then
-- handle session close
api_watchdog.cancel()
nav.unload_api()
self.api.linked = false
self.api.r_seq_num = nil
self.api.addr = comms.BROADCAST
@@ -371,18 +679,19 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
-- get configuration
local conf = { num_units = fac_config[1], cooling = fac_config[2] }
---@todo unit options
iocontrol.init_fac(conf, 1)
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, self.sv.addr, self.api.addr)
iocontrol.report_link_state(LINK_STATE.LINKED, nil, self.api.addr)
else
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY)
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")
@@ -390,24 +699,29 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
else
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
elseif est_ack == ESTABLISH_ACK.BAD_API_VERSION then
if self.api.last_est_ack ~= est_ack then
log.info("coordinator api version mismatch")
end
else
log.debug("coordinator SCADA_MGMT establish packet reply unsupported")
if self.api.last_est_ack ~= est_ack then
if est_ack == ESTABLISH_ACK.DENY then
log.info("coordinator connection denied")
iocontrol.report_crd_link_error("denied")
elseif est_ack == ESTABLISH_ACK.COLLISION then
log.info("coordinator connection denied due to collision")
iocontrol.report_crd_link_error("collision")
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
log.info("coordinator comms version mismatch")
iocontrol.report_crd_link_error("comms version mismatch")
elseif est_ack == ESTABLISH_ACK.BAD_API_VERSION then
log.info("coordinator api version mismatch")
iocontrol.report_crd_link_error("API version mismatch")
else
log.debug("coordinator SCADA_MGMT establish packet reply unsupported")
iocontrol.report_crd_link_error("unknown reply")
end
end
-- unlink
self.api.addr = comms.BROADCAST
self.api.linked = false
end
self.api.last_est_ack = est_ack
@@ -421,16 +735,16 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
elseif r_chan == config.SVR_Channel then
-- check sequence number
if self.sv.r_seq_num == nil then
self.sv.r_seq_num = packet.scada_frame.seq_num()
elseif self.connected and ((self.sv.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
log.warning("sequence out-of-order (SVR): last = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
self.sv.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.sv.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order (SVR): next = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
elseif self.sv.linked and (src_addr ~= self.sv.addr) then
log.debug("received packet from unknown computer " .. src_addr .. " while linked (SVR expected " .. self.sv.addr ..
"); channel in use by another system?")
return
else
self.sv.r_seq_num = packet.scada_frame.seq_num()
self.sv.r_seq_num = packet.scada_frame.seq_num() + 1
end
-- feed watchdog on valid sequence number
@@ -459,6 +773,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
elseif packet.type == MGMT_TYPE.CLOSE then
-- handle session close
sv_watchdog.cancel()
nav.unload_sv()
self.sv.linked = false
self.sv.r_seq_num = nil
self.sv.addr = comms.BROADCAST
@@ -518,25 +833,33 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
self.sv.linked = true
self.sv.addr = src_addr
iocontrol.report_svr_link_error("")
if self.api.linked then
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, nil)
else
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY)
end
elseif est_ack == ESTABLISH_ACK.DENY then
if self.sv.last_est_ack ~= est_ack then
log.info("supervisor connection denied")
end
elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.sv.last_est_ack ~= est_ack then
log.info("supervisor connection denied due to collision")
end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.sv.last_est_ack ~= est_ack then
log.info("supervisor comms version mismatch")
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, self.sv.addr, nil)
end
else
log.debug("supervisor SCADA_MGMT establish packet reply unsupported")
if self.sv.last_est_ack ~= est_ack then
if est_ack == ESTABLISH_ACK.DENY then
log.info("supervisor connection denied")
iocontrol.report_svr_link_error("denied")
elseif est_ack == ESTABLISH_ACK.COLLISION then
log.info("supervisor connection denied due to collision")
iocontrol.report_svr_link_error("collision")
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
log.info("supervisor comms version mismatch")
iocontrol.report_svr_link_error("comms version mismatch")
else
log.debug("supervisor SCADA_MGMT establish packet reply unsupported")
iocontrol.report_svr_link_error("unknown reply")
end
end
-- unlink
self.sv.addr = comms.BROADCAST
self.sv.linked = false
end
self.sv.last_est_ack = est_ack
@@ -559,6 +882,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
---@nodiscard
function public.is_api_linked() return self.api.linked end
-- check if we are still linked with the supervisor and coordinator
---@nodiscard
function public.is_linked() return self.sv.linked and self.api.linked end
return public
end

View File

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

View File

@@ -2,27 +2,35 @@
-- SCADA System Access on a Pocket Computer
--
---@diagnostic disable-next-line: undefined-global
local _is_pocket_env = pocket or periphemu -- luacheck: ignore pocket
require("/initenv").init_env()
local crash = require("scada-common.crash")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core")
local configure = require("pocket.configure")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer")
local threads = require("pocket.threads")
local POCKET_VERSION = "v0.9.0-alpha"
local POCKET_VERSION = "v0.11.8-alpha"
local println = util.println
local println_ts = util.println_ts
-- check environment (allows Pocket or CraftOS-PC)
if not _is_pocket_env then
println("You can only use this application on a pocket computer.")
return
end
----------------------------------------
-- get configuration
----------------------------------------
@@ -72,9 +80,51 @@ local function main()
iocontrol.get_db().version = POCKET_VERSION
----------------------------------------
-- setup communications & clocks
-- memory allocation
----------------------------------------
-- shared memory across threads
---@class pkt_shared_memory
local __shared_memory = {
-- pocket system state flags
---@class pkt_state
pkt_state = {
ui_ok = false,
ui_error = nil,
shutdown = false
},
-- core pocket devices
pkt_dev = {
modem = ppm.get_wireless_modem()
},
-- system objects
pkt_sys = {
nic = nil, ---@type nic
pocket_comms = nil, ---@type pocket_comms
sv_wd = nil, ---@type watchdog
api_wd = nil, ---@type watchdog
nav = nil ---@type pocket_nav
},
-- message queues
q = {
mq_render = mqueue.new()
}
}
local smem_dev = __shared_memory.pkt_dev
local smem_sys = __shared_memory.pkt_sys
local pkt_state = __shared_memory.pkt_state
----------------------------------------
-- setup system
----------------------------------------
smem_sys.nav = pocket.init_nav(__shared_memory)
-- message authentication init
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
network.init_mac(config.AuthKey)
@@ -83,112 +133,59 @@ local function main()
iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED)
-- get the communications modem
local modem = ppm.get_wireless_modem()
if modem == nil then
if smem_dev.modem == nil then
println("startup> wireless modem not found: please craft the pocket computer with a wireless modem")
log.fatal("startup> no wireless modem on startup")
return
end
-- create connection watchdogs
local conn_wd = {
sv = util.new_watchdog(config.ConnTimeout),
api = util.new_watchdog(config.ConnTimeout)
}
conn_wd.sv.cancel()
conn_wd.api.cancel()
smem_sys.sv_wd = util.new_watchdog(config.ConnTimeout)
smem_sys.sv_wd.cancel()
smem_sys.api_wd = util.new_watchdog(config.ConnTimeout)
smem_sys.api_wd.cancel()
log.debug("startup> conn watchdogs created")
-- create network interface then setup comms
local nic = network.nic(modem)
local pocket_comms = pocket.comms(POCKET_VERSION, nic, conn_wd.sv, conn_wd.api)
smem_sys.nic = network.nic(smem_dev.modem)
smem_sys.pocket_comms = pocket.comms(POCKET_VERSION, smem_sys.nic, smem_sys.sv_wd, smem_sys.api_wd, smem_sys.nav)
log.debug("startup> comms init")
-- base loop clock (2Hz, 10 ticks)
local MAIN_CLOCK = 0.5
local loop_clock = util.new_clock(MAIN_CLOCK)
-- init I/O control
iocontrol.init_core(pocket_comms)
iocontrol.init_core(smem_sys.pocket_comms, smem_sys.nav, config)
----------------------------------------
-- start the UI
----------------------------------------
local ui_ok, message = renderer.try_start_ui()
if not ui_ok then
println(util.c("UI error: ", message))
log.error(util.c("startup> GUI render failed with error ", message))
else
-- start clock
loop_clock.start()
local ui_message
pkt_state.ui_ok, ui_message = renderer.try_start_ui()
if not pkt_state.ui_ok then
println(util.c("UI error: ", ui_message))
log.error(util.c("startup> GUI render failed with error ", ui_message))
end
----------------------------------------
-- main event loop
-- start system
----------------------------------------
if ui_ok then
-- start connection watchdogs
conn_wd.sv.feed()
conn_wd.api.feed()
log.debug("startup> conn watchdogs started")
if pkt_state.ui_ok then
-- init threads
local main_thread = threads.thread__main(__shared_memory)
local render_thread = threads.thread__render(__shared_memory)
local io_db = iocontrol.get_db()
local nav = io_db.nav
log.info("startup> completed")
-- main event loop
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event
if event == "timer" then
if loop_clock.is_clock(param1) then
-- main loop tick
-- relink if necessary
pocket_comms.link_update()
-- update any tasks for the active page
local page_tasks = nav.get_current_page().tasks
for i = 1, #page_tasks do page_tasks[i]() end
loop_clock.start()
elseif conn_wd.sv.is_timer(param1) then
-- supervisor watchdog timeout
log.info("supervisor server timeout")
pocket_comms.close_sv()
elseif conn_wd.api.is_timer(param1) then
-- coordinator watchdog timeout
log.info("coordinator api server timeout")
pocket_comms.close_api()
else
-- a non-clock/main watchdog timer event
-- notify timer callback dispatcher
tcd.handle(param1)
end
elseif event == "modem_message" then
-- got a packet
local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
pocket_comms.handle_packet(packet)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
event == "double_click" then
-- handle a monitor touch event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
end
-- check for termination request
if event == "terminate" or ppm.should_terminate() then
log.info("terminate requested, closing server connections...")
pocket_comms.close()
log.info("connections closed")
break
end
end
-- run threads
parallel.waitForAll(main_thread.p_exec, render_thread.p_exec)
renderer.close_ui()
if not pkt_state.ui_ok then
println(util.c("UI crashed with error: ", pkt_state.ui_error))
end
else
println_ts("UI creation failed")
end
println_ts("exited")

219
pocket/threads.lua Normal file
View File

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

View File

@@ -3,6 +3,7 @@
--
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local core = require("graphics.core")
@@ -15,9 +16,10 @@ local Checkbox = require("graphics.elements.controls.checkbox")
local PushButton = require("graphics.elements.controls.push_button")
local SwitchButton = require("graphics.elements.controls.switch_button")
local ALIGN = core.ALIGN
local cpair = core.cpair
local ALIGN = core.ALIGN
local APP_ID = pocket.APP_ID
-- create diagnostic app pages
---@param root graphics_element parent
@@ -30,7 +32,7 @@ local function create_pages(root)
local alarm_test = Div{parent=root,x=1,y=1}
local alarm_app = db.nav.register_app(iocontrol.APP_ID.ALARMS, alarm_test)
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 }
@@ -44,13 +46,13 @@ local function create_pages(root)
local audio = Div{parent=alarm_test,x=1,y=1}
TextBox{parent=audio,y=1,text="Alarm Sounder Tests",height=1,alignment=ALIGN.CENTER}
TextBox{parent=audio,y=1,text="Alarm Sounder Tests",alignment=ALIGN.CENTER}
ttest.ready_warn = TextBox{parent=audio,y=2,text="",height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
ttest.ready_warn = TextBox{parent=audio,y=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
local tones = Div{parent=audio,x=2,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)}
TextBox{parent=tones,text="Tones",height=1,alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
TextBox{parent=tones,text="Tones",alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
local test_btns = {}
test_btns[1] = SwitchButton{parent=tones,text="TEST 1",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_1}
@@ -73,7 +75,7 @@ local function create_pages(root)
local alarms = Div{parent=audio,x=11,y=3,height=15,fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=alarms,text="Alarms (\x13)",height=1,alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
TextBox{parent=alarms,text="Alarms (\x13)",alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
local alarm_btns = {}
alarm_btns[1] = Checkbox{parent=alarms,label="BREACH",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_breach}
@@ -100,7 +102,7 @@ local function create_pages(root)
local states = Div{parent=audio,x=2,y=14,height=5,width=8}
TextBox{parent=states,text="States",height=1,alignment=ALIGN.CENTER}
TextBox{parent=states,text="States",alignment=ALIGN.CENTER}
local t_1 = IndicatorLight{parent=states,label="1",colors=c_blue_gray}
local t_2 = IndicatorLight{parent=states,label="2",colors=c_blue_gray}
local t_3 = IndicatorLight{parent=states,label="3",colors=c_blue_gray}

View File

@@ -3,12 +3,15 @@
--
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)
@@ -16,7 +19,7 @@ local function create_pages(root)
local main = Div{parent=root,x=1,y=1}
db.nav.register_app(iocontrol.APP_ID.DUMMY, main).new_page(nil, function () end)
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}

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

View File

@@ -19,9 +19,10 @@ local TextBox = require("graphics.elements.textbox")
local PushButton = require("graphics.elements.controls.push_button")
local ALIGN = core.ALIGN
local cpair = core.cpair
local ALIGN = core.ALIGN
local APP_ID = pocket.APP_ID
-- create system app pages
---@param root graphics_element parent
@@ -34,7 +35,7 @@ local function create_pages(root)
local about_root = Div{parent=root,x=1,y=1}
local about_app = db.nav.register_app(iocontrol.APP_ID.ABOUT, about_root)
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)
@@ -43,7 +44,7 @@ local function create_pages(root)
local about = Div{parent=about_root,x=1,y=2}
TextBox{parent=about,y=1,text="System Information",height=1,alignment=ALIGN.CENTER}
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)
@@ -58,36 +59,36 @@ local function create_pages(root)
local config = pocket.config
local nt_div = Div{parent=about_root,x=1,y=2}
TextBox{parent=nt_div,y=1,text="Network Details",height=1,alignment=ALIGN.CENTER}
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",height=1,alignment=ALIGN.LEFT,fg_bg=label}
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),height=1,alignment=ALIGN.LEFT}
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",height=1,alignment=ALIGN.LEFT,fg_bg=label}
local sv = TextBox{parent=nt_div,x=2,text="",height=1,alignment=ALIGN.LEFT}
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",height=1,alignment=ALIGN.LEFT,fg_bg=label}
local coord = TextBox{parent=nt_div,x=2,text="",height=1,alignment=ALIGN.LEFT}
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", function (addr) sv.set_value(util.c(addr, ":", config.SVR_Channel)) end)
coord.register(db.ps, "api_addr", function (addr) coord.set_value(util.c(addr, ":", config.CRD_Channel)) end)
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",height=1,alignment=ALIGN.LEFT,fg_bg=label}
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,height=1,alignment=ALIGN.LEFT}
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",height=1,alignment=ALIGN.CENTER}
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}
@@ -95,45 +96,45 @@ local function create_pages(root)
local fw_list = Div{parent=fw_list_box,x=1,y=2,height=18}
TextBox{parent=fw_list,x=2,text="Pocket Version",height=1,alignment=ALIGN.LEFT,fg_bg=label}
TextBox{parent=fw_list,x=2,text=db.version,height=1,alignment=ALIGN.LEFT}
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",height=1,alignment=ALIGN.LEFT,fg_bg=label}
TextBox{parent=fw_list,x=2,text=comms.version,height=1,alignment=ALIGN.LEFT}
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",height=1,alignment=ALIGN.LEFT,fg_bg=label}
TextBox{parent=fw_list,x=2,text=comms.api_version,height=1,alignment=ALIGN.LEFT}
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",height=1,alignment=ALIGN.LEFT,fg_bg=label}
TextBox{parent=fw_list,x=2,text=util.version,height=1,alignment=ALIGN.LEFT}
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",height=1,alignment=ALIGN.LEFT,fg_bg=label}
TextBox{parent=fw_list,x=2,text=core.version,height=1,alignment=ALIGN.LEFT}
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",height=1,alignment=ALIGN.LEFT,fg_bg=label}
TextBox{parent=fw_list,x=2,text=lockbox.version,height=1,alignment=ALIGN.LEFT}
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",height=1,alignment=ALIGN.CENTER}
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",height=1,alignment=ALIGN.LEFT,fg_bg=label}
TextBox{parent=hw_div,x=2,text=_VERSION,height=1,alignment=ALIGN.LEFT}
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",height=1,alignment=ALIGN.LEFT,fg_bg=label}
TextBox{parent=hw_div,x=2,text=_HOST,height=6,alignment=ALIGN.LEFT}
TextBox{parent=hw_div,x=2,text="Environment",fg_bg=label}
TextBox{parent=hw_div,x=2,text=_HOST,height=6}
--#endregion

View File

@@ -2,40 +2,43 @@
-- Unit Overview Page
--
local util = require("scada-common.util")
-- local log = require("scada-common.log")
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local core = require("graphics.core")
local style = require("pocket.ui.style")
local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local boiler = require("pocket.ui.pages.unit_boiler")
local reactor = require("pocket.ui.pages.unit_reactor")
local turbine = require("pocket.ui.pages.unit_turbine")
local DataIndicator = require("graphics.elements.indicators.data")
local IconIndicator = require("graphics.elements.indicators.icon")
-- local RadIndicator = require("graphics.elements.indicators.rad")
-- local VerticalBar = require("graphics.elements.indicators.vbar")
local core = require("graphics.core")
local PushButton = require("graphics.elements.controls.push_button")
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 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 = "+" }
}
local APP_ID = pocket.APP_ID
local 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" }
}
-- 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 = "-" },
@@ -43,60 +46,58 @@ local emc_ind_s = {
{ color = cpair(colors.black, colors.green), symbol = "+" }
}
local red_ind_s = {
{ color = cpair(colors.black, colors.lightGray), symbol = "+" },
{ color = cpair(colors.black, colors.red), symbol = "-" }
}
local yel_ind_s = {
{ color = cpair(colors.black, colors.lightGray), symbol = "+" },
{ color = cpair(colors.black, colors.yellow), symbol = "-" }
}
-- new unit page view
---@param root graphics_element parent
local function new_view(root)
local db = iocontrol.get_db()
local main = Div{parent=root,x=1,y=1}
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(iocontrol.APP_ID.UNITS, main)
local app = db.nav.register_app(APP_ID.UNITS, frame, nil, false, true)
TextBox{parent=main,y=2,text="Units App",height=1,alignment=ALIGN.CENTER}
local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1}
TextBox{parent=main,y=4,text="Loading...",height=1,alignment=ALIGN.CENTER}
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 label = cpair(colors.lightGray, 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 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(iocontrol.APP_ID.ROOT) end },
{ 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 = function () end },
{ 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.lightBlue), callback = function () end })
-- end
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.white), callback = function () end })
-- 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()
local page_div = Div{parent=main,x=2,y=2,width=main.get_width()-2}
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {}
@@ -124,7 +125,8 @@ local function new_view(root)
end
for i = 1, db.facility.num_units do
local u_div = panes[i] ---@type graphics_element
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
@@ -142,23 +144,20 @@ local function new_view(root)
local u_page = app.new_page(nil, i)
u_page.tasks = { update }
TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,height=1,alignment=ALIGN.CENTER}
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,height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.black)}
TextBox{parent=u_div,y=3,text=type,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.black)}
local lu_col = cpair(colors.lightGray, colors.lightGray)
local text_fg = cpair(colors.white, colors._INHERIT)
local rate = DataIndicator{parent=u_div,y=5,lu_colors=lu_col,label="Rate",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="K",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
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", temp.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()
@@ -186,6 +185,8 @@ local function new_view(root)
--#endregion
util.nop()
--#region Alarms Tab
local alm_div = Div{parent=page_div}
@@ -193,26 +194,52 @@ local function new_view(root)
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="Unit Alarms",height=1,alignment=ALIGN.CENTER}
TextBox{parent=alm_div,y=1,text="Status Info Display",alignment=ALIGN.CENTER}
TextBox{parent=alm_div,y=3,text="work in progress",height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.black)}
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_div = Div{parent=page_div}
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",height=1,alignment=ALIGN.CENTER}
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)
@@ -246,17 +273,24 @@ local function new_view(root)
--#endregion
--#region Reactor Tab
nav_links[i].reactor = reactor(app, u_page, panes, page_div, u_ps, update)
--#endregion
--#region RCS Tab
local rcs_div = Div{parent=page_div}
table.insert(panes, rcs_div)
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",height=1,alignment=ALIGN.CENTER}
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}
@@ -275,7 +309,7 @@ local function new_view(root)
c_mwrf.register(u_ps, "MaxWaterReturnFeed", c_mwrf.update)
-- rcs_div.line_break()
-- TextBox{parent=rcs_div,text="Mismatches",height=1,alignment=ALIGN.CENTER,fg_bg=label}
-- 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}
@@ -285,7 +319,7 @@ local function new_view(root)
c_sfm.register(u_ps, "SteamFeedMismatch", c_sfm.update)
rcs_div.line_break()
-- TextBox{parent=rcs_div,text="Aggregate Checks",height=1,alignment=ALIGN.CENTER,fg_bg=label}
-- 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}
@@ -304,6 +338,32 @@ local function new_view(root)
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
@@ -311,9 +371,27 @@ local function new_view(root)
app.set_root_pane(u_pane)
set_sidebar(active_unit)
-- done, show the app
load_pane.set_value(2)
end
app.set_on_load(load)
-- 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

View File

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

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,92 +2,84 @@
-- Pocket GUI Root
--
local iocontrol = require("pocket.iocontrol")
local util = require("scada-common.util")
local diag_apps = require("pocket.ui.apps.diag_apps")
local dummy_app = require("pocket.ui.apps.dummy_app")
local sys_apps = require("pocket.ui.apps.sys_apps")
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 home_page = require("pocket.ui.pages.home_page")
local unit_page = require("pocket.ui.pages.unit_page")
local home_page = require("pocket.ui.pages.home_page")
local style = require("pocket.ui.style")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local PushButton = require("graphics.elements.controls.push_button")
local Sidebar = require("graphics.elements.controls.sidebar")
local WaitingAnim = require("graphics.elements.animations.waiting")
local SignalBar = require("graphics.elements.indicators.signal")
local PushButton = require("graphics.elements.controls.push_button")
local Sidebar = require("graphics.elements.controls.sidebar")
local LINK_STATE = iocontrol.LINK_STATE
local SignalBar = require("graphics.elements.indicators.signal")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
-- create new main view
---@param main graphics_element main displaybox
local function init(main)
local db = iocontrol.get_db()
-- window header message
TextBox{parent=main,y=1,text="WIP ALPHA APP S C ",alignment=ALIGN.LEFT,height=1,fg_bg=style.header}
-- window header message and connection status
TextBox{parent=main,y=1,text="EARLY ACCESS ALPHA S C ",fg_bg=style.header}
local svr_conn = SignalBar{parent=main,y=1,x=22,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
local crd_conn = SignalBar{parent=main,y=1,x=26,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
db.ps.subscribe("svr_conn_quality", svr_conn.set_value)
db.ps.subscribe("crd_conn_quality", crd_conn.set_value)
--#region root panel panes (connection screens + main screen)
local root_pane_div = Div{parent=main,x=1,y=2}
local conn_sv_wait = conn_waiting(root_pane_div, 6, false)
local conn_api_wait = conn_waiting(root_pane_div, 6, true)
local start_pane = Div{parent=main,x=1,y=2}
local main_pane = Div{parent=main,x=1,y=2}
local root_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={conn_sv_wait,conn_api_wait,main_pane}}
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(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)
end
end)
--#endregion
--#region main page panel panes & sidebar
local root_pane = MultiPane{parent=main,x=1,y=2,panes={start_pane,main_pane}}
local page_div = Div{parent=main_pane,x=4,y=1}
-- create all the apps & pages
home_page(page_div)
unit_page(page_div)
diag_apps(page_div)
unit_app(page_div)
guide_app(page_div)
loader_app(page_div)
sys_apps(page_div)
diag_apps(page_div)
dummy_app(page_div)
assert(#db.nav.get_containers() == iocontrol.APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered")
-- 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")
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)})
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}
db.nav.open_app(iocontrol.APP_ID.ROOT)
db.nav.open_app(APP_ID.ROOT)
--#endregion
-- done with initial render, lets go!
root_pane.set_value(2)
end
return init

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

@@ -3,6 +3,7 @@
--
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local core = require("graphics.core")
@@ -12,11 +13,10 @@ local TextBox = require("graphics.elements.textbox")
local App = require("graphics.elements.controls.app")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = iocontrol.APP_ID
local ALIGN = core.ALIGN
local APP_ID = pocket.APP_ID
-- new home page view
---@param root graphics_element parent
@@ -25,7 +25,7 @@ local function new_view(root)
local main = Div{parent=root,x=1,y=1,height=19}
local app = db.nav.register_app(iocontrol.APP_ID.ROOT, main)
local app = db.nav.register_app(APP_ID.ROOT, main)
local apps_1 = Div{parent=main,x=1,y=1,height=15}
local apps_2 = Div{parent=main,x=1,y=1,height=15}
@@ -51,10 +51,10 @@ local function new_view(root)
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.DUMMY)end,app_fg_bg=cpair(colors.black,colors.cyan),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,height=1,alignment=ALIGN.CENTER}
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}

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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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,38 +2,30 @@
-- Configuration GUI
--
local log = require("scada-common.log")
local rsio = require("scada-common.rsio")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local log = require("scada-common.log")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core")
local themes = require("graphics.themes")
local check = require("reactor-plc.config.check")
local system = require("reactor-plc.config.system")
local DisplayBox = require("graphics.elements.displaybox")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local core = require("graphics.core")
local themes = require("graphics.themes")
local CheckBox = require("graphics.elements.controls.checkbox")
local PushButton = require("graphics.elements.controls.push_button")
local Radio2D = require("graphics.elements.controls.radio_2d")
local RadioButton = require("graphics.elements.controls.radio_button")
local DisplayBox = require("graphics.elements.displaybox")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local NumberField = require("graphics.elements.form.number_field")
local TextField = require("graphics.elements.form.text_field")
local IndLight = require("graphics.elements.indicators.light")
local PushButton = require("graphics.elements.controls.push_button")
local println = util.println
local tri = util.trinary
local cpair = core.cpair
local LEFT = core.ALIGN.LEFT
local CENTER = core.ALIGN.CENTER
local RIGHT = core.ALIGN.RIGHT
-- changes to the config data/format to let the user know
local changes = {
@@ -48,22 +40,22 @@ local configurator = {}
local style = {}
style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray)
style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray)
style.colors = themes.smooth_stone.colors
style.colors = themes.smooth_stone.colors
local bw_fg_bg = cpair(colors.black, colors.white)
local g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
local nav_fg_bg = bw_fg_bg
local btn_act_fg_bg = cpair(colors.white, colors.gray)
style.bw_fg_bg = cpair(colors.black, colors.white)
style.g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
style.nav_fg_bg = style.bw_fg_bg
style.btn_act_fg_bg = cpair(colors.white, colors.gray)
style.btn_dis_fg_bg = cpair(colors.lightGray, colors.white)
---@class _plc_cfg_tool_ctl
local tool_ctl = {
ask_config = false,
has_config = false,
viewing_config = false,
importing_legacy = false,
jumped_to_color = false,
view_cfg = nil, ---@type graphics_element
@@ -72,16 +64,8 @@ local tool_ctl = {
color_apply = nil, ---@type graphics_element
settings_apply = nil, ---@type graphics_element
set_networked = nil, ---@type function
bundled_emcool = nil, ---@type function
gen_summary = nil, ---@type function
show_current_cfg = nil, ---@type function
load_legacy = nil, ---@type function
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type graphics_element
auth_key_textbox = nil, ---@type graphics_element
auth_key_value = ""
}
---@class plc_config
@@ -127,27 +111,6 @@ local fields = {
{ "ColorMode", "Color Mode", themes.COLOR_MODE.STANDARD }
}
local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" }
local side_options_map = { "top", "bottom", "left", "right", "front", "back" }
local color_options = { "Red", "Orange", "Yellow", "Lime", "Green", "Cyan", "Light Blue", "Blue", "Purple", "Magenta", "Pink", "White", "Light Gray", "Gray", "Black", "Brown" }
local color_options_map = { colors.red, colors.orange, colors.yellow, colors.lime, colors.green, colors.cyan, colors.lightBlue, colors.blue, colors.purple, colors.magenta, colors.pink, colors.white, colors.lightGray, colors.gray, colors.black, colors.brown }
-- convert text representation to index
---@param side string
local function side_to_idx(side)
for k, v in ipairs(side_options_map) do
if v == side then return k end
end
end
-- convert color to index
---@param color color
local function color_to_idx(color)
for k, v in ipairs(color_options_map) do
if v == color then return k end
end
end
-- load data from the settings file
---@param target plc_config
---@param raw boolean? true to not use default values
@@ -164,11 +127,16 @@ end
-- create the config view
---@param display graphics_element
local function config_view(display)
local bw_fg_bg = style.bw_fg_bg
local g_lg_fg_bg = style.g_lg_fg_bg
local nav_fg_bg = style.nav_fg_bg
local btn_act_fg_bg = style.btn_act_fg_bg
local btn_dis_fg_bg = style.btn_dis_fg_bg
---@diagnostic disable-next-line: undefined-field
local function exit() os.queueEvent("terminate") end
TextBox{parent=display,y=1,text="Reactor PLC Configurator",alignment=CENTER,height=1,fg_bg=style.header}
TextBox{parent=display,y=1,text="Reactor PLC Configurator",alignment=CENTER,fg_bg=style.header}
local root_pane_div = Div{parent=display,x=1,y=2}
@@ -179,17 +147,18 @@ local function config_view(display)
local clr_cfg = Div{parent=root_pane_div,x=1,y=1}
local summary = Div{parent=root_pane_div,x=1,y=1}
local changelog = Div{parent=root_pane_div,x=1,y=1}
local check_sys = Div{parent=root_pane_div,x=1,y=1}
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,plc_cfg,net_cfg,log_cfg,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
TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the Reactor PLC configurator! Please select one of the following options."}
if tool_ctl.ask_config then
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device has no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device had no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
y_start = y_start + 5
end
@@ -206,7 +175,7 @@ local function config_view(display)
end
PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure System",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
local function jump_color()
tool_ctl.jumped_to_color = true
@@ -216,7 +185,8 @@ local function config_view(display)
end
PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
PushButton{parent=main_page,x=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}
if not tool_ctl.has_config then
@@ -224,554 +194,43 @@ local function config_view(display)
tool_ctl.color_cfg.disable()
end
--#region PLC
--#endregion
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}
--#region System Configuration
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)}
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}
system.create(tool_ctl, main_pane, settings, divs, style, exit)
--#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,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="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,height=1,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,height=1,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
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
--#region Config Change Log
local cl = Div{parent=changelog,x=2,y=4,width=49}
TextBox{parent=changelog,x=1,y=2,height=1,text=" Config Change Log",fg_bg=bw_fg_bg}
TextBox{parent=changelog,x=1,y=2,text=" Config Change Log",fg_bg=bw_fg_bg}
local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
for _, change in ipairs(changes) do
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg}
TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
for _, v in ipairs(change[2]) do
local e = Div{parent=c_log,height=#util.strwrap(v,46)}
TextBox{parent=e,y=1,x=1,text="- ",height=1,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=e,y=1,x=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
end
end
PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
-- set tool functions now that we have the elements
--#endregion
function tool_ctl.set_networked(enable)
tmp_cfg.Networked = enable
if enable then u_id.set_max(4) else u_id.set_max(999) end
end
--#region Self-Check
function tool_ctl.bundled_emcool(en) if en then color.enable() else color.disable() end end
check.create(main_pane, settings_cfg, check_sys, style)
-- load a legacy config file
function tool_ctl.load_legacy()
local config = require("reactor-plc.config")
tmp_cfg.Networked = config.NETWORKED
tmp_cfg.UnitID = config.REACTOR_ID
tmp_cfg.EmerCoolEnable = type(config.EMERGENCY_COOL) == "table"
if tmp_cfg.EmerCoolEnable then
tmp_cfg.EmerCoolSide = config.EMERGENCY_COOL.side
tmp_cfg.EmerCoolColor = config.EMERGENCY_COOL.color
else
tmp_cfg.EmerCoolSide = nil
tmp_cfg.EmerCoolColor = nil
end
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
tmp_cfg.PLC_Channel = config.PLC_CHANNEL
tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT
tmp_cfg.TrustedRange = config.TRUSTED_RANGE
tmp_cfg.AuthKey = config.AUTH_KEY or ""
tmp_cfg.LogMode = config.LOG_MODE
tmp_cfg.LogPath = config.LOG_PATH
tmp_cfg.LogDebug = config.LOG_DEBUG or false
tool_ctl.gen_summary(tmp_cfg)
sum_pane.set_value(1)
main_pane.set_value(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
if cfg.AuthKey then tool_ctl.show_key_btn.enable() else tool_ctl.show_key_btn.disable() end
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" 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 tool_ctl.auth_key_textbox = textbox end
end
end
--#endregion
end
-- reset terminal screen
@@ -802,7 +261,7 @@ function configurator.configure(ask_config)
config_view(display)
while true do
local event, param1, param2, param3 = util.pull_event()
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event
if event == "timer" then
@@ -815,6 +274,8 @@ function configurator.configure(ask_config)
if k_e then display.handle_key(k_e) end
elseif event == "paste" then
display.handle_paste(param1)
elseif event == "modem_message" then
check.receive_sv(param1, param2, param3, param4, param5)
end
if event == "terminate" then return end

View File

@@ -40,7 +40,7 @@ local function init(panel)
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)
--
@@ -115,7 +115,7 @@ local function init(panel)
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=5,width=6,height=1,text=comp_id,fg_bg=disabled_fg}
TextBox{parent=system,x=9,y=5,width=6,text=comp_id,fg_bg=disabled_fg}
--
-- 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 fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)

View File

@@ -57,41 +57,47 @@ function plc.load_config()
config.FrontPanelTheme = settings.get("FrontPanelTheme")
config.ColorMode = settings.get("ColorMode")
return plc.validate_config(config)
end
-- validate a PLC configuration
---@param cfg plc_config
function plc.validate_config(cfg)
local cfv = util.new_validator()
cfv.assert_type_bool(config.Networked)
cfv.assert_type_int(config.UnitID)
cfv.assert_type_bool(config.EmerCoolEnable)
cfv.assert_type_bool(cfg.Networked)
cfv.assert_type_int(cfg.UnitID)
cfv.assert_type_bool(cfg.EmerCoolEnable)
if config.Networked == true then
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.PLC_Channel)
cfv.assert_type_num(config.ConnTimeout)
cfv.assert_min(config.ConnTimeout, 2)
cfv.assert_type_num(config.TrustedRange)
cfv.assert_min(config.TrustedRange, 0)
cfv.assert_type_str(config.AuthKey)
if cfg.Networked == true then
cfv.assert_channel(cfg.SVR_Channel)
cfv.assert_channel(cfg.PLC_Channel)
cfv.assert_type_num(cfg.ConnTimeout)
cfv.assert_min(cfg.ConnTimeout, 2)
cfv.assert_type_num(cfg.TrustedRange)
cfv.assert_min(cfg.TrustedRange, 0)
cfv.assert_type_str(cfg.AuthKey)
if type(config.AuthKey) == "string" then
local len = string.len(config.AuthKey)
if type(cfg.AuthKey) == "string" then
local len = string.len(cfg.AuthKey)
cfv.assert(len == 0 or len >= 8)
end
end
cfv.assert_type_int(config.LogMode)
cfv.assert_range(config.LogMode, 0, 1)
cfv.assert_type_str(config.LogPath)
cfv.assert_type_bool(config.LogDebug)
cfv.assert_type_int(cfg.LogMode)
cfv.assert_range(cfg.LogMode, 0, 1)
cfv.assert_type_str(cfg.LogPath)
cfv.assert_type_bool(cfg.LogDebug)
cfv.assert_type_int(config.FrontPanelTheme)
cfv.assert_range(config.FrontPanelTheme, 1, 2)
cfv.assert_type_int(config.ColorMode)
cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
cfv.assert_type_int(cfg.FrontPanelTheme)
cfv.assert_range(cfg.FrontPanelTheme, 1, 2)
cfv.assert_type_int(cfg.ColorMode)
cfv.assert_range(cfg.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
-- check emergency coolant configuration if enabled
if config.EmerCoolEnable then
cfv.assert_eq(rsio.is_valid_side(config.EmerCoolSide), true)
cfv.assert_eq(config.EmerCoolColor == nil or rsio.is_color(config.EmerCoolColor), true)
if cfg.EmerCoolEnable then
cfv.assert_eq(rsio.is_valid_side(cfg.EmerCoolSide), true)
cfv.assert_eq(cfg.EmerCoolColor == nil or rsio.is_color(cfg.EmerCoolColor), true)
end
return cfv.valid()
@@ -524,8 +530,8 @@ end
function plc.comms(version, nic, reactor, rps, conn_watchdog)
local self = {
sv_addr = comms.BROADCAST,
seq_num = 0,
r_seq_num = nil,
seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, ---@type nil|integer
scrammed = false,
linked = false,
last_est_ack = ESTABLISH_ACK.ALLOW,
@@ -571,33 +577,17 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
self.seq_num = self.seq_num + 1
end
-- variable reactor status information, excluding heating rate
-- dynamic reactor status information, excluding heating rate
---@return table data_table, boolean faulted
local function _reactor_status()
local function _get_reactor_status()
local fuel = nil
local waste = nil
local coolant = nil
local hcoolant = nil
local data_table = {
false, -- getStatus
0, -- getBurnRate
0, -- getActualBurnRate
0, -- getTemperature
0, -- getDamagePercent
0, -- getBoilEfficiency
0, -- getEnvironmentalLoss
0, -- fuel_amnt
0, -- getFuelFilledPercentage
0, -- waste_amnt
0, -- getWasteFilledPercentage
"", -- coolant_name
0, -- coolant_amnt
0, -- getCoolantFilledPercentage
"", -- hcoolant_name
0, -- hcoolant_amnt
0 -- getHeatedCoolantFilledPercentage
}
local data_table = {}
reactor.__p_disable_afc()
local tasks = {
function () data_table[1] = reactor.getStatus() end,
@@ -637,30 +627,32 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
data_table[16] = hcoolant.amount
end
reactor.__p_enable_afc()
return data_table, reactor.__p_is_faulted()
end
-- update the status cache if changed
---@return boolean changed
local function _update_status_cache()
local status, faulted = _reactor_status()
local status, faulted = _get_reactor_status()
local changed = false
if self.status_cache ~= nil then
if not faulted then
if not faulted then
if self.status_cache ~= nil then
for i = 1, #status do
if status[i] ~= self.status_cache[i] then
changed = true
break
end
end
else
changed = true
end
else
changed = true
end
if changed and not faulted then
self.status_cache = status
if changed then
self.status_cache = status
end
end
return changed
@@ -679,9 +671,11 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
_send(msg_type, { status })
end
-- send structure properties (these should not change, server will cache these)
-- send static structure properties, cached by server
local function _send_struct()
local mek_data = { false, 0, 0, 0, types.new_zero_coordinate(), types.new_zero_coordinate(), 0, 0, 0, 0, 0, 0, 0, 0 }
local mek_data = {}
reactor.__p_disable_afc()
local tasks = {
function () mek_data[1] = reactor.getLength() end,
@@ -705,6 +699,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
_send(RPLC_TYPE.MEK_STRUCT, mek_data)
self.resend_build = false
end
reactor.__p_enable_afc()
end
-- PUBLIC FUNCTIONS --
@@ -835,16 +831,16 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
if l_chan == config.PLC_Channel then
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num()
elseif self.linked and ((self.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
self.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
elseif self.linked and (src_addr ~= self.sv_addr) then
log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr ..
"); channel in use by another system?")
return
else
self.r_seq_num = packet.scada_frame.seq_num()
self.r_seq_num = packet.scada_frame.seq_num() + 1
end
-- feed the watchdog first so it doesn't uhh...eat our packets :)
@@ -1030,10 +1026,9 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
println_ts("linked!")
log.info("supervisor establish request approved, linked to SV (CID#" .. src_addr .. ")")
-- link + reset remote sequence number and cache
-- link + reset cache
self.sv_addr = src_addr
self.linked = true
self.r_seq_num = nil
self.status_cache = nil
if plc_state.reactor_formed then _send_struct() end

View File

@@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "v1.7.11"
local R_PLC_VERSION = "v1.8.6"
local println = util.println
local println_ts = util.println_ts

View File

@@ -84,7 +84,8 @@ assert(#PORT_DSGN == rsio.NUM_PORTS)
local changes = {
{ "v1.7.9", { "ConnTimeout can now have a fractional part" } },
{ "v1.7.15", { "Added front panel UI theme", "Added color accessibility modes" } },
{ "v1.9.2", { "Added standard with black off state color mode", "Added blue indicator color modes" } }
{ "v1.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
@@ -158,7 +159,6 @@ local tool_ctl = {
p_idx = nil, ---@type graphics_element
p_unit = 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_ext = nil, ---@type graphics_element
p_err = nil, ---@type graphics_element
@@ -264,7 +264,7 @@ local function config_view(display)
---@diagnostic disable-next-line: undefined-field
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}
@@ -285,7 +285,7 @@ local function config_view(display)
local y_start = 2
if tool_ctl.ask_config then
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device 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
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."}
@@ -344,7 +344,7 @@ local function config_view(display)
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=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."}
@@ -353,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}
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 vol = tonumber(s_vol.get_value())
@@ -377,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}}
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=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}
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}
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 svr_c = tonumber(svr_chan.get_value())
@@ -411,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=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}
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"}
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,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 timeout_val = tonumber(timeout.get_value())
@@ -445,7 +445,7 @@ 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=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 function censor_key(enable) censor(tri(enable, "*", nil)) end
@@ -455,7 +455,7 @@ local function config_view(display)
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 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()
@@ -475,20 +475,20 @@ local function config_view(display)
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}
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 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 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
@@ -516,17 +516,17 @@ 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}}
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=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}
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,height=1,text="Preview"}
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)}
@@ -553,7 +553,7 @@ local function config_view(display)
end
end
TextBox{parent=clr_c_2,x=1,y=7,height=1,width=10,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=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}
@@ -603,7 +603,7 @@ local function config_view(display)
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=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}
@@ -625,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}}
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)}
@@ -650,8 +650,11 @@ local function config_view(display)
---@param exclude_conns boolean? true to exclude saving peripheral/redstone connections
local function save_and_continue(exclude_conns)
for k, v in pairs(tmp_cfg) do
if not (exclude_conns and (k == "Peripherals" or k == "Redstone")) then settings.set(k, v) end
for _, field in ipairs(fields) do
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
-- always set these if missing
@@ -698,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.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)}
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}
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)}
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}
@@ -720,7 +723,7 @@ local function config_view(display)
show_rs_conns()
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."}
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}
@@ -752,15 +755,15 @@ local function config_view(display)
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)}
for _, change in ipairs(changes) do
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg}
TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
for _, v in ipairs(change[2]) do
local e = Div{parent=c_log,height=#util.strwrap(v,46)}
TextBox{parent=e,y=1,x=1,text="- ",height=1,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=e,y=1,x=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
end
end
@@ -781,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}}
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)}
@@ -807,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=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)}
@@ -828,53 +831,35 @@ local function config_view(display)
tool_ctl.p_name_msg.set_value("Configuring peripheral on '" .. name .. "':")
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
tool_ctl.p_prompt.set_value("This is the # boiler for reactor unit # .")
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()
reposition("This is reactor unit # 's # boiler.", 31, 2, 23, 4, 7)
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.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.")
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).")
elseif type == "turbineValve" then
tool_ctl.p_prompt.set_value("This is the # turbine for reactor unit # .")
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()
reposition("This is reactor unit # 's # turbine.", 31, 3, 23, 4, 7)
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.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.")
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).")
elseif type == "solarNeutronActivator" then
reposition("This SNA is for reactor unit # .", 46, 1, 31, 4, 7)
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_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.")
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.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
tool_ctl.p_idx.enable()
@@ -885,22 +870,12 @@ local function config_view(display)
tool_ctl.p_unit.enable()
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.")
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.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
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.")
elseif type == "inductionPort" or type == "spsPort" then
local dev = tri(type == "inductionPort", "induction matrix", "SPS")
@@ -908,7 +883,6 @@ local function config_view(display)
tool_ctl.p_unit.hide(true)
tool_ctl.p_prompt.set_value("This is the " .. dev .. " for the facility.")
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.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
@@ -934,8 +908,8 @@ local function config_view(display)
---@cast entry ppm_entry
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)}
TextBox{parent=line,x=11,y=1,height=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=1,text=name,fg_bg=cpair(colors.black,bkg)}
TextBox{parent=line,x=11,y=2,text=entry.type,fg_bg=cpair(colors.gray,bkg)}
alternate = not alternate
end
@@ -948,7 +922,7 @@ local function config_view(display)
TextBox{parent=peri_c_3,x=1,y=6,height=4,text="Peripheral Name"}
local p_name = TextField{parent=peri_c_3,x=1,y=7,width=49,height=1,max_len=128,fg_bg=bw_fg_bg}
local p_type = Radio2D{parent=peri_c_3,x=1,y=9,rows=4,columns=2,default=1,options=RTU_DEV_TYPES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple}
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)
local function submit_manual_peri()
@@ -965,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_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_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_end = TextBox{parent=peri_c_4,x=22,y=6,height=6,width=1,text=")"}
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","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_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()
function tool_ctl.p_assign(opt)
@@ -988,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_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)
local function back_from_peri_opts()
@@ -1073,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=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=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}
@@ -1095,9 +1068,9 @@ local function config_view(display)
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 function rs_revert()
@@ -1125,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."}
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)}
@@ -1186,8 +1159,8 @@ local function config_view(display)
-- add entries to redstone option list
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)}
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=22,y=1,height=1,text="Create all 4 waste entries",fg_bg=cpair(colors.gray,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,text="Create all 4 waste entries",fg_bg=cpair(colors.gray,colors.white)}
for i = 1, rsio.NUM_PORTS do
local p = PORT_DESC_MAP[i][1]
@@ -1197,8 +1170,8 @@ local function config_view(display)
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(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=22,y=1,height=1,text=PORT_DESC_MAP[i][2],fg_bg=cpair(colors.gray,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,text=PORT_DESC_MAP[i][2],fg_bg=cpair(colors.gray,colors.white)}
end
PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
@@ -1212,10 +1185,10 @@ local function config_view(display)
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,height=1,text="Output Side"}
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,height=1,text="Unit ID"}
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)
@@ -1229,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.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)
local function back_from_rs_opts()
@@ -1285,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=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=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}
@@ -1386,9 +1359,9 @@ local function config_view(display)
end
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=2,height=1,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=1,text="@ "..def.name,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=1,y=2,text=status,fg_bg=cpair(color,colors.white)}
TextBox{parent=line,x=1,y=3,text=desc,fg_bg=cpair(colors.gray,colors.white)}
end
rs_import_list.remove_all()
@@ -1407,10 +1380,10 @@ local function config_view(display)
if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end
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=2,y=1,width=14,height=1,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=40,y=1,height=1,text=unit,fg_bg=cpair(colors.gray,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,text=name}
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,text=unit,fg_bg=cpair(colors.gray,colors.white)}
end
end
@@ -1545,9 +1518,9 @@ local function config_view(display)
end
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=2,height=1,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=1,text="@ "..def.name,fg_bg=cpair(colors.black,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,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)}
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}
@@ -1613,10 +1586,10 @@ local function config_view(display)
if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end
local entry = Div{parent=rs_list,height=1}
TextBox{parent=entry,x=1,y=1,width=1,height=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=16,y=1,width=string.len(conn),height=1,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=1,y=1,width=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=entry,x=2,y=1,width=14,text=name}
TextBox{parent=entry,x=16,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=entry,x=33,y=1,width=1,text=unit,fg_bg=cpair(colors.gray,colors.white)}
PushButton{parent=entry,x=35,y=1,min_width=6,height=1,text="EDIT",callback=function()edit_rs_entry(i)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
PushButton{parent=entry,x=41,y=1,min_width=8,height=1,text="DELETE",callback=function()delete_rs_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
end

View File

@@ -35,7 +35,7 @@ local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC
local function init(panel, units)
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
@@ -98,9 +98,9 @@ local function init(panel, units)
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=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}
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 fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@@ -126,7 +126,7 @@ local function init(panel, units)
-- show routine statuses
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}
rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update)
end
@@ -144,13 +144,13 @@ local function init(panel, units)
-- unit name identifier (type + index)
local function get_name(t) return util.c(UNIT_TYPE_LABELS[t + 1], " ", util.trinary(util.is_int(unit.index), unit.index, "")) end
local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(unit.type),width=15,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)
-- assignment (unit # or facility)
local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor)
TextBox{parent=unit_hw_statuses,y=i,x=19,text=for_unit,height=1,fg_bg=disabled_fg}
TextBox{parent=unit_hw_statuses,y=i,x=19,text=for_unit,fg_bg=disabled_fg}
end
end

View File

@@ -284,8 +284,8 @@ end
function rtu.comms(version, nic, conn_watchdog)
local self = {
sv_addr = comms.BROADCAST,
seq_num = 0,
r_seq_num = nil,
seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
r_seq_num = nil, ---@type nil|integer
txn_id = 0,
last_est_ack = ESTABLISH_ACK.ALLOW
}
@@ -442,16 +442,16 @@ function rtu.comms(version, nic, conn_watchdog)
if l_chan == config.RTU_Channel then
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num()
elseif rtu_state.linked and ((self.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
self.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
elseif 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 ..
"); channel in use by another system?")
return
else
self.r_seq_num = packet.scada_frame.seq_num()
self.r_seq_num = packet.scada_frame.seq_num() + 1
end
-- feed watchdog on valid sequence number
@@ -556,7 +556,6 @@ function rtu.comms(version, nic, conn_watchdog)
-- establish allowed
rtu_state.linked = true
self.sv_addr = packet.scada_frame.src_addr()
self.r_seq_num = nil
println_ts("supervisor connection established")
log.info("supervisor connection established")
else
@@ -572,7 +571,9 @@ function rtu.comms(version, nic, conn_watchdog)
end
end
public.unlink(rtu_state)
-- unlink
self.sv_addr = comms.BROADCAST
rtu_state.linked = false
end
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 turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "v1.9.6"
local RTU_VERSION = "v1.10.6"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
@@ -93,14 +93,6 @@ local function main()
network.init_mac(config.AuthKey)
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
audio.generate_tones()
@@ -116,14 +108,15 @@ local function main()
-- RTU gateway devices (not RTU units)
rtu_dev = {
modem = ppm.get_wireless_modem(),
sounders = {}
},
-- system objects
rtu_sys = {
nic = network.nic(modem),
rtu_comms = nil, ---@type rtu_comms
conn_watchdog = nil, ---@type watchdog
nic = nil, ---@type nic
rtu_comms = nil, ---@type rtu_comms
conn_watchdog = nil, ---@type watchdog
units = {}
},
@@ -134,8 +127,9 @@ local function main()
}
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
@@ -501,8 +495,6 @@ local function main()
-- start system
----------------------------------------
local rtu_state = __shared_memory.rtu_state
log.debug("boot> running sys_config()")
if sys_config() then
@@ -517,23 +509,33 @@ local function main()
log.info("startup> running in headless mode without front panel")
end
-- check modem
if smem_dev.modem == nil then
println("startup> wireless modem not found")
log.fatal("no wireless modem on startup")
return
end
databus.tx_hw_modem(true)
-- find and setup all speakers
local speakers = ppm.get_all_devices("speaker")
for _, s in pairs(speakers) do
local sounder = rtu.init_sounder(s)
table.insert(__shared_memory.rtu_dev.sounders, sounder)
table.insert(smem_dev.sounders, sounder)
log.debug(util.c("startup> added speaker, attached as ", sounder.name))
end
databus.tx_hw_spkr_count(#__shared_memory.rtu_dev.sounders)
databus.tx_hw_spkr_count(#smem_dev.sounders)
-- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("startup> conn watchdog started")
-- setup comms
smem_sys.nic = network.nic(smem_dev.modem)
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
log.debug("startup> comms init")

View File

@@ -17,8 +17,8 @@ local max_distance = nil
local comms = {}
-- protocol/data versions (protocol/data independent changes tracked by util.lua version)
comms.version = "2.5.1"
comms.api_version = "0.0.2"
comms.version = "3.0.0"
comms.api_version = "0.0.3"
---@enum PROTOCOL
local PROTOCOL = {
@@ -240,6 +240,8 @@ function comms.scada_packet()
---@nodiscard
function public.modem_event() return self.modem_msg_in end
---@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
---@nodiscard
@@ -278,7 +280,7 @@ function comms.authd_packet()
src_addr = comms.BROADCAST,
dest_addr = comms.BROADCAST,
mac = "",
payload = ""
payload = {}
}
---@class authd_packet
@@ -286,14 +288,13 @@ function comms.authd_packet()
-- make an authenticated SCADA packet
---@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)
self.valid = true
self.src_addr = s_packet.src_addr()
self.dest_addr = s_packet.dest_addr()
self.payload = textutils.serialize(s_packet.raw_sendable(), { allow_repetitions = true, compact = true })
self.mac = mac(self.payload)
self.raw = { self.src_addr, self.dest_addr, self.mac, self.payload }
self.mac = mac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true }))
self.raw = { self.src_addr, self.dest_addr, self.mac, s_packet.raw_sendable() }
end
-- parse in a modem message as an authenticated SCADA packet
@@ -330,14 +331,14 @@ function comms.authd_packet()
self.src_addr = nil
self.dest_addr = nil
self.mac = ""
self.payload = ""
self.payload = {}
end
-- check if this packet is destined for this device
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
type(self.mac) == "string" and type(self.payload) == "string"
type(self.mac) == "string" and type(self.payload) == "table"
end
end

View File

@@ -52,7 +52,6 @@ local alarms = {}
-- unit alarms
alarms.HIGH_TEMP = 1150 -- temp >= 1150K
alarms.HIGH_WASTE = 0.85 -- fill > 85%
alarms.HIGH_RADIATION = 0.00005 -- 50 uSv/h, not yet damaging but this isn't good

View File

@@ -80,7 +80,7 @@ end
---@param modem table modem to use
function network.nic(modem)
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 = {}
}
@@ -114,7 +114,7 @@ function network.nic(modem)
modem.open(channel)
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
if key ~= "transmit" and key ~= "open" and key ~= "close" and key ~= "closeAll" then public[key] = func end
end
@@ -175,7 +175,7 @@ function network.nic(modem)
---@param packet scada_packet packet
function public.transmit(dest_channel, local_channel, packet)
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
-- local start = util.time_ms()
@@ -184,7 +184,7 @@ function network.nic(modem)
---@cast tx_packet authd_packet
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
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)
if a_packet.is_valid() then
-- local start = util.time_ms()
local packet_hmac = a_packet.mac()
local msg = a_packet.data()
local computed_hmac = compute_hmac(msg)
s_packet.receive(side, sender, reply_to, a_packet.data(), distance)
if packet_hmac == computed_hmac then
-- log.debug("crypto.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms")
s_packet.receive(side, sender, reply_to, textutils.unserialize(msg), distance)
s_packet.stamp_authenticated()
else
-- log.debug("crypto.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
if s_packet.is_valid() then
-- local start = util.time_ms()
local computed_hmac = compute_hmac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true }))
if a_packet.mac() == computed_hmac then
-- log.debug("network.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms")
s_packet.stamp_authenticated()
else
-- log.debug("network.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
end
end
end
else

View File

@@ -5,7 +5,7 @@
---@class types
local types = {}
-- CLASSES --
--#region CLASSES
---@class tank_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 rsio table|nil
--#endregion
-- ALIASES --
---@alias color integer
-- ENUMERATION TYPES --
--#region
--#region ENUMERATION TYPES
---@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
types.PANEL_LINK_STATE = {
@@ -128,6 +170,15 @@ function types.rtu_type_to_string(utype)
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
types.TRI_FAIL = {
OK = 1,
@@ -249,8 +300,7 @@ types.ALARM_STATE_NAMES = {
--#endregion
-- STRING TYPES --
--#region
--#region STRING TYPES
---@alias side
---|"top"
@@ -364,8 +414,7 @@ types.DUMPING_MODE = {
--#endregion
-- MODBUS --
--#region
--#region MODBUS
-- MODBUS function codes
---@enum MODBUS_FCODE

View File

@@ -4,6 +4,8 @@
local cc_strings = require("cc.strings")
local const = require("scada-common.constants")
local math = math
local string = string
local table = table
@@ -22,7 +24,7 @@ local t_pack = table.pack
local util = {}
-- scada-common version
util.version = "1.3.0"
util.version = "1.4.3"
util.TICK_TIME_S = 0.05
util.TICK_TIME_MS = 50
@@ -108,12 +110,31 @@ function util.pad(str, n)
return t_concat{util.spaces(lpad), str, util.spaces(rpad)}
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
---@nodiscard
---@param str string
---@param limit integer line limit
---@param limit integer line limit, must be greater than 0
---@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
---@nodiscard
@@ -368,72 +389,86 @@ end
--#endregion
--#region MEKANISM POWER
--#region MEKANISM MATH
-- convert Joules to FE
-- convert Joules to FE (or RF)
---@nodiscard
---@param J number Joules
---@return number FE Forge Energy
function util.joules_to_fe(J) return (J * 0.4) end
---@return number FE Forge Energy or Redstone Flux
function util.joules_to_fe_rf(J) return (J * 0.4) end
-- convert FE to Joules
-- convert FE (or RF) to Joules
---@nodiscard
---@param FE number Forge Energy
---@param FE number Forge Energy or Redstone Flux
---@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
local function MFE(fe) return fe / 1000000.0 end
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)
-- format a power value into XXX.XX UNIT format<br>
-- example for FE: FE, kFE, MFE, GFE, TFE, PFE, EFE, ZFE
---@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 format? string format override
---@return string str, string? unit
function util.power_format(fe, combine_label, format)
---@return string str, string unit
function util.power_format(e, label, combine_label, format)
local unit, value
if type(format) ~= "string" then format = "%.2f" end
if fe < 1000.0 then
unit = "FE"
value = fe
elseif fe < 1000000.0 then
unit = "kFE"
value = kFE(fe)
elseif fe < 1000000000.0 then
unit = "MFE"
value = MFE(fe)
elseif fe < 1000000000000.0 then
unit = "GFE"
value = GFE(fe)
elseif fe < 1000000000000000.0 then
unit = "TFE"
value = TFE(fe)
elseif fe < 1000000000000000000.0 then
unit = "PFE"
value = PFE(fe)
elseif fe < 1000000000000000000000.0 then
unit = "EFE"
value = EFE(fe)
if e < 1000.0 then
unit = ""
value = e
elseif e < 1000000.0 then
unit = "k"
value = e / 1000.0
elseif e < 1000000000.0 then
unit = "M"
value = e / 1000000.0
elseif e < 1000000000000.0 then
unit = "G"
value = e / 1000000000.0
elseif e < 1000000000000000.0 then
unit = "T"
value = e / 1000000000000.0
elseif e < 1000000000000000000.0 then
unit = "P"
value = e / 1000000000000000.0
elseif e < 1000000000000000000000.0 then
-- if you accomplish this please touch grass
unit = "E"
value = e / 1000000000000000000.0
else
unit = "ZFE"
value = ZFE(fe)
-- how & why did you do this?
unit = "Z"
value = e / 1000000000000000000000.0
end
unit = unit .. label
if combine_label then
return util.sprintf(util.c(format, " %s"), value, unit)
return util.sprintf(util.c(format, " %s"), value, unit), unit
else
return util.sprintf(format, value), unit
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
--#region UTILITY CLASSES

View File

@@ -1,35 +1,31 @@
local util = require("scada-common.util")
local BOOTLOADER_VERSION = "1.1"
local println = util.println
local BOOTLOADER_VERSION = "1.0"
println("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION)
println("BOOT> SCANNING FOR APPLICATIONS...")
print("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION)
print("BOOT> SCANNING FOR APPLICATIONS...")
local exit_code
if fs.exists("reactor-plc/startup.lua") then
println("BOOT> EXEC REACTOR PLC STARTUP")
print("BOOT> EXEC REACTOR PLC STARTUP")
exit_code = shell.execute("reactor-plc/startup")
elseif fs.exists("rtu/startup.lua") then
println("BOOT> EXEC RTU STARTUP")
print("BOOT> EXEC RTU STARTUP")
exit_code = shell.execute("rtu/startup")
elseif fs.exists("supervisor/startup.lua") then
println("BOOT> EXEC SUPERVISOR STARTUP")
print("BOOT> EXEC SUPERVISOR STARTUP")
exit_code = shell.execute("supervisor/startup")
elseif fs.exists("coordinator/startup.lua") then
println("BOOT> EXEC COORDINATOR STARTUP")
print("BOOT> EXEC COORDINATOR STARTUP")
exit_code = shell.execute("coordinator/startup")
elseif fs.exists("pocket/startup.lua") then
println("BOOT> EXEC POCKET STARTUP")
print("BOOT> EXEC POCKET STARTUP")
exit_code = shell.execute("pocket/startup")
else
println("BOOT> NO SCADA STARTUP FOUND")
println("BOOT> EXIT")
print("BOOT> NO SCADA STARTUP FOUND")
print("BOOT> EXIT")
return false
end
if not exit_code then println("BOOT> APPLICATION CRASHED") end
if not exit_code then print("BOOT> APPLICATION CRASHED") end
return exit_code

View File

@@ -159,7 +159,7 @@ local function config_view(display)
---@diagnostic disable-next-line: undefined-field
local function exit() os.queueEvent("terminate") end
TextBox{parent=display,y=1,text="Supervisor Configurator",alignment=CENTER,height=1,fg_bg=style.header}
TextBox{parent=display,y=1,text="Supervisor Configurator",alignment=CENTER,fg_bg=style.header}
local root_pane_div = Div{parent=display,x=1,y=2}
@@ -181,7 +181,7 @@ local function config_view(display)
TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the Supervisor configurator! Please select one of the following options."}
if tool_ctl.ask_config then
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device has no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device had no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
y_start = y_start + 5
end
@@ -228,13 +228,13 @@ local function config_view(display)
local svr_pane = MultiPane{parent=svr_cfg,x=1,y=4,panes={svr_c_1,svr_c_2,svr_c_3,svr_c_4,svr_c_5,svr_c_6,svr_c_7}}
TextBox{parent=svr_cfg,x=1,y=2,height=1,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)}
TextBox{parent=svr_cfg,x=1,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)}
TextBox{parent=svr_c_1,x=1,y=1,height=3,text="Please enter the number of reactors you have, also referred to as reactor units or 'units' for short. A maximum of 4 is currently supported."}
local num_units = NumberField{parent=svr_c_1,x=1,y=5,width=5,max_chars=2,default=ini_cfg.UnitCount,min=1,max=4,fg_bg=bw_fg_bg}
TextBox{parent=svr_c_1,x=7,y=5,height=1,text="reactors"}
TextBox{parent=svr_c_1,x=7,y=5,text="reactors"}
local nu_error = TextBox{parent=svr_c_1,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=svr_c_1,x=8,y=14,width=35,text="Please set the number of reactors.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_num_units()
local count = tonumber(num_units.get_value())
@@ -255,7 +255,7 @@ local function config_view(display)
PushButton{parent=svr_c_1,x=44,y=14,text="Next \x1a",callback=submit_num_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=svr_c_2,x=1,y=1,height=4,text="Please provide the reactor cooling configuration below. This includes the number of turbines, boilers, and if that reactor has a connection to a dynamic tank for emergency coolant."}
TextBox{parent=svr_c_2,x=1,y=6,height=1,text="UNIT TURBINES BOILERS HAS TANK CONNECTION?",fg_bg=g_lg_fg_bg}
TextBox{parent=svr_c_2,x=1,y=6,text="UNIT TURBINES BOILERS HAS TANK CONNECTION?",fg_bg=g_lg_fg_bg}
for i = 1, 4 do
local num_t, num_b, has_t = 1, 0, false
@@ -277,7 +277,7 @@ local function config_view(display)
tool_ctl.cooling_elems[i] = { line = line, turbines = turbines, boilers = boilers, tank = tank }
end
local cool_err = TextBox{parent=svr_c_2,x=8,y=14,height=1,width=33,text="Please fill out all fields.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local cool_err = TextBox{parent=svr_c_2,x=8,y=14,width=33,text="Please fill out all fields.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_cooling()
local any_missing = false
@@ -345,15 +345,15 @@ local function config_view(display)
local val = math.max(1, ini_cfg.FacilityTankDefs[i] or 2)
local div = Div{parent=svr_c_4,x=1,y=3+(2*i),height=2}
TextBox{parent=div,x=1,y=1,width=33,height=1,text="Unit "..i.." will be connected to..."}
TextBox{parent=div,x=6,y=2,width=3,height=1,text="..."}
TextBox{parent=div,x=1,y=1,width=33,text="Unit "..i.." will be connected to..."}
TextBox{parent=div,x=6,y=2,width=3,text="..."}
local tank_opt = Radio2D{parent=div,x=9,y=2,rows=1,columns=2,default=val,options={"its own Unit Tank","a Facility Tank"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.yellow,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
local no_tank = TextBox{parent=div,x=9,y=2,width=34,height=1,text="no tank (as you set two steps ago)",fg_bg=cpair(colors.gray,colors.lightGray),hidden=true}
local no_tank = TextBox{parent=div,x=9,y=2,width=34,text="no tank (as you set two steps ago)",fg_bg=cpair(colors.gray,colors.lightGray),hidden=true}
tool_ctl.tank_elems[i] = { div = div, tank_opt = tank_opt, no_tank = no_tank }
end
local tank_err = TextBox{parent=svr_c_4,x=8,y=14,height=1,width=33,text="You selected no facility tanks.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local tank_err = TextBox{parent=svr_c_4,x=8,y=14,width=33,text="You selected no facility tanks.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function hide_fconn(i)
if i > 1 then tool_ctl.vis_ftanks[i].pipe_conn.hide(true)
@@ -402,8 +402,8 @@ local function config_view(display)
PushButton{parent=svr_c_4,x=1,y=14,text="\x1b Back",callback=function()svr_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=svr_c_4,x=44,y=14,text="Next \x1a",callback=submit_tank_defs,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=svr_c_5,x=1,y=1,height=1,text="Please select your dynamic tank layout."}
TextBox{parent=svr_c_5,x=12,y=3,height=1,text="Facility Tanks Unit Tanks",fg_bg=g_lg_fg_bg}
TextBox{parent=svr_c_5,x=1,y=1,text="Please select your dynamic tank layout."}
TextBox{parent=svr_c_5,x=12,y=3,text="Facility Tanks Unit Tanks",fg_bg=g_lg_fg_bg}
--#region Tank Layout Visualizer
@@ -416,25 +416,25 @@ local function config_view(display)
-- draw unit tanks and their pipes
for i = 1, 4 do
local line = Div{parent=vis,x=22,y=(i*2)-1,width=13,height=1}
TextBox{parent=line,width=5,height=1,text=string.rep("\x8c",5),fg_bg=pipe_cpair}
local label = TextBox{parent=line,x=7,y=1,width=7,height=1,text="Tank ?"}
TextBox{parent=line,width=5,text=string.rep("\x8c",5),fg_bg=pipe_cpair}
local label = TextBox{parent=line,x=7,y=1,width=7,text="Tank ?"}
tool_ctl.vis_utanks[i] = { line = line, label = label }
end
-- draw facility tank connections
local ftank_1 = Div{parent=vis,x=1,y=1,width=13,height=1}
TextBox{parent=ftank_1,width=7,height=1,text="Tank F1"}
TextBox{parent=ftank_1,width=7,text="Tank F1"}
tool_ctl.vis_ftanks[1] = {
line = ftank_1, pipe_direct = TextBox{parent=ftank_1,x=9,y=1,width=5,text=string.rep("\x8c",5),fg_bg=pipe_cpair}
}
for i = 2, 4 do
local line = Div{parent=vis,x=1,y=(i-1)*2,width=13,height=2}
local pipe_conn = TextBox{parent=line,x=13,y=2,width=1,height=1,text="\x8c",fg_bg=pipe_cpair}
local pipe_conn = TextBox{parent=line,x=13,y=2,width=1,text="\x8c",fg_bg=pipe_cpair}
local pipe_chain = TextBox{parent=line,x=12,y=1,width=1,height=2,text="\x95\n\x8d",fg_bg=pipe_cpair}
local pipe_direct = TextBox{parent=line,x=9,y=2,width=4,height=1,text="\x8c\x8c\x8c\x8c",fg_bg=pipe_cpair}
local label = TextBox{parent=line,x=1,y=2,width=7,height=1,text=""}
local pipe_direct = TextBox{parent=line,x=9,y=2,width=4,text="\x8c\x8c\x8c\x8c",fg_bg=pipe_cpair}
local label = TextBox{parent=line,x=1,y=2,width=7,text=""}
tool_ctl.vis_ftanks[i] = { line = line, pipe_conn = pipe_conn, pipe_chain = pipe_chain, pipe_direct = pipe_direct, label = label }
end
@@ -453,15 +453,16 @@ local function config_view(display)
vis_unit_list.set_value(u_text)
local vis_ftanks = tool_ctl.vis_ftanks
local next_idx = 1
if is_ft(1) then
next_idx = 2
if (mode == 1 and (is_ft(2) or is_ft(3) or is_ft(4))) or (mode == 2 and (is_ft(2) or is_ft(3))) or ((mode == 3 or mode == 5) and is_ft(2)) then
tool_ctl.vis_ftanks[1].pipe_direct.set_value("\x8c\x8c\x8c\x9c\x8c")
vis_ftanks[1].pipe_direct.set_value("\x8c\x8c\x8c\x9c\x8c")
else
tool_ctl.vis_ftanks[1].pipe_direct.set_value(string.rep("\x8c",5))
vis_ftanks[1].pipe_direct.set_value(string.rep("\x8c",5))
end
end
@@ -469,108 +470,108 @@ local function config_view(display)
local _2_46_need_chain = (mode == 4 and (is_ft(3) or is_ft(4))) or (mode == 6 and is_ft(3))
if is_ft(2) then
tool_ctl.vis_ftanks[2].label.set_value("Tank F" .. next_idx)
vis_ftanks[2].label.set_value("Tank F" .. next_idx)
if (mode < 4 or mode == 5) and is_ft(1) then
tool_ctl.vis_ftanks[2].label.hide(true)
tool_ctl.vis_ftanks[2].pipe_direct.hide(true)
vis_ftanks[2].label.hide(true)
vis_ftanks[2].pipe_direct.hide(true)
if _2_12_need_passt then
tool_ctl.vis_ftanks[2].pipe_chain.set_value("\x95\n\x9d")
vis_ftanks[2].pipe_chain.set_value("\x95\n\x9d")
else
tool_ctl.vis_ftanks[2].pipe_chain.set_value("\x95\n\x8d")
vis_ftanks[2].pipe_chain.set_value("\x95\n\x8d")
end
tool_ctl.vis_ftanks[2].pipe_chain.show()
vis_ftanks[2].pipe_chain.show()
else
tool_ctl.vis_ftanks[2].label.show()
vis_ftanks[2].label.show()
next_idx = next_idx + 1
tool_ctl.vis_ftanks[2].pipe_chain.hide(true)
vis_ftanks[2].pipe_chain.hide(true)
if _2_12_need_passt or _2_46_need_chain then
tool_ctl.vis_ftanks[2].pipe_direct.set_value("\x8c\x8c\x8c\x9c")
vis_ftanks[2].pipe_direct.set_value("\x8c\x8c\x8c\x9c")
else
tool_ctl.vis_ftanks[2].pipe_direct.set_value("\x8c\x8c\x8c\x8c")
vis_ftanks[2].pipe_direct.set_value("\x8c\x8c\x8c\x8c")
end
tool_ctl.vis_ftanks[2].pipe_direct.show()
vis_ftanks[2].pipe_direct.show()
end
tool_ctl.vis_ftanks[2].line.show()
vis_ftanks[2].line.show()
elseif is_ft(1) and _2_12_need_passt then
tool_ctl.vis_ftanks[2].label.hide(true)
tool_ctl.vis_ftanks[2].pipe_direct.hide(true)
tool_ctl.vis_ftanks[2].pipe_chain.set_value("\x95\n\x95")
tool_ctl.vis_ftanks[2].pipe_chain.show()
tool_ctl.vis_ftanks[2].line.show()
vis_ftanks[2].label.hide(true)
vis_ftanks[2].pipe_direct.hide(true)
vis_ftanks[2].pipe_chain.set_value("\x95\n\x95")
vis_ftanks[2].pipe_chain.show()
vis_ftanks[2].line.show()
else
tool_ctl.vis_ftanks[2].line.hide(true)
vis_ftanks[2].line.hide(true)
end
if is_ft(3) then
tool_ctl.vis_ftanks[3].label.set_value("Tank F" .. next_idx)
vis_ftanks[3].label.set_value("Tank F" .. next_idx)
if (mode < 3 and (is_ft(1) or is_ft(2))) or ((mode == 4 or mode == 6) and is_ft(2)) then
tool_ctl.vis_ftanks[3].label.hide(true)
tool_ctl.vis_ftanks[3].pipe_direct.hide(true)
vis_ftanks[3].label.hide(true)
vis_ftanks[3].pipe_direct.hide(true)
if (mode == 1 or mode == 4) and is_ft(4) then
tool_ctl.vis_ftanks[3].pipe_chain.set_value("\x95\n\x9d")
vis_ftanks[3].pipe_chain.set_value("\x95\n\x9d")
else
tool_ctl.vis_ftanks[3].pipe_chain.set_value("\x95\n\x8d")
vis_ftanks[3].pipe_chain.set_value("\x95\n\x8d")
end
tool_ctl.vis_ftanks[3].pipe_chain.show()
vis_ftanks[3].pipe_chain.show()
else
tool_ctl.vis_ftanks[3].label.show()
vis_ftanks[3].label.show()
next_idx = next_idx + 1
tool_ctl.vis_ftanks[3].pipe_chain.hide(true)
vis_ftanks[3].pipe_chain.hide(true)
if (mode == 1 or mode == 3 or mode == 4 or mode == 7) and is_ft(4) then
tool_ctl.vis_ftanks[3].pipe_direct.set_value("\x8c\x8c\x8c\x9c")
vis_ftanks[3].pipe_direct.set_value("\x8c\x8c\x8c\x9c")
else
tool_ctl.vis_ftanks[3].pipe_direct.set_value("\x8c\x8c\x8c\x8c")
vis_ftanks[3].pipe_direct.set_value("\x8c\x8c\x8c\x8c")
end
tool_ctl.vis_ftanks[3].pipe_direct.show()
vis_ftanks[3].pipe_direct.show()
end
tool_ctl.vis_ftanks[3].line.show()
vis_ftanks[3].line.show()
elseif (mode == 1 and is_ft(4) and (is_ft(1) or is_ft(2))) or (mode == 4 and is_ft(2) and is_ft(4)) then
tool_ctl.vis_ftanks[3].label.hide(true)
tool_ctl.vis_ftanks[3].pipe_direct.hide(true)
tool_ctl.vis_ftanks[3].pipe_chain.set_value("\x95\n\x95")
tool_ctl.vis_ftanks[3].pipe_chain.show()
tool_ctl.vis_ftanks[3].line.show()
vis_ftanks[3].label.hide(true)
vis_ftanks[3].pipe_direct.hide(true)
vis_ftanks[3].pipe_chain.set_value("\x95\n\x95")
vis_ftanks[3].pipe_chain.show()
vis_ftanks[3].line.show()
else
tool_ctl.vis_ftanks[3].line.hide(true)
vis_ftanks[3].line.hide(true)
end
if is_ft(4) then
tool_ctl.vis_ftanks[4].label.set_value("Tank F" .. next_idx)
vis_ftanks[4].label.set_value("Tank F" .. next_idx)
if (mode == 1 and (is_ft(1) or is_ft(2) or is_ft(3))) or ((mode == 3 or mode == 7) and is_ft(3)) or (mode == 4 and (is_ft(2) or is_ft(3))) then
tool_ctl.vis_ftanks[4].label.hide(true)
tool_ctl.vis_ftanks[4].pipe_direct.hide(true)
tool_ctl.vis_ftanks[4].pipe_chain.show()
vis_ftanks[4].label.hide(true)
vis_ftanks[4].pipe_direct.hide(true)
vis_ftanks[4].pipe_chain.show()
else
tool_ctl.vis_ftanks[4].label.show()
tool_ctl.vis_ftanks[4].pipe_chain.hide(true)
tool_ctl.vis_ftanks[4].pipe_direct.show()
vis_ftanks[4].label.show()
vis_ftanks[4].pipe_chain.hide(true)
vis_ftanks[4].pipe_direct.show()
end
tool_ctl.vis_ftanks[4].line.show()
vis_ftanks[4].line.show()
else
tool_ctl.vis_ftanks[4].line.hide(true)
vis_ftanks[4].line.hide(true)
end
end
local function change_mode(mode)
tmp_cfg.FacilityTankMode = mode
tool_ctl.vis_draw(mode)
end
local tank_modes = { "Mode 1", "Mode 2", "Mode 3", "Mode 4", "Mode 5", "Mode 6", "Mode 7", "Mode 8" }
local tank_mode = RadioButton{parent=svr_c_5,x=1,y=4,callback=tool_ctl.vis_draw,default=math.max(1,ini_cfg.FacilityTankMode),options=tank_modes,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.yellow}
local tank_mode = RadioButton{parent=svr_c_5,x=1,y=4,callback=change_mode,default=math.max(1,ini_cfg.FacilityTankMode),options=tank_modes,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.yellow}
--#endregion
local function submit_mode()
tmp_cfg.FacilityTankMode = tank_mode.get_value()
svr_pane.set_value(7)
end
PushButton{parent=svr_c_5,x=1,y=14,text="\x1b Back",callback=function()svr_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=svr_c_5,x=44,y=14,text="Next \x1a",callback=submit_mode,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=svr_c_5,x=44,y=14,text="Next \x1a",callback=function()svr_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=svr_c_5,x=8,y=14,min_width=7,text="About",callback=function()svr_pane.set_value(6)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg}
@@ -608,32 +609,32 @@ 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}}
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 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}
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=9,height=1,width=11,text="PLC Channel"}
TextBox{parent=net_c_1,x=1,y=9,width=11,text="PLC Channel"}
local plc_chan = NumberField{parent=net_c_1,x=21,y=9,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=9,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=10,height=1,width=19,text="RTU Gateway Channel"}
TextBox{parent=net_c_1,x=1,y=10,width=19,text="RTU Gateway Channel"}
local rtu_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=10,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=11,height=1,width=19,text="Coordinator Channel"}
TextBox{parent=net_c_1,x=1,y=11,width=19,text="Coordinator Channel"}
local crd_chan = NumberField{parent=net_c_1,x=21,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
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}
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 svr_c, plc_c, rtu_c = tonumber(svr_chan.get_value()), tonumber(plc_chan.get_value()), tonumber(rtu_chan.get_value())
@@ -649,24 +650,24 @@ 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=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=8,height=1,width=11,text="PLC Timeout"}
TextBox{parent=net_c_2,x=1,y=8,width=11,text="PLC Timeout"}
local plc_timeout = NumberField{parent=net_c_2,x=21,y=8,width=7,default=ini_cfg.PLC_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=1,y=9,height=1,width=19,text="RTU Gateway Timeout"}
TextBox{parent=net_c_2,x=1,y=9,width=19,text="RTU Gateway Timeout"}
local rtu_timeout = NumberField{parent=net_c_2,x=21,y=9,width=7,default=ini_cfg.RTU_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=1,y=10,height=1,width=19,text="Coordinator Timeout"}
TextBox{parent=net_c_2,x=1,y=10,width=19,text="Coordinator Timeout"}
local crd_timeout = NumberField{parent=net_c_2,x=21,y=10,width=7,default=ini_cfg.CRD_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=1,y=11,height=1,width=14,text="Pocket Timeout"}
TextBox{parent=net_c_2,x=1,y=11,width=14,text="Pocket Timeout"}
local pkt_timeout = NumberField{parent=net_c_2,x=21,y=11,width=7,default=ini_cfg.PKT_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=29,y=8,height=4,width=7,text="seconds\nseconds\nseconds\nseconds",fg_bg=g_lg_fg_bg}
local ct_err = TextBox{parent=net_c_2,x=8,y=14,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 plc_cto, rtu_cto, crd_cto, pkt_cto = tonumber(plc_timeout.get_value()), tonumber(rtu_timeout.get_value()), tonumber(crd_timeout.get_value()), tonumber(pkt_timeout.get_value())
@@ -680,13 +681,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=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=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local tr_err = TextBox{parent=net_c_3,x=8,y=14,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 range_val = tonumber(range.get_value())
@@ -703,7 +704,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=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 function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
@@ -713,7 +714,7 @@ local function config_view(display)
hide_key.set_value(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 v = key.get_value()
@@ -733,20 +734,20 @@ local function config_view(display)
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}
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 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 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
@@ -774,17 +775,17 @@ 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}}
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=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}
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,height=1,text="Preview"}
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)}
@@ -811,7 +812,7 @@ local function config_view(display)
end
end
TextBox{parent=clr_c_2,x=1,y=7,height=1,width=10,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=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}
@@ -865,7 +866,7 @@ local function config_view(display)
clr_pane.set_value(1)
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=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
@@ -884,7 +885,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}}
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)}
@@ -906,7 +907,10 @@ local function config_view(display)
end
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("/supervisor.settings") then
load_settings(settings_cfg, true)
@@ -961,7 +965,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=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!"}
TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"}
local function go_home()
main_pane.set_value(1)
@@ -994,15 +998,15 @@ local function config_view(display)
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)}
for _, change in ipairs(changes) do
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg}
TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
for _, v in ipairs(change[2]) do
local e = Div{parent=c_log,height=#util.strwrap(v,46)}
TextBox{parent=e,y=1,x=1,text="- ",height=1,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=e,y=1,x=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
end
end
@@ -1013,8 +1017,8 @@ local function config_view(display)
local i_err = Div{parent=import_err,x=2,y=4,width=49}
TextBox{parent=import_err,x=1,y=2,height=1,text=" Import Error",fg_bg=cpair(colors.black,colors.red)}
TextBox{parent=i_err,x=1,y=1,height=1,text="There is a problem with your config.lua file:"}
TextBox{parent=import_err,x=1,y=2,text=" Import Error",fg_bg=cpair(colors.black,colors.red)}
TextBox{parent=i_err,x=1,y=1,text="There is a problem with your config.lua file:"}
local import_err_msg = TextBox{parent=i_err,x=1,y=3,height=6,text=""}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,832 @@
local audio = require("scada-common.audio")
local const = require("scada-common.constants")
local log = require("scada-common.log")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local qtypes = require("supervisor.session.rtu.qtypes")
local TONE = audio.TONE
local ALARM = types.ALARM
local PRIO = types.ALARM_PRIORITY
local ALARM_STATE = types.ALARM_STATE
local CONTAINER_MODE = types.CONTAINER_MODE
local PROCESS = types.PROCESS
local PROCESS_NAMES = types.PROCESS_NAMES
local WASTE_MODE = types.WASTE_MODE
local WASTE = types.WASTE_PRODUCT
local IO = rsio.IO
local ALARM_LIMS = const.ALARM_LIMITS
local DTV_RTU_S_DATA = qtypes.DTV_RTU_S_DATA
-- 7.14 kJ per blade for 1 mB of fissile fuel<br>
-- 2856 FE per blade per 1 mB, 285.6 FE per blade per 0.1 mB (minimum)
local POWER_PER_BLADE = util.joules_to_fe_rf(7140)
local FLOW_STABILITY_DELAY_S = const.FLOW_STABILITY_DELAY_MS / 1000
local CHARGE_Kp = 0.15
local CHARGE_Ki = 0.0
local CHARGE_Kd = 0.6
local RATE_Kp = 2.45
local RATE_Ki = 0.4825
local RATE_Kd = -1.0
local self = nil ---@type _facility_self
local next_mode = 0
local charge_update = 0
local rate_update = 0
---@class facility_update_extension
local update = {}
--#region PRIVATE FUNCTIONS
-- check if all auto-controlled units completed ramping
---@nodiscard
local function all_units_ramped()
local all_ramped = true
for i = 1, #self.prio_defs do
local units = self.prio_defs[i]
for u = 1, #units do
all_ramped = all_ramped and units[u].auto_ramp_complete()
end
end
return all_ramped
end
-- split a burn rate among the reactors
---@param burn_rate number burn rate assignment
---@param ramp boolean true to ramp, false to set right away
---@param abort_on_fault boolean? true to exit if one device has an effective burn rate different than its limit
---@return integer unallocated_br100, boolean? aborted
local function allocate_burn_rate(burn_rate, ramp, abort_on_fault)
local unallocated = math.floor(burn_rate * 100)
-- go through all priority groups
for i = 1, #self.prio_defs do
local units = self.prio_defs[i]
if #units > 0 then
local split = math.floor(unallocated / #units)
local splits = {}
for u = 1, #units do splits[u] = split end
splits[#units] = splits[#units] + (unallocated % #units)
-- go through all reactor units in this group
for id = 1, #units do
local u = units[id] ---@type reactor_unit
local ctl = u.get_control_inf()
local lim_br100 = u.auto_get_effective_limit()
if abort_on_fault and (lim_br100 ~= ctl.lim_br100) then
-- effective limit differs from set limit, unit is degraded
return unallocated, true
end
local last = ctl.br100
if splits[id] <= lim_br100 then
ctl.br100 = splits[id]
else
ctl.br100 = lim_br100
if id < #units then
local remaining = #units - id
split = math.floor(unallocated / remaining)
for x = (id + 1), #units do splits[x] = split end
splits[#units] = splits[#units] + (unallocated % remaining)
end
end
unallocated = math.max(0, unallocated - ctl.br100)
if last ~= ctl.br100 then u.auto_commit_br100(ramp) end
end
end
end
return unallocated, false
end
-- set idle state of all assigned reactors
---@param idle boolean idle state
local function set_idling(idle)
for i = 1, #self.prio_defs do
for _, u in pairs(self.prio_defs[i]) do u.auto_set_idle(idle) end
end
end
--#endregion
--#region PUBLIC FUNCTIONS
-- automatic control pre-update logic
function update.pre_auto()
-- unlink RTU sessions if they are closed
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end
-- check if test routines are allowed right now
self.allow_testing = true
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
self.allow_testing = self.allow_testing and u.is_safe_idle()
end
-- current state for process control
charge_update = 0
rate_update = 0
-- calculate moving averages for induction matrix
if self.induction[1] ~= nil then
local matrix = self.induction[1] ---@type unit_session
local db = matrix.get_db() ---@type imatrix_session_db
local build_update = db.build.last_update
rate_update = db.state.last_update
charge_update = db.tanks.last_update
local has_data = build_update > 0 and rate_update > 0 and charge_update > 0
if matrix.is_faulted() then
-- a fault occured, cannot reliably update stats
has_data = false
self.im_stat_init = false
self.imtx_faulted_times = { build_update, rate_update, charge_update }
elseif not self.im_stat_init then
-- prevent operation with partially invalid data
-- all fields must have updated since the last fault
has_data = self.imtx_faulted_times[1] < build_update and
self.imtx_faulted_times[2] < rate_update and
self.imtx_faulted_times[3] < charge_update
end
if has_data then
local energy = util.joules_to_fe_rf(db.tanks.energy)
local input = util.joules_to_fe_rf(db.state.last_input)
local output = util.joules_to_fe_rf(db.state.last_output)
if self.im_stat_init then
self.avg_charge.record(energy, charge_update)
self.avg_inflow.record(input, rate_update)
self.avg_outflow.record(output, rate_update)
if charge_update ~= self.imtx_last_charge_t then
local delta = (energy - self.imtx_last_charge) / (charge_update - self.imtx_last_charge_t)
self.imtx_last_charge = energy
self.imtx_last_charge_t = charge_update
-- if the capacity changed, toss out existing data
if db.build.max_energy ~= self.imtx_last_capacity then
self.imtx_last_capacity = db.build.max_energy
self.avg_net.reset()
else
self.avg_net.record(delta, charge_update)
end
end
else
self.im_stat_init = true
self.avg_charge.reset(energy)
self.avg_inflow.reset(input)
self.avg_outflow.reset(output)
self.avg_net.reset()
self.imtx_last_capacity = db.build.max_energy
self.imtx_last_charge = energy
self.imtx_last_charge_t = charge_update
end
else
-- prevent use by control systems
rate_update = 0
charge_update = 0
end
else
self.im_stat_init = false
end
self.all_sys_ok = true
for i = 1, #self.units do
self.all_sys_ok = self.all_sys_ok and not self.units[i].get_control_inf().degraded
end
end
-- run auto control
---@param ExtChargeIdling boolean ExtChargeIdling config field
function update.auto_control(ExtChargeIdling)
local AUTO_SCRAM = self.types.AUTO_SCRAM
local START_STATUS = self.types.START_STATUS
local avg_charge = self.avg_charge.compute()
local avg_inflow = self.avg_inflow.compute()
local avg_outflow = self.avg_outflow.compute()
local now = os.clock()
local state_changed = self.mode ~= self.last_mode
next_mode = self.mode
-- once auto control is started, sort the priority sublists by limits
if state_changed then
self.saturated = false
log.debug(util.c("FAC: state changed from ", PROCESS_NAMES[self.last_mode + 1], " to ", PROCESS_NAMES[self.mode + 1]))
if (self.last_mode == PROCESS.INACTIVE) or (self.last_mode == PROCESS.GEN_RATE_FAULT_IDLE) then
self.start_fail = START_STATUS.OK
if (self.mode ~= PROCESS.MATRIX_FAULT_IDLE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then
-- auto clear ASCRAM
self.ascram = false
self.ascram_reason = AUTO_SCRAM.NONE
end
local blade_count = nil
self.max_burn_combined = 0.0
for i = 1, #self.prio_defs do
table.sort(self.prio_defs[i],
---@param a reactor_unit
---@param b reactor_unit
function (a, b) return a.get_control_inf().lim_br100 < b.get_control_inf().lim_br100 end
)
for _, u in pairs(self.prio_defs[i]) do
local u_blade_count = u.get_control_inf().blade_count
if blade_count == nil then
blade_count = u_blade_count
elseif (u_blade_count ~= blade_count) and (self.mode == PROCESS.GEN_RATE) then
log.warning("FAC: cannot start GEN_RATE process with inconsistent unit blade counts")
next_mode = PROCESS.INACTIVE
self.start_fail = START_STATUS.BLADE_MISMATCH
end
if self.start_fail == START_STATUS.OK then u.auto_engage() end
self.max_burn_combined = self.max_burn_combined + (u.get_control_inf().lim_br100 / 100.0)
end
end
log.debug(util.c("FAC: computed a max combined burn rate of ", self.max_burn_combined, "mB/t"))
if blade_count == nil then
-- no units
log.warning("FAC: cannot start process control with 0 units assigned")
next_mode = PROCESS.INACTIVE
self.start_fail = START_STATUS.NO_UNITS
else
self.charge_conversion = blade_count * POWER_PER_BLADE
end
elseif self.mode == PROCESS.INACTIVE then
for i = 1, #self.prio_defs do
-- disable reactors and disengage auto control
for _, u in pairs(self.prio_defs[i]) do
u.disable()
u.auto_set_idle(false)
u.auto_disengage()
end
end
log.info("FAC: disengaging auto control (now inactive)")
end
self.initial_ramp = true
self.waiting_on_ramp = false
self.waiting_on_stable = false
else
self.initial_ramp = false
end
-- update unit ready state
local assign_count = 0
self.units_ready = true
for i = 1, #self.prio_defs do
for _, u in pairs(self.prio_defs[i]) do
assign_count = assign_count + 1
self.units_ready = self.units_ready and u.get_control_inf().ready
end
end
-- perform mode-specific operations
if self.mode == PROCESS.INACTIVE then
if not self.units_ready then
self.status_text = { "NOT READY", "assigned units not ready" }
else
-- clear ASCRAM once ready
self.ascram = false
self.ascram_reason = AUTO_SCRAM.NONE
if self.start_fail == START_STATUS.NO_UNITS and assign_count == 0 then
self.status_text = { "START FAILED", "no units were assigned" }
elseif self.start_fail == START_STATUS.BLADE_MISMATCH then
self.status_text = { "START FAILED", "turbine blade count mismatch" }
else
self.status_text = { "IDLE", "control disengaged" }
end
end
elseif self.mode == PROCESS.MAX_BURN then
-- run units at their limits
if state_changed then
self.time_start = now
self.saturated = true
self.status_text = { "MONITORED MODE", "running reactors at limit" }
log.info("FAC: MAX_BURN process mode started")
end
allocate_burn_rate(self.max_burn_combined, true)
elseif self.mode == PROCESS.BURN_RATE then
-- a total aggregate burn rate
if state_changed then
self.time_start = now
self.status_text = { "BURN RATE MODE", "running" }
log.info("FAC: BURN_RATE process mode started")
end
local unallocated = allocate_burn_rate(self.burn_target, true)
self.saturated = self.burn_target == self.max_burn_combined or unallocated > 0
elseif self.mode == PROCESS.CHARGE then
-- target a level of charge
if state_changed then
self.time_start = now
self.last_time = now
self.last_error = 0
self.accumulator = 0
-- enabling idling on all assigned units
set_idling(true)
self.status_text = { "CHARGE MODE", "running control loop" }
log.info("FAC: CHARGE mode starting PID control")
elseif self.last_update < charge_update then
-- convert to kFE to make constants not microscopic
local error = util.round((self.charge_setpoint - avg_charge) / 1000) / 1000
-- stop accumulator when saturated to avoid windup
if not self.saturated then
self.accumulator = self.accumulator + (error * (now - self.last_time))
end
-- local runtime = now - self.time_start
local integral = self.accumulator
local derivative = (error - self.last_error) / (now - self.last_time)
local P = CHARGE_Kp * error
local I = CHARGE_Ki * integral
local D = CHARGE_Kd * derivative
local output = P + I + D
-- clamp at range -> output clamped (out_c)
local out_c = math.max(0, math.min(output, self.max_burn_combined))
self.saturated = output ~= out_c
if not ExtChargeIdling then
-- stop idling early if the output is zero, we are at or above the setpoint, and are not losing charge
set_idling(not ((out_c == 0) and (error <= 0) and (avg_outflow <= 0)))
end
-- log.debug(util.sprintf("CHARGE[%f] { CHRG[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%f] }",
-- runtime, avg_charge, error, integral, output, out_c, P, I, D))
allocate_burn_rate(out_c, true)
self.last_time = now
self.last_error = error
end
self.last_update = charge_update
elseif self.mode == PROCESS.GEN_RATE then
-- target a rate of generation
if state_changed then
-- estimate an initial output
local output = self.gen_rate_setpoint / self.charge_conversion
local unallocated = allocate_burn_rate(output, true)
self.saturated = output >= self.max_burn_combined or unallocated > 0
self.waiting_on_ramp = true
self.status_text = { "GENERATION MODE", "starting up" }
log.info(util.c("FAC: GEN_RATE process mode initial ramp started (initial target is ", output, " mB/t)"))
elseif self.waiting_on_ramp then
if all_units_ramped() then
self.waiting_on_ramp = false
self.waiting_on_stable = true
self.time_start = now
self.status_text = { "GENERATION MODE", "holding ramped rate" }
log.info("FAC: GEN_RATE process mode initial ramp completed, holding for stablization time")
end
elseif self.waiting_on_stable then
if (now - self.time_start) > FLOW_STABILITY_DELAY_S then
self.waiting_on_stable = false
self.time_start = now
self.last_time = now
self.last_error = 0
self.accumulator = 0
self.status_text = { "GENERATION MODE", "running control loop" }
log.info("FAC: GEN_RATE process mode initial hold completed, starting PID control")
end
elseif self.last_update < rate_update then
-- convert to MFE (in rounded kFE) to make constants not microscopic
local error = util.round((self.gen_rate_setpoint - avg_inflow) / 1000) / 1000
-- stop accumulator when saturated to avoid windup
if not self.saturated then
self.accumulator = self.accumulator + (error * (now - self.last_time))
end
-- local runtime = now - self.time_start
local integral = self.accumulator
local derivative = (error - self.last_error) / (now - self.last_time)
local P = RATE_Kp * error
local I = RATE_Ki * integral
local D = RATE_Kd * derivative
-- velocity (rate) (derivative of charge level => rate) feed forward
local FF = self.gen_rate_setpoint / self.charge_conversion
local output = P + I + D + FF
-- clamp at range -> output clamped (sp_c)
local out_c = math.max(0, math.min(output, self.max_burn_combined))
self.saturated = output ~= out_c
-- log.debug(util.sprintf("GEN_RATE[%f] { RATE[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%f] }",
-- runtime, avg_inflow, error, integral, output, out_c, P, I, D))
allocate_burn_rate(out_c, false)
self.last_time = now
self.last_error = error
end
self.last_update = rate_update
elseif self.mode == PROCESS.MATRIX_FAULT_IDLE then
-- exceeded charge, wait until condition clears
if self.ascram_reason == AUTO_SCRAM.NONE then
next_mode = self.return_mode
log.info("FAC: exiting matrix fault idle state due to fault resolution")
elseif self.ascram_reason == AUTO_SCRAM.CRIT_ALARM then
next_mode = PROCESS.SYSTEM_ALARM_IDLE
log.info("FAC: exiting matrix fault idle state due to critical unit alarm")
end
elseif self.mode == PROCESS.SYSTEM_ALARM_IDLE then
-- do nothing, wait for user to confirm (stop and reset)
elseif self.mode == PROCESS.GEN_RATE_FAULT_IDLE then
-- system faulted (degraded/not ready) while running generation rate mode
-- mode will need to be fully restarted once everything is OK to re-ramp to feed-forward
if self.units_ready then
log.info("FAC: system ready after faulting out of GEN_RATE process mode, switching back...")
next_mode = PROCESS.GEN_RATE
end
elseif self.mode ~= PROCESS.INACTIVE then
log.error(util.c("FAC: unsupported process mode ", self.mode, ", switching to inactive"))
next_mode = PROCESS.INACTIVE
end
end
-- update automatic safety logic
function update.auto_safety()
local AUTO_SCRAM = self.types.AUTO_SCRAM
local astatus = self.ascram_status
if self.induction[1] ~= nil then
local db = self.induction[1].get_db() ---@type imatrix_session_db
-- clear matrix disconnected
if astatus.matrix_dc then
astatus.matrix_dc = false
log.info("FAC: induction matrix reconnected, clearing ASCRAM condition")
end
-- check matrix fill too high
local was_fill = astatus.matrix_fill
astatus.matrix_fill = (db.tanks.energy_fill >= ALARM_LIMS.CHARGE_HIGH) or (astatus.matrix_fill and db.tanks.energy_fill > ALARM_LIMS.CHARGE_RE_ENABLE)
if was_fill and not astatus.matrix_fill then
log.info(util.c("FAC: charge state of induction matrix entered acceptable range <= ", ALARM_LIMS.CHARGE_RE_ENABLE * 100, "%"))
end
-- check for critical unit alarms
astatus.crit_alarm = false
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
if u.has_alarm_min_prio(PRIO.CRITICAL) then
astatus.crit_alarm = true
break
end
end
-- check for facility radiation
if #self.envd > 0 then
local max_rad = 0
for i = 1, #self.envd do
local envd = self.envd[i] ---@type unit_session
local e_db = envd.get_db() ---@type envd_session_db
if e_db.radiation_raw > max_rad then max_rad = e_db.radiation_raw end
end
astatus.radiation = max_rad >= ALARM_LIMS.FAC_HIGH_RAD
else
-- don't clear, if it is true then we lost it with high radiation, so just keep alarming
-- operator can restart the system or hit the stop/reset button
end
-- system not ready, will need to restart GEN_RATE mode
-- clears when we enter the fault waiting state
astatus.gen_fault = self.mode == PROCESS.GEN_RATE and not self.units_ready
else
astatus.matrix_dc = true
end
if (self.mode ~= PROCESS.INACTIVE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then
local scram = astatus.matrix_dc or astatus.matrix_fill or astatus.crit_alarm or astatus.gen_fault
if scram and not self.ascram then
-- SCRAM all units
for i = 1, #self.prio_defs do
for _, u in pairs(self.prio_defs[i]) do
u.auto_scram()
end
end
if astatus.crit_alarm then
-- highest priority alarm
next_mode = PROCESS.SYSTEM_ALARM_IDLE
self.ascram_reason = AUTO_SCRAM.CRIT_ALARM
self.status_text = { "AUTOMATIC SCRAM", "critical unit alarm tripped" }
log.info("FAC: automatic SCRAM due to critical unit alarm")
log.warning("FAC: emergency exit of process control due to critical unit alarm")
elseif astatus.radiation then
next_mode = PROCESS.SYSTEM_ALARM_IDLE
self.ascram_reason = AUTO_SCRAM.RADIATION
self.status_text = { "AUTOMATIC SCRAM", "facility radiation high" }
log.info("FAC: automatic SCRAM due to high facility radiation")
elseif astatus.matrix_dc then
next_mode = PROCESS.MATRIX_FAULT_IDLE
self.ascram_reason = AUTO_SCRAM.MATRIX_DC
self.status_text = { "AUTOMATIC SCRAM", "induction matrix disconnected" }
if self.mode ~= PROCESS.MATRIX_FAULT_IDLE then self.return_mode = self.mode end
log.info("FAC: automatic SCRAM due to induction matrix disconnection")
elseif astatus.matrix_fill then
next_mode = PROCESS.MATRIX_FAULT_IDLE
self.ascram_reason = AUTO_SCRAM.MATRIX_FILL
self.status_text = { "AUTOMATIC SCRAM", "induction matrix fill high" }
if self.mode ~= PROCESS.MATRIX_FAULT_IDLE then self.return_mode = self.mode end
log.info("FAC: automatic SCRAM due to induction matrix high charge")
elseif astatus.gen_fault then
-- lowest priority alarm
next_mode = PROCESS.GEN_RATE_FAULT_IDLE
self.ascram_reason = AUTO_SCRAM.GEN_FAULT
self.status_text = { "GENERATION MODE IDLE", "paused: system not ready" }
log.info("FAC: automatic SCRAM due to unit problem while in GEN_RATE mode, will resume once all units are ready")
end
end
self.ascram = scram
if not self.ascram then
self.ascram_reason = AUTO_SCRAM.NONE
-- reset PLC RPS trips if we should
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
u.auto_cond_rps_reset()
end
end
end
end
-- update last mode and set next mode
function update.post_auto()
self.last_mode = self.mode
self.mode = next_mode
end
-- update alarm audio control
function update.alarm_audio()
local allow_test = self.allow_testing and self.test_tone_set
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
-- reset tone states before re-evaluting
for i = 1, #self.tone_states do self.tone_states[i] = false end
if allow_test then
alarms = self.test_alarm_states
else
-- check all alarms for all units
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
for id, alarm in pairs(u.get_alarms()) do
alarms[id] = alarms[id] or (alarm == ALARM_STATE.TRIPPED)
end
end
if not self.test_tone_reset then
-- clear testing alarms if we aren't using them
for i = 1, #self.test_alarm_states do self.test_alarm_states[i] = false end
end
end
-- Evaluate Alarms --
-- containment breach is worst case CRITICAL alarm, this takes priority
if alarms[ALARM.ContainmentBreach] then
self.tone_states[TONE.T_1800Hz_Int_4Hz] = true
else
-- critical damage is highest priority CRITICAL level alarm
if alarms[ALARM.CriticalDamage] then
self.tone_states[TONE.T_660Hz_Int_125ms] = true
else
-- EMERGENCY level alarms + URGENT over temp
if alarms[ALARM.ReactorDamage] or alarms[ALARM.ReactorOverTemp] or alarms[ALARM.ReactorWasteLeak] then
self.tone_states[TONE.T_544Hz_440Hz_Alt] = true
-- URGENT level turbine trip
elseif alarms[ALARM.TurbineTrip] then
self.tone_states[TONE.T_745Hz_Int_1Hz] = true
-- URGENT level reactor lost
elseif alarms[ALARM.ReactorLost] then
self.tone_states[TONE.T_340Hz_Int_2Hz] = true
-- TIMELY level alarms
elseif alarms[ALARM.ReactorHighTemp] or alarms[ALARM.ReactorHighWaste] or alarms[ALARM.RCSTransient] then
self.tone_states[TONE.T_800Hz_Int] = true
end
end
-- check RPS transient URGENT level alarm
if alarms[ALARM.RPSTransient] then
self.tone_states[TONE.T_1000Hz_Int] = true
-- disable really painful audio combination
self.tone_states[TONE.T_340Hz_Int_2Hz] = false
end
end
-- radiation is a big concern, always play this CRITICAL level alarm if active
if alarms[ALARM.ContainmentRadiation] then
self.tone_states[TONE.T_800Hz_1000Hz_Alt] = true
-- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled
-- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one
if self.tone_states[TONE.T_1000Hz_Int] and alarms[ALARM.ReactorLost] then self.tone_states[TONE.T_340Hz_Int_2Hz] = true end
-- it sounds *really* bad if this is in conjunction with these other tones, so disable them
self.tone_states[TONE.T_745Hz_Int_1Hz] = false
self.tone_states[TONE.T_800Hz_Int] = false
self.tone_states[TONE.T_1000Hz_Int] = false
end
-- add to tone states if testing is active
if allow_test then
for i = 1, #self.tone_states do
self.tone_states[i] = self.tone_states[i] or self.test_tone_states[i]
end
self.test_tone_reset = false
else
if not self.test_tone_reset then
-- clear testing tones if we aren't using them
for i = 1, #self.test_tone_states do self.test_tone_states[i] = false end
end
-- flag that tones were reset
self.test_tone_set = false
self.test_tone_reset = true
end
end
-- update facility redstone
---@param ack_all function acknowledge all alarms
function update.redstone(ack_all)
if #self.redstone > 0 then
-- handle facility SCRAM
if self.io_ctl.digital_read(IO.F_SCRAM) then
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
u.cond_scram()
end
end
-- handle facility ack
if self.io_ctl.digital_read(IO.F_ACK) then ack_all() end
-- update facility alarm outputs
local has_prio_alarm, has_any_alarm = false, false
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
if u.has_alarm_min_prio(PRIO.EMERGENCY) then
has_prio_alarm, has_any_alarm = true, true
break
elseif u.has_alarm_min_prio(PRIO.TIMELY) then
has_any_alarm = true
end
end
self.io_ctl.digital_write(IO.F_ALARM, has_prio_alarm)
self.io_ctl.digital_write(IO.F_ALARM_ANY, has_any_alarm)
-- update induction matrix related outputs
if self.induction[1] ~= nil then
local db = self.induction[1].get_db() ---@type imatrix_session_db
self.io_ctl.digital_write(IO.F_MATRIX_LOW, db.tanks.energy_fill < const.RS_THRESHOLDS.IMATRIX_CHARGE_LOW)
self.io_ctl.digital_write(IO.F_MATRIX_HIGH, db.tanks.energy_fill > const.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH)
self.io_ctl.analog_write(IO.F_MATRIX_CHG, db.tanks.energy_fill, 0, 1)
end
end
end
-- update unit tasks
function update.unit_mgmt()
local insufficent_po_rate = false
local need_emcool = false
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
-- update auto waste processing
if u.get_control_inf().waste_mode == WASTE_MODE.AUTO then
if (u.get_sna_rate() * 10.0) < u.get_burn_rate() then
insufficent_po_rate = true
end
end
-- check if unit activated emergency coolant & uses facility tanks
if (self.cooling_conf.fac_tank_mode > 0) and u.is_emer_cool_tripped() and (self.cooling_conf.fac_tank_defs[i] == 2) then
need_emcool = true
end
end
-- update waste product
self.current_waste_product = self.waste_product
if (not self.sps_low_power) and (self.waste_product == WASTE.ANTI_MATTER) and (self.induction[1] ~= nil) then
local db = self.induction[1].get_db() ---@type imatrix_session_db
if db.tanks.energy_fill >= 0.15 then
self.disabled_sps = false
elseif self.disabled_sps or ((db.tanks.last_update > 0) and (db.tanks.energy_fill < 0.1)) then
self.disabled_sps = true
self.current_waste_product = WASTE.POLONIUM
end
else
self.disabled_sps = false
end
if self.pu_fallback and insufficent_po_rate then
self.current_waste_product = WASTE.PLUTONIUM
end
-- make sure dynamic tanks are allowing outflow if required
-- set all, rather than trying to determine which is for which (simpler & safer)
-- there should be no need for any to be in fill only mode
if need_emcool then
for i = 1, #self.tanks do
local session = self.tanks[i] ---@type unit_session
local tank = session.get_db() ---@type dynamicv_session_db
if tank.state.container_mode == CONTAINER_MODE.FILL then
session.get_cmd_queue().push_data(DTV_RTU_S_DATA.SET_CONT_MODE, CONTAINER_MODE.BOTH)
end
end
end
end
--#endregion
-- link the self instance and return the update interface
---@param fac_self _facility_self
return function (fac_self)
self = fac_self
return update
end

View File

@@ -0,0 +1,47 @@
--
-- RTU ID Check Failure Entry
--
local types = require("scada-common.types")
local style = require("supervisor.panel.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local ALIGN = core.ALIGN
local cpair = core.cpair
-- create an ID check list entry
---@param parent graphics_element parent
---@param msg string message
---@param fail_code integer failure code
local function init(parent, msg, fail_code)
-- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.theme.highlight_box_bright}
local fg_bg = cpair(colors.black,colors.yellow)
local tag = "MISSING"
if fail_code == types.RTU_ID_FAIL.OUT_OF_RANGE then
fg_bg = cpair(colors.black,colors.orange)
tag = "BAD INDEX"
elseif fail_code == types.RTU_ID_FAIL.DUPLICATE then
fg_bg = cpair(colors.black,colors.red)
tag = "DUPLICATE"
end
TextBox{parent=entry,y=1,text="",width=11,fg_bg=fg_bg}
TextBox{parent=entry,text=tag,alignment=ALIGN.CENTER,width=11,fg_bg=fg_bg}
TextBox{parent=entry,text="",width=11,fg_bg=fg_bg}
TextBox{parent=entry,x=13,y=2,text=msg}
return root
end
return init

View File

@@ -31,18 +31,18 @@ local function init(parent, id)
local ps_prefix = "pdg_" .. id .. "_"
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=s_hi_box}
local pdg_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)}
TextBox{parent=entry,x=1,y=3,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 pdg_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)}
TextBox{parent=entry,x=1,y=3,text="",width=8,fg_bg=s_hi_box}
pdg_addr.register(databus.ps, ps_prefix .. "addr", pdg_addr.set_value)
TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1}
local pdg_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=label_fg}
TextBox{parent=entry,x=10,y=2,text="FW:",width=3}
local pdg_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,fg_bg=label_fg}
pdg_fw_v.register(databus.ps, ps_prefix .. "fw", pdg_fw_v.set_value)
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1}
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4}
local pdg_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=label_fg}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,fg_bg=label_fg}
pdg_rtt.register(databus.ps, ps_prefix .. "rtt", pdg_rtt.update)
pdg_rtt.register(databus.ps, ps_prefix .. "rtt_color", pdg_rtt.recolor)

View File

@@ -31,22 +31,22 @@ local function init(parent, id)
local ps_prefix = "rtu_" .. id .. "_"
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=s_hi_box}
local rtu_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)}
TextBox{parent=entry,x=1,y=3,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 rtu_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)}
TextBox{parent=entry,x=1,y=3,text="",width=8,fg_bg=s_hi_box}
rtu_addr.register(databus.ps, ps_prefix .. "addr", rtu_addr.set_value)
TextBox{parent=entry,x=10,y=2,text="UNITS:",width=7,height=1}
TextBox{parent=entry,x=10,y=2,text="UNITS:",width=7}
local unit_count = DataIndicator{parent=entry,x=17,y=2,label="",unit="",format="%2d",value=0,width=2,fg_bg=style.fp.label_d_fg}
unit_count.register(databus.ps, ps_prefix .. "units", unit_count.set_value)
TextBox{parent=entry,x=21,y=2,text="FW:",width=3,height=1}
local rtu_fw_v = TextBox{parent=entry,x=25,y=2,text=" ------- ",width=9,height=1,fg_bg=label_fg}
TextBox{parent=entry,x=21,y=2,text="FW:",width=3}
local rtu_fw_v = TextBox{parent=entry,x=25,y=2,text=" ------- ",width=9,fg_bg=label_fg}
rtu_fw_v.register(databus.ps, ps_prefix .. "fw", rtu_fw_v.set_value)
TextBox{parent=entry,x=36,y=2,text="RTT:",width=4,height=1}
TextBox{parent=entry,x=36,y=2,text="RTT:",width=4}
local rtu_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=label_fg}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,fg_bg=label_fg}
rtu_rtt.register(databus.ps, ps_prefix .. "rtt", rtu_rtt.update)
rtu_rtt.register(databus.ps, ps_prefix .. "rtt_color", rtu_rtt.recolor)

View File

@@ -10,6 +10,7 @@ local supervisor = require("supervisor.supervisor")
local pgi = require("supervisor.panel.pgi")
local style = require("supervisor.panel.style")
local chk_entry = require("supervisor.panel.components.chk_entry")
local pdg_entry = require("supervisor.panel.components.pdg_entry")
local rtu_entry = require("supervisor.panel.components.rtu_entry")
@@ -40,7 +41,7 @@ local function init(panel)
local label_fg = style.fp.label_fg
local label_d_fg = style.fp.label_d_fg
TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=ALIGN.CENTER,height=1,fg_bg=style.theme.header}
TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
local page_div = Div{parent=panel,x=1,y=3}
@@ -66,15 +67,15 @@ local function init(panel)
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=style.fp.disabled_fg}
TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=style.fp.disabled_fg}
--
-- about footer
--
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@@ -83,7 +84,7 @@ local function init(panel)
-- page handling
--
-- plc page
-- plc sessions page
local plc_page = Div{parent=page_div,x=1,y=1,hidden=true}
local plc_list = Div{parent=plc_page,x=2,y=2,width=49}
@@ -92,36 +93,36 @@ local function init(panel)
local ps_prefix = "plc_" .. i .. "_"
local plc_entry = Div{parent=plc_list,height=3,fg_bg=s_hi_bright}
TextBox{parent=plc_entry,x=1,y=1,text="",width=8,height=1,fg_bg=s_hi_box}
TextBox{parent=plc_entry,x=1,y=2,text="UNIT "..i,alignment=ALIGN.CENTER,width=8,height=1,fg_bg=s_hi_box}
TextBox{parent=plc_entry,x=1,y=3,text="",width=8,height=1,fg_bg=s_hi_box}
TextBox{parent=plc_entry,x=1,y=1,text="",width=8,fg_bg=s_hi_box}
TextBox{parent=plc_entry,x=1,y=2,text="UNIT "..i,alignment=ALIGN.CENTER,width=8,fg_bg=s_hi_box}
TextBox{parent=plc_entry,x=1,y=3,text="",width=8,fg_bg=s_hi_box}
local conn = LED{parent=plc_entry,x=10,y=2,label="LINK",colors=cpair(colors.green_hc,colors.green_off)}
conn.register(databus.ps, ps_prefix .. "conn", conn.update)
local plc_addr = TextBox{parent=plc_entry,x=17,y=2,text=" --- ",width=5,height=1,fg_bg=label_d_fg}
local plc_addr = TextBox{parent=plc_entry,x=17,y=2,text=" --- ",width=5,fg_bg=label_d_fg}
plc_addr.register(databus.ps, ps_prefix .. "addr", plc_addr.set_value)
TextBox{parent=plc_entry,x=23,y=2,text="FW:",width=3,height=1}
local plc_fw_v = TextBox{parent=plc_entry,x=27,y=2,text=" ------- ",width=9,height=1,fg_bg=label_fg}
TextBox{parent=plc_entry,x=23,y=2,text="FW:",width=3}
local plc_fw_v = TextBox{parent=plc_entry,x=27,y=2,text=" ------- ",width=9,fg_bg=label_fg}
plc_fw_v.register(databus.ps, ps_prefix .. "fw", plc_fw_v.set_value)
TextBox{parent=plc_entry,x=37,y=2,text="RTT:",width=4,height=1}
TextBox{parent=plc_entry,x=37,y=2,text="RTT:",width=4}
local plc_rtt = DataIndicator{parent=plc_entry,x=42,y=2,label="",unit="",format="%4d",value=0,width=4,fg_bg=label_fg}
TextBox{parent=plc_entry,x=47,y=2,text="ms",width=4,height=1,fg_bg=label_fg}
TextBox{parent=plc_entry,x=47,y=2,text="ms",width=4,fg_bg=label_fg}
plc_rtt.register(databus.ps, ps_prefix .. "rtt", plc_rtt.update)
plc_rtt.register(databus.ps, ps_prefix .. "rtt_color", plc_rtt.recolor)
plc_list.line_break()
end
-- rtu page
-- rtu sessions page
local rtu_page = Div{parent=page_div,x=1,y=1,hidden=true}
local rtu_list = ListBox{parent=rtu_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=rtu_list,height=1,hidden=true} -- padding
-- coordinator page
-- coordinator session page
local crd_page = Div{parent=page_div,x=1,y=1,hidden=true}
local crd_box = Div{parent=crd_page,x=2,y=2,width=49,height=4,fg_bg=s_hi_bright}
@@ -129,29 +130,51 @@ local function init(panel)
local crd_conn = LED{parent=crd_box,x=2,y=2,label="CONNECTION",colors=cpair(colors.green_hc,colors.green_off)}
crd_conn.register(databus.ps, "crd_conn", crd_conn.update)
TextBox{parent=crd_box,x=4,y=3,text="COMPUTER",width=8,height=1,fg_bg=label_d_fg}
local crd_addr = TextBox{parent=crd_box,x=13,y=3,text="---",width=5,height=1,fg_bg=label_d_fg}
TextBox{parent=crd_box,x=4,y=3,text="COMPUTER",width=8,fg_bg=label_d_fg}
local crd_addr = TextBox{parent=crd_box,x=13,y=3,text="---",width=5,fg_bg=label_d_fg}
crd_addr.register(databus.ps, "crd_addr", crd_addr.set_value)
TextBox{parent=crd_box,x=22,y=2,text="FW:",width=3,height=1}
local crd_fw_v = TextBox{parent=crd_box,x=26,y=2,text=" ------- ",width=9,height=1,fg_bg=label_fg}
TextBox{parent=crd_box,x=22,y=2,text="FW:",width=3}
local crd_fw_v = TextBox{parent=crd_box,x=26,y=2,text=" ------- ",width=9,fg_bg=label_fg}
crd_fw_v.register(databus.ps, "crd_fw", crd_fw_v.set_value)
TextBox{parent=crd_box,x=36,y=2,text="RTT:",width=4,height=1}
TextBox{parent=crd_box,x=36,y=2,text="RTT:",width=4}
local crd_rtt = DataIndicator{parent=crd_box,x=41,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=crd_box,x=47,y=2,text="ms",width=4,height=1,fg_bg=label_fg}
TextBox{parent=crd_box,x=47,y=2,text="ms",width=4,fg_bg=label_fg}
crd_rtt.register(databus.ps, "crd_rtt", crd_rtt.update)
crd_rtt.register(databus.ps, "crd_rtt_color", crd_rtt.recolor)
-- pocket diagnostics page
-- pocket sessions page
local pkt_page = Div{parent=page_div,x=1,y=1,hidden=true}
local pdg_list = ListBox{parent=pkt_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=pdg_list,height=1,hidden=true} -- padding
-- RTU device ID check/diagnostics page
local chk_page = Div{parent=page_div,x=1,y=1,hidden=true}
local chk_list = ListBox{parent=chk_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=chk_list,height=1,hidden=true} -- padding
-- info page
local info_page = Div{parent=page_div,x=1,y=1,hidden=true}
local info = Div{parent=info_page,height=6,x=2,y=2}
TextBox{parent=info,text="SVR \x1a Supervisor Status"}
TextBox{parent=info,text="PLC \x1a Reactor PLC Connections"}
TextBox{parent=info,text="RTU \x1a RTU Gateway Connections"}
TextBox{parent=info,text="CRD \x1a Coordinator Connection"}
TextBox{parent=info,text="PKT \x1a Pocket Connections"}
TextBox{parent=info,text="DEV \x1a RTU Device/Configuration Alerts"}
local notes = Div{parent=info_page,width=49,height=8,x=2,y=9,fg_bg=style.fp.disabled_fg}
TextBox{parent=notes,text="The DEV tab will show missing devices and devices that connected with incorrect information. Missing entries will indicate how the configuration should be, duplicate entries will indicate what is a duplicate, and out-of-range entries will indicate the invalid entry. An out-of-range example is a #2 turbine when you should only have 1 turbine for that unit."}
-- assemble page panes
local panes = { main_page, plc_page, rtu_page, crd_page, pkt_page }
local panes = { main_page, plc_page, rtu_page, crd_page, pkt_page, chk_page, info_page }
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
@@ -161,12 +184,14 @@ local function init(panel)
{ name = "RTU", color = style.fp.text },
{ name = "CRD", color = style.fp.text },
{ name = "PKT", color = style.fp.text },
{ name = "DEV", color = style.fp.text },
{ name = "INF", color = style.fp.text }
}
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=style.theme.highlight_box_bright}
TabBar{parent=panel,y=2,tabs=tabs,min_width=7,callback=page_pane.set_value,fg_bg=style.theme.highlight_box_bright}
-- link RTU/PDG list management to PGI
pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry)
-- link RTU/PDG/CHK list management to PGI
pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry, chk_list, chk_entry)
end
return init

View File

@@ -10,10 +10,12 @@ local pgi = {}
local data = {
rtu_list = nil, ---@type nil|graphics_element
pdg_list = nil, ---@type nil|graphics_element
chk_list = nil, ---@type nil|graphics_element
rtu_entry = nil, ---@type function
pdg_entry = nil, ---@type function
-- session entries
s_entries = { rtu = {}, pdg = {} }
chk_entry = nil, ---@type function
-- list entries
entries = { rtu = {}, pdg = {}, chk = {}, missing = {} }
}
-- link list boxes
@@ -21,19 +23,25 @@ local data = {
---@param rtu_entry function RTU entry constructor
---@param pdg_list graphics_element pocket diagnostics list element
---@param pdg_entry function pocket diagnostics entry constructor
function pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry)
---@param chk_list graphics_element CHK list element
---@param chk_entry function CHK entry constructor
function pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry, chk_list, chk_entry)
data.rtu_list = rtu_list
data.pdg_list = pdg_list
data.chk_list = chk_list
data.rtu_entry = rtu_entry
data.pdg_entry = pdg_entry
data.chk_entry = chk_entry
end
-- unlink all fields, disabling the PGI
function pgi.unlink()
data.rtu_list = nil
data.pdg_list = nil
data.chk_list = nil
data.rtu_entry = nil
data.pdg_entry = nil
data.chk_entry = nil
end
-- add an RTU entry to the RTU list
@@ -43,7 +51,8 @@ function pgi.create_rtu_entry(session_id)
local success, result = pcall(data.rtu_entry, data.rtu_list, session_id)
if success then
data.s_entries.rtu[session_id] = result
data.entries.rtu[session_id] = result
log.debug(util.c("PGI: created RTU entry (", session_id, ")"))
else
log.error(util.c("PGI: failed to create RTU entry (", result, ")"), true)
end
@@ -53,15 +62,17 @@ end
-- delete an RTU entry from the RTU list
---@param session_id integer RTU session
function pgi.delete_rtu_entry(session_id)
if data.s_entries.rtu[session_id] ~= nil then
local success, result = pcall(data.s_entries.rtu[session_id].delete)
data.s_entries.rtu[session_id] = nil
if data.entries.rtu[session_id] ~= nil then
local success, result = pcall(data.entries.rtu[session_id].delete)
data.entries.rtu[session_id] = nil
if not success then
if success then
log.debug(util.c("PGI: deleted RTU entry (", session_id, ")"))
else
log.error(util.c("PGI: failed to delete RTU entry (", result, ")"), true)
end
else
log.debug(util.c("PGI: tried to delete unknown RTU entry ", session_id))
log.warning(util.c("PGI: tried to delete unknown RTU entry ", session_id))
end
end
@@ -72,7 +83,8 @@ function pgi.create_pdg_entry(session_id)
local success, result = pcall(data.pdg_entry, data.pdg_list, session_id)
if success then
data.s_entries.pdg[session_id] = result
data.entries.pdg[session_id] = result
log.debug(util.c("PGI: created PDG entry (", session_id, ")"))
else
log.error(util.c("PGI: failed to create PDG entry (", result, ")"), true)
end
@@ -82,15 +94,92 @@ end
-- delete a PDG entry from the PDG list
---@param session_id integer pocket diagnostics session
function pgi.delete_pdg_entry(session_id)
if data.s_entries.pdg[session_id] ~= nil then
local success, result = pcall(data.s_entries.pdg[session_id].delete)
data.s_entries.pdg[session_id] = nil
if data.entries.pdg[session_id] ~= nil then
local success, result = pcall(data.entries.pdg[session_id].delete)
data.entries.pdg[session_id] = nil
if not success then
if success then
log.debug(util.c("PGI: deleted PDG entry (", session_id, ")"))
else
log.error(util.c("PGI: failed to delete PDG entry (", result, ")"), true)
end
else
log.debug(util.c("PGI: tried to delete unknown PDG entry ", session_id))
log.warning(util.c("PGI: tried to delete unknown PDG entry ", session_id))
end
end
-- add a device ID check failure entry to the CHK list
---@note this assumes only one type of failure can occur per each RTU gateway session's RTU, which is the case
---@param unit unit_session RTU session
---@param fail_code integer failure code
---@param msg string description to show the user
function pgi.create_chk_entry(unit, fail_code, msg)
local gw_session = unit.get_session_id()
if data.chk_list ~= nil and data.chk_entry ~= nil then
if not data.entries.chk[gw_session] then data.entries.chk[gw_session] = {} end
local success, result = pcall(data.chk_entry, data.chk_list, msg, fail_code)
if success then
data.entries.chk[gw_session][unit.get_unit_id()] = result
log.debug(util.c("PGI: created CHK entry (", gw_session, ":", unit.get_unit_id(), ")"))
else
log.error(util.c("PGI: failed to create CHK entry (", result, ")"), true)
end
end
end
-- delete a device ID check failure entry from the CHK list
---@note this assumes only one type of failure can occur per each RTU gateway session's RTU, which is the case
---@param unit unit_session RTU session
function pgi.delete_chk_entry(unit)
local gw_session = unit.get_session_id()
local ent_chk = data.entries.chk
if ent_chk[gw_session] ~= nil and ent_chk[gw_session][unit.get_unit_id()] ~= nil then
local success, result = pcall(ent_chk[gw_session][unit.get_unit_id()].delete)
ent_chk[gw_session][unit.get_unit_id()] = nil
if success then
log.debug(util.c("PGI: deleted CHK entry ", gw_session, ":", unit.get_unit_id()))
else
log.error(util.c("PGI: failed to delete CHK entry (", result, ")"), true)
end
else
log.warning(util.c("PGI: tried to delete unknown CHK entry with session of ", gw_session, " and unit ID of ", unit.get_unit_id()))
end
end
-- add a device ID missing entry to the CHK list
---@param message string missing device message
function pgi.create_missing_entry(message)
if data.chk_list ~= nil and data.chk_entry ~= nil then
local success, result = pcall(data.chk_entry, data.chk_list, message, 4)
if success then
data.entries.missing[message] = result
log.debug(util.c("PGI: created missing CHK entry (", message, ")"))
else
log.error(util.c("PGI: failed to create missing CHK entry (", result, ")"), true)
end
end
end
-- delete a device ID missing entry from the CHK list
---@param message string missing device message
function pgi.delete_missing_entry(message)
if data.entries.missing[message] ~= nil then
local success, result = pcall(data.entries.missing[message].delete)
data.entries.missing[message] = nil
if success then
log.debug(util.c("PGI: deleted missing CHK entry \"", message, "\""))
else
log.error(util.c("PGI: failed to delete missing CHK entry (", result, ")"), true)
end
else
log.warning(util.c("PGI: tried to delete unknown missing CHK entry \"", message, "\""))
end
end

View File

@@ -43,22 +43,23 @@ local PERIODICS = {
---@nodiscard
---@param id integer session ID
---@param s_addr integer device source address
---@param i_seq_num integer initial sequence number
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
---@param facility facility facility data table
---@param fp_ok boolean if the front panel UI is running
function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facility, fp_ok)
function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, facility, fp_ok)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end
local log_header = "crdn_session(" .. id .. "): "
local log_tag = "crdn_session(" .. id .. "): "
local self = {
units = facility.get_units(),
-- connection properties
seq_num = 0,
r_seq_num = nil,
seq_num = i_seq_num + 2, -- next after the establish approval was sent
r_seq_num = i_seq_num + 1,
connected = true,
conn_watchdog = util.new_watchdog(timeout),
establish_time = util.time_s(),
@@ -182,13 +183,11 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
---@param pkt mgmt_frame|crdn_frame
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return
else
self.r_seq_num = pkt.scada_frame.seq_num()
self.r_seq_num = pkt.scada_frame.seq_num() + 1
end
-- feed watchdog
@@ -206,7 +205,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then
log.warning(log_header .. "COORD KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
log.warning(log_tag .. "COORD KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
end
-- log.debug(log_header .. "COORD RTT = " .. self.last_rtt .. "ms")
@@ -214,13 +213,17 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
databus.tx_crd_rtt(self.last_rtt)
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
end
elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session
_close()
elseif pkt.type == MGMT_TYPE.ESTABLISH then
-- something is wrong, kill the session
_close()
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
---@cast pkt crdn_frame
@@ -253,7 +256,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
_send(CRDN_TYPE.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) })
else
log.debug(log_header .. "CRDN auto start (with configuration) packet length mismatch")
log.debug(log_tag .. "CRDN auto start (with configuration) packet length mismatch")
end
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
facility.ack_all()
@@ -262,25 +265,25 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
if pkt.length == 2 then
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_waste_product(pkt.data[2]) })
else
log.debug(log_header .. "CRDN set waste mode packet length mismatch")
log.debug(log_tag .. "CRDN set waste mode packet length mismatch")
end
elseif cmd == FAC_COMMAND.SET_PU_FB then
if pkt.length == 2 then
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_pu_fallback(pkt.data[2]) })
else
log.debug(log_header .. "CRDN set pu fallback packet length mismatch")
log.debug(log_tag .. "CRDN set pu fallback packet length mismatch")
end
elseif cmd == FAC_COMMAND.SET_SPS_LP then
if pkt.length == 2 then
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_sps_low_power(pkt.data[2]) })
else
log.debug(log_header .. "CRDN set sps low power packet length mismatch")
log.debug(log_tag .. "CRDN set sps low power packet length mismatch")
end
else
log.debug(log_header .. "CRDN facility command unknown")
log.debug(log_tag .. "CRDN facility command unknown")
end
else
log.debug(log_header .. "CRDN facility command packet length mismatch")
log.debug(log_tag .. "CRDN facility command packet length mismatch")
end
elseif pkt.type == CRDN_TYPE.UNIT_BUILDS then
-- acknowledgement to coordinator receiving builds
@@ -308,13 +311,13 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
if pkt.length == 3 then
out_queue.push_data(SV_Q_DATA.SET_BURN, data)
else
log.debug(log_header .. "CRDN unit command burn rate missing option")
log.debug(log_tag .. "CRDN unit command burn rate missing option")
end
elseif cmd == UNIT_COMMAND.SET_WASTE then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and (pkt.data[3] > 0) and (pkt.data[3] <= 4) then
unit.set_waste_mode(pkt.data[3])
else
log.debug(log_header .. "CRDN unit command set waste missing/invalid option")
log.debug(log_tag .. "CRDN unit command set waste missing/invalid option")
end
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
unit.ack_all()
@@ -323,32 +326,32 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
if pkt.length == 3 then
unit.ack_alarm(pkt.data[3])
else
log.debug(log_header .. "CRDN unit command ack alarm missing alarm id")
log.debug(log_tag .. "CRDN unit command ack alarm missing alarm id")
end
elseif cmd == UNIT_COMMAND.RESET_ALARM then
if pkt.length == 3 then
unit.reset_alarm(pkt.data[3])
else
log.debug(log_header .. "CRDN unit command reset alarm missing alarm id")
log.debug(log_tag .. "CRDN unit command reset alarm missing alarm id")
end
elseif cmd == UNIT_COMMAND.SET_GROUP then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and (pkt.data[3] >= 0) and (pkt.data[3] <= 4) then
facility.set_group(unit.get_id(), pkt.data[3])
_send(CRDN_TYPE.UNIT_CMD, { cmd, uid, pkt.data[3] })
else
log.debug(log_header .. "CRDN unit command set group missing group id")
log.debug(log_tag .. "CRDN unit command set group missing group id")
end
else
log.debug(log_header .. "CRDN unit command unknown")
log.debug(log_tag .. "CRDN unit command unknown")
end
else
log.debug(log_header .. "CRDN unit command invalid")
log.debug(log_tag .. "CRDN unit command invalid")
end
else
log.debug(log_header .. "CRDN unit command packet length mismatch")
log.debug(log_tag .. "CRDN unit command packet length mismatch")
end
else
log.debug(log_header .. "handler received unexpected SCADA_CRDN packet type " .. pkt.type)
log.debug(log_tag .. "handler received unexpected SCADA_CRDN packet type " .. pkt.type)
end
end
end
@@ -371,7 +374,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
_close()
_send_mgmt(MGMT_TYPE.CLOSE, {})
println("connection to coordinator " .. id .. " closed by server")
log.info(log_header .. "session closed by server")
log.info(log_tag .. "session closed by server")
end
-- iterate the session
@@ -438,14 +441,14 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
_send(CRDN_TYPE.FAC_BUILDS, { facility.get_build(cmd.val.type) })
end
else
log.error(log_header .. "unsupported data command received in in_queue (this is a bug)", true)
log.error(log_tag .. "unsupported data command received in in_queue (this is a bug)", true)
end
end
end
-- max 100ms spent processing queue
if util.time() - handle_start > 100 then
log.warning(log_header .. "exceeded 100ms queue process limit")
log.warning(log_tag .. "exceeded 100ms queue process limit")
break
end
end
@@ -453,7 +456,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
-- exit if connection was closed
if not self.connected then
println("connection to coordinator closed by remote host")
log.info(log_header .. "session closed by remote host")
log.info(log_tag .. "session closed by remote host")
return self.connected
end

View File

@@ -48,16 +48,17 @@ local PERIODICS = {
---@nodiscard
---@param id integer session ID
---@param s_addr integer device source address
---@param i_seq_num integer initial sequence number
---@param reactor_id integer reactor ID
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
---@param fp_ok boolean if the front panel UI is running
function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, fp_ok)
function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue, timeout, fp_ok)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end
local log_header = "plc_session(" .. id .. "): "
local log_tag = "plc_session(" .. id .. "): "
local self = {
commanded_state = false,
@@ -66,8 +67,8 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
ramping_rate = false,
auto_lock = false,
-- connection properties
seq_num = 0,
r_seq_num = nil,
seq_num = i_seq_num + 2, -- next after the establish approval was sent
r_seq_num = i_seq_num + 1,
connected = true,
received_struct = false,
received_status_cache = false,
@@ -183,78 +184,118 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
self.sDB.max_op_temp_H2O = max_burn * 2 * (JOULES_PER_MB * heat_cap ^ -1) + BASE_BOIL_TEMP
self.sDB.max_op_temp_Na = max_burn * (JOULES_PER_MB * heat_cap ^ -1) + BASE_BOIL_TEMP
log.info(util.sprintf(log_header .. "computed maximum operational temperatures %.3fK (H2O) and %.3fK (Na)",
log.info(util.sprintf(log_tag .. "computed maximum operational temperatures %.3fK (H2O) and %.3fK (Na)",
self.sDB.max_op_temp_H2O, self.sDB.max_op_temp_Na))
end
-- copy in the RPS status
---@param rps_status table
local function _copy_rps_status(rps_status)
self.sDB.rps_tripped = rps_status[1]
self.sDB.rps_trip_cause = rps_status[2]
self.sDB.rps_status.high_dmg = rps_status[3]
self.sDB.rps_status.high_temp = rps_status[4]
self.sDB.rps_status.low_cool = rps_status[5]
self.sDB.rps_status.ex_waste = rps_status[6]
self.sDB.rps_status.ex_hcool = rps_status[7]
self.sDB.rps_status.no_fuel = rps_status[8]
self.sDB.rps_status.fault = rps_status[9]
self.sDB.rps_status.timeout = rps_status[10]
self.sDB.rps_status.manual = rps_status[11]
self.sDB.rps_status.automatic = rps_status[12]
self.sDB.rps_status.sys_fail = rps_status[13]
self.sDB.rps_status.force_dis = rps_status[14]
local rps = self.sDB.rps_status
self.sDB.rps_tripped = rps_status[1]
self.sDB.rps_trip_cause = rps_status[2]
rps.high_dmg = rps_status[3]
rps.high_temp = rps_status[4]
rps.low_cool = rps_status[5]
rps.ex_waste = rps_status[6]
rps.ex_hcool = rps_status[7]
rps.no_fuel = rps_status[8]
rps.fault = rps_status[9]
rps.timeout = rps_status[10]
rps.manual = rps_status[11]
rps.automatic = rps_status[12]
rps.sys_fail = rps_status[13]
rps.force_dis = rps_status[14]
end
-- copy in the reactor status
---@param mek_data table
local function _copy_status(mek_data)
local stat = self.sDB.mek_status
local struct = self.sDB.mek_struct
-- copy status information
self.sDB.mek_status.status = mek_data[1]
self.sDB.mek_status.burn_rate = mek_data[2]
self.sDB.mek_status.act_burn_rate = mek_data[3]
self.sDB.mek_status.temp = mek_data[4]
self.sDB.mek_status.damage = mek_data[5]
self.sDB.mek_status.boil_eff = mek_data[6]
self.sDB.mek_status.env_loss = mek_data[7]
stat.status = mek_data[1]
stat.burn_rate = mek_data[2]
stat.act_burn_rate = mek_data[3]
stat.temp = mek_data[4]
stat.damage = mek_data[5]
stat.boil_eff = mek_data[6]
stat.env_loss = mek_data[7]
-- copy container information
self.sDB.mek_status.fuel = mek_data[8]
self.sDB.mek_status.fuel_fill = mek_data[9]
self.sDB.mek_status.waste = mek_data[10]
self.sDB.mek_status.waste_fill = mek_data[11]
self.sDB.mek_status.ccool_type = mek_data[12]
self.sDB.mek_status.ccool_amnt = mek_data[13]
self.sDB.mek_status.ccool_fill = mek_data[14]
self.sDB.mek_status.hcool_type = mek_data[15]
self.sDB.mek_status.hcool_amnt = mek_data[16]
self.sDB.mek_status.hcool_fill = mek_data[17]
stat.fuel = mek_data[8]
stat.fuel_fill = mek_data[9]
stat.waste = mek_data[10]
stat.waste_fill = mek_data[11]
stat.ccool_type = mek_data[12]
stat.ccool_amnt = mek_data[13]
stat.ccool_fill = mek_data[14]
stat.hcool_type = mek_data[15]
stat.hcool_amnt = mek_data[16]
stat.hcool_fill = mek_data[17]
-- update computable fields if we have our structure
if self.received_struct then
self.sDB.mek_status.fuel_need = self.sDB.mek_struct.fuel_cap - self.sDB.mek_status.fuel_fill
self.sDB.mek_status.waste_need = self.sDB.mek_struct.waste_cap - self.sDB.mek_status.waste_fill
self.sDB.mek_status.cool_need = self.sDB.mek_struct.ccool_cap - self.sDB.mek_status.ccool_fill
self.sDB.mek_status.hcool_need = self.sDB.mek_struct.hcool_cap - self.sDB.mek_status.hcool_fill
stat.fuel_need = struct.fuel_cap - stat.fuel_fill
stat.waste_need = struct.waste_cap - stat.waste_fill
stat.cool_need = struct.ccool_cap - stat.ccool_fill
stat.hcool_need = struct.hcool_cap - stat.hcool_fill
end
end
-- copy in the reactor structure
---@param mek_data table
local function _copy_struct(mek_data)
self.sDB.mek_struct.length = mek_data[1]
self.sDB.mek_struct.width = mek_data[2]
self.sDB.mek_struct.height = mek_data[3]
self.sDB.mek_struct.min_pos = mek_data[4]
self.sDB.mek_struct.max_pos = mek_data[5]
self.sDB.mek_struct.heat_cap = mek_data[6]
self.sDB.mek_struct.fuel_asm = mek_data[7]
self.sDB.mek_struct.fuel_sa = mek_data[8]
self.sDB.mek_struct.fuel_cap = mek_data[9]
self.sDB.mek_struct.waste_cap = mek_data[10]
self.sDB.mek_struct.ccool_cap = mek_data[11]
self.sDB.mek_struct.hcool_cap = mek_data[12]
self.sDB.mek_struct.max_burn = mek_data[13]
local struct = self.sDB.mek_struct
struct.length = mek_data[1]
struct.width = mek_data[2]
struct.height = mek_data[3]
struct.min_pos = mek_data[4]
struct.max_pos = mek_data[5]
struct.heat_cap = mek_data[6]
struct.fuel_asm = mek_data[7]
struct.fuel_sa = mek_data[8]
struct.fuel_cap = mek_data[9]
struct.waste_cap = mek_data[10]
struct.ccool_cap = mek_data[11]
struct.hcool_cap = mek_data[12]
struct.max_burn = mek_data[13]
end
-- handle a reactor status packet
---@param pkt rplc_frame
local function _handle_status(pkt)
local valid = (type(pkt.data[1]) == "number") and (type(pkt.data[2]) == "boolean") and
(type(pkt.data[3]) == "boolean") and (type(pkt.data[4]) == "boolean") and
(type(pkt.data[5]) == "number")
if valid then
self.sDB.last_status_update = pkt.data[1]
self.sDB.control_state = pkt.data[2]
self.sDB.no_reactor = pkt.data[3]
self.sDB.formed = pkt.data[4]
self.sDB.auto_ack_token = pkt.data[5]
if (not self.sDB.no_reactor) and self.sDB.formed and (type(pkt.data[6]) == "number") then
self.sDB.mek_status.heating_rate = pkt.data[6] or 0.0
-- attempt to read mek_data table
if type(pkt.data[7]) == "table" then
if #pkt.data[7] == 17 then
_copy_status(pkt.data[7])
self.received_status_cache = true
else
log.error(log_tag .. "RPLC status packet reactor data length mismatch")
end
end
end
else
log.debug(log_tag .. "RPLC status packet invalid")
end
end
-- mark this PLC session as closed, stop watchdog
@@ -300,7 +341,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
if pkt.length == 1 then
return pkt.data[1]
else
log.debug(log_header .. "RPLC ACK length mismatch")
log.debug(log_tag .. "RPLC ACK length mismatch")
return nil
end
end
@@ -309,13 +350,11 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
---@param pkt mgmt_frame|rplc_frame
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return
else
self.r_seq_num = pkt.scada_frame.seq_num()
self.r_seq_num = pkt.scada_frame.seq_num() + 1
end
-- process packet
@@ -323,7 +362,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
---@cast pkt rplc_frame
-- check reactor ID
if pkt.id ~= reactor_id then
log.warning(log_header .. "discarding RPLC packet with ID not matching reactor ID: reactor " .. reactor_id .. " != " .. pkt.id)
log.warning(log_tag .. "discarding RPLC packet with ID not matching reactor ID: reactor " .. reactor_id .. " != " .. pkt.id)
return
end
@@ -334,50 +373,19 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
if pkt.type == RPLC_TYPE.STATUS then
-- status packet received, update data
if pkt.length >= 5 then
if (type(pkt.data[1]) == "number") and (type(pkt.data[2]) == "boolean") and (type(pkt.data[3]) == "boolean") and
(type(pkt.data[4]) == "boolean") and (type(pkt.data[5]) == "number") then
self.sDB.last_status_update = pkt.data[1]
self.sDB.control_state = pkt.data[2]
self.sDB.no_reactor = pkt.data[3]
self.sDB.formed = pkt.data[4]
self.sDB.auto_ack_token = pkt.data[5]
if (not self.sDB.no_reactor) and self.sDB.formed and (type(pkt.data[6]) == "number") then
self.sDB.mek_status.heating_rate = pkt.data[6] or 0.0
-- attempt to read mek_data table
if type(pkt.data[7]) == "table" then
local status = pcall(_copy_status, pkt.data[7])
if status then
-- copied in status data OK
self.received_status_cache = true
else
-- error copying status data
log.error(log_header .. "failed to parse status packet data")
end
end
end
else
log.debug(log_header .. "RPLC status packet invalid")
end
_handle_status(pkt)
else
log.debug(log_header .. "RPLC status packet length mismatch")
log.debug(log_tag .. "RPLC status packet length mismatch")
end
elseif pkt.type == RPLC_TYPE.MEK_STRUCT then
-- received reactor structure, record it
if pkt.length == 14 then
local status = pcall(_copy_struct, pkt.data)
if status then
-- copied in structure data OK
_compute_op_temps()
self.received_struct = true
out_queue.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, reactor_id)
else
-- error copying structure data
log.error(log_header .. "failed to parse struct packet data")
end
if pkt.length == 13 then
_copy_struct(pkt.data)
_compute_op_temps()
self.received_struct = true
out_queue.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, reactor_id)
else
log.debug(log_header .. "RPLC struct packet length mismatch")
log.debug(log_tag .. "RPLC struct packet length mismatch")
end
elseif pkt.type == RPLC_TYPE.MEK_BURN_RATE then
-- burn rate acknowledgement
@@ -385,7 +393,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
if ack then
self.acks.burn_rate = true
elseif ack == false then
log.debug(log_header .. "burn rate update failed!")
log.debug(log_tag .. "burn rate update failed!")
end
-- send acknowledgement to coordinator
@@ -400,7 +408,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
if ack then
self.sDB.control_state = true
elseif ack == false then
log.debug(log_header .. "enable failed!")
log.debug(log_tag .. "enable failed!")
end
-- send acknowledgement to coordinator
@@ -416,7 +424,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
self.acks.disable = true
self.sDB.control_state = false
elseif ack == false then
log.debug(log_header .. "disable failed!")
log.debug(log_tag .. "disable failed!")
end
elseif pkt.type == RPLC_TYPE.RPS_SCRAM then
-- manual SCRAM acknowledgement
@@ -425,7 +433,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
self.acks.scram = true
self.sDB.control_state = false
elseif ack == false then
log.debug(log_header .. "manual SCRAM failed!")
log.debug(log_tag .. "manual SCRAM failed!")
end
-- send acknowledgement to coordinator
@@ -441,7 +449,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
self.acks.ascram = true
self.sDB.control_state = false
elseif ack == false then
log.debug(log_header .. " automatic SCRAM failed!")
log.debug(log_tag .. " automatic SCRAM failed!")
end
elseif pkt.type == RPLC_TYPE.RPS_STATUS then
-- RPS status packet received, copy data
@@ -451,10 +459,10 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
-- copied in RPS status data OK
else
-- error copying RPS status data
log.error(log_header .. "failed to parse RPS status packet data")
log.error(log_tag .. "failed to parse RPS status packet data")
end
else
log.debug(log_header .. "RPLC RPS status packet length mismatch")
log.debug(log_tag .. "RPLC RPS status packet length mismatch")
end
elseif pkt.type == RPLC_TYPE.RPS_ALARM then
-- RPS alarm
@@ -464,10 +472,10 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
-- copied in RPS status data OK
else
-- error copying RPS status data
log.error(log_header .. "failed to parse RPS alarm status data")
log.error(log_tag .. "failed to parse RPS alarm status data")
end
else
log.debug(log_header .. "RPLC RPS alarm packet length mismatch")
log.debug(log_tag .. "RPLC RPS alarm packet length mismatch")
end
elseif pkt.type == RPLC_TYPE.RPS_RESET then
-- RPS reset acknowledgement
@@ -477,7 +485,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
self.sDB.rps_tripped = false
self.sDB.rps_trip_cause = "ok"
elseif ack == false then
log.debug(log_header .. "RPS reset failed")
log.debug(log_tag .. "RPS reset failed")
end
-- send acknowledgement to coordinator
@@ -490,7 +498,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
-- RPS auto control reset acknowledgement
local ack = _get_ack(pkt)
if not ack then
log.debug(log_header .. "RPS auto reset failed")
log.debug(log_tag .. "RPS auto reset failed")
end
elseif pkt.type == RPLC_TYPE.AUTO_BURN_RATE then
if pkt.length == 1 then
@@ -498,18 +506,18 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
if ack == PLC_AUTO_ACK.FAIL then
self.acks.burn_rate = false
log.debug(log_header .. "RPLC automatic burn rate set fail")
log.debug(log_tag .. "RPLC automatic burn rate set fail")
elseif ack == PLC_AUTO_ACK.DIRECT_SET_OK or ack == PLC_AUTO_ACK.RAMP_SET_OK or ack == PLC_AUTO_ACK.ZERO_DIS_OK then
self.acks.burn_rate = true
else
self.acks.burn_rate = false
log.debug(log_header .. "RPLC automatic burn rate ack unknown")
log.debug(log_tag .. "RPLC automatic burn rate ack unknown")
end
else
log.debug(log_header .. "RPLC automatic burn rate ack packet length mismatch")
log.debug(log_tag .. "RPLC automatic burn rate ack packet length mismatch")
end
else
log.debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type)
log.debug(log_tag .. "handler received unsupported RPLC packet type " .. pkt.type)
end
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
---@cast pkt mgmt_frame
@@ -522,7 +530,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then
log.warning(log_header .. "PLC KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
log.warning(log_tag .. "PLC KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
end
-- log.debug(log_header .. "PLC RTT = " .. self.last_rtt .. "ms")
@@ -530,13 +538,17 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
databus.tx_plc_rtt(reactor_id, self.last_rtt)
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
end
elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session
_close()
elseif pkt.type == MGMT_TYPE.ESTABLISH then
-- something is wrong, kill the session
_close()
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end
end
end
@@ -631,7 +643,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
_close()
_send_mgmt(MGMT_TYPE.CLOSE, {})
println("connection to reactor " .. reactor_id .. " PLC closed by server")
log.info(log_header .. "session closed by server")
log.info(log_tag .. "session closed by server")
end
-- iterate the session
@@ -688,7 +700,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
_send(RPLC_TYPE.RPS_AUTO_RESET, {})
end
else
log.error(log_header .. "unsupported command received in in_queue (this is a bug)", true)
log.error(log_tag .. "unsupported command received in in_queue (this is a bug)", true)
end
elseif message.qtype == mqueue.TYPE.DATA then
-- instruction with body
@@ -737,14 +749,14 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
end
end
else
log.error(log_header .. "unsupported data command received in in_queue (this is a bug)", true)
log.error(log_tag .. "unsupported data command received in in_queue (this is a bug)", true)
end
end
end
-- max 100ms spent processing queue
if util.time() - handle_start > 100 then
log.warning(log_header .. "exceeded 100ms queue process limit")
log.warning(log_tag .. "exceeded 100ms queue process limit")
break
end
end
@@ -752,7 +764,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
-- exit if connection was closed
if not self.connected then
println("connection to reactor " .. reactor_id .. " PLC closed by remote host")
log.info(log_header .. "session closed by remote host")
log.info(log_tag .. "session closed by remote host")
return self.connected
end
@@ -794,7 +806,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
if not self.received_status_cache then
if rtimes.status_req - util.time() <= 0 then
_send(RPLC_TYPE.MEK_STATUS, {})
_send(RPLC_TYPE.STATUS, {})
rtimes.status_req = util.time() + RETRY_PERIOD
end
end

View File

@@ -30,21 +30,22 @@ local PERIODICS = {
---@nodiscard
---@param id integer session ID
---@param s_addr integer device source address
---@param i_seq_num integer initial sequence number
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
---@param facility facility facility data table
---@param fp_ok boolean if the front panel UI is running
function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility, fp_ok)
function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, facility, fp_ok)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end
local log_header = "pdg_session(" .. id .. "): "
local log_tag = "pdg_session(" .. id .. "): "
local self = {
-- connection properties
seq_num = 0,
r_seq_num = nil,
seq_num = i_seq_num + 2, -- next after the establish approval was sent
r_seq_num = i_seq_num + 1,
connected = true,
conn_watchdog = util.new_watchdog(timeout),
last_rtt = 0,
@@ -93,13 +94,11 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility,
---@param pkt mgmt_frame
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return
else
self.r_seq_num = pkt.scada_frame.seq_num()
self.r_seq_num = pkt.scada_frame.seq_num() + 1
end
-- feed watchdog
@@ -117,7 +116,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility,
self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then
log.warning(log_header .. "PDG KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
log.warning(log_tag .. "PDG KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
end
-- log.debug(log_header .. "PDG RTT = " .. self.last_rtt .. "ms")
@@ -125,11 +124,15 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility,
databus.tx_pdg_rtt(id, self.last_rtt)
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
end
elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session
_close()
elseif pkt.type == MGMT_TYPE.ESTABLISH then
-- something is wrong, kill the session
_close()
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
elseif pkt.type == MGMT_TYPE.DIAG_TONE_GET then
-- get the state of alarm tones
_send_mgmt(MGMT_TYPE.DIAG_TONE_GET, facility.get_alarm_tones())
@@ -146,13 +149,13 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility,
local allow_testing, test_tone_states = facility.diag_set_test_tone(pkt.data[1], pkt.data[2])
_send_mgmt(MGMT_TYPE.DIAG_TONE_SET, { allow_testing, test_tone_states })
else
log.debug(log_header .. "SCADA diag tone set packet data type mismatch")
log.debug(log_tag .. "SCADA diag tone set packet data type mismatch")
end
else
log.debug(log_header .. "SCADA diag tone set packet length mismatch")
log.debug(log_tag .. "SCADA diag tone set packet length mismatch")
end
else
log.debug(log_header .. "DIAG_TONE_SET is blocked without HMAC for security")
log.debug(log_tag .. "DIAG_TONE_SET is blocked without HMAC for security")
end
if not valid then _send_mgmt(MGMT_TYPE.DIAG_TONE_SET, { false }) end
@@ -169,18 +172,18 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility,
local allow_testing, test_alarm_states = facility.diag_set_test_alarm(pkt.data[1], pkt.data[2])
_send_mgmt(MGMT_TYPE.DIAG_ALARM_SET, { allow_testing, test_alarm_states })
else
log.debug(log_header .. "SCADA diag alarm set packet data type mismatch")
log.debug(log_tag .. "SCADA diag alarm set packet data type mismatch")
end
else
log.debug(log_header .. "SCADA diag alarm set packet length mismatch")
log.debug(log_tag .. "SCADA diag alarm set packet length mismatch")
end
else
log.debug(log_header .. "DIAG_ALARM_SET is blocked without HMAC for security")
log.debug(log_tag .. "DIAG_ALARM_SET is blocked without HMAC for security")
end
if not valid then _send_mgmt(MGMT_TYPE.DIAG_ALARM_SET, { false }) end
else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end
end
end
@@ -206,7 +209,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility,
_close()
_send_mgmt(MGMT_TYPE.CLOSE, {})
println("connection to pocket diag session " .. id .. " closed by server")
log.info(log_header .. "session closed by server")
log.info(log_tag .. "session closed by server")
end
-- iterate the session
@@ -237,7 +240,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility,
-- max 100ms spent processing queue
if util.time() - handle_start > 100 then
log.warning(log_header .. "exceeded 100ms queue process limit")
log.warning(log_tag .. "exceeded 100ms queue process limit")
break
end
end
@@ -245,7 +248,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility,
-- exit if connection was closed
if not self.connected then
println("connection to pocket diag session " .. id .. " closed by remote host")
log.info(log_header .. "session closed by remote host")
log.info(log_tag .. "session closed by remote host")
return self.connected
end

View File

@@ -30,29 +30,30 @@ local PERIODICS = {
ALARM_TONES = 500
}
-- create a new RTU session
-- create a new RTU gateway session
---@nodiscard
---@param id integer session ID
---@param s_addr integer device source address
---@param i_seq_num integer initial sequence number
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
---@param advertisement table RTU device advertisement
---@param advertisement table RTU gateway device advertisement
---@param facility facility facility data table
---@param fp_ok boolean if the front panel UI is running
function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement, facility, fp_ok)
function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, advertisement, facility, fp_ok)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end
local log_header = "rtu_session(" .. id .. "): "
local log_tag = "rtu_gw_session(" .. id .. "): "
local self = {
modbus_q = mqueue.new(),
advert = advertisement,
fac_units = facility.get_units(),
-- connection properties
seq_num = 0,
r_seq_num = nil,
seq_num = i_seq_num + 2, -- next after the establish approval was sent
r_seq_num = i_seq_num + 1,
connected = true,
conn_watchdog = util.new_watchdog(timeout),
last_rtt = 0,
@@ -123,7 +124,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
if u_type == false then
-- validation fail
log.debug(log_header .. "_handle_advertisement(): advertisement unit validation failure")
log.debug(log_tag .. "_handle_advertisement(): advertisement unit validation failure")
else
if unit_advert.reactor > 0 then
local target_unit = self.fac_units[unit_advert.reactor] ---@type reactor_unit
@@ -155,9 +156,9 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
if type(unit) ~= "nil" then target_unit.add_envd(unit) end
elseif u_type == RTU_UNIT_TYPE.VIRTUAL then
-- skip virtual units
log.debug(util.c(log_header, "skipping virtual RTU unit #", i))
log.debug(util.c(log_tag, "skipping virtual RTU #", i))
else
log.warning(util.c(log_header, "_handle_advertisement(): encountered unsupported reactor-specific RTU type ", type_string))
log.warning(util.c(log_tag, "_handle_advertisement(): encountered unsupported reactor-specific RTU type ", type_string))
end
else
-- facility RTUs
@@ -183,9 +184,9 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
if type(unit) ~= "nil" then facility.add_envd(unit) end
elseif u_type == RTU_UNIT_TYPE.VIRTUAL then
-- skip virtual units
log.debug(util.c(log_header, "skipping virtual RTU unit #", i))
log.debug(util.c(log_tag, "skipping virtual RTU #", i))
else
log.warning(util.c(log_header, "_handle_advertisement(): encountered unsupported facility RTU type ", type_string))
log.warning(util.c(log_tag, "_handle_advertisement(): encountered unsupported facility RTU type ", type_string))
end
end
end
@@ -194,20 +195,20 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
self.units[i] = unit
unit_count = unit_count + 1
elseif u_type ~= RTU_UNIT_TYPE.VIRTUAL then
log.warning(util.c(log_header, "_handle_advertisement(): problem occured while creating a unit (type is ", type_string, ")"))
log.warning(util.c(log_tag, "_handle_advertisement(): problem occured while creating a unit (type is ", type_string, ")"))
end
end
databus.tx_rtu_units(id, unit_count)
end
-- mark this RTU session as closed, stop watchdog
-- mark this RTU gateway session as closed, stop watchdog
local function _close()
self.conn_watchdog.cancel()
self.connected = false
databus.tx_rtu_disconnected(id)
-- mark all RTU unit sessions as closed so the reactor unit knows
-- mark all RTU sessions as closed so the reactor unit knows
for _, unit in pairs(self.units) do unit.close() end
end
@@ -240,13 +241,11 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
---@param pkt modbus_frame|mgmt_frame
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
elseif (self.r_seq_num + 1) ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return
else
self.r_seq_num = pkt.scada_frame.seq_num()
self.r_seq_num = pkt.scada_frame.seq_num() + 1
end
-- feed watchdog
@@ -266,27 +265,31 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
-- keep alive reply
if pkt.length == 2 then
local srv_start = pkt.data[1]
-- local rtu_send = pkt.data[2]
-- local rtu_gw_send = pkt.data[2]
local srv_now = util.time()
self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then
log.warning(log_header .. "RTU KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
log.warning(log_tag .. "RTU GW KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
end
-- log.debug(log_header .. "RTU RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "RTU TT = " .. (srv_now - rtu_send) .. "ms")
-- log.debug(log_tag .. "RTU GW RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_tag .. "RTU GW TT = " .. (srv_now - rtu_gw_send) .. "ms")
databus.tx_rtu_rtt(id, self.last_rtt)
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
end
elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session
_close()
elseif pkt.type == MGMT_TYPE.ESTABLISH then
-- something is wrong, kill the session
_close()
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
elseif pkt.type == MGMT_TYPE.RTU_ADVERT then
-- RTU unit advertisement
log.debug(log_header .. "received updated advertisement")
-- RTU advertisement
log.debug(log_tag .. "received updated advertisement")
self.advert = pkt.data
-- handle advertisement; this will re-create all unit sub-sessions
@@ -299,17 +302,17 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
unit.invalidate_cache()
end
else
log.debug(log_header .. "SCADA RTU device re-mount packet length mismatch")
log.debug(log_tag .. "SCADA RTU GW device re-mount packet length mismatch")
end
else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end
end
end
-- PUBLIC FUNCTIONS --
-- get the session ID
-- get the gateway session ID
function public.get_id() return id end
-- check if a timer matches this session's watchdog
@@ -323,8 +326,8 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
function public.close()
_close()
_send_mgmt(MGMT_TYPE.CLOSE, {})
println(log_header .. "connection to RTU closed by server")
log.info(log_header .. "session closed by server")
println(log_tag .. "connection to RTU GW closed by server")
log.info(log_tag .. "session closed by server")
end
-- iterate the session
@@ -355,7 +358,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
-- max 100ms spent processing queue
if util.time() - handle_start > 100 then
log.warning(log_header .. "exceeded 100ms queue process limit")
log.warning(log_tag .. "exceeded 100ms queue process limit")
break
end
end
@@ -363,7 +366,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
-- exit if connection was closed
if not self.connected then
println("RTU connection " .. id .. " closed by remote host")
log.info(log_header .. "session closed by remote host")
log.info(log_tag .. "session closed by remote host")
return self.connected
end

View File

@@ -32,10 +32,10 @@ local PERIODICS = {
-- create a new boilerv rtu session runner
---@nodiscard
---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID
---@param session_id integer RTU gateway session ID
---@param unit_id integer RTU ID
---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue
---@param out_queue mqueue RTU message out queue
function boilerv.new(session_id, unit_id, advert, out_queue)
-- checks
if advert.type ~= RTU_UNIT_TYPE.BOILER_VALVE then

View File

@@ -44,10 +44,10 @@ local PERIODICS = {
-- create a new dynamicv rtu session runner
---@nodiscard
---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID
---@param session_id integer RTU gateway session ID
---@param unit_id integer RTU ID
---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue
---@param out_queue mqueue RTU message out queue
function dynamicv.new(session_id, unit_id, advert, out_queue)
-- checks
if advert.type ~= RTU_UNIT_TYPE.DYNAMIC_VALVE then

View File

@@ -23,10 +23,10 @@ local PERIODICS = {
-- create a new environment detector rtu session runner
---@nodiscard
---@param session_id integer
---@param unit_id integer
---@param advert rtu_advertisement
---@param out_queue mqueue
---@param session_id integer RTU gateway session ID
---@param unit_id integer RTU ID
---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU message out queue
function envd.new(session_id, unit_id, advert, out_queue)
-- checks
if advert.type ~= RTU_UNIT_TYPE.ENV_DETECTOR then

View File

@@ -32,10 +32,10 @@ local PERIODICS = {
-- create a new imatrix rtu session runner
---@nodiscard
---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID
---@param session_id integer RTU gateway session ID
---@param unit_id integer RTU ID
---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue
---@param out_queue mqueue RTU message out queue
function imatrix.new(session_id, unit_id, advert, out_queue)
-- checks
if advert.type ~= RTU_UNIT_TYPE.IMATRIX then

View File

@@ -45,10 +45,10 @@ local PERIODICS = {
-- create a new redstone rtu session runner
---@nodiscard
---@param session_id integer
---@param unit_id integer
---@param advert rtu_advertisement
---@param out_queue mqueue
---@param session_id integer RTU gateway session ID
---@param unit_id integer RTU ID
---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU message out queue
function redstone.new(session_id, unit_id, advert, out_queue)
-- type check
if advert.type ~= RTU_UNIT_TYPE.REDSTONE then

View File

@@ -29,10 +29,10 @@ local PERIODICS = {
-- create a new sna rtu session runner
---@nodiscard
---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID
---@param session_id integer RTU gateway session ID
---@param unit_id integer RTU ID
---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue
---@param out_queue mqueue RTU message out queue
function sna.new(session_id, unit_id, advert, out_queue)
-- type check
if advert.type ~= RTU_UNIT_TYPE.SNA then

View File

@@ -32,10 +32,10 @@ local PERIODICS = {
-- create a new sps rtu session runner
---@nodiscard
---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID
---@param session_id integer RTU gateway session ID
---@param unit_id integer RTU ID
---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue
---@param out_queue mqueue RTU message out queue
function sps.new(session_id, unit_id, advert, out_queue)
-- type check
if advert.type ~= RTU_UNIT_TYPE.SPS then

View File

@@ -44,10 +44,10 @@ local PERIODICS = {
-- create a new turbinev rtu session runner
---@nodiscard
---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID
---@param session_id integer RTU gateway session ID
---@param unit_id integer RTU ID
---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue
---@param out_queue mqueue RTU message out queue
function turbinev.new(session_id, unit_id, advert, out_queue)
-- checks
if advert.type ~= RTU_UNIT_TYPE.TURBINE_VALVE then

View File

@@ -24,7 +24,7 @@ unit_session.RTU_US_DATA = RTU_US_DATA
-- create a new unit session runner
---@nodiscard
---@param session_id integer RTU session ID
---@param session_id integer RTU gateway session ID
---@param unit_id integer MODBUS unit ID
---@param advert rtu_advertisement RTU advertisement for this unit
---@param out_queue mqueue send queue
@@ -144,12 +144,15 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
-- PUBLIC FUNCTIONS --
-- get the unit ID
-- get the RTU gateway session ID
---@nodiscard
function public.get_session_id() return session_id end
-- get the unit ID
---@nodiscard
function public.get_unit_id() return unit_id end
-- get the RTU type
---@nodiscard
function public.get_unit_type() return advert.type end
-- get the device index
---@nodiscard
function public.get_device_idx() return self.device_index or 0 end

View File

@@ -1,9 +1,15 @@
--
-- Supervisor Sessions Handler
--
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util")
local databus = require("supervisor.databus")
local facility = require("supervisor.facility")
local pgi = require("supervisor.panel.pgi")
local coordinator = require("supervisor.session.coordinator")
local plc = require("supervisor.session.plc")
@@ -11,13 +17,15 @@ local pocket = require("supervisor.session.pocket")
local rtu = require("supervisor.session.rtu")
local svqtypes = require("supervisor.session.svqtypes")
-- Supervisor Sessions Handler
local RTU_ID_FAIL = types.RTU_ID_FAIL
local RTU_TYPES = types.RTU_UNIT_TYPE
local SV_Q_DATA = svqtypes.SV_Q_DATA
local SV_Q_DATA = svqtypes.SV_Q_DATA
local PLC_S_CMDS = plc.PLC_S_CMDS
local PLC_S_DATA = plc.PLC_S_DATA
local CRD_S_DATA = coordinator.CRD_S_DATA
local PLC_S_CMDS = plc.PLC_S_CMDS
local PLC_S_DATA = plc.PLC_S_DATA
local CRD_S_DATA = coordinator.CRD_S_DATA
local svsessions = {}
@@ -37,12 +45,13 @@ local self = {
config = nil, ---@type svr_config
facility = nil, ---@type facility|nil
sessions = { rtu = {}, plc = {}, crd = {}, pdg = {} },
next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 }
next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 },
dev_dbg = { duplicate = {}, out_of_range = {}, connected = {} }
}
---@alias sv_session_structs plc_session_struct|rtu_session_struct|crd_session_struct|pdg_session_struct
-- PRIVATE FUNCTIONS --
--#region PRIVATE FUNCTIONS
-- handle a session output queue
---@param session sv_session_structs
@@ -190,18 +199,184 @@ local function _find_session(list, s_addr)
return nil
end
-- PUBLIC FUNCTIONS --
-- periodically remove disconnected RTU gateway's RTU ID warnings and update the missing device list
local function _update_dev_dbg()
-- remove disconnected units from check failures lists
local f = function (unit) return unit.is_connected() end
util.filter_table(self.dev_dbg.duplicate, f, pgi.delete_chk_entry)
util.filter_table(self.dev_dbg.out_of_range, f, pgi.delete_chk_entry)
-- update missing list
local conns = self.dev_dbg.connected
local units = self.facility.get_units()
local rtu_conns = self.facility.check_rtu_conns()
local function report(disconnected, msg)
if disconnected then pgi.create_missing_entry(msg) else pgi.delete_missing_entry(msg) end
end
-- look for disconnected facility RTUs
if rtu_conns.induction ~= conns.induction then
report(conns.induction, util.c("the facility's induction matrix"))
conns.induction = rtu_conns.induction
end
if rtu_conns.sps ~= conns.sps then
report(conns.sps, util.c("the facility's SPS"))
conns.sps = rtu_conns.sps
end
for i = 1, #conns.tanks do
if (rtu_conns.tanks[i] or false) ~= conns.tanks[i] then
report(conns.tanks[i], util.c("the facility's #", i, " dynamic tank"))
conns.tanks[i] = rtu_conns.tanks[i] or false
end
end
-- look for disconnected unit RTUs
for u = 1, #units do
local u_conns = conns.units[u]
rtu_conns = units[u].check_rtu_conns()
for i = 1, #u_conns.boilers do
if (rtu_conns.boilers[i] or false) ~= u_conns.boilers[i] then
report(u_conns.boilers[i], util.c("unit ", u, "'s #", i, " boiler"))
u_conns.boilers[i] = rtu_conns.boilers[i] or false
end
end
for i = 1, #u_conns.turbines do
if (rtu_conns.turbines[i] or false) ~= u_conns.turbines[i] then
report(u_conns.turbines[i], util.c("unit ", u, "'s #", i, " turbine"))
u_conns.turbines[i] = rtu_conns.turbines[i] or false
end
end
for i = 1, #u_conns.tanks do
if (rtu_conns.tanks[i] or false) ~= u_conns.tanks[i] then
report(u_conns.tanks[i], util.c("unit ", u, "'s dynamic tank"))
u_conns.tanks[i] = rtu_conns.tanks[i] or false
end
end
end
end
--#endregion
--#region PUBLIC FUNCTIONS
-- on attempted link of an RTU to a facility or unit object, verify its ID and report a problem if it can't be accepted
---@param unit unit_session RTU session
---@param list table table of RTU sessions
---@param max integer max of this type of RTU
---@return RTU_ID_FAIL fail_code, string fail_str
function svsessions.check_rtu_id(unit, list, max)
local fail_code, fail_str = RTU_ID_FAIL.OK, "OK"
if (unit.get_device_idx() < 1 and max ~= 1) or unit.get_device_idx() > max then
-- out-of-range
fail_code, fail_str = RTU_ID_FAIL.OUT_OF_RANGE, "index out of range"
table.insert(self.dev_dbg.out_of_range, unit)
else
for _, u in ipairs(list) do
if u.get_device_idx() == unit.get_device_idx() then
-- duplicate
fail_code, fail_str = RTU_ID_FAIL.DUPLICATE, "duplicate index"
table.insert(self.dev_dbg.duplicate, unit)
break
end
end
end
-- make sure this won't exceed the maximum allowable devices
if fail_code == RTU_ID_FAIL.OK and #list >= max then
fail_code, fail_str = RTU_ID_FAIL.MAX_DEVICES, "too many of this type"
end
-- add to the list for the user
if fail_code ~= RTU_ID_FAIL.OK and fail_code ~= RTU_ID_FAIL.MAX_DEVICES then
local r_id, idx, type = unit.get_reactor(), unit.get_device_idx(), unit.get_unit_type()
local msg
if r_id == 0 then
msg = "the facility's "
if type == RTU_TYPES.IMATRIX then
msg = msg .. "induction matrix"
elseif type == RTU_TYPES.SPS then
msg = msg .. "SPS"
elseif type == RTU_TYPES.DYNAMIC_VALVE then
msg = util.c(msg, "#", idx, " dynamic tank")
elseif type == RTU_TYPES.ENV_DETECTOR then
msg = util.c(msg, "#", idx, " env. detector")
else
msg = msg .. " ? (error)"
end
else
msg = util.c("unit ", r_id, "'s ")
if type == RTU_TYPES.BOILER_VALVE then
msg = util.c(msg, "#", idx, " boiler")
elseif type == RTU_TYPES.TURBINE_VALVE then
msg = util.c(msg, "#", idx, " turbine")
elseif type == RTU_TYPES.DYNAMIC_VALVE then
msg = msg .. "dynamic tank"
elseif type == RTU_TYPES.ENV_DETECTOR then
msg = util.c(msg, "#", idx, " env. detector")
else
msg = msg .. " ? (error)"
end
end
pgi.create_chk_entry(unit, fail_code, msg)
end
return fail_code, fail_str
end
-- initialize svsessions
---@param nic nic network interface device
---@param fp_ok boolean front panel active
---@param config svr_config supervisor configuration
---@param cooling_conf sv_cooling_conf cooling configuration definition
function svsessions.init(nic, fp_ok, config, cooling_conf)
---@param facility facility
function svsessions.init(nic, fp_ok, config, facility)
self.nic = nic
self.fp_ok = fp_ok
self.config = config
self.facility = facility.new(config, cooling_conf)
self.facility = facility
-- initialize connection tracking table by setting all expected devices to true
-- if connections are missing, missing entries will then be created on the next update
self.dev_dbg.connected = { induction = true, sps = true, tanks = {}, units = {} }
local cool_conf = facility.get_cooling_conf()
for i = 1, #cool_conf.fac_tank_list do
if cool_conf.fac_tank_list[i] == 2 then
table.insert(self.dev_dbg.connected.tanks, true)
end
end
for i = 1, config.UnitCount do
local r_cool = cool_conf.r_cool[i]
local conns = { boilers = {}, turbines = {}, tanks = {} }
for b = 1, r_cool.BoilerCount do conns.boilers[b] = true end
for t = 1, r_cool.TurbineCount do conns.turbines[t] = true end
if r_cool.TankConnection and cool_conf.fac_tank_defs[i] == 1 then
conns.tanks[1] = true
end
self.dev_dbg.connected.units[i] = conns
end
end
-- find an RTU session by the computer ID
@@ -273,11 +448,12 @@ end
-- establish a new PLC session
---@nodiscard
---@param source_addr integer
---@param for_reactor integer
---@param version string
---@param source_addr integer PLC computer ID
---@param i_seq_num integer initial (most recent) sequence number
---@param for_reactor integer unit ID
---@param version string PLC version
---@return integer|false session_id
function svsessions.establish_plc_session(source_addr, for_reactor, version)
function svsessions.establish_plc_session(source_addr, i_seq_num, for_reactor, version)
if svsessions.get_reactor_session(for_reactor) == nil and for_reactor >= 1 and for_reactor <= self.config.UnitCount then
---@class plc_session_struct
local plc_s = {
@@ -294,7 +470,7 @@ function svsessions.establish_plc_session(source_addr, for_reactor, version)
local id = self.next_ids.plc
plc_s.instance = plc.new_session(id, source_addr, for_reactor, plc_s.in_queue, plc_s.out_queue, self.config.PLC_Timeout, self.fp_ok)
plc_s.instance = plc.new_session(id, source_addr, i_seq_num, for_reactor, plc_s.in_queue, plc_s.out_queue, self.config.PLC_Timeout, self.fp_ok)
table.insert(self.sessions.plc, plc_s)
local units = self.facility.get_units()
@@ -320,13 +496,14 @@ function svsessions.establish_plc_session(source_addr, for_reactor, version)
end
end
-- establish a new RTU session
-- establish a new RTU gateway session
---@nodiscard
---@param source_addr integer
---@param advertisement table
---@param version string
---@param source_addr integer RTU gateway computer ID
---@param i_seq_num integer initial (most recent) sequence number
---@param advertisement table RTU capability advertisement
---@param version string RTU gateway version
---@return integer session_id
function svsessions.establish_rtu_session(source_addr, advertisement, version)
function svsessions.establish_rtu_session(source_addr, i_seq_num, advertisement, version)
---@class rtu_session_struct
local rtu_s = {
s_type = "rtu",
@@ -341,7 +518,7 @@ function svsessions.establish_rtu_session(source_addr, advertisement, version)
local id = self.next_ids.rtu
rtu_s.instance = rtu.new_session(id, source_addr, rtu_s.in_queue, rtu_s.out_queue, self.config.RTU_Timeout, advertisement, self.facility, self.fp_ok)
rtu_s.instance = rtu.new_session(id, source_addr, i_seq_num, rtu_s.in_queue, rtu_s.out_queue, self.config.RTU_Timeout, advertisement, self.facility, self.fp_ok)
table.insert(self.sessions.rtu, rtu_s)
local mt = {
@@ -362,10 +539,11 @@ end
-- establish a new coordinator session
---@nodiscard
---@param source_addr integer
---@param version string
---@param source_addr integer coordinator computer ID
---@param i_seq_num integer initial (most recent) sequence number
---@param version string coordinator version
---@return integer|false session_id
function svsessions.establish_crd_session(source_addr, version)
function svsessions.establish_crd_session(source_addr, i_seq_num, version)
if svsessions.get_crd_session() == nil then
---@class crd_session_struct
local crd_s = {
@@ -381,7 +559,7 @@ function svsessions.establish_crd_session(source_addr, version)
local id = self.next_ids.crd
crd_s.instance = coordinator.new_session(id, source_addr, crd_s.in_queue, crd_s.out_queue, self.config.CRD_Timeout, self.facility, self.fp_ok)
crd_s.instance = coordinator.new_session(id, source_addr, i_seq_num, crd_s.in_queue, crd_s.out_queue, self.config.CRD_Timeout, self.facility, self.fp_ok)
table.insert(self.sessions.crd, crd_s)
local mt = {
@@ -406,10 +584,11 @@ end
-- establish a new pocket diagnostics session
---@nodiscard
---@param source_addr integer
---@param version string
---@param source_addr integer pocket computer ID
---@param i_seq_num integer initial (most recent) sequence number
---@param version string pocket version
---@return integer|false session_id
function svsessions.establish_pdg_session(source_addr, version)
function svsessions.establish_pdg_session(source_addr, i_seq_num, version)
---@class pdg_session_struct
local pdg_s = {
s_type = "pkt",
@@ -424,7 +603,7 @@ function svsessions.establish_pdg_session(source_addr, version)
local id = self.next_ids.pdg
pdg_s.instance = pocket.new_session(id, source_addr, pdg_s.in_queue, pdg_s.out_queue, self.config.PKT_Timeout, self.facility, self.fp_ok)
pdg_s.instance = pocket.new_session(id, source_addr, i_seq_num, pdg_s.in_queue, pdg_s.out_queue, self.config.PKT_Timeout, self.facility, self.fp_ok)
table.insert(self.sessions.pdg, pdg_s)
local mt = {
@@ -462,6 +641,9 @@ function svsessions.iterate_all()
-- iterate units
self.facility.update_units()
-- update tracking of bad RTU IDs and missing devices
_update_dev_dbg()
end
-- delete all closed sessions
@@ -478,4 +660,6 @@ function svsessions.close_all()
svsessions.free_all_closed()
end
--#endregion
return svsessions

View File

@@ -16,12 +16,13 @@ local core = require("graphics.core")
local configure = require("supervisor.configure")
local databus = require("supervisor.databus")
local facility = require("supervisor.facility")
local renderer = require("supervisor.renderer")
local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v1.3.11"
local SUPERVISOR_VERSION = "v1.5.2"
local println = util.println
local println_ts = util.println_ts
@@ -129,9 +130,12 @@ local function main()
println_ts = function (_) end
end
-- create facility and unit objects
local sv_facility = facility.new(config)
-- create network interface then setup comms
local nic = network.nic(modem)
local superv_comms = supervisor.comms(SUPERVISOR_VERSION, nic, fp_ok)
local superv_comms = supervisor.comms(SUPERVISOR_VERSION, nic, fp_ok, sv_facility)
-- base loop clock (6.67Hz, 3 ticks)
local MAIN_CLOCK = 0.15
@@ -214,7 +218,7 @@ local function main()
elseif event == "modem_message" then
-- got a packet
local packet = superv_comms.parse_packet(param1, param2, param3, param4, param5)
superv_comms.handle_packet(packet)
if packet then superv_comms.handle_packet(packet) end
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

View File

@@ -102,14 +102,12 @@ end
---@param _version string supervisor version
---@param nic nic network interface device
---@param fp_ok boolean if the front panel UI is running
---@param facility facility facility instance
---@diagnostic disable-next-line: unused-local
function supervisor.comms(_version, nic, fp_ok)
function supervisor.comms(_version, nic, fp_ok, facility)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end
---@class sv_cooling_conf
local cooling_conf = { r_cool = config.CoolingConfig, fac_tank_mode = config.FacilityTankMode, fac_tank_defs = config.FacilityTankDefs }
local self = {
last_est_acks = {}
}
@@ -122,8 +120,8 @@ function supervisor.comms(_version, nic, fp_ok)
nic.closeAll()
nic.open(config.SVR_Channel)
-- pass modem, status, and config data to svsessions
svsessions.init(nic, fp_ok, config, cooling_conf)
-- pass system data and objects to svsessions
svsessions.init(nic, fp_ok, config, facility)
-- send an establish request response
---@param packet scada_packet
@@ -191,283 +189,282 @@ function supervisor.comms(_version, nic, fp_ok)
end
-- handle a packet
---@param packet modbus_frame|rplc_frame|mgmt_frame|crdn_frame|nil
---@param packet modbus_frame|rplc_frame|mgmt_frame|crdn_frame
function public.handle_packet(packet)
if packet ~= nil then
local l_chan = packet.scada_frame.local_channel()
local r_chan = packet.scada_frame.remote_channel()
local src_addr = packet.scada_frame.src_addr()
local protocol = packet.scada_frame.protocol()
local l_chan = packet.scada_frame.local_channel()
local r_chan = packet.scada_frame.remote_channel()
local src_addr = packet.scada_frame.src_addr()
local protocol = packet.scada_frame.protocol()
local i_seq_num = packet.scada_frame.seq_num()
if l_chan ~= config.SVR_Channel then
log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == config.PLC_Channel then
-- look for an associated session
local session = svsessions.find_plc_session(src_addr)
if l_chan ~= config.SVR_Channel then
log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == config.PLC_Channel then
-- look for an associated session
local session = svsessions.find_plc_session(src_addr)
if protocol == PROTOCOL.RPLC then
---@cast packet rplc_frame
-- reactor PLC packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug("discarding RPLC packet without a known session")
end
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping PLC establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.PLC then
-- PLC linking request
if packet.length == 4 and type(packet.data[4]) == "number" then
local reactor_id = packet.data[4]
local plc_id = svsessions.establish_plc_session(src_addr, reactor_id, firmware_v)
if plc_id == false then
-- reactor already has a PLC assigned
if last_ack ~= ESTABLISH_ACK.COLLISION then
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION)
else
-- got an ID; assigned to a reactor successfully
println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \xbb reactor ", reactor_id, " connected"))
log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [@", src_addr, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
end
else
log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on PLC channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug("invalid establish packet (on PLC channel)")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding PLC SCADA_MGMT packet without a known session from computer ", src_addr))
end
if protocol == PROTOCOL.RPLC then
---@cast packet rplc_frame
-- reactor PLC packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
log.debug(util.c("illegal packet type ", protocol, " on PLC channel"))
-- any other packet should be session related, discard it
log.debug("discarding RPLC packet without a known session")
end
elseif r_chan == config.RTU_Channel then
-- look for an associated session
local session = svsessions.find_rtu_session(src_addr)
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
if protocol == PROTOCOL.MODBUS_TCP then
---@cast packet modbus_frame
-- MODBUS response
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug("discarding MODBUS_TCP packet without a known session")
end
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping RTU establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.RTU then
if packet.length == 4 then
-- this is an RTU advertisement for a new session
local rtu_advert = packet.data[4]
local s_id = svsessions.establish_rtu_session(src_addr, rtu_advert, firmware_v)
println(util.c("RTU (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("RTU_ESTABLISH: RTU (",firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
else
log.debug("RTU_ESTABLISH: packet length mismatch")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on RTU channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping PLC establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
else
log.debug("invalid establish packet (on RTU channel)")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding RTU SCADA_MGMT packet without a known session from computer ", src_addr))
end
else
log.debug(util.c("illegal packet type ", protocol, " on RTU channel"))
end
elseif r_chan == config.CRD_Channel then
-- look for an associated session
local session = svsessions.find_crd_session(src_addr)
if protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.PLC then
-- PLC linking request
if packet.length == 4 and type(packet.data[4]) == "number" then
local reactor_id = packet.data[4]
local plc_id = svsessions.establish_plc_session(src_addr, i_seq_num, reactor_id, firmware_v)
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping coordinator establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.CRD then
-- this is an attempt to establish a new coordinator session
local s_id = svsessions.establish_crd_session(src_addr, firmware_v)
if s_id ~= false then
println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("CRD_ESTABLISH: coordinator (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { config.UnitCount, cooling_conf })
else
if plc_id == false then
-- reactor already has a PLC assigned
if last_ack ~= ESTABLISH_ACK.COLLISION then
log.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator")
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION)
else
-- got an ID; assigned to a reactor successfully
println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \xbb reactor ", reactor_id, " connected"))
log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [@", src_addr, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
end
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on coordinator channel"))
log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug("CRD_ESTABLISH: establish packet length mismatch")
log.debug(util.c("illegal establish packet for device ", dev_type, " on PLC channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding coordinator SCADA_MGMT packet without a known session from computer ", src_addr))
end
elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
-- coordinator packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding coordinator SCADA_CRDN packet without a known session from computer ", src_addr))
log.debug("invalid establish packet (on PLC channel)")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug(util.c("illegal packet type ", protocol, " on coordinator channel"))
end
elseif r_chan == config.PKT_Channel then
-- look for an associated session
local session = svsessions.find_pdg_session(src_addr)
if protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping PDG establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.PKT then
-- this is an attempt to establish a new pocket diagnostic session
local s_id = svsessions.establish_pdg_session(src_addr, firmware_v)
println(util.c("PKT (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("PDG_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on pocket channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug("PDG_ESTABLISH: establish packet length mismatch")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding pocket SCADA_MGMT packet without a known session from computer ", src_addr))
end
elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
-- coordinator packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding pocket SCADA_CRDN packet without a known session from computer ", src_addr))
end
else
log.debug(util.c("illegal packet type ", protocol, " on pocket channel"))
-- any other packet should be session related, discard it
log.debug(util.c("discarding PLC SCADA_MGMT packet without a known session from computer ", src_addr))
end
else
log.debug("received packet for unknown channel " .. r_chan, true)
log.debug(util.c("illegal packet type ", protocol, " on PLC channel"))
end
elseif r_chan == config.RTU_Channel then
-- look for an associated session
local session = svsessions.find_rtu_session(src_addr)
if protocol == PROTOCOL.MODBUS_TCP then
---@cast packet modbus_frame
-- MODBUS response
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug("discarding MODBUS_TCP packet without a known session")
end
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping RTU establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.RTU then
if packet.length == 4 then
-- this is an RTU advertisement for a new session
local rtu_advert = packet.data[4]
local s_id = svsessions.establish_rtu_session(src_addr, i_seq_num, rtu_advert, firmware_v)
println(util.c("RTU (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("RTU_ESTABLISH: RTU (",firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
else
log.debug("RTU_ESTABLISH: packet length mismatch")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on RTU channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug("invalid establish packet (on RTU channel)")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding RTU SCADA_MGMT packet without a known session from computer ", src_addr))
end
else
log.debug(util.c("illegal packet type ", protocol, " on RTU channel"))
end
elseif r_chan == config.CRD_Channel then
-- look for an associated session
local session = svsessions.find_crd_session(src_addr)
if protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping coordinator establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.CRD then
-- this is an attempt to establish a new coordinator session
local s_id = svsessions.establish_crd_session(src_addr, i_seq_num, firmware_v)
if s_id ~= false then
println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("CRD_ESTABLISH: coordinator (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { config.UnitCount, facility.get_cooling_conf() })
else
if last_ack ~= ESTABLISH_ACK.COLLISION then
log.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator")
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION)
end
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on coordinator channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug("CRD_ESTABLISH: establish packet length mismatch")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding coordinator SCADA_MGMT packet without a known session from computer ", src_addr))
end
elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
-- coordinator packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding coordinator SCADA_CRDN packet without a known session from computer ", src_addr))
end
else
log.debug(util.c("illegal packet type ", protocol, " on coordinator channel"))
end
elseif r_chan == config.PKT_Channel then
-- look for an associated session
local session = svsessions.find_pdg_session(src_addr)
if protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping PDG establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.PKT then
-- this is an attempt to establish a new pocket diagnostic session
local s_id = svsessions.establish_pdg_session(src_addr, i_seq_num, firmware_v)
println(util.c("PKT (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("PDG_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on pocket channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug("PDG_ESTABLISH: establish packet length mismatch")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding pocket SCADA_MGMT packet without a known session from computer ", src_addr))
end
elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
-- coordinator packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding pocket SCADA_CRDN packet without a known session from computer ", src_addr))
end
else
log.debug(util.c("illegal packet type ", protocol, " on pocket channel"))
end
else
log.debug("received packet for unknown channel " .. r_chan, true)
end
end

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