Compare commits

...

166 Commits

Author SHA1 Message Date
Mikayla
3c7fff28c9 Merge pull request #443 from MikaylaFischler/devel
2024.03.08 Release
2024-03-08 22:40:28 -05:00
Mikayla Fischler
d1e4ea586e supervisor comment cleanup 2024-03-05 21:47:14 -05:00
Mikayla Fischler
4e789ab92d cleanup and refactors 2024-03-05 21:24:17 -05:00
Mikayla Fischler
0892a57d35 #442 return rather than assert on configuration error 2024-03-05 20:17:52 -05:00
Mikayla Fischler
1bc4828010 #438 use reported polonium rate rather than an estimate 2024-03-05 19:35:54 -05:00
Mikayla Fischler
fb5a9d5d9e #432 fixes and enhancements to coordinator waiting on chunk loads 2024-03-05 17:16:35 -05:00
Mikayla Fischler
adbf1f2f78 #441 #431 bugfixes to the bugfixes 2024-03-05 17:12:12 -05:00
Mikayla Fischler
2f99aaeedb #431 handle ppm mount of unformed reactor race condition 2024-03-05 10:56:27 -05:00
Mikayla Fischler
f9917b786c #432 wait 20s on computer power on before assuming monitor configuration problem 2024-02-25 18:02:13 -05:00
Mikayla Fischler
d6185e0183 #433 use os.clock instead of util.time_s for coordinator connection timeout to supervisor 2024-02-25 13:13:36 -05:00
Mikayla Fischler
cdd31508d9 #430 fixed unit boiler, turbine, and tank status indicators flashing OFF-LINE when online 2024-02-22 21:35:08 -05:00
Mikayla
83d62991f8 Merge pull request #429 from MikaylaFischler/328-coordinator-temperature-unit-options
328 coordinator temperature unit options
2024-02-22 20:50:01 -05:00
Mikayla Fischler
f207a950e4 fixed bug with hmac still being used for connecting in coordinator configurator after clearing key 2024-02-22 19:25:55 -05:00
Mikayla Fischler
0b0051dc2f #328 K, C, F, and R temperature unit options 2024-02-22 19:25:16 -05:00
Mikayla Fischler
f152c37ea9 #387 handle resizing, improved reconnect handling, fixed disconnect detection bug 2024-02-21 20:33:07 -05:00
Mikayla Fischler
372fd426d8 test code for psil allocations 2024-02-21 18:48:55 -05:00
Mikayla
8347afb6d0 Merge pull request #420 from MikaylaFischler/devel
2024.02.19 Release
2024-02-21 13:12:51 -05:00
Mikayla Fischler
10d0a9763a disabled a verbose log message 2024-02-21 12:59:48 -05:00
Mikayla Fischler
96691d773a supervisor debug log messages and #427 fix 2024-02-21 12:58:49 -05:00
Mikayla Fischler
910509d764 coordinator configurator bugfixes 2024-02-20 19:33:14 -05:00
Mikayla Fischler
158cc39b80 #421 remove 'latest' branch 2024-02-19 20:40:05 -05:00
Mikayla
940f71aa35 Merge pull request #422 from MikaylaFischler/145-graphical-configure-utilities
Pocket Configurator and Other Fixes
2024-02-19 20:34:41 -05:00
Mikayla Fischler
788cef8f86 comment cleanup and absolute paths while saving 2024-02-19 20:32:33 -05:00
Mikayla Fischler
baaef862ab #145 coordinator configurator enhancements 2024-02-19 20:26:05 -05:00
Mikayla Fischler
96db709ced ccmsi print fix 2024-02-19 20:24:30 -05:00
Mikayla Fischler
c47fa5433f #408 improvements to pocket configurator 2024-02-19 19:36:27 -05:00
Mikayla Fischler
440b724798 #424 tick comms version up 2024-02-19 19:33:08 -05:00
Mikayla Fischler
126d6eb163 #424 fixed key derivation init 2024-02-19 19:28:12 -05:00
Mikayla Fischler
8ac46faf36 set text scales before checking monitor dimensions 2024-02-19 18:56:24 -05:00
Mikayla Fischler
f112746e12 #408 increment bootloader version for pocket configurator, minification 2024-02-19 14:27:02 -05:00
Mikayla Fischler
76f21e925b #145 removed unneeded references to config.lua files + some minification 2024-02-19 14:18:23 -05:00
Mikayla Fischler
a330249c7e #408 integrate new settings with pocket 2024-02-19 14:07:26 -05:00
Mikayla Fischler
bbc64c8dc2 #145 fixed oversized listboxes 2024-02-19 13:54:23 -05:00
Mikayla Fischler
bb062cf397 #408 added pocket configure to configure launcher 2024-02-19 13:50:38 -05:00
Mikayla Fischler
53bb36ce8d #145 fixed change log page on coordinator 2024-02-19 13:50:03 -05:00
Mikayla Fischler
8237113577 #408 pocket configurator 2024-02-19 13:49:50 -05:00
Mikayla Fischler
02906ae707 add FUNDING.yml 2024-02-19 12:49:26 -05:00
Mikayla Fischler
6d0e777e68 updated copyright and removed coordinator from list mentioning config.lua 2024-02-18 21:40:25 -05:00
Mikayla
36468c4043 Merge pull request #419 from MikaylaFischler/145-graphical-configure-utilities
Coordinator Configurator
2024-02-18 21:36:50 -05:00
Mikayla Fischler
1cf7375311 #309 cleanup 2024-02-18 21:34:25 -05:00
Mikayla Fischler
ca55948286 #309 remove legacy config.lua 2024-02-18 21:25:21 -05:00
Mikayla Fischler
195f59178f #309 bugfix to apisessions still using old config 2024-02-18 21:24:30 -05:00
Mikayla Fischler
e416faf313 #309 integrated process control with new settings file 2024-02-18 20:47:37 -05:00
Mikayla Fischler
20bff48cfd #309 cleanup, fixes, optimizations 2024-02-18 20:21:07 -05:00
Mikayla Fischler
1a9892b291 cleanup and optimizations 2024-02-18 16:49:39 -05:00
Mikayla Fischler
827953c0a1 more luacheck fixes 2024-02-18 15:31:45 -05:00
Mikayla Fischler
56e69e3a29 luacheck fixes 2024-02-18 15:30:18 -05:00
Mikayla Fischler
1984b63837 additional config validations 2024-02-18 15:25:30 -05:00
Mikayla Fischler
36b12d5dea #309 integrated new configuration into coordinator 2024-02-18 15:21:00 -05:00
Mikayla Fischler
3e83c8e2c6 #309 moved monitor block size to ppm and fixed size estimation for monitor requirements 2024-02-18 13:00:18 -05:00
Mikayla Fischler
1fa8c03dff #309 import legacy configs 2024-02-18 00:56:36 -05:00
Mikayla Fischler
d6de9c266b #309 viewing and saving coordinator config 2024-02-17 22:39:50 -05:00
Mikayla Fischler
2142c1b4f7 updated license year 2024-02-17 18:39:02 -05:00
Mikayla Fischler
cafba6c67a #309 legacy options and general improvements 2024-02-17 18:38:36 -05:00
Mikayla Fischler
6eccebbe39 #409 fixed positioning 2024-02-17 18:16:21 -05:00
Mikayla Fischler
5e9f03c900 #418 fixed graphics bug with redraw 2024-02-17 18:12:28 -05:00
Mikayla Fischler
7374bb02d1 #309 speaker and time format configuration 2024-02-14 14:41:34 -05:00
Mikayla Fischler
d19794ae4f #309 unassign unused monitors on unit count reduction and support autofilling fields when editing existing monitor configs 2024-02-14 10:24:27 -05:00
Mikayla Fischler
108cf75cad #309 coordinator monitor configuration 2024-02-14 09:43:30 -05:00
Mikayla Fischler
34cbb6be39 bugfixes 2024-02-03 01:48:56 -05:00
Mikayla
907f27baf8 #309 show data received from supervisor 2024-02-02 23:01:51 +00:00
Mikayla Fischler
4710fa7cee #309 WIP coordinator configurator 2024-01-31 14:10:03 -05:00
Mikayla
bfa1f4d0c6 Merge pull request #404 from MikaylaFischler/devel
2023.12.31 Release
2023-12-31 21:34:40 -05:00
Mikayla Fischler
737e0d72b0 changed supervisor facility config color theme as green is for the summary already 2023-12-31 15:41:28 -05:00
Mikayla Fischler
6edeb3e3b8 add default value to sounder volume for very old RTU config imports 2023-12-31 15:00:38 -05:00
Mikayla Fischler
fb00e98a5b more supervisor configurator bugfixes 2023-12-31 15:00:08 -05:00
Mikayla Fischler
4f952eff83 fixed supervisor incorrectly trying to validate tank defs when tank mode is zero 2023-12-31 14:14:35 -05:00
Mikayla Fischler
1eede97c08 fixed supervisor not using proper config on front panel 2023-12-31 14:04:22 -05:00
Mikayla Fischler
95419562ee no longer mention config.lua for supervisor update 2023-12-31 13:17:49 -05:00
Mikayla Fischler
7b85d947c4 fixed supervisor always using MAC 2023-12-30 22:57:30 -05:00
Mikayla Fischler
08e670091a #396 fixed fractional connection timeouts being treated as invalid 2023-12-30 20:25:57 -05:00
Mikayla
1348b632a8 Merge pull request #397 from MikaylaFischler/145-graphical-configure-utilities
Supervisor Configurator
2023-12-30 20:19:34 -05:00
Mikayla Fischler
8cd5162362 fixed PLCs not connecting, fixed facility tank mode checkbox not changing after import, and reordered info on tank mode vis about page 2023-12-30 20:18:58 -05:00
Mikayla Fischler
1c410a89d8 incremented comms version due to data change 2023-12-30 19:40:53 -05:00
Mikayla Fischler
6a931fced4 cleanup and fixes 2023-12-30 19:21:44 -05:00
Mikayla Fischler
622e2eeb90 more useful messages, incremented bootloader version 2023-12-30 14:51:25 -05:00
Mikayla Fischler
42cd9fff0c improved number field precision handling and limited decimal precision of timeouts #396 2023-12-30 14:41:03 -05:00
Mikayla Fischler
2a85a438ba #396 connection timeouts can now have a fractional part 2023-12-29 14:33:22 -05:00
Mikayla Fischler
338b3b1615 addressed luacheck warning 2023-12-29 14:29:46 -05:00
Mikayla Fischler
363f164f47 #308 deleted old config.lua 2023-12-29 14:12:54 -05:00
Mikayla Fischler
739f04ece9 #308 integrated new settings file with supervisor 2023-12-29 13:58:28 -05:00
Mikayla Fischler
c6ade68ce2 #308 importing legacy config 2023-12-29 12:40:48 -05:00
Mikayla Fischler
7d60e259e2 #308 supervisor configurator bugfixes and saving of settings 2023-12-29 01:07:50 -05:00
Mikayla Fischler
cd71c6a9c1 #308 summary display of supervisor config 2023-12-29 00:19:17 -05:00
Mikayla Fischler
26fe130609 #308 supervisor configurator completed facility tank mode and network config pages 2023-12-28 15:06:30 -05:00
Mikayla Fischler
95f87b1b05 #308 significantly improved facility dynamic tank configuration visualization 2023-12-26 13:13:05 -05:00
Mikayla Fischler
aebdf3e8df fixed include ordering 2023-12-26 13:11:46 -05:00
Mikayla Fischler
5d4fc36256 #308 WIP supervisor configurator 2023-12-18 15:23:51 -05:00
Mikayla
b799d785b9 Merge pull request #394 from MikaylaFischler/devel
2023.12.17 Release
2023-12-17 21:02:32 -05:00
Mikayla
d55442fa53 Merge pull request #393 from MikaylaFischler/145-graphical-configure-utilities
Bring in changes from 145 branch to devel for release
2023-12-17 20:51:05 -05:00
Mikayla Fischler
c870b749a4 cleanup 2023-12-17 20:48:02 -05:00
Mikayla Fischler
4421cbc0c5 fixed input/output side text being sometimes wrong on rtu configurator redstone editing 2023-12-17 20:47:17 -05:00
Mikayla Fischler
b6a3305f23 minor minification 2023-12-17 20:43:40 -05:00
Mikayla Fischler
5680260136 use existing is_valid_port rather than repeating the code 2023-12-17 20:43:08 -05:00
Mikayla Fischler
bc66ea6ecb #381 fixed plc main thread crash on modem connect after boot with no modem 2023-12-17 20:28:26 -05:00
Mikayla Fischler
1b20218445 #194 #382 ccmsi no longer deletes drive mounts and now prompts to delete unknown files/folders in root 2023-12-17 20:10:11 -05:00
Mikayla Fischler
466e442353 #389 added width to RTU front panel entry name box 2023-12-17 19:39:00 -05:00
Mikayla Fischler
9e6751f47f #391 fixed editing of redstone entries 2023-12-17 19:32:01 -05:00
Mikayla Fischler
f868923905 #392 fixed typo preventing water level low indicator from working 2023-12-17 18:04:09 -05:00
Mikayla Fischler
5d3fd6d939 #390 fixed not being able to edit entries after using ALL_WASTE shortcut 2023-12-17 17:46:18 -05:00
Mikayla Fischler
37659d687e #388 fixed peripherals list not updating on add/delete of config entry 2023-12-17 17:22:29 -05:00
Mikayla Fischler
f23b7e2c2f fixed out of bounds coordinates crashing GUI for form fields 2023-12-17 12:56:08 -05:00
Mikayla Fischler
55ccdd63d4 don't mention config.lua on update for apps that don't have it 2023-12-17 12:55:00 -05:00
Mikayla Fischler
5c88890ed4 removed redundant min_width values 2023-12-14 20:51:54 -05:00
Mikayla Fischler
fa0185c9a4 fixed checkbox width 2023-12-13 12:20:12 -05:00
Mikayla Fischler
e1ed9a8e5e fixed error messages not fitting and say input side when configuring inputs on RTU configurator 2023-11-29 22:25:34 -05:00
Mikayla
9f7e3bc282 Merge pull request #383 from MikaylaFischler/devel
2023.11.28 Beta Hotfix
2023-11-28 20:35:56 -05:00
Mikayla Fischler
4ec060ba24 #378 fixed unit input not being re-shown on RTU configurator 2023-11-28 18:05:07 -05:00
Mikayla
b1f1753a8d Merge pull request #379 from MikaylaFischler/377-compatibility-fixes-for-lua-5.2
#377 switched to using ... for vararg
2023-11-28 17:54:36 -05:00
Mikayla Fischler
94a62f8c31 #377 switched to using ... for vararg 2023-11-27 19:32:52 -05:00
Mikayla
e6f49f256c Merge pull request #372 from MikaylaFischler/devel
2023.11.15 Hotfix
2023-11-15 19:41:29 -05:00
Mikayla Fischler
a048b0aa4a #371 fixed RTU configurator bug with empty peripherals or redstone, re-ordered settings load, added type checks 2023-11-15 19:30:49 -05:00
Mikayla
b6617c140c Merge pull request #369 from MikaylaFischler/devel
2023.11.14 Release
2023-11-14 22:10:12 -05:00
Mikayla Fischler
1fdf012f65 properly clear peripherals and redstone when importing 2023-11-14 22:00:01 -05:00
Mikayla Fischler
8fe0321ac0 fixed RTU authkey check 2023-11-14 19:40:55 -05:00
Mikayla Fischler
4a2199fa13 readme update 2023-11-14 19:40:29 -05:00
Mikayla
69680a53a0 Merge pull request #368 from MikaylaFischler/145-graphical-configure-utilities
RTU configurator and other updates
2023-11-12 18:37:23 -05:00
Mikayla Fischler
785dea6545 #306 fixed incorrect screenflow and changed peripheral import validation symbols 2023-11-12 18:36:16 -05:00
Mikayla Fischler
885932afe1 don't try to log if log.init wasn't called 2023-11-12 18:35:46 -05:00
Mikayla Fischler
a38ccf3dcc #145 #306 improvements and fixes, better peripheral import 2023-11-12 18:27:24 -05:00
Mikayla Fischler
d7b1f9cc7e #306 #362 bugfixes 2023-11-12 16:55:24 -05:00
Mikayla Fischler
6e92097544 fixed util.concat handling of nil parameters 2023-11-12 16:06:16 -05:00
Mikayla Fischler
76403b4ddc cleanup and grammar 2023-11-12 15:38:25 -05:00
Mikayla Fischler
41ad8d8edb #306 prevent duplicate redstone inputs 2023-11-12 14:35:53 -05:00
Mikayla Fischler
68754977b0 cleanup and fixes 2023-11-12 14:21:48 -05:00
Mikayla Fischler
78ad6d5457 luacheck fix 2023-11-12 12:00:42 -05:00
Mikayla Fischler
cb049ebf41 #194 changed 'newer' to 'different' in ccmsi 2023-11-12 11:57:05 -05:00
Mikayla Fischler
f2f5c3201f #362 taking max of connected radiation monitors 2023-11-12 11:54:47 -05:00
Mikayla Fischler
1ba178eae8 #306 delete legacy RTU config 2023-11-06 10:22:52 -05:00
Mikayla Fischler
838f80c30c #306 #362 supervisor updates for RTU config changes 2023-11-06 10:21:42 -05:00
Mikayla Fischler
dc0408881e #145 use rsio.color_name on PLC configurator 2023-11-06 10:11:57 -05:00
Mikayla Fischler
1b5e8cb69c #306 RTU integration with new settings 2023-11-06 09:25:44 -05:00
Mikayla Fischler
9e13a3a467 added ability to view reactor PLC config after importing if cancelled before deleting 2023-11-05 13:23:45 -05:00
Mikayla Fischler
32653c3b8a param type change and added validator.assert 2023-11-05 13:23:22 -05:00
Mikayla Fischler
16258a2631 rtu configurator config import 2023-11-04 15:06:29 -04:00
Mikayla Fischler
4c646249ad plc configurator cleanup 2023-11-04 14:57:17 -04:00
Mikayla Fischler
45c8a8d8a9 added peripheral connections to rtu configurator 2023-11-04 13:29:38 -04:00
Mikayla Fischler
eff3444834 added type def to ppm and return a copy of the peripherals list rather than the table itself 2023-11-04 12:56:49 -04:00
Mikayla Fischler
25f68f338c bootloader cleanup and added license to installer downloads 2023-11-04 12:51:24 -04:00
Mikayla Fischler
7ef363a3c2 fixed installer typo 2023-11-04 12:49:54 -04:00
Mikayla Fischler
3065e2bece plc configurator clear settings when loading settings and show actual current settings on view 2023-11-04 12:49:14 -04:00
Mikayla Fischler
1075d66122 graphics bugfix with disabled input fields 2023-11-04 12:48:06 -04:00
Mikayla Fischler
d477b33774 fixed reposition not repositioning frame for mouse events 2023-10-21 13:58:42 -04:00
Mikayla Fischler
7b374f8618 rtu redstone configuration 2023-10-19 23:35:18 -04:00
Mikayla Fischler
4869c00c0e added side type alias and added some validation to RSIO 2023-10-19 23:22:04 -04:00
Mikayla Fischler
ff4a5a68d9 reactor PLC configurator emercoolcolor correction 2023-10-19 23:20:41 -04:00
Mikayla Fischler
d77a527b15 added text alignment to push buttons and added keyboard events to listbox 2023-10-19 23:20:04 -04:00
Mikayla Fischler
01caca48dc listbox improvements, tabbing while staying in frame (autoscroll) 2023-10-15 17:02:48 -04:00
Mikayla Fischler
43e545b6ae fixed unfocus all 2023-10-15 16:49:03 -04:00
Mikayla Fischler
8b65956dcc #306 base RTU configurator 2023-10-15 13:26:49 -04:00
Mikayla
7b522ae120 Merge pull request #360 from MikaylaFischler/devel
2023.10.14 Release
2023-10-14 19:46:33 -04:00
Mikayla Fischler
43d134d9ad comment clarity 2023-10-14 18:49:51 -04:00
Mikayla Fischler
24c787f47d #353 fixed auto lock not restoring on reconnect 2023-10-14 17:57:50 -04:00
Mikayla Fischler
f95ac8be8c #359 drop packets with nil distances if using trusted range feature 2023-10-14 12:17:25 -04:00
Mikayla Fischler
41442012c2 #357 min length of auth key and some cleanup, added config change log 2023-10-14 12:02:25 -04:00
Mikayla Fischler
6f1195dded fixed element assertion scope on frame validations 2023-10-14 11:40:36 -04:00
Mikayla Fischler
86e8feaabc #358 fixed non-networked PLC operation 2023-10-14 10:07:56 -04:00
Mikayla Fischler
670ca78a5b #356 fixed extract assert msg 2023-10-14 00:38:13 -04:00
Mikayla Fischler
73ceed0f60 #354 another reversion 2023-10-13 23:53:15 -04:00
Mikayla Fischler
8412270772 #354 some reversions 2023-10-13 23:48:30 -04:00
Mikayla Fischler
686b47898c #354 optimizations/minification 2023-10-08 12:47:51 -04:00
Mikayla
e03eaf2982 #354 type check functions 2023-10-07 20:57:24 +00:00
Mikayla
8b1775b0af Merge pull request #352 from MikaylaFischler/devel
2023.10.04 Hotfix
2023-10-04 20:03:06 -04:00
Mikayla
8bbd04d133 Update feature_request.md 2023-10-04 17:32:43 -04:00
Mikayla
26dc6ff6d1 #350 handle RPS_DISABLE ack packet on supervisor 2023-10-04 21:28:14 +00:00
Mikayla
24d190921d #349 F_ALARM_ANY rsio output added 2023-10-04 21:26:07 +00:00
75 changed files with 6880 additions and 1959 deletions

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

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

View File

@@ -2,7 +2,7 @@
name: Feature request name: Feature request
about: Suggest an idea for this project about: Suggest an idea for this project
title: '' title: ''
labels: enhancement labels: "enhancement,feature request"
assignees: '' assignees: ''
--- ---

View File

@@ -5,12 +5,10 @@ on:
push: push:
branches: branches:
- main - main
- latest
- devel - devel
pull_request: pull_request:
branches: branches:
- main - main
- latest
- devel - devel
jobs: jobs:
check: check:

View File

@@ -6,7 +6,6 @@ on:
push: push:
branches: branches:
- main - main
- latest
- devel - devel
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
@@ -43,7 +42,7 @@ jobs:
- name: Create outputs folders - name: Create outputs folders
if: success() || failure() if: success() || failure()
shell: bash shell: bash
run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/latest deploy/manifests/devel run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/devel
- name: Generate manifest and shields for main branch - name: Generate manifest and shields for main branch
id: manifest-main id: manifest-main
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }} if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
@@ -51,21 +50,6 @@ jobs:
- name: Save main's manifest - name: Save main's manifest
if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }} if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/main run: mv install_manifest.json deploy/manifests/main
# Generate manifest for latest branch
- name: Checkout latest
id: checkout-latest
if: success() || failure()
uses: actions/checkout@v3
with:
ref: 'latest'
clean: false
- name: Generate manifest for latest
id: manifest-latest
if: ${{ (success() || failure()) && steps.checkout-latest.outcome == 'success' }}
run: python imgen.py
- name: Save latest's manifest
if: ${{ (success() || failure()) && steps.manifest-latest.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/latest
# Generate manifest for devel branch # Generate manifest for devel branch
- name: Checkout devel - name: Checkout devel
id: checkout-devel id: checkout-devel

View File

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

View File

@@ -36,8 +36,9 @@ Mod Requirements:
Mod Recommendations: Mod Recommendations:
- Advanced Peripherals (adds the capability to detect environmental radiation levels) - Advanced Peripherals (adds the capability to detect environmental radiation levels)
- Immersive Engineering (provides bundled redstone, though any mod containing bundled redstone will do)
v10.1+ is required due the complete support of CC:Tweaked added in Mekanism v10.1 v10.1+ is required due to the complete support of CC:Tweaked added in Mekanism v10.1
## Installation ## Installation

145
ccmsi.lua
View File

@@ -1,7 +1,7 @@
--[[ --[[
CC-MEK-SCADA Installer Utility CC-MEK-SCADA Installer Utility
Copyright (c) 2023 Mikayla Fischler Copyright (c) 2023 - 2024 Mikayla Fischler
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, associated documentation files (the "Software"), to deal in the Software without restriction,
@@ -18,7 +18,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
local function println(message) print(tostring(message)) end local function println(message) print(tostring(message)) end
local function print(message) term.write(tostring(message)) end local function print(message) term.write(tostring(message)) end
local CCMSI_VERSION = "v1.11a" local CCMSI_VERSION = "v1.14"
local install_dir = "/.install-cache" local install_dir = "/.install-cache"
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/" local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
@@ -26,12 +26,13 @@ local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada
local opts = { ... } local opts = { ... }
local mode, app, target local mode, app, target
local install_manifest = manifest_path .. "main/install_manifest.json" local install_manifest = manifest_path.."main/install_manifest.json"
local function red() term.setTextColor(colors.red) end local function red() term.setTextColor(colors.red) end
local function orange() term.setTextColor(colors.orange) end local function orange() term.setTextColor(colors.orange) end
local function yellow() term.setTextColor(colors.yellow) end local function yellow() term.setTextColor(colors.yellow) end
local function green() term.setTextColor(colors.green) end local function green() term.setTextColor(colors.green) end
local function cyan() term.setTextColor(colors.cyan) end
local function blue() term.setTextColor(colors.blue) end local function blue() term.setTextColor(colors.blue) end
local function white() term.setTextColor(colors.white) end local function white() term.setTextColor(colors.white) end
local function lgray() term.setTextColor(colors.lightGray) end local function lgray() term.setTextColor(colors.lightGray) end
@@ -58,17 +59,17 @@ local function ask_y_n(question, default)
end end
-- print out a white + blue text message -- print out a white + blue text message
local function pkg_message(message, package) white();print(message .. " ");blue();println(package);white() end local function pkg_message(message, package) white();print(message.." ");blue();println(package);white() end
-- indicate actions to be taken based on package differences for installs/updates -- indicate actions to be taken based on package differences for installs/updates
local function show_pkg_change(name, v) local function show_pkg_change(name, v)
if v.v_local ~= nil then if v.v_local ~= nil then
if v.v_local ~= v.v_remote then if v.v_local ~= v.v_remote then
print("[" .. name .. "] updating ");blue();print(v.v_local);white();print(" \xbb ");blue();println(v.v_remote);white() print("["..name.."] updating ");blue();print(v.v_local);white();print(" \xbb ");blue();println(v.v_remote);white()
elseif mode == "install" then elseif mode == "install" then
pkg_message("[" .. name .. "] reinstalling", v.v_local) pkg_message("["..name.."] reinstalling", v.v_local)
end end
else pkg_message("[" .. name .. "] new install of", v.v_remote) end else pkg_message("["..name.."] new install of", v.v_remote) end
return v.v_local ~= v.v_remote return v.v_local ~= v.v_remote
end end
@@ -89,7 +90,7 @@ local function get_remote_manifest()
local response, error = http.get(install_manifest) local response, error = http.get(install_manifest)
if response == nil then if response == nil then
orange();println("Failed to get installation manifest from GitHub, cannot update or install.") orange();println("Failed to get installation manifest from GitHub, cannot update or install.")
red();println("HTTP error: " .. error);white() red();println("HTTP error: "..error);white()
return false, {} return false, {}
end end
@@ -154,57 +155,55 @@ local function _clean_dir(dir, tree)
if tree == nil then tree = {} end if tree == nil then tree = {} end
local ls = fs.list(dir) local ls = fs.list(dir)
for _, val in pairs(ls) do for _, val in pairs(ls) do
local path = dir .. "/" .. val local path = dir.."/"..val
if fs.isDir(path) then if fs.isDir(path) then
_clean_dir(path, tree[val]) _clean_dir(path, tree[val])
if #fs.list(path) == 0 then fs.delete(path);println("deleted " .. path) end if #fs.list(path) == 0 then fs.delete(path);println("deleted "..path) end
elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then ---@fixme remove condition after migration to settings files elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then
fs.delete(path) fs.delete(path)
println("deleted " .. path) println("deleted "..path)
end end
end end
end end
-- go through app/common directories to delete unused files -- go through app/common directories to delete unused files
local function clean(manifest) local function clean(manifest)
local root_ext = false
local tree = gen_tree(manifest) local tree = gen_tree(manifest)
table.insert(tree, "install_manifest.json") table.insert(tree, "install_manifest.json")
table.insert(tree, "ccmsi.lua") table.insert(tree, "ccmsi.lua")
table.insert(tree, "log.txt") ---@fixme fix after migration to settings files? table.insert(tree, "log.txt") ---@fixme fix after migration to settings files?
lgray()
local ls = fs.list("/") local ls = fs.list("/")
for _, val in pairs(ls) do for _, val in pairs(ls) do
if fs.isDir(val) then if fs.isDriveRoot(val) then
if tree[val] ~= nil then _clean_dir("/" .. val, tree[val]) end yellow();println("skipped mount '"..val.."'")
if #fs.list(val) == 0 then fs.delete(val);println("deleted " .. val) end 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 elseif not _in_array(val, tree) and (string.find(val, ".settings") == nil) then
root_ext = true white();if ask_y_n("delete the unused file '"..val.."'") then fs.delete(val);lgray();println("deleted "..val) end
yellow();println(val .. " not used")
end end
end end
white() white()
if root_ext then println("Files in root directory won't be automatically deleted.") end
end end
-- get and validate command line options -- get and validate command line options
println("-- CC Mekanism SCADA Installer " .. CCMSI_VERSION .. " --") println("-- CC Mekanism SCADA Installer "..CCMSI_VERSION.." --")
if #opts == 0 or opts[1] == "help" then if #opts == 0 or opts[1] == "help" then
println("usage: ccmsi <mode> <app> <branch>") println("usage: ccmsi <mode> <app> <branch>")
println("<mode>") println("<mode>")
lgray() lgray()
println(" check - check latest versions avilable") println(" check - check latest versions available")
yellow() yellow()
println(" ccmsi check <branch> for target") println(" ccmsi check <branch> for target")
lgray() lgray()
println(" install - fresh install, overwrites config.lua") println(" install - fresh install")
println(" update - update files EXCEPT for config.lua") println(" update - update files")
println(" uninstall - delete files INCLUDING config/logs") println(" uninstall - delete files INCLUDING config/logs")
white();println("<app>");lgray() white();println("<app>");lgray()
println(" reactor-plc - reactor PLC firmware") println(" reactor-plc - reactor PLC firmware")
@@ -214,7 +213,7 @@ if #opts == 0 or opts[1] == "help" then
println(" pocket - pocket application") println(" pocket - pocket application")
println(" installer - ccmsi installer (update only)") println(" installer - ccmsi installer (update only)")
white();println("<branch>") white();println("<branch>")
lgray();println(" main (default) | latest | devel");white() lgray();println(" main (default) | devel");white()
return return
else else
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" }) mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
@@ -234,14 +233,14 @@ else
-- determine target -- determine target
if mode == "check" then target = opts[2] else target = opts[3] end if mode == "check" then target = opts[2] else target = opts[3] end
if (target ~= "main") and (target ~= "latest") and (target ~= "devel") then if (target ~= "main") and (target ~= "devel") then
if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end
target = "main" target = "main"
end end
-- set paths -- set paths
install_manifest = manifest_path .. target .. "/install_manifest.json" install_manifest = manifest_path..target.."/install_manifest.json"
repo_path = repo_path .. target .. "/" repo_path = repo_path..target.."/"
end end
-- run selected mode -- run selected mode
@@ -261,23 +260,21 @@ if mode == "check" then
-- list all versions -- list all versions
for key, value in pairs(manifest.versions) do for key, value in pairs(manifest.versions) do
term.setTextColor(colors.purple) term.setTextColor(colors.purple)
print(string.format("%-14s", "[" .. key .. "]")) print(string.format("%-14s", "["..key.."]"))
if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then
blue();print(local_manifest.versions[key]) blue();print(local_manifest.versions[key])
if value ~= local_manifest.versions[key] then if value ~= local_manifest.versions[key] then
white();print(" (") white();print(" (")
term.setTextColor(colors.cyan) cyan();print(value);white();println(" available)")
print(value);white();println(" available)")
else green();println(" (up to date)") end else green();println(" (up to date)") end
else else
lgray();print("not installed");white();print(" (latest ") lgray();print("not installed");white();print(" (latest ")
term.setTextColor(colors.cyan) cyan();print(value);white();println(")")
print(value);white();println(")")
end end
end end
if manifest.versions.installer ~= local_manifest.versions.installer then if manifest.versions.installer ~= local_manifest.versions.installer then
yellow();println("\nA newer version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white() yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white()
end end
elseif mode == "install" or mode == "update" then elseif mode == "install" or mode == "update" then
local update_installer = app == "installer" local update_installer = app == "installer"
@@ -315,13 +312,13 @@ elseif mode == "install" or mode == "update" then
end end
if manifest.versions.installer ~= CCMSI_VERSION then if manifest.versions.installer ~= CCMSI_VERSION then
if not update_installer then yellow();println("A newer version of the installer is available, it is recommended to update to it.");white() end if not update_installer then yellow();println("A different version of the installer is available, it is recommended to update to it.");white() end
if update_installer or ask_y_n("Would you like to update now") then if update_installer or ask_y_n("Would you like to update now") then
lgray();println("GET ccmsi.lua") lgray();println("GET ccmsi.lua")
local dl, err = http.get(repo_path .. "ccmsi.lua") local dl, err = http.get(repo_path.."ccmsi.lua")
if dl == nil then if dl == nil then
red();println("HTTP Error " .. err) red();println("HTTP Error "..err)
println("Installer download failed.");white() println("Installer download failed.");white()
else else
local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location
@@ -345,12 +342,8 @@ elseif mode == "install" or mode == "update" then
ver.lockbox.v_remote = manifest.versions.lockbox ver.lockbox.v_remote = manifest.versions.lockbox
green() green()
if mode == "install" then if mode == "install" then print("Installing ") else print("Updating ") end
println("Installing " .. app .. " files...") println(app.." files...");white()
elseif mode == "update" then
println("Updating " .. app .. " files... (keeping old config.lua)")
end
white()
ver.boot.changed = show_pkg_change("bootldr", ver.boot) ver.boot.changed = show_pkg_change("bootldr", ver.boot)
ver.common.changed = show_pkg_change("common", ver.common) ver.common.changed = show_pkg_change("common", ver.common)
@@ -376,7 +369,6 @@ elseif mode == "install" or mode == "update" then
local file_list = manifest.files local file_list = manifest.files
local size_list = manifest.sizes local size_list = manifest.sizes
local dependencies = manifest.depends[app] local dependencies = manifest.depends[app]
local config_file = app .. "/config.lua"
table.insert(dependencies, app) table.insert(dependencies, app)
@@ -423,15 +415,15 @@ elseif mode == "install" or mode == "update" then
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do for _, file in pairs(files) do
println("GET " .. file) println("GET "..file)
local dl, err = http.get(repo_path .. file) local dl, err = http.get(repo_path..file)
if dl == nil then if dl == nil then
red();println("HTTP Error " .. err) red();println("HTTP Error "..err)
success = false success = false
break break
else else
local handle = fs.open(install_dir .. "/" .. file, "w") local handle = fs.open(install_dir.."/"..file, "w")
handle.write(dl.readAll()) handle.write(dl.readAll())
handle.close() handle.close()
end end
@@ -450,11 +442,9 @@ elseif mode == "install" or mode == "update" then
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do for _, file in pairs(files) do
if mode == "install" or file ~= config_file then local temp_file = install_dir.."/"..file
local temp_file = install_dir .. "/" .. file if fs.exists(file) then fs.delete(file) end
if fs.exists(file) then fs.delete(file) end fs.move(temp_file, file)
fs.move(temp_file, file)
end
end end
end end
end end
@@ -487,19 +477,17 @@ elseif mode == "install" or mode == "update" then
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do for _, file in pairs(files) do
if mode == "install" or file ~= config_file then println("GET "..file)
println("GET " .. file) local dl, err = http.get(repo_path..file)
local dl, err = http.get(repo_path .. file)
if dl == nil then if dl == nil then
red();println("HTTP Error " .. err) red();println("HTTP Error "..err)
success = false success = false
break break
else else
local handle = fs.open("/" .. file, "w") local handle = fs.open("/"..file, "w")
handle.write(dl.readAll()) handle.write(dl.readAll())
handle.close() handle.close()
end
end end
end end
end end
@@ -529,11 +517,11 @@ elseif mode == "uninstall" then
end end
if manifest.versions[app] == nil then if manifest.versions[app] == nil then
red();println("Error: '" .. app .. "' is not installed.") red();println("Error: '"..app.."' is not installed.")
return return
end end
orange();println("Uninstalling all " .. app .. " files...") orange();println("Uninstalling all "..app.." files...")
-- ask for confirmation -- ask for confirmation
if not ask_y_n("Continue", false) then return end if not ask_y_n("Continue", false) then return end
@@ -548,16 +536,16 @@ elseif mode == "uninstall" then
-- delete log file -- delete log file
local log_deleted = false local log_deleted = false
local settings_file = app .. ".settings" local settings_file = app..".settings"
local legacy_config_file = app .. "/config.lua" local legacy_config_file = app.."/config.lua"
lgray() lgray()
if fs.exists(legacy_config_file) then if fs.exists(legacy_config_file) then
log_deleted = pcall(function () log_deleted = pcall(function ()
local config = require(app .. ".config") local config = require(app..".config")
if fs.exists(config.LOG_PATH) then if fs.exists(config.LOG_PATH) then
fs.delete(config.LOG_PATH) fs.delete(config.LOG_PATH)
println("deleted log file " .. config.LOG_PATH) println("deleted log file "..config.LOG_PATH)
end end
end) end)
elseif fs.exists(settings_file) and settings.load(settings_file) then elseif fs.exists(settings_file) and settings.load(settings_file) then
@@ -565,7 +553,7 @@ elseif mode == "uninstall" then
if log ~= nil and fs.exists(log) then if log ~= nil and fs.exists(log) then
log_deleted = true log_deleted = true
fs.delete(log) fs.delete(log)
println("deleted log file " .. log) println("deleted log file "..log)
end end
end end
@@ -579,7 +567,7 @@ elseif mode == "uninstall" then
for _, dependency in pairs(dependencies) do for _, dependency in pairs(dependencies) do
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do for _, file in pairs(files) do
if fs.exists(file) then fs.delete(file);println("deleted " .. file) end if fs.exists(file) then fs.delete(file);println("deleted "..file) end
end end
local folder = files[1] local folder = files[1]
@@ -590,13 +578,16 @@ elseif mode == "uninstall" then
if fs.isDir(folder) then if fs.isDir(folder) then
fs.delete(folder) fs.delete(folder)
println("deleted directory " .. folder) println("deleted directory "..folder)
end end
end end
if fs.exists(legacy_config_file) then
fs.delete(legacy_config_file);println("deleted "..legacy_config_file)
end
if fs.exists(settings_file) then if fs.exists(settings_file) then
fs.delete(settings_file) fs.delete(settings_file);println("deleted "..settings_file)
println("deleted " .. settings_file)
end end
fs.delete("install_manifest.json") fs.delete("install_manifest.json")

View File

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

View File

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

1322
coordinator/configure.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,11 +9,6 @@ local process = require("coordinator.process")
local apisessions = require("coordinator.session.apisessions") local apisessions = require("coordinator.session.apisessions")
local dialog = require("coordinator.ui.dialog")
local print = util.print
local println = util.println
local PROTOCOL = comms.PROTOCOL local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK local ESTABLISH_ACK = comms.ESTABLISH_ACK
@@ -26,182 +21,155 @@ local LINK_TIMEOUT = 60.0
local coordinator = {} local coordinator = {}
-- request the user to select a monitor ---@type crd_config
---@nodiscard local config = {}
---@param names table available monitors
---@return boolean|string|nil
local function ask_monitor(names)
println("available monitors:")
for i = 1, #names do
print(" " .. names[i])
end
println("")
println("select a monitor or type c to cancel")
local iface = dialog.ask_options(names, "c") coordinator.config = config
if iface ~= false and iface ~= nil then -- load the coordinator configuration<br>
util.filter_table(names, function (x) return x ~= iface end) -- status of 0 is OK, 1 is bad config, 2 is bad monitor config
---@return 0|1|2 status, nil|monitors_struct|string monitors (or error message)
function coordinator.load_config()
if not settings.load("/coordinator.settings") then return 1 end
config.UnitCount = settings.get("UnitCount")
config.SpeakerVolume = settings.get("SpeakerVolume")
config.Time24Hour = settings.get("Time24Hour")
config.TempScale = settings.get("TempScale")
config.DisableFlowView = settings.get("DisableFlowView")
config.MainDisplay = settings.get("MainDisplay")
config.FlowDisplay = settings.get("FlowDisplay")
config.UnitDisplays = settings.get("UnitDisplays")
config.SVR_Channel = settings.get("SVR_Channel")
config.CRD_Channel = settings.get("CRD_Channel")
config.PKT_Channel = settings.get("PKT_Channel")
config.SVR_Timeout = settings.get("SVR_Timeout")
config.API_Timeout = settings.get("API_Timeout")
config.TrustedRange = settings.get("TrustedRange")
config.AuthKey = settings.get("AuthKey")
config.LogMode = settings.get("LogMode")
config.LogPath = settings.get("LogPath")
config.LogDebug = settings.get("LogDebug")
local cfv = util.new_validator()
cfv.assert_type_int(config.UnitCount)
cfv.assert_range(config.UnitCount, 1, 4)
cfv.assert_type_bool(config.Time24Hour)
cfv.assert_type_int(config.TempScale)
cfv.assert_range(config.TempScale, 1, 4)
cfv.assert_type_bool(config.DisableFlowView)
cfv.assert_type_table(config.UnitDisplays)
cfv.assert_type_num(config.SpeakerVolume)
cfv.assert_range(config.SpeakerVolume, 0, 3)
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.CRD_Channel)
cfv.assert_channel(config.PKT_Channel)
cfv.assert_type_num(config.SVR_Timeout)
cfv.assert_min(config.SVR_Timeout, 2)
cfv.assert_type_num(config.API_Timeout)
cfv.assert_min(config.API_Timeout, 2)
cfv.assert_type_num(config.TrustedRange)
cfv.assert_min(config.TrustedRange, 0)
cfv.assert_type_str(config.AuthKey)
if type(config.AuthKey) == "string" then
local len = string.len(config.AuthKey)
cfv.assert_eq(len == 0 or len >= 8, true)
end end
return iface cfv.assert_type_int(config.LogMode)
end cfv.assert_range(config.LogMode, 0, 1)
cfv.assert_type_str(config.LogPath)
cfv.assert_type_bool(config.LogDebug)
-- Monitor Setup
-- configure monitor layout
---@param num_units integer number of units expected
---@param disable_flow_view boolean disable flow view (legacy)
---@return boolean success, monitors_struct? monitors
function coordinator.configure_monitors(num_units, disable_flow_view)
---@class monitors_struct ---@class monitors_struct
local monitors = { local monitors = {
primary = nil, ---@type table|nil main = nil, ---@type table|nil
primary_name = "", main_name = "",
flow = nil, ---@type table|nil flow = nil, ---@type table|nil
flow_name = "", flow_name = "",
unit_displays = {}, unit_displays = {},
unit_name_map = {} unit_name_map = {}
} }
local monitors_avail = ppm.get_monitor_list() local mon_cfv = util.new_validator()
local names = {}
local available = {}
-- get all interface names -- get all interface names
for iface, _ in pairs(monitors_avail) do local names = {}
table.insert(names, iface) for iface, _ in pairs(ppm.get_monitor_list()) do table.insert(names, iface) end
table.insert(available, iface)
end
-- we need a certain number of monitors (1 per unit + 1 primary display + 1 flow display) local function setup_monitors()
local num_displays_needed = num_units + util.trinary(disable_flow_view, 1, 2) mon_cfv.assert_type_str(config.MainDisplay)
if #names < num_displays_needed then if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end
local message = "not enough monitors connected (need " .. num_displays_needed .. ")" mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount)
println(message)
log.warning(message)
return false
end
-- attempt to load settings if mon_cfv.valid() then
if not settings.load("/coord.settings") then local w, h, _
log.warning("configure_monitors(): failed to load coordinator settings file (may not exist yet)")
else
local _primary = settings.get("PRIMARY_DISPLAY")
local _flow = settings.get("FLOW_DISPLAY")
local _unitd = settings.get("UNIT_DISPLAYS")
-- filter out already assigned monitors if not util.table_contains(names, config.MainDisplay) then
util.filter_table(available, function (x) return x ~= _primary end) return 2, "Main monitor is not connected."
util.filter_table(available, function (x) return x ~= _flow end)
if type(_unitd) == "table" then
util.filter_table(available, function (x) return not util.table_contains(_unitd, x) end)
end
end
---------------------
-- PRIMARY DISPLAY --
---------------------
local iface_primary_display = settings.get("PRIMARY_DISPLAY") ---@type boolean|string|nil
if not util.table_contains(names, iface_primary_display) then
println("primary display is not connected")
local response = dialog.ask_y_n("would you like to change it", true)
if response == false then return false end
iface_primary_display = nil
end
while iface_primary_display == nil and #available > 0 do
iface_primary_display = ask_monitor(available)
end
if type(iface_primary_display) ~= "string" then return false end
settings.set("PRIMARY_DISPLAY", iface_primary_display)
util.filter_table(available, function (x) return x ~= iface_primary_display end)
monitors.primary = ppm.get_periph(iface_primary_display)
monitors.primary_name = iface_primary_display
--------------------------
-- FLOW MONITOR DISPLAY --
--------------------------
if not disable_flow_view then
local iface_flow_display = settings.get("FLOW_DISPLAY") ---@type boolean|string|nil
if not util.table_contains(names, iface_flow_display) then
println("flow monitor display is not connected")
local response = dialog.ask_y_n("would you like to change it", true)
if response == false then return false end
iface_flow_display = nil
end
while iface_flow_display == nil and #available > 0 do
iface_flow_display = ask_monitor(available)
end
if type(iface_flow_display) ~= "string" then return false end
settings.set("FLOW_DISPLAY", iface_flow_display)
util.filter_table(available, function (x) return x ~= iface_flow_display end)
monitors.flow = ppm.get_periph(iface_flow_display)
monitors.flow_name = iface_flow_display
end
-------------------
-- UNIT DISPLAYS --
-------------------
local unit_displays = settings.get("UNIT_DISPLAYS")
if unit_displays == nil then
unit_displays = {}
for i = 1, num_units do
local display = nil
while display == nil and #available > 0 do
println("please select monitor for unit #" .. i)
display = ask_monitor(available)
end end
if display == false then return false end monitors.main = ppm.get_periph(config.MainDisplay)
monitors.main_name = config.MainDisplay
unit_displays[i] = display monitors.main.setTextScale(0.5)
end w, _ = ppm.monitor_block_size(monitors.main.getSize())
else if w ~= 8 then
-- make sure all displays are connected return 2, util.c("Main monitor width is incorrect (was ", w, ", must be 8).")
for i = 1, num_units do
local display = unit_displays[i]
if not util.table_contains(names, display) then
println("unit #" .. i .. " display is not connected")
local response = dialog.ask_y_n("would you like to change it", true)
if response == false then return false end
display = nil
end end
while display == nil and #available > 0 do if not config.DisableFlowView then
display = ask_monitor(available) if not util.table_contains(names, config.FlowDisplay) then
return 2, "Flow monitor is not connected."
end
monitors.flow = ppm.get_periph(config.FlowDisplay)
monitors.flow_name = config.FlowDisplay
monitors.flow.setTextScale(0.5)
w, _ = ppm.monitor_block_size(monitors.flow.getSize())
if w ~= 8 then
return 2, util.c("Flow monitor width is incorrect (was ", w, ", must be 8).")
end
end end
if display == false then return false end for i = 1, config.UnitCount do
local display = config.UnitDisplays[i]
if type(display) ~= "string" or not util.table_contains(names, display) then
return 2, "Unit " .. i .. " monitor is not connected."
end
unit_displays[i] = display monitors.unit_displays[i] = ppm.get_periph(display)
end monitors.unit_name_map[i] = display
monitors.unit_displays[i].setTextScale(0.5)
w, h = ppm.monitor_block_size(monitors.unit_displays[i].getSize())
if w ~= 4 or h ~= 4 then
return 2, util.c("Unit ", i, " monitor size is incorrect (was ", w, " by ", h,", must be 4 by 4).")
end
end
else return 2, "Monitor configuration invalid." end
end end
settings.set("UNIT_DISPLAYS", unit_displays) if cfv.valid() then
if not settings.save("/coord.settings") then local ok, result, message = pcall(setup_monitors)
log.warning("configure_monitors(): failed to save coordinator settings file") assert(ok, util.c("fatal error while trying to verify monitors: ", result))
end if result == 2 then return 2, message end
else return 1 end
for i = 1, #unit_displays do return 0, monitors
monitors.unit_displays[i] = ppm.get_periph(unit_displays[i])
monitors.unit_name_map[i] = unit_displays[i]
end
return true, monitors
end end
-- dmesg print wrapper -- dmesg print wrapper
@@ -246,13 +214,8 @@ end
---@nodiscard ---@nodiscard
---@param version string coordinator version ---@param version string coordinator version
---@param nic nic network interface device ---@param nic nic network interface device
---@param num_units integer number of configured units for number of monitors, checked against SV
---@param crd_channel integer port of configured supervisor
---@param svr_channel integer listening port for supervisor replys
---@param pkt_channel integer listening port for pocket API
---@param range integer trusted device connection range
---@param sv_watchdog watchdog ---@param sv_watchdog watchdog
function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pkt_channel, range, sv_watchdog) function coordinator.comms(version, nic, sv_watchdog)
local self = { local self = {
sv_linked = false, sv_linked = false,
sv_addr = comms.BROADCAST, sv_addr = comms.BROADCAST,
@@ -267,16 +230,16 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
est_task_done = nil est_task_done = nil
} }
comms.set_trusted_range(range) comms.set_trusted_range(config.TrustedRange)
-- PRIVATE FUNCTIONS --
-- configure network channels -- configure network channels
nic.closeAll() nic.closeAll()
nic.open(crd_channel) nic.open(config.CRD_Channel)
-- link nic to apisessions -- pass config to apisessions
apisessions.init(nic) apisessions.init(nic, config)
-- PRIVATE FUNCTIONS --
-- send a packet to the supervisor -- send a packet to the supervisor
---@param msg_type MGMT_TYPE|CRDN_TYPE ---@param msg_type MGMT_TYPE|CRDN_TYPE
@@ -296,7 +259,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
pkt.make(msg_type, msg) pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable()) s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable())
nic.transmit(svr_channel, crd_channel, s_pkt) nic.transmit(config.SVR_Channel, config.CRD_Channel, s_pkt)
self.sv_seq_num = self.sv_seq_num + 1 self.sv_seq_num = self.sv_seq_num + 1
end end
@@ -310,7 +273,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack }) m_pkt.make(MGMT_TYPE.ESTABLISH, { ack })
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
nic.transmit(pkt_channel, crd_channel, s_pkt) nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt)
self.last_api_est_acks[packet.src_addr()] = ack self.last_api_est_acks[packet.src_addr()] = ack
end end
@@ -339,24 +302,24 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
if not self.sv_linked then if not self.sv_linked then
if self.est_tick_waiting == nil then if self.est_tick_waiting == nil then
self.est_start = util.time_s() self.est_start = os.clock()
self.est_last = self.est_start self.est_last = self.est_start
self.est_tick_waiting, self.est_task_done = self.est_tick_waiting, self.est_task_done =
coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. svr_channel) coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SVR_Channel)
_send_establish() _send_establish()
else else
self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (util.time_s() - self.est_start))) self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (os.clock() - self.est_start)))
end end
if abort or (util.time_s() - self.est_start) >= LINK_TIMEOUT then if abort or (os.clock() - self.est_start) >= LINK_TIMEOUT then
self.est_task_done(false) self.est_task_done(false)
if abort then if abort then
coordinator.log_comms("supervisor connection attempt cancelled by user") coordinator.log_comms("supervisor connection attempt cancelled by user")
elseif self.sv_config_err then elseif self.sv_config_err then
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file") coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
elseif not self.sv_linked then elseif not self.sv_linked then
if self.last_est_ack == ESTABLISH_ACK.DENY then if self.last_est_ack == ESTABLISH_ACK.DENY then
coordinator.log_comms("supervisor connection attempt denied") coordinator.log_comms("supervisor connection attempt denied")
@@ -371,11 +334,11 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
ok = false ok = false
elseif self.sv_config_err then elseif self.sv_config_err then
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file") coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
ok = false ok = false
elseif (util.time_s() - self.est_last) > 1.0 then elseif (os.clock() - self.est_last) > 1.0 then
_send_establish() _send_establish()
self.est_last = util.time_s() self.est_last = os.clock()
end end
elseif self.est_tick_waiting ~= nil then elseif self.est_tick_waiting ~= nil then
self.est_task_done(true) self.est_task_done(true)
@@ -405,10 +368,10 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
end end
-- send the auto process control configuration with a start command -- send the auto process control configuration with a start command
---@param config coord_auto_config configuration ---@param auto_cfg coord_auto_config configuration
function public.send_auto_start(config) function public.send_auto_start(auto_cfg)
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, { _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits FAC_COMMAND.START, auto_cfg.mode, auto_cfg.burn_target, auto_cfg.charge_target, auto_cfg.gen_target, auto_cfg.limits
}) })
end end
@@ -464,9 +427,9 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
local src_addr = packet.scada_frame.src_addr() local src_addr = packet.scada_frame.src_addr()
local protocol = packet.scada_frame.protocol() local protocol = packet.scada_frame.protocol()
if l_chan ~= crd_channel then if l_chan ~= config.CRD_Channel then
log.debug("received packet on unconfigured channel " .. l_chan, true) log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == pkt_channel then elseif r_chan == config.PKT_Channel then
if not self.sv_linked then if not self.sv_linked then
log.debug("discarding pocket API packet before linked to supervisor") log.debug("discarding pocket API packet before linked to supervisor")
elseif protocol == PROTOCOL.SCADA_CRDN then elseif protocol == PROTOCOL.SCADA_CRDN then
@@ -526,7 +489,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
else else
log.debug("illegal packet type " .. protocol .. " on pocket channel", true) log.debug("illegal packet type " .. protocol .. " on pocket channel", true)
end end
elseif r_chan == svr_channel then elseif r_chan == config.SVR_Channel then
-- check sequence number -- check sequence number
if self.sv_r_seq_num == nil then if self.sv_r_seq_num == nil then
self.sv_r_seq_num = packet.scada_frame.seq_num() self.sv_r_seq_num = packet.scada_frame.seq_num()
@@ -699,24 +662,24 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
-- connection with supervisor established -- connection with supervisor established
if packet.length == 2 then if packet.length == 2 then
local est_ack = packet.data[1] local est_ack = packet.data[1]
local config = packet.data[2] local sv_config = packet.data[2]
if est_ack == ESTABLISH_ACK.ALLOW then if est_ack == ESTABLISH_ACK.ALLOW then
-- reset to disconnected before validating -- reset to disconnected before validating
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED) iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
if type(config) == "table" and #config == 2 then if type(sv_config) == "table" and #sv_config == 2 then
-- get configuration -- get configuration
---@class facility_conf ---@class facility_conf
local conf = { local conf = {
num_units = config[1], ---@type integer num_units = sv_config[1], ---@type integer
cooling = config[2] ---@type sv_cooling_conf cooling = sv_config[2] ---@type sv_cooling_conf
} }
if conf.num_units == num_units then if conf.num_units == config.UnitCount then
-- init io controller -- init io controller
iocontrol.init(conf, public) iocontrol.init(conf, public, config.TempScale)
self.sv_addr = src_addr self.sv_addr = src_addr
self.sv_linked = true self.sv_linked = true

View File

@@ -47,7 +47,23 @@ end
-- initialize the coordinator IO controller -- initialize the coordinator IO controller
---@param conf facility_conf configuration ---@param conf facility_conf configuration
---@param comms coord_comms comms reference ---@param comms coord_comms comms reference
function iocontrol.init(conf, comms) ---@param temp_scale integer temperature unit (1 = K, 2 = C, 3 = F, 4 = R)
function iocontrol.init(conf, comms, temp_scale)
-- temperature unit label and conversion function (from Kelvin)
if temp_scale == 2 then
io.temp_label = "\xb0C"
io.temp_convert = function (t) return t - 273.15 end
elseif temp_scale == 3 then
io.temp_label = "\xb0F"
io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end
elseif temp_scale == 4 then
io.temp_label = "\xb0R"
io.temp_convert = function (t) return 1.8 * t end
else
io.temp_label = "K"
io.temp_convert = function (t) return t end
end
-- facility data structure -- facility data structure
---@class ioctl_facility ---@class ioctl_facility
io.facility = { io.facility = {
@@ -110,9 +126,9 @@ function iocontrol.init(conf, comms)
-- determine tank information -- determine tank information
if io.facility.tank_mode == 0 then if io.facility.tank_mode == 0 then
io.facility.tank_defs = {} io.facility.tank_defs = {}
-- on facility tank mode 0, setup tank defs to match unit TANK option -- on facility tank mode 0, setup tank defs to match unit tank option
for i = 1, conf.num_units do for i = 1, conf.num_units do
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TANK, 1, 0) io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TankConnection, 1, 0)
end end
io.facility.tank_list = { table.unpack(io.facility.tank_defs) } io.facility.tank_list = { table.unpack(io.facility.tank_defs) }
@@ -214,12 +230,15 @@ function iocontrol.init(conf, comms)
num_boilers = 0, num_boilers = 0,
num_turbines = 0, num_turbines = 0,
num_snas = 0, num_snas = 0,
has_tank = conf.cooling.r_cool[i].TANK, has_tank = conf.cooling.r_cool[i].TankConnection,
control_state = false, control_state = false,
burn_rate_cmd = 0.0, burn_rate_cmd = 0.0,
radiation = types.new_zero_radiation_reading(), radiation = types.new_zero_radiation_reading(),
sna_prod_rate = 0.0,
sna_peak_rate = 0.0,
sna_max_rate = 0.0,
sna_out_rate = 0.0,
waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM, waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
waste_product = types.WASTE_PRODUCT.PLUTONIUM, waste_product = types.WASTE_PRODUCT.PLUTONIUM,
@@ -295,13 +314,13 @@ function iocontrol.init(conf, comms)
end end
-- create boiler tables -- create boiler tables
for _ = 1, conf.cooling.r_cool[i].BOILERS do for _ = 1, conf.cooling.r_cool[i].BoilerCount do
table.insert(entry.boiler_ps_tbl, psil.create()) table.insert(entry.boiler_ps_tbl, psil.create())
table.insert(entry.boiler_data_tbl, {}) table.insert(entry.boiler_data_tbl, {})
end end
-- create turbine tables -- create turbine tables
for _ = 1, conf.cooling.r_cool[i].TURBINES do for _ = 1, conf.cooling.r_cool[i].TurbineCount do
table.insert(entry.turbine_ps_tbl, psil.create()) table.insert(entry.turbine_ps_tbl, psil.create())
table.insert(entry.turbine_data_tbl, {}) table.insert(entry.turbine_data_tbl, {})
end end
@@ -469,7 +488,7 @@ function iocontrol.record_unit_builds(builds)
-- note: if not all units and RTUs are connected, some will be nil -- note: if not all units and RTUs are connected, some will be nil
for id, build in pairs(builds) do for id, build in pairs(builds) do
local unit = io.units[id] ---@type ioctl_unit local unit = io.units[id] ---@type ioctl_unit
local log_header = util.c("iocontrol.record_unit_builds[UNIT ", id, "]: ") local log_header = util.c("iocontrol.record_unit_builds[UNIT ", id, "]: ")
@@ -694,8 +713,8 @@ function iocontrol.update_facility_status(status)
for id, sps in pairs(rtu_statuses.sps) do for id, sps in pairs(rtu_statuses.sps) do
if type(fac.sps_data_tbl[id]) == "table" then if type(fac.sps_data_tbl[id]) == "table" then
local data = fac.sps_data_tbl[id] ---@type sps_session_db local data = fac.sps_data_tbl[id] ---@type sps_session_db
local ps = fac.sps_ps_tbl[id] ---@type psil local ps = fac.sps_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(sps, data, ps) local rtu_faulted = _record_multiblock_status(sps, data, ps)
@@ -732,8 +751,8 @@ function iocontrol.update_facility_status(status)
for id, tank in pairs(rtu_statuses.tanks) do for id, tank in pairs(rtu_statuses.tanks) do
if type(fac.tank_data_tbl[id]) == "table" then if type(fac.tank_data_tbl[id]) == "table" then
local data = fac.tank_data_tbl[id] ---@type dynamicv_session_db local data = fac.tank_data_tbl[id] ---@type dynamicv_session_db
local ps = fac.tank_ps_tbl[id] ---@type psil local ps = fac.tank_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(tank, data, ps) local rtu_faulted = _record_multiblock_status(tank, data, ps)
@@ -760,20 +779,34 @@ function iocontrol.update_facility_status(status)
end end
-- environment detector status -- environment detector status
if type(rtu_statuses.rad_mon) == "table" then if type(rtu_statuses.envds) == "table" then
if #rtu_statuses.rad_mon > 0 then local max_rad, max_reading, any_conn, any_faulted = 0, types.new_zero_radiation_reading(), false, false
local rad_mon = rtu_statuses.rad_mon[1]
local rtu_faulted = rad_mon[1] ---@type boolean
fac.radiation = rad_mon[2] ---@type number
fac.ps.publish("rad_computed_status", util.trinary(rtu_faulted, 2, 3)) for _, envd in pairs(rtu_statuses.envds) do
fac.ps.publish("radiation", fac.radiation) local rtu_faulted = envd[1] ---@type boolean
local radiation = envd[2] ---@type radiation_reading
local rad_raw = envd[3] ---@type number
any_conn = true
any_faulted = any_faulted or rtu_faulted
if rad_raw > max_rad then
max_rad = rad_raw
max_reading = radiation
end
end
if any_conn then
fac.radiation = max_reading
fac.ps.publish("rad_computed_status", util.trinary(any_faulted, 2, 3))
else else
fac.radiation = types.new_zero_radiation_reading() fac.radiation = types.new_zero_radiation_reading()
fac.ps.publish("rad_computed_status", 1) fac.ps.publish("rad_computed_status", 1)
end end
fac.ps.publish("radiation", fac.radiation)
else else
log.debug(log_header .. "radiation monitor list not a table") log.debug(log_header .. "environment detector list not a table")
valid = false valid = false
end end
else else
@@ -909,7 +942,7 @@ function iocontrol.update_unit_statuses(statuses)
local boil_sum = 0 local boil_sum = 0
for id = 1, #unit.boiler_ps_tbl do for id = 1, #unit.boiler_ps_tbl do
if rtu_statuses.boilers[i] == nil then if rtu_statuses.boilers[id] == nil then
-- disconnected -- disconnected
unit.boiler_ps_tbl[id].publish("computed_status", 1) unit.boiler_ps_tbl[id].publish("computed_status", 1)
end end
@@ -917,8 +950,8 @@ function iocontrol.update_unit_statuses(statuses)
for id, boiler in pairs(rtu_statuses.boilers) do for id, boiler in pairs(rtu_statuses.boilers) do
if type(unit.boiler_data_tbl[id]) == "table" then if type(unit.boiler_data_tbl[id]) == "table" then
local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
local ps = unit.boiler_ps_tbl[id] ---@type psil local ps = unit.boiler_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(boiler, data, ps) local rtu_faulted = _record_multiblock_status(boiler, data, ps)
@@ -952,7 +985,7 @@ function iocontrol.update_unit_statuses(statuses)
local flow_sum = 0 local flow_sum = 0
for id = 1, #unit.turbine_ps_tbl do for id = 1, #unit.turbine_ps_tbl do
if rtu_statuses.turbines[i] == nil then if rtu_statuses.turbines[id] == nil then
-- disconnected -- disconnected
unit.turbine_ps_tbl[id].publish("computed_status", 1) unit.turbine_ps_tbl[id].publish("computed_status", 1)
end end
@@ -960,8 +993,8 @@ function iocontrol.update_unit_statuses(statuses)
for id, turbine in pairs(rtu_statuses.turbines) do for id, turbine in pairs(rtu_statuses.turbines) do
if type(unit.turbine_data_tbl[id]) == "table" then if type(unit.turbine_data_tbl[id]) == "table" then
local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db
local ps = unit.turbine_ps_tbl[id] ---@type psil local ps = unit.turbine_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(turbine, data, ps) local rtu_faulted = _record_multiblock_status(turbine, data, ps)
@@ -995,7 +1028,7 @@ function iocontrol.update_unit_statuses(statuses)
-- dynamic tank statuses -- dynamic tank statuses
if type(rtu_statuses.tanks) == "table" then if type(rtu_statuses.tanks) == "table" then
for id = 1, #unit.tank_ps_tbl do for id = 1, #unit.tank_ps_tbl do
if rtu_statuses.tanks[i] == nil then if rtu_statuses.tanks[id] == nil then
-- disconnected -- disconnected
unit.tank_ps_tbl[id].publish("computed_status", 1) unit.tank_ps_tbl[id].publish("computed_status", 1)
end end
@@ -1033,13 +1066,15 @@ function iocontrol.update_unit_statuses(statuses)
-- solar neutron activator status info -- solar neutron activator status info
if type(rtu_statuses.sna) == "table" then if type(rtu_statuses.sna) == "table" then
unit.num_snas = rtu_statuses.sna[1] ---@type integer unit.num_snas = rtu_statuses.sna[1] ---@type integer
unit.sna_prod_rate = rtu_statuses.sna[2] ---@type number unit.sna_peak_rate = rtu_statuses.sna[2] ---@type number
unit.sna_peak_rate = rtu_statuses.sna[3] ---@type number unit.sna_max_rate = rtu_statuses.sna[3] ---@type number
unit.sna_out_rate = rtu_statuses.sna[4] ---@type number
unit.unit_ps.publish("sna_count", unit.num_snas) unit.unit_ps.publish("sna_count", unit.num_snas)
unit.unit_ps.publish("sna_prod_rate", unit.sna_prod_rate)
unit.unit_ps.publish("sna_peak_rate", unit.sna_peak_rate) unit.unit_ps.publish("sna_peak_rate", unit.sna_peak_rate)
unit.unit_ps.publish("sna_max_rate", unit.sna_max_rate)
unit.unit_ps.publish("sna_out_rate", unit.sna_out_rate)
sna_count_sum = sna_count_sum + unit.num_snas sna_count_sum = sna_count_sum + unit.num_snas
else else
@@ -1048,16 +1083,28 @@ function iocontrol.update_unit_statuses(statuses)
end end
-- environment detector status -- environment detector status
if type(rtu_statuses.rad_mon) == "table" then if type(rtu_statuses.envds) == "table" then
if #rtu_statuses.rad_mon > 0 then local max_rad, max_reading, any_conn = 0, types.new_zero_radiation_reading(), false
local rad_mon = rtu_statuses.rad_mon[1]
-- local rtu_faulted = rad_mon[1] ---@type boolean
unit.radiation = rad_mon[2] ---@type number
unit.unit_ps.publish("radiation", unit.radiation) for _, envd in pairs(rtu_statuses.envds) do
local radiation = envd[2] ---@type radiation_reading
local rad_raw = envd[3] ---@type number
any_conn = true
if rad_raw > max_rad then
max_rad = rad_raw
max_reading = radiation
end
end
if any_conn then
unit.radiation = max_reading
else else
unit.radiation = types.new_zero_radiation_reading() unit.radiation = types.new_zero_radiation_reading()
end end
unit.unit_ps.publish("radiation", unit.radiation)
else else
log.debug(log_header .. "radiation monitor list not a table") log.debug(log_header .. "radiation monitor list not a table")
valid = false valid = false
@@ -1175,7 +1222,7 @@ function iocontrol.update_unit_statuses(statuses)
local u_spent_rate = waste_rate local u_spent_rate = waste_rate
local u_pu_rate = util.trinary(is_pu, waste_rate, 0.0) local u_pu_rate = util.trinary(is_pu, waste_rate, 0.0)
local u_po_rate = util.trinary(not is_pu, math.min(waste_rate, unit.sna_prod_rate), 0.0) local u_po_rate = unit.sna_out_rate
unit.unit_ps.publish("pu_rate", u_pu_rate) unit.unit_ps.publish("pu_rate", u_pu_rate)
unit.unit_ps.publish("po_rate", u_po_rate) unit.unit_ps.publish("po_rate", u_po_rate)
@@ -1183,14 +1230,15 @@ function iocontrol.update_unit_statuses(statuses)
unit.unit_ps.publish("sna_in", util.trinary(is_pu, 0, burn_rate)) unit.unit_ps.publish("sna_in", util.trinary(is_pu, 0, burn_rate))
if unit.waste_product == types.WASTE_PRODUCT.POLONIUM then if unit.waste_product == types.WASTE_PRODUCT.POLONIUM then
u_spent_rate = u_po_rate
unit.unit_ps.publish("po_pl_rate", u_po_rate) unit.unit_ps.publish("po_pl_rate", u_po_rate)
unit.unit_ps.publish("po_am_rate", 0) unit.unit_ps.publish("po_am_rate", 0)
po_pl_rate = po_pl_rate + u_po_rate po_pl_rate = po_pl_rate + u_po_rate
elseif unit.waste_product == types.WASTE_PRODUCT.ANTI_MATTER then elseif unit.waste_product == types.WASTE_PRODUCT.ANTI_MATTER then
u_spent_rate = 0
unit.unit_ps.publish("po_pl_rate", 0) unit.unit_ps.publish("po_pl_rate", 0)
unit.unit_ps.publish("po_am_rate", u_po_rate) unit.unit_ps.publish("po_am_rate", u_po_rate)
po_am_rate = po_am_rate + u_po_rate po_am_rate = po_am_rate + u_po_rate
u_spent_rate = 0
else else
unit.unit_ps.publish("po_pl_rate", 0) unit.unit_ps.publish("po_pl_rate", 0)
unit.unit_ps.publish("po_am_rate", 0) unit.unit_ps.publish("po_am_rate", 0)

View File

@@ -19,15 +19,20 @@ local process = {}
local self = { local self = {
io = nil, ---@type ioctl io = nil, ---@type ioctl
comms = nil, ---@type coord_comms comms = nil, ---@type coord_comms
---@class coord_auto_config ---@class coord_control_states
config = { control_states = {
mode = PROCESS.INACTIVE, ---@class coord_auto_config
burn_target = 0.0, process = {
charge_target = 0.0, mode = PROCESS.INACTIVE,
gen_target = 0.0, burn_target = 0.0,
limits = {}, charge_target = 0.0,
waste_product = PRODUCT.PLUTONIUM, gen_target = 0.0,
pu_fallback = false limits = {},
waste_product = PRODUCT.PLUTONIUM,
pu_fallback = false
},
waste_modes = {},
priority_groups = {}
} }
} }
@@ -42,63 +47,64 @@ function process.init(iocontrol, coord_comms)
self.io = iocontrol self.io = iocontrol
self.comms = coord_comms self.comms = coord_comms
local ctl_proc = self.control_states.process
for i = 1, self.io.facility.num_units do for i = 1, self.io.facility.num_units do
self.config.limits[i] = 0.1 ctl_proc.limits[i] = 0.1
end end
-- load settings local ctrl_states = settings.get("ControlStates", {})
if not settings.load("/coord.settings") then local config = ctrl_states.process ---@type coord_auto_config
log.error("process.init(): failed to load coordinator settings file")
end
-- facility auto control configuration -- facility auto control configuration
local config = settings.get("PROCESS") ---@type coord_auto_config|nil
if type(config) == "table" then if type(config) == "table" then
self.config.mode = config.mode ctl_proc.mode = config.mode
self.config.burn_target = config.burn_target ctl_proc.burn_target = config.burn_target
self.config.charge_target = config.charge_target ctl_proc.charge_target = config.charge_target
self.config.gen_target = config.gen_target ctl_proc.gen_target = config.gen_target
self.config.limits = config.limits ctl_proc.limits = config.limits
self.config.waste_product = config.waste_product ctl_proc.waste_product = config.waste_product
self.config.pu_fallback = config.pu_fallback ctl_proc.pu_fallback = config.pu_fallback
self.io.facility.ps.publish("process_mode", self.config.mode) self.io.facility.ps.publish("process_mode", ctl_proc.mode)
self.io.facility.ps.publish("process_burn_target", self.config.burn_target) self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
self.io.facility.ps.publish("process_charge_target", self.config.charge_target) self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target)
self.io.facility.ps.publish("process_gen_target", self.config.gen_target) self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target)
self.io.facility.ps.publish("process_waste_product", self.config.waste_product) self.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
self.io.facility.ps.publish("process_pu_fallback", self.config.pu_fallback) self.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback)
for id = 1, math.min(#self.config.limits, self.io.facility.num_units) do for id = 1, math.min(#ctl_proc.limits, self.io.facility.num_units) do
local unit = self.io.units[id] ---@type ioctl_unit local unit = self.io.units[id] ---@type ioctl_unit
unit.unit_ps.publish("burn_limit", self.config.limits[id]) unit.unit_ps.publish("burn_limit", ctl_proc.limits[id])
end end
log.info("PROCESS: loaded auto control settings from coord.settings") log.info("PROCESS: loaded auto control settings")
-- notify supervisor of auto waste config -- notify supervisor of auto waste config
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, self.config.waste_product) self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, ctl_proc.waste_product)
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, self.config.pu_fallback) self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, ctl_proc.pu_fallback)
end end
-- unit waste states -- unit waste states
local waste_modes = settings.get("WASTE_MODES") ---@type table|nil local waste_modes = ctrl_states.waste_modes ---@type table|nil
if type(waste_modes) == "table" then if type(waste_modes) == "table" then
for id, mode in pairs(waste_modes) do for id, mode in pairs(waste_modes) do
self.control_states.waste_modes[id] = mode
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode) self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
end end
log.info("PROCESS: loaded unit waste mode settings from coord.settings") log.info("PROCESS: loaded unit waste mode settings")
end end
-- unit priority groups -- unit priority groups
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil local prio_groups = ctrl_states.priority_groups ---@type table|nil
if type(prio_groups) == "table" then if type(prio_groups) == "table" then
for id, group in pairs(prio_groups) do for id, group in pairs(prio_groups) do
self.control_states.priority_groups[id] = group
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group) self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group)
end end
log.info("PROCESS: loaded priority groups settings from coord.settings") log.info("PROCESS: loaded priority groups settings")
end end
end end
@@ -155,15 +161,10 @@ function process.set_unit_waste(id, mode)
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode) self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode)) log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil self.control_states.waste_modes[id] = mode
settings.set("ControlStates", self.control_states)
if type(waste_mode) ~= "table" then waste_mode = {} end if not settings.save("/coordinator.settings") then
waste_mode[id] = mode
settings.set("WASTE_MODES", waste_mode)
if not settings.save("/coord.settings") then
log.error("process.set_unit_waste(): failed to save coordinator settings file") log.error("process.set_unit_waste(): failed to save coordinator settings file")
end end
end end
@@ -198,15 +199,10 @@ function process.set_group(unit_id, group_id)
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, unit_id, group_id) self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, unit_id, group_id)
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id)) log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil self.control_states.priority_groups[unit_id] = group_id
settings.set("ControlStates", self.control_states)
if type(prio_groups) ~= "table" then prio_groups = {} end if not settings.save("/coordinator.settings") then
prio_groups[unit_id] = group_id
settings.set("PRIORITY_GROUPS", prio_groups)
if not settings.save("/coord.settings") then
log.error("process.set_group(): failed to save coordinator settings file") log.error("process.set_group(): failed to save coordinator settings file")
end end
end end
@@ -217,20 +213,14 @@ end
-- write auto process control to config file -- write auto process control to config file
local function _write_auto_config() local function _write_auto_config()
-- attempt to load settings
if not settings.load("/coord.settings") then
log.warning("process._write_auto_config(): failed to load coordinator settings file")
end
-- save config -- save config
settings.set("PROCESS", self.config) settings.set("ControlStates", self.control_states)
local saved = settings.save("/coord.settings") local saved = settings.save("/coordinator.settings")
if not saved then if not saved then
log.warning("process._write_auto_config(): failed to save coordinator settings file") log.warning("process._write_auto_config(): failed to save coordinator settings file")
end end
return not not saved return saved
end end
-- stop automatic process control -- stop automatic process control
@@ -241,7 +231,7 @@ end
-- start automatic process control -- start automatic process control
function process.start_auto() function process.start_auto()
self.comms.send_auto_start(self.config) self.comms.send_auto_start(self.control_states.process)
log.debug("PROCESS: START AUTO CTL") log.debug("PROCESS: START AUTO CTL")
end end
@@ -253,7 +243,7 @@ function process.set_process_waste(product)
log.debug(util.c("PROCESS: SET WASTE ", product)) log.debug(util.c("PROCESS: SET WASTE ", product))
-- update config table and save -- update config table and save
self.config.waste_product = product self.control_states.process.waste_product = product
_write_auto_config() _write_auto_config()
end end
@@ -265,7 +255,7 @@ function process.set_pu_fallback(enabled)
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled)) log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
-- update config table and save -- update config table and save
self.config.pu_fallback = enabled self.control_states.process.pu_fallback = enabled
_write_auto_config() _write_auto_config()
end end
@@ -279,11 +269,12 @@ function process.save(mode, burn_target, charge_target, gen_target, limits)
log.debug("PROCESS: SAVE") log.debug("PROCESS: SAVE")
-- update config table -- update config table
self.config.mode = mode local ctl_proc = self.control_states.process
self.config.burn_target = burn_target ctl_proc.mode = mode
self.config.charge_target = charge_target ctl_proc.burn_target = burn_target
self.config.gen_target = gen_target ctl_proc.charge_target = charge_target
self.config.limits = limits ctl_proc.gen_target = gen_target
ctl_proc.limits = limits
-- save config -- save config
self.io.facility.save_cfg_ack(_write_auto_config()) self.io.facility.save_cfg_ack(_write_auto_config())
@@ -294,22 +285,23 @@ end
function process.start_ack_handle(response) function process.start_ack_handle(response)
local ack = response[1] local ack = response[1]
self.config.mode = response[2] local ctl_proc = self.control_states.process
self.config.burn_target = response[3] ctl_proc.mode = response[2]
self.config.charge_target = response[4] ctl_proc.burn_target = response[3]
self.config.gen_target = response[5] ctl_proc.charge_target = response[4]
ctl_proc.gen_target = response[5]
for i = 1, math.min(#response[6], self.io.facility.num_units) do for i = 1, math.min(#response[6], self.io.facility.num_units) do
self.config.limits[i] = response[6][i] ctl_proc.limits[i] = response[6][i]
local unit = self.io.units[i] ---@type ioctl_unit local unit = self.io.units[i] ---@type ioctl_unit
unit.unit_ps.publish("burn_limit", self.config.limits[i]) unit.unit_ps.publish("burn_limit", ctl_proc.limits[i])
end end
self.io.facility.ps.publish("process_mode", self.config.mode) self.io.facility.ps.publish("process_mode", ctl_proc.mode)
self.io.facility.ps.publish("process_burn_target", self.config.burn_target) self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
self.io.facility.ps.publish("process_charge_target", self.config.charge_target) self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target)
self.io.facility.ps.publish("process_gen_target", self.config.gen_target) self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target)
self.io.facility.start_ack(ack) self.io.facility.start_ack(ack)
end end
@@ -317,14 +309,14 @@ end
-- record waste product state after attempting to change it -- record waste product state after attempting to change it
---@param response WASTE_PRODUCT supervisor waste product state ---@param response WASTE_PRODUCT supervisor waste product state
function process.waste_ack_handle(response) function process.waste_ack_handle(response)
self.config.waste_product = response self.control_states.process.waste_product = response
self.io.facility.ps.publish("process_waste_product", response) self.io.facility.ps.publish("process_waste_product", response)
end end
-- record plutonium fallback state after attempting to change it -- record plutonium fallback state after attempting to change it
---@param response boolean supervisor plutonium fallback state ---@param response boolean supervisor plutonium fallback state
function process.pu_fb_ack_handle(response) function process.pu_fb_ack_handle(response)
self.config.pu_fallback = response self.control_states.process.pu_fallback = response
self.io.facility.ps.publish("process_pu_fallback", response) self.io.facility.ps.publish("process_pu_fallback", response)
end end

View File

@@ -3,7 +3,6 @@
-- --
local log = require("scada-common.log") local log = require("scada-common.log")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
@@ -53,6 +52,16 @@ local function _init_display(monitor)
end end
end end
-- print out that the monitor is too small
---@param monitor table monitor
local function _print_too_small(monitor)
monitor.setCursorPos(1, 1)
monitor.setBackgroundColor(colors.black)
monitor.setTextColor(colors.red)
monitor.clear()
monitor.write("monitor too small")
end
-- disable the flow view -- disable the flow view
---@param disable boolean ---@param disable boolean
function renderer.legacy_disable_flow_view(disable) function renderer.legacy_disable_flow_view(disable)
@@ -65,15 +74,15 @@ function renderer.set_displays(monitors)
engine.monitors = monitors engine.monitors = monitors
-- report to front panel as connected -- report to front panel as connected
iocontrol.fp_monitor_state("main", engine.monitors.primary ~= nil) iocontrol.fp_monitor_state("main", engine.monitors.main ~= nil)
iocontrol.fp_monitor_state("flow", engine.monitors.flow ~= nil) iocontrol.fp_monitor_state("flow", engine.monitors.flow ~= nil)
for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end
end end
-- init all displays in use by the renderer -- init all displays in use by the renderer
function renderer.init_displays() function renderer.init_displays()
-- init primary and flow monitors -- init main and flow monitors
_init_display(engine.monitors.primary) _init_display(engine.monitors.main)
if not engine.disable_flow_view then _init_display(engine.monitors.flow) end if not engine.disable_flow_view then _init_display(engine.monitors.flow) end
-- init unit displays -- init unit displays
@@ -93,43 +102,10 @@ function renderer.init_displays()
end end
end end
-- check main display width
---@nodiscard
---@return boolean width_okay
function renderer.validate_main_display_width()
local w, _ = engine.monitors.primary.getSize()
return w == 164
end
-- check flow display width
---@nodiscard
---@return boolean width_okay
function renderer.validate_flow_display_width()
local w, _ = engine.monitors.flow.getSize()
return w == 164
end
-- check display sizes
---@nodiscard
---@return boolean valid all unit display dimensions OK
function renderer.validate_unit_display_sizes()
local valid = true
for id, monitor in ipairs(engine.monitors.unit_displays) do
local w, h = monitor.getSize()
if w ~= 79 or h ~= 52 then
log.warning(util.c("RENDERER: unit ", id, " display resolution not 79 wide by 52 tall: ", w, ", ", h))
valid = false
end
end
return valid
end
-- initialize the dmesg output window -- initialize the dmesg output window
function renderer.init_dmesg() function renderer.init_dmesg()
local disp_x, disp_y = engine.monitors.primary.getSize() local disp_w, disp_h = engine.monitors.main.getSize()
engine.dmesg_window = window.create(engine.monitors.primary, 1, 1, disp_x, disp_y) engine.dmesg_window = window.create(engine.monitors.main, 1, 1, disp_w, disp_h)
log.direct_dmesg(engine.dmesg_window) log.direct_dmesg(engine.dmesg_window)
end end
@@ -200,8 +176,8 @@ function renderer.try_start_ui()
status, msg = pcall(function () status, msg = pcall(function ()
-- show main view on main monitor -- show main view on main monitor
if engine.monitors.primary ~= nil then if engine.monitors.main ~= nil then
engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root} engine.ui.main_display = DisplayBox{window=engine.monitors.main,fg_bg=style.root}
main_view(engine.ui.main_display) main_view(engine.ui.main_display)
end end
@@ -276,43 +252,43 @@ function renderer.ui_ready() return engine.ui_ready end
function renderer.handle_disconnect(device) function renderer.handle_disconnect(device)
local is_used = false local is_used = false
if engine.monitors ~= nil then if not engine.monitors then return false end
if engine.monitors.primary == device then
if engine.ui.main_display ~= nil then
-- delete element tree and clear root UI elements
engine.ui.main_display.delete()
end
is_used = true if engine.monitors.main == device then
engine.monitors.primary = nil if engine.ui.main_display ~= nil then
engine.ui.main_display = nil -- delete element tree and clear root UI elements
engine.ui.main_display.delete()
end
iocontrol.fp_monitor_state("main", false) is_used = true
elseif engine.monitors.flow == device then engine.monitors.main = nil
if engine.ui.flow_display ~= nil then engine.ui.main_display = nil
-- delete element tree and clear root UI elements
engine.ui.flow_display.delete()
end
is_used = true iocontrol.fp_monitor_state("main", false)
engine.monitors.flow = nil elseif engine.monitors.flow == device then
engine.ui.flow_display = nil if engine.ui.flow_display ~= nil then
-- delete element tree and clear root UI elements
engine.ui.flow_display.delete()
end
iocontrol.fp_monitor_state("flow", false) is_used = true
else engine.monitors.flow = nil
for idx, monitor in pairs(engine.monitors.unit_displays) do engine.ui.flow_display = nil
if monitor == device then
if engine.ui.unit_displays[idx] ~= nil then
engine.ui.unit_displays[idx].delete()
end
is_used = true iocontrol.fp_monitor_state("flow", false)
engine.monitors.unit_displays[idx] = nil else
engine.ui.unit_displays[idx] = nil for idx, monitor in pairs(engine.monitors.unit_displays) do
if monitor == device then
iocontrol.fp_monitor_state(idx, false) if engine.ui.unit_displays[idx] ~= nil then
break engine.ui.unit_displays[idx].delete()
end end
is_used = true
engine.monitors.unit_displays[idx] = nil
engine.ui.unit_displays[idx] = nil
iocontrol.fp_monitor_state(idx, false)
break
end end
end end
end end
@@ -327,52 +303,29 @@ end
function renderer.handle_reconnect(name, device) function renderer.handle_reconnect(name, device)
local is_used = false local is_used = false
if engine.monitors ~= nil then if not engine.monitors then return false end
if engine.monitors.primary_name == name then
is_used = true
_init_display(device)
engine.monitors.primary = device
local disp_x, disp_y = engine.monitors.primary.getSize() -- note: handle_resize is a more adaptive way of re-initializing a connected monitor
engine.dmesg_window.reposition(1, 1, disp_x, disp_y, engine.monitors.primary) -- since it can handle a monitor being reconnected that isn't the right size
if engine.ui_ready and (engine.ui.main_display == nil) then if engine.monitors.main_name == name then
engine.dmesg_window.setVisible(false) is_used = true
engine.monitors.main = device
engine.ui.main_display = DisplayBox{window=device,fg_bg=style.root} renderer.handle_resize(name)
main_view(engine.ui.main_display) elseif engine.monitors.flow_name == name then
else is_used = true
engine.dmesg_window.setVisible(true) engine.monitors.flow = device
engine.dmesg_window.redraw()
end
iocontrol.fp_monitor_state("main", true) renderer.handle_resize(name)
elseif engine.monitors.flow_name == name then else
is_used = true for idx, monitor in ipairs(engine.monitors.unit_name_map) do
_init_display(device) if monitor == name then
engine.monitors.flow = device is_used = true
engine.monitors.unit_displays[idx] = device
if engine.ui_ready and (engine.ui.flow_display == nil) then renderer.handle_resize(name)
engine.ui.flow_display = DisplayBox{window=device,fg_bg=style.root} break
flow_view(engine.ui.flow_display)
end
iocontrol.fp_monitor_state("flow", true)
else
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
if monitor == name then
is_used = true
_init_display(device)
engine.monitors.unit_displays[idx] = device
if engine.ui_ready and (engine.ui.unit_displays[idx] == nil) then
engine.ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
unit_view(engine.ui.unit_displays[idx], idx)
end
iocontrol.fp_monitor_state(idx, true)
break
end
end end
end end
end end
@@ -380,6 +333,137 @@ function renderer.handle_reconnect(name, device)
return is_used return is_used
end end
-- handle a monitor being resized<br>
-- returns if this monitor is assigned + if the assigned screen still fits
---@param name string monitor name
---@return boolean is_used, boolean is_ok
function renderer.handle_resize(name)
local is_used = false
local is_ok = true
local ui = engine.ui
if not engine.monitors then return false, false end
if engine.monitors.main_name == name and engine.monitors.main then
local device = engine.monitors.main ---@type table
-- this is necessary if the bottom left block was broken and on reconnect
_init_display(device)
is_used = true
-- resize dmesg window if needed, but don't make it thinner
local disp_w, disp_h = engine.monitors.main.getSize()
local dmsg_w, _ = engine.dmesg_window.getSize()
engine.dmesg_window.reposition(1, 1, math.max(disp_w, dmsg_w), disp_h, engine.monitors.main)
if ui.main_display then
ui.main_display.delete()
ui.main_display = nil
end
iocontrol.fp_monitor_state("main", true)
engine.dmesg_window.setVisible(not engine.ui_ready)
if engine.ui_ready then
local ok = pcall(function ()
ui.main_display = DisplayBox{window=device,fg_bg=style.root}
main_view(ui.main_display)
end)
if not ok then
if ui.main_display then
ui.main_display.delete()
ui.main_display = nil
end
_print_too_small(device)
iocontrol.fp_monitor_state("main", false)
is_ok = false
end
else engine.dmesg_window.redraw() end
elseif engine.monitors.flow_name == name and engine.monitors.flow then
local device = engine.monitors.flow ---@type table
-- this is necessary if the bottom left block was broken and on reconnect
_init_display(device)
is_used = true
if ui.flow_display then
ui.flow_display.delete()
ui.flow_display = nil
end
iocontrol.fp_monitor_state("flow", true)
if engine.ui_ready then
engine.dmesg_window.setVisible(false)
local ok = pcall(function ()
ui.flow_display = DisplayBox{window=device,fg_bg=style.root}
flow_view(ui.flow_display)
end)
if not ok then
if ui.flow_display then
ui.flow_display.delete()
ui.flow_display = nil
end
_print_too_small(device)
iocontrol.fp_monitor_state("flow", false)
is_ok = false
end
end
else
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
local device = engine.monitors.unit_displays[idx]
if monitor == name and device then
-- this is necessary if the bottom left block was broken and on reconnect
_init_display(device)
is_used = true
if ui.unit_displays[idx] then
ui.unit_displays[idx].delete()
ui.unit_displays[idx] = nil
end
iocontrol.fp_monitor_state(idx, true)
if engine.ui_ready then
engine.dmesg_window.setVisible(false)
local ok = pcall(function ()
ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
unit_view(ui.unit_displays[idx], idx)
end)
if not ok then
if ui.unit_displays[idx] then
ui.unit_displays[idx].delete()
ui.unit_displays[idx] = nil
end
_print_too_small(device)
iocontrol.fp_monitor_state(idx, false)
is_ok = false
end
end
break
end
end
end
return is_used, is_ok
end
-- handle a touch event -- handle a touch event
---@param event mouse_interaction|nil ---@param event mouse_interaction|nil
@@ -388,16 +472,15 @@ function renderer.handle_mouse(event)
if engine.fp_ready and event.monitor == "terminal" then if engine.fp_ready and event.monitor == "terminal" then
engine.ui.front_panel.handle_mouse(event) engine.ui.front_panel.handle_mouse(event)
elseif engine.ui_ready then elseif engine.ui_ready then
if event.monitor == engine.monitors.primary_name then if event.monitor == engine.monitors.main_name then
engine.ui.main_display.handle_mouse(event) if engine.ui.main_display then engine.ui.main_display.handle_mouse(event) end
elseif event.monitor == engine.monitors.flow_name then elseif event.monitor == engine.monitors.flow_name then
engine.ui.flow_display.handle_mouse(event) if engine.ui.flow_display then engine.ui.flow_display.handle_mouse(event) end
else else
for id, monitor in ipairs(engine.monitors.unit_name_map) do for id, monitor in ipairs(engine.monitors.unit_name_map) do
if event.monitor == monitor then local display = engine.ui.unit_displays[id]
local layout = engine.ui.unit_displays[id] ---@type graphics_element if event.monitor == monitor and display then
layout.handle_mouse(event) if display then display.handle_mouse(event) end
break
end end
end end
end end

View File

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

View File

@@ -14,7 +14,7 @@ local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local config = require("coordinator.config") local configure = require("coordinator.configure")
local coordinator = require("coordinator.coordinator") local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local renderer = require("coordinator.renderer") local renderer = require("coordinator.renderer")
@@ -22,7 +22,9 @@ local sounder = require("coordinator.sounder")
local apisessions = require("coordinator.session.apisessions") local apisessions = require("coordinator.session.apisessions")
local COORDINATOR_VERSION = "v1.0.16" local COORDINATOR_VERSION = "v1.2.11"
local CHUNK_LOAD_DELAY_S = 30.0
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@@ -34,32 +36,66 @@ local log_comms = coordinator.log_comms
local log_crypto = coordinator.log_crypto local log_crypto = coordinator.log_crypto
---------------------------------------- ----------------------------------------
-- config validation -- get configuration
---------------------------------------- ----------------------------------------
local cfv = util.new_validator() -- mount connected devices (required for monitor setup)
ppm.mount_all()
cfv.assert_channel(config.SVR_CHANNEL) local wait_on_load = true
cfv.assert_channel(config.CRD_CHANNEL) local loaded, monitors = coordinator.load_config()
cfv.assert_channel(config.PKT_CHANNEL)
cfv.assert_type_int(config.TRUSTED_RANGE)
cfv.assert_type_num(config.SV_TIMEOUT)
cfv.assert_min(config.SV_TIMEOUT, 2)
cfv.assert_type_num(config.API_TIMEOUT)
cfv.assert_min(config.API_TIMEOUT, 2)
cfv.assert_type_int(config.NUM_UNITS)
cfv.assert_type_num(config.SOUNDER_VOLUME)
cfv.assert_type_bool(config.TIME_24_HOUR)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
assert(cfv.valid(), "bad config file: missing/invalid fields") -- if the computer just started, its chunk may have just loaded (...or the user rebooted)
-- if monitor config failed, maybe an adjacent chunk containing all or part of a monitor has not loaded yet, so keep trying
while wait_on_load and loaded == 2 and os.clock() < CHUNK_LOAD_DELAY_S do
term.clear()
term.setCursorPos(1, 1)
println("There was a monitor configuration problem at boot.\n")
println("Startup will keep trying every 2s in case of chunk load delays.\n")
println(util.sprintf("The configurator will be started in %ds if all attempts fail.\n", math.max(0, CHUNK_LOAD_DELAY_S - os.clock())))
println("(click to skip to the configurator)")
local timer_id = util.start_timer(2)
while true do
local event, param1 = util.pull_event()
if event == "timer" and param1 == timer_id then
-- remount and re-attempt
ppm.mount_all()
loaded, monitors = coordinator.load_config()
break
elseif event == "mouse_click" or event == "terminate" then
wait_on_load = false
break
end
end
end
if loaded ~= 0 then
-- try to reconfigure (user action)
local success, error = configure.configure(loaded, monitors)
if success then
loaded, monitors = coordinator.load_config()
if loaded ~= 0 then
println(util.trinary(loaded == 2, "monitor configuration invalid", "failed to load a valid configuration") .. ", please reconfigure")
return
end
else
println("configuration error: " .. error)
return
end
end
-- passed checks, good now
---@cast monitors monitors_struct
local config = coordinator.config
---------------------------------------- ----------------------------------------
-- log init -- log init
---------------------------------------- ----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true) log.init(config.LogPath, config.LogMode, config.LogDebug)
log.info("========================================") log.info("========================================")
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION) log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
@@ -77,39 +113,16 @@ local function main()
-- system startup -- system startup
---------------------------------------- ----------------------------------------
-- mount connected devices -- log mounts now since mounting was done before logging was ready
ppm.mount_all() ppm.log_mounts()
-- report versions/init fp PSIL -- report versions/init fp PSIL
iocontrol.init_fp(COORDINATOR_VERSION, comms.version) iocontrol.init_fp(COORDINATOR_VERSION, comms.version)
-- setup monitors
local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS, config.DISABLE_FLOW_VIEW == true)
if not configured or monitors == nil then
println("startup> monitor setup failed")
log.fatal("monitor configuration failed")
return
end
-- init renderer -- init renderer
renderer.legacy_disable_flow_view(config.DISABLE_FLOW_VIEW == true) renderer.legacy_disable_flow_view(config.DisableFlowView)
renderer.set_displays(monitors) renderer.set_displays(monitors)
renderer.init_displays() renderer.init_displays()
if not renderer.validate_main_display_width() then
println("startup> main display must be 8 blocks wide")
log.fatal("main display not wide enough")
return
elseif (config.DISABLE_FLOW_VIEW ~= true) and not renderer.validate_flow_display_width() then
println("startup> flow display must be 8 blocks wide")
log.fatal("flow display not wide enough")
return
elseif not renderer.validate_unit_display_sizes() then
println("startup> one or more unit display dimensions incorrect; they must be 4x4 blocks")
log.fatal("unit display dimensions incorrect")
return
end
renderer.init_dmesg() renderer.init_dmesg()
-- lets get started! -- lets get started!
@@ -132,7 +145,7 @@ local function main()
else else
local sounder_start = util.time_ms() local sounder_start = util.time_ms()
log_boot("annunciator alarm speaker connected") log_boot("annunciator alarm speaker connected")
sounder.init(speaker, config.SOUNDER_VOLUME) sounder.init(speaker, config.SpeakerVolume)
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms") log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
log_sys("annunciator alarm configured") log_sys("annunciator alarm configured")
iocontrol.fp_has_speaker(true) iocontrol.fp_has_speaker(true)
@@ -143,8 +156,8 @@ local function main()
---------------------------------------- ----------------------------------------
-- message authentication init -- message authentication init
if type(config.AUTH_KEY) == "string" then if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
local init_time = network.init_mac(config.AUTH_KEY) local init_time = network.init_mac(config.AuthKey)
log_crypto("HMAC init took " .. init_time .. "ms") log_crypto("HMAC init took " .. init_time .. "ms")
end end
@@ -161,14 +174,13 @@ local function main()
end end
-- create connection watchdog -- create connection watchdog
local conn_watchdog = util.new_watchdog(config.SV_TIMEOUT) local conn_watchdog = util.new_watchdog(config.SVR_Timeout)
conn_watchdog.cancel() conn_watchdog.cancel()
log.debug("startup> conn watchdog created") log.debug("startup> conn watchdog created")
-- create network interface then setup comms -- create network interface then setup comms
local nic = network.nic(modem) local nic = network.nic(modem)
local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, config.NUM_UNITS, config.CRD_CHANNEL, local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, conn_watchdog)
config.SVR_CHANNEL, config.PKT_CHANNEL, config.TRUSTED_RANGE, conn_watchdog)
log.debug("startup> comms init") log.debug("startup> comms init")
log_comms("comms initialized") log_comms("comms initialized")
@@ -214,7 +226,7 @@ local function main()
local link_failed = false local link_failed = false
local ui_ok = true local ui_ok = true
local date_format = util.trinary(config.TIME_24_HOUR, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y") local date_format = util.trinary(config.Time24Hour, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y")
-- start clock -- start clock
loop_clock.start() loop_clock.start()
@@ -291,6 +303,11 @@ local function main()
iocontrol.fp_has_speaker(true) iocontrol.fp_has_speaker(true)
end end
end end
elseif event == "monitor_resize" then
local is_used, is_ok = renderer.handle_resize(param1)
if is_used then
log_sys(util.c("configured monitor ", param1, " resized, ", util.trinary(is_ok, "display still fits", "display no longer fits")))
end
elseif event == "timer" then elseif event == "timer" then
if loop_clock.is_clock(param1) then if loop_clock.is_clock(param1) then
-- main loop tick -- main loop tick

View File

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

View File

@@ -1,5 +1,7 @@
local types = require("scada-common.types") local types = require("scada-common.types")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
local core = require("graphics.core") local core = require("graphics.core")
@@ -23,15 +25,17 @@ local lu_col = style.lu_colors
---@param y integer top left y ---@param y integer top left y
---@param ps psil ps interface ---@param ps psil ps interface
local function new_view(root, x, y, ps) local function new_view(root, x, y, ps)
local db = iocontrol.get_db()
local reactor = Rectangle{parent=root,border=border(1, colors.gray, true),width=30,height=7,x=x,y=y} local reactor = Rectangle{parent=root,border=border(1, colors.gray, true),width=30,height=7,x=x,y=y}
local status = StateIndicator{parent=reactor,x=6,y=1,states=style.reactor.states,value=1,min_width=16} local status = StateIndicator{parent=reactor,x=6,y=1,states=style.reactor.states,value=1,min_width=16}
local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit="K",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg} local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg_bg}
local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg} local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg}
local heating_r = DataIndicator{parent=reactor,x=2,y=5,lu_colors=lu_col,label="Heating:",unit="mB/t",format="%12.0f",value=0,commas=true,width=26,fg_bg=text_fg_bg} local heating_r = DataIndicator{parent=reactor,x=2,y=5,lu_colors=lu_col,label="Heating:",unit="mB/t",format="%12.0f",value=0,commas=true,width=26,fg_bg=text_fg_bg}
status.register(ps, "computed_status", status.update) status.register(ps, "computed_status", status.update)
core_temp.register(ps, "temp", core_temp.update) core_temp.register(ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end)
burn_r.register(ps, "act_burn_rate", burn_r.update) burn_r.register(ps, "act_burn_rate", burn_r.update)
heating_r.register(ps, "heating_rate", heating_r.update) heating_r.register(ps, "heating_rate", heating_r.update)

View File

@@ -3,6 +3,7 @@
-- --
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
@@ -51,8 +52,9 @@ local period = core.flasher.PERIOD
---@param parent graphics_element parent ---@param parent graphics_element parent
---@param id integer ---@param id integer
local function init(parent, id) local function init(parent, id)
local unit = iocontrol.get_db().units[id] ---@type ioctl_unit local db = iocontrol.get_db()
local f_ps = iocontrol.get_db().facility.ps local unit = db.units[id] ---@type ioctl_unit
local f_ps = db.facility.ps
local main = Div{parent=parent,x=1,y=1} local main = Div{parent=parent,x=1,y=1}
@@ -114,8 +116,9 @@ 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",height=1,width=9,fg_bg=style.label}
local core_temp = DataIndicator{parent=main,x=32,label="",format="%11.2f",value=0,unit="K",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg} local fmt = util.trinary(string.len(db.temp_label) == 2, "%10.2f", "%11.2f")
core_temp.register(u_ps, "temp", core_temp.update) 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=bw_fg_bg}
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",height=1,width=9,fg_bg=style.label}
local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg} local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
@@ -257,7 +260,7 @@ local function init(parent, id)
if unit.num_boilers > 0 then if unit.num_boilers > 0 then
TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red} local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
b1_wll.register(b_ps[1], "WasterLevelLow", b1_wll.update) b1_wll.register(b_ps[1], "WaterLevelLow", b1_wll.update)
TextBox{parent=rcs_tags,text="B1",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel} local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
@@ -273,7 +276,7 @@ local function init(parent, id)
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red} local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
b2_wll.register(b_ps[2], "WasterLevelLow", b2_wll.update) b2_wll.register(b_ps[2], "WaterLevelLow", b2_wll.update)
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel} local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}

View File

@@ -181,10 +181,10 @@ local function make(parent, x, y, wide, unit)
local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg} local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg}
local pu_rate = DataIndicator{parent=waste,x=_wide(82,70),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg} local pu_rate = DataIndicator{parent=waste,x=_wide(82,70),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
local po_rate = DataIndicator{parent=waste,x=_wide(52,45),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg} local po_rate = DataIndicator{parent=waste,x=_wide(52,45),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg}
local popl_rate = DataIndicator{parent=waste,x=_wide(82,70),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg} local popl_rate = DataIndicator{parent=waste,x=_wide(82,70),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg}
local poam_rate = DataIndicator{parent=waste,x=_wide(82,70),y=10,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg} local poam_rate = DataIndicator{parent=waste,x=_wide(82,70),y=10,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg}
local spent_rate = DataIndicator{parent=waste,x=_wide(117,99),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg} local spent_rate = DataIndicator{parent=waste,x=_wide(117,98),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%8.3f",value=0,width=13,fg_bg=bw_fg_bg}
waste_rate.register(unit.unit_ps, "act_burn_rate", waste_rate.update) waste_rate.register(unit.unit_ps, "act_burn_rate", waste_rate.update)
pu_rate.register(unit.unit_ps, "pu_rate", pu_rate.update) pu_rate.register(unit.unit_ps, "pu_rate", pu_rate.update)
@@ -214,7 +214,7 @@ local function make(parent, x, y, wide, unit)
sna_act.register(unit.unit_ps, "po_rate", function (r) sna_act.update(r > 0) end) sna_act.register(unit.unit_ps, "po_rate", function (r) sna_act.update(r > 0) end)
sna_cnt.register(unit.unit_ps, "sna_count", sna_cnt.update) sna_cnt.register(unit.unit_ps, "sna_count", sna_cnt.update)
sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.update) sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.update)
sna_max.register(unit.unit_ps, "sna_prod_rate", sna_max.update) sna_max.register(unit.unit_ps, "sna_max_rate", sna_max.update)
sna_in.register(unit.unit_ps, "sna_in", sna_in.update) sna_in.register(unit.unit_ps, "sna_in", sna_in.update)
return root return root

View File

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

View File

@@ -348,7 +348,7 @@ local function init(main)
status.register(facility.sps_ps_tbl[1], "computed_status", status.update) status.register(facility.sps_ps_tbl[1], "computed_status", status.update)
TextBox{parent=sps_box,x=2,y=3,text="Input Rate",height=1,width=10,fg_bg=style.label} TextBox{parent=sps_box,x=2,y=3,text="Input Rate",height=1,width=10,fg_bg=style.label}
local sps_in = DataIndicator{parent=sps_box,x=2,label="",format="%15.3f",value=0,unit="mB/t",lu_colors=lu_col,width=20,fg_bg=bw_fg_bg} 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=bw_fg_bg}
sps_in.register(facility.ps, "po_am_rate", sps_in.update) sps_in.register(facility.ps, "po_am_rate", sps_in.update)
@@ -370,8 +370,8 @@ local function init(main)
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,height=1,fg_bg=wh_gray}
local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=bw_fg_bg} local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=bw_fg_bg}
local pu = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17} local pu = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17}
local po = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Po",unit="mB/t",format="%9.3f",value=0,width=17} local po = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Po",unit="mB/t",format="%9.2f",value=0,width=17}
local popl = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="PoPl",unit="mB/t",format="%7.3f",value=0,width=17} local popl = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="PoPl",unit="mB/t",format="%7.2f",value=0,width=17}
pu.register(facility.ps, "pu_rate", pu.update) pu.register(facility.ps, "pu_rate", pu.update)
po.register(facility.ps, "po_rate", po.update) po.register(facility.ps, "po_rate", po.update)

View File

@@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
local core = {} local core = {}
core.version = "2.0.0" core.version = "2.1.1"
core.flasher = flasher core.flasher = flasher
core.events = events core.events = events
@@ -115,7 +115,7 @@ end
-- extract the custom element assert message, dropping the path to the element file -- extract the custom element assert message, dropping the path to the element file
function core.extract_assert_msg(msg) function core.extract_assert_msg(msg)
return string.sub(msg, (string.find(msg, "@") + 1) or 1) return string.sub(msg, (string.find(msg, "@") or 0) + 1)
end end
-- Interactive Field Manager -- Interactive Field Manager
@@ -173,7 +173,7 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
if e.enabled then if e.enabled then
e.w_set_bkg(fg_bg.bkg) e.w_set_bkg(fg_bg.bkg)
e.w_set_fgd(fg_bg.fgd) e.w_set_fgd(fg_bg.fgd)
else elseif dis_fg_bg ~= nil then
e.w_set_bkg(dis_fg_bg.bkg) e.w_set_bkg(dis_fg_bg.bkg)
e.w_set_fgd(dis_fg_bg.fgd) e.w_set_fgd(dis_fg_bg.fgd)
end end

View File

@@ -91,6 +91,8 @@ function element.new(args, child_offset_x, child_offset_y)
p_window = nil, ---@type table p_window = nil, ---@type table
position = events.new_coord_2d(1, 1), position = events.new_coord_2d(1, 1),
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
offset_x = 0,
offset_y = 0,
next_y = 1, -- next child y coordinate next_y = 1, -- next child y coordinate
next_id = 0, -- next child ID next_id = 0, -- next child ID
subscriptions = {}, subscriptions = {},
@@ -105,6 +107,7 @@ function element.new(args, child_offset_x, child_offset_y)
value = nil, ---@type any value = nil, ---@type any
window = nil, ---@type table window = nil, ---@type table
content_window = nil, ---@type table|nil content_window = nil, ---@type table|nil
mouse_window_shift = { x = 0, y = 0 },
fg_bg = core.cpair(colors.white, colors.black), fg_bg = core.cpair(colors.white, colors.black),
frame = core.gframe(1, 1, 1, 1), frame = core.gframe(1, 1, 1, 1),
children = {}, children = {},
@@ -193,6 +196,10 @@ function element.new(args, child_offset_x, child_offset_y)
---@param offset_y integer y offset for mouse events ---@param offset_y integer y offset for mouse events
---@param next_y integer next line if no y was provided ---@param next_y integer next line if no y was provided
function protected.prepare_template(offset_x, offset_y, next_y) function protected.prepare_template(offset_x, offset_y, next_y)
-- record offsets in case there is a reposition
self.offset_x = offset_x
self.offset_y = offset_y
-- get frame coordinates/size -- get frame coordinates/size
if args.gframe ~= nil then if args.gframe ~= nil then
protected.frame.x = args.gframe.x protected.frame.x = args.gframe.x
@@ -217,10 +224,10 @@ function element.new(args, child_offset_x, child_offset_y)
end end
-- check frame -- check frame
element.assert(f.x >= 1, "frame x not >= 1", 2) element.assert(f.x >= 1, "frame x not >= 1", 3)
element.assert(f.y >= 1, "frame y not >= 1", 2) element.assert(f.y >= 1, "frame y not >= 1", 3)
element.assert(f.w >= 1, "frame width not >= 1", 2) element.assert(f.w >= 1, "frame width not >= 1", 3)
element.assert(f.h >= 1, "frame height not >= 1", 2) element.assert(f.h >= 1, "frame height not >= 1", 3)
-- create window -- create window
protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, args.hidden ~= true) protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, args.hidden ~= true)
@@ -344,6 +351,10 @@ function element.new(args, child_offset_x, child_offset_y)
-- handle this element having been unfocused -- handle this element having been unfocused
function protected.on_unfocused() end function protected.on_unfocused() end
-- handle this element having had a child focused
---@param child graphics_element
function protected.on_child_focused(child) end
-- handle this element having been shown -- handle this element having been shown
function protected.on_shown() end function protected.on_shown() end
@@ -520,6 +531,13 @@ function element.new(args, child_offset_x, child_offset_y)
else args.parent.__focus_child(child) end else args.parent.__focus_child(child) end
end end
-- a child was focused, used to make sure it is actually visible to the user in the content frame
---@param child graphics_element
function public.__child_focused(child)
protected.on_child_focused(child)
if not self.is_root then args.parent.__child_focused(public) end
end
-- get a child element -- get a child element
---@nodiscard ---@nodiscard
---@param id element_id ---@param id element_id
@@ -652,6 +670,7 @@ function element.new(args, child_offset_x, child_offset_y)
if args.can_focus and protected.enabled and not self.focused then if args.can_focus and protected.enabled and not self.focused then
self.focused = true self.focused = true
protected.on_focused() protected.on_focused()
if not self.is_root then args.parent.__child_focused(public) end
end end
end end
@@ -666,7 +685,7 @@ function element.new(args, child_offset_x, child_offset_y)
-- unfocus this element and all its children -- unfocus this element and all its children
function public.unfocus_all() function public.unfocus_all()
public.unfocus() public.unfocus()
for _, child in pairs(protected.children) do child.get().unfocus() end for _, child in pairs(protected.children) do child.get().unfocus_all() end
end end
-- custom recolor command, varies by element if implemented -- custom recolor command, varies by element if implemented
@@ -681,7 +700,22 @@ function element.new(args, child_offset_x, child_offset_y)
-- offsets relative to parent frame are where (1, 1) would be on top of the parent's top left corner -- offsets relative to parent frame are where (1, 1) would be on top of the parent's top left corner
---@param x integer x position relative to parent frame ---@param x integer x position relative to parent frame
---@param y integer y position relative to parent frame ---@param y integer y position relative to parent frame
function public.reposition(x, y) protected.window.reposition(x, y) end function public.reposition(x, y)
protected.window.reposition(x, y)
-- record position
self.position.x, self.position.y = protected.window.getPosition()
-- shift per parent child offset
self.position.x = self.position.x + self.offset_x
self.position.y = self.position.y + self.offset_y
-- calculate mouse event bounds
self.bounds.x1 = self.position.x
self.bounds.x2 = self.position.x + protected.frame.w - 1
self.bounds.y1 = self.position.y
self.bounds.y2 = self.position.y + protected.frame.h - 1
end
-- FUNCTION CALLBACKS -- -- FUNCTION CALLBACKS --
@@ -704,10 +738,11 @@ function element.new(args, child_offset_x, child_offset_y)
end end
local event_T = events.mouse_transposed(event, self.position.x, self.position.y) local event_T = events.mouse_transposed(event, self.position.x, self.position.y)
-- handle the mouse event then pass to children
protected.handle_mouse(event_T) protected.handle_mouse(event_T)
for _, child in pairs(protected.children) do child.get().handle_mouse(event_T) end
-- shift child event if the content window has moved then pass to children
local c_event_T = events.mouse_transposed(event_T, protected.mouse_window_shift.x + 1, protected.mouse_window_shift.y + 1)
for _, child in pairs(protected.children) do child.get().handle_mouse(c_event_T) end
elseif event.type == events.MOUSE_CLICK.DOWN or event.type == events.MOUSE_CLICK.TAP then elseif event.type == events.MOUSE_CLICK.DOWN or event.type == events.MOUSE_CLICK.TAP then
-- clicked out, unfocus this element and children -- clicked out, unfocus this element and children
public.unfocus_all() public.unfocus_all()
@@ -808,9 +843,12 @@ function element.new(args, child_offset_x, child_offset_y)
-- re-draw this element and all its children -- re-draw this element and all its children
function public.redraw() function public.redraw()
local bg, fg = protected.window.getBackgroundColor(), protected.window.getTextColor()
protected.window.setBackgroundColor(protected.fg_bg.bkg) protected.window.setBackgroundColor(protected.fg_bg.bkg)
protected.window.setTextColor(protected.fg_bg.fgd) protected.window.setTextColor(protected.fg_bg.fgd)
protected.window.clear() protected.window.clear()
protected.window.setBackgroundColor(bg)
protected.window.setTextColor(fg)
protected.redraw() protected.redraw()
for _, child in pairs(protected.children) do child.get().redraw() end for _, child in pairs(protected.children) do child.get().redraw() end
end end

View File

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

View File

@@ -5,6 +5,8 @@ local tcd = require("scada-common.tcd")
local core = require("graphics.core") local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
local ALIGN = core.ALIGN
local MOUSE_CLICK = core.events.MOUSE_CLICK local MOUSE_CLICK = core.events.MOUSE_CLICK
local KEY_CLICK = core.events.KEY_CLICK local KEY_CLICK = core.events.KEY_CLICK
@@ -12,6 +14,7 @@ local KEY_CLICK = core.events.KEY_CLICK
---@field text string button text ---@field text string button text
---@field callback function function to call on touch ---@field callback function function to call on touch
---@field min_width? integer text length if omitted ---@field min_width? integer text length if omitted
---@field alignment? ALIGN text align if min width > length
---@field active_fg_bg? cpair foreground/background colors when pressed ---@field active_fg_bg? cpair foreground/background colors when pressed
---@field dis_fg_bg? cpair foreground/background colors when disabled ---@field dis_fg_bg? cpair foreground/background colors when disabled
---@field parent graphics_element ---@field parent graphics_element
@@ -31,6 +34,7 @@ local function push_button(args)
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0") element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
local text_width = string.len(args.text) local text_width = string.len(args.text)
local alignment = args.alignment or ALIGN.CENTER
-- set automatic settings -- set automatic settings
args.can_focus = true args.can_focus = true
@@ -41,9 +45,15 @@ local function push_button(args)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1 local h_pad = 1
local v_pad = math.floor(e.frame.h / 2) + 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
end
-- draw the button -- draw the button
function e.redraw() function e.redraw()
e.window.clear() e.window.clear()

View File

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

View File

@@ -45,7 +45,7 @@ local function text_field(args)
if core.events.was_clicked(event.type) then if core.events.was_clicked(event.type) then
e.take_focus() e.take_focus()
if event.type == MOUSE_CLICK.UP then if event.type == MOUSE_CLICK.UP and e.in_frame_bounds(event.current.x, event.current.y) then
ifield.move_cursor(event.current.x) ifield.move_cursor(event.current.x)
end end
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then

View File

@@ -5,6 +5,7 @@ local tcd = require("scada-common.tcd")
local core = require("graphics.core") local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
local KEY_CLICK = core.events.KEY_CLICK
local MOUSE_CLICK = core.events.MOUSE_CLICK local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class listbox_args ---@class listbox_args
@@ -33,6 +34,8 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@param args listbox_args ---@param args listbox_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function listbox(args) local function listbox(args)
args.can_focus = true
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
@@ -128,7 +131,7 @@ local function listbox(args)
end end
e.w_set_cur(e.frame.w, i) e.w_set_cur(e.frame.w, i)
e.w_write(" ") if e.is_focused() then e.w_write("\x7f") else e.w_write(" ") end
end end
e.w_set_bkg(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
@@ -158,6 +161,9 @@ local function listbox(args)
scroll_frame.reposition(1, 1 + scroll_offset) scroll_frame.reposition(1, 1 + scroll_offset)
scroll_frame.setVisible(true) scroll_frame.setVisible(true)
-- shift mouse events
e.mouse_window_shift.y = scroll_offset
draw_bar() draw_bar()
end end
@@ -219,6 +225,32 @@ local function listbox(args)
end end
end end
-- handle focus
e.on_focused = draw_bar
e.on_unfocused = draw_bar
-- handle a child in the list being focused, make sure it is visible
function e.on_child_focused(child)
for i = 1, #list do
local item = list[i] ---@type listbox_item
if item.e == child then
if (item.y + scroll_offset) <= 0 then
scroll_offset = 1 - item.y
update_positions()
draw_bar()
elseif (item.y + scroll_offset) == 1 then
-- do nothing, it's right at the top (if the bottom doesn't fit we can't easily fix that)
elseif ((item.h + item.y - 1) + scroll_offset) > e.frame.h then
scroll_offset = 1 - ((item.h + item.y) - e.frame.h)
update_positions()
draw_bar()
end
return
end
end
end
-- handle mouse interaction -- handle mouse interaction
---@param event mouse_interaction mouse event ---@param event mouse_interaction mouse event
function e.handle_mouse(event) function e.handle_mouse(event)
@@ -226,23 +258,27 @@ local function listbox(args)
if event.type == MOUSE_CLICK.TAP then if event.type == MOUSE_CLICK.TAP then
if event.current.x == e.frame.w then if event.current.x == e.frame.w then
if event.current.y == 1 or event.current.y < bar_bounds[1] then if event.current.y == 1 or event.current.y < bar_bounds[1] then
draw_arrows(1)
scroll_up() scroll_up()
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end if event.current.y == 1 then
draw_arrows(1)
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
end
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
draw_arrows(-1)
scroll_down() scroll_down()
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end if event.current.y == e.frame.h then
draw_arrows(-1)
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
end
end end
end end
elseif event.type == MOUSE_CLICK.DOWN then elseif event.type == MOUSE_CLICK.DOWN then
if event.current.x == e.frame.w then if event.current.x == e.frame.w then
if event.current.y == 1 or event.current.y < bar_bounds[1] then if event.current.y == 1 or event.current.y < bar_bounds[1] then
draw_arrows(1)
scroll_up() scroll_up()
if event.current.y == 1 then draw_arrows(1) end
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
draw_arrows(-1)
scroll_down() scroll_down()
if event.current.y == e.frame.h then draw_arrows(-1) end
else else
-- clicked on bar -- clicked on bar
holding_bar = true holding_bar = true
@@ -274,6 +310,24 @@ local function listbox(args)
end end
end end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
if event.key == keys.up then
scroll_up()
elseif event.key == keys.down then
scroll_down()
elseif event.key == keys.home then
scroll_offset = 0
update_positions()
elseif event.key == keys["end"] then
scroll_offset = max_down_scroll
update_positions()
end
end
end
-- element redraw -- element redraw
function e.redraw() function e.redraw()
draw_arrows(0) draw_arrows(0)

View File

@@ -60,7 +60,7 @@ def make_manifest(size):
}, },
"files" : { "files" : {
# common files # common files
"system" : [ "initenv.lua", "startup.lua", "configure.lua" ], "system" : [ "initenv.lua", "startup.lua", "configure.lua", "LICENSE" ],
"common" : list_files("./scada-common"), "common" : list_files("./scada-common"),
"graphics" : list_files("./graphics"), "graphics" : list_files("./graphics"),
"lockbox" : list_files("./lockbox"), "lockbox" : list_files("./lockbox"),

View File

@@ -2,9 +2,7 @@ return {
-- initialize booted environment -- initialize booted environment
init_env = function () init_env = function ()
local _require, _env = require("cc.require"), setmetatable({}, { __index = _ENV }) local _require, _env = require("cc.require"), setmetatable({}, { __index = _ENV })
-- overwrite require/package globals
require, package = _require.make(_env, "/") require, package = _require.make(_env, "/")
-- reset terminal
term.clear(); term.setCursorPos(1, 1) term.clear(); term.setCursorPos(1, 1)
end end
} }

View File

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

578
pocket/configure.lua Normal file
View File

@@ -0,0 +1,578 @@
--
-- Configuration GUI
--
local log = require("scada-common.log")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core")
local DisplayBox = require("graphics.elements.displaybox")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local CheckBox = require("graphics.elements.controls.checkbox")
local PushButton = require("graphics.elements.controls.push_button")
local RadioButton = require("graphics.elements.controls.radio_button")
local NumberField = require("graphics.elements.form.number_field")
local TextField = require("graphics.elements.form.text_field")
local println = util.println
local tri = util.trinary
local cpair = core.cpair
local 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 = {}
---@class pkt_configurator
local configurator = {}
local style = {}
style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray)
style.colors = {
{ c = colors.red, hex = 0xdf4949 },
{ c = colors.orange, hex = 0xffb659 },
{ c = colors.yellow, hex = 0xfffc79 },
{ c = colors.lime, hex = 0x80ff80 },
{ c = colors.green, hex = 0x4aee8a },
{ c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff },
{ c = colors.purple, hex = 0xb156ee },
{ c = colors.pink, hex = 0xf26ba2 },
{ c = colors.magenta, hex = 0xf9488a },
{ c = colors.lightGray, hex = 0xcacaca },
{ c = colors.gray, hex = 0x575757 }
}
local bw_fg_bg = cpair(colors.black, colors.white)
local g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
local nav_fg_bg = bw_fg_bg
local btn_act_fg_bg = cpair(colors.white, colors.gray)
local dis_fg_bg = cpair(colors.lightGray,colors.white)
local tool_ctl = {
ask_config = false,
has_config = false,
viewing_config = false,
importing_legacy = false,
view_cfg = nil, ---@type graphics_element
settings_apply = nil, ---@type graphics_element
set_networked = nil, ---@type function
bundled_emcool = nil, ---@type function
gen_summary = nil, ---@type function
show_current_cfg = nil, ---@type function
load_legacy = nil, ---@type function
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type graphics_element
auth_key_textbox = nil, ---@type graphics_element
auth_key_value = ""
}
---@class pkt_config
local tmp_cfg = {
SVR_Channel = nil, ---@type integer
CRD_Channel = nil, ---@type integer
PKT_Channel = nil, ---@type integer
ConnTimeout = nil, ---@type number
TrustedRange = nil, ---@type number
AuthKey = nil, ---@type string|nil
LogMode = 0,
LogPath = "",
LogDebug = false,
}
---@class pkt_config
local ini_cfg = {}
---@class pkt_config
local settings_cfg = {}
-- all settings fields, their nice names, and their default values
local fields = {
{ "SVR_Channel", "SVR Channel", 16240 },
{ "CRD_Channel", "CRD Channel", 16243 },
{ "PKT_Channel", "PKT Channel", 16244 },
{ "ConnTimeout", "Connection Timeout", 5 },
{ "TrustedRange", "Trusted Range", 0 },
{ "AuthKey", "Facility Auth Key" , ""},
{ "LogMode", "Log Mode", log.MODE.APPEND },
{ "LogPath", "Log Path", "/log.txt" },
{ "LogDebug","Log Debug Messages", false }
}
-- load data from the settings file
---@param target pkt_config
---@param raw boolean? true to not use default values
local function load_settings(target, raw)
for _, v in pairs(fields) do settings.unset(v[1]) end
local loaded = settings.load("/pocket.settings")
for _, v in pairs(fields) do target[v[1]] = settings.get(v[1], tri(raw, nil, v[3])) end
return loaded
end
-- create the config view
---@param display graphics_element
local function config_view(display)
---@diagnostic disable-next-line: undefined-field
local function exit() os.queueEvent("terminate") end
TextBox{parent=display,y=1,text="Pocket Configurator",alignment=CENTER,height=1,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 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}}
-- Main Page
local y_start = 7
TextBox{parent=main_page,x=2,y=2,height=4,text="Welcome to the Pocket configurator! Please select one of the following options."}
if tool_ctl.ask_config then
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Please configure before starting up.",fg_bg=cpair(colors.red,colors.lightGray)}
y_start = y_start + 3
end
local function view_config()
tool_ctl.viewing_config = true
tool_ctl.gen_summary(settings_cfg)
tool_ctl.settings_apply.hide(true)
main_pane.set_value(4)
end
if fs.exists("/pocket/config.lua") then
PushButton{parent=main_page,x=2,y=y_start,min_width=22,text="Import Legacy Config",callback=function()tool_ctl.load_legacy()end,fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=btn_act_fg_bg}
y_start = y_start + 2
end
PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure Device",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg}
if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end
PushButton{parent=main_page,x=2,y=18,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
PushButton{parent=main_page,x=14,y=18,min_width=12,text="Change Log",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#region Network
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=24}
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=24}
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=24}
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=24}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
TextBox{parent=net_cfg,x=1,y=2,height=1,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=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"}
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"}
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"}
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 function submit_channels()
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then
tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c
net_pane.set_value(2)
chan_err.hide(true)
else chan_err.show() end
end
PushButton{parent=net_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)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=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"}
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 function submit_timeouts()
local timeout_val = tonumber(timeout.get_value())
if timeout_val ~= nil then
tmp_cfg.ConnTimeout = timeout_val
net_pane.set_value(3)
ct_err.hide(true)
else ct_err.show() end
end
PushButton{parent=net_c_2,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=19,y=15,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,height=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 function submit_tr()
local range_val = tonumber(range.get_value())
if range_val ~= nil then
tmp_cfg.TrustedRange = range_val
net_pane.set_value(4)
tr_err.hide(true)
else tr_err.show() end
end
PushButton{parent=net_c_3,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=19,y=15,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_4,x=1,y=1,height=4,text="Optionally, set the facility authentication key. Do NOT use one of your passwords."}
TextBox{parent=net_c_4,x=1,y=6,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=1,y=12,height=1,text="Facility Auth Key"}
local key, _, censor = TextField{parent=net_c_4,x=1,y=13,max_len=64,value=ini_cfg.AuthKey,width=24,height=1,fg_bg=bw_fg_bg}
local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
-- declare back first so tabbing makes sense visually
PushButton{parent=net_c_4,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local hide_key = CheckBox{parent=net_c_4,x=8,y=15,label="Hide Key",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
hide_key.set_value(true)
censor_key(true)
local key_err = TextBox{parent=net_c_4,x=1,y=14,height=1,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)
key_err.hide(true)
else key_err.show() end
end
PushButton{parent=net_c_4,x=19,y=15,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Logging
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=24}
TextBox{parent=log_cfg,x=1,y=2,height=1,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=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=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 function submit_log()
if path.get_value() ~= "" then
path_err.hide(true)
tmp_cfg.LogMode = mode.get_value() - 1
tmp_cfg.LogPath = path.get_value()
tmp_cfg.LogDebug = en_dbg.get_value()
tool_ctl.gen_summary(tmp_cfg)
tool_ctl.viewing_config = false
tool_ctl.importing_legacy = false
tool_ctl.settings_apply.show()
main_pane.set_value(4)
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=19,y=15,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Summary and Saving
local sum_c_1 = Div{parent=summary,x=2,y=4,width=24}
local sum_c_2 = Div{parent=summary,x=2,y=4,width=24}
local sum_c_3 = Div{parent=summary,x=2,y=4,width=24}
local sum_c_4 = Div{parent=summary,x=2,y=4,width=24}
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
TextBox{parent=summary,x=1,y=2,height=1,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=11,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function back_from_summary()
if tool_ctl.viewing_config or tool_ctl.importing_legacy then
main_pane.set_value(1)
tool_ctl.viewing_config = false
tool_ctl.importing_legacy = false
tool_ctl.settings_apply.show()
else
main_pane.set_value(3)
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("/pocket.settings") then
load_settings(settings_cfg, true)
load_settings(ini_cfg)
try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(crd_chan, ini_cfg.CRD_Channel)
try_set(pkt_chan, ini_cfg.PKT_Channel)
try_set(timeout, ini_cfg.ConnTimeout)
try_set(range, ini_cfg.TrustedRange)
try_set(key, ini_cfg.AuthKey)
try_set(mode, ini_cfg.LogMode)
try_set(path, ini_cfg.LogPath)
try_set(en_dbg, ini_cfg.LogDebug)
tool_ctl.view_cfg.enable()
if tool_ctl.importing_legacy then
tool_ctl.importing_legacy = false
sum_pane.set_value(3)
else
sum_pane.set_value(2)
end
else
sum_pane.set_value(4)
end
end
PushButton{parent=sum_c_1,x=1,y=15,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=1,y=13,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg}
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=18,y=15,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
TextBox{parent=sum_c_2,x=1,y=1,height=1,text="Settings saved!"}
local function go_home()
main_pane.set_value(1)
net_pane.set_value(1)
sum_pane.set_value(1)
end
PushButton{parent=sum_c_2,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_2,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=sum_c_3,x=1,y=1,height=4,text="The old config.lua file will now be deleted, then the configurator will exit."}
local function delete_legacy()
fs.delete("/pocket/config.lua")
exit()
end
PushButton{parent=sum_c_3,x=1,y=15,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_3,x=19,y=15,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=sum_c_4,x=1,y=1,height=8,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
PushButton{parent=sum_c_4,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_4,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
--#endregion
-- Config Change Log
local cl = Div{parent=changelog,x=2,y=4,width=24}
TextBox{parent=changelog,x=1,y=2,height=1,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}
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=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
end
end
PushButton{parent=cl,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
-- set tool functions now that we have the elements
-- load a legacy config file
function tool_ctl.load_legacy()
local config = require("pocket.config")
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
tmp_cfg.CRD_Channel = config.CRD_CHANNEL
tmp_cfg.PKT_Channel = config.PKT_CHANNEL
tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT
tmp_cfg.TrustedRange = config.TRUSTED_RANGE
tmp_cfg.AuthKey = config.AUTH_KEY or ""
tmp_cfg.LogMode = config.LOG_MODE
tmp_cfg.LogPath = config.LOG_PATH
tmp_cfg.LogDebug = config.LOG_DEBUG or false
tool_ctl.gen_summary(tmp_cfg)
sum_pane.set_value(1)
main_pane.set_value(4)
tool_ctl.importing_legacy = true
end
-- expose the auth key on the summary page
function tool_ctl.show_auth_key()
tool_ctl.show_key_btn.disable()
tool_ctl.auth_key_textbox.set_value(tool_ctl.auth_key_value)
end
-- generate the summary list
---@param cfg pkt_config
function tool_ctl.gen_summary(cfg)
setting_list.remove_all()
local alternate = false
local inner_width = setting_list.get_width() - 1
tool_ctl.show_key_btn.enable()
tool_ctl.auth_key_value = cfg.AuthKey or "" -- to show auth key
for i = 1, #fields do
local f = fields[i]
local height = 1
local label_w = string.len(f[2])
local val_max_w = (inner_width - label_w) - 1
local raw = cfg[f[1]]
local val = util.strval(raw)
if f[1] == "AuthKey" then val = string.rep("*", string.len(val))
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end
if val == "nil" then val = "<not set>" end
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if string.len(val) > val_max_w then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end
local line = Div{parent=setting_list,height=height,fg_bg=c}
TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)}
local textbox
if height > 1 then
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end
if f[1] == "AuthKey" then tool_ctl.auth_key_textbox = textbox end
end
end
end
-- reset terminal screen
local function reset_term()
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
end
-- run the pcoket configurator
---@param ask_config? boolean indicate if this is being called by the startup app due to an invalid configuration
function configurator.configure(ask_config)
tool_ctl.ask_config = ask_config == true
load_settings(settings_cfg, true)
tool_ctl.has_config = load_settings(ini_cfg)
reset_term()
-- set overridden colors
for i = 1, #style.colors do
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
end
local status, error = pcall(function ()
local display = DisplayBox{window=term.current(),fg_bg=style.root}
config_view(display)
while true do
local event, param1, param2, param3 = util.pull_event()
-- handle event
if event == "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
local m_e = core.events.new_mouse_event(event, param1, param2, param3)
if m_e then display.handle_mouse(m_e) end
elseif event == "char" or event == "key" or event == "key_up" then
local k_e = core.events.new_key_event(event, param1, param2)
if k_e then display.handle_key(k_e) end
elseif event == "paste" then
display.handle_paste(param1)
end
if event == "terminate" then return end
end
end)
-- restore colors
for i = 1, #style.colors do
local r, g, b = term.nativePaletteColor(style.colors[i].c)
term.setPaletteColor(style.colors[i].c, r, g, b)
end
reset_term()
if not status then
println("configurator error: " .. error)
end
return status, error
end
return configurator

View File

@@ -13,17 +13,57 @@ local LINK_STATE = iocontrol.LINK_STATE
local pocket = {} local pocket = {}
---@type pkt_config
local config = {}
pocket.config = config
-- load the pocket configuration
function pocket.load_config()
if not settings.load("/pocket.settings") then return false end
config.SVR_Channel = settings.get("SVR_Channel")
config.CRD_Channel = settings.get("CRD_Channel")
config.PKT_Channel = settings.get("PKT_Channel")
config.ConnTimeout = settings.get("ConnTimeout")
config.TrustedRange = settings.get("TrustedRange")
config.AuthKey = settings.get("AuthKey")
config.LogMode = settings.get("LogMode")
config.LogPath = settings.get("LogPath")
config.LogDebug = settings.get("LogDebug")
local cfv = util.new_validator()
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.CRD_Channel)
cfv.assert_channel(config.PKT_Channel)
cfv.assert_type_num(config.ConnTimeout)
cfv.assert_min(config.ConnTimeout, 2)
cfv.assert_type_num(config.TrustedRange)
cfv.assert_min(config.TrustedRange, 0)
cfv.assert_type_str(config.AuthKey)
if type(config.AuthKey) == "string" then
local len = string.len(config.AuthKey)
cfv.assert(len == 0 or len >= 8)
end
cfv.assert_type_int(config.LogMode)
cfv.assert_range(config.LogMode, 0, 1)
cfv.assert_type_str(config.LogPath)
cfv.assert_type_bool(config.LogDebug)
return cfv.valid()
end
-- pocket coordinator + supervisor communications -- pocket coordinator + supervisor communications
---@nodiscard ---@nodiscard
---@param version string pocket version ---@param version string pocket version
---@param nic nic network interface device ---@param nic nic network interface device
---@param pkt_channel integer pocket comms channel
---@param svr_channel integer supervisor access channel
---@param crd_channel integer coordinator access channel
---@param range integer trusted device connection range
---@param sv_watchdog watchdog ---@param sv_watchdog watchdog
---@param api_watchdog watchdog ---@param api_watchdog watchdog
function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range, sv_watchdog, api_watchdog) function pocket.comms(version, nic, sv_watchdog, api_watchdog)
local self = { local self = {
sv = { sv = {
linked = false, linked = false,
@@ -42,13 +82,13 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
establish_delay_counter = 0 establish_delay_counter = 0
} }
comms.set_trusted_range(range) comms.set_trusted_range(config.TrustedRange)
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure network channels -- configure network channels
nic.closeAll() nic.closeAll()
nic.open(pkt_channel) nic.open(config.PKT_Channel)
-- send a management packet to the supervisor -- send a management packet to the supervisor
---@param msg_type MGMT_TYPE ---@param msg_type MGMT_TYPE
@@ -60,7 +100,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
pkt.make(msg_type, msg) pkt.make(msg_type, msg)
s_pkt.make(self.sv.addr, self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) s_pkt.make(self.sv.addr, self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
nic.transmit(svr_channel, pkt_channel, s_pkt) nic.transmit(config.SVR_Channel, config.PKT_Channel, s_pkt)
self.sv.seq_num = self.sv.seq_num + 1 self.sv.seq_num = self.sv.seq_num + 1
end end
@@ -74,7 +114,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
pkt.make(msg_type, msg) pkt.make(msg_type, msg)
s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
nic.transmit(crd_channel, pkt_channel, s_pkt) nic.transmit(config.CRD_Channel, config.PKT_Channel, s_pkt)
self.api.seq_num = self.api.seq_num + 1 self.api.seq_num = self.api.seq_num + 1
end end
@@ -217,9 +257,9 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
local protocol = packet.scada_frame.protocol() local protocol = packet.scada_frame.protocol()
local src_addr = packet.scada_frame.src_addr() local src_addr = packet.scada_frame.src_addr()
if l_chan ~= pkt_channel then if l_chan ~= config.PKT_Channel then
log.debug("received packet on unconfigured channel " .. l_chan, true) log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == crd_channel then elseif r_chan == config.CRD_Channel then
-- check sequence number -- check sequence number
if self.api.r_seq_num == nil then if self.api.r_seq_num == nil then
self.api.r_seq_num = packet.scada_frame.seq_num() self.api.r_seq_num = packet.scada_frame.seq_num()
@@ -308,7 +348,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
else else
log.debug("illegal packet type " .. protocol .. " from coordinator", true) log.debug("illegal packet type " .. protocol .. " from coordinator", true)
end end
elseif r_chan == svr_channel then elseif r_chan == config.SVR_Channel then
-- check sequence number -- check sequence number
if self.sv.r_seq_num == nil then if self.sv.r_seq_num == nil then
self.sv.r_seq_num = packet.scada_frame.seq_num() self.sv.r_seq_num = packet.scada_frame.seq_num()

View File

@@ -13,38 +13,41 @@ local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local config = require("pocket.config") local configure = require("pocket.configure")
local iocontrol = require("pocket.iocontrol") local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket") local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer") local renderer = require("pocket.renderer")
local POCKET_VERSION = "v0.6.3-alpha" local POCKET_VERSION = "v0.7.1-alpha"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
---------------------------------------- ----------------------------------------
-- config validation -- get configuration
---------------------------------------- ----------------------------------------
local cfv = util.new_validator() if not pocket.load_config() then
-- try to reconfigure (user action)
local success, error = configure.configure(true)
if success then
if not pocket.load_config() then
println("failed to load a valid configuration, please reconfigure")
return
end
else
println("configuration error: " .. error)
return
end
end
cfv.assert_channel(config.SVR_CHANNEL) local config = pocket.config
cfv.assert_channel(config.CRD_CHANNEL)
cfv.assert_channel(config.PKT_CHANNEL)
cfv.assert_type_int(config.TRUSTED_RANGE)
cfv.assert_type_num(config.COMMS_TIMEOUT)
cfv.assert_min(config.COMMS_TIMEOUT, 2)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
assert(cfv.valid(), "bad config file: missing/invalid fields")
---------------------------------------- ----------------------------------------
-- log init -- log init
---------------------------------------- ----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true) log.init(config.LogPath, config.LogMode, config.LogDebug)
log.info("========================================") log.info("========================================")
log.info("BOOTING pocket.startup " .. POCKET_VERSION) log.info("BOOTING pocket.startup " .. POCKET_VERSION)
@@ -69,8 +72,8 @@ local function main()
---------------------------------------- ----------------------------------------
-- message authentication init -- message authentication init
if type(config.AUTH_KEY) == "string" then if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
network.init_mac(config.AUTH_KEY) network.init_mac(config.AuthKey)
end end
iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED) iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED)
@@ -85,8 +88,8 @@ local function main()
-- create connection watchdogs -- create connection watchdogs
local conn_wd = { local conn_wd = {
sv = util.new_watchdog(config.COMMS_TIMEOUT), sv = util.new_watchdog(config.ConnTimeout),
api = util.new_watchdog(config.COMMS_TIMEOUT) api = util.new_watchdog(config.ConnTimeout)
} }
conn_wd.sv.cancel() conn_wd.sv.cancel()
@@ -96,8 +99,7 @@ local function main()
-- create network interface then setup comms -- create network interface then setup comms
local nic = network.nic(modem) local nic = network.nic(modem)
local pocket_comms = pocket.comms(POCKET_VERSION, nic, config.PKT_CHANNEL, config.SVR_CHANNEL, local pocket_comms = pocket.comms(POCKET_VERSION, nic, conn_wd.sv, conn_wd.api)
config.CRD_CHANNEL, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api)
log.debug("startup> comms init") log.debug("startup> comms init")
-- base loop clock (2Hz, 10 ticks) -- base loop clock (2Hz, 10 ticks)

View File

@@ -3,6 +3,7 @@
-- --
local log = require("scada-common.log") local log = require("scada-common.log")
local rsio = require("scada-common.rsio")
local tcd = require("scada-common.tcd") local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
@@ -23,6 +24,7 @@ local NumberField = require("graphics.elements.form.number_field")
local TextField = require("graphics.elements.form.text_field") local TextField = require("graphics.elements.form.text_field")
local println = util.println local println = util.println
local tri = util.trinary
local cpair = core.cpair local cpair = core.cpair
@@ -30,6 +32,12 @@ local LEFT = core.ALIGN.LEFT
local CENTER = core.ALIGN.CENTER local CENTER = core.ALIGN.CENTER
local RIGHT = core.ALIGN.RIGHT local RIGHT = core.ALIGN.RIGHT
-- changes to the config data/format to let the user know
local changes = {
{"v1.6.2", { "AuthKey minimum length is now 8 (if set)" } },
{"v1.6.8", { "ConnTimeout can now have a fractional part" } }
}
---@class plc_configurator ---@class plc_configurator
local configurator = {} local configurator = {}
@@ -37,7 +45,6 @@ local style = {}
style.root = cpair(colors.black, colors.lightGray) style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray) style.header = cpair(colors.white, colors.gray)
style.label = cpair(colors.gray, colors.lightGray)
style.colors = { style.colors = {
{ c = colors.red, hex = 0xdf4949 }, { c = colors.red, hex = 0xdf4949 },
@@ -55,6 +62,11 @@ style.colors = {
{ c = colors.gray, hex = 0x575757 } { c = colors.gray, hex = 0x575757 }
} }
local bw_fg_bg = cpair(colors.black, colors.white)
local g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
local nav_fg_bg = bw_fg_bg
local btn_act_fg_bg = cpair(colors.white, colors.gray)
local tool_ctl = { local tool_ctl = {
ask_config = false, ask_config = false,
has_config = false, has_config = false,
@@ -81,13 +93,13 @@ local tmp_cfg = {
Networked = false, Networked = false,
UnitID = 0, UnitID = 0,
EmerCoolEnable = false, EmerCoolEnable = false,
EmerCoolSide = nil, EmerCoolSide = nil, ---@type string|nil
EmerCoolColor = nil, EmerCoolColor = nil, ---@type color|nil
SVR_Channel = nil, SVR_Channel = nil, ---@type integer
PLC_Channel = nil, PLC_Channel = nil, ---@type integer
ConnTimeout = nil, ConnTimeout = nil, ---@type number
TrustedRange = nil, TrustedRange = nil, ---@type number
AuthKey = nil, AuthKey = nil, ---@type string|nil
LogMode = 0, LogMode = 0,
LogPath = "", LogPath = "",
LogDebug = false, LogDebug = false,
@@ -95,21 +107,24 @@ local tmp_cfg = {
---@class plc_config ---@class plc_config
local ini_cfg = {} local ini_cfg = {}
---@class plc_config
local settings_cfg = {}
-- all settings fields, their nice names, and their default values
local fields = { local fields = {
{ "Networked", "Networked" }, { "Networked", "Networked", false },
{ "UnitID", "Unit ID" }, { "UnitID", "Unit ID", 1 },
{ "EmerCoolEnable", "Emergency Coolant" }, { "EmerCoolEnable", "Emergency Coolant", false },
{ "EmerCoolSide", "Emergency Coolant Side" }, { "EmerCoolSide", "Emergency Coolant Side", nil },
{ "EmerCoolColor", "Emergency Coolant Color" }, { "EmerCoolColor", "Emergency Coolant Color", nil },
{ "SVR_Channel", "SVR Channel" }, { "SVR_Channel", "SVR Channel", 16240 },
{ "PLC_Channel", "PLC Channel" }, { "PLC_Channel", "PLC Channel", 16241 },
{ "ConnTimeout", "Connection Timeout" }, { "ConnTimeout", "Connection Timeout", 5 },
{ "TrustedRange", "Trusted Range" }, { "TrustedRange", "Trusted Range", 0 },
{ "AuthKey", "Facility Auth Key" }, { "AuthKey", "Facility Auth Key" , ""},
{ "LogMode", "Log Mode" }, { "LogMode", "Log Mode", log.MODE.APPEND },
{ "LogPath", "Log Path" }, { "LogPath", "Log Path", "/log.txt" },
{ "LogDebug","Log Debug Messages" } { "LogDebug","Log Debug Messages", false }
} }
local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" } local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" }
@@ -117,25 +132,6 @@ 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 = { "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 } 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 }
local color_name_map = {
[colors.red] = "red",
[colors.orange] = "orange",
[colors.yellow] = "yellow",
[colors.lime] = "lime",
[colors.green] = "green",
[colors.cyan] = "cyan",
[colors.lightBlue] = "lightBlue",
[colors.blue] = "blue",
[colors.purple] = "purple",
[colors.magenta] = "magenta",
[colors.pink] = "pink",
[colors.white] = "white",
[colors.lightGray] = "lightGray",
[colors.gray] = "gray",
[colors.black] = "black",
[colors.brown] = "brown"
}
-- convert text representation to index -- convert text representation to index
---@param side string ---@param side string
local function side_to_idx(side) local function side_to_idx(side)
@@ -154,27 +150,20 @@ end
-- load data from the settings file -- load data from the settings file
---@param target plc_config ---@param target plc_config
local function load_settings(target) ---@param raw boolean? true to not use default values
target.Networked = settings.get("Networked", false) local function load_settings(target, raw)
target.UnitID = settings.get("UnitID", 1) for _, v in pairs(fields) do settings.unset(v[1]) end
target.EmerCoolEnable = settings.get("EmerCoolEnable", false)
target.EmerCoolSide = settings.get("EmerCoolSide", nil) local loaded = settings.load("/reactor-plc.settings")
target.EmerCoolColor = settings.get("EmerCoolColor", nil)
target.SVR_Channel = settings.get("SVR_Channel", 16240) for _, v in pairs(fields) do target[v[1]] = settings.get(v[1], tri(raw, nil, v[3])) end
target.PLC_Channel = settings.get("PLC_Channel", 16241)
target.ConnTimeout = settings.get("ConnTimeout", 5) return loaded
target.TrustedRange = settings.get("TrustedRange", 0)
target.AuthKey = settings.get("AuthKey", "")
target.LogMode = settings.get("LogMode", log.MODE.APPEND)
target.LogPath = settings.get("LogPath", "/log.txt")
target.LogDebug = settings.get("LogDebug", false)
end end
-- create the config view -- create the config view
---@param display graphics_element ---@param display graphics_element
local function config_view(display) local function config_view(display)
local nav_fg_bg = cpair(colors.black,colors.white)
local btn_act_fg_bg = cpair(colors.white,colors.gray)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local function exit() os.queueEvent("terminate") end local function exit() os.queueEvent("terminate") end
@@ -188,23 +177,24 @@ local function config_view(display)
local net_cfg = Div{parent=root_pane_div,x=1,y=1} local net_cfg = Div{parent=root_pane_div,x=1,y=1}
local log_cfg = Div{parent=root_pane_div,x=1,y=1} local log_cfg = Div{parent=root_pane_div,x=1,y=1}
local summary = Div{parent=root_pane_div,x=1,y=1} local summary = Div{parent=root_pane_div,x=1,y=1}
local changelog = Div{parent=root_pane_div,x=1,y=1}
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,plc_cfg,net_cfg,log_cfg,summary}} local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,plc_cfg,net_cfg,log_cfg,summary,changelog}}
-- MAIN PAGE -- Main Page
local y_start = 5 local y_start = 5
TextBox{parent=main_page,x=2,y=2,height=2,text_align=CENTER,text="Welcome to the Reactor PLC configurator! Please select one of the following options."} TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the Reactor PLC configurator! Please select one of the following options."}
if tool_ctl.ask_config then if tool_ctl.ask_config then
TextBox{parent=main_page,x=2,y=y_start,height=2,text_align=CENTER,text="Notice: This device has no valid config. The configurator has been automatically started.",fg_bg=cpair(colors.red,colors.lightGray)} 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)}
y_start = y_start + 3 y_start = y_start + 5
end end
local function view_config() local function view_config()
tool_ctl.viewing_config = true tool_ctl.viewing_config = true
tool_ctl.gen_summary(ini_cfg) tool_ctl.gen_summary(settings_cfg)
tool_ctl.settings_apply.hide(true) tool_ctl.settings_apply.hide(true)
main_pane.set_value(5) main_pane.set_value(5)
end end
@@ -220,8 +210,9 @@ local function config_view(display)
if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end
PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
-- PLC CONFIG --#region PLC
local plc_c_1 = Div{parent=plc_cfg,x=2,y=4,width=49} 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_2 = Div{parent=plc_cfg,x=2,y=4,width=49}
@@ -230,10 +221,10 @@ local function config_view(display)
local plc_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4}} local plc_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4}}
TextBox{parent=plc_cfg,x=1,y=2,height=1,text_align=CENTER,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)} 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_align=CENTER,text="Would you like to set this PLC as networked?"} 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_align=CENTER,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=cpair(colors.gray,colors.lightGray)} 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 networked = CheckBox{parent=plc_c_1,x=1,y=8,label="Networked",default=ini_cfg.Networked,box_fg_bg=cpair(colors.orange,colors.black)}
@@ -242,16 +233,16 @@ local function config_view(display)
plc_pane.set_value(2) plc_pane.set_value(2)
end end
PushButton{parent=plc_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=plc_c_1,x=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,min_width=6,text="Next \x1a",callback=submit_networked,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_align=CENTER,text="Please enter the reactor unit ID for this PLC."} 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_align=CENTER,text="If this is a networked PLC, currently only IDs 1 through 4 are acceptable.",fg_bg=cpair(colors.gray,colors.lightGray)} 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_align=CENTER,text="Unit #"} TextBox{parent=plc_c_2,x=1,y=6,height=1,text="Unit #"}
local u_id = NumberField{parent=plc_c_2,x=7,y=6,width=5,max_digits=3,default=ini_cfg.UnitID,min=1,fg_bg=cpair(colors.black,colors.white)} 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_align=LEFT,text="Please set a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} 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 function submit_id()
local unit_id = tonumber(u_id.get_value()) local unit_id = tonumber(u_id.get_value())
@@ -262,11 +253,11 @@ local function config_view(display)
else u_id_err.show() end else u_id_err.show() end
end end
PushButton{parent=plc_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=plc_c_2,x=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,min_width=6,text="Next \x1a",callback=submit_id,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_align=CENTER,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=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_align=CENTER,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=cpair(colors.gray,colors.lightGray)} 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 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)}
@@ -279,27 +270,29 @@ local function config_view(display)
if tmp_cfg.EmerCoolEnable then plc_pane.set_value(4) else next_from_plc() end if tmp_cfg.EmerCoolEnable then plc_pane.set_value(4) else next_from_plc() end
end end
PushButton{parent=plc_c_3,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=plc_c_3,x=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,min_width=6,text="Next \x1a",callback=submit_en_emcool,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_align=CENTER,text="Emergency Coolant Redstone Output Side"} 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} 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_align=CENTER,text="Bundled Redstone Configuration"} 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 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=cpair(colors.gray,colors.lightGray)} 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 if ini_cfg.EmerCoolColor == nil then color.disable() end
local function submit_emcool() local function submit_emcool()
tmp_cfg.EmerCoolSide = side_options_map[side.get_value()] tmp_cfg.EmerCoolSide = side_options_map[side.get_value()]
tmp_cfg.EmerCoolColor = color_options_map[color.get_value()] tmp_cfg.EmerCoolColor = util.trinary(bundled.get_value(), color_options_map[color.get_value()], nil)
next_from_plc() next_from_plc()
end end
PushButton{parent=plc_c_4,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=plc_c_4,x=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,min_width=6,text="Next \x1a",callback=submit_emcool,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}
-- NET CONFIG --#endregion
--#region Network
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49} 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_2 = Div{parent=net_cfg,x=2,y=4,width=49}
@@ -307,19 +300,19 @@ local function config_view(display)
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}} local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}}
TextBox{parent=net_cfg,x=1,y=2,height=1,text_align=CENTER,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} 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_align=CENTER,text="Please set the network channels below."} 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_align=CENTER,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=cpair(colors.gray,colors.lightGray)} 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_align=CENTER,text="Supervisor Channel"} 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=cpair(colors.black,colors.white)} 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_align=CENTER,text="[SVR_CHANNEL]",fg_bg=cpair(colors.gray,colors.lightGray)} 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_align=CENTER,text="PLC Channel"} 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=cpair(colors.black,colors.white)} 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_align=CENTER,text="[PLC_CHANNEL]",fg_bg=cpair(colors.gray,colors.lightGray)} 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_align=LEFT,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} 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 function submit_channels()
local svr_c = tonumber(svr_chan.get_value()) local svr_c = tonumber(svr_chan.get_value())
@@ -338,19 +331,19 @@ local function config_view(display)
end end
end end
PushButton{parent=net_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=net_c_1,x=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,min_width=6,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,height=1,text_align=CENTER,text="Connection Timeout"} TextBox{parent=net_c_2,x=1,y=1,height=1,text="Connection Timeout"}
local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,fg_bg=cpair(colors.black,colors.white)} 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_align=CENTER,text="seconds (default 5)",fg_bg=cpair(colors.gray,colors.lightGray)} 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_align=CENTER,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=cpair(colors.gray,colors.lightGray)} 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_align=CENTER,text="Trusted Range"} TextBox{parent=net_c_2,x=1,y=8,height=1,text="Trusted Range"}
local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_digits=20,allow_decimal=true,fg_bg=cpair(colors.black,colors.white)} 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_align=CENTER,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=cpair(colors.gray,colors.lightGray)} 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_align=LEFT,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} 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 function submit_ct_tr()
local timeout_val = tonumber(timeout.get_value()) local timeout_val = tonumber(timeout.get_value())
@@ -369,14 +362,14 @@ local function config_view(display)
end end
end end
PushButton{parent=net_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=net_c_2,x=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,min_width=6,text="Next \x1a",callback=submit_ct_tr,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_align=CENTER,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."} TextBox{parent=net_c_3,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
TextBox{parent=net_c_3,x=1,y=4,height=6,text_align=CENTER,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=cpair(colors.gray,colors.lightGray)} 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_align=CENTER,text="Facility Auth Key"} 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=cpair(colors.black,colors.white)} local key, _, censor = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
@@ -385,32 +378,40 @@ local function config_view(display)
hide_key.set_value(true) hide_key.set_value(true)
censor_key(true) censor_key(true)
local key_err = TextBox{parent=net_c_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 function submit_auth()
tmp_cfg.AuthKey = key.get_value() local v = key.get_value()
main_pane.set_value(4) 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 end
PushButton{parent=net_c_3,x=1,y=14,min_width=6,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=net_c_3,x=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,min_width=6,text="Next \x1a",callback=submit_auth,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}
-- LOG CONFIG --#endregion
--#region Logging
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49} local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49}
TextBox{parent=log_cfg,x=1,y=2,height=1,text_align=CENTER,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)} 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_align=CENTER,text="Please configure logging below."} 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_align=CENTER,text="Log File Mode"} 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} 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_align=CENTER,text="Log File Path"} 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=cpair(colors.black,colors.white)} local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)} local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
TextBox{parent=log_c_1,x=3,y=11,height=2,text_align=CENTER,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=cpair(colors.gray,colors.lightGray)} 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_align=LEFT,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,height=1,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_log() local function submit_log()
if path.get_value() ~= "" then if path.get_value() ~= "" then
@@ -430,10 +431,12 @@ local function config_view(display)
if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(2) end if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(2) end
end end
PushButton{parent=log_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=back_from_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=log_c_1,x=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,min_width=6,text="Next \x1a",callback=submit_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}
-- SUMMARY OF CHANGES --#endregion
--#region Summary and Saving
local sum_c_1 = Div{parent=summary,x=2,y=4,width=49} 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_2 = Div{parent=summary,x=2,y=4,width=49}
@@ -442,9 +445,9 @@ local function config_view(display)
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}} local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
TextBox{parent=summary,x=1,y=2,height=1,text_align=CENTER,text=" Summary",fg_bg=cpair(colors.black,colors.green)} TextBox{parent=summary,x=1,y=2,height=1,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=51,scroll_height=100,fg_bg=cpair(colors.black,colors.white),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function back_from_settings() local function back_from_settings()
if tool_ctl.viewing_config or tool_ctl.importing_legacy then if tool_ctl.viewing_config or tool_ctl.importing_legacy then
@@ -466,7 +469,8 @@ local function config_view(display)
local function save_and_continue() local function save_and_continue()
for k, v in pairs(tmp_cfg) do settings.set(k, v) end for k, v in pairs(tmp_cfg) do settings.set(k, v) end
if settings.save("reactor-plc.settings") then if settings.save("/reactor-plc.settings") then
load_settings(settings_cfg, true)
load_settings(ini_cfg) load_settings(ini_cfg)
try_set(networked, ini_cfg.Networked) try_set(networked, ini_cfg.Networked)
@@ -484,6 +488,8 @@ local function config_view(display)
try_set(path, ini_cfg.LogPath) try_set(path, ini_cfg.LogPath)
try_set(en_dbg, ini_cfg.LogDebug) try_set(en_dbg, ini_cfg.LogDebug)
tool_ctl.view_cfg.enable()
if tool_ctl.importing_legacy then if tool_ctl.importing_legacy then
tool_ctl.importing_legacy = false tool_ctl.importing_legacy = false
sum_pane.set_value(3) sum_pane.set_value(3)
@@ -495,11 +501,11 @@ local function config_view(display)
end end
end end
PushButton{parent=sum_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} 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.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} 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_align=CENTER,text="Settings saved!"} TextBox{parent=sum_c_2,x=1,y=1,height=1,text="Settings saved!"}
local function go_home() local function go_home()
main_pane.set_value(1) main_pane.set_value(1)
@@ -511,7 +517,7 @@ local function config_view(display)
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=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)} 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_align=CENTER,text="The old config.lua file will now be deleted, then the configurator will exit."} 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() local function delete_legacy()
fs.delete("/reactor-plc/config.lua") fs.delete("/reactor-plc/config.lua")
@@ -521,11 +527,31 @@ local function config_view(display)
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=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)} 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_align=CENTER,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."} 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=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)} PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
--#endregion
-- Config Change Log
local cl = Div{parent=changelog,x=2,y=4,width=49}
TextBox{parent=changelog,x=1,y=2,height=1,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}
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=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 -- set tool functions now that we have the elements
function tool_ctl.set_networked(enable) function tool_ctl.set_networked(enable)
@@ -593,10 +619,10 @@ local function config_view(display)
if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) end if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) end
if f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end if f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end
if f[1] == "EmerCoolColor" and raw ~= nil then val = color_name_map[raw] end if f[1] == "EmerCoolColor" and raw ~= nil then val = rsio.color_name(raw) end
if val == "nil" then val = "n/a" end if val == "nil" then val = "<not set>" end
local c = util.trinary(alternate, cpair(colors.gray,colors.lightGray), cpair(colors.gray,colors.white)) local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate alternate = not alternate
if string.len(val) > val_max_w then if string.len(val) > val_max_w then
@@ -628,12 +654,12 @@ local function reset_term()
end end
-- run the reactor PLC configurator -- run the reactor PLC configurator
---@param ask_config? boolean indicate if this is being called by the PLC startup app due to an invalid configuration ---@param ask_config? boolean indicate if this is being called by the startup app due to an invalid configuration
function configurator.configure(ask_config) function configurator.configure(ask_config)
tool_ctl.ask_config = ask_config == true tool_ctl.ask_config = ask_config == true
tool_ctl.has_config = settings.load("/reactor-plc.settings")
load_settings(ini_cfg) load_settings(settings_cfg, true)
tool_ctl.has_config = load_settings(ini_cfg)
reset_term() reset_term()
@@ -651,18 +677,14 @@ function configurator.configure(ask_config)
-- handle event -- handle event
if event == "timer" then if event == "timer" then
-- notify timer callback dispatcher
tcd.handle(param1) tcd.handle(param1)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then 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
local m_e = core.events.new_mouse_event(event, param1, param2, param3) local m_e = core.events.new_mouse_event(event, param1, param2, param3)
if m_e then display.handle_mouse(m_e) end if m_e then display.handle_mouse(m_e) end
elseif event == "char" or event == "key" or event == "key_up" then elseif event == "char" or event == "key" or event == "key_up" then
-- handle a key event
local k_e = core.events.new_key_event(event, param1, param2) local k_e = core.events.new_key_event(event, param1, param2)
if k_e then display.handle_key(k_e) end if k_e then display.handle_key(k_e) end
elseif event == "paste" then elseif event == "paste" then
-- handle a paste event
display.handle_paste(param1) display.handle_paste(param1)
end end

View File

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

View File

@@ -37,14 +37,17 @@ function plc.load_config()
config.Networked = settings.get("Networked") config.Networked = settings.get("Networked")
config.UnitID = settings.get("UnitID") config.UnitID = settings.get("UnitID")
config.EmerCoolEnable = settings.get("EmerCoolEnable") config.EmerCoolEnable = settings.get("EmerCoolEnable")
config.EmerCoolSide = settings.get("EmerCoolSide") config.EmerCoolSide = settings.get("EmerCoolSide")
config.EmerCoolColor = settings.get("EmerCoolColor") config.EmerCoolColor = settings.get("EmerCoolColor")
config.SVR_Channel = settings.get("SVR_Channel") config.SVR_Channel = settings.get("SVR_Channel")
config.PLC_Channel = settings.get("PLC_Channel") config.PLC_Channel = settings.get("PLC_Channel")
config.ConnTimeout = settings.get("ConnTimeout") config.ConnTimeout = settings.get("ConnTimeout")
config.TrustedRange = settings.get("TrustedRange") config.TrustedRange = settings.get("TrustedRange")
config.AuthKey = settings.get("AuthKey") config.AuthKey = settings.get("AuthKey")
config.LogMode = settings.get("LogMode") config.LogMode = settings.get("LogMode")
config.LogPath = settings.get("LogPath") config.LogPath = settings.get("LogPath")
config.LogDebug = settings.get("LogDebug") config.LogDebug = settings.get("LogDebug")
@@ -54,14 +57,24 @@ function plc.load_config()
cfv.assert_type_bool(config.Networked) cfv.assert_type_bool(config.Networked)
cfv.assert_type_int(config.UnitID) cfv.assert_type_int(config.UnitID)
cfv.assert_type_bool(config.EmerCoolEnable) cfv.assert_type_bool(config.EmerCoolEnable)
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.PLC_Channel) if config.Networked == true then
cfv.assert_type_int(config.ConnTimeout) cfv.assert_channel(config.SVR_Channel)
cfv.assert_min(config.ConnTimeout, 2) cfv.assert_channel(config.PLC_Channel)
cfv.assert_type_num(config.TrustedRange) cfv.assert_type_num(config.ConnTimeout)
cfv.assert_min(config.TrustedRange, 0) cfv.assert_min(config.ConnTimeout, 2)
cfv.assert_type_str(config.AuthKey) cfv.assert_type_num(config.TrustedRange)
cfv.assert_min(config.TrustedRange, 0)
cfv.assert_type_str(config.AuthKey)
if type(config.AuthKey) == "string" then
local len = string.len(config.AuthKey)
cfv.assert_eq(len == 0 or len >= 8, true)
end
end
cfv.assert_type_int(config.LogMode) cfv.assert_type_int(config.LogMode)
cfv.assert_range(config.LogMode, 0, 1)
cfv.assert_type_str(config.LogPath) cfv.assert_type_str(config.LogPath)
cfv.assert_type_bool(config.LogDebug) cfv.assert_type_bool(config.LogDebug)
@@ -116,6 +129,21 @@ function plc.rps_init(reactor, is_formed)
end end
end end
-- check if the result of a peripheral call was OK, handle the failure if not
---@nodiscard
---@param result any PPM function call result
---@return boolean succeeded if the result is OK, false if it was a PPM failure
local function _check_and_handle_ppm_call(result)
if result == ppm.ACCESS_FAULT then
_set_fault()
elseif result == ppm.UNDEFINED_FIELD then
_set_fault()
self.formed = false
else return true end
return false
end
-- set emergency coolant control (if configured) -- set emergency coolant control (if configured)
---@param state boolean true to enable emergency coolant, false to disable ---@param state boolean true to enable emergency coolant, false to disable
local function _set_emer_cool(state) local function _set_emer_cool(state)
@@ -154,25 +182,20 @@ function plc.rps_init(reactor, is_formed)
-- check if the reactor is formed -- check if the reactor is formed
local function _is_formed() local function _is_formed()
local formed = reactor.isFormed() local formed = reactor.isFormed()
if formed == ppm.ACCESS_FAULT then if _check_and_handle_ppm_call(formed) then
-- lost the peripheral or terminated, handled later
_set_fault()
else
self.formed = formed self.formed = formed
end
if not self.state[state_keys.sys_fail] then -- always update, since some ppm failures constitute not being formed
self.state[state_keys.sys_fail] = not formed if not self.state[state_keys.sys_fail] then
end self.state[state_keys.sys_fail] = not self.formed
end end
end end
-- check if the reactor is force disabled -- check if the reactor is force disabled
local function _is_force_disabled() local function _is_force_disabled()
local disabled = reactor.isForceDisabled() local disabled = reactor.isForceDisabled()
if disabled == ppm.ACCESS_FAULT then if _check_and_handle_ppm_call(disabled) then
-- lost the peripheral or terminated, handled later
_set_fault()
else
self.force_disabled = disabled self.force_disabled = disabled
if not self.state[state_keys.force_disabled] then if not self.state[state_keys.force_disabled] then
@@ -184,22 +207,16 @@ function plc.rps_init(reactor, is_formed)
-- check for high damage -- check for high damage
local function _high_damage() local function _high_damage()
local damage_percent = reactor.getDamagePercent() local damage_percent = reactor.getDamagePercent()
if damage_percent == ppm.ACCESS_FAULT then if _check_and_handle_ppm_call(damage_percent) and not self.state[state_keys.high_dmg] then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.high_dmg] then
self.state[state_keys.high_dmg] = damage_percent >= RPS_LIMITS.MAX_DAMAGE_PERCENT self.state[state_keys.high_dmg] = damage_percent >= RPS_LIMITS.MAX_DAMAGE_PERCENT
end end
end end
-- check if the reactor is at a critically high temperature -- check if the reactor is at a critically high temperature
local function _high_temp() local function _high_temp()
-- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200 -- mekanism: MAX_DAMAGE_TEMPERATURE = 1200K
local temp = reactor.getTemperature() local temp = reactor.getTemperature()
if temp == ppm.ACCESS_FAULT then if _check_and_handle_ppm_call(temp) and not self.state[state_keys.high_temp] then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.high_temp] then
self.state[state_keys.high_temp] = temp >= RPS_LIMITS.MAX_DAMAGE_TEMPERATURE self.state[state_keys.high_temp] = temp >= RPS_LIMITS.MAX_DAMAGE_TEMPERATURE
end end
end end
@@ -207,10 +224,7 @@ function plc.rps_init(reactor, is_formed)
-- check if there is very low coolant -- check if there is very low coolant
local function _low_coolant() local function _low_coolant()
local coolant_filled = reactor.getCoolantFilledPercentage() local coolant_filled = reactor.getCoolantFilledPercentage()
if coolant_filled == ppm.ACCESS_FAULT then if _check_and_handle_ppm_call(coolant_filled) and not self.state[state_keys.low_coolant] then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.low_coolant] then
self.state[state_keys.low_coolant] = coolant_filled < RPS_LIMITS.MIN_COOLANT_FILL self.state[state_keys.low_coolant] = coolant_filled < RPS_LIMITS.MIN_COOLANT_FILL
end end
end end
@@ -218,10 +232,7 @@ function plc.rps_init(reactor, is_formed)
-- check for excess waste (>80% filled) -- check for excess waste (>80% filled)
local function _excess_waste() local function _excess_waste()
local w_filled = reactor.getWasteFilledPercentage() local w_filled = reactor.getWasteFilledPercentage()
if w_filled == ppm.ACCESS_FAULT then if _check_and_handle_ppm_call(w_filled) and not self.state[state_keys.ex_waste] then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.ex_waste] then
self.state[state_keys.ex_waste] = w_filled > RPS_LIMITS.MAX_WASTE_FILL self.state[state_keys.ex_waste] = w_filled > RPS_LIMITS.MAX_WASTE_FILL
end end
end end
@@ -229,10 +240,7 @@ function plc.rps_init(reactor, is_formed)
-- check for heated coolant backup (>95% filled) -- check for heated coolant backup (>95% filled)
local function _excess_heated_coolant() local function _excess_heated_coolant()
local hc_filled = reactor.getHeatedCoolantFilledPercentage() local hc_filled = reactor.getHeatedCoolantFilledPercentage()
if hc_filled == ppm.ACCESS_FAULT then if _check_and_handle_ppm_call(hc_filled) and not self.state[state_keys.ex_hcoolant] then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.ex_hcoolant] then
self.state[state_keys.ex_hcoolant] = hc_filled > RPS_LIMITS.MAX_HEATED_COLLANT_FILL self.state[state_keys.ex_hcoolant] = hc_filled > RPS_LIMITS.MAX_HEATED_COLLANT_FILL
end end
end end
@@ -240,10 +248,7 @@ function plc.rps_init(reactor, is_formed)
-- check if there is no fuel -- check if there is no fuel
local function _insufficient_fuel() local function _insufficient_fuel()
local fuel = reactor.getFuelFilledPercentage() local fuel = reactor.getFuelFilledPercentage()
if fuel == ppm.ACCESS_FAULT then if _check_and_handle_ppm_call(fuel) and not self.state[state_keys.no_fuel] then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.no_fuel] then
self.state[state_keys.no_fuel] = fuel <= RPS_LIMITS.NO_FUEL_FILL self.state[state_keys.no_fuel] = fuel <= RPS_LIMITS.NO_FUEL_FILL
end end
end end
@@ -464,13 +469,22 @@ function plc.rps_init(reactor, is_formed)
self.tripped = false self.tripped = false
self.trip_cause = RPS_TRIP_CAUSE.OK self.trip_cause = RPS_TRIP_CAUSE.OK
for i = 1, #self.state do for i = 1, #self.state do self.state[i] = false end
self.state[i] = false
end
if not quiet then log.info("RPS: reset") end if not quiet then log.info("RPS: reset") end
end end
-- partial RPS reset that only clears fault and sys_fail
function public.reset_formed()
self.tripped = false
self.trip_cause = RPS_TRIP_CAUSE.OK
self.state[state_keys.fault] = false
self.state[state_keys.sys_fail] = false
log.info("RPS: partial reset on formed")
end
-- reset the automatic and timeout trip flags, then clear trip if that was the trip cause -- reset the automatic and timeout trip flags, then clear trip if that was the trip cause
function public.auto_reset() function public.auto_reset()
self.state[state_keys.automatic] = false self.state[state_keys.automatic] = false

View File

@@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer") local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads") local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "v1.6.0" local R_PLC_VERSION = "v1.6.14"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@@ -31,9 +31,13 @@ if not plc.load_config() then
-- try to reconfigure (user action) -- try to reconfigure (user action)
local success, error = configure.configure(true) local success, error = configure.configure(true)
if success then if success then
assert(plc.load_config(), "failed to load valid reactor PLC configuration") if not plc.load_config() then
println("failed to load a valid configuration, please reconfigure")
return
end
else else
assert(success, "reactor PLC configuration error: " .. error) println("configuration error: " .. error)
return
end end
end end
@@ -50,7 +54,7 @@ log.info("BOOTING reactor-plc.startup " .. R_PLC_VERSION)
log.info("========================================") log.info("========================================")
println(">> Reactor PLC " .. R_PLC_VERSION .. " <<") println(">> Reactor PLC " .. R_PLC_VERSION .. " <<")
crash.set_env("plc", R_PLC_VERSION) crash.set_env("reactor-plc", R_PLC_VERSION)
---------------------------------------- ----------------------------------------
-- main application -- main application
@@ -69,7 +73,7 @@ local function main()
ppm.mount_all() ppm.mount_all()
-- message authentication init -- message authentication init
if string.len(config.AuthKey) > 0 then if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
network.init_mac(config.AuthKey) network.init_mac(config.AuthKey)
end end
@@ -131,15 +135,22 @@ local function main()
-- we need a reactor, can at least do some things even if it isn't formed though -- we need a reactor, can at least do some things even if it isn't formed though
if plc_state.no_reactor then if plc_state.no_reactor then
println("init> fission reactor not found"); println("init> fission reactor not found")
log.warning("init> no reactor on startup") log.warning("init> no reactor on startup")
plc_state.init_ok = false plc_state.init_ok = false
plc_state.degraded = true plc_state.degraded = true
elseif not smem_dev.reactor.isFormed() then elseif not smem_dev.reactor.isFormed() then
println("init> fission reactor not formed"); println("init> fission reactor is not formed")
log.warning("init> reactor logic adapter present, but reactor is not formed") log.warning("init> reactor logic adapter present, but reactor is not formed")
plc_state.degraded = true
plc_state.reactor_formed = false
elseif smem_dev.reactor.getStatus() == ppm.UNDEFINED_FIELD then
-- reactor formed after ppm.mount_all was called
println("init> fission reactor was not formed")
log.warning("init> reactor reported formed, but multiblock functions are not available")
plc_state.degraded = true plc_state.degraded = true
plc_state.reactor_formed = false plc_state.reactor_formed = false
end end

View File

@@ -125,9 +125,8 @@ function threads.thread__main(smem, init)
plc_comms.reconnect_reactor(plc_dev.reactor) plc_comms.reconnect_reactor(plc_dev.reactor)
end end
-- reset RPS for newly connected reactor -- partial reset of RPS, specific to becoming formed
-- without this, is_formed will be out of date and cause it to think its no longer formed again rps.reset_formed()
rps.reset()
else else
-- fully lost the reactor now :( -- fully lost the reactor now :(
println_ts("reactor lost (failed reconnect)!") println_ts("reactor lost (failed reconnect)!")
@@ -173,7 +172,8 @@ function threads.thread__main(smem, init)
plc_state.degraded = true plc_state.degraded = true
elseif networked and type == "modem" then elseif networked and type == "modem" then
-- we only care if this is our wireless modem -- we only care if this is our wireless modem
if nic.is_modem(device) then -- note, check init_ok first since nic will be nil if it is false
if plc_state.init_ok and nic.is_modem(device) then
nic.disconnect() nic.disconnect()
println_ts("comms modem disconnected!") println_ts("comms modem disconnected!")
@@ -193,7 +193,7 @@ function threads.thread__main(smem, init)
end end
end end
else else
log.warning("non-comms modem disconnected") log.warning("a modem was disconnected")
end end
end end
end end
@@ -230,12 +230,12 @@ function threads.thread__main(smem, init)
plc_comms.reconnect_reactor(plc_dev.reactor) plc_comms.reconnect_reactor(plc_dev.reactor)
end end
-- reset RPS for newly connected reactor -- partial reset of RPS, specific to becoming formed
-- without this, is_formed will be out of date and cause it to think its no longer formed again rps.reset_formed()
rps.reset()
end end
elseif networked and type == "modem" then elseif networked and type == "modem" then
if device.isWireless() and not nic.is_connected() then -- note, check init_ok first since nic will be nil if it is false
if device.isWireless() and not (plc_state.init_ok and nic.is_connected()) then
-- reconnected modem -- reconnected modem
plc_dev.modem = device plc_dev.modem = device
plc_state.no_modem = false plc_state.no_modem = false

View File

@@ -1,73 +0,0 @@
local rsio = require("scada-common.rsio")
local config = {}
-- supervisor comms channel
config.SVR_CHANNEL = 16240
-- RTU/MODBUS comms channel
config.RTU_CHANNEL = 16242
-- max trusted modem message distance (0 to disable check)
config.TRUSTED_RANGE = 0
-- time in seconds (>= 2) before assuming a remote device is no longer active
config.COMMS_TIMEOUT = 5
-- facility authentication key (do NOT use one of your passwords)
-- this enables verifying that messages are authentic
-- all devices on the same network must use the same key
-- config.AUTH_KEY = "SCADAfacility123"
-- alarm sounder volume (0.0 to 3.0, 1.0 being standard max volume, this is the option given to to speaker.play())
-- note: alarm sine waves are at half saturation, so that multiple will be required to reach full scale
config.SOUNDER_VOLUME = 1.0
-- log path
config.LOG_PATH = "/log.txt"
-- log mode
-- 0 = APPEND (adds to existing file on start)
-- 1 = NEW (replaces existing file on start)
config.LOG_MODE = 0
-- true to log verbose debug messages
config.LOG_DEBUG = false
-- RTU peripheral devices (named: side/network device name)
config.RTU_DEVICES = {
{
name = "boilerValve_0",
index = 1,
for_reactor = 1
},
{
name = "turbineValve_0",
index = 1,
for_reactor = 1
}
}
-- RTU redstone interface definitions
config.RTU_REDSTONE = {
-- {
-- for_reactor = 1,
-- io = {
-- {
-- port = rsio.IO.WASTE_PO,
-- side = "top",
-- bundled_color = colors.red
-- },
-- {
-- port = rsio.IO.WASTE_PU,
-- side = "top",
-- bundled_color = colors.orange
-- },
-- {
-- port = rsio.IO.WASTE_POPL,
-- side = "top",
-- bundled_color = colors.yellow
-- },
-- {
-- port = rsio.IO.WASTE_AM,
-- side = "top",
-- bundled_color = colors.lime
-- }
-- }
-- }
}
return config

1531
rtu/configure.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -109,12 +109,10 @@ local function init(panel, units)
unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update) unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update)
-- unit name identifier (type + index) -- unit name identifier (type + index)
local name = util.c(UNIT_TYPE_LABELS[unit.type + 1], " ", unit.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=name,height=1} local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(unit.type),width=15,height=1}
name_box.register(databus.ps, "unit_type_" .. i, function (t) name_box.register(databus.ps, "unit_type_" .. i, function (t) name_box.set_value(get_name(t)) end)
name_box.set_value(util.c(UNIT_TYPE_LABELS[t + 1], " ", unit.index))
end)
-- assignment (unit # or facility) -- assignment (unit # or facility)
local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor) local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor)

View File

@@ -5,7 +5,6 @@ local log = require("scada-common.log")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local config = require("rtu.config")
local databus = require("rtu.databus") local databus = require("rtu.databus")
local modbus = require("rtu.modbus") local modbus = require("rtu.modbus")
@@ -17,6 +16,57 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
---@type rtu_config
local config = {}
rtu.config = config
-- load the RTU configuration
function rtu.load_config()
if not settings.load("/rtu.settings") then return false end
config.Peripherals = settings.get("Peripherals")
config.Redstone = settings.get("Redstone")
config.SpeakerVolume = settings.get("SpeakerVolume")
config.SVR_Channel = settings.get("SVR_Channel")
config.RTU_Channel = settings.get("RTU_Channel")
config.ConnTimeout = settings.get("ConnTimeout")
config.TrustedRange = settings.get("TrustedRange")
config.AuthKey = settings.get("AuthKey")
config.LogMode = settings.get("LogMode")
config.LogPath = settings.get("LogPath")
config.LogDebug = settings.get("LogDebug")
local cfv = util.new_validator()
cfv.assert_type_num(config.SpeakerVolume)
cfv.assert_range(config.SpeakerVolume, 0, 3)
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.RTU_Channel)
cfv.assert_type_num(config.ConnTimeout)
cfv.assert_min(config.ConnTimeout, 2)
cfv.assert_type_num(config.TrustedRange)
cfv.assert_min(config.TrustedRange, 0)
cfv.assert_type_str(config.AuthKey)
if type(config.AuthKey) == "string" then
local len = string.len(config.AuthKey)
cfv.assert_eq(len == 0 or len >= 8, true)
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_table(config.Peripherals)
cfv.assert_type_table(config.Redstone)
return cfv.valid()
end
-- create a new RTU unit -- create a new RTU unit
---@nodiscard ---@nodiscard
function rtu.init_unit() function rtu.init_unit()
@@ -175,7 +225,7 @@ function rtu.init_sounder(speaker)
function spkr_ctl.continue() function spkr_ctl.continue()
if spkr_ctl.playing then if spkr_ctl.playing then
if spkr_ctl.speaker ~= nil and spkr_ctl.stream.has_next_block() then if spkr_ctl.speaker ~= nil and spkr_ctl.stream.has_next_block() then
local success = spkr_ctl.speaker.playAudio(spkr_ctl.stream.get_next_block(), config.SOUNDER_VOLUME) local success = spkr_ctl.speaker.playAudio(spkr_ctl.stream.get_next_block(), config.SpeakerVolume)
if not success then log.error(util.c("rtu_sounder(", spkr_ctl.name, "): error playing audio")) end if not success then log.error(util.c("rtu_sounder(", spkr_ctl.name, "): error playing audio")) end
end end
end end
@@ -203,11 +253,8 @@ end
---@nodiscard ---@nodiscard
---@param version string RTU version ---@param version string RTU version
---@param nic nic network interface device ---@param nic nic network interface device
---@param rtu_channel integer PLC comms channel
---@param svr_channel integer supervisor server channel
---@param range integer trusted device connection range
---@param conn_watchdog watchdog watchdog reference ---@param conn_watchdog watchdog watchdog reference
function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog) function rtu.comms(version, nic, conn_watchdog)
local self = { local self = {
sv_addr = comms.BROADCAST, sv_addr = comms.BROADCAST,
seq_num = 0, seq_num = 0,
@@ -218,13 +265,13 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
local insert = table.insert local insert = table.insert
comms.set_trusted_range(range) comms.set_trusted_range(config.TrustedRange)
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure modem channels -- configure modem channels
nic.closeAll() nic.closeAll()
nic.open(rtu_channel) nic.open(config.RTU_Channel)
-- send a scada management packet -- send a scada management packet
---@param msg_type MGMT_TYPE ---@param msg_type MGMT_TYPE
@@ -236,7 +283,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
m_pkt.make(msg_type, msg) m_pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
nic.transmit(svr_channel, rtu_channel, s_pkt) nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
end end
@@ -280,7 +327,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
function public.send_modbus(m_pkt) function public.send_modbus(m_pkt)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
nic.transmit(svr_channel, rtu_channel, s_pkt) nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
end end
@@ -365,7 +412,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
local l_chan = packet.scada_frame.local_channel() local l_chan = packet.scada_frame.local_channel()
local src_addr = packet.scada_frame.src_addr() local src_addr = packet.scada_frame.src_addr()
if l_chan == rtu_channel then if l_chan == config.RTU_Channel then
-- check sequence number -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num() self.r_seq_num = packet.scada_frame.seq_num()

View File

@@ -15,7 +15,7 @@ local rsio = require("scada-common.rsio")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local config = require("rtu.config") local configure = require("rtu.configure")
local databus = require("rtu.databus") local databus = require("rtu.databus")
local modbus = require("rtu.modbus") local modbus = require("rtu.modbus")
local renderer = require("rtu.renderer") local renderer = require("rtu.renderer")
@@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu") local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "v1.6.6" local RTU_VERSION = "v1.7.14"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
@@ -40,27 +40,30 @@ local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
---------------------------------------- ----------------------------------------
-- config validation -- get configuration
---------------------------------------- ----------------------------------------
local cfv = util.new_validator() if not rtu.load_config() then
-- try to reconfigure (user action)
local success, error = configure.configure(true)
if success then
if not rtu.load_config() then
println("failed to load a valid configuration, please reconfigure")
return
end
else
println("configuration error: " .. error)
return
end
end
cfv.assert_channel(config.SVR_CHANNEL) local config = rtu.config
cfv.assert_channel(config.RTU_CHANNEL)
cfv.assert_type_int(config.TRUSTED_RANGE)
cfv.assert_type_num(config.COMMS_TIMEOUT)
cfv.assert_min(config.COMMS_TIMEOUT, 2)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
cfv.assert_type_table(config.RTU_DEVICES)
cfv.assert_type_table(config.RTU_REDSTONE)
assert(cfv.valid(), "bad config file: missing/invalid fields")
---------------------------------------- ----------------------------------------
-- log init -- log init
---------------------------------------- ----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true) log.init(config.LogPath, config.LogMode, config.LogDebug)
log.info("========================================") log.info("========================================")
log.info("BOOTING rtu.startup " .. RTU_VERSION) log.info("BOOTING rtu.startup " .. RTU_VERSION)
@@ -85,8 +88,8 @@ local function main()
ppm.mount_all() ppm.mount_all()
-- message authentication init -- message authentication init
if type(config.AUTH_KEY) == "string" then if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
network.init_mac(config.AUTH_KEY) network.init_mac(config.AuthKey)
end end
-- get modem -- get modem
@@ -139,173 +142,174 @@ local function main()
local units = __shared_memory.rtu_sys.units local units = __shared_memory.rtu_sys.units
local rtu_redstone = config.RTU_REDSTONE local rtu_redstone = config.Redstone
local rtu_devices = config.RTU_DEVICES local rtu_devices = config.Peripherals
-- configure RTU gateway based on config file definitions -- configure RTU gateway based on settings file definitions
local function configure() local function sys_config()
-- redstone interfaces -- redstone interfaces
local rs_rtus = {}
-- go through redstone definitions list
for entry_idx = 1, #rtu_redstone do for entry_idx = 1, #rtu_redstone do
local rs_rtu = redstone_rtu.new() local entry = rtu_redstone[entry_idx] ---@type rtu_rs_definition
local io_table = rtu_redstone[entry_idx].io ---@type table local assignment
local io_reactor = rtu_redstone[entry_idx].for_reactor ---@type integer local for_reactor = entry.unit
local iface_name = util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side)
-- CHECK: reactor ID must be >= to 1 if util.is_int(entry.unit) and entry.unit > 0 and entry.unit < 5 then
if (not util.is_int(io_reactor)) or (io_reactor < 0) then ---@cast for_reactor integer
local message = util.c("configure> redstone entry #", entry_idx, " : ", io_reactor, " isn't an integer >= 0") assignment = "reactor unit " .. entry.unit
println(message) if rs_rtus[for_reactor] == nil then
log.fatal(message) log.debug(util.c("sys_config> allocated redstone RTU for reactor unit ", entry.unit))
return false rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} }
end
-- CHECK: io table exists
if type(io_table) ~= "table" then
local message = util.c("configure> redstone entry #", entry_idx, " no IO table found")
println(message)
log.fatal(message)
return false
end
local capabilities = {}
log.debug(util.c("configure> starting redstone RTU I/O linking for reactor ", io_reactor, "..."))
local continue = true
-- CHECK: no duplicate entries
for i = 1, #units do
local unit = units[i] ---@type rtu_unit_registry_entry
if unit.reactor == io_reactor and unit.type == RTU_UNIT_TYPE.REDSTONE then
-- duplicate entry
local message = util.c("configure> skipping definition block #", entry_idx, " for reactor ", io_reactor,
" with already defined redstone I/O")
println(message)
log.warning(message)
continue = false
break
end end
elseif entry.unit == nil then
assignment = "facility"
for_reactor = 0
if rs_rtus[for_reactor] == nil then
log.debug(util.c("sys_config> allocated redstone RTU for the facility"))
rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} }
end
else
local message = util.c("sys_config> invalid unit assignment at block index #", entry_idx)
println(message)
log.fatal(message)
return false
end end
-- not a duplicate -- verify configuration
if continue then local valid = false
for i = 1, #io_table do if rsio.is_valid_port(entry.port) and rsio.is_valid_side(entry.side) then
local valid = false valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color))
local conf = io_table[i] end
-- verify configuration local rs_rtu = rs_rtus[for_reactor].rtu
if rsio.is_valid_port(conf.port) and rsio.is_valid_side(conf.side) then local capabilities = rs_rtus[for_reactor].capabilities
if conf.bundled_color then
valid = rsio.is_color(conf.bundled_color)
else
valid = true
end
end
if not valid then if not valid then
local message = util.c("configure> invalid redstone definition at index ", i, " in definition block #", entry_idx, local message = util.c("sys_config> invalid redstone definition at block index #", entry_idx)
" (for reactor ", io_reactor, ")") println(message)
log.fatal(message)
return false
else
-- link redstone in RTU
local mode = rsio.get_io_mode(entry.port)
if mode == rsio.IO_MODE.DIGITAL_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, entry.port) then
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name)
println(message) println(message)
log.fatal(message) log.warning(message)
return false
else else
-- link redstone in RTU rs_rtu.link_di(entry.side, entry.color)
local mode = rsio.get_io_mode(conf.port)
if mode == rsio.IO_MODE.DIGITAL_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, conf.port) then
local message = util.c("configure> skipping duplicate input for port ", rsio.to_string(conf.port), " on side ", conf.side)
println(message)
log.warning(message)
else
rs_rtu.link_di(conf.side, conf.bundled_color)
end
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
rs_rtu.link_do(conf.side, conf.bundled_color)
elseif mode == rsio.IO_MODE.ANALOG_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, conf.port) then
local message = util.c("configure> skipping duplicate input for port ", rsio.to_string(conf.port), " on side ", conf.side)
println(message)
log.warning(message)
else
rs_rtu.link_ai(conf.side)
end
elseif mode == rsio.IO_MODE.ANALOG_OUT then
rs_rtu.link_ao(conf.side)
else
-- should be unreachable code, we already validated ports
log.error("configure> fell through if chain attempting to identify IO mode", true)
println("configure> encountered a software error, check logs")
return false
end
table.insert(capabilities, conf.port)
log.debug(util.c("configure> linked redstone ", #capabilities, ": ", rsio.to_string(conf.port),
" (", conf.side, ") for reactor ", io_reactor))
end end
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
rs_rtu.link_do(entry.side, entry.color)
elseif mode == rsio.IO_MODE.ANALOG_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, entry.port) then
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name)
println(message)
log.warning(message)
else
rs_rtu.link_ai(entry.side)
end
elseif mode == rsio.IO_MODE.ANALOG_OUT then
rs_rtu.link_ao(entry.side)
else
-- should be unreachable code, we already validated ports
log.error("sys_config> fell through if chain attempting to identify IO mode at block index #" .. entry_idx, true)
println("sys_config> encountered a software error, check logs")
return false
end end
---@class rtu_unit_registry_entry table.insert(capabilities, entry.port)
local unit = {
uid = 0, ---@type integer
name = "redstone_io", ---@type string
type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE
index = entry_idx, ---@type integer
reactor = io_reactor, ---@type integer
device = capabilities, ---@type table use device field for redstone ports
is_multiblock = false, ---@type boolean
formed = nil, ---@type boolean|nil
hw_state = RTU_UNIT_HW_STATE.OK, ---@type RTU_UNIT_HW_STATE
rtu = rs_rtu, ---@type rtu_device|rtu_rs_device
modbus_io = modbus.new(rs_rtu, false),
pkt_queue = nil, ---@type mqueue|nil
thread = nil ---@type parallel_thread|nil
}
table.insert(units, unit) log.debug(util.c("sys_config> linked redstone ", #capabilities, ": ", rsio.to_string(entry.port), " (", iface_name, ") for ", assignment))
local for_message = "facility"
if io_reactor > 0 then
for_message = util.c("reactor ", io_reactor)
end
log.info(util.c("configure> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message))
unit.uid = #units
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
end end
end end
-- create unit entries for redstone RTUs
for for_reactor, def in pairs(rs_rtus) do
---@class rtu_unit_registry_entry
local unit = {
uid = 0, ---@type integer
name = "redstone_io", ---@type string
type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE
index = false, ---@type integer|false
reactor = for_reactor, ---@type integer
device = def.capabilities, ---@type table use device field for redstone ports
is_multiblock = false, ---@type boolean
formed = nil, ---@type boolean|nil
hw_state = RTU_UNIT_HW_STATE.OK, ---@type RTU_UNIT_HW_STATE
rtu = def.rtu, ---@type rtu_device|rtu_rs_device
modbus_io = modbus.new(def.rtu, false),
pkt_queue = nil, ---@type mqueue|nil
thread = nil ---@type parallel_thread|nil
}
table.insert(units, unit)
local for_message = "facility"
if util.is_int(for_reactor) then
for_message = util.c("reactor unit ", for_reactor)
end
log.info(util.c("sys_config> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message))
unit.uid = #units
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
end
-- mounted peripherals -- mounted peripherals
for i = 1, #rtu_devices do for i = 1, #rtu_devices do
local name = rtu_devices[i].name local entry = rtu_devices[i] ---@type rtu_peri_definition
local index = rtu_devices[i].index local name = entry.name
local for_reactor = rtu_devices[i].for_reactor local index = entry.index
local for_reactor = util.trinary(entry.unit == nil, 0, entry.unit)
-- CHECK: name is a string -- CHECK: name is a string
if type(name) ~= "string" then if type(name) ~= "string" then
local message = util.c("configure> device entry #", i, ": device ", name, " isn't a string") local message = util.c("sys_config> device entry #", i, ": device ", name, " isn't a string")
println(message) println(message)
log.fatal(message) log.fatal(message)
return false return false
end end
-- CHECK: index is an integer >= 1 -- CHECK: index type
if (not util.is_int(index)) or (index <= 0) then if (index ~= nil) and (not util.is_int(index)) then
local message = util.c("configure> device entry #", i, ": index ", index, " isn't an integer >= 1") local message = util.c("sys_config> device entry #", i, ": index ", index, " isn't valid")
println(message) println(message)
log.fatal(message) log.fatal(message)
return false return false
end end
-- CHECK: index range
local function validate_index(min, max)
if (not util.is_int(index)) or ((index < min) and (max ~= nil and index > max)) then
local message = util.c("sys_config> device entry #", i, ": index ", index, " isn't >= ", min)
if max ~= nil then message = util.c(message, " and <= ", max) end
println(message)
log.fatal(message)
return false
else return true end
end
-- CHECK: reactor is an integer >= 0 -- CHECK: reactor is an integer >= 0
if (not util.is_int(for_reactor)) or (for_reactor < 0) then local function validate_assign(for_facility)
local message = util.c("configure> device entry #", i, ": reactor ", for_reactor, " isn't an integer >= 0") if for_facility and for_reactor ~= 0 then
println(message) local message = util.c("sys_config> device entry #", i, ": must only be for the facility")
log.fatal(message) println(message)
return false log.fatal(message)
return false
elseif (not for_facility) and ((not util.is_int(for_reactor)) or (for_reactor < 1) or (for_reactor > 4)) then
local message = util.c("sys_config> device entry #", i, ": unit assignment ", for_reactor, " isn't vaild")
println(message)
log.fatal(message)
return false
else return true end
end end
local device = ppm.get_periph(name) local device = ppm.get_periph(name)
@@ -318,7 +322,7 @@ local function main()
local faulted = nil ---@type boolean|nil local faulted = nil ---@type boolean|nil
if device == nil then if device == nil then
local message = util.c("configure> '", name, "' not found, using placeholder") local message = util.c("sys_config> '", name, "' not found, using placeholder")
println(message) println(message)
log.warning(message) log.warning(message)
@@ -330,70 +334,93 @@ local function main()
if type == "boilerValve" then if type == "boilerValve" then
-- boiler multiblock -- boiler multiblock
if not validate_index(1, 2) then return false end
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.BOILER_VALVE rtu_type = RTU_UNIT_TYPE.BOILER_VALVE
rtu_iface, faulted = boilerv_rtu.new(device) rtu_iface, faulted = boilerv_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed")) println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed boiler multiblock")) log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed boiler multiblock"))
return false return false
end end
elseif type == "turbineValve" then elseif type == "turbineValve" then
-- turbine multiblock -- turbine multiblock
if not validate_index(1, 3) then return false end
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE
rtu_iface, faulted = turbinev_rtu.new(device) rtu_iface, faulted = turbinev_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed")) println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed turbine multiblock")) log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed turbine multiblock"))
return false return false
end end
elseif type == "dynamicValve" then elseif type == "dynamicValve" then
-- dynamic tank multiblock -- dynamic tank multiblock
if entry.unit == nil then
if not validate_index(1, 4) then return false end
if not validate_assign(true) then return false end
else
if not validate_index(1, 1) then return false end
if not validate_assign() then return false end
end
rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE
rtu_iface, faulted = dynamicv_rtu.new(device) rtu_iface, faulted = dynamicv_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed")) println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed dynamic tank multiblock")) log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed dynamic tank multiblock"))
return false return false
end end
elseif type == "inductionPort" then elseif type == "inductionPort" then
-- induction matrix multiblock -- induction matrix multiblock
if not validate_assign(true) then return false end
rtu_type = RTU_UNIT_TYPE.IMATRIX rtu_type = RTU_UNIT_TYPE.IMATRIX
rtu_iface, faulted = imatrix_rtu.new(device) rtu_iface, faulted = imatrix_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed")) println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed induction matrix multiblock")) log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed induction matrix multiblock"))
return false return false
end end
elseif type == "spsPort" then elseif type == "spsPort" then
-- SPS multiblock -- SPS multiblock
if not validate_assign(true) then return false end
rtu_type = RTU_UNIT_TYPE.SPS rtu_type = RTU_UNIT_TYPE.SPS
rtu_iface, faulted = sps_rtu.new(device) rtu_iface, faulted = sps_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed")) println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed SPS multiblock")) log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed SPS multiblock"))
return false return false
end end
elseif type == "solarNeutronActivator" then elseif type == "solarNeutronActivator" then
-- SNA -- SNA
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.SNA rtu_type = RTU_UNIT_TYPE.SNA
rtu_iface, faulted = sna_rtu.new(device) rtu_iface, faulted = sna_rtu.new(device)
elseif type == "environmentDetector" then elseif type == "environmentDetector" then
-- advanced peripherals environment detector -- advanced peripherals environment detector
if not validate_index(1) then return false end
if not validate_assign(entry.unit == nil) then return false end
rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
rtu_iface, faulted = envd_rtu.new(device) rtu_iface, faulted = envd_rtu.new(device)
elseif type == ppm.VIRTUAL_DEVICE_TYPE then elseif type == ppm.VIRTUAL_DEVICE_TYPE then
@@ -401,7 +428,7 @@ local function main()
rtu_type = RTU_UNIT_TYPE.VIRTUAL rtu_type = RTU_UNIT_TYPE.VIRTUAL
rtu_iface = rtu.init_unit().interface() rtu_iface = rtu.init_unit().interface()
else else
local message = util.c("configure> device '", name, "' is not a known type (", type, ")") local message = util.c("sys_config> device '", name, "' is not a known type (", type, ")")
println_ts(message) println_ts(message)
log.fatal(message) log.fatal(message)
return false return false
@@ -409,12 +436,12 @@ local function main()
if is_multiblock then if is_multiblock then
if not formed then if not formed then
log.info(util.c("configure> device '", name, "' is not formed")) log.info(util.c("sys_config> device '", name, "' is not formed"))
elseif faulted then elseif faulted then
-- sometimes there is a race condition on server boot where it reports formed, but -- sometimes there is a race condition on server boot where it reports formed, but
-- the other functions are not yet defined (that's the theory at least). mark as unformed to attempt connection later -- the other functions are not yet defined (that's the theory at least). mark as unformed to attempt connection later
formed = false formed = false
log.warning(util.c("configure> device '", name, "' is formed, but initialization had one or more faults: marked as unformed")) log.warning(util.c("sys_config> device '", name, "' is formed, but initialization had one or more faults: marked as unformed"))
end end
end end
@@ -423,7 +450,7 @@ local function main()
uid = 0, ---@type integer uid = 0, ---@type integer
name = name, ---@type string name = name, ---@type string
type = rtu_type, ---@type RTU_UNIT_TYPE type = rtu_type, ---@type RTU_UNIT_TYPE
index = index, ---@type integer index = index or false, ---@type integer|false
reactor = for_reactor, ---@type integer reactor = for_reactor, ---@type integer
device = device, ---@type table device = device, ---@type table
is_multiblock = is_multiblock, ---@type boolean is_multiblock = is_multiblock, ---@type boolean
@@ -444,7 +471,7 @@ local function main()
for_message = util.c("reactor ", for_reactor) for_message = util.c("reactor ", for_reactor)
end end
log.info(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ") [", index, "] for ", for_message)) log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ") [", index, "] for ", for_message))
rtu_unit.uid = #units rtu_unit.uid = #units
@@ -465,7 +492,6 @@ local function main()
databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state) databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state)
end end
-- we made it through all that trusting-user-to-write-a-config-file chaos
return true return true
end end
@@ -475,9 +501,9 @@ local function main()
local rtu_state = __shared_memory.rtu_state local rtu_state = __shared_memory.rtu_state
log.debug("boot> running configure()") log.debug("boot> running sys_config()")
if configure() then if sys_config() then
-- start UI -- start UI
local message local message
rtu_state.fp_ok, message = renderer.try_start_ui(units) rtu_state.fp_ok, message = renderer.try_start_ui(units)
@@ -502,12 +528,11 @@ local function main()
databus.tx_hw_spkr_count(#__shared_memory.rtu_dev.sounders) databus.tx_hw_spkr_count(#__shared_memory.rtu_dev.sounders)
-- start connection watchdog -- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT) smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("startup> conn watchdog started") log.debug("startup> conn watchdog started")
-- setup comms -- setup comms
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, config.RTU_CHANNEL, config.SVR_CHANNEL, smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
config.TRUSTED_RANGE, smem_sys.conn_watchdog)
log.debug("startup> comms init") log.debug("startup> comms init")
-- init threads -- init threads

View File

@@ -28,6 +28,147 @@ local UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local COMMS_SLEEP = 100 -- (100ms, 2 ticks) local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
---@param smem rtu_shared_memory
---@param println_ts function
---@param iface string
---@param type string
---@param device table
---@param unit rtu_unit_registry_entry
local function handle_unit_mount(smem, println_ts, iface, type, device, unit)
local sys = smem.rtu_sys
-- find disconnected device to reconnect
-- note: cannot check isFormed as that would yield this coroutine and consume events
if unit.name == iface then
local resend_advert, faulted, unknown, invalid = false, false, false, false
local function fail(msg)
invalid = true
log.error(msg .. " in config")
end
-- found, re-link
unit.device = device
if unit.type == RTU_UNIT_TYPE.VIRTUAL then
resend_advert = true
if type == "boilerValve" then
-- boiler multiblock
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("boiler '", unit.name, "' cannot init, not assigned to a valid unit")) end
if (unit.index == false) or unit.index < 1 or unit.index > 2 then fail(util.c("boiler '", unit.name, "' cannot init, invalid index provided")) end
unit.type = RTU_UNIT_TYPE.BOILER_VALVE
elseif type == "turbineValve" then
-- turbine multiblock
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("turbine '", unit.name, "' cannot init, not assigned to a valid unit")) end
if (unit.index == false) or unit.index < 1 or unit.index > 3 then fail(util.c("turbine '", unit.name, "' cannot init, invalid index provided")) end
unit.type = RTU_UNIT_TYPE.TURBINE_VALVE
elseif type == "dynamicValve" then
-- dynamic tank multiblock
if unit.reactor < 0 or unit.reactor > 4 then fail(util.c("dynamic tank '", unit.name, "' cannot init, no valid assignment provided")) end
if (unit.reactor == 0 and ((unit.index == false) or unit.index < 1 or unit.index > 4)) or
(unit.reactor > 0 and unit.index ~= 1) then
fail(util.c("dynamic tank '", unit.name, "' cannot init, invalid index provided"))
end
unit.type = RTU_UNIT_TYPE.DYNAMIC_VALVE
elseif type == "inductionPort" then
-- induction matrix multiblock
if unit.reactor ~= 0 then fail(util.c("induction matrix '", unit.name, "' cannot init, not assigned to facility")) end
unit.type = RTU_UNIT_TYPE.IMATRIX
elseif type == "spsPort" then
-- SPS multiblock
if unit.reactor ~= 0 then fail(util.c("SPS '", unit.name, "' cannot init, not assigned to facility")) end
unit.type = RTU_UNIT_TYPE.SPS
elseif type == "solarNeutronActivator" then
-- SNA
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("SNA '", unit.name, "' cannot init, not assigned to a valid unit")) end
unit.type = RTU_UNIT_TYPE.SNA
elseif type == "environmentDetector" then
-- advanced peripherals environment detector
if unit.reactor < 0 or unit.reactor > 4 then fail(util.c("environment detector '", unit.name, "' cannot init, no valid assignment provided")) end
if (unit.index == false) or unit.index < 1 then fail(util.c("environment detector '", unit.name, "' cannot init, invalid index provided")) end
unit.type = RTU_UNIT_TYPE.ENV_DETECTOR
else
resend_advert = false
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
end
databus.tx_unit_hw_type(unit.uid, unit.type)
end
-- if disconnected on startup, config wouldn't have been validated
-- checking now that it has connected; the config isn't valid, so don't connect it
if invalid then
unit.hw_state = UNIT_HW_STATE.OFFLINE
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
return
end
-- note for multiblock structures: if not formed, indexing the multiblock functions results in a PPM fault
if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
unit.rtu, faulted = boilerv_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
unit.rtu, faulted = turbinev_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
unit.rtu, faulted = dynamicv_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
unit.rtu, faulted = imatrix_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.SPS then
unit.rtu, faulted = sps_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.SNA then
unit.rtu, faulted = sna_rtu.new(device)
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
unit.rtu, faulted = envd_rtu.new(device)
else
unknown = true
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
end
if unit.is_multiblock then
unit.hw_state = UNIT_HW_STATE.UNFORMED
if unit.formed == false then
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
end
elseif faulted then
unit.hw_state = UNIT_HW_STATE.FAULTED
elseif not unknown then
unit.hw_state = UNIT_HW_STATE.OK
else
unit.hw_state = UNIT_HW_STATE.OFFLINE
end
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
if not unknown then
unit.modbus_io = modbus.new(unit.rtu, true)
local type_name = types.rtu_type_to_string(unit.type)
local message = util.c("reconnected the ", type_name, " on interface ", unit.name)
println_ts(message)
log.info(message)
if resend_advert then
sys.rtu_comms.send_advertisement(sys.units)
else
sys.rtu_comms.send_remounted(unit.uid)
end
end
end
end
-- main thread -- main thread
---@nodiscard ---@nodiscard
---@param smem rtu_shared_memory ---@param smem rtu_shared_memory
@@ -180,102 +321,7 @@ function threads.thread__main(smem)
else else
-- relink lost peripheral to correct unit entry -- relink lost peripheral to correct unit entry
for i = 1, #units do for i = 1, #units do
local unit = units[i] ---@type rtu_unit_registry_entry handle_unit_mount(smem, println_ts, param1, type, device, units[i])
-- find disconnected device to reconnect
-- note: cannot check isFormed as that would yield this coroutine and consume events
if unit.name == param1 then
local resend_advert = false
local faulted = false
local unknown = false
-- found, re-link
unit.device = device
if unit.type == RTU_UNIT_TYPE.VIRTUAL then
resend_advert = true
if type == "boilerValve" then
-- boiler multiblock
unit.type = RTU_UNIT_TYPE.BOILER_VALVE
elseif type == "turbineValve" then
-- turbine multiblock
unit.type = RTU_UNIT_TYPE.TURBINE_VALVE
elseif type == "inductionPort" then
-- induction matrix multiblock
unit.type = RTU_UNIT_TYPE.IMATRIX
elseif type == "spsPort" then
-- SPS multiblock
unit.type = RTU_UNIT_TYPE.SPS
elseif type == "solarNeutronActivator" then
-- SNA
unit.type = RTU_UNIT_TYPE.SNA
elseif type == "environmentDetector" then
-- advanced peripherals environment detector
unit.type = RTU_UNIT_TYPE.ENV_DETECTOR
else
resend_advert = false
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
end
databus.tx_unit_hw_type(unit.uid, unit.type)
end
-- note for multiblock structures: if not formed, indexing the multiblock functions results in a PPM fault
if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
unit.rtu, faulted = boilerv_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
unit.rtu, faulted = turbinev_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
unit.rtu, faulted = dynamicv_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
unit.rtu, faulted = imatrix_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.SPS then
unit.rtu, faulted = sps_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.SNA then
unit.rtu, faulted = sna_rtu.new(device)
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
unit.rtu, faulted = envd_rtu.new(device)
else
unknown = true
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
end
if unit.is_multiblock then
unit.hw_state = UNIT_HW_STATE.UNFORMED
if unit.formed == false then
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
end
elseif faulted then
unit.hw_state = UNIT_HW_STATE.FAULTED
elseif not unknown then
unit.hw_state = UNIT_HW_STATE.OK
else
unit.hw_state = UNIT_HW_STATE.OFFLINE
end
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
if not unknown then
unit.modbus_io = modbus.new(unit.rtu, true)
local type_name = types.rtu_type_to_string(unit.type)
local message = util.c("reconnected the ", type_name, " on interface ", unit.name)
println_ts(message)
log.info(message)
if resend_advert then
rtu_comms.send_advertisement(units)
else
rtu_comms.send_remounted(unit.uid)
end
end
end
end end
end end
end end

View File

@@ -16,8 +16,8 @@ local max_distance = nil
---@class comms ---@class comms
local comms = {} local comms = {}
-- protocol version (non-protocol changes tracked by util.lua version) -- protocol/data version (protocol/data independent changes tracked by util.lua version)
comms.version = "2.4.0" comms.version = "2.4.5"
---@enum PROTOCOL ---@enum PROTOCOL
local PROTOCOL = { local PROTOCOL = {
@@ -311,7 +311,7 @@ function comms.authd_packet()
self.valid = false self.valid = false
self.raw = self.modem_msg_in.msg self.raw = self.modem_msg_in.msg
if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then if (type(max_distance) == "number") and ((type(distance) ~= "number") or (distance > max_distance)) then
-- outside of maximum allowable transmission distance -- outside of maximum allowable transmission distance
-- log.debug("comms.authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)") -- log.debug("comms.authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
else else

View File

@@ -13,6 +13,7 @@ local MODE = { APPEND = 0, NEW = 1 }
log.MODE = MODE log.MODE = MODE
local logger = { local logger = {
not_ready = true,
path = "/log.txt", path = "/log.txt",
mode = MODE.APPEND, mode = MODE.APPEND,
debug = false, debug = false,
@@ -32,6 +33,8 @@ local free_space = fs.getFreeSpace
-- private log write function -- private log write function
---@param msg string ---@param msg string
local function _log(msg) local function _log(msg)
if logger.not_ready then return end
local out_of_space = false local out_of_space = false
local time_stamp = os.date("[%c] ") local time_stamp = os.date("[%c] ")
local stamped = time_stamp .. util.strval(msg) local stamped = time_stamp .. util.strval(msg)
@@ -94,6 +97,8 @@ function log.init(path, write_mode, include_debug, dmesg_redirect)
else else
logger.dmesg_out = term.current() logger.dmesg_out = term.current()
end end
logger.not_ready = false
end end
-- close the log file handle -- close the log file handle

View File

@@ -7,7 +7,7 @@ local log = require("scada-common.log")
local util = require("scada-common.util") local util = require("scada-common.util")
local md5 = require("lockbox.digest.md5") local md5 = require("lockbox.digest.md5")
local sha256 = require("lockbox.digest.sha2_256") local sha1 = require("lockbox.digest.sha1")
local pbkdf2 = require("lockbox.kdf.pbkdf2") local pbkdf2 = require("lockbox.kdf.pbkdf2")
local hmac = require("lockbox.mac.hmac") local hmac = require("lockbox.mac.hmac")
local stream = require("lockbox.util.stream") local stream = require("lockbox.util.stream")
@@ -31,12 +31,12 @@ function network.init_mac(passkey)
local key_deriv = pbkdf2() local key_deriv = pbkdf2()
-- setup PBKDF2 -- setup PBKDF2
key_deriv.setPassword(passkey) key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha1))
key_deriv.setBlockLen(20)
key_deriv.setDKeyLen(20)
key_deriv.setIterations(256)
key_deriv.setSalt("pepper") key_deriv.setSalt("pepper")
key_deriv.setIterations(32) key_deriv.setPassword(passkey)
key_deriv.setBlockLen(8)
key_deriv.setDKeyLen(16)
key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha256))
key_deriv.finish() key_deriv.finish()
c_eng.key = array.fromHex(key_deriv.asHex()) c_eng.key = array.fromHex(key_deriv.asHex())
@@ -53,6 +53,11 @@ function network.init_mac(passkey)
return init_time return init_time
end end
-- de-initialize message authentication system
function network.deinit_mac()
c_eng.key, c_eng.hmac = nil, nil
end
-- generate HMAC of message -- generate HMAC of message
---@nodiscard ---@nodiscard
---@param message string initial value concatenated with ciphertext ---@param message string initial value concatenated with ciphertext

View File

@@ -9,7 +9,7 @@ local util = require("scada-common.util")
local ppm = {} local ppm = {}
local ACCESS_FAULT = nil ---@type nil local ACCESS_FAULT = nil ---@type nil
local UNDEFINED_FIELD = "undefined field" local UNDEFINED_FIELD = "__PPM_UNDEF_FIELD__"
local VIRTUAL_DEVICE_TYPE = "ppm_vdev" local VIRTUAL_DEVICE_TYPE = "ppm_vdev"
ppm.ACCESS_FAULT = ACCESS_FAULT ppm.ACCESS_FAULT = ACCESS_FAULT
@@ -155,16 +155,16 @@ local function peri_init(iface)
self.fault_counts[key] = self.fault_counts[key] + 1 self.fault_counts[key] = self.fault_counts[key] + 1
return (function () return ACCESS_FAULT end) return (function () return UNDEFINED_FIELD end)
end end
} }
setmetatable(self.device, mt) setmetatable(self.device, mt)
return { ---@class ppm_entry
type = self.type, local entry = { type = self.type, dev = self.device }
dev = self.device
} return entry
end end
---------------------- ----------------------
@@ -300,6 +300,17 @@ function ppm.handle_unmount(iface)
return pm_type, pm_dev return pm_type, pm_dev
end end
-- log all mounts, to be used if `ppm.mount_all` is called before logging is ready
function ppm.log_mounts()
for iface, mount in pairs(ppm_sys.mounts) do
log.info(util.c("PPM: had found a ", mount.type, " (", iface, ")"))
end
if util.table_len(ppm_sys.mounts) == 0 then
log.warning("PPM: no devices had been found")
end
end
-- GENERAL ACCESSORS -- -- GENERAL ACCESSORS --
-- list all available peripherals -- list all available peripherals
@@ -310,7 +321,11 @@ function ppm.list_avail() return peripheral.getNames() end
-- list mounted peripherals -- list mounted peripherals
---@nodiscard ---@nodiscard
---@return table mounts ---@return table mounts
function ppm.list_mounts() return ppm_sys.mounts end function ppm.list_mounts()
local list = {}
for k, v in pairs(ppm_sys.mounts) do list[k] = v end
return list
end
-- get a mounted peripheral side/interface by device table -- get a mounted peripheral side/interface by device table
---@nodiscard ---@nodiscard
@@ -417,4 +432,15 @@ function ppm.get_monitor_list()
return list return list
end end
-- HELPER FUNCTIONS
-- get the block size of a monitor given its width and height <b>at a text scale of 0.5</b>
---@nodiscard
---@param width integer character width
---@param height integer character height
---@return integer block_width, integer block_height
function ppm.monitor_block_size(width, height)
return math.floor((width - 15) / 21) + 1, math.floor((height - 10) / 14) + 1
end
return ppm return ppm

View File

@@ -7,9 +7,7 @@ local util = require("scada-common.util")
---@class rsio ---@class rsio
local rsio = {} local rsio = {}
---------------------- --#region RS I/O Constants
-- RS I/O CONSTANTS --
----------------------
---@enum IO_LVL I/O logic level ---@enum IO_LVL I/O logic level
local IO_LVL = { local IO_LVL = {
@@ -53,75 +51,120 @@ local IO_PORT = {
-- facility -- facility
F_ALARM = 7, -- active high, facility-wide alarm (any high priority unit alarm) F_ALARM = 7, -- active high, facility-wide alarm (any high priority unit alarm)
F_ALARM_ANY = 8, -- active high, any alarm regardless of priority
-- waste -- waste
WASTE_PU = 8, -- active low, waste -> plutonium -> pellets route WASTE_PU = 9, -- active low, waste -> plutonium -> pellets route
WASTE_PO = 9, -- active low, waste -> polonium route WASTE_PO = 10, -- active low, waste -> polonium route
WASTE_POPL = 10, -- active low, polonium -> pellets route WASTE_POPL = 11, -- active low, polonium -> pellets route
WASTE_AM = 11, -- active low, polonium -> anti-matter route WASTE_AM = 12, -- active low, polonium -> anti-matter route
-- reactor -- reactor
R_ACTIVE = 12, -- active high, if the reactor is active R_ACTIVE = 13, -- active high, reactor is active
R_AUTO_CTRL = 13, -- active high, if the reactor burn rate is automatic R_AUTO_CTRL = 14, -- active high, reactor burn rate is automatic
R_SCRAMMED = 14, -- active high, if the reactor is scrammed R_SCRAMMED = 15, -- active high, reactor is scrammed
R_AUTO_SCRAM = 15, -- active high, if the reactor was automatically scrammed R_AUTO_SCRAM = 16, -- active high, reactor was automatically scrammed
R_HIGH_DMG = 16, -- active high, if the reactor damage is high R_HIGH_DMG = 17, -- active high, reactor damage is high
R_HIGH_TEMP = 17, -- active high, if the reactor is at a high temperature R_HIGH_TEMP = 18, -- active high, reactor is at a high temperature
R_LOW_COOLANT = 18, -- active high, if the reactor has very low coolant R_LOW_COOLANT = 19, -- active high, reactor has very low coolant
R_EXCESS_HC = 19, -- active high, if the reactor has excess heated coolant R_EXCESS_HC = 20, -- active high, reactor has excess heated coolant
R_EXCESS_WS = 20, -- active high, if the reactor has excess waste R_EXCESS_WS = 21, -- active high, reactor has excess waste
R_INSUFF_FUEL = 21, -- active high, if the reactor has insufficent fuel R_INSUFF_FUEL = 22, -- active high, reactor has insufficent fuel
R_PLC_FAULT = 22, -- active high, if the reactor PLC reports a device access fault R_PLC_FAULT = 23, -- active high, reactor PLC reports a device access fault
R_PLC_TIMEOUT = 23, -- active high, if the reactor PLC has not been heard from R_PLC_TIMEOUT = 24, -- active high, reactor PLC has not been heard from
-- unit outputs -- unit outputs
U_ALARM = 24, -- active high, unit alarm U_ALARM = 25, -- active high, unit alarm
U_EMER_COOL = 25 -- active low, emergency coolant control U_EMER_COOL = 26 -- active low, emergency coolant control
} }
rsio.IO_LVL = IO_LVL rsio.IO_LVL = IO_LVL
rsio.IO_DIR = IO_DIR rsio.IO_DIR = IO_DIR
rsio.IO_MODE = IO_MODE rsio.IO_MODE = IO_MODE
rsio.IO = IO_PORT rsio.IO = IO_PORT
rsio.NUM_PORTS = IO_PORT.U_EMER_COOL
----------------------- -- self checks
-- UTILITY FUNCTIONS --
----------------------- local dup_chk = {}
for _, v in pairs(IO_PORT) do
assert(dup_chk[v] ~= true, "duplicate in port list")
dup_chk[v] = true
end
assert(#dup_chk == rsio.NUM_PORTS, "port list malformed")
--#endregion
--#region Utility Functions
local PORT_NAMES = {
"F_SCRAM",
"F_ACK",
"R_SCRAM",
"R_RESET",
"R_ENABLE",
"U_ACK",
"F_ALARM",
"F_ALARM_ANY",
"WASTE_PU",
"WASTE_PO",
"WASTE_POPL",
"WASTE_AM",
"R_ACTIVE",
"R_AUTO_CTRL",
"R_SCRAMMED",
"R_AUTO_SCRAM",
"R_HIGH_DMG",
"R_HIGH_TEMP",
"R_LOW_COOLANT",
"R_EXCESS_HC",
"R_EXCESS_WS",
"R_INSUFF_FUEL",
"R_PLC_FAULT",
"R_PLC_TIMEOUT",
"U_ALARM",
"U_EMER_COOL"
}
local MODES = {
IO_MODE.DIGITAL_IN, -- F_SCRAM
IO_MODE.DIGITAL_IN, -- F_ACK
IO_MODE.DIGITAL_IN, -- R_SCRAM
IO_MODE.DIGITAL_IN, -- R_RESET
IO_MODE.DIGITAL_IN, -- R_ENABLE
IO_MODE.DIGITAL_IN, -- U_ACK
IO_MODE.DIGITAL_OUT, -- F_ALARM
IO_MODE.DIGITAL_OUT, -- F_ALARM_ANY
IO_MODE.DIGITAL_OUT, -- WASTE_PU
IO_MODE.DIGITAL_OUT, -- WASTE_PO
IO_MODE.DIGITAL_OUT, -- WASTE_POPL
IO_MODE.DIGITAL_OUT, -- WASTE_AM
IO_MODE.DIGITAL_OUT, -- R_ACTIVE
IO_MODE.DIGITAL_OUT, -- R_AUTO_CTRL
IO_MODE.DIGITAL_OUT, -- R_SCRAMMED
IO_MODE.DIGITAL_OUT, -- R_AUTO_SCRAM
IO_MODE.DIGITAL_OUT, -- R_HIGH_DMG
IO_MODE.DIGITAL_OUT, -- R_HIGH_TEMP
IO_MODE.DIGITAL_OUT, -- R_LOW_COOLANT
IO_MODE.DIGITAL_OUT, -- R_EXCESS_HC
IO_MODE.DIGITAL_OUT, -- R_EXCESS_WS
IO_MODE.DIGITAL_OUT, -- R_INSUFF_FUEL
IO_MODE.DIGITAL_OUT, -- R_PLC_FAULT
IO_MODE.DIGITAL_OUT, -- R_PLC_TIMEOUT
IO_MODE.DIGITAL_OUT, -- U_ALARM
IO_MODE.DIGITAL_OUT -- U_EMER_COOL
}
assert(rsio.NUM_PORTS == #PORT_NAMES, "port names length incorrect")
assert(rsio.NUM_PORTS == #MODES, "modes length incorrect")
-- port to string -- port to string
---@nodiscard ---@nodiscard
---@param port IO_PORT ---@param port IO_PORT
function rsio.to_string(port) function rsio.to_string(port)
local names = { if util.is_int(port) and port > 0 and port <= #PORT_NAMES then
"F_SCRAM", return PORT_NAMES[port]
"F_ACK",
"R_SCRAM",
"R_RESET",
"R_ENABLE",
"U_ACK",
"F_ALARM",
"WASTE_PU",
"WASTE_PO",
"WASTE_POPL",
"WASTE_AM",
"R_ACTIVE",
"R_AUTO_CTRL",
"R_SCRAMMED",
"R_AUTO_SCRAM",
"R_HIGH_DMG",
"R_HIGH_TEMP",
"R_LOW_COOLANT",
"R_EXCESS_HC",
"R_EXCESS_WS",
"R_INSUFF_FUEL",
"R_PLC_FAULT",
"R_PLC_TIMEOUT",
"U_ALARM",
"U_EMER_COOL"
}
if util.is_int(port) and port > 0 and port <= #names then
return names[port]
else else
return "UNKNOWN" return "UNKNOWN"
end end
@@ -153,6 +196,8 @@ local RS_DIO_MAP = {
-- F_ALARM -- F_ALARM
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- F_ALARM_ANY
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- WASTE_PU -- WASTE_PU
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
@@ -194,49 +239,29 @@ local RS_DIO_MAP = {
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT } { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }
} }
assert(rsio.NUM_PORTS == #RS_DIO_MAP, "RS_DIO_MAP length incorrect")
-- get the I/O direction of a port
---@nodiscard
---@param port IO_PORT
---@return IO_DIR
function rsio.get_io_dir(port)
if rsio.is_valid_port(port) then return RS_DIO_MAP[port].mode
else return IO_DIR.IN end
end
-- get the mode of a port -- get the mode of a port
---@nodiscard ---@nodiscard
---@param port IO_PORT ---@param port IO_PORT
---@return IO_MODE ---@return IO_MODE
function rsio.get_io_mode(port) function rsio.get_io_mode(port)
local modes = { if rsio.is_valid_port(port) then return MODES[port]
IO_MODE.DIGITAL_IN, -- F_SCRAM else return IO_MODE.ANALOG_IN end
IO_MODE.DIGITAL_IN, -- F_ACK
IO_MODE.DIGITAL_IN, -- R_SCRAM
IO_MODE.DIGITAL_IN, -- R_RESET
IO_MODE.DIGITAL_IN, -- R_ENABLE
IO_MODE.DIGITAL_IN, -- U_ACK
IO_MODE.DIGITAL_OUT, -- F_ALARM
IO_MODE.DIGITAL_OUT, -- WASTE_PU
IO_MODE.DIGITAL_OUT, -- WASTE_PO
IO_MODE.DIGITAL_OUT, -- WASTE_POPL
IO_MODE.DIGITAL_OUT, -- WASTE_AM
IO_MODE.DIGITAL_OUT, -- R_ACTIVE
IO_MODE.DIGITAL_OUT, -- R_AUTO_CTRL
IO_MODE.DIGITAL_OUT, -- R_SCRAMMED
IO_MODE.DIGITAL_OUT, -- R_AUTO_SCRAM
IO_MODE.DIGITAL_OUT, -- R_HIGH_DMG
IO_MODE.DIGITAL_OUT, -- R_HIGH_TEMP
IO_MODE.DIGITAL_OUT, -- R_LOW_COOLANT
IO_MODE.DIGITAL_OUT, -- R_EXCESS_HC
IO_MODE.DIGITAL_OUT, -- R_EXCESS_WS
IO_MODE.DIGITAL_OUT, -- R_INSUFF_FUEL
IO_MODE.DIGITAL_OUT, -- R_PLC_FAULT
IO_MODE.DIGITAL_OUT, -- R_PLC_TIMEOUT
IO_MODE.DIGITAL_OUT, -- U_ALARM
IO_MODE.DIGITAL_OUT -- U_EMER_COOL
}
if util.is_int(port) and port > 0 and port <= #modes then
return modes[port]
else
return IO_MODE.ANALOG_IN
end
end end
-------------------- --#endregion
-- GENERIC CHECKS --
-------------------- --#region Generic Checks
local RS_SIDES = rs.getSides() local RS_SIDES = rs.getSides()
@@ -245,7 +270,7 @@ local RS_SIDES = rs.getSides()
---@param port IO_PORT ---@param port IO_PORT
---@return boolean valid ---@return boolean valid
function rsio.is_valid_port(port) function rsio.is_valid_port(port)
return util.is_int(port) and (port > 0) and (port <= IO_PORT.U_EMER_COOL) return util.is_int(port) and port > 0 and port <= rsio.NUM_PORTS
end end
-- check if a side is valid -- check if a side is valid
@@ -263,15 +288,27 @@ end
-- check if a color is a valid single color -- check if a color is a valid single color
---@nodiscard ---@nodiscard
---@param color integer ---@param color any
---@return boolean valid ---@return boolean valid
function rsio.is_color(color) function rsio.is_color(color)
return util.is_int(color) and (color > 0) and (_B_AND(color, (color - 1)) == 0) return util.is_int(color) and (color > 0) and (_B_AND(color, (color - 1)) == 0)
end end
----------------- -- color to string
-- DIGITAL I/O -- ---@nodiscard
----------------- ---@param color color
---@return string
function rsio.color_name(color)
local color_name_map = { [colors.red] = "red", [colors.orange] = "orange", [colors.yellow] = "yellow", [colors.lime] = "lime", [colors.green] = "green", [colors.cyan] = "cyan", [colors.lightBlue] = "lightBlue", [colors.blue] = "blue", [colors.purple] = "purple", [colors.magenta] = "magenta", [colors.pink] = "pink", [colors.white] = "white", [colors.lightGray] = "lightGray", [colors.gray] = "gray", [colors.black] = "black", [colors.brown] = "brown" }
if rsio.is_color(color) then
return color_name_map[color]
else return "unknown" end
end
--#endregion
--#region Digital I/O
-- get digital I/O level reading from a redstone boolean input value -- get digital I/O level reading from a redstone boolean input value
---@nodiscard ---@nodiscard
@@ -285,9 +322,7 @@ end
---@nodiscard ---@nodiscard
---@param level IO_LVL logic level ---@param level IO_LVL logic level
---@return boolean ---@return boolean
function rsio.digital_write(level) function rsio.digital_write(level) return level == IO_LVL.HIGH end
return level == IO_LVL.HIGH
end
-- returns the level corresponding to active -- returns the level corresponding to active
---@nodiscard ---@nodiscard
@@ -317,9 +352,9 @@ function rsio.digital_is_active(port, level)
end end
end end
---------------- --#endregion
-- ANALOG I/O --
---------------- --#region Analog I/O
-- read an analog value scaled from min to max -- read an analog value scaled from min to max
---@nodiscard ---@nodiscard
@@ -343,4 +378,6 @@ function rsio.analog_write(value, min, max)
return math.floor(scaled_value * 15) return math.floor(scaled_value * 15)
end end
--#endregion
return rsio return rsio

View File

@@ -63,7 +63,7 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
---@class rtu_advertisement ---@class rtu_advertisement
---@field type RTU_UNIT_TYPE ---@field type RTU_UNIT_TYPE
---@field index integer ---@field index integer|false
---@field reactor integer ---@field reactor integer
---@field rsio table|nil ---@field rsio table|nil
@@ -252,6 +252,14 @@ types.ALARM_STATE_NAMES = {
-- STRING TYPES -- -- STRING TYPES --
--#region --#region
---@alias side
---|"top"
---|"bottom"
---|"left"
---|"right"
---|"front"
---|"back"
---@alias os_event ---@alias os_event
---| "alarm" ---| "alarm"
---| "char" ---| "char"

View File

@@ -4,19 +4,30 @@
local cc_strings = require("cc.strings") local cc_strings = require("cc.strings")
local math = math
local string = string
local table = table
local os = os
local getmetatable = getmetatable
local print = print
local tostring = tostring
local type = type
local t_concat = table.concat
local t_insert = table.insert
local t_pack = table.pack
---@class util ---@class util
local util = {} local util = {}
-- scada-common version -- scada-common version
util.version = "1.1.2" util.version = "1.1.18"
-- ENVIRONMENT CONSTANTS --
util.TICK_TIME_S = 0.05 util.TICK_TIME_S = 0.05
util.TICK_TIME_MS = 50 util.TICK_TIME_MS = 50
-- OPERATORS -- --#region OPERATORS
--#region
-- trinary operator -- trinary operator
---@nodiscard ---@nodiscard
@@ -30,37 +41,29 @@ end
--#endregion --#endregion
-- PRINT -- --#region PRINT
--#region
local p_time = "[%H:%M:%S] "
-- print -- print
---@param message any ---@param message any
function util.print(message) function util.print(message) term.write(tostring(message)) end
term.write(tostring(message))
end
-- print line -- print line
---@param message any ---@param message any
function util.println(message) function util.println(message) print(tostring(message)) end
print(tostring(message))
end
-- timestamped print -- timestamped print
---@param message any ---@param message any
function util.print_ts(message) function util.print_ts(message) term.write(os.date(p_time) .. tostring(message)) end
term.write(os.date("[%H:%M:%S] ") .. tostring(message))
end
-- timestamped print line -- timestamped print line
---@param message any ---@param message any
function util.println_ts(message) function util.println_ts(message) print(os.date(p_time) .. tostring(message)) end
print(os.date("[%H:%M:%S] ") .. tostring(message))
end
--#endregion --#endregion
-- STRING TOOLS -- --#region STRING TOOLS
--#region
-- get a value as a string -- get a value as a string
---@nodiscard ---@nodiscard
@@ -68,21 +71,29 @@ end
---@return string ---@return string
function util.strval(val) function util.strval(val)
local t = type(val) local t = type(val)
if t == "string" then return val end
-- this depends on Lua short-circuiting the or check for metatables (note: metatables won't have metatables) -- this depends on Lua short-circuiting the or check for metatables (note: metatables won't have metatables)
if (t == "table" and (getmetatable(val) == nil or getmetatable(val).__tostring == nil)) or t == "function" then if (t == "table" and (getmetatable(val) == nil or getmetatable(val).__tostring == nil)) or t == "function" then
return "[" .. tostring(val) .. "]" return t_concat{"[", tostring(val), "]"}
else else return tostring(val) end
return tostring(val) end
end
-- tokenize a string by a separator<br>
-- does not behave exactly like C's strtok
---@param str string string to tokenize
---@param sep string separator to tokenize by
---@return table token_list
function util.strtok(str, sep)
local list = {}
for part in string.gmatch(str, "([^" .. sep .. "]+)") do t_insert(list, part) end
return list
end end
-- repeat a space n times -- repeat a space n times
---@nodiscard ---@nodiscard
---@param n integer ---@param n integer
---@return string ---@return string
function util.spaces(n) function util.spaces(n) return string.rep(" ", n) end
return string.rep(" ", n)
end
-- pad text to a minimum width -- pad text to a minimum width
---@nodiscard ---@nodiscard
@@ -94,7 +105,7 @@ function util.pad(str, n)
local lpad = math.floor((n - len) / 2) local lpad = math.floor((n - len) / 2)
local rpad = (n - len) - lpad local rpad = (n - len) - lpad
return util.spaces(lpad) .. str .. util.spaces(rpad) return t_concat{util.spaces(lpad), str, util.spaces(rpad)}
end end
-- wrap a string into a table of lines -- wrap a string into a table of lines
@@ -104,19 +115,14 @@ end
---@return table lines ---@return table lines
function util.strwrap(str, limit) return cc_strings.wrap(str, limit) end function util.strwrap(str, limit) return cc_strings.wrap(str, limit) end
-- luacheck: no unused args
-- concatenation with built-in to string -- concatenation with built-in to string
---@nodiscard ---@nodiscard
---@vararg any ---@vararg any
---@return string ---@return string
---@diagnostic disable-next-line: unused-vararg
function util.concat(...) function util.concat(...)
local str = "" local args, strings = t_pack(...), {}
for i = 1, args.n do strings[i] = util.strval(args[i]) end
for _, v in ipairs(arg) do str = str .. util.strval(v) end return t_concat(strings)
return str
end end
-- alias -- alias
@@ -126,12 +132,7 @@ util.c = util.concat
---@nodiscard ---@nodiscard
---@param format string ---@param format string
---@vararg any ---@vararg any
---@diagnostic disable-next-line: unused-vararg function util.sprintf(format, ...) return string.format(format, ...) end
function util.sprintf(format, ...)
return string.format(format, table.unpack(arg))
end
-- luacheck: unused args
-- format a number string with commas as the thousands separator<br> -- format a number string with commas as the thousands separator<br>
-- subtracts from spaces at the start if present for each comma used -- subtracts from spaces at the start if present for each comma used
@@ -144,7 +145,7 @@ function util.comma_format(num)
local i = 1 local i = 1
while i > 0 do while i > 0 do
formatted, i = formatted:gsub("^(%s-%d+)(%d%d%d)", '%1,%2') formatted, i = formatted:gsub("^(%s-%d+)(%d%d%d)", "%1,%2")
if i > 0 then commas = commas + 1 end if i > 0 then commas = commas + 1 end
end end
@@ -158,31 +159,24 @@ end
--#endregion --#endregion
-- MATH -- --#region MATH
--#region
-- is a value an integer -- is a value an integer
---@nodiscard ---@nodiscard
---@param x any value ---@param x any value
---@return boolean is_integer if the number is an integer ---@return boolean is_integer
function util.is_int(x) function util.is_int(x) return type(x) == "number" and x == math.floor(x) end
return type(x) == "number" and x == math.floor(x)
end
-- get the sign of a number -- get the sign of a number
---@nodiscard ---@nodiscard
---@param x number value ---@param x number value
---@return integer sign (-1 for < 0, 1 otherwise) ---@return integer sign (-1 for < 0, 1 otherwise)
function util.sign(x) function util.sign(x) return util.trinary(x < 0, -1, 1) end
return util.trinary(x < 0, -1, 1)
end
-- round a number to an integer -- round a number to an integer
---@nodiscard ---@nodiscard
---@return integer rounded ---@return integer rounded
function util.round(x) function util.round(x) return math.floor(x + 0.5) end
return math.floor(x + 0.5)
end
-- get a new moving average object -- get a new moving average object
---@nodiscard ---@nodiscard
@@ -191,7 +185,7 @@ end
function util.mov_avg(length, default) function util.mov_avg(length, default)
local data = {} local data = {}
local index = 1 local index = 1
local last_t = 0 ---@type number|nil local last_t = 0 ---@type number|nil
---@class moving_average ---@class moving_average
local public = {} local public = {}
@@ -200,16 +194,14 @@ function util.mov_avg(length, default)
---@param x number value ---@param x number value
function public.reset(x) function public.reset(x)
data = {} data = {}
for _ = 1, length do table.insert(data, x) end for _ = 1, length do t_insert(data, x) end
end end
-- record a new value -- record a new value
---@param x number new value ---@param x number new value
---@param t number? optional last update time to prevent duplicated entries ---@param t number? optional last update time to prevent duplicated entries
function public.record(x, t) function public.record(x, t)
if type(t) == "number" and last_t == t then if type(t) == "number" and last_t == t then return end
return
end
data[index] = x data[index] = x
last_t = t last_t = t
@@ -232,23 +224,21 @@ function util.mov_avg(length, default)
return public return public
end end
-- TIME -- --#endregion
--#region TIME
-- current time -- current time
---@nodiscard ---@nodiscard
---@return integer milliseconds ---@return integer milliseconds
function util.time_ms()
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
return os.epoch('local') function util.time_ms() return os.epoch("local") end
end
-- current time -- current time
---@nodiscard ---@nodiscard
---@return number seconds ---@return number seconds
function util.time_s()
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
return os.epoch('local') / 1000.0 function util.time_s() return os.epoch("local") / 1000.0 end
end
-- current time -- current time
---@nodiscard ---@nodiscard
@@ -257,17 +247,14 @@ function util.time() return util.time_ms() end
--#endregion --#endregion
-- OS -- --#region OS
--#region
-- OS pull event raw wrapper with types -- OS pull event raw wrapper with types
---@nodiscard ---@nodiscard
---@param target_event? string event to wait for ---@param target_event? string event to wait for
---@return os_event event, any param1, any param2, any param3, any param4, any param5 ---@return os_event event, any param1, any param2, any param3, any param4, any param5
function util.pull_event(target_event)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
return os.pullEventRaw(target_event) function util.pull_event(target_event) return os.pullEventRaw(target_event) end
end
-- OS queue event raw wrapper with types -- OS queue event raw wrapper with types
---@param event os_event ---@param event os_event
@@ -285,30 +272,25 @@ end
---@nodiscard ---@nodiscard
---@param t number timer duration in seconds ---@param t number timer duration in seconds
---@return integer timer ID ---@return integer timer ID
function util.start_timer(t)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
return os.startTimer(t) function util.start_timer(t) return os.startTimer(t) end
end
-- cancel an OS timer -- cancel an OS timer
---@param timer integer timer ID ---@param timer integer timer ID
function util.cancel_timer(timer)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
os.cancelTimer(timer) function util.cancel_timer(timer) os.cancelTimer(timer) end
end
--#endregion --#endregion
-- PARALLELIZATION -- --#region PARALLELIZATION
--#region
-- protected sleep call so we still are in charge of catching termination -- protected sleep call so we still are in charge of catching termination<br>
---@param t integer seconds -- returns the result of pcall
---@param t number seconds
---@return boolean success, any result, any ...
--- EVENT_CONSUMER: this function consumes events --- EVENT_CONSUMER: this function consumes events
function util.psleep(t)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
pcall(os.sleep, t) function util.psleep(t) return pcall(os.sleep, t) end
end
-- no-op to provide a brief pause (1 tick) to yield<br> -- no-op to provide a brief pause (1 tick) to yield<br>
--- EVENT_CONSUMER: this function consumes events --- EVENT_CONSUMER: this function consumes events
@@ -330,8 +312,7 @@ end
--#endregion --#endregion
-- TABLE UTILITIES -- --#region TABLE UTILITIES
--#region
-- delete elements from a table if the passed function returns false when passed a table element<br> -- delete elements from a table if the passed function returns false when passed a table element<br>
-- put briefly: deletes elements that return false, keeps elements that return true -- put briefly: deletes elements that return false, keeps elements that return true
@@ -369,10 +350,19 @@ function util.table_contains(t, element)
return false return false
end end
-- count the length of a table, even if the values are not sequential or contain named keys
---@nodiscard
---@param t table
---@return integer length
function util.table_len(t)
local n = 0
for _, _ in pairs(t) do n = n + 1 end
return n
end
--#endregion --#endregion
-- MEKANISM POWER -- --#region MEKANISM POWER
--#region
-- convert Joules to FE -- convert Joules to FE
---@nodiscard ---@nodiscard
@@ -401,8 +391,7 @@ local function ZFE(fe) return fe / 1000000000000000000000.0 end -- how & why did
---@param format? string format override ---@param format? string format override
---@return string str, string? unit ---@return string str, string? unit
function util.power_format(fe, combine_label, format) function util.power_format(fe, combine_label, format)
local unit local unit, value
local value
if type(format) ~= "string" then format = "%.2f" end if type(format) ~= "string" then format = "%.2f" end
@@ -441,8 +430,7 @@ end
--#endregion --#endregion
-- UTILITY CLASSES -- --#region UTILITY CLASSES
--#region
-- WATCHDOG -- -- WATCHDOG --
@@ -451,32 +439,25 @@ end
---@nodiscard ---@nodiscard
---@param timeout number timeout duration ---@param timeout number timeout duration
function util.new_watchdog(timeout) function util.new_watchdog(timeout)
local self = { local self = { timeout = timeout, wd_timer = util.start_timer(timeout) }
timeout = timeout,
wd_timer = util.start_timer(timeout)
}
---@class watchdog ---@class watchdog
local public = {} local public = {}
-- check if a timer is this watchdog -- check if a timer is this watchdog
---@nodiscard ---@nodiscard
---@param timer number timer event timer ID ---@param timer number event timer ID
function public.is_timer(timer) return self.wd_timer == timer end function public.is_timer(timer) return self.wd_timer == timer end
-- satiate the beast -- satiate the beast
function public.feed() function public.feed()
if self.wd_timer ~= nil then public.cancel()
util.cancel_timer(self.wd_timer)
end
self.wd_timer = util.start_timer(self.timeout) self.wd_timer = util.start_timer(self.timeout)
end end
-- cancel the watchdog -- cancel the watchdog
function public.cancel() function public.cancel()
if self.wd_timer ~= nil then if self.wd_timer ~= nil then util.cancel_timer(self.wd_timer) end
util.cancel_timer(self.wd_timer)
end
end end
return public return public
@@ -489,10 +470,7 @@ end
---@nodiscard ---@nodiscard
---@param period number clock period ---@param period number clock period
function util.new_clock(period) function util.new_clock(period)
local self = { local self = { period = period, timer = nil }
period = period,
timer = nil
}
---@class clock ---@class clock
local public = {} local public = {}
@@ -525,6 +503,7 @@ function util.new_validator()
function public.assert_type_str(value) valid = valid and type(value) == "string" end function public.assert_type_str(value) valid = valid and type(value) == "string" end
function public.assert_type_table(value) valid = valid and type(value) == "table" end function public.assert_type_table(value) valid = valid and type(value) == "table" end
function public.assert(check) valid = valid and (check == true) end
function public.assert_eq(check, expect) valid = valid and check == expect end function public.assert_eq(check, expect) valid = valid and check == expect end
function public.assert_min(check, min) valid = valid and check >= min end function public.assert_min(check, min) valid = valid and check >= min end
function public.assert_min_ex(check, min) valid = valid and check > min end function public.assert_min_ex(check, min) valid = valid and check > min end
@@ -533,7 +512,7 @@ function util.new_validator()
function public.assert_range(check, min, max) valid = valid and check >= min and check <= max end function public.assert_range(check, min, max) valid = valid and check >= min and check <= max end
function public.assert_range_ex(check, min, max) valid = valid and check > min and check < max end function public.assert_range_ex(check, min, max) valid = valid and check > min and check < max end
function public.assert_channel(channel) valid = valid and type(channel) == "number" and channel >= 0 and channel <= 65535 end function public.assert_channel(channel) valid = valid and util.is_int(channel) and channel >= 0 and channel <= 65535 end
-- check if all assertions passed successfully -- check if all assertions passed successfully
---@nodiscard ---@nodiscard

View File

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

View File

@@ -1,56 +0,0 @@
local config = {}
-- supervisor comms channel
config.SVR_CHANNEL = 16240
-- PLC comms channel
config.PLC_CHANNEL = 16241
-- RTU/MODBUS comms channel
config.RTU_CHANNEL = 16242
-- coordinator comms channel
config.CRD_CHANNEL = 16243
-- pocket comms channel
config.PKT_CHANNEL = 16244
-- max trusted modem message distance
-- (0 to disable check)
config.TRUSTED_RANGE = 0
-- time in seconds (>= 2) before assuming a remote
-- device is no longer active
config.PLC_TIMEOUT = 5
config.RTU_TIMEOUT = 5
config.CRD_TIMEOUT = 5
config.PKT_TIMEOUT = 5
-- facility authentication key
-- (do NOT use one of your passwords)
-- this enables verifying that messages are authentic
-- all devices on this network must use this key
-- config.AUTH_KEY = "SCADAfacility123"
-- expected number of reactors
config.NUM_REACTORS = 4
-- expected number of devices for each unit
config.REACTOR_COOLING = {
-- reactor unit 1
{ BOILERS = 1, TURBINES = 1, TANK = false },
-- reactor unit 2
{ BOILERS = 1, TURBINES = 1, TANK = false },
-- reactor unit 3
{ BOILERS = 1, TURBINES = 1, TANK = false },
-- reactor unit 4
{ BOILERS = 1, TURBINES = 1, TANK = false }
}
-- advanced facility dynamic tank configuration
-- (see wiki for details)
-- by default, dynamic tanks are for each unit
config.FAC_TANK_MODE = 0
config.FAC_TANK_DEFS = { 0, 0, 0, 0 }
-- log path
config.LOG_PATH = "/log.txt"
-- log mode
-- 0 = APPEND (adds to existing file on start)
-- 1 = NEW (replaces existing file on start)
config.LOG_MODE = 0
-- true to log verbose debug messages
config.LOG_DEBUG = false
return config

1096
supervisor/configure.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -135,7 +135,7 @@ function facility.new(num_reactors, cooling_conf)
-- create units -- create units
for i = 1, num_reactors do for i = 1, num_reactors do
table.insert(self.units, unit.new(i, cooling_conf.r_cool[i].BOILERS, cooling_conf.r_cool[i].TURBINES)) table.insert(self.units, unit.new(i, cooling_conf.r_cool[i].BoilerCount, cooling_conf.r_cool[i].TurbineCount))
table.insert(self.group_map, 0) table.insert(self.group_map, 0)
end end
@@ -152,6 +152,8 @@ function facility.new(num_reactors, cooling_conf)
table.insert(self.test_tone_states, false) table.insert(self.test_tone_states, false)
end end
-- PRIVATE FUNCTIONS --
-- check if all auto-controlled units completed ramping -- check if all auto-controlled units completed ramping
---@nodiscard ---@nodiscard
local function _all_units_ramped() local function _all_units_ramped()
@@ -228,13 +230,11 @@ function facility.new(num_reactors, cooling_conf)
---@class facility ---@class facility
local public = {} local public = {}
-- ADD/LINK DEVICES -- --#region Add/Link Devices
-- link a redstone RTU session -- link a redstone RTU session
---@param rs_unit unit_session ---@param rs_unit unit_session
function public.add_redstone(rs_unit) function public.add_redstone(rs_unit) table.insert(self.redstone, rs_unit) end
table.insert(self.redstone, rs_unit)
end
-- link an induction matrix RTU session -- link an induction matrix RTU session
---@param imatrix unit_session ---@param imatrix unit_session
@@ -258,23 +258,11 @@ function facility.new(num_reactors, cooling_conf)
-- link a dynamic tank RTU session -- link a dynamic tank RTU session
---@param dynamic_tank unit_session ---@param dynamic_tank unit_session
---@return boolean linked dynamic tank accepted (max 1) function public.add_tank(dynamic_tank) table.insert(self.tanks, dynamic_tank) end
function public.add_tank(dynamic_tank)
if #self.tanks == 0 then
table.insert(self.tanks, dynamic_tank)
return true
else return false end
end
-- link an environment detector RTU session -- link an environment detector RTU session
---@param envd unit_session ---@param envd unit_session
---@return boolean linked environment detector accepted (max 1) function public.add_envd(envd) table.insert(self.envd, envd) end
function public.add_envd(envd)
if #self.envd == 0 then
table.insert(self.envd, envd)
return true
else return false end
end
-- purge devices associated with the given RTU session ID -- purge devices associated with the given RTU session ID
---@param session integer RTU session ID ---@param session integer RTU session ID
@@ -282,11 +270,9 @@ function facility.new(num_reactors, cooling_conf)
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (s) return s.get_session_id() ~= session end) end for _, v in pairs(self.rtu_list) do util.filter_table(v, function (s) return s.get_session_id() ~= session end) end
end end
-- UPDATE -- --#endregion
-- supervisor sessions reporting the list of active RTU sessions --#region Update
---@param rtu_sessions table session list of all connected RTUs
function public.report_rtus(rtu_sessions) self.rtu_conn_count = #rtu_sessions end
-- update (iterate) the facility management -- update (iterate) the facility management
function public.update() function public.update()
@@ -337,7 +323,7 @@ function facility.new(num_reactors, cooling_conf)
-- Run Process Control -- -- Run Process Control --
------------------------- -------------------------
--#region Process Control --#region
local avg_charge = self.avg_charge.compute() local avg_charge = self.avg_charge.compute()
local avg_inflow = self.avg_inflow.compute() local avg_inflow = self.avg_inflow.compute()
@@ -351,7 +337,7 @@ function facility.new(num_reactors, cooling_conf)
if state_changed then if state_changed then
self.saturated = false self.saturated = false
log.debug("FAC: state changed from " .. PROCESS_NAMES[self.last_mode + 1] .. " to " .. PROCESS_NAMES[self.mode + 1]) 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 if (self.last_mode == PROCESS.INACTIVE) or (self.last_mode == PROCESS.GEN_RATE_FAULT_IDLE) then
self.start_fail = START_STATUS.OK self.start_fail = START_STATUS.OK
@@ -389,6 +375,8 @@ function facility.new(num_reactors, cooling_conf)
end end
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 if blade_count == nil then
-- no units -- no units
log.warning("FAC: cannot start process control with 0 units assigned") log.warning("FAC: cannot start process control with 0 units assigned")
@@ -450,7 +438,7 @@ function facility.new(num_reactors, cooling_conf)
self.saturated = true self.saturated = true
self.status_text = { "MONITORED MODE", "running reactors at limit" } self.status_text = { "MONITORED MODE", "running reactors at limit" }
log.info(util.c("FAC: MAX_BURN process mode started")) log.info("FAC: MAX_BURN process mode started")
end end
_allocate_burn_rate(self.max_burn_combined, true) _allocate_burn_rate(self.max_burn_combined, true)
@@ -459,7 +447,7 @@ function facility.new(num_reactors, cooling_conf)
if state_changed then if state_changed then
self.time_start = now self.time_start = now
self.status_text = { "BURN RATE MODE", "running" } self.status_text = { "BURN RATE MODE", "running" }
log.info(util.c("FAC: BURN_RATE process mode started")) log.info("FAC: BURN_RATE process mode started")
end end
local unallocated = _allocate_burn_rate(self.burn_target, true) local unallocated = _allocate_burn_rate(self.burn_target, true)
@@ -473,7 +461,7 @@ function facility.new(num_reactors, cooling_conf)
self.accumulator = 0 self.accumulator = 0
self.status_text = { "CHARGE MODE", "running control loop" } self.status_text = { "CHARGE MODE", "running control loop" }
log.info(util.c("FAC: CHARGE mode starting PID control")) log.info("FAC: CHARGE mode starting PID control")
elseif self.last_update ~= charge_update then elseif self.last_update ~= charge_update then
-- convert to kFE to make constants not microscopic -- convert to kFE to make constants not microscopic
local error = util.round((self.charge_setpoint - avg_charge) / 1000) / 1000 local error = util.round((self.charge_setpoint - avg_charge) / 1000) / 1000
@@ -609,7 +597,7 @@ function facility.new(num_reactors, cooling_conf)
-- Evaluate Automatic SCRAM -- -- Evaluate Automatic SCRAM --
------------------------------ ------------------------------
--#region Automatic SCRAM --#region
local astatus = self.ascram_status local astatus = self.ascram_status
@@ -628,7 +616,7 @@ function facility.new(num_reactors, cooling_conf)
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) 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 if was_fill and not astatus.matrix_fill then
log.info("FAC: charge state of induction matrix entered acceptable range <= " .. (ALARM_LIMS.CHARGE_RE_ENABLE * 100) .. "%") log.info(util.c("FAC: charge state of induction matrix entered acceptable range <= ", ALARM_LIMS.CHARGE_RE_ENABLE * 100, "%"))
end end
-- check for critical unit alarms -- check for critical unit alarms
@@ -643,11 +631,16 @@ function facility.new(num_reactors, cooling_conf)
end end
-- check for facility radiation -- check for facility radiation
if self.envd[1] ~= nil then if #self.envd > 0 then
local envd = self.envd[1] ---@type unit_session local max_rad = 0
local e_db = envd.get_db() ---@type envd_session_db
astatus.radiation = e_db.radiation_raw > ALARM_LIMS.FAC_HIGH_RAD 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 else
-- don't clear, if it is true then we lost it with high radiation, so just keep alarming -- 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 -- operator can restart the system or hit the stop/reset button
@@ -734,6 +727,8 @@ function facility.new(num_reactors, cooling_conf)
-- Handle Redstone I/O -- -- Handle Redstone I/O --
------------------------- -------------------------
--#region
if #self.redstone > 0 then if #self.redstone > 0 then
-- handle facility SCRAM -- handle facility SCRAM
if self.io_ctl.digital_read(IO.F_SCRAM) then if self.io_ctl.digital_read(IO.F_SCRAM) then
@@ -746,24 +741,31 @@ function facility.new(num_reactors, cooling_conf)
-- handle facility ack -- handle facility ack
if self.io_ctl.digital_read(IO.F_ACK) then public.ack_all() end if self.io_ctl.digital_read(IO.F_ACK) then public.ack_all() end
-- update facility alarm output (check if emergency+ alarms are active) -- update facility alarm outputs
local has_alarm = false local has_prio_alarm, has_any_alarm = false, false
for i = 1, #self.units do for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit local u = self.units[i] ---@type reactor_unit
if u.has_alarm_min_prio(PRIO.EMERGENCY) then if u.has_alarm_min_prio(PRIO.EMERGENCY) then
has_alarm = true has_prio_alarm, has_any_alarm = true, true
break break
elseif u.has_alarm_min_prio(PRIO.TIMELY) then
has_any_alarm = true
end end
end end
self.io_ctl.digital_write(IO.F_ALARM, has_alarm) self.io_ctl.digital_write(IO.F_ALARM, has_prio_alarm)
self.io_ctl.digital_write(IO.F_ALARM_ANY, has_any_alarm)
end end
--#endregion
---------------- ----------------
-- Unit Tasks -- -- Unit Tasks --
---------------- ----------------
--#region
local insufficent_po_rate = false local insufficent_po_rate = false
local need_emcool = false local need_emcool = false
@@ -802,10 +804,14 @@ function facility.new(num_reactors, cooling_conf)
end end
end end
--#endregion
------------------------ ------------------------
-- Update Alarm Tones -- -- Update Alarm Tones --
------------------------ ------------------------
--#region
local allow_test = self.allow_testing and self.test_tone_set local allow_test = self.allow_testing and self.test_tone_set
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false } local alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
@@ -892,6 +898,8 @@ function facility.new(num_reactors, cooling_conf)
self.test_tone_set = false self.test_tone_set = false
self.test_tone_reset = true self.test_tone_reset = true
end end
--#endregion
end end
-- call the update function of all units in the facility<br> -- call the update function of all units in the facility<br>
@@ -904,7 +912,9 @@ function facility.new(num_reactors, cooling_conf)
end end
end end
-- COMMANDS -- --#endregion
--#region Commands
-- SCRAM all reactor units -- SCRAM all reactor units
function public.scram_all() function public.scram_all()
@@ -992,7 +1002,9 @@ function facility.new(num_reactors, cooling_conf)
} }
end end
-- SETTINGS -- --#endregion
--#region Settings
-- set the automatic control group of a unit -- set the automatic control group of a unit
---@param unit_id integer unit ID ---@param unit_id integer unit ID
@@ -1033,7 +1045,9 @@ function facility.new(num_reactors, cooling_conf)
return self.pu_fallback return self.pu_fallback
end end
-- DIAGNOSTIC TESTING -- --#endregion
--#region Diagnostic Testing
-- attempt to set a test tone state -- attempt to set a test tone state
---@param id TONE|0 tone ID or 0 to disable all ---@param id TONE|0 tone ID or 0 to disable all
@@ -1073,7 +1087,9 @@ function facility.new(num_reactors, cooling_conf)
return self.allow_testing, self.test_alarm_states return self.allow_testing, self.test_alarm_states
end end
-- READ STATES/PROPERTIES -- --#endregion
--#region Read States/Properties
-- get current alarm tone on/off states -- get current alarm tone on/off states
---@nodiscard ---@nodiscard
@@ -1090,22 +1106,22 @@ function facility.new(num_reactors, cooling_conf)
build.induction = {} build.induction = {}
for i = 1, #self.induction do for i = 1, #self.induction do
local matrix = self.induction[i] ---@type unit_session local matrix = self.induction[i] ---@type unit_session
build.induction[matrix.get_device_idx()] = { matrix.get_db().formed, matrix.get_db().build } build.induction[i] = { matrix.get_db().formed, matrix.get_db().build }
end end
end end
if all or type == RTU_UNIT_TYPE.SPS then if all or type == RTU_UNIT_TYPE.SPS then
build.sps = {} build.sps = {}
for i = 1, #self.sps do for i = 1, #self.sps do
local sps = self.sps[i] ---@type unit_session local sps = self.sps[i] ---@type unit_session
build.sps[sps.get_device_idx()] = { sps.get_db().formed, sps.get_db().build } build.sps[i] = { sps.get_db().formed, sps.get_db().build }
end end
end end
if all or type == RTU_UNIT_TYPE.DYNAMIC_VALVE then if all or type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
build.tanks = {} build.tanks = {}
for i = 1, #self.tanks do for i = 1, #self.tanks do
local tank = self.tanks[i] ---@type unit_session local tank = self.tanks[i] ---@type unit_session
build.tanks[tank.get_device_idx()] = { tank.get_db().formed, tank.get_db().build } build.tanks[tank.get_device_idx()] = { tank.get_db().formed, tank.get_db().build }
end end
end end
@@ -1157,7 +1173,7 @@ function facility.new(num_reactors, cooling_conf)
for i = 1, #self.induction do for i = 1, #self.induction do
local matrix = self.induction[i] ---@type unit_session local matrix = self.induction[i] ---@type unit_session
local db = matrix.get_db() ---@type imatrix_session_db local db = matrix.get_db() ---@type imatrix_session_db
status.induction[matrix.get_device_idx()] = { matrix.is_faulted(), db.formed, db.state, db.tanks } status.induction[i] = { matrix.is_faulted(), db.formed, db.state, db.tanks }
end end
-- status of sps -- status of sps
@@ -1165,7 +1181,7 @@ function facility.new(num_reactors, cooling_conf)
for i = 1, #self.sps do for i = 1, #self.sps do
local sps = self.sps[i] ---@type unit_session local sps = self.sps[i] ---@type unit_session
local db = sps.get_db() ---@type sps_session_db local db = sps.get_db() ---@type sps_session_db
status.sps[sps.get_device_idx()] = { sps.is_faulted(), db.formed, db.state, db.tanks } status.sps[i] = { sps.is_faulted(), db.formed, db.state, db.tanks }
end end
-- status of dynamic tanks -- status of dynamic tanks
@@ -1177,15 +1193,22 @@ function facility.new(num_reactors, cooling_conf)
end end
-- radiation monitors (environment detectors) -- radiation monitors (environment detectors)
status.rad_mon = {} status.envds = {}
for i = 1, #self.envd do for i = 1, #self.envd do
local envd = self.envd[i] ---@type unit_session local envd = self.envd[i] ---@type unit_session
status.rad_mon[envd.get_device_idx()] = { envd.is_faulted(), envd.get_db().radiation } local db = envd.get_db() ---@type envd_session_db
status.envds[envd.get_device_idx()] = { envd.is_faulted(), db.radiation, db.radiation_raw }
end end
return status return status
end end
--#endregion
-- supervisor sessions reporting the list of active RTU sessions
---@param rtu_sessions table session list of all connected RTUs
function public.report_rtus(rtu_sessions) self.rtu_conn_count = #rtu_sessions end
-- get the units in this facility -- get the units in this facility
---@nodiscard ---@nodiscard
function public.get_units() return self.units end function public.get_units() return self.units end

View File

@@ -4,8 +4,8 @@
local util = require("scada-common.util") local util = require("scada-common.util")
local config = require("supervisor.config")
local databus = require("supervisor.databus") local databus = require("supervisor.databus")
local supervisor = require("supervisor.supervisor")
local pgi = require("supervisor.panel.pgi") local pgi = require("supervisor.panel.pgi")
local style = require("supervisor.panel.style") local style = require("supervisor.panel.style")
@@ -88,7 +88,7 @@ local function init(panel)
local plc_page = Div{parent=page_div,x=1,y=1,hidden=true} local plc_page = Div{parent=page_div,x=1,y=1,hidden=true}
local plc_list = Div{parent=plc_page,x=2,y=2,width=49} local plc_list = Div{parent=plc_page,x=2,y=2,width=49}
for i = 1, config.NUM_REACTORS do for i = 1, supervisor.config.UnitCount do
local ps_prefix = "plc_" .. i .. "_" local ps_prefix = "plc_" .. i .. "_"
local plc_entry = Div{parent=plc_list,height=3,fg_bg=bw_fg_bg} local plc_entry = Div{parent=plc_list,height=3,fg_bg=bw_fg_bg}

View File

@@ -390,6 +390,15 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
cmd = UNIT_COMMAND.START, cmd = UNIT_COMMAND.START,
ack = ack ack = ack
}) })
elseif pkt.type == RPLC_TYPE.RPS_DISABLE then
-- disable acknowledgement
local ack = _get_ack(pkt)
if ack then
self.acks.disable = true
self.sDB.control_state = false
elseif ack == false then
log.debug(log_header .. "disable failed!")
end
elseif pkt.type == RPLC_TYPE.RPS_SCRAM then elseif pkt.type == RPLC_TYPE.RPS_SCRAM then
-- manual SCRAM acknowledgement -- manual SCRAM acknowledgement
local ack = _get_ack(pkt) local ack = _get_ack(pkt)
@@ -581,6 +590,9 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
end end
end end
-- check if the manual lockout for automatic control is active
function public.is_auto_locked() return self.auto_lock end
-- set the burn rate on behalf of automatic control -- set the burn rate on behalf of automatic control
---@param rate number burn rate ---@param rate number burn rate
---@param ramp boolean true to ramp, false to not ---@param ramp boolean true to ramp, false to not
@@ -791,7 +803,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
-- reactor disable request retry -- reactor disable request retry
if not self.acks.disable then if not self.acks.disable then
if rtimes.disable_req - util.time() <= 0 then if rtimes.disable_req - util.time() <= 0 then
_send(RPLC_TYPE.RPS_DISABLE, {}) _send(RPLC_TYPE.RPS_DISABLE, {})
rtimes.disable_req = util.time() + RETRY_PERIOD rtimes.disable_req = util.time() + RETRY_PERIOD
end end
@@ -800,7 +812,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
-- SCRAM request retry -- SCRAM request retry
if not self.acks.scram then if not self.acks.scram then
if rtimes.scram_req - util.time() <= 0 then if rtimes.scram_req - util.time() <= 0 then
_send(RPLC_TYPE.RPS_SCRAM, {}) _send(RPLC_TYPE.RPS_SCRAM, {})
rtimes.scram_req = util.time() + RETRY_PERIOD rtimes.scram_req = util.time() + RETRY_PERIOD
end end

View File

@@ -100,7 +100,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
-- validate unit advertisement -- validate unit advertisement
local advert_validator = util.new_validator() local advert_validator = util.new_validator()
advert_validator.assert_type_int(unit_advert.index) advert_validator.assert(util.is_int(unit_advert.index) or (unit_advert.index == false))
advert_validator.assert_type_int(unit_advert.reactor) advert_validator.assert_type_int(unit_advert.reactor)
if u_type == RTU_UNIT_TYPE.REDSTONE then if u_type == RTU_UNIT_TYPE.REDSTONE then
@@ -108,7 +108,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
end end
if advert_validator.valid() then if advert_validator.valid() then
advert_validator.assert_min(unit_advert.index, 1) if util.is_int(unit_advert.index) then advert_validator.assert_min(unit_advert.index, 1) end
advert_validator.assert_min(unit_advert.reactor, 0) advert_validator.assert_min(unit_advert.reactor, 0)
advert_validator.assert_max(unit_advert.reactor, #self.fac_units) advert_validator.assert_max(unit_advert.reactor, #self.fac_units)
if not advert_validator.valid() then u_type = false end if not advert_validator.valid() then u_type = false end

View File

@@ -37,13 +37,16 @@ local PERIODICS = {
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU unit message out queue
function boilerv.new(session_id, unit_id, advert, out_queue) function boilerv.new(session_id, unit_id, advert, out_queue)
-- type check -- checks
if advert.type ~= RTU_UNIT_TYPE.BOILER_VALVE then if advert.type ~= RTU_UNIT_TYPE.BOILER_VALVE then
log.error("attempt to instantiate boilerv RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") log.error("attempt to instantiate boilerv RTU for type " .. types.rtu_type_to_string(advert.type))
return nil
elseif not util.is_int(advert.index) then
log.error("attempt to instantiate boilerv RTU without index")
return nil return nil
end end
local log_tag = "session.rtu(" .. session_id .. ").boilerv(" .. advert.index .. "): " local log_tag = util.c("session.rtu(", session_id, ").boilerv(", advert.index, ")[@", unit_id, "]: ")
local self = { local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),

View File

@@ -49,13 +49,16 @@ local PERIODICS = {
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU unit message out queue
function dynamicv.new(session_id, unit_id, advert, out_queue) function dynamicv.new(session_id, unit_id, advert, out_queue)
-- type check -- checks
if advert.type ~= RTU_UNIT_TYPE.DYNAMIC_VALVE then if advert.type ~= RTU_UNIT_TYPE.DYNAMIC_VALVE then
log.error("attempt to instantiate dynamicv RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") log.error("attempt to instantiate dynamicv RTU for type " .. types.rtu_type_to_string(advert.type))
return nil
elseif not util.is_int(advert.index) then
log.error("attempt to instantiate dynamicv RTU without index")
return nil return nil
end end
local log_tag = "session.rtu(" .. session_id .. ").dynamicv(" .. advert.index .. "): " local log_tag = util.c("session.rtu(", session_id, ").dynamicv(", advert.index, ")[@", unit_id, "]: ")
local self = { local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),

View File

@@ -28,13 +28,16 @@ local PERIODICS = {
---@param advert rtu_advertisement ---@param advert rtu_advertisement
---@param out_queue mqueue ---@param out_queue mqueue
function envd.new(session_id, unit_id, advert, out_queue) function envd.new(session_id, unit_id, advert, out_queue)
-- type check -- checks
if advert.type ~= RTU_UNIT_TYPE.ENV_DETECTOR then if advert.type ~= RTU_UNIT_TYPE.ENV_DETECTOR then
log.error("attempt to instantiate envd RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") log.error("attempt to instantiate envd RTU for type " .. types.rtu_type_to_string(advert.type))
return nil
elseif not util.is_int(advert.index) then
log.error("attempt to instantiate envd RTU without index")
return nil return nil
end end
local log_tag = "session.rtu(" .. session_id .. ").envd(" .. advert.index .. "): " local log_tag = util.c("session.rtu(", session_id, ").envd(", advert.index, ")[@", unit_id, "]: ")
local self = { local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),

View File

@@ -37,13 +37,13 @@ local PERIODICS = {
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU unit message out queue
function imatrix.new(session_id, unit_id, advert, out_queue) function imatrix.new(session_id, unit_id, advert, out_queue)
-- type check -- checks
if advert.type ~= RTU_UNIT_TYPE.IMATRIX then if advert.type ~= RTU_UNIT_TYPE.IMATRIX then
log.error("attempt to instantiate imatrix RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") log.error("attempt to instantiate imatrix RTU for type " .. types.rtu_type_to_string(advert.type))
return nil return nil
end end
local log_tag = "session.rtu(" .. session_id .. ").imatrix(" .. advert.index .. "): " local log_tag = util.c("session.rtu(", session_id, ").imatrix[@", unit_id, "]: ")
local self = { local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),

View File

@@ -52,12 +52,11 @@ local PERIODICS = {
function redstone.new(session_id, unit_id, advert, out_queue) function redstone.new(session_id, unit_id, advert, out_queue)
-- type check -- type check
if advert.type ~= RTU_UNIT_TYPE.REDSTONE then if advert.type ~= RTU_UNIT_TYPE.REDSTONE then
log.error("attempt to instantiate redstone RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") log.error("attempt to instantiate redstone RTU for type " .. types.rtu_type_to_string(advert.type))
return nil return nil
end end
-- for redstone, use unit ID not device index local log_tag = util.c("session.rtu(", session_id, ").redstone[@", unit_id, "]: ")
local log_tag = "session.rtu(" .. session_id .. ").redstone(" .. unit_id .. "): "
local self = { local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),

View File

@@ -36,11 +36,11 @@ local PERIODICS = {
function sna.new(session_id, unit_id, advert, out_queue) function sna.new(session_id, unit_id, advert, out_queue)
-- type check -- type check
if advert.type ~= RTU_UNIT_TYPE.SNA then if advert.type ~= RTU_UNIT_TYPE.SNA then
log.error("attempt to instantiate sna RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") log.error("attempt to instantiate sna RTU for type " .. types.rtu_type_to_string(advert.type))
return nil return nil
end end
local log_tag = "session.rtu(" .. session_id .. ").sna(" .. advert.index .. "): " local log_tag = util.c("session.rtu(", session_id, ").sna[@", unit_id, "]: ")
local self = { local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),

View File

@@ -39,11 +39,11 @@ local PERIODICS = {
function sps.new(session_id, unit_id, advert, out_queue) function sps.new(session_id, unit_id, advert, out_queue)
-- type check -- type check
if advert.type ~= RTU_UNIT_TYPE.SPS then if advert.type ~= RTU_UNIT_TYPE.SPS then
log.error("attempt to instantiate sps RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") log.error("attempt to instantiate sps RTU for type " .. types.rtu_type_to_string(advert.type))
return nil return nil
end end
local log_tag = "session.rtu(" .. session_id .. ").sps(" .. advert.index .. "): " local log_tag = util.c("session.rtu(", session_id, ").sps[@", unit_id, "]: ")
local self = { local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),

View File

@@ -49,13 +49,16 @@ local PERIODICS = {
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU unit message out queue
function turbinev.new(session_id, unit_id, advert, out_queue) function turbinev.new(session_id, unit_id, advert, out_queue)
-- type check -- checks
if advert.type ~= RTU_UNIT_TYPE.TURBINE_VALVE then if advert.type ~= RTU_UNIT_TYPE.TURBINE_VALVE then
log.error("attempt to instantiate turbinev RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") log.error("attempt to instantiate turbinev RTU for type " .. types.rtu_type_to_string(advert.type))
return nil
elseif not util.is_int(advert.index) then
log.error("attempt to instantiate turbinev RTU without index")
return nil return nil
end end
local log_tag = "session.rtu(" .. session_id .. ").turbinev(" .. advert.index .. "): " local log_tag = util.c("session.rtu(", session_id, ").turbinev(", advert.index, ")[@", unit_id, "]: ")
local self = { local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),

View File

@@ -152,7 +152,7 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
function public.get_unit_id() return unit_id end function public.get_unit_id() return unit_id end
-- get the device index -- get the device index
---@nodiscard ---@nodiscard
function public.get_device_idx() return self.device_index end function public.get_device_idx() return self.device_index or 0 end
-- get the reactor ID -- get the reactor ID
---@nodiscard ---@nodiscard
function public.get_reactor() return self.reactor end function public.get_reactor() return self.reactor end

View File

@@ -2,16 +2,14 @@ local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util") local util = require("scada-common.util")
local config = require("supervisor.config")
local databus = require("supervisor.databus") local databus = require("supervisor.databus")
local facility = require("supervisor.facility") local facility = require("supervisor.facility")
local svqtypes = require("supervisor.session.svqtypes")
local coordinator = require("supervisor.session.coordinator") local coordinator = require("supervisor.session.coordinator")
local plc = require("supervisor.session.plc") local plc = require("supervisor.session.plc")
local pocket = require("supervisor.session.pocket") local pocket = require("supervisor.session.pocket")
local rtu = require("supervisor.session.rtu") local rtu = require("supervisor.session.rtu")
local svqtypes = require("supervisor.session.svqtypes")
-- Supervisor Sessions Handler -- Supervisor Sessions Handler
@@ -36,7 +34,7 @@ svsessions.SESSION_TYPE = SESSION_TYPE
local self = { local self = {
nic = nil, ---@type nic|nil nic = nil, ---@type nic|nil
fp_ok = false, fp_ok = false,
num_reactors = 0, config = nil, ---@type svr_config
facility = nil, ---@type facility|nil facility = nil, ---@type facility|nil
sessions = { rtu = {}, plc = {}, crd = {}, pdg = {} }, 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 }
@@ -60,7 +58,7 @@ local function _sv_handle_outq(session)
if msg ~= nil then if msg ~= nil then
if msg.qtype == mqueue.TYPE.PACKET then if msg.qtype == mqueue.TYPE.PACKET then
-- handle a packet to be sent -- handle a packet to be sent
self.nic.transmit(session.r_chan, config.SVR_CHANNEL, msg.message) self.nic.transmit(session.r_chan, self.config.SVR_Channel, msg.message)
elseif msg.qtype == mqueue.TYPE.COMMAND then elseif msg.qtype == mqueue.TYPE.COMMAND then
-- handle instruction/notification -- handle instruction/notification
elseif msg.qtype == mqueue.TYPE.DATA then elseif msg.qtype == mqueue.TYPE.DATA then
@@ -135,7 +133,7 @@ local function _shutdown(session)
while session.out_queue.ready() do while session.out_queue.ready() do
local msg = session.out_queue.pop() local msg = session.out_queue.pop()
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then
self.nic.transmit(session.r_chan, config.SVR_CHANNEL, msg.message) self.nic.transmit(session.r_chan, self.config.SVR_Channel, msg.message)
end end
end end
@@ -197,13 +195,13 @@ end
-- initialize svsessions -- initialize svsessions
---@param nic nic network interface device ---@param nic nic network interface device
---@param fp_ok boolean front panel active ---@param fp_ok boolean front panel active
---@param num_reactors integer number of reactors ---@param config svr_config supervisor configuration
---@param cooling_conf sv_cooling_conf cooling configuration definition ---@param cooling_conf sv_cooling_conf cooling configuration definition
function svsessions.init(nic, fp_ok, num_reactors, cooling_conf) function svsessions.init(nic, fp_ok, config, cooling_conf)
self.nic = nic self.nic = nic
self.fp_ok = fp_ok self.fp_ok = fp_ok
self.num_reactors = num_reactors self.config = config
self.facility = facility.new(num_reactors, cooling_conf) self.facility = facility.new(config.UnitCount, cooling_conf)
end end
-- find an RTU session by the computer ID -- find an RTU session by the computer ID
@@ -280,14 +278,14 @@ end
---@param version string ---@param version string
---@return integer|false session_id ---@return integer|false session_id
function svsessions.establish_plc_session(source_addr, for_reactor, version) function svsessions.establish_plc_session(source_addr, for_reactor, version)
if svsessions.get_reactor_session(for_reactor) == nil and for_reactor >= 1 and for_reactor <= self.num_reactors then if svsessions.get_reactor_session(for_reactor) == nil and for_reactor >= 1 and for_reactor <= self.config.UnitCount then
---@class plc_session_struct ---@class plc_session_struct
local plc_s = { local plc_s = {
s_type = "plc", s_type = "plc",
open = true, open = true,
reactor = for_reactor, reactor = for_reactor,
version = version, version = version,
r_chan = config.PLC_CHANNEL, r_chan = self.config.PLC_Channel,
s_addr = source_addr, s_addr = source_addr,
in_queue = mqueue.new(), in_queue = mqueue.new(),
out_queue = mqueue.new(), out_queue = mqueue.new(),
@@ -296,8 +294,7 @@ function svsessions.establish_plc_session(source_addr, for_reactor, version)
local id = self.next_ids.plc 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, 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)
config.PLC_TIMEOUT, self.fp_ok)
table.insert(self.sessions.plc, plc_s) table.insert(self.sessions.plc, plc_s)
local units = self.facility.get_units() local units = self.facility.get_units()
@@ -305,8 +302,7 @@ function svsessions.establish_plc_session(source_addr, for_reactor, version)
local mt = { local mt = {
---@param s plc_session_struct ---@param s plc_session_struct
__tostring = function (s) return util.c("PLC [", s.instance.get_id(), "] for reactor #", s.reactor, __tostring = function (s) return util.c("PLC [", s.instance.get_id(), "] for reactor #", s.reactor, " (@", s.s_addr, ")") end
" (@", s.s_addr, ")") end
} }
setmetatable(plc_s, mt) setmetatable(plc_s, mt)
@@ -336,7 +332,7 @@ function svsessions.establish_rtu_session(source_addr, advertisement, version)
s_type = "rtu", s_type = "rtu",
open = true, open = true,
version = version, version = version,
r_chan = config.RTU_CHANNEL, r_chan = self.config.RTU_Channel,
s_addr = source_addr, s_addr = source_addr,
in_queue = mqueue.new(), in_queue = mqueue.new(),
out_queue = mqueue.new(), out_queue = mqueue.new(),
@@ -345,8 +341,7 @@ function svsessions.establish_rtu_session(source_addr, advertisement, version)
local id = self.next_ids.rtu local id = self.next_ids.rtu
rtu_s.instance = rtu.new_session(id, source_addr, rtu_s.in_queue, rtu_s.out_queue, config.RTU_TIMEOUT, 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)
advertisement, self.facility, self.fp_ok)
table.insert(self.sessions.rtu, rtu_s) table.insert(self.sessions.rtu, rtu_s)
local mt = { local mt = {
@@ -377,7 +372,7 @@ function svsessions.establish_crd_session(source_addr, version)
s_type = "crd", s_type = "crd",
open = true, open = true,
version = version, version = version,
r_chan = config.CRD_CHANNEL, r_chan = self.config.CRD_Channel,
s_addr = source_addr, s_addr = source_addr,
in_queue = mqueue.new(), in_queue = mqueue.new(),
out_queue = mqueue.new(), out_queue = mqueue.new(),
@@ -386,8 +381,7 @@ function svsessions.establish_crd_session(source_addr, version)
local id = self.next_ids.crd local id = self.next_ids.crd
crd_s.instance = coordinator.new_session(id, source_addr, crd_s.in_queue, crd_s.out_queue, config.CRD_TIMEOUT, 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)
self.facility, self.fp_ok)
table.insert(self.sessions.crd, crd_s) table.insert(self.sessions.crd, crd_s)
local mt = { local mt = {
@@ -421,7 +415,7 @@ function svsessions.establish_pdg_session(source_addr, version)
s_type = "pkt", s_type = "pkt",
open = true, open = true,
version = version, version = version,
r_chan = config.PKT_CHANNEL, r_chan = self.config.PKT_Channel,
s_addr = source_addr, s_addr = source_addr,
in_queue = mqueue.new(), in_queue = mqueue.new(),
out_queue = mqueue.new(), out_queue = mqueue.new(),
@@ -430,8 +424,7 @@ function svsessions.establish_pdg_session(source_addr, version)
local id = self.next_ids.pdg local id = self.next_ids.pdg
pdg_s.instance = pocket.new_session(id, source_addr, pdg_s.in_queue, pdg_s.out_queue, config.PKT_TIMEOUT, self.facility, 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)
self.fp_ok)
table.insert(self.sessions.pdg, pdg_s) table.insert(self.sessions.pdg, pdg_s)
local mt = { local mt = {

View File

@@ -14,70 +14,71 @@ local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local config = require("supervisor.config") local configure = require("supervisor.configure")
local databus = require("supervisor.databus") local databus = require("supervisor.databus")
local renderer = require("supervisor.renderer") local renderer = require("supervisor.renderer")
local supervisor = require("supervisor.supervisor") local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v1.0.6" local SUPERVISOR_VERSION = "v1.2.11"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
---------------------------------------- ----------------------------------------
-- config validation -- get configuration
---------------------------------------- ----------------------------------------
if not supervisor.load_config() then
-- try to reconfigure (user action)
local success, error = configure.configure(true)
if success then
if not supervisor.load_config() then
println("failed to load a valid configuration, please reconfigure")
return
end
else
println("configuration error: " .. error)
return
end
end
local config = supervisor.config
local cfv = util.new_validator() local cfv = util.new_validator()
cfv.assert_channel(config.SVR_CHANNEL) cfv.assert_eq(#config.CoolingConfig, config.UnitCount)
cfv.assert_channel(config.PLC_CHANNEL) assert(cfv.valid(), "startup> the number of reactor cooling configurations is different than the number of units")
cfv.assert_channel(config.RTU_CHANNEL)
cfv.assert_channel(config.CRD_CHANNEL)
cfv.assert_channel(config.PKT_CHANNEL)
cfv.assert_type_int(config.TRUSTED_RANGE)
cfv.assert_type_num(config.PLC_TIMEOUT)
cfv.assert_min(config.PLC_TIMEOUT, 2)
cfv.assert_type_num(config.RTU_TIMEOUT)
cfv.assert_min(config.RTU_TIMEOUT, 2)
cfv.assert_type_num(config.CRD_TIMEOUT)
cfv.assert_min(config.CRD_TIMEOUT, 2)
cfv.assert_type_num(config.PKT_TIMEOUT)
cfv.assert_min(config.PKT_TIMEOUT, 2)
cfv.assert_type_int(config.NUM_REACTORS)
cfv.assert_type_table(config.REACTOR_COOLING)
cfv.assert_type_int(config.FAC_TANK_MODE)
cfv.assert_type_table(config.FAC_TANK_DEFS)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
assert(cfv.valid(), "bad config file: missing/invalid fields") for i = 1, config.UnitCount do
cfv.assert_type_table(config.CoolingConfig[i])
assert(cfv.valid(), "startup> missing cooling entry for reactor unit " .. i)
cfv.assert_type_int(config.CoolingConfig[i].BoilerCount)
cfv.assert_type_int(config.CoolingConfig[i].TurbineCount)
cfv.assert_type_bool(config.CoolingConfig[i].TankConnection)
assert(cfv.valid(), "startup> missing boiler/turbine/tank fields for reactor unit " .. i)
cfv.assert_range(config.CoolingConfig[i].BoilerCount, 0, 2)
cfv.assert_range(config.CoolingConfig[i].TurbineCount, 1, 3)
assert(cfv.valid(), "startup> out-of-range number of boilers and/or turbines provided for reactor unit " .. i)
end
assert((config.FAC_TANK_MODE == 0) or (config.NUM_REACTORS == #config.FAC_TANK_DEFS), if config.FacilityTankMode > 0 then
"bad config file: FAC_TANK_DEFS length not equal to NUM_REACTORS") assert(config.UnitCount == #config.FacilityTankDefs, "startup> the number of facility tank definitions must be equal to the number of units in facility tank mode")
cfv.assert_eq(#config.REACTOR_COOLING, config.NUM_REACTORS) for i = 1, config.UnitCount do
assert(cfv.valid(), "config: number of cooling configs different than number of units") local def = config.FacilityTankDefs[i]
cfv.assert_type_int(def)
for i = 1, config.NUM_REACTORS do cfv.assert_range(def, 0, 2)
cfv.assert_type_table(config.REACTOR_COOLING[i]) assert(cfv.valid(), "startup> invalid facility tank definition for reactor unit " .. i)
assert(cfv.valid(), "config: missing cooling entry for reactor " .. i) end
cfv.assert_type_int(config.REACTOR_COOLING[i].BOILERS)
cfv.assert_type_int(config.REACTOR_COOLING[i].TURBINES)
cfv.assert_type_bool(config.REACTOR_COOLING[i].TANK)
assert(cfv.valid(), "config: missing boilers/turbines for reactor " .. i)
cfv.assert_min(config.REACTOR_COOLING[i].BOILERS, 0)
cfv.assert_min(config.REACTOR_COOLING[i].TURBINES, 1)
assert(cfv.valid(), "config: bad number of boilers/turbines for reactor " .. i)
end end
---------------------------------------- ----------------------------------------
-- log init -- log init
---------------------------------------- ----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true) log.init(config.LogPath, config.LogMode, config.LogDebug)
log.info("========================================") log.info("========================================")
log.info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION) log.info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION)
@@ -102,8 +103,8 @@ local function main()
ppm.mount_all() ppm.mount_all()
-- message authentication init -- message authentication init
if type(config.AUTH_KEY) == "string" then if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
network.init_mac(config.AUTH_KEY) network.init_mac(config.AuthKey)
end end
-- get modem -- get modem

View File

@@ -2,8 +2,6 @@ local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local util = require("scada-common.util") local util = require("scada-common.util")
local config = require("supervisor.config")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local supervisor = {} local supervisor = {}
@@ -13,6 +11,79 @@ local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
---@type svr_config
local config = {}
supervisor.config = config
-- load the supervisor configuration
function supervisor.load_config()
if not settings.load("/supervisor.settings") then return false end
config.UnitCount = settings.get("UnitCount")
config.CoolingConfig = settings.get("CoolingConfig")
config.FacilityTankMode = settings.get("FacilityTankMode")
config.FacilityTankDefs = settings.get("FacilityTankDefs")
config.SVR_Channel = settings.get("SVR_Channel")
config.PLC_Channel = settings.get("PLC_Channel")
config.RTU_Channel = settings.get("RTU_Channel")
config.CRD_Channel = settings.get("CRD_Channel")
config.PKT_Channel = settings.get("PKT_Channel")
config.PLC_Timeout = settings.get("PLC_Timeout")
config.RTU_Timeout = settings.get("RTU_Timeout")
config.CRD_Timeout = settings.get("CRD_Timeout")
config.PKT_Timeout = settings.get("PKT_Timeout")
config.TrustedRange = settings.get("TrustedRange")
config.AuthKey = settings.get("AuthKey")
config.LogMode = settings.get("LogMode")
config.LogPath = settings.get("LogPath")
config.LogDebug = settings.get("LogDebug")
local cfv = util.new_validator()
cfv.assert_type_int(config.UnitCount)
cfv.assert_range(config.UnitCount, 1, 4)
cfv.assert_type_table(config.CoolingConfig)
cfv.assert_type_table(config.FacilityTankDefs)
cfv.assert_type_int(config.FacilityTankMode)
cfv.assert_range(config.FacilityTankMode, 0, 8)
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.PLC_Channel)
cfv.assert_channel(config.RTU_Channel)
cfv.assert_channel(config.CRD_Channel)
cfv.assert_channel(config.PKT_Channel)
cfv.assert_type_num(config.PLC_Timeout)
cfv.assert_min(config.PLC_Timeout, 2)
cfv.assert_type_num(config.RTU_Timeout)
cfv.assert_min(config.RTU_Timeout, 2)
cfv.assert_type_num(config.CRD_Timeout)
cfv.assert_min(config.CRD_Timeout, 2)
cfv.assert_type_num(config.PKT_Timeout)
cfv.assert_min(config.PKT_Timeout, 2)
cfv.assert_type_num(config.TrustedRange)
cfv.assert_min(config.TrustedRange, 0)
if type(config.AuthKey) == "string" then
local len = string.len(config.AuthKey)
cfv.assert_eq(len == 0 or len >= 8, true)
end
cfv.assert_type_int(config.LogMode)
cfv.assert_range(config.LogMode, 0, 1)
cfv.assert_type_str(config.LogPath)
cfv.assert_type_bool(config.LogDebug)
return cfv.valid()
end
-- supervisory controller communications -- supervisory controller communications
---@nodiscard ---@nodiscard
---@param _version string supervisor version ---@param _version string supervisor version
@@ -23,32 +94,23 @@ function supervisor.comms(_version, nic, fp_ok)
-- print a log message to the terminal as long as the UI isn't running -- 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 function println(message) if not fp_ok then util.println_ts(message) end end
-- channel list from config
local svr_channel = config.SVR_CHANNEL
local plc_channel = config.PLC_CHANNEL
local rtu_channel = config.RTU_CHANNEL
local crd_channel = config.CRD_CHANNEL
local pkt_channel = config.PKT_CHANNEL
-- configuration data
local num_reactors = config.NUM_REACTORS
---@class sv_cooling_conf ---@class sv_cooling_conf
local cooling_conf = { r_cool = config.REACTOR_COOLING, fac_tank_mode = config.FAC_TANK_MODE, fac_tank_defs = config.FAC_TANK_DEFS } local cooling_conf = { r_cool = config.CoolingConfig, fac_tank_mode = config.FacilityTankMode, fac_tank_defs = config.FacilityTankDefs }
local self = { local self = {
last_est_acks = {} last_est_acks = {}
} }
comms.set_trusted_range(config.TRUSTED_RANGE) comms.set_trusted_range(config.TrustedRange)
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure modem channels -- configure modem channels
nic.closeAll() nic.closeAll()
nic.open(svr_channel) nic.open(config.SVR_Channel)
-- pass modem, status, and config data to svsessions -- pass modem, status, and config data to svsessions
svsessions.init(nic, fp_ok, num_reactors, cooling_conf) svsessions.init(nic, fp_ok, config, cooling_conf)
-- send an establish request response -- send an establish request response
---@param packet scada_packet ---@param packet scada_packet
@@ -61,7 +123,7 @@ function supervisor.comms(_version, nic, fp_ok)
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data }) m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data })
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
nic.transmit(packet.remote_channel(), svr_channel, s_pkt) nic.transmit(packet.remote_channel(), config.SVR_Channel, s_pkt)
self.last_est_acks[packet.src_addr()] = ack self.last_est_acks[packet.src_addr()] = ack
end end
@@ -124,9 +186,9 @@ function supervisor.comms(_version, nic, fp_ok)
local src_addr = packet.scada_frame.src_addr() local src_addr = packet.scada_frame.src_addr()
local protocol = packet.scada_frame.protocol() local protocol = packet.scada_frame.protocol()
if l_chan ~= svr_channel then if l_chan ~= config.SVR_Channel then
log.debug("received packet on unconfigured channel " .. l_chan, true) log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == plc_channel then elseif r_chan == config.PLC_Channel then
-- look for an associated session -- look for an associated session
local session = svsessions.find_plc_session(src_addr) local session = svsessions.find_plc_session(src_addr)
@@ -137,9 +199,8 @@ function supervisor.comms(_version, nic, fp_ok)
-- pass the packet onto the session handler -- pass the packet onto the session handler
session.in_queue.push_packet(packet) session.in_queue.push_packet(packet)
else else
-- unknown session, force a re-link -- any other packet should be session related, discard it
log.debug("PLC_ESTABLISH: no session but not an establish, forcing relink") log.debug("discarding RPLC packet without a known session")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end end
elseif protocol == PROTOCOL.SCADA_MGMT then elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
@@ -201,7 +262,7 @@ function supervisor.comms(_version, nic, fp_ok)
else else
log.debug(util.c("illegal packet type ", protocol, " on PLC channel")) log.debug(util.c("illegal packet type ", protocol, " on PLC channel"))
end end
elseif r_chan == rtu_channel then elseif r_chan == config.RTU_Channel then
-- look for an associated session -- look for an associated session
local session = svsessions.find_rtu_session(src_addr) local session = svsessions.find_rtu_session(src_addr)
@@ -265,7 +326,7 @@ function supervisor.comms(_version, nic, fp_ok)
else else
log.debug(util.c("illegal packet type ", protocol, " on RTU channel")) log.debug(util.c("illegal packet type ", protocol, " on RTU channel"))
end end
elseif r_chan == crd_channel then elseif r_chan == config.CRD_Channel then
-- look for an associated session -- look for an associated session
local session = svsessions.find_crd_session(src_addr) local session = svsessions.find_crd_session(src_addr)
@@ -299,7 +360,7 @@ function supervisor.comms(_version, nic, fp_ok)
println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected")) 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)) 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, { num_reactors, cooling_conf }) _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { config.UnitCount, cooling_conf })
else else
if last_ack ~= ESTABLISH_ACK.COLLISION then 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.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator")
@@ -332,7 +393,7 @@ function supervisor.comms(_version, nic, fp_ok)
else else
log.debug(util.c("illegal packet type ", protocol, " on coordinator channel")) log.debug(util.c("illegal packet type ", protocol, " on coordinator channel"))
end end
elseif r_chan == pkt_channel then elseif r_chan == config.PKT_Channel then
-- look for an associated session -- look for an associated session
local session = svsessions.find_pdg_session(src_addr) local session = svsessions.find_pdg_session(src_addr)

View File

@@ -77,7 +77,6 @@ function unit.new(reactor_id, num_boilers, num_turbines)
tanks = {}, tanks = {},
snas = {}, snas = {},
envd = {}, envd = {},
sna_prod_rate = 0,
-- redstone control -- redstone control
io_ctl = nil, ---@type rs_controller io_ctl = nil, ---@type rs_controller
valves = {}, ---@type unit_valves valves = {}, ---@type unit_valves
@@ -256,7 +255,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
--#region time derivative utility functions --#region Time Derivative Utility Functions
-- compute a change with respect to time of the given value -- compute a change with respect to time of the given value
---@param key string value key ---@param key string value key
@@ -331,7 +330,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
--#endregion --#endregion
--#region redstone I/O --#region Redstone I/O
-- create a generic valve interface -- create a generic valve interface
---@nodiscard ---@nodiscard
@@ -398,8 +397,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
---@class reactor_unit ---@class reactor_unit
local public = {} local public = {}
-- ADD/LINK DEVICES -- --#region Add/Link Devices
--#region
-- link the PLC -- link the PLC
---@param plc_session plc_session_struct ---@param plc_session plc_session_struct
@@ -489,7 +487,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
--#endregion --#endregion
-- UPDATE SESSION -- --#region Update Session
-- update (iterate) this unit -- update (iterate) this unit
function public.update() function public.update()
@@ -498,7 +496,6 @@ function unit.new(reactor_id, num_boilers, num_turbines)
self.plc_s = nil self.plc_s = nil
self.plc_i = nil self.plc_i = nil
self.db.control.br100 = 0 self.db.control.br100 = 0
self.db.control.lim_br100 = 0
end end
-- unlink RTU unit sessions if they are closed -- unlink RTU unit sessions if they are closed
@@ -525,12 +522,14 @@ function unit.new(reactor_id, num_boilers, num_turbines)
end end
end end
-- check plc formed/faulted -- plc instance checks
if self.plc_i ~= nil then if self.plc_i ~= nil then
-- check if degraded
local rps = self.plc_i.get_rps() local rps = self.plc_i.get_rps()
if rps.fault or rps.sys_fail then if rps.fault or rps.sys_fail then self.db.control.degraded = true end
self.db.control.degraded = true
end -- re-engage auto lock if it reconnected without it
if self.auto_engaged and not self.plc_i.is_auto_locked() then self.plc_i.auto_lock(true) end
end end
-- update deltas -- update deltas
@@ -556,13 +555,15 @@ function unit.new(reactor_id, num_boilers, num_turbines)
end end
end end
-- AUTO CONTROL OPERATIONS -- --#endregion
--#region
--#region Auto Control Operations
-- engage automatic control -- engage automatic control
function public.auto_engage() function public.auto_engage()
self.auto_engaged = true self.auto_engaged = true
if self.plc_i ~= nil then if self.plc_i ~= nil then
log.debug(util.c("UNIT ", self.r_id, ": engaged auto control"))
self.plc_i.auto_lock(true) self.plc_i.auto_lock(true)
end end
end end
@@ -571,6 +572,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
function public.auto_disengage() function public.auto_disengage()
self.auto_engaged = false self.auto_engaged = false
if self.plc_i ~= nil then if self.plc_i ~= nil then
log.debug(util.c("UNIT ", self.r_id, ": disengaged auto control"))
self.plc_i.auto_lock(false) self.plc_i.auto_lock(false)
self.db.control.br100 = 0 self.db.control.br100 = 0
end end
@@ -581,12 +583,12 @@ function unit.new(reactor_id, num_boilers, num_turbines)
---@nodiscard ---@nodiscard
---@return integer lim_br100 ---@return integer lim_br100
function public.auto_get_effective_limit() function public.auto_get_effective_limit()
if (not self.db.control.ready) or self.db.control.degraded or self.plc_cache.rps_trip then local ctrl = self.db.control
self.db.control.br100 = 0 if (not ctrl.ready) or ctrl.degraded or self.plc_cache.rps_trip then
-- log.debug(util.c("UNIT ", self.r_id, ": effective limit is zero! ready[", ctrl.ready, "] degraded[", ctrl.degraded, "] rps_trip[", self.plc_cache.rps_trip, "]"))
ctrl.br100 = 0
return 0 return 0
else else return ctrl.lim_br100 end
return self.db.control.lim_br100
end
end end
-- set the automatic burn rate based on the last set burn rate in 100ths -- set the automatic burn rate based on the last set burn rate in 100ths
@@ -594,8 +596,8 @@ function unit.new(reactor_id, num_boilers, num_turbines)
function public.auto_commit_br100(ramp) function public.auto_commit_br100(ramp)
if self.auto_engaged then if self.auto_engaged then
if self.plc_i ~= nil then if self.plc_i ~= nil then
log.debug(util.c("UNIT ", self.r_id, ": commit br100 of ", self.db.control.br100, " with ramp set to ", ramp))
self.plc_i.auto_set_burn(self.db.control.br100 / 100, ramp) self.plc_i.auto_set_burn(self.db.control.br100 / 100, ramp)
if ramp then self.ramp_target_br100 = self.db.control.br100 end if ramp then self.ramp_target_br100 = self.db.control.br100 end
end end
end end
@@ -642,8 +644,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
--#endregion --#endregion
-- OPERATIONS -- --#region Operations
--#region
-- queue a command to disable the reactor -- queue a command to disable the reactor
function public.disable() function public.disable()
@@ -723,8 +724,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
--#endregion --#endregion
-- READ STATES/PROPERTIES -- --#region Read States/Properties
--#region
-- check if an alarm of at least a certain priority level is tripped -- check if an alarm of at least a certain priority level is tripped
---@nodiscard ---@nodiscard
@@ -854,25 +854,28 @@ function unit.new(reactor_id, num_boilers, num_turbines)
status.tanks[tank.get_device_idx()] = { tank.is_faulted(), db.formed, db.state, db.tanks } status.tanks[tank.get_device_idx()] = { tank.is_faulted(), db.formed, db.state, db.tanks }
end end
-- basic SNA statistical information -- SNA statistical information
local total_peak = 0 local total_peak, total_avail, total_out = 0, 0, 0
for i = 1, #self.snas do for i = 1, #self.snas do
local db = self.snas[i].get_db() ---@type sna_session_db local db = self.snas[i].get_db() ---@type sna_session_db
total_peak = total_peak + db.state.peak_production total_peak = total_peak + db.state.peak_production
total_avail = total_avail + db.state.production_rate
total_out = total_out + math.min(db.tanks.input.amount / 10, db.state.production_rate)
end end
status.sna = { #self.snas, public.get_sna_rate(), total_peak } status.sna = { #self.snas, total_peak, total_avail, total_out }
-- radiation monitors (environment detectors) -- radiation monitors (environment detectors)
status.rad_mon = {} status.envds = {}
for i = 1, #self.envd do for i = 1, #self.envd do
local envd = self.envd[i] ---@type unit_session local envd = self.envd[i] ---@type unit_session
status.rad_mon[envd.get_device_idx()] = { envd.is_faulted(), envd.get_db().radiation } local db = envd.get_db() ---@type envd_session_db
status.envds[envd.get_device_idx()] = { envd.is_faulted(), db.radiation, db.radiation_raw }
end end
return status return status
end end
-- get the current total [max] production rate is -- get the current total max production rate
---@nodiscard ---@nodiscard
---@return number total_avail_rate ---@return number total_avail_rate
function public.get_sna_rate() function public.get_sna_rate()

View File

@@ -47,24 +47,23 @@ function logic.update_annunciator(self)
local num_boilers = self.num_boilers local num_boilers = self.num_boilers
local num_turbines = self.num_turbines local num_turbines = self.num_turbines
local annunc = self.db.annunciator
self.db.annunciator.RCSFault = false annunc.RCSFault = false
-- variables for boiler, or reactor if no boilers used -- variables for boiler, or reactor if no boilers used
local total_boil_rate = 0.0 local total_boil_rate = 0.0
------------- --#region Reactor
-- REACTOR --
-------------
self.db.annunciator.AutoControl = self.auto_engaged annunc.AutoControl = self.auto_engaged
-- check PLC status -- check PLC status
self.db.annunciator.PLCOnline = self.plc_i ~= nil annunc.PLCOnline = self.plc_i ~= nil
local plc_ready = self.db.annunciator.PLCOnline local plc_ready = annunc.PLCOnline
if self.db.annunciator.PLCOnline then if plc_ready then
local plc_db = self.plc_i.get_db() local plc_db = self.plc_i.get_db()
-- update ready state -- update ready state
@@ -75,7 +74,7 @@ function logic.update_annunciator(self)
(next(self.plc_i.get_status()) ~= nil) and (next(self.plc_i.get_struct()) ~= nil) (next(self.plc_i.get_status()) ~= nil) and (next(self.plc_i.get_struct()) ~= nil)
-- update auto control limit -- update auto control limit
if (self.db.control.lim_br100 == 0) or ((self.db.control.lim_br100 / 100) > plc_db.mek_struct.max_burn) then if (plc_db.mek_struct.max_burn > 0) and ((self.db.control.lim_br100 / 100) > plc_db.mek_struct.max_burn) then
self.db.control.lim_br100 = math.floor(plc_db.mek_struct.max_burn * 100) self.db.control.lim_br100 = math.floor(plc_db.mek_struct.max_burn * 100)
end end
@@ -110,29 +109,29 @@ function logic.update_annunciator(self)
-- heartbeat blink about every second -- heartbeat blink about every second
if self.last_heartbeat + 1000 < plc_db.last_status_update then if self.last_heartbeat + 1000 < plc_db.last_status_update then
self.db.annunciator.PLCHeartbeat = not self.db.annunciator.PLCHeartbeat annunc.PLCHeartbeat = not annunc.PLCHeartbeat
self.last_heartbeat = plc_db.last_status_update self.last_heartbeat = plc_db.last_status_update
end end
local flow_low = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, ANNUNC_LIMS.RCSFlowLow_NA, ANNUNC_LIMS.RCSFlowLow_H2O) local flow_low = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, ANNUNC_LIMS.RCSFlowLow_NA, ANNUNC_LIMS.RCSFlowLow_H2O)
-- update other annunciator fields -- update other annunciator fields
self.db.annunciator.ReactorSCRAM = plc_db.rps_tripped annunc.ReactorSCRAM = plc_db.rps_tripped
self.db.annunciator.ManualReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.MANUAL annunc.ManualReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.MANUAL
self.db.annunciator.AutoReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.AUTOMATIC annunc.AutoReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.AUTOMATIC
self.db.annunciator.RCPTrip = plc_db.rps_tripped and (plc_db.rps_status.ex_hcool or plc_db.rps_status.low_cool) annunc.RCPTrip = plc_db.rps_tripped and (plc_db.rps_status.ex_hcool or plc_db.rps_status.low_cool)
self.db.annunciator.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < flow_low annunc.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < flow_low
self.db.annunciator.CoolantLevelLow = plc_db.mek_status.ccool_fill < ANNUNC_LIMS.CoolantLevelLow annunc.CoolantLevelLow = plc_db.mek_status.ccool_fill < ANNUNC_LIMS.CoolantLevelLow
self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > ANNUNC_LIMS.ReactorTempHigh annunc.ReactorTempHigh = plc_db.mek_status.temp > ANNUNC_LIMS.ReactorTempHigh
self.db.annunciator.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > ANNUNC_LIMS.ReactorHighDeltaT annunc.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > ANNUNC_LIMS.ReactorHighDeltaT
self.db.annunciator.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= ANNUNC_LIMS.FuelLevelLow annunc.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= ANNUNC_LIMS.FuelLevelLow
self.db.annunciator.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= ANNUNC_LIMS.WasteLevelHigh annunc.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= ANNUNC_LIMS.WasteLevelHigh
local heating_rate_conv = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, 200000, 20000) local heating_rate_conv = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, 200000, 20000)
local high_rate = plc_db.mek_status.burn_rate >= (plc_db.mek_status.ccool_amnt * 0.27 / heating_rate_conv) local high_rate = plc_db.mek_status.burn_rate >= (plc_db.mek_status.ccool_amnt * 0.27 / heating_rate_conv)
-- this advisory applies when no coolant is buffered (which we can't easily determine)<br> -- this advisory applies when no coolant is buffered (which we can't easily determine)<br>
-- it's a rough estimation, see GitHub cc-mek-scada/wiki/High-Rate-Calculation -- it's a rough estimation, see GitHub cc-mek-scada/wiki/High-Rate-Calculation
self.db.annunciator.HighStartupRate = not plc_db.mek_status.status and high_rate annunc.HighStartupRate = not plc_db.mek_status.status and high_rate
-- if no boilers, use reactor heating rate to check for boil rate mismatch -- if no boilers, use reactor heating rate to check for boil rate mismatch
if num_boilers == 0 then if num_boilers == 0 then
@@ -142,37 +141,41 @@ function logic.update_annunciator(self)
self.plc_cache.ok = false self.plc_cache.ok = false
end end
--------------- --#endregion
-- MISC RTUs --
--------------- --#region Misc RTUs
local max_rad, any_faulted = 0, false
self.db.annunciator.RadiationMonitor = 1
self.db.annunciator.RadiationWarning = false
for i = 1, #self.envd do for i = 1, #self.envd do
local envd = self.envd[i] ---@type unit_session local envd = self.envd[i] ---@type unit_session
self.db.annunciator.RadiationMonitor = util.trinary(envd.is_faulted(), 2, 3) local db = envd.get_db() ---@type envd_session_db
self.db.annunciator.RadiationWarning = envd.get_db().radiation_raw >= ANNUNC_LIMS.RadiationWarning any_faulted = any_faulted or envd.is_faulted()
break if db.radiation_raw > max_rad then max_rad = db.radiation_raw end
end end
self.db.annunciator.EmergencyCoolant = 1 annunc.RadiationMonitor = util.trinary(#self.envd == 0, 1, util.trinary(any_faulted, 2, 3))
annunc.RadiationWarning = max_rad >= ANNUNC_LIMS.RadiationWarning
annunc.EmergencyCoolant = 1
for i = 1, #self.redstone do for i = 1, #self.redstone do
local db = self.redstone[i].get_db() ---@type redstone_session_db local db = self.redstone[i].get_db() ---@type redstone_session_db
local io = db.io[IO.U_EMER_COOL] ---@type rs_db_dig_io|nil local io = db.io[IO.U_EMER_COOL] ---@type rs_db_dig_io|nil
if io ~= nil then if io ~= nil then
self.db.annunciator.EmergencyCoolant = util.trinary(io.read(), 3, 2) annunc.EmergencyCoolant = util.trinary(io.read(), 3, 2)
break break
end end
end end
------------- --#endregion
-- BOILERS --
------------- --#region Boilers
local boilers_ready = num_boilers == #self.boilers local boilers_ready = num_boilers == #self.boilers
-- clear boiler online flags -- clear boiler online flags
for i = 1, num_boilers do self.db.annunciator.BoilerOnline[i] = false end for i = 1, num_boilers do annunc.BoilerOnline[i] = false end
-- aggregated statistics -- aggregated statistics
local boiler_steam_dt_sum = 0.0 local boiler_steam_dt_sum = 0.0
@@ -185,7 +188,7 @@ function logic.update_annunciator(self)
local boiler = session.get_db() ---@type boilerv_session_db local boiler = session.get_db() ---@type boilerv_session_db
local idx = session.get_device_idx() local idx = session.get_device_idx()
self.db.annunciator.RCSFault = self.db.annunciator.RCSFault or (not boiler.formed) or session.is_faulted() annunc.RCSFault = annunc.RCSFault or (not boiler.formed) or session.is_faulted()
-- update ready state -- update ready state
-- - must be formed -- - must be formed
@@ -199,8 +202,8 @@ function logic.update_annunciator(self)
boiler_steam_dt_sum = _get_dt(DT_KEYS.BoilerSteam .. idx) boiler_steam_dt_sum = _get_dt(DT_KEYS.BoilerSteam .. idx)
boiler_water_dt_sum = _get_dt(DT_KEYS.BoilerWater .. idx) boiler_water_dt_sum = _get_dt(DT_KEYS.BoilerWater .. idx)
self.db.annunciator.BoilerOnline[idx] = true annunc.BoilerOnline[idx] = true
self.db.annunciator.WaterLevelLow[idx] = boiler.tanks.water_fill < ANNUNC_LIMS.WaterLevelLow annunc.WaterLevelLow[idx] = boiler.tanks.water_fill < ANNUNC_LIMS.WaterLevelLow
end end
-- check heating rate low -- check heating rate low
@@ -209,14 +212,14 @@ function logic.update_annunciator(self)
-- check for inactive boilers while reactor is active -- check for inactive boilers while reactor is active
for i = 1, #self.boilers do for i = 1, #self.boilers do
local boiler = self.boilers[i] ---@type unit_session local boiler = self.boilers[i] ---@type unit_session
local idx = boiler.get_device_idx() local idx = boiler.get_device_idx()
local db = boiler.get_db() ---@type boilerv_session_db local db = boiler.get_db() ---@type boilerv_session_db
if r_db.mek_status.status then if r_db.mek_status.status then
self.db.annunciator.HeatingRateLow[idx] = db.state.boil_rate == 0 annunc.HeatingRateLow[idx] = db.state.boil_rate == 0
else else
self.db.annunciator.HeatingRateLow[idx] = false annunc.HeatingRateLow[idx] = false
end end
end end
end end
@@ -225,18 +228,18 @@ function logic.update_annunciator(self)
boiler_water_dt_sum = _get_dt(DT_KEYS.ReactorCCool) boiler_water_dt_sum = _get_dt(DT_KEYS.ReactorCCool)
end end
--------------------------- --#endregion
-- COOLANT FEED MISMATCH --
--------------------------- --#region Coolant Feed Mismatch
-- check coolant feed mismatch if using boilers, otherwise calculate with reactor -- check coolant feed mismatch if using boilers, otherwise calculate with reactor
local cfmismatch = false local cfmismatch = false
if num_boilers > 0 then if num_boilers > 0 then
for i = 1, #self.boilers do for i = 1, #self.boilers do
local boiler = self.boilers[i] ---@type unit_session local boiler = self.boilers[i] ---@type unit_session
local idx = boiler.get_device_idx() local idx = boiler.get_device_idx()
local db = boiler.get_db() ---@type boilerv_session_db local db = boiler.get_db() ---@type boilerv_session_db
local gaining_hc = _get_dt(DT_KEYS.BoilerHCool .. idx) > 10.0 or db.tanks.hcool_fill == 1 local gaining_hc = _get_dt(DT_KEYS.BoilerHCool .. idx) > 10.0 or db.tanks.hcool_fill == 1
@@ -256,16 +259,16 @@ function logic.update_annunciator(self)
cfmismatch = cfmismatch or _get_dt(DT_KEYS.ReactorCCool) < -10.0 or (gaining_hc and r_db.mek_status.ccool_fill == 0) cfmismatch = cfmismatch or _get_dt(DT_KEYS.ReactorCCool) < -10.0 or (gaining_hc and r_db.mek_status.ccool_fill == 0)
end end
self.db.annunciator.CoolantFeedMismatch = cfmismatch annunc.CoolantFeedMismatch = cfmismatch
-------------- --#endregion
-- TURBINES --
-------------- --#region Turbines
local turbines_ready = num_turbines == #self.turbines local turbines_ready = num_turbines == #self.turbines
-- clear turbine online flags -- clear turbine online flags
for i = 1, num_turbines do self.db.annunciator.TurbineOnline[i] = false end for i = 1, num_turbines do annunc.TurbineOnline[i] = false end
-- aggregated statistics -- aggregated statistics
local total_flow_rate = 0 local total_flow_rate = 0
@@ -277,10 +280,10 @@ function logic.update_annunciator(self)
-- go through turbines for stats and online -- go through turbines for stats and online
for i = 1, #self.turbines do for i = 1, #self.turbines do
local session = self.turbines[i] ---@type unit_session local session = self.turbines[i] ---@type unit_session
local turbine = session.get_db() ---@type turbinev_session_db local turbine = session.get_db() ---@type turbinev_session_db
self.db.annunciator.RCSFault = self.db.annunciator.RCSFault or (not turbine.formed) or session.is_faulted() annunc.RCSFault = annunc.RCSFault or (not turbine.formed) or session.is_faulted()
-- update ready state -- update ready state
-- - must be formed -- - must be formed
@@ -295,61 +298,48 @@ function logic.update_annunciator(self)
max_water_return_rate = max_water_return_rate + turbine.build.max_water_output max_water_return_rate = max_water_return_rate + turbine.build.max_water_output
self.db.control.blade_count = self.db.control.blade_count + turbine.build.blades self.db.control.blade_count = self.db.control.blade_count + turbine.build.blades
self.db.annunciator.TurbineOnline[session.get_device_idx()] = true annunc.TurbineOnline[session.get_device_idx()] = true
end end
-- check for boil rate mismatch (> 4% error) either between reactor and turbine or boiler and turbine -- check for boil rate mismatch (> 4% error) either between reactor and turbine or boiler and turbine
self.db.annunciator.BoilRateMismatch = math.abs(total_boil_rate - total_input_rate) > (0.04 * total_boil_rate) annunc.BoilRateMismatch = math.abs(total_boil_rate - total_input_rate) > (0.04 * total_boil_rate)
-- check for steam feed mismatch and max return rate -- check for steam feed mismatch and max return rate
local steam_dt_max = util.trinary(num_boilers == 0, ANNUNC_LIMS.SFM_MaxSteamDT_H20, ANNUNC_LIMS.SFM_MaxSteamDT_NA) local steam_dt_max = util.trinary(num_boilers == 0, ANNUNC_LIMS.SFM_MaxSteamDT_H20, ANNUNC_LIMS.SFM_MaxSteamDT_NA)
local water_dt_min = util.trinary(num_boilers == 0, ANNUNC_LIMS.SFM_MinWaterDT_H20, ANNUNC_LIMS.SFM_MinWaterDT_NA) local water_dt_min = util.trinary(num_boilers == 0, ANNUNC_LIMS.SFM_MinWaterDT_H20, ANNUNC_LIMS.SFM_MinWaterDT_NA)
local sfmismatch = math.abs(total_flow_rate - total_input_rate) > ANNUNC_LIMS.SteamFeedMismatch local sfmismatch = math.abs(total_flow_rate - total_input_rate) > ANNUNC_LIMS.SteamFeedMismatch
sfmismatch = sfmismatch or boiler_steam_dt_sum > steam_dt_max or boiler_water_dt_sum < water_dt_min sfmismatch = sfmismatch or boiler_steam_dt_sum > steam_dt_max or boiler_water_dt_sum < water_dt_min
self.db.annunciator.SteamFeedMismatch = sfmismatch annunc.SteamFeedMismatch = sfmismatch
self.db.annunciator.MaxWaterReturnFeed = max_water_return_rate == total_flow_rate and total_flow_rate ~= 0 annunc.MaxWaterReturnFeed = max_water_return_rate == total_flow_rate and total_flow_rate ~= 0
-- turbine safety checks -- turbine safety checks
for i = 1, #self.turbines do for i = 1, #self.turbines do
local turbine = self.turbines[i] ---@type unit_session local turbine = self.turbines[i] ---@type unit_session
local db = turbine.get_db() ---@type turbinev_session_db local db = turbine.get_db() ---@type turbinev_session_db
local idx = turbine.get_device_idx() local idx = turbine.get_device_idx()
-- check if steam dumps are open -- check if steam dumps are open
if db.state.dumping_mode == DUMPING_MODE.IDLE then if db.state.dumping_mode == DUMPING_MODE.IDLE then
self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.OK annunc.SteamDumpOpen[idx] = TRI_FAIL.OK
elseif db.state.dumping_mode == DUMPING_MODE.DUMPING_EXCESS then elseif db.state.dumping_mode == DUMPING_MODE.DUMPING_EXCESS then
self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.PARTIAL annunc.SteamDumpOpen[idx] = TRI_FAIL.PARTIAL
else else
self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.FULL annunc.SteamDumpOpen[idx] = TRI_FAIL.FULL
end end
-- check if turbines are at max speed but not keeping up -- check if turbines are at max speed but not keeping up
self.db.annunciator.TurbineOverSpeed[idx] = (db.state.flow_rate == db.build.max_flow_rate) and (_get_dt(DT_KEYS.TurbineSteam .. idx) > 0.0) annunc.TurbineOverSpeed[idx] = (db.state.flow_rate == db.build.max_flow_rate) and (_get_dt(DT_KEYS.TurbineSteam .. idx) > 0.0)
--[[ -- see notes at cc-mek-scada/wiki/Annunciator-Panels#Generator-Trip
Generator Trip annunc.GeneratorTrip[idx] = (_get_dt(DT_KEYS.TurbinePower .. idx) > 0.0) and (db.tanks.energy_fill > 0.05)
a generator trip is when a generator suddenly and unexpectedly loses it's external load, which occurs when a power plant
is disconnected from the grid. in our case, this is when the turbine is disconnected, or what it's connected to becomes
fully charged. this is identified by detecting if:
- the internal power storage of the turbine is increasing AND
- there is at least 5% energy fill (preventing false trips with periodic power extraction from other mods)
this would then mean there is no external load and there will be a turbine trip soon if this is not resolved
]]--
self.db.annunciator.GeneratorTrip[idx] = (_get_dt(DT_KEYS.TurbinePower .. idx) > 0.0) and (db.tanks.energy_fill > 0.05)
--[[ -- see notes at cc-mek-scada/wiki/Annunciator-Panels#Turbine-Trip
Turbine Trip
a turbine trip is when the turbine stops, which means we are no longer receiving water and lose the ability to cool.
this can be identified by these conditions:
- the current flow rate is 0 mB/t and it should not be
- can initially catch this by detecting a 0 flow rate with a non-zero input rate, but eventually the steam will fill up
- can later identified by presence of steam in tank with a 0 flow rate
]]--
local has_steam = db.state.steam_input_rate > 0 or db.tanks.steam_fill > 0.01 local has_steam = db.state.steam_input_rate > 0 or db.tanks.steam_fill > 0.01
self.db.annunciator.TurbineTrip[idx] = has_steam and db.state.flow_rate == 0 annunc.TurbineTrip[idx] = has_steam and db.state.flow_rate == 0
end end
--#endregion
-- update auto control ready state for this unit -- update auto control ready state for this unit
self.db.control.ready = plc_ready and boilers_ready and turbines_ready self.db.control.ready = plc_ready and boilers_ready and turbines_ready
end end
@@ -577,6 +567,7 @@ end
---@param self _unit_self unit instance ---@param self _unit_self unit instance
function logic.update_status_text(self) function logic.update_status_text(self)
local AISTATE = self.types.AISTATE local AISTATE = self.types.AISTATE
local annunc = self.db.annunciator
-- check if an alarm is active (tripped or ack'd) -- check if an alarm is active (tripped or ack'd)
---@nodiscard ---@nodiscard
@@ -666,13 +657,13 @@ function logic.update_status_text(self)
if plc_db.mek_status.status then if plc_db.mek_status.status then
self.status_text[1] = "ACTIVE" self.status_text[1] = "ACTIVE"
if self.db.annunciator.ReactorHighDeltaT then if annunc.ReactorHighDeltaT then
self.status_text[2] = "core temperature rising" self.status_text[2] = "core temperature rising"
elseif self.db.annunciator.ReactorTempHigh then elseif annunc.ReactorTempHigh then
self.status_text[2] = "core temp high, system nominal" self.status_text[2] = "core temp high, system nominal"
elseif self.db.annunciator.FuelInputRateLow then elseif annunc.FuelInputRateLow then
self.status_text[2] = "insufficient fuel input rate" self.status_text[2] = "insufficient fuel input rate"
elseif self.db.annunciator.WasteLineOcclusion then elseif annunc.WasteLineOcclusion then
self.status_text[2] = "insufficient waste output rate" self.status_text[2] = "insufficient waste output rate"
elseif (util.time_ms() - self.last_rate_change_ms) <= FLOW_STABILITY_DELAY_MS then elseif (util.time_ms() - self.last_rate_change_ms) <= FLOW_STABILITY_DELAY_MS then
self.status_text[2] = "awaiting flow stability" self.status_text[2] = "awaiting flow stability"
@@ -711,7 +702,7 @@ function logic.update_status_text(self)
end end
self.status_text = { "RPS SCRAM", cause } self.status_text = { "RPS SCRAM", cause }
elseif self.db.annunciator.RadiationWarning then elseif annunc.RadiationWarning then
-- elevated, non-hazardous level of radiation is low priority, so display it now if everything else was fine -- elevated, non-hazardous level of radiation is low priority, so display it now if everything else was fine
self.status_text = { "RADIATION DETECTED", "elevated level of radiation" } self.status_text = { "RADIATION DETECTED", "elevated level of radiation" }
else else
@@ -726,7 +717,7 @@ function logic.update_status_text(self)
self.status_text[2] = "core hot" self.status_text[2] = "core hot"
end end
end end
elseif self.db.annunciator.RadiationWarning then elseif annunc.RadiationWarning then
-- in case PLC was disconnected but radiation is present -- in case PLC was disconnected but radiation is present
self.status_text = { "RADIATION DETECTED", "elevated level of radiation" } self.status_text = { "RADIATION DETECTED", "elevated level of radiation" }
else else
@@ -738,6 +729,7 @@ end
---@param self _unit_self unit instance ---@param self _unit_self unit instance
function logic.handle_redstone(self) function logic.handle_redstone(self)
local AISTATE = self.types.AISTATE local AISTATE = self.types.AISTATE
local annunc = self.db.annunciator
-- check if an alarm is active (tripped or ack'd) -- check if an alarm is active (tripped or ack'd)
---@nodiscard ---@nodiscard
@@ -806,7 +798,7 @@ function logic.handle_redstone(self)
----------------------- -----------------------
local enable_emer_cool = self.plc_cache.rps_status.low_cool or local enable_emer_cool = self.plc_cache.rps_status.low_cool or
(self.auto_engaged and self.db.annunciator.CoolantLevelLow and is_active(self.alarms.ReactorOverTemp)) (self.auto_engaged and annunc.CoolantLevelLow and is_active(self.alarms.ReactorOverTemp))
-- don't turn off emergency coolant on sufficient coolant level since it might drop again -- don't turn off emergency coolant on sufficient coolant level since it might drop again
-- turn off once system is OK again -- turn off once system is OK again
@@ -822,7 +814,7 @@ function logic.handle_redstone(self)
end end
end end
if self.db.annunciator.EmergencyCoolant > 1 and self.emcool_opened then if annunc.EmergencyCoolant > 1 and self.emcool_opened then
log.info(util.c("UNIT ", self.r_id, " emergency coolant valve closed")) log.info(util.c("UNIT ", self.r_id, " emergency coolant valve closed"))
log.info(util.c("UNIT ", self.r_id, " turbines set to not dump steam")) log.info(util.c("UNIT ", self.r_id, " turbines set to not dump steam"))
end end
@@ -849,7 +841,7 @@ function logic.handle_redstone(self)
end end
end end
if self.db.annunciator.EmergencyCoolant > 1 and not self.emcool_opened then if annunc.EmergencyCoolant > 1 and not self.emcool_opened then
log.info(util.c("UNIT ", self.r_id, " emergency coolant valve opened")) log.info(util.c("UNIT ", self.r_id, " emergency coolant valve opened"))
log.info(util.c("UNIT ", self.r_id, " turbines set to dump excess steam")) log.info(util.c("UNIT ", self.r_id, " turbines set to dump excess steam"))
end end

View File

@@ -0,0 +1,56 @@
-- add this to psil:
--[[
-- count the number of subscribers in this PSIL instance
---@return integer count
function public.count()
local c = 0
for _, val in pairs(ic) do
for _ = 1, #val.subscribers do c = c + 1 end
end
return c
end
]]--
-- add this to coordinator iocontrol front panel heartbeat function:
--[[
if io.facility then
local count = io.facility.ps.count()
count = count + io.facility.env_d_ps.count()
for x = 1, #io.facility.induction_ps_tbl do
count = count + io.facility.induction_ps_tbl[x].count()
end
for x = 1, #io.facility.sps_ps_tbl do
count = count + io.facility.sps_ps_tbl[x].count()
end
for x = 1, #io.facility.tank_ps_tbl do
count = count + io.facility.tank_ps_tbl[x].count()
end
for i = 1, #io.units do
local entry = io.units[i] ---@type ioctl_unit
count = count + entry.unit_ps.count()
for x = 1, #entry.boiler_ps_tbl do
count = count + entry.boiler_ps_tbl[x].count()
end
for x = 1, #entry.turbine_ps_tbl do
count = count + entry.turbine_ps_tbl[x].count()
end
for x = 1, #entry.tank_ps_tbl do
count = count + entry.tank_ps_tbl[x].count()
end
end
log.debug(count)
end
]]--