Compare commits

...

254 Commits

Author SHA1 Message Date
Mikayla
56377ef595 Merge pull request #322 from MikaylaFischler/devel
2023.08.22 Hotfix 2
2023-08-22 21:49:32 -04:00
Mikayla Fischler
95c300e450 #321 fixed boiler flow indicators on flow monitor 2023-08-22 21:46:34 -04:00
Mikayla
2985898b7e Merge pull request #320 from MikaylaFischler/devel
2023.08.22 Hotfix
2023-08-22 20:05:53 -04:00
Mikayla Fischler
57d50e6745 #319 updated installer version 2023-08-22 19:56:47 -04:00
Mikayla Fischler
3dc1a06969 #319 fixed installer bug on fresh install 2023-08-22 19:55:34 -04:00
Mikayla Fischler
fcba935240 updated readme with new installer 2023-08-22 19:13:34 -04:00
Mikayla Fischler
9b32bb4675 deleted legacy install manifest for v1.0 installer 2023-08-22 19:06:36 -04:00
Mikayla
f59f484e7b Merge pull request #317 from MikaylaFischler/devel
2023.08.22 Release
2023-08-22 18:42:21 -04:00
Mikayla Fischler
0fe9b391d8 #313 installer self-update fix, added update command for it 2023-08-21 22:47:00 -04:00
Mikayla
97f0191875 #313 installer self update 2023-08-22 02:18:25 +00:00
Mikayla Fischler
70db8d782c fixed unit dynamic tank state indicator 2023-08-21 22:05:02 -04:00
Mikayla
2acd166c3e Merge pull request #316 from MikaylaFischler/232-waste-valve-and-flow-monitoring-display
232 Waste Valve and Flow Monitoring Display
2023-08-21 21:54:22 -04:00
Mikayla Fischler
c78f7e173a #232 cleanup, changed antimatter rate to be integer on main display 2023-08-21 21:53:31 -04:00
Mikayla Fischler
99a0b0a55a #232 documentation and refactor 2023-08-21 21:44:15 -04:00
Mikayla Fischler
6e51e70b62 #232 cleanup and fixes 2023-08-21 21:37:56 -04:00
Mikayla Fischler
fd2abad5cf changed some green/red indicators to be green/gray for contrast 2023-08-21 21:35:32 -04:00
Mikayla Fischler
b93c6b7c6e fixes per luacheck 2023-08-20 23:53:49 -04:00
Mikayla Fischler
8b3f558f68 Merge branch 'devel' into 232-waste-valve-and-flow-monitoring-display 2023-08-20 23:43:07 -04:00
Mikayla Fischler
8c5289867c #232 updated coordinator monitor disconnect/reconnect handling for changes 2023-08-20 23:28:48 -04:00
Mikayla Fischler
d179920565 #232 option to disable flow view screen for legacy setups 2023-08-20 23:23:23 -04:00
Mikayla Fischler
504ee0594f #315 switch off dynamic tank fill mode if emergency coolant is required 2023-08-20 22:56:51 -04:00
Mikayla Fischler
a92f182156 #232 fixed incorrect arrows on turbine flow view 2023-08-20 22:53:14 -04:00
Mikayla Fischler
c5d38a5584 #232 added container mode indicators for tanks 2023-08-20 17:30:34 -04:00
Mikayla Fischler
9bf07e6c3e completed work on updated pipenet 2023-08-20 17:04:14 -04:00
Mikayla Fischler
7656936982 #232 cleanup, added general stats 2023-08-20 16:52:12 -04:00
Mikayla Fischler
f477ad9426 #232 re-indexed valve IDs 2023-08-19 23:28:03 -04:00
Mikayla Fischler
59950e9d15 #232 connected valve indicators 2023-08-19 23:24:20 -04:00
Mikayla Fischler
11d86d92eb #232 bugfixes and linked up indicators to data 2023-08-19 20:06:37 -04:00
Mikayla Fischler
1275f61113 #232 refactor and fixed sv config verify 2023-08-19 13:42:07 -04:00
Mikayla Fischler
d17e2b8321 #232 completed display of flow/dynamic tank/sps, dynamically sized 2023-08-19 13:38:05 -04:00
Mikayla Fischler
ce780c3d72 added common color pairs to coordinator style 2023-08-13 00:51:37 -04:00
Mikayla Fischler
76ab4e17bf #232 WIP full flow view drawn out 2023-08-13 00:11:58 -04:00
Mikayla Fischler
ac1733c46e #314 20s grace period for coordinator render to finish to prevent timeouts 2023-08-12 15:16:37 -04:00
Mikayla
17731de61b #312 improved reactor peripheral handling 2023-08-11 14:20:13 +00:00
Mikayla Fischler
d85385c1fe #232 continued work on flow monitor, added SPS display 2023-08-10 23:31:38 -04:00
Mikayla Fischler
e0809f52a6 #232 WIP coordinator flow view 2023-08-09 23:26:06 -04:00
Mikayla Fischler
b2c55f9d4b #303 check modem message distance for nil 2023-08-02 10:13:54 -04:00
Mikayla
ba896ea163 Merge pull request #302 from MikaylaFischler/common-cleanup
Common Cleanup
2023-07-30 21:38:18 -04:00
Mikayla Fischler
1a64591256 #282 version the common directory 2023-07-30 20:46:04 -04:00
Mikayla Fischler
9ce75eb4bd #283 common cleanup, added lockbox version to crash dump, changed crash handler to pcall requires for graphics/lockbox 2023-07-30 12:24:54 -04:00
Mikayla
451f804f87 Merge pull request #301 from MikaylaFischler/rtu-speaker-system
RTU Speaker System and Pocket Diagnostics
2023-07-30 00:14:49 -04:00
Mikayla Fischler
724d13510d optimizations and cleanup for pull request 2023-07-30 00:13:26 -04:00
Mikayla Fischler
3f01ce7ec5 lowered SVS queue process time limit warning to debug level 2023-07-29 18:44:17 -04:00
Mikayla Fischler
df67795239 #290 pocket page management and alarm test tool, supervisor pocket diagnostics system 2023-07-29 18:16:59 -04:00
Mikayla Fischler
775d4dc95b #264 improvements to RTU speaker sounder 2023-07-29 17:57:51 -04:00
Mikayla Fischler
b3c7263bc4 #299 fixed mouse events passing to hidden elements 2023-07-29 00:25:20 -04:00
Mikayla Fischler
9f8732830c #264, #280 fixed sounder issues 2023-07-26 22:37:25 -04:00
Mikayla Fischler
1c87ef18a1 #297 added tone packet to valid MGMT packet types 2023-07-26 21:33:43 -04:00
Mikayla Fischler
f111b711c5 #264, #280 send tones to RTUs 2023-07-26 21:02:34 -04:00
Mikayla Fischler
92d1945bea #264 WIP RTU alarm sounders 2023-07-26 20:48:44 -04:00
Mikayla Fischler
4192ea426c #280 moved alarm sounder logic to supervisor and tone control to common 2023-07-26 20:48:11 -04:00
Mikayla
7bd8f34773 update README.md 2023-07-19 20:06:13 -04:00
Mikayla
bdbb3071b3 Merge pull request #294 from MikaylaFischler/devel
2023.07.19 Hotfix
2023-07-19 19:18:48 -04:00
Mikayla Fischler
def02a94d2 #293 fixed race condition with graphics element IDs 2023-07-19 11:27:33 -04:00
Mikayla Fischler
681bb0963e #291 RTU comms thread no longer yields every packet 2023-07-18 22:28:43 -04:00
Mikayla
8f7d7c3ead Merge pull request #288 from MikaylaFischler/devel
2023.07.17 Hotfix
2023-07-17 22:48:03 -04:00
Mikayla Fischler
c0f45cfb8b updated comments 2023-07-17 22:47:19 -04:00
Mikayla Fischler
455653074a #287 fixed coordinator not notifying supervisor of auto waste config 2023-07-17 22:09:21 -04:00
Mikayla Fischler
1202289fab #285 #286 mitigated false trips 2023-07-17 20:59:45 -04:00
Mikayla
acb7b5b4cb update README.md with new installer pastebin 2023-07-16 21:29:41 -04:00
Mikayla
9bd79dacad Merge pull request #281 from MikaylaFischler/devel
2023.07.16 Release
2023-07-16 21:25:00 -04:00
Mikayla Fischler
c544d140bf installer key handling improvements 2023-07-16 21:21:33 -04:00
Mikayla Fischler
353cb3622b improved installer any key detection 2023-07-16 21:11:27 -04:00
Mikayla Fischler
b54f15bad6 #274 bugfixes and optimizations 2023-07-16 21:07:37 -04:00
Mikayla Fischler
4d9783beca fixed installer clean bug 2023-07-16 20:58:34 -04:00
Mikayla Fischler
5529774b0e changed installer press enter to continue to any key, fixed some text colors 2023-07-16 20:53:39 -04:00
Mikayla Fischler
2a541ef3fe #274 cleanup functionality added to installer 2023-07-16 19:42:20 -04:00
Mikayla Fischler
e1b4d72ef8 updated legacy install manifest 2023-07-15 13:33:51 -04:00
Mikayla Fischler
6a0992c7a4 removed unused variable 2023-07-15 13:33:18 -04:00
Mikayla Fischler
cff7c724be #272 fixed bug with transmitting unit dynamic tank table 2023-07-15 13:31:48 -04:00
Mikayla Fischler
47bda73afe #272 basic dynamic tank data in supervisor and coordinator 2023-07-15 13:16:36 -04:00
Mikayla
8daedc109c added discord info to readme 2023-07-13 13:50:17 -04:00
Mikayla Fischler
a164c18a50 removed unused fields from dynamic tank rtu 2023-07-13 12:19:25 -04:00
Mikayla Fischler
4d663ada8d added high contrast yellow to rtu/plc/coord front panels 2023-07-12 13:38:37 -04:00
Mikayla Fischler
084a153a79 #268 fixed incorrect info print on extra wireless modem connection 2023-07-11 21:06:47 -04:00
Mikayla Fischler
4ed6ec1c63 correctly print new messages without overwrites in dmesg even if a prior message is a progress one 2023-07-11 21:01:24 -04:00
Mikayla Fischler
d3c2ba7bee update install manifest 2023-07-11 20:32:37 -04:00
Mikayla Fischler
55ff9dad4b #249 coordinator handle monitor disconnects/reconnects 2023-07-11 20:32:10 -04:00
Mikayla Fischler
0d6022f5e3 fixed always reporting failure to connect to supervisor even when inaccurate 2023-07-11 18:31:53 -04:00
Mikayla Fischler
8b136d78a8 #268 better handling of wireless modem peripherals 2023-07-11 18:22:09 -04:00
Mikayla Fischler
a5214730ef #260 added dynamic tank RTU 2023-07-11 17:27:03 -04:00
Mikayla Fischler
9f3ad3caf0 removed PLC establish packet handling when already linked 2023-07-11 16:17:24 -04:00
Mikayla
9bb2a99be5 Merge pull request #279 from MikaylaFischler/265-coordinator-front-panel
265 coordinator front panel
2023-07-11 15:37:17 -04:00
Mikayla Fischler
65ace26258 corrected comments 2023-07-11 15:36:41 -04:00
Mikayla Fischler
61d975d13f updated error messages for consistency 2023-07-11 15:15:44 -04:00
Mikayla Fischler
1d7d6e9817 update legacy install manifest 2023-07-11 13:38:21 -04:00
Mikayla Fischler
a2e0999cea combine coordinator supervisor connection event loop with main loop 2023-07-11 13:32:26 -04:00
Mikayla Fischler
1edee7f64b updated graphics comments 2023-07-09 23:42:44 -04:00
Mikayla Fischler
df61ec2c62 #265 coordinator front panel 2023-07-09 23:31:56 -04:00
Mikayla Fischler
bf7a316b04 don't start flasher if already started 2023-07-09 23:24:41 -04:00
Mikayla Fischler
96c4444184 corrected some comments 2023-07-09 23:22:24 -04:00
Mikayla Fischler
59eac62c33 #270 validate reactor PLC status packet types 2023-07-08 18:07:40 -04:00
Mikayla
ab193db153 Merge pull request #277 from MikaylaFischler/25-process-waste-control
25 process waste control
2023-07-08 17:12:23 -04:00
Mikayla Fischler
7d65bba589 fixes/cleanups for pull request 2023-07-08 17:11:51 -04:00
Mikayla Fischler
dcef5a96f0 removed unused function 2023-07-08 16:57:41 -04:00
Mikayla Fischler
ba0900ac65 #25 sna/sps integration, plutonium fallback, waste rate reporting 2023-07-08 16:57:13 -04:00
Mikayla Fischler
8f54e95519 #25 continued WIP waste control, main view updated and unit fields modified 2023-07-06 01:36:06 -04:00
Mikayla Fischler
7b9824b6f9 added checkbox graphics element 2023-07-01 19:40:33 -04:00
Mikayla Fischler
b6835fc7d1 #276 updated readme 2023-06-29 16:54:46 -04:00
Mikayla
bc5a94cd3b Update README.md 2023-06-29 12:57:25 -04:00
Mikayla
2a3d868402 Merge pull request #273 from MikaylaFischler/devel
2023.06.29 Release
2023-06-29 12:33:12 -04:00
Mikayla Fischler
b998634da1 installer fixes 2023-06-29 12:29:30 -04:00
Mikayla
5225380523 Merge pull request #271 from MikaylaFischler/51-hmac-message-authentication
HMAC Message Authentication
2023-06-27 19:09:38 -04:00
Mikayla Fischler
0e7ea7102c removed extra verbose comment in configs 2023-06-27 19:08:33 -04:00
Mikayla Fischler
8924ba4e99 cleanup and luacheck fixes 2023-06-27 19:05:51 -04:00
Mikayla Fischler
a8071db08e #51 send serialized data to properly MAC 2023-06-27 18:36:16 -04:00
Mikayla Fischler
fb3c7ded06 updated lockbox benchmark 2023-06-26 20:44:55 -04:00
Mikayla Fischler
f6b0a49904 added graphics version to crash dump 2023-06-26 14:03:36 -04:00
Mikayla Fischler
bfbbfb164b #51 include versioned lockbox in installer, reduced installer file size 2023-06-25 17:53:02 -04:00
Mikayla Fischler
57763702ff #51 init mac component from config key 2023-06-25 14:00:18 -04:00
Mikayla Fischler
f469754bb7 #51 network file cleanup 2023-06-25 13:06:03 -04:00
Mikayla Fischler
336662de62 #51 nic integration with rtu and supervisor 2023-06-25 12:59:38 -04:00
Mikayla Fischler
9073009eb0 #51 usage of nic.receive and some cleanup 2023-06-23 14:12:41 -04:00
Mikayla Fischler
ffac6996ed #51 PLC changes for new networking 2023-06-23 13:52:24 -04:00
Mikayla Fischler
da3c92b3bf Merge branch 'devel' into 51-hmac-message-authentication 2023-06-22 16:05:46 -04:00
Mikayla
712c7a8f3b #266 added health check to ppm and strengthened reliability of RTU hw state reporting 2023-06-22 19:46:17 +00:00
Mikayla
737afe586d renamed lockbox benchmark 2023-06-22 14:22:32 +00:00
Mikayla
d69796b607 lockbox benchmark cleanup 2023-06-22 14:21:00 +00:00
Mikayla
1cdf66a8c3 #51 WIP network interface controller 2023-06-21 23:04:39 +00:00
Mikayla
282c7db3eb Merge branch 'devel' into 51-hmac-message-authentication 2023-06-18 19:23:56 +00:00
Mikayla Fischler
a02529b9f7 #263 fixed bug with supervisor group map length not matching number of reactors 2023-06-18 15:19:01 -04:00
Mikayla Fischler
af38025f50 #262 don't ever abort RTU unit parsing on error, just skip 2023-06-18 14:26:38 -04:00
Mikayla Fischler
b28e4d1e95 #258 installer bugfix 2023-06-18 14:04:49 -04:00
Mikayla Fischler
75dfa3ae73 #258 luacheck fix 2023-06-18 13:16:28 -04:00
Mikayla Fischler
4a3455fa60 #258 luacheck fix 2023-06-18 13:13:34 -04:00
Mikayla Fischler
a2fa6570dc #258 installer improvement 2023-06-18 13:12:34 -04:00
Mikayla Fischler
aef8281ad6 #258 more installer fixes 2023-06-18 01:19:00 -04:00
Mikayla Fischler
d42327a20d #258 bugfixes 2023-06-18 01:09:46 -04:00
Mikayla Fischler
49db75f34d #258 installer bugfix 2023-06-18 01:04:40 -04:00
Mikayla Fischler
bc87030491 #258 installer improvements and test change to graphics version 2023-06-18 00:48:06 -04:00
Mikayla Fischler
9266d7d8e1 #258 versioned graphics component 2023-06-18 00:40:01 -04:00
Mikayla
ef5567ad46 #51 hmac verification 2023-06-11 18:26:55 +00:00
Mikayla Fischler
302f3d913f unlikely to use ldoc due to incompatibilities with vscode lua extension luadocs 2023-06-08 11:39:07 -04:00
Mikayla Fischler
650b9c1811 #244 luadoc actions fixes 2023-06-08 10:58:06 -04:00
Mikayla Fischler
543ac8c9fe updated ldoc version 2023-06-08 10:50:39 -04:00
Mikayla Fischler
7f19f76c0b update comment to force re-run 2023-06-08 10:46:35 -04:00
Mikayla
8d76c86309 fixed pages.yml format error 2023-06-08 10:42:49 -04:00
Mikayla Fischler
a4be6a6dde #244 luadoc in github actions 2023-06-08 10:41:44 -04:00
Mikayla Fischler
8b926a0978 #257 tick supervisor version to force installers to re-pull graphics 2023-06-07 21:42:21 -04:00
Mikayla Fischler
775ffc8094 added graphics to supervisor depends 2023-06-07 21:25:42 -04:00
Mikayla
13a8435f6c Merge pull request #256 from MikaylaFischler/devel
2023.06.07 Hotfix
2023-06-07 18:42:29 -04:00
Mikayla Fischler
5d6dda5619 #255 fixed rectangle element not offsetting mouse events 2023-06-07 18:38:00 -04:00
Mikayla
f8221ad0f1 update README.md with new pastebin link 2023-06-07 18:16:36 -04:00
Mikayla
193aeed6df Merge pull request #254 from MikaylaFischler/devel
2023.06.07 Release
2023-06-07 17:46:50 -04:00
Mikayla Fischler
8c87cb3e26 actions fixed maybe 2023-06-07 16:07:12 -04:00
Mikayla Fischler
1548cd706d actions test 6 2023-06-07 16:04:39 -04:00
Mikayla Fischler
996272e108 actions test 5 2023-06-07 16:01:04 -04:00
Mikayla Fischler
ef673bdf1b actions test 4 2023-06-07 15:57:07 -04:00
Mikayla Fischler
7aa236e987 actions test 3 2023-06-07 15:55:46 -04:00
Mikayla Fischler
35d857a5f4 actions testing 2 2023-06-07 15:51:54 -04:00
Mikayla Fischler
c2c87ec6c6 github actions testing 2023-06-07 15:45:37 -04:00
Mikayla Fischler
5ce54d78e1 github actions 2023-06-07 15:44:07 -04:00
Mikayla Fischler
c05a312f6c actions testing 2023-06-07 15:43:03 -04:00
Mikayla Fischler
86325d9527 more possible actions fixes 2023-06-07 15:37:48 -04:00
Mikayla Fischler
5074ca89f0 possible actions fix? 2023-06-07 15:35:25 -04:00
Mikayla Fischler
c22b048608 actions fix maybe 2023-06-07 15:28:49 -04:00
Mikayla Fischler
7ae3014e06 updated step conditions 2023-06-07 15:24:44 -04:00
Mikayla Fischler
8fa37cc9be manifest update: don't clean on checkout 2023-06-07 15:17:05 -04:00
Mikayla Fischler
2c730fbdc2 update to manifest generation to skip failed branches 2023-06-07 15:11:42 -04:00
Mikayla Fischler
5c21140025 updated manifest generation to include all data in each go 2023-06-07 15:05:36 -04:00
Mikayla Fischler
7859e5ea4c updated old install manifest 2023-06-07 14:55:56 -04:00
Mikayla
0b5ee8eabc Merge pull request #252 from MikaylaFischler/225-consolidate-network-channels
225 Consolidate Network Channels
2023-06-07 14:27:10 -04:00
Mikayla Fischler
1decd88415 last few cleanups 2023-06-07 14:22:35 -04:00
Mikayla Fischler
f1b1f0b75a updated supervisor front panel default computer ID place holders and fixed PDG establish using channel in messages 2023-06-07 14:18:13 -04:00
Mikayla Fischler
f37f2f009f comms only set nil max distance if requested max is exactly 0 2023-06-07 14:17:10 -04:00
Mikayla Fischler
15b071378c #225 removed redundant checks on remote address, added clarity to log messages 2023-06-07 12:48:43 -04:00
Mikayla Fischler
5ba06dcdaf #225 log message fixes and sv addr checks for RTU 2023-06-07 12:35:17 -04:00
Mikayla Fischler
f4e7137eb3 #225 pocket verify packets are from linked computer 2023-06-07 12:27:13 -04:00
Mikayla Fischler
cf881548d7 config file comments 2023-06-07 12:25:50 -04:00
Mikayla Fischler
0a6fd35f93 supervisor front panel computer IDs cleanup 2023-06-06 21:56:17 -04:00
Mikayla Fischler
671f8b55bc updated supervisor front panel RTT coloring limits 2023-06-06 19:49:28 -04:00
Mikayla Fischler
e16b0d237e #225 fixed svsessions __tostring for sessions, refactored s_addr to src_addr 2023-06-06 19:45:04 -04:00
Mikayla Fischler
55dab6d675 don't print brackets in util.strval if metatable __tostring is present 2023-06-06 19:41:55 -04:00
Mikayla Fischler
0f5ae9a756 #225 coordinator changes for new comms 2023-06-06 19:41:09 -04:00
Mikayla Fischler
cdff7af431 #225 pocket properly handle disconnects and address validation 2023-06-05 20:59:28 -04:00
Mikayla Fischler
c536b823e7 #225 PLC/RTUs drop incoming packets from devices other than the configured supervisor while linked 2023-06-05 19:12:43 -04:00
Mikayla Fischler
b20d42ff38 #225 added computer IDs to PLC/RTU front panels, updated supervisor front panel to use computer ID terminology 2023-06-05 18:10:53 -04:00
Mikayla Fischler
63147bfab5 #248 fixed network light not going out on PLC/RTU when disconnected 2023-06-05 17:47:43 -04:00
Mikayla Fischler
360609df1f #225 network changes for supervisor sessions 2023-06-05 17:24:00 -04:00
Mikayla Fischler
9a5fc92c86 Merge branch 'devel' into 225-consolidate-network-channels 2023-06-05 01:18:13 -04:00
Mikayla
337fca7e7c #225 work in progress comms changes 2023-06-05 05:13:22 +00:00
Mikayla Fischler
38fc7189ba #245 fixed coordinator missing pocket packets and connection timeouts 2023-06-03 18:51:59 -04:00
Mikayla Fischler
0b939be412 #231 renamed unit a_ functions to auto_ 2023-06-03 17:59:20 -04:00
Mikayla Fischler
351842c9a1 updated RTU to say STATUS instead of POWER on first LED 2023-06-03 17:44:32 -04:00
Mikayla Fischler
8d248408d4 #247 renamed tcallbackdsp to tcd and added handler to RTU 2023-06-03 17:40:57 -04:00
Mikayla
2427561dc5 Merge pull request #246 from MikaylaFischler/front-panels
Supervisor Front Panel
2023-06-03 17:32:37 -04:00
Mikayla Fischler
b4932b33b6 code cleanup 2023-06-03 17:31:06 -04:00
Mikayla Fischler
24a7275543 fixed trailing whitespace 2023-06-03 15:50:44 -04:00
Mikayla Fischler
529371a0fd #184 support supervisor running without front panel, halved heartbeat blink rate 2023-06-03 15:45:48 -04:00
Mikayla Fischler
69df5edbeb #184 RTU and pocket lists on supervisor front panel, element delete() bugfix 2023-06-03 14:33:08 -04:00
Mikayla Fischler
153a83e569 listbox improvements 2023-06-01 13:00:45 -04:00
Mikayla Fischler
ef1ec220a4 #184 initial draft of listbox element and associated supervisor front panel test example 2023-05-31 11:44:41 -04:00
Mikayla Fischler
8f2e9fe319 more manifest.yml fixes 2023-05-31 11:19:32 -04:00
Mikayla Fischler
86ad2a1069 fixed a typo in manifest.yml 2023-05-31 11:17:44 -04:00
Mikayla Fischler
494dc437a5 fixed error in manifest.yml 2023-05-31 11:16:56 -04:00
Mikayla Fischler
deec1ff1df fix shields deploy 2023-05-31 11:15:55 -04:00
Mikayla Fischler
4c35233289 #243 changed rectangle to use content window, significant simplification of offset logic, improved delete rendering 2023-05-30 20:43:33 -04:00
Mikayla Fischler
de9cb3bd3a #243 graphics core updates for content windows, redrawing, and handling of addition/removal of children 2023-05-30 19:51:10 -04:00
Mikayla Fischler
270726e276 Merge branch 'devel' into front-panels 2023-05-30 00:30:08 -04:00
Mikayla Fischler
dbd74afbe6 #242 update per luacheck 2023-05-27 23:53:20 -04:00
Mikayla Fischler
37a91986e5 #242 updated installer for github pages manifest 2023-05-27 23:50:00 -04:00
Mikayla Fischler
a892c0cf41 #242 create manifest.yml 2023-05-27 23:36:32 -04:00
Mikayla Fischler
b7d90872d5 Merge branch 'devel' into front-panels 2023-05-25 17:48:20 -04:00
Mikayla Fischler
82ab85daa5 updated install manifest hotfix 2023.05.23 2023-05-25 17:41:44 -04:00
Mikayla Fischler
f9aa75a105 graphics element hidden on creation option, changed hide/show logic to only hide/show current element 2023-05-25 17:40:16 -04:00
Mikayla Fischler
e313b77abc Merge branch 'devel' into front-panels 2023-05-23 20:25:19 -04:00
Mikayla
a14ffea6f0 Merge pull request #239 from MikaylaFischler/devel
2023.05.23 Hotfix
2023-05-23 19:54:35 -04:00
Mikayla Fischler
43a0ff86d7 #238 bugfix for push button and sidebar in bounds checks 2023-05-23 19:51:48 -04:00
Mikayla Fischler
ece7c0fe9a #184 supervisor graphics updates for new system, added PLC and CRD pages on supervisor front panel 2023-05-23 19:22:22 -04:00
Mikayla
97cee58e5a Merge pull request #236 from MikaylaFischler/devel
2023.05.22 Release
2023-05-22 10:06:18 -04:00
Mikayla Fischler
4aba79f232 Merge branch 'devel' into front-panels 2023-05-20 09:44:26 -04:00
Mikayla Fischler
b8c81e2e70 Merge branch 'graphics-rearchitect' into devel 2023-05-20 08:38:29 -04:00
Mikayla Fischler
142f2c363a #234 made debug config setting optional, defaults to false 2023-05-19 19:12:27 -04:00
Mikayla
de99169db8 Merge pull request #235 from MikaylaFischler/graphics-rearchitect
Graphics Rearchitect: Part 2
2023-05-19 18:15:11 -04:00
Mikayla Fischler
d5446f970b updated install manifest and removed early ref to listbox 2023-05-19 17:38:55 -04:00
Mikayla Fischler
792cb46ce6 resolved register simplification 2023-05-19 17:38:08 -04:00
Mikayla Fischler
86615b03ff fixed unused variable 2023-05-18 20:42:15 -04:00
Mikayla Fischler
d5fe790c86 #227 move graphics windows 2023-05-18 20:21:23 -04:00
Mikayla Fischler
beda7624f4 #233 fixed mouse enter/exit behavior via simplification 2023-05-18 10:58:42 -04:00
Mikayla Fischler
82e3fa494c #229 pocket changes for UI element register change 2023-05-14 19:13:44 -04:00
Mikayla Fischler
466902371a #229 coordinator changes for UI element register change 2023-05-14 19:13:12 -04:00
Mikayla Fischler
e763af9981 #229 PLC changes for UI element register change 2023-05-13 09:43:42 -04:00
Mikayla Fischler
b2115fd077 #229 element PSIL register/deletion, changes for RTU to use new PSIL register 2023-05-13 08:50:13 -04:00
Mikayla Fischler
36bd2c5e08 enabled debug logs on turbine modbustest 2023-05-12 13:52:42 -04:00
Mikayla Fischler
f6610489c2 #224 fix for RTU unit indexing on supervisor when virtual units were present 2023-05-11 20:54:43 -04:00
Mikayla Fischler
e159dbb850 #184 updated supervisor for new mouse events 2023-05-11 20:06:41 -04:00
Mikayla Fischler
513c72ea79 Merge branch 'devel' into front-panels 2023-05-11 20:02:42 -04:00
Mikayla Fischler
a81fd49604 updated manifest 2023-05-11 20:01:04 -04:00
Mikayla
b430a22f08 Merge pull request #230 from MikaylaFischler/graphics-rearchitect
Graphics Rearchitect Part 1: Mouse Events
2023-05-11 19:59:52 -04:00
Mikayla
a220713385 Merge branch 'devel' into graphics-rearchitect 2023-05-11 19:57:57 -04:00
Mikayla Fischler
fac9a8d104 updated install manifest 2023-05-11 19:56:45 -04:00
Mikayla Fischler
0783c4c01f #226 bugfixes and pocket mouse events 2023-05-11 19:55:02 -04:00
Mikayla Fischler
676dfc8c22 #226 mouse events in coordinator 2023-05-10 20:01:06 -04:00
Mikayla
50c0a4a3eb #222 added debug log enable to configs 2023-05-10 20:57:23 +00:00
Mikayla
032284e90d #224 skip virtual RTU units when parsing advertisements instead of aborting 2023-05-10 20:40:52 +00:00
Mikayla
3a0d677c16 #226 updated PLC/RTU front panels to use new mouse events 2023-05-10 19:21:54 +00:00
Mikayla Fischler
2c2f936232 #226 updated the other controls for new mouse events, added tabbar control 2023-05-10 11:46:06 -04:00
Mikayla Fischler
4ef1915137 #226 multi button updated for new graphics mouse events 2023-05-10 11:08:24 -04:00
Mikayla Fischler
40fa0de7a3 #226 hazard and push buttons updated for new graphics mouse events 2023-05-10 10:56:56 -04:00
Mikayla Fischler
b8a8da1ac4 #226 graphics core changes for mouse events 2023-05-09 20:29:07 -04:00
Mikayla
e26dc905f8 #226 updated mouse events WIP 2023-05-07 01:27:36 +00:00
Mikayla Fischler
c7edd8c487 updated install manifest after luacheck changes 2023-05-05 14:12:35 -04:00
Mikayla Fischler
d3249da102 removed check.yml comment about -a 2023-05-05 14:11:15 -04:00
Mikayla Fischler
0e1f23efe8 fixed luacheck comments 2023-05-05 14:09:50 -04:00
Mikayla Fischler
5a139c2dd6 possible luacheck fixes 2023-05-05 14:07:15 -04:00
Mikayla Fischler
30ba8bdccf luacheck fixes continued 2023-05-05 14:04:28 -04:00
Mikayla Fischler
b2e21cb6d9 luacheck fixes 2023-05-05 14:02:25 -04:00
Mikayla Fischler
8064b33a36 some luacheck fixes 2023-05-05 13:55:14 -04:00
Mikayla Fischler
7e33f22577 luacheck suppression attempt 2023-05-05 13:15:17 -04:00
Mikayla Fischler
464451c378 unused vararg suppression, re-enable unused args luacheck 2023-05-05 13:09:53 -04:00
Mikayla Fischler
0778a442b1 diagnostic suppression 2023-05-05 13:07:48 -04:00
Mikayla Fischler
2c7b98ba42 #184 WIP supervisor front panel 2023-05-05 13:04:13 -04:00
Mikayla Fischler
ff9a18a019 rtu log message cleanup 2023-04-29 23:49:04 -04:00
Mikayla Fischler
81005d3e2c pocket cleanup 2023-04-29 23:48:50 -04:00
164 changed files with 9931 additions and 6431 deletions

View File

@@ -22,11 +22,10 @@ jobs:
uses: lunarmodules/luacheck@v1.1.0 uses: lunarmodules/luacheck@v1.1.0
with: with:
# Argument Explanations # Argument Explanations
# -a = Disable warning for unused arguments
# -i 121 = Setting a read-only global variable # -i 121 = Setting a read-only global variable
# 512 = Loop can be executed at most once # 512 = Loop can be executed at most once
# 542 = An empty if branch # 542 = An empty if branch
# --no-max-line-length = Disable warnings for long line lengths # --no-max-line-length = Disable warnings for long line lengths
# --exclude-files ... = Exclude lockbox library (external) and config files # --exclude-files ... = Exclude lockbox library (external) and config files
# --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os' # --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os'
args: . --no-max-line-length -a -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http parallel periphemu peripheral read rs settings shell term textutils window args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http parallel periphemu peripheral read rs settings shell term textutils window

95
.github/workflows/manifest.yml vendored Normal file
View File

@@ -0,0 +1,95 @@
# Deploy installation manifests and shields versions
name: Deploy Installation Data
on:
workflow_dispatch:
push:
branches:
- main
- latest
- devel
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Setup Python
uses: actions/setup-python@v3.1.3
# Generate manifest + shields files for main branch
- name: Checkout main
id: checkout-main
uses: actions/checkout@v3
with:
ref: 'main'
clean: false
- name: Create outputs folders
if: success() || failure()
shell: bash
run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/latest deploy/manifests/devel
- name: Generate manifest and shields for main branch
id: manifest-main
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
run: python imgen.py shields
- name: Save main's manifest
if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/main
# Generate manifest for latest branch
- name: Checkout latest
id: checkout-latest
if: success() || failure()
uses: actions/checkout@v3
with:
ref: 'latest'
clean: false
- name: Generate manifest for latest
id: manifest-latest
if: ${{ (success() || failure()) && steps.checkout-latest.outcome == 'success' }}
run: python imgen.py
- name: Save latest's manifest
if: ${{ (success() || failure()) && steps.manifest-latest.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/latest
# Generate manifest for devel branch
- name: Checkout devel
id: checkout-devel
if: success() || failure()
uses: actions/checkout@v3
with:
ref: 'devel'
clean: false
- name: Generate manifest for devel
id: manifest-devel
if: ${{ (success() || failure()) && steps.checkout-devel.outcome == 'success' }}
run: python imgen.py
- name: Save devel's manifest
if: ${{ (success() || failure()) && steps.manifest-devel.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/devel
# All artifacts ready now, upload deploy directory
- name: Upload artifacts
id: upload-artifacts
if: ${{ (success() || failure()) && (steps.manifest-main.outcome == 'success' || steps.manifest-latest.outcome == 'success' || steps.manifest-devel.outcome == 'success') }}
uses: actions/upload-pages-artifact@v1
with:
# Upload manifest JSON
path: 'deploy/'
- name: Deploy to GitHub Pages
if: ${{ (success() || failure()) && steps.upload-artifacts.outcome == 'success' }}
id: deployment
uses: actions/deploy-pages@v2

View File

@@ -1,47 +0,0 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy Component Versions
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Setup Python
uses: actions/setup-python@v3.1.3
- run: mkdir shields
- run: python imgen.py shields
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
# Upload shields JSON
path: 'shields/'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2

2
.gitignore vendored
View File

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

View File

@@ -7,6 +7,29 @@ Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fi
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=latest&label=latest) ![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=latest&label=latest)
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=devel&label=devel) ![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=devel&label=devel)
### [Join](https://discord.gg/R9NSCkhcwt) the Discord!
![Discord](https://img.shields.io/discord/1129075839288496259)
## Released Component Versions
![Installer](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Finstaller.json)
![Bootloader](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fbootloader.json)
![Comms](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fcomms.json)
![Graphics](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fgraphics.json)
![Lockbox](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Flockbox.json)
![Reactor PLC](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Freactor-plc.json)
![RTU](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Frtu.json)
![Supervisor](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fsupervisor.json)
![Coordinator](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fcoordinator.json)
![Pocket](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fpocket.json)
## Requirements
Mod Requirements: Mod Requirements:
- CC: Tweaked - CC: Tweaked
- Mekanism v10.1+ - Mekanism v10.1+
@@ -16,32 +39,11 @@ Mod Recommendations:
v10.1+ is required due the complete support of CC:Tweaked added in Mekanism v10.1 v10.1+ is required due the complete support of CC:Tweaked added in Mekanism v10.1
There was also an apparent bug with boilers disconnecting and reconnecting when active in my test world on 10.0.24, so it may not even have been an option to fully implement this with support for 10.0.
## Released Component Versions
### Core
![Bootloader](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fbootloader.json)
![Comms](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fcomms.json)
### Utilities
![Installer](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Finstaller.json)
### Applications
![Reactor PLC](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Freactor-plc.json)
![RTU](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Frtu.json)
![Supervisor](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fsupervisor.json)
![Coordinator](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fcoordinator.json)
![Pocket](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fpocket.json)
## Installation ## Installation
You can install this on a ComputerCraft computer using either: You can install this on a ComputerCraft computer using either:
* `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua` * `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua`
* `pastebin get iUMjgW0C ccmsi.lua` * `pastebin get sqUN6VUb ccmsi.lua`
## [SCADA](https://en.wikipedia.org/wiki/SCADA) ## [SCADA](https://en.wikipedia.org/wiki/SCADA)
> Supervisory control and data acquisition (SCADA) is a control system architecture comprising computers, networked data communications and graphical user interfaces for high-level supervision of machines and processes. It also covers sensors and other devices, such as programmable logic controllers, which interface with process plant or machinery. > Supervisory control and data acquisition (SCADA) is a control system architecture comprising computers, networked data communications and graphical user interfaces for high-level supervision of machines and processes. It also covers sensors and other devices, such as programmable logic controllers, which interface with process plant or machinery.
@@ -86,14 +88,8 @@ A vaguely-modbus [modbus](https://en.wikipedia.org/wiki/Modbus) communication pr
- Input Registers: Multi-Byte Read-Only (analog inputs) - Input Registers: Multi-Byte Read-Only (analog inputs)
- Holding Registers: Multi-Byte Read/Write (analog I/O) - Holding Registers: Multi-Byte Read/Write (analog I/O)
### Security and Encryption ### Security
TBD, I am planning on AES symmetric encryption for security + HMAC to prevent replay attacks. This will be done utilizing this codebase: https://github.com/somesocks/lua-lockbox. HMAC message authentication is available as a configuration option to prevent replay attacks and generally prevent control or false data reporting within a system's network. This is done utilizing the [lua-lockbox](https://github.com/somesocks/lua-lockbox) project.
This is somewhat important here as otherwise anyone can just control your setup, which is undeseriable. Unlike normal Minecraft PVP chaos, it would be very difficult to identify who is messing with your system, as with an Ender Modem they can do it from effectively anywhere and the server operators would have to check every computer's filesystem to find suspicious code. The other, simpler security feature is to enforce a maximum authorized transmission range, which is also a configurable feature on each device.
The other security mitigation for commanding (no effect on monitoring) is to enforce a maximum authorized transmission range, which has been added as a configurable feature.
## Known Issues
None yet since the switch to requiring 10.1+!

712
ccmsi.lua
View File

@@ -1,8 +1,6 @@
--
-- ComputerCraft Mekanism SCADA System Installer Utility
--
--[[ --[[
CC-MEK-SCADA Installer Utility
Copyright (c) 2023 Mikayla Fischler Copyright (c) 2023 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
@@ -20,29 +18,97 @@ 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.0" local CCMSI_VERSION = "v1.9a"
local install_dir = "/.install-cache" local install_dir = "/.install-cache"
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/" local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/"
local opts = { ... } local opts = { ... }
local mode = nil local mode, app, target
local app = nil local install_manifest = manifest_path .. "main/install_manifest.json"
local function red() term.setTextColor(colors.red) end
local function orange() term.setTextColor(colors.orange) end
local function yellow() term.setTextColor(colors.yellow) end
local function green() term.setTextColor(colors.green) end
local function blue() term.setTextColor(colors.blue) end
local function white() term.setTextColor(colors.white) end
local function lgray() term.setTextColor(colors.lightGray) end
-- get command line option in list
local function get_opt(opt, options)
for _, v in pairs(options) do if opt == v then return v end end
return nil
end
-- wait for any key to be pressed
---@diagnostic disable-next-line: undefined-field
local function any_key() os.pullEvent("key_up") end
-- ask the user yes or no
local function ask_y_n(question, default)
print(question)
if default == true then print(" (Y/n)? ") else print(" (y/N)? ") end
local response = read();any_key()
if response == "" then return default
elseif response == "Y" or response == "y" then return true
elseif response == "N" or response == "n" then return false
else return nil end
end
-- print out a white + blue text message
local function pkg_message(message, package) white();print(message .. " ");blue();println(package);white() end
-- indicate actions to be taken based on package differences for installs/updates
local function show_pkg_change(name, v)
if v.v_local ~= nil then
if v.v_local ~= v.v_remote then
print("[" .. name .. "] updating ");blue();print(v.v_local);white();print(" \xbb ");blue();println(v.v_remote);white()
elseif mode == "install" then
pkg_message("[" .. name .. "] reinstalling", v.v_local)
end
else pkg_message("[" .. name .. "] new install of", v.v_remote) end
return v.v_local ~= v.v_remote
end
-- read the local manifest file
local function read_local_manifest()
local local_ok = false
local local_manifest = {}
local imfile = fs.open("install_manifest.json", "r")
if imfile ~= nil then
local_ok, local_manifest = pcall(function () return textutils.unserializeJSON(imfile.readAll()) end)
imfile.close()
end
return local_ok, local_manifest
end
-- get the manifest from GitHub
local function get_remote_manifest()
local response, error = http.get(install_manifest)
if response == nil then
orange();println("Failed to get installation manifest from GitHub, cannot update or install.")
red();println("HTTP error: " .. error);white()
return false, {}
end
local ok, manifest = pcall(function () return textutils.unserializeJSON(response.readAll()) end)
if not ok then red();println("error parsing remote installation manifest");white() end
return ok, manifest
end
-- record the local installation manifest -- record the local installation manifest
---@param manifest table
---@param dependencies table
local function write_install_manifest(manifest, dependencies) local function write_install_manifest(manifest, dependencies)
local versions = {} local versions = {}
for key, value in pairs(manifest.versions) do for key, value in pairs(manifest.versions) do
local is_dependency = false local is_dependency = false
for _, dependency in pairs(dependencies) do for _, dependency in pairs(dependencies) do
if key == "bootloader" and dependency == "system" then if (key == "bootloader" and dependency == "system") or key == dependency then
is_dependency = true is_dependency = true;break
break
end end
end end
if key == app or key == "comms" or is_dependency then versions[key] = value end if key == app or key == "comms" or is_dependency then versions[key] = value end
end end
@@ -53,116 +119,141 @@ local function write_install_manifest(manifest, dependencies)
imfile.close() imfile.close()
end end
-- -- recursively build a tree out of the file manifest
local function gen_tree(manifest)
local function _tree_add(tree, split)
if #split > 1 then
local name = table.remove(split, 1)
if tree[name] == nil then tree[name] = {} end
table.insert(tree[name], _tree_add(tree[name], split))
else return split[1] end
return nil
end
local list, tree = {}, {}
-- make a list of each and every file
for _, files in pairs(manifest.files) do for i = 1, #files do table.insert(list, files[i]) end end
for i = 1, #list do
local split = {}
string.gsub(list[i], "([^/]+)", function(c) split[#split + 1] = c end)
if #split == 1 then table.insert(tree, list[i])
else table.insert(tree, _tree_add(tree, split)) end
end
return tree
end
local function _in_array(val, array)
for _, v in pairs(array) do if v == val then return true end end
return false
end
local function _clean_dir(dir, tree)
if tree == nil then tree = {} end
local ls = fs.list(dir)
for _, val in pairs(ls) do
local path = dir .. "/" .. val
if fs.isDir(path) then
_clean_dir(path, tree[val])
if #fs.list(path) == 0 then fs.delete(path);println("deleted " .. path) end
elseif not _in_array(val, tree) then
fs.delete(path)
println("deleted " .. path)
end
end
end
-- go through app/common directories to delete unused files
local function clean(manifest)
local root_ext = false
local tree = gen_tree(manifest)
table.insert(tree, "install_manifest.json")
table.insert(tree, "ccmsi.lua")
table.insert(tree, "log.txt")
lgray()
local ls = fs.list("/")
for _, val in pairs(ls) do
if fs.isDir(val) then
if tree[val] ~= nil then _clean_dir("/" .. val, tree[val]) end
if #fs.list(val) == 0 then fs.delete(val);println("deleted " .. val) end
elseif not _in_array(val, tree) then
root_ext = true
yellow();println(val .. " not used")
end
end
white()
if root_ext then println("Files in root directory won't be automatically deleted.") end
end
-- get and validate command line options -- 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> <tag/branch>") println("usage: ccmsi <mode> <app> <branch>")
println("<mode>") println("<mode>")
term.setTextColor(colors.lightGray) lgray()
println(" check - check latest versions avilable") println(" check - check latest versions avilable")
term.setTextColor(colors.yellow) yellow()
println(" ccmsi check <tag/branch> for target") println(" ccmsi check <branch> for target")
term.setTextColor(colors.lightGray) lgray()
println(" install - fresh install, overwrites config") println(" install - fresh install, overwrites config")
println(" update - update files EXCEPT for config/logs") println(" update - update files EXCEPT for config/logs")
println(" remove - delete files EXCEPT for config/logs") println(" remove - delete files EXCEPT for config/logs")
println(" purge - delete files INCLUDING config/logs") println(" purge - delete files INCLUDING config/logs")
term.setTextColor(colors.white) white();println("<app>");lgray()
println("<app>")
term.setTextColor(colors.lightGray)
println(" reactor-plc - reactor PLC firmware") println(" reactor-plc - reactor PLC firmware")
println(" rtu - RTU firmware") println(" rtu - RTU firmware")
println(" supervisor - supervisor server application") println(" supervisor - supervisor server application")
println(" coordinator - coordinator application") println(" coordinator - coordinator application")
println(" pocket - pocket application") println(" pocket - pocket application")
term.setTextColor(colors.white) println(" installer - ccmsi installer (update only)")
println("<tag/branch>") white();println("<branch>")
term.setTextColor(colors.yellow) lgray();println(" main (default) | latest | devel");white()
println(" second parameter when used with check")
term.setTextColor(colors.lightGray)
println(" note: defaults to main")
println(" target GitHub tag or branch name")
return return
else else
for _, v in pairs({ "check", "install", "update", "remove", "purge" }) do mode = get_opt(opts[1], { "check", "install", "update", "remove", "purge" })
if opts[1] == v then
mode = v
break
end
end
if mode == nil then if mode == nil then
println("unrecognized mode") red();println("Unrecognized mode.");white()
return return
end end
for _, v in pairs({ "reactor-plc", "rtu", "supervisor", "coordinator", "pocket" }) do app = get_opt(opts[2], { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket", "installer" })
if opts[2] == v then
app = v
break
end
end
if app == nil and mode ~= "check" then if app == nil and mode ~= "check" then
println("unrecognized application") red();println("Unrecognized application.");white()
return
elseif app == "installer" and mode ~= "update" then
red();println("Installer app only supports 'update' option.");white()
return return
end end
-- determine target
if mode == "check" then target = opts[2] else target = opts[3] end
if (target ~= "main") and (target ~= "latest") and (target ~= "devel") then
if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end
target = "main"
end
-- set paths
install_manifest = manifest_path .. target .. "/install_manifest.json"
repo_path = repo_path .. target .. "/"
end end
--
-- run selected mode -- run selected mode
--
if mode == "check" then if mode == "check" then
------------------------- local ok, manifest = get_remote_manifest()
-- GET REMOTE MANIFEST -- if not ok then return end
-------------------------
if opts[2] then repo_path = repo_path .. opts[2] .. "/" else repo_path = repo_path .. "main/" end
local install_manifest = repo_path .. "install_manifest.json"
local response, error = http.get(install_manifest)
if response == nil then
term.setTextColor(colors.orange)
println("failed to get installation manifest from GitHub, cannot update or install")
term.setTextColor(colors.red)
println("HTTP error: " .. error)
term.setTextColor(colors.white)
return
end
local ok, manifest = pcall(function () return textutils.unserializeJSON(response.readAll()) end)
if not ok then
term.setTextColor(colors.red)
println("error parsing remote installation manifest")
term.setTextColor(colors.white)
return
end
------------------------
-- GET LOCAL MANIFEST --
------------------------
local imfile = fs.open("install_manifest.json", "r")
local local_ok = false
local local_manifest = {}
if imfile ~= nil then
local_ok, local_manifest = pcall(function () return textutils.unserializeJSON(imfile.readAll()) end)
imfile.close()
end
local local_ok, local_manifest = read_local_manifest()
if not local_ok then if not local_ok then
term.setTextColor(colors.yellow) yellow();println("failed to load local installation information");white()
println("failed to load local installation information")
term.setTextColor(colors.white)
local_manifest = { versions = { installer = CCMSI_VERSION } } local_manifest = { versions = { installer = CCMSI_VERSION } }
else else
local_manifest.versions.installer = CCMSI_VERSION local_manifest.versions.installer = CCMSI_VERSION
@@ -173,189 +264,107 @@ if mode == "check" then
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
term.setTextColor(colors.blue) blue();print(local_manifest.versions[key])
print(local_manifest.versions[key])
if value ~= local_manifest.versions[key] then if value ~= local_manifest.versions[key] then
term.setTextColor(colors.white) white();print(" (")
print(" (")
term.setTextColor(colors.cyan) term.setTextColor(colors.cyan)
print(value) print(value);white();println(" available)")
term.setTextColor(colors.white) else green();println(" (up to date)") end
println(" available)")
else
term.setTextColor(colors.green)
println(" (up to date)")
end
else else
term.setTextColor(colors.lightGray) lgray();print("not installed");white();print(" (latest ")
print("not installed")
term.setTextColor(colors.white)
print(" (latest ")
term.setTextColor(colors.cyan) term.setTextColor(colors.cyan)
print(value) print(value);white();println(")")
term.setTextColor(colors.white)
println(")")
end end
end end
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()
end
elseif mode == "install" or mode == "update" then elseif mode == "install" or mode == "update" then
------------------------- local update_installer = app == "installer"
-- GET REMOTE MANIFEST -- local ok, manifest = get_remote_manifest()
------------------------- if not ok then return end
if opts[3] then repo_path = repo_path .. opts[3] .. "/" else repo_path = repo_path .. "main/" end local ver = {
local install_manifest = repo_path .. "install_manifest.json" app = { v_local = nil, v_remote = nil, changed = false },
boot = { v_local = nil, v_remote = nil, changed = false },
comms = { v_local = nil, v_remote = nil, changed = false },
common = { v_local = nil, v_remote = nil, changed = false },
graphics = { v_local = nil, v_remote = nil, changed = false },
lockbox = { v_local = nil, v_remote = nil, changed = false }
}
local response, error = http.get(install_manifest) -- try to find local versions
local local_ok, lmnf = read_local_manifest()
if not local_ok then
if mode == "update" then
red();println("Failed to load local installation information, cannot update.");white()
return
end
elseif not update_installer then
ver.boot.v_local = lmnf.versions.bootloader
ver.app.v_local = lmnf.versions[app]
ver.comms.v_local = lmnf.versions.comms
ver.common.v_local = lmnf.versions.common
ver.graphics.v_local = lmnf.versions.graphics
ver.lockbox.v_local = lmnf.versions.lockbox
if response == nil then if lmnf.versions[app] == nil then
term.setTextColor(colors.orange) red();println("Another application is already installed, please purge it before installing a new application.");white()
println("failed to get installation manifest from GitHub, cannot update or install") return
term.setTextColor(colors.red) end
println("HTTP error: " .. error) end
term.setTextColor(colors.white)
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 update_installer or ask_y_n("Would you like to update now") then
lgray();println("GET ccmsi.lua")
local dl, err = http.get(repo_path .. "ccmsi.lua")
if dl == nil then
red();println("HTTP Error " .. err)
println("Installer download failed.");white()
else
local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location
handle.write(dl.readAll())
handle.close()
green();println("Installer updated successfully.");white()
end
return
end
elseif update_installer then
green();println("Installer already up-to-date.");white()
return return
end end
local ok, manifest = pcall(function () return textutils.unserializeJSON(response.readAll()) end) ver.boot.v_remote = manifest.versions.bootloader
ver.app.v_remote = manifest.versions[app]
ver.comms.v_remote = manifest.versions.comms
ver.common.v_remote = manifest.versions.common
ver.graphics.v_remote = manifest.versions.graphics
ver.lockbox.v_remote = manifest.versions.lockbox
if not ok then green()
term.setTextColor(colors.red)
println("error parsing remote installation manifest")
term.setTextColor(colors.white)
end
------------------------
-- GET LOCAL MANIFEST --
------------------------
local imfile = fs.open("install_manifest.json", "r")
local local_ok = false
local local_manifest = {}
if imfile ~= nil then
local_ok, local_manifest = pcall(function () return textutils.unserializeJSON(imfile.readAll()) end)
imfile.close()
end
local local_app_version = nil
local local_comms_version = nil
local local_boot_version = nil
-- try to find local versions
if not local_ok then
if mode == "update" then
term.setTextColor(colors.red)
println("failed to load local installation information, cannot update")
term.setTextColor(colors.white)
return
end
else
local_app_version = local_manifest.versions[app]
local_comms_version = local_manifest.versions.comms
local_boot_version = local_manifest.versions.bootloader
if local_manifest.versions[app] == nil then
term.setTextColor(colors.red)
println("another application is already installed, please purge it before installing a new application")
term.setTextColor(colors.white)
return
end
local_manifest.versions.installer = CCMSI_VERSION
if manifest.versions.installer ~= CCMSI_VERSION then
term.setTextColor(colors.yellow)
println("a newer version of the installer is available, consider downloading it")
term.setTextColor(colors.white)
end
end
local remote_app_version = manifest.versions[app]
local remote_comms_version = manifest.versions.comms
local remote_boot_version = manifest.versions.bootloader
term.setTextColor(colors.green)
if mode == "install" then if mode == "install" then
println("installing " .. app .. " files...") println("Installing " .. app .. " files...")
elseif mode == "update" then elseif mode == "update" then
println("updating " .. app .. " files... (keeping old config.lua)") println("Updating " .. app .. " files... (keeping old config.lua)")
end end
term.setTextColor(colors.white) white()
-- display bootloader version change information ver.boot.changed = show_pkg_change("bootldr", ver.boot)
if local_boot_version ~= nil then ver.common.changed = show_pkg_change("common", ver.common)
if local_boot_version ~= remote_boot_version then ver.comms.changed = show_pkg_change("comms", ver.comms)
print("[bootldr] updating ") if ver.comms.changed and ver.comms.v_local ~= nil then
term.setTextColor(colors.blue) print("[comms] ");yellow();println("other devices on the network will require an update");white()
print(local_boot_version)
term.setTextColor(colors.white)
print(" \xbb ")
term.setTextColor(colors.blue)
println(remote_boot_version)
term.setTextColor(colors.white)
elseif mode == "install" then
print("[bootldr] reinstalling ")
term.setTextColor(colors.blue)
println(local_boot_version)
term.setTextColor(colors.white)
end
else
print("[bootldr] new install of ")
term.setTextColor(colors.blue)
println(remote_boot_version)
term.setTextColor(colors.white)
end end
ver.app.changed = show_pkg_change(app, ver.app)
ver.graphics.changed = show_pkg_change("graphics", ver.graphics)
ver.lockbox.changed = show_pkg_change("lockbox", ver.lockbox)
-- display app version change information -- ask for confirmation
if local_app_version ~= nil then if not ask_y_n("Continue", false) then return end
if local_app_version ~= remote_app_version then
print("[" .. app .. "] updating ")
term.setTextColor(colors.blue)
print(local_app_version)
term.setTextColor(colors.white)
print(" \xbb ")
term.setTextColor(colors.blue)
println(remote_app_version)
term.setTextColor(colors.white)
elseif mode == "install" then
print("[" .. app .. "] reinstalling ")
term.setTextColor(colors.blue)
println(local_app_version)
term.setTextColor(colors.white)
end
else
print("[" .. app .. "] new install of ")
term.setTextColor(colors.blue)
println(remote_app_version)
term.setTextColor(colors.white)
end
-- display comms version change information
if local_comms_version ~= nil then
if local_comms_version ~= remote_comms_version then
print("[comms] updating ")
term.setTextColor(colors.blue)
print(local_comms_version)
term.setTextColor(colors.white)
print(" \xbb ")
term.setTextColor(colors.blue)
println(remote_comms_version)
term.setTextColor(colors.white)
print("[comms] ")
term.setTextColor(colors.yellow)
println("other devices on the network will require an update")
term.setTextColor(colors.white)
elseif mode == "install" then
print("[comms] reinstalling ")
term.setTextColor(colors.blue)
println(local_comms_version)
term.setTextColor(colors.white)
end
else
print("[comms] new install of ")
term.setTextColor(colors.blue)
println(remote_comms_version)
term.setTextColor(colors.white)
end
-------------------------- --------------------------
-- START INSTALL/UPDATE -- -- START INSTALL/UPDATE --
@@ -380,54 +389,45 @@ elseif mode == "install" or mode == "update" then
-- check space constraints -- check space constraints
if space_available < space_required then if space_available < space_required then
single_file_mode = true single_file_mode = true
term.setTextColor(colors.yellow) yellow();println("WARNING: Insufficient space available for a full download!");white()
println("WARNING: Insufficient space available for a full download!")
term.setTextColor(colors.white)
println("Files can be downloaded one by one, so if you are replacing a current install this will not be a problem unless installation fails.") println("Files can be downloaded one by one, so if you are replacing a current install this will not be a problem unless installation fails.")
println("Do you wish to continue? (y/N)") if mode == "update" then println("If installation still fails, delete this device's log file or uninstall the app (not purge) and try again.") end
if not ask_y_n("Do you wish to continue", false) then
local confirm = read() println("Operation cancelled.")
if confirm ~= "y" and confirm ~= "Y" then
println("installation cancelled")
return return
end end
end end
---@diagnostic disable-next-line: undefined-field
os.sleep(2)
local success = true local success = true
-- helper function to check if a dependency is unchanged
local function unchanged(dependency)
if dependency == "system" then return not ver.boot.changed
elseif dependency == "graphics" then return not ver.graphics.changed
elseif dependency == "lockbox" then return not ver.lockbox.changed
elseif dependency == "common" then return not (ver.common.changed or ver.comms.changed)
elseif dependency == app then return not ver.app.changed
else return true end
end
if not single_file_mode then if not single_file_mode then
if fs.exists(install_dir) then if fs.exists(install_dir) then fs.delete(install_dir);fs.makeDir(install_dir) end
fs.delete(install_dir)
fs.makeDir(install_dir)
end
-- download all dependencies -- download all dependencies
for _, dependency in pairs(dependencies) do for _, dependency in pairs(dependencies) do
if mode == "update" and ((dependency == "system" and local_boot_version == remote_boot_version) or (local_app_version == remote_app_version)) then if mode == "update" and unchanged(dependency) then
-- skip system package if unchanged, skip app package if not changed pkg_message("skipping download of unchanged package", dependency)
-- skip packages that have no version if app version didn't change
term.setTextColor(colors.white)
print("skipping download of unchanged package ")
term.setTextColor(colors.blue)
println(dependency)
else else
term.setTextColor(colors.white) pkg_message("downloading package", dependency)
print("downloading package ") lgray()
term.setTextColor(colors.blue)
println(dependency)
term.setTextColor(colors.lightGray)
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
term.setTextColor(colors.red) red();println("HTTP Error " .. err)
println("GET HTTP Error " .. err)
success = false success = false
break break
else else
@@ -442,20 +442,12 @@ elseif mode == "install" or mode == "update" then
-- copy in downloaded files (installation) -- copy in downloaded files (installation)
if success then if success then
for _, dependency in pairs(dependencies) do for _, dependency in pairs(dependencies) do
if mode == "update" and ((dependency == "system" and local_boot_version == remote_boot_version) or (local_app_version == remote_app_version)) then if mode == "update" and unchanged(dependency) then
-- skip system package if unchanged, skip app package if not changed pkg_message("skipping install of unchanged package", dependency)
-- skip packages that have no version if app version didn't change
term.setTextColor(colors.white)
print("skipping install of unchanged package ")
term.setTextColor(colors.blue)
println(dependency)
else else
term.setTextColor(colors.white) pkg_message("installing package", dependency)
print("installing package ") lgray()
term.setTextColor(colors.blue)
println(dependency)
term.setTextColor(colors.lightGray)
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 if mode == "install" or file ~= config_file then
@@ -471,41 +463,28 @@ elseif mode == "install" or mode == "update" then
fs.delete(install_dir) fs.delete(install_dir)
if success then if success then
-- if we made it here, then none of the file system functions threw exceptions
-- that means everything is OK
write_install_manifest(manifest, dependencies) write_install_manifest(manifest, dependencies)
term.setTextColor(colors.green) green()
if mode == "install" then if mode == "install" then
println("installation completed successfully") println("Installation completed successfully.")
else else println("Update completed successfully.") end
println("update completed successfully") white();println("Ready to clean up unused files, press any key to continue...")
end any_key();clean(manifest)
white();println("Done.")
else else
if mode == "install" then if mode == "install" then
term.setTextColor(colors.red) red();println("Installation failed.")
println("installation failed") else orange();println("Update failed, existing files unmodified.") end
else
term.setTextColor(colors.orange)
println("update failed, existing files unmodified")
end
end end
else else
-- go through all files and replace one by one -- go through all files and replace one by one
for _, dependency in pairs(dependencies) do for _, dependency in pairs(dependencies) do
if mode == "update" and ((dependency == "system" and local_boot_version == remote_boot_version) or (local_app_version == remote_app_version)) then if mode == "update" and unchanged(dependency) then
-- skip system package if unchanged, skip app package if not changed pkg_message("skipping install of unchanged package", dependency)
-- skip packages that have no version if app version didn't change
term.setTextColor(colors.white)
print("skipping install of unchanged package ")
term.setTextColor(colors.blue)
println(dependency)
else else
term.setTextColor(colors.white) pkg_message("installing package", dependency)
print("installing package ") lgray()
term.setTextColor(colors.blue)
println(dependency)
term.setTextColor(colors.lightGray)
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 if mode == "install" or file ~= config_file then
@@ -513,7 +492,7 @@ elseif mode == "install" or mode == "update" then
local dl, err = http.get(repo_path .. file) local dl, err = http.get(repo_path .. file)
if dl == nil then if dl == nil then
println("GET HTTP Error " .. err) red();println("HTTP Error " .. err)
success = false success = false
break break
else else
@@ -527,55 +506,43 @@ elseif mode == "install" or mode == "update" then
end end
if success then if success then
-- if we made it here, then none of the file system functions threw exceptions
-- that means everything is OK
write_install_manifest(manifest, dependencies) write_install_manifest(manifest, dependencies)
term.setTextColor(colors.green) green()
if mode == "install" then if mode == "install" then
println("installation completed successfully") println("Installation completed successfully.")
else else println("Update completed successfully.") end
println("update completed successfully") white();println("Ready to clean up unused files, press any key to continue...")
end any_key();clean(manifest)
white();println("Done.")
else else
term.setTextColor(colors.red) red()
if mode == "install" then if mode == "install" then
println("installation failed, files may have been skipped") println("Installation failed, files may have been skipped.")
else else println("Update failed, files may have been skipped.") end
println("update failed, files may have been skipped")
end
end end
end end
elseif mode == "remove" or mode == "purge" then elseif mode == "remove" or mode == "purge" then
local imfile = fs.open("install_manifest.json", "r") local ok, manifest = read_local_manifest()
local ok = false
local manifest = {}
if imfile ~= nil then
ok, manifest = pcall(function () return textutils.unserializeJSON(imfile.readAll()) end)
imfile.close()
end
if not ok then if not ok then
term.setTextColor(colors.red) red();println("Error parsing local installation manifest.");white()
println("error parsing local installation manifest")
term.setTextColor(colors.white)
return return
elseif mode == "remove" and manifest.versions[app] == nil then elseif mode == "remove" and manifest.versions[app] == nil then
term.setTextColor(colors.red) red();println(app .. " is not installed, cannot remove.");white()
println(app .. " is not installed")
term.setTextColor(colors.white)
return return
end end
term.setTextColor(colors.orange) orange()
if mode == "remove" then if mode == "remove" then
println("removing all " .. app .. " files except for config.lua and log.txt...") println("Removing all " .. app .. " files except for config and log...")
elseif mode == "purge" then elseif mode == "purge" then
println("purging all " .. app .. " files...") println("Purging all " .. app .. " files including config and log...")
end end
---@diagnostic disable-next-line: undefined-field -- ask for confirmation
os.sleep(2) if not ask_y_n("Continue", false) then return end
-- delete unused files first
clean(manifest)
local file_list = manifest.files local file_list = manifest.files
local dependencies = manifest.depends[app] local dependencies = manifest.depends[app]
@@ -583,9 +550,8 @@ elseif mode == "remove" or mode == "purge" then
table.insert(dependencies, app) table.insert(dependencies, app)
term.setTextColor(colors.lightGray)
-- delete log file if purging -- delete log file if purging
lgray()
if mode == "purge" and fs.exists(config_file) then if mode == "purge" and fs.exists(config_file) then
local log_deleted = pcall(function () local log_deleted = pcall(function ()
local config = require(app .. ".config") local config = require(app .. ".config")
@@ -596,11 +562,9 @@ elseif mode == "remove" or mode == "purge" then
end) end)
if not log_deleted then if not log_deleted then
term.setTextColor(colors.red) red();println("Failed to delete log file.")
println("failed to delete log file") white();println("press any key to continue...")
term.setTextColor(colors.lightGray) any_key();lgray()
---@diagnostic disable-next-line: undefined-field
os.sleep(1)
end end
end end
@@ -609,10 +573,7 @@ elseif mode == "remove" or mode == "purge" 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 == "purge" or file ~= config_file then if mode == "purge" or file ~= config_file then
if fs.exists(file) then if fs.exists(file) then fs.delete(file);println("deleted " .. file) end
fs.delete(file)
println("deleted " .. file)
end
end end
end end
@@ -621,11 +582,7 @@ elseif mode == "remove" or mode == "purge" then
local folder = files[1] local folder = files[1]
while true do while true do
local dir = fs.getDir(folder) local dir = fs.getDir(folder)
if dir == "" or dir == ".." then if dir == "" or dir == ".." then break else folder = dir end
break
else
folder = dir
end
end end
if fs.isDir(folder) then if fs.isDir(folder) then
@@ -633,19 +590,15 @@ elseif mode == "remove" or mode == "purge" then
println("deleted directory " .. folder) println("deleted directory " .. folder)
end end
elseif dependency == app then elseif dependency == app then
-- delete individual subdirectories so we can leave the config
for _, folder in pairs(files) do for _, folder in pairs(files) do
while true do while true do
local dir = fs.getDir(folder) local dir = fs.getDir(folder)
if dir == "" or dir == ".." or dir == app then if dir == "" or dir == ".." or dir == app then break else folder = dir end
break
else
folder = dir
end
end end
if folder ~= app and fs.isDir(folder) then if folder ~= app and fs.isDir(folder) then
fs.delete(folder) fs.delete(folder);println("deleted app subdirectory " .. folder)
println("deleted app subdirectory " .. folder)
end end
end end
end end
@@ -658,13 +611,12 @@ elseif mode == "remove" or mode == "purge" then
else else
-- remove all data from versions list to show nothing is installed -- remove all data from versions list to show nothing is installed
manifest.versions = {} manifest.versions = {}
imfile = fs.open("install_manifest.json", "w") local imfile = fs.open("install_manifest.json", "w")
imfile.write(textutils.serializeJSON(manifest)) imfile.write(textutils.serializeJSON(manifest))
imfile.close() imfile.close()
end end
term.setTextColor(colors.green) green();println("Done!")
println("done!")
end end
term.setTextColor(colors.white) white()

View File

@@ -1,16 +1,20 @@
local config = {} local config = {}
-- port of the SCADA supervisor -- supervisor comms channel
config.SCADA_SV_PORT = 16100 config.SVR_CHANNEL = 16240
-- port to listen to incoming packets from supervisor -- coordinator comms channel
config.SCADA_SV_CTL_LISTEN = 16101 config.CRD_CHANNEL = 16243
-- listen port for SCADA coordinator API access -- pocket comms channel
config.SCADA_API_LISTEN = 16200 config.PKT_CHANNEL = 16244
-- max trusted modem message distance (0 to disable check) -- max trusted modem message distance (0 to disable check)
config.TRUSTED_RANGE = 0 config.TRUSTED_RANGE = 0
-- time in seconds (>= 2) before assuming a remote device is no longer active -- time in seconds (>= 2) before assuming a remote device is no longer active
config.SV_TIMEOUT = 5 config.SV_TIMEOUT = 5
config.API_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 -- expected number of reactor units, used only to require that number of unit monitors
config.NUM_UNITS = 4 config.NUM_UNITS = 4
@@ -22,11 +26,16 @@ config.SOUNDER_VOLUME = 1.0
-- true for 24 hour time on main view screen -- true for 24 hour time on main view screen
config.TIME_24_HOUR = true config.TIME_24_HOUR = true
-- disable flow view (for legacy layouts)
config.DISABLE_FLOW_VIEW = false
-- log path -- log path
config.LOG_PATH = "/log.txt" config.LOG_PATH = "/log.txt"
-- log mode -- log mode
-- 0 = APPEND (adds to existing file on start) -- 0 = APPEND (adds to existing file on start)
-- 1 = NEW (replaces existing file on start) -- 1 = NEW (replaces existing file on start)
config.LOG_MODE = 0 config.LOG_MODE = 0
-- true to log verbose debug messages
config.LOG_DEBUG = false
return config return config

View File

@@ -2,6 +2,7 @@ local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local util = require("scada-common.util") local util = require("scada-common.util")
local types = require("scada-common.types")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process") local process = require("coordinator.process")
@@ -12,7 +13,6 @@ local dialog = require("coordinator.ui.dialog")
local print = util.print local print = util.print
local println = util.println local println = util.println
local println_ts = util.println_ts
local PROTOCOL = comms.PROTOCOL local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE local DEVICE_TYPE = comms.DEVICE_TYPE
@@ -22,6 +22,8 @@ local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE
local UNIT_COMMAND = comms.UNIT_COMMAND local UNIT_COMMAND = comms.UNIT_COMMAND
local FAC_COMMAND = comms.FAC_COMMAND local FAC_COMMAND = comms.FAC_COMMAND
local LINK_TIMEOUT = 60.0
local coordinator = {} local coordinator = {}
-- request the user to select a monitor -- request the user to select a monitor
@@ -47,12 +49,15 @@ end
-- configure monitor layout -- configure monitor layout
---@param num_units integer number of units expected ---@param num_units integer number of units expected
---@param disable_flow_view boolean disable flow view (legacy)
---@return boolean success, monitors_struct? monitors ---@return boolean success, monitors_struct? monitors
function coordinator.configure_monitors(num_units) function coordinator.configure_monitors(num_units, disable_flow_view)
---@class monitors_struct ---@class monitors_struct
local monitors = { local monitors = {
primary = nil, primary = nil,
primary_name = "", primary_name = "",
flow = nil,
flow_name = "",
unit_displays = {}, unit_displays = {},
unit_name_map = {} unit_name_map = {}
} }
@@ -67,8 +72,8 @@ function coordinator.configure_monitors(num_units)
table.insert(available, iface) table.insert(available, iface)
end end
-- we need a certain number of monitors (1 per unit + 1 primary display) -- we need a certain number of monitors (1 per unit + 1 primary display + 1 flow display)
local num_displays_needed = num_units + 1 local num_displays_needed = num_units + util.trinary(disable_flow_view, 1, 2)
if #names < num_displays_needed then if #names < num_displays_needed then
local message = "not enough monitors connected (need " .. num_displays_needed .. ")" local message = "not enough monitors connected (need " .. num_displays_needed .. ")"
println(message) println(message)
@@ -81,10 +86,12 @@ function coordinator.configure_monitors(num_units)
log.warning("configure_monitors(): failed to load coordinator settings file (may not exist yet)") log.warning("configure_monitors(): failed to load coordinator settings file (may not exist yet)")
else else
local _primary = settings.get("PRIMARY_DISPLAY") local _primary = settings.get("PRIMARY_DISPLAY")
local _flow = settings.get("FLOW_DISPLAY")
local _unitd = settings.get("UNIT_DISPLAYS") local _unitd = settings.get("UNIT_DISPLAYS")
-- filter out already assigned monitors -- filter out already assigned monitors
util.filter_table(available, function (x) return x ~= _primary end) util.filter_table(available, function (x) return x ~= _primary end)
util.filter_table(available, function (x) return x ~= _flow end)
if type(_unitd) == "table" then if type(_unitd) == "table" then
util.filter_table(available, function (x) return not util.table_contains(_unitd, x) end) util.filter_table(available, function (x) return not util.table_contains(_unitd, x) end)
end end
@@ -104,7 +111,6 @@ function coordinator.configure_monitors(num_units)
end end
while iface_primary_display == nil and #available > 0 do while iface_primary_display == nil and #available > 0 do
-- lets get a monitor
iface_primary_display = ask_monitor(available) iface_primary_display = ask_monitor(available)
end end
@@ -116,6 +122,33 @@ function coordinator.configure_monitors(num_units)
monitors.primary = ppm.get_periph(iface_primary_display) monitors.primary = ppm.get_periph(iface_primary_display)
monitors.primary_name = 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 -- -- UNIT DISPLAYS --
------------------- -------------------
@@ -128,7 +161,6 @@ function coordinator.configure_monitors(num_units)
local display = nil local display = nil
while display == nil and #available > 0 do while display == nil and #available > 0 do
-- lets get a monitor
println("please select monitor for unit #" .. i) println("please select monitor for unit #" .. i)
display = ask_monitor(available) display = ask_monitor(available)
end end
@@ -150,7 +182,6 @@ function coordinator.configure_monitors(num_units)
end end
while display == nil and #available > 0 do while display == nil and #available > 0 do
-- lets get a monitor
display = ask_monitor(available) display = ask_monitor(available)
end end
@@ -183,7 +214,8 @@ local function log_dmesg(message, dmesg_tag, working)
GRAPHICS = colors.green, GRAPHICS = colors.green,
SYSTEM = colors.cyan, SYSTEM = colors.cyan,
BOOT = colors.blue, BOOT = colors.blue,
COMMS = colors.purple COMMS = colors.purple,
CRYPTO = colors.yellow
} }
if working then if working then
@@ -197,6 +229,7 @@ function coordinator.log_graphics(message) log_dmesg(message, "GRAPHICS") end
function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end
function coordinator.log_boot(message) log_dmesg(message, "BOOT") end function coordinator.log_boot(message) log_dmesg(message, "BOOT") end
function coordinator.log_comms(message) log_dmesg(message, "COMMS") end function coordinator.log_comms(message) log_dmesg(message, "COMMS") end
function coordinator.log_crypto(message) log_dmesg(message, "CRYPTO") end
-- log a message for communications connecting, providing access to progress indication control functions -- log a message for communications connecting, providing access to progress indication control functions
---@nodiscard ---@nodiscard
@@ -212,38 +245,38 @@ end
-- coordinator communications -- coordinator communications
---@nodiscard ---@nodiscard
---@param version string coordinator version ---@param version string coordinator version
---@param modem table modem device ---@param nic nic network interface device
---@param sv_port integer port of configured supervisor ---@param num_units integer number of configured units for number of monitors, checked against SV
---@param sv_listen integer listening port for supervisor replys ---@param crd_channel integer port of configured supervisor
---@param api_listen integer listening port for pocket API ---@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 range integer trusted device connection range
---@param sv_watchdog watchdog ---@param sv_watchdog watchdog
function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range, sv_watchdog) function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pkt_channel, range, sv_watchdog)
local self = { local self = {
sv_linked = false, sv_linked = false,
sv_addr = comms.BROADCAST,
sv_seq_num = 0, sv_seq_num = 0,
sv_r_seq_num = nil, sv_r_seq_num = nil,
sv_config_err = false, sv_config_err = false,
connected = false,
last_est_ack = ESTABLISH_ACK.ALLOW, last_est_ack = ESTABLISH_ACK.ALLOW,
last_api_est_acks = {} last_api_est_acks = {},
est_start = 0,
est_last = 0,
est_tick_waiting = nil,
est_task_done = nil
} }
comms.set_trusted_range(range) comms.set_trusted_range(range)
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure modem channels -- configure network channels
local function _conf_channels() nic.closeAll()
modem.closeAll() nic.open(crd_channel)
modem.open(sv_listen)
modem.open(api_listen)
end
_conf_channels() -- link nic to apisessions
apisessions.init(nic)
-- link modem to apisessions
apisessions.init(modem)
-- send a packet to the supervisor -- send a packet to the supervisor
---@param msg_type SCADA_MGMT_TYPE|SCADA_CRDN_TYPE ---@param msg_type SCADA_MGMT_TYPE|SCADA_CRDN_TYPE
@@ -261,23 +294,24 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
end end
pkt.make(msg_type, msg) pkt.make(msg_type, msg)
s_pkt.make(self.sv_seq_num, protocol, pkt.raw_sendable()) s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable())
modem.transmit(sv_port, sv_listen, s_pkt.raw_sendable()) nic.transmit(svr_channel, crd_channel, s_pkt)
self.sv_seq_num = self.sv_seq_num + 1 self.sv_seq_num = self.sv_seq_num + 1
end end
-- send an API establish request response -- send an API establish request response
---@param dest integer ---@param packet scada_packet
---@param msg table ---@param ack ESTABLISH_ACK
local function _send_api_establish_ack(seq_id, dest, msg) local function _send_api_establish_ack(packet, ack)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet() local m_pkt = comms.mgmt_packet()
m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg) m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, { ack })
s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
modem.transmit(dest, api_listen, s_pkt.raw_sendable()) nic.transmit(pkt_channel, crd_channel, s_pkt)
self.last_api_est_acks[packet.src_addr()] = ack
end end
-- attempt connection establishment -- attempt connection establishment
@@ -296,82 +330,78 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
---@class coord_comms ---@class coord_comms
local public = {} local public = {}
-- reconnect a newly connected modem -- try to connect to the supervisor if not already linked
---@param new_modem table ---@param abort boolean? true to print out cancel info if not linked (use on program terminate)
function public.reconnect_modem(new_modem) ---@return boolean ok, boolean start_ui
modem = new_modem function public.try_connect(abort)
apisessions.relink_modem(new_modem) local ok = true
_conf_channels() local start_ui = false
if not self.sv_linked then
if self.est_tick_waiting == nil then
self.est_start = util.time_s()
self.est_last = self.est_start
self.est_tick_waiting, self.est_task_done =
coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. svr_channel)
_send_establish()
else
self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (util.time_s() - self.est_start)))
end
if abort or (util.time_s() - self.est_start) >= LINK_TIMEOUT then
self.est_task_done(false)
if abort then
coordinator.log_comms("supervisor connection attempt cancelled by user")
elseif self.sv_config_err then
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
elseif not self.sv_linked then
if self.last_est_ack == ESTABLISH_ACK.DENY then
coordinator.log_comms("supervisor connection attempt denied")
elseif self.last_est_ack == ESTABLISH_ACK.COLLISION then
coordinator.log_comms("supervisor connection failed due to collision")
elseif self.last_est_ack == ESTABLISH_ACK.BAD_VERSION then
coordinator.log_comms("supervisor connection failed due to version mismatch")
else
coordinator.log_comms("supervisor connection failed with no valid response")
end
end
ok = false
elseif self.sv_config_err then
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
ok = false
elseif (util.time_s() - self.est_last) > 1.0 then
_send_establish()
self.est_last = util.time_s()
end
elseif self.est_tick_waiting ~= nil then
self.est_task_done(true)
self.est_tick_waiting = nil
self.est_task_done = nil
start_ui = true
end
return ok, start_ui
end end
-- close the connection to the server -- close the connection to the server
function public.close() function public.close()
sv_watchdog.cancel() sv_watchdog.cancel()
self.sv_addr = comms.BROADCAST
self.sv_linked = false self.sv_linked = false
self.sv_r_seq_num = nil
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {}) _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {})
end end
-- attempt to connect to the subervisor
---@nodiscard
---@param timeout_s number timeout in seconds
---@param tick_dmesg_waiting function callback to tick dmesg waiting
---@param task_done function callback to show done on dmesg
---@return boolean sv_linked true if connected, false otherwise
--- EVENT_CONSUMER: this function consumes events
function public.sv_connect(timeout_s, tick_dmesg_waiting, task_done)
local clock = util.new_clock(1)
local start = util.time_s()
local terminated = false
_send_establish()
clock.start()
while (util.time_s() - start) < timeout_s and (not self.sv_linked) and (not self.sv_config_err) do
local event, p1, p2, p3, p4, p5 = util.pull_event()
if event == "timer" and clock.is_clock(p1) then
-- timed out attempt, try again
tick_dmesg_waiting(math.max(0, timeout_s - (util.time_s() - start)))
_send_establish()
clock.start()
elseif event == "modem_message" then
-- handle message
local packet = public.parse_packet(p1, p2, p3, p4, p5)
if packet ~= nil and packet.type == SCADA_MGMT_TYPE.ESTABLISH then
public.handle_packet(packet)
end
elseif event == "terminate" then
terminated = true
break
end
end
task_done(self.sv_linked)
if terminated then
coordinator.log_comms("supervisor connection attempt cancelled by user")
elseif self.sv_config_err then
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
elseif not self.sv_linked then
if self.last_est_ack == ESTABLISH_ACK.DENY then
coordinator.log_comms("supervisor connection attempt denied")
elseif self.last_est_ack == ESTABLISH_ACK.COLLISION then
coordinator.log_comms("supervisor connection failed due to collision")
elseif self.last_est_ack == ESTABLISH_ACK.BAD_VERSION then
coordinator.log_comms("supervisor connection failed due to version mismatch")
else
coordinator.log_comms("supervisor connection failed with no valid response")
end
end
return self.sv_linked
end
-- send a facility command -- send a facility command
---@param cmd FAC_COMMAND command ---@param cmd FAC_COMMAND command
function public.send_fac_command(cmd) ---@param option any? optional option options for the optional options (like waste mode)
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd }) function public.send_fac_command(cmd, option)
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd, option })
end end
-- send the auto process control configuration with a start command -- send the auto process control configuration with a start command
@@ -385,7 +415,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
-- send a unit command -- send a unit command
---@param cmd UNIT_COMMAND command ---@param cmd UNIT_COMMAND command
---@param unit integer unit ID ---@param unit integer unit ID
---@param option any? optional option options for the optional options (like burn rate) (does option still look like a word?) ---@param option any? optional option options for the optional options (like burn rate)
function public.send_unit_command(cmd, unit, option) function public.send_unit_command(cmd, unit, option)
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_CMD, { cmd, unit, option }) _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
end end
@@ -398,13 +428,10 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
---@param distance integer ---@param distance integer
---@return mgmt_frame|crdn_frame|capi_frame|nil packet ---@return mgmt_frame|crdn_frame|capi_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance) function public.parse_packet(side, sender, reply_to, message, distance)
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
local pkt = nil local pkt = nil
local s_pkt = comms.scada_packet()
-- parse packet as generic SCADA packet if s_pkt then
s_pkt.receive(side, sender, reply_to, message, distance)
if s_pkt.is_valid() then
-- get as SCADA management packet -- get as SCADA management packet
if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet() local mgmt_pkt = comms.mgmt_packet()
@@ -433,17 +460,25 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
-- handle a packet -- handle a packet
---@param packet mgmt_frame|crdn_frame|capi_frame|nil ---@param packet mgmt_frame|crdn_frame|capi_frame|nil
---@return boolean close_ui
function public.handle_packet(packet) function public.handle_packet(packet)
local was_linked = self.sv_linked
if packet ~= nil then if packet ~= nil then
local l_port = packet.scada_frame.local_port() local l_chan = packet.scada_frame.local_channel()
local r_port = packet.scada_frame.remote_port() local r_chan = packet.scada_frame.remote_channel()
local src_addr = packet.scada_frame.src_addr()
local protocol = packet.scada_frame.protocol() local protocol = packet.scada_frame.protocol()
if l_port == api_listen then if l_chan ~= crd_channel then
if protocol == PROTOCOL.COORD_API then log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == pkt_channel then
if not self.sv_linked then
log.debug("discarding pocket API packet before linked to supervisor")
elseif protocol == PROTOCOL.COORD_API then
---@cast packet capi_frame ---@cast packet capi_frame
-- look for an associated session -- look for an associated session
local session = apisessions.find_session(r_port) local session = apisessions.find_session(src_addr)
-- API packet -- API packet
if session ~= nil then if session ~= nil then
@@ -456,7 +491,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
elseif protocol == PROTOCOL.SCADA_MGMT then elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
-- look for an associated session -- look for an associated session
local session = apisessions.find_session(r_port) local session = apisessions.find_session(src_addr)
-- SCADA management packet -- SCADA management packet
if session ~= nil then if session ~= nil then
@@ -464,8 +499,6 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
session.in_queue.push_packet(packet) session.in_queue.push_packet(packet)
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- establish a new session -- establish a new session
local next_seq_id = packet.scada_frame.seq_num() + 1
-- validate packet and continue -- validate packet and continue
if packet.length == 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then if packet.length == 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1] local comms_v = packet.data[1]
@@ -473,42 +506,42 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
local dev_type = packet.data[3] local dev_type = packet.data[3]
if comms_v ~= comms.version then if comms_v ~= comms.version then
if self.last_api_est_acks[r_port] ~= ESTABLISH_ACK.BAD_VERSION then if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping API establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) log.info(util.c("dropping API establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
self.last_api_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION
end end
_send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION }) _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.PKT then elseif dev_type == DEVICE_TYPE.PKT then
-- pocket linking request -- pocket linking request
local id = apisessions.establish_session(l_port, r_port, firmware_v) local id = apisessions.establish_session(src_addr, firmware_v)
println(util.c("API: pocket (", firmware_v, ") [:", r_port, "] connected with session ID ", id)) coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id))
coordinator.log_comms(util.c("API: pocket (", firmware_v, ") [:", r_port, "] connected with session ID ", id))
_send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW }) _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW)
self.last_api_est_acks[r_port] = ESTABLISH_ACK.ALLOW
else else
log.debug(util.c("illegal establish packet for device ", dev_type, " on API listening channel")) log.debug(util.c("API_ESTABLISH: illegal establish packet for device ", dev_type, " on pocket channel"))
_send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
end end
else else
log.debug("invalid establish packet (on API listening channel)") log.debug("invalid establish packet (on API listening channel)")
_send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
end end
else else
-- any other packet should be session related, discard it -- any other packet should be session related, discard it
log.debug(util.c(r_port, "->", l_port, ": discarding SCADA_MGMT packet without a known session")) log.debug(util.c("discarding pocket SCADA_MGMT packet without a known session from computer ", src_addr))
end end
else else
log.debug("illegal packet type " .. protocol .. " on api listening channel", true) log.debug("illegal packet type " .. protocol .. " on pocket channel", true)
end end
elseif l_port == sv_listen then elseif r_chan == 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()
elseif self.connected and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then elseif self.sv_linked and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return return false
elseif self.sv_linked and src_addr ~= self.sv_addr then
log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?")
return false
else else
self.sv_r_seq_num = packet.scada_frame.seq_num() self.sv_r_seq_num = packet.scada_frame.seq_num()
end end
@@ -570,6 +603,10 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
end end
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
iocontrol.get_db().facility.ack_alarms_ack(ack) iocontrol.get_db().facility.ack_alarms_ack(ack)
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
process.waste_ack_handle(packet.data[2])
elseif cmd == FAC_COMMAND.SET_PU_FB then
process.pu_fb_ack_handle(packet.data[2])
else else
log.debug(util.c("received facility command ack with unknown command ", cmd)) log.debug(util.c("received facility command ack with unknown command ", cmd))
end end
@@ -634,36 +671,68 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
end end
elseif protocol == PROTOCOL.SCADA_MGMT then elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then if self.sv_linked then
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back
if packet.length == 1 then
local timestamp = packet.data[1]
local trip_time = util.time() - timestamp
if trip_time > 750 then
log.warning("coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
end
-- log.debug("coordinator RTT = " .. trip_time .. "ms")
iocontrol.get_db().facility.ps.publish("sv_ping", trip_time)
_send_keep_alive_ack(timestamp)
else
log.debug("SCADA keep alive packet length mismatch")
end
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
-- handle session close
sv_watchdog.cancel()
self.sv_addr = comms.BROADCAST
self.sv_linked = false
self.sv_r_seq_num = nil
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
log.info("server connection closed by remote host")
else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
end
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- 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 config = packet.data[2]
if est_ack == ESTABLISH_ACK.ALLOW then if est_ack == ESTABLISH_ACK.ALLOW then
if type(config) == "table" and #config > 1 then -- reset to disconnected before validating
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
if type(config) == "table" and #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 = config[1], ---@type integer
defs = {} -- boilers and turbines cooling = config[2] ---@type sv_cooling_conf
} }
if (#config - 1) == (conf.num_units * 2) then if conf.num_units == num_units then
-- record sequence of pairs of [#boilers, #turbines] per unit
for i = 2, #config do
table.insert(conf.defs, config[i])
end
-- init io controller -- init io controller
iocontrol.init(conf, public) iocontrol.init(conf, public)
self.sv_addr = src_addr
self.sv_linked = true self.sv_linked = true
self.sv_r_seq_num = nil
self.sv_config_err = false self.sv_config_err = false
iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED)
else else
self.sv_config_err = true self.sv_config_err = true
log.warning("invalid supervisor configuration definitions received, establish failed") log.warning("supervisor config's number of units don't match coordinator's config, establish failed")
end end
else else
log.debug("invalid supervisor configuration table received, establish failed") log.debug("invalid supervisor configuration table received, establish failed")
@@ -678,14 +747,17 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
if est_ack == ESTABLISH_ACK.DENY then if est_ack == ESTABLISH_ACK.DENY then
if self.last_est_ack ~= est_ack then if self.last_est_ack ~= est_ack then
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DENIED)
log.info("supervisor connection denied") log.info("supervisor connection denied")
end end
elseif est_ack == ESTABLISH_ACK.COLLISION then elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.last_est_ack ~= est_ack then if self.last_est_ack ~= est_ack then
iocontrol.fp_link_state(types.PANEL_LINK_STATE.COLLISION)
log.warning("supervisor connection denied due to collision") log.warning("supervisor connection denied due to collision")
end end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.last_est_ack ~= est_ack then if self.last_est_ack ~= est_ack then
iocontrol.fp_link_state(types.PANEL_LINK_STATE.BAD_VERSION)
log.warning("supervisor comms version mismatch") log.warning("supervisor comms version mismatch")
end end
else else
@@ -696,34 +768,6 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
else else
log.debug("SCADA_MGMT establish packet length mismatch") log.debug("SCADA_MGMT establish packet length mismatch")
end end
elseif self.sv_linked then
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back
if packet.length == 1 then
local timestamp = packet.data[1]
local trip_time = util.time() - timestamp
if trip_time > 750 then
log.warning("coord KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
end
-- log.debug("coord RTT = " .. trip_time .. "ms")
iocontrol.get_db().facility.ps.publish("sv_ping", trip_time)
_send_keep_alive_ack(timestamp)
else
log.debug("SCADA keep alive packet length mismatch")
end
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
-- handle session close
sv_watchdog.cancel()
self.sv_linked = false
println_ts("server connection closed by remote host")
log.info("server connection closed by remote host")
else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
end
else else
log.debug("discarding non-link SCADA_MGMT packet before linked") log.debug("discarding non-link SCADA_MGMT packet before linked")
end end
@@ -731,9 +775,11 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
log.debug("illegal packet type " .. protocol .. " on supervisor listening channel", true) log.debug("illegal packet type " .. protocol .. " on supervisor listening channel", true)
end end
else else
log.debug("received packet on unconfigured channel " .. l_port, true) log.debug("received packet for unknown channel " .. r_chan, true)
end end
end end
return was_linked and not self.sv_linked
end end
-- check if the coordinator is still linked to the supervisor -- check if the coordinator is still linked to the supervisor

View File

@@ -10,26 +10,50 @@ local util = require("scada-common.util")
local process = require("coordinator.process") local process = require("coordinator.process")
local sounder = require("coordinator.sounder") local sounder = require("coordinator.sounder")
local pgi = require("coordinator.ui.pgi")
local ALARM_STATE = types.ALARM_STATE local ALARM_STATE = types.ALARM_STATE
local PROCESS = types.PROCESS local PROCESS = types.PROCESS
-- nominal RTT is ping (0ms to 10ms usually) + 500ms for CRD main loop tick
local WARN_RTT = 1000 -- 2x as long as expected w/ 0 ping
local HIGH_RTT = 1500 -- 3.33x as long as expected w/ 0 ping
local iocontrol = {} local iocontrol = {}
---@class ioctl ---@class ioctl
local io = {} local io = {}
-- luacheck: no unused args
-- placeholder acknowledge function for type hinting -- placeholder acknowledge function for type hinting
---@param success boolean ---@param success boolean
---@diagnostic disable-next-line: unused-local ---@diagnostic disable-next-line: unused-local
local function __generic_ack(success) end local function __generic_ack(success) end
-- luacheck: unused args
-- initialize front panel PSIL
---@param firmware_v string coordinator version
---@param comms_v string comms version
function iocontrol.init_fp(firmware_v, comms_v)
---@class ioctl_front_panel
io.fp = { ps = psil.create() }
io.fp.ps.publish("version", firmware_v)
io.fp.ps.publish("comms_version", comms_v)
end
-- 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) function iocontrol.init(conf, comms)
-- facility data structure
---@class ioctl_facility ---@class ioctl_facility
io.facility = { io.facility = {
num_units = conf.num_units, ---@type integer num_units = conf.num_units,
tank_mode = conf.cooling.fac_tank_mode,
tank_defs = conf.cooling.fac_tank_defs,
all_sys_ok = false, all_sys_ok = false,
rtu_count = 0, rtu_count = 0,
@@ -48,6 +72,10 @@ function iocontrol.init(conf, comms)
gen_fault = false gen_fault = false
}, },
---@type WASTE_PRODUCT
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
auto_pu_fallback_active = false,
radiation = types.new_zero_radiation_reading(), radiation = types.new_zero_radiation_reading(),
save_cfg_ack = __generic_ack, save_cfg_ack = __generic_ack,
@@ -56,22 +84,124 @@ function iocontrol.init(conf, comms)
scram_ack = __generic_ack, scram_ack = __generic_ack,
ack_alarms_ack = __generic_ack, ack_alarms_ack = __generic_ack,
alarm_tones = { false, false, false, false, false, false, false, false },
ps = psil.create(), ps = psil.create(),
induction_ps_tbl = {}, induction_ps_tbl = {},
induction_data_tbl = {}, induction_data_tbl = {},
sps_ps_tbl = {},
sps_data_tbl = {},
tank_ps_tbl = {},
tank_data_tbl = {},
env_d_ps = psil.create(), env_d_ps = psil.create(),
env_d_data = {} env_d_data = {}
} }
-- create induction tables (currently only 1 is supported) -- create induction and SPS tables (currently only 1 of each is supported)
for _ = 1, conf.num_units do table.insert(io.facility.induction_ps_tbl, psil.create())
local data = {} ---@type imatrix_session_db table.insert(io.facility.induction_data_tbl, {})
table.insert(io.facility.induction_ps_tbl, psil.create()) table.insert(io.facility.sps_ps_tbl, psil.create())
table.insert(io.facility.induction_data_tbl, data) table.insert(io.facility.sps_data_tbl, {})
-- determine tank information
if io.facility.tank_mode == 0 then
io.facility.tank_defs = {}
-- on facility tank mode 0, setup tank defs to match unit TANK option
for i = 1, conf.num_units do
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TANK, 1, 0)
end
io.facility.tank_list = { table.unpack(io.facility.tank_defs) }
else
-- decode the layout of tanks from the connections definitions
local tank_mode = io.facility.tank_mode
local tank_defs = io.facility.tank_defs
local tank_list = { table.unpack(tank_defs) }
local function calc_fdef(start_idx, end_idx)
local first = 4
for i = start_idx, end_idx do
if io.facility.tank_defs[i] == 2 then
if i < first then first = i end
end
end
return first
end
if tank_mode == 1 then
-- (1) 1 total facility tank (A A A A)
local first_fdef = calc_fdef(1, #tank_defs)
for i = 1, #tank_defs do
if i > first_fdef and tank_defs[i] == 2 then
tank_list[i] = 0
end
end
elseif tank_mode == 2 then
-- (2) 2 total facility tanks (A A A B)
local first_fdef = calc_fdef(1, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 3 then
-- (3) 2 total facility tanks (A A B B)
for _, a in pairs({ 1, 3 }) do
local b = a + 1
if (tank_defs[a] == 2) and (tank_defs[b] == 2) then
tank_list[b] = 0
end
end
elseif tank_mode == 4 then
-- (4) 2 total facility tanks (A B B B)
local first_fdef = calc_fdef(2, #tank_defs)
for i = 1, #tank_defs do
if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 5 then
-- (5) 3 total facility tanks (A A B C)
local first_fdef = calc_fdef(1, math.min(2, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 6 then
-- (6) 3 total facility tanks (A B B C)
local first_fdef = calc_fdef(2, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 7 then
-- (7) 3 total facility tanks (A B C C)
local first_fdef = calc_fdef(3, #tank_defs)
for i = 1, #tank_defs do
if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
end
io.facility.tank_list = tank_list
end end
-- create facility tank tables
for i = 1, #io.facility.tank_list do
if io.facility.tank_list[i] == 2 then
table.insert(io.facility.tank_ps_tbl, psil.create())
table.insert(io.facility.tank_data_tbl, {})
end
end
-- create unit data structures
io.units = {} io.units = {}
for i = 1, conf.num_units do for i = 1, conf.num_units do
local function ack(alarm) process.ack_alarm(i, alarm) end local function ack(alarm) process.ack_alarm(i, alarm) end
@@ -83,11 +213,16 @@ function iocontrol.init(conf, comms)
num_boilers = 0, num_boilers = 0,
num_turbines = 0, num_turbines = 0,
num_snas = 0,
has_tank = conf.cooling.r_cool[i].TANK,
control_state = false, control_state = false,
burn_rate_cmd = 0.0, burn_rate_cmd = 0.0,
waste_control = 0,
radiation = types.new_zero_radiation_reading(), radiation = types.new_zero_radiation_reading(),
sna_prod_rate = 0.0,
waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
waste_product = types.WASTE_PRODUCT.PLUTONIUM,
-- auto control group -- auto control group
a_group = 0, a_group = 0,
@@ -96,10 +231,10 @@ function iocontrol.init(conf, comms)
scram = function () process.scram(i) end, scram = function () process.scram(i) end,
reset_rps = function () process.reset_rps(i) end, reset_rps = function () process.reset_rps(i) end,
ack_alarms = function () process.ack_all_alarms(i) end, ack_alarms = function () process.ack_all_alarms(i) end,
set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate
set_waste = function (mode) process.set_waste(i, mode) end, ---@param mode integer waste processing mode set_waste = function (mode) process.set_unit_waste(i, mode) end, ---@param mode WASTE_MODE waste processing mode
set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0 set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0 for manual
start_ack = __generic_ack, start_ack = __generic_ack,
scram_ack = __generic_ack, scram_ack = __generic_ack,
@@ -148,21 +283,33 @@ function iocontrol.init(conf, comms)
boiler_data_tbl = {}, boiler_data_tbl = {},
turbine_ps_tbl = {}, turbine_ps_tbl = {},
turbine_data_tbl = {} turbine_data_tbl = {},
tank_ps_tbl = {},
tank_data_tbl = {}
} }
-- on other facility modes, overwrite unit TANK option with facility tank defs
if io.facility.tank_mode ~= 0 then
entry.has_tank = conf.cooling.fac_tank_defs[i] > 0
end
-- create boiler tables -- create boiler tables
for _ = 1, conf.defs[(i * 2) - 1] do for _ = 1, conf.cooling.r_cool[i].BOILERS do
local data = {} ---@type boilerv_session_db
table.insert(entry.boiler_ps_tbl, psil.create()) table.insert(entry.boiler_ps_tbl, psil.create())
table.insert(entry.boiler_data_tbl, data) table.insert(entry.boiler_data_tbl, {})
end end
-- create turbine tables -- create turbine tables
for _ = 1, conf.defs[i * 2] do for _ = 1, conf.cooling.r_cool[i].TURBINES do
local data = {} ---@type turbinev_session_db
table.insert(entry.turbine_ps_tbl, psil.create()) table.insert(entry.turbine_ps_tbl, psil.create())
table.insert(entry.turbine_data_tbl, data) table.insert(entry.turbine_data_tbl, {})
end
-- create tank tables
if io.facility.tank_defs[i] == 1 then
table.insert(entry.tank_ps_tbl, psil.create())
table.insert(entry.tank_data_tbl, {})
end end
entry.num_boilers = #entry.boiler_data_tbl entry.num_boilers = #entry.boiler_data_tbl
@@ -175,6 +322,102 @@ function iocontrol.init(conf, comms)
process.init(io, comms) process.init(io, comms)
end end
--#region Front Panel PSIL
-- toggle heartbeat indicator
function iocontrol.heartbeat() io.fp.ps.toggle("heartbeat") end
-- report presence of the wireless modem
---@param has_modem boolean
function iocontrol.fp_has_modem(has_modem) io.fp.ps.publish("has_modem", has_modem) end
-- report presence of the speaker
---@param has_speaker boolean
function iocontrol.fp_has_speaker(has_speaker) io.fp.ps.publish("has_speaker", has_speaker) end
-- report supervisor link state
---@param state integer
function iocontrol.fp_link_state(state) io.fp.ps.publish("link_state", state) end
-- report monitor connection state
---@param id string|integer unit ID for unit monitor, "main" for main monitor, or "flow" for flow monitor
function iocontrol.fp_monitor_state(id, connected)
local name = nil
if id == "main" then
name = "main_monitor"
elseif id == "flow" then
name = "flow_monitor"
elseif type(id) == "number" then
name = "unit_monitor_" .. id
end
if name ~= nil then
io.fp.ps.publish(name, connected)
end
end
-- report PKT firmware version and PKT session connection state
---@param session_id integer PKT session
---@param fw string firmware version
---@param s_addr integer PKT computer ID
function iocontrol.fp_pkt_connected(session_id, fw, s_addr)
io.fp.ps.publish("pkt_" .. session_id .. "_fw", fw)
io.fp.ps.publish("pkt_" .. session_id .. "_addr", util.sprintf("@ C% 3d", s_addr))
pgi.create_pkt_entry(session_id)
end
-- report PKT session disconnected
---@param session_id integer PKT session
function iocontrol.fp_pkt_disconnected(session_id)
pgi.delete_pkt_entry(session_id)
end
-- transmit PKT session RTT
---@param session_id integer PKT session
---@param rtt integer round trip time
function iocontrol.fp_pkt_rtt(session_id, rtt)
io.fp.ps.publish("pkt_" .. session_id .. "_rtt", rtt)
if rtt > HIGH_RTT then
io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.red)
elseif rtt > WARN_RTT then
io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.yellow_hc)
else
io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.green)
end
end
--#endregion
--#region Builds
-- record and publish multiblock RTU build data
---@param id integer
---@param entry table
---@param data_tbl table
---@param ps_tbl table
---@param create boolean? true to create an entry if non exists, false to fail on missing
---@return boolean ok true if data saved, false if invalid ID
local function _record_multiblock_build(id, entry, data_tbl, ps_tbl, create)
local exists = type(data_tbl[id]) == "table"
if exists or create then
if not exists then
ps_tbl[id] = psil.create()
data_tbl[id] = {}
end
data_tbl[id].formed = entry[1] ---@type boolean
data_tbl[id].build = entry[2] ---@type table
ps_tbl[id].publish("formed", entry[1])
for key, val in pairs(data_tbl[id].build) do ps_tbl[id].publish(key, val) end
end
return exists or (create == true)
end
-- populate facility structure builds -- populate facility structure builds
---@param build table ---@param build table
---@return boolean valid ---@return boolean valid
@@ -187,21 +430,29 @@ function iocontrol.record_facility_builds(build)
-- induction matricies -- induction matricies
if type(build.induction) == "table" then if type(build.induction) == "table" then
for id, matrix in pairs(build.induction) do for id, matrix in pairs(build.induction) do
if type(fac.induction_data_tbl[id]) == "table" then if not _record_multiblock_build(id, matrix, fac.induction_data_tbl, fac.induction_ps_tbl) then
fac.induction_data_tbl[id].formed = matrix[1] ---@type boolean
fac.induction_data_tbl[id].build = matrix[2] ---@type table
fac.induction_ps_tbl[id].publish("formed", matrix[1])
for key, val in pairs(fac.induction_data_tbl[id].build) do
fac.induction_ps_tbl[id].publish(key, val)
end
else
log.debug(util.c("iocontrol.record_facility_builds: invalid induction matrix id ", id)) log.debug(util.c("iocontrol.record_facility_builds: invalid induction matrix id ", id))
valid = false valid = false
end end
end end
end end
-- SPS
if type(build.sps) == "table" then
for id, sps in pairs(build.sps) do
if not _record_multiblock_build(id, sps, fac.sps_data_tbl, fac.sps_ps_tbl) then
log.debug(util.c("iocontrol.record_facility_builds: invalid SPS id ", id))
valid = false
end
end
end
-- dynamic tanks
if type(build.tanks) == "table" then
for id, tank in pairs(build.tanks) do
_record_multiblock_build(id, tank, fac.tank_data_tbl, fac.tank_ps_tbl, true)
end
end
else else
log.debug("facility builds not a table") log.debug("facility builds not a table")
valid = false valid = false
@@ -245,16 +496,7 @@ function iocontrol.record_unit_builds(builds)
-- boiler builds -- boiler builds
if type(build.boilers) == "table" then if type(build.boilers) == "table" then
for b_id, boiler in pairs(build.boilers) do for b_id, boiler in pairs(build.boilers) do
if type(unit.boiler_data_tbl[b_id]) == "table" then if not _record_multiblock_build(b_id, boiler, unit.boiler_data_tbl, unit.boiler_ps_tbl) then
unit.boiler_data_tbl[b_id].formed = boiler[1] ---@type boolean
unit.boiler_data_tbl[b_id].build = boiler[2] ---@type table
unit.boiler_ps_tbl[b_id].publish("formed", boiler[1])
for key, val in pairs(unit.boiler_data_tbl[b_id].build) do
unit.boiler_ps_tbl[b_id].publish(key, val)
end
else
log.debug(util.c(log_header, "invalid boiler id ", b_id)) log.debug(util.c(log_header, "invalid boiler id ", b_id))
valid = false valid = false
end end
@@ -264,27 +506,49 @@ function iocontrol.record_unit_builds(builds)
-- turbine builds -- turbine builds
if type(build.turbines) == "table" then if type(build.turbines) == "table" then
for t_id, turbine in pairs(build.turbines) do for t_id, turbine in pairs(build.turbines) do
if type(unit.turbine_data_tbl[t_id]) == "table" then if not _record_multiblock_build(t_id, turbine, unit.turbine_data_tbl, unit.turbine_ps_tbl) then
unit.turbine_data_tbl[t_id].formed = turbine[1] ---@type boolean
unit.turbine_data_tbl[t_id].build = turbine[2] ---@type table
unit.turbine_ps_tbl[t_id].publish("formed", turbine[1])
for key, val in pairs(unit.turbine_data_tbl[t_id].build) do
unit.turbine_ps_tbl[t_id].publish(key, val)
end
else
log.debug(util.c(log_header, "invalid turbine id ", t_id)) log.debug(util.c(log_header, "invalid turbine id ", t_id))
valid = false valid = false
end end
end end
end end
-- dynamic tank builds
if type(build.tanks) == "table" then
for d_id, d_tank in pairs(build.tanks) do
_record_multiblock_build(d_id, d_tank, unit.tank_data_tbl, unit.tank_ps_tbl, true)
end
end
end end
end end
return valid return valid
end end
--#endregion
--#region Statuses
-- record and publish multiblock status data
---@param entry any
---@param data imatrix_session_db|sps_session_db|dynamicv_session_db|turbinev_session_db|boilerv_session_db
---@param ps psil
---@return boolean is_faulted
local function _record_multiblock_status(entry, data, ps)
local is_faulted = entry[1] ---@type boolean
data.formed = entry[2] ---@type boolean
data.state = entry[3] ---@type table
data.tanks = entry[4] ---@type table
ps.publish("formed", data.formed)
ps.publish("faulted", is_faulted)
for key, val in pairs(data.state) do ps.publish(key, val) end
for key, val in pairs(data.tanks) do ps.publish(key, val) end
return is_faulted
end
-- update facility status -- update facility status
---@param status table ---@param status table
---@return boolean valid ---@return boolean valid
@@ -302,7 +566,7 @@ function iocontrol.update_facility_status(status)
local ctl_status = status[1] local ctl_status = status[1]
if type(ctl_status) == "table" and #ctl_status == 14 then if type(ctl_status) == "table" and #ctl_status == 16 then
fac.all_sys_ok = ctl_status[1] fac.all_sys_ok = ctl_status[1]
fac.auto_ready = ctl_status[2] fac.auto_ready = ctl_status[2]
@@ -350,6 +614,12 @@ function iocontrol.update_facility_status(status)
io.units[i].unit_ps.publish("auto_group", names[group_map[i] + 1]) io.units[i].unit_ps.publish("auto_group", names[group_map[i] + 1])
end end
end end
fac.auto_current_waste_product = ctl_status[15]
fac.auto_pu_fallback_active = ctl_status[16]
fac.ps.publish("current_waste_product", fac.auto_current_waste_product)
fac.ps.publish("pu_fallback_active", fac.auto_pu_fallback_active)
else else
log.debug(log_header .. "control status not a table or length mismatch") log.debug(log_header .. "control status not a table or length mismatch")
valid = false valid = false
@@ -386,36 +656,23 @@ function iocontrol.update_facility_status(status)
for id, matrix in pairs(rtu_statuses.induction) do for id, matrix in pairs(rtu_statuses.induction) do
if type(fac.induction_data_tbl[id]) == "table" then if type(fac.induction_data_tbl[id]) == "table" then
local rtu_faulted = matrix[1] ---@type boolean local data = fac.induction_data_tbl[id] ---@type imatrix_session_db
fac.induction_data_tbl[id].formed = matrix[2] ---@type boolean local ps = fac.induction_ps_tbl[id] ---@type psil
fac.induction_data_tbl[id].state = matrix[3] ---@type table
fac.induction_data_tbl[id].tanks = matrix[4] ---@type table
local data = fac.induction_data_tbl[id] ---@type imatrix_session_db local rtu_faulted = _record_multiblock_status(matrix, data, ps)
fac.induction_ps_tbl[id].publish("formed", data.formed) if rtu_faulted then
fac.induction_ps_tbl[id].publish("faulted", rtu_faulted) ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.formed then if data.tanks.energy_fill >= 0.99 then
if rtu_faulted then ps.publish("computed_status", 6) -- full
fac.induction_ps_tbl[id].publish("computed_status", 3) -- faulted
elseif data.tanks.energy_fill >= 0.99 then
fac.induction_ps_tbl[id].publish("computed_status", 6) -- full
elseif data.tanks.energy_fill <= 0.01 then elseif data.tanks.energy_fill <= 0.01 then
fac.induction_ps_tbl[id].publish("computed_status", 5) -- empty ps.publish("computed_status", 5) -- empty
else else
fac.induction_ps_tbl[id].publish("computed_status", 4) -- on-line ps.publish("computed_status", 4) -- on-line
end end
else else
fac.induction_ps_tbl[id].publish("computed_status", 2) -- not formed ps.publish("computed_status", 2) -- not formed
end
for key, val in pairs(fac.induction_data_tbl[id].state) do
fac.induction_ps_tbl[id].publish(key, val)
end
for key, val in pairs(fac.induction_data_tbl[id].tanks) do
fac.induction_ps_tbl[id].publish(key, val)
end end
else else
log.debug(util.c(log_header, "invalid induction matrix id ", id)) log.debug(util.c(log_header, "invalid induction matrix id ", id))
@@ -426,6 +683,82 @@ function iocontrol.update_facility_status(status)
valid = false valid = false
end end
-- SPS statuses
if type(rtu_statuses.sps) == "table" then
for id = 1, #fac.sps_ps_tbl do
if rtu_statuses.sps[id] == nil then
-- disconnected
fac.sps_ps_tbl[id].publish("computed_status", 1)
end
end
for id, sps in pairs(rtu_statuses.sps) do
if type(fac.sps_data_tbl[id]) == "table" then
local data = fac.sps_data_tbl[id] ---@type sps_session_db
local ps = fac.sps_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(sps, data, ps)
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.state.process_rate > 0 then
ps.publish("computed_status", 5) -- active
else
ps.publish("computed_status", 4) -- idle
end
else
ps.publish("computed_status", 2) -- not formed
end
io.facility.ps.publish("am_rate", data.state.process_rate * 1000)
else
log.debug(util.c(log_header, "invalid sps id ", id))
end
end
else
log.debug(log_header .. "sps list not a table")
valid = false
end
-- dynamic tank statuses
if type(rtu_statuses.tanks) == "table" then
for id = 1, #fac.tank_ps_tbl do
if rtu_statuses.tanks[id] == nil then
-- disconnected
fac.tank_ps_tbl[id].publish("computed_status", 1)
end
end
for id, tank in pairs(rtu_statuses.tanks) do
if type(fac.tank_data_tbl[id]) == "table" then
local data = fac.tank_data_tbl[id] ---@type dynamicv_session_db
local ps = fac.tank_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(tank, data, ps)
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.tanks.fill >= 0.99 then
ps.publish("computed_status", 6) -- full
elseif data.tanks.fill < 0.20 then
ps.publish("computed_status", 5) -- low
else
ps.publish("computed_status", 4) -- on-line
end
else
ps.publish("computed_status", 2) -- not formed
end
else
log.debug(util.c(log_header, "invalid dynamic tank id ", id))
end
end
else
log.debug(log_header .. "dyanmic tank list not a table")
valid = false
end
-- environment detector status -- environment detector status
if type(rtu_statuses.rad_mon) == "table" then if type(rtu_statuses.rad_mon) == "table" then
if #rtu_statuses.rad_mon > 0 then if #rtu_statuses.rad_mon > 0 then
@@ -449,6 +782,16 @@ function iocontrol.update_facility_status(status)
end end
fac.ps.publish("rtu_count", fac.rtu_count) fac.ps.publish("rtu_count", fac.rtu_count)
-- alarm tone commands
if (type(status[3]) == "table") and (#status[3] == 8) then
fac.alarm_tones = status[3]
sounder.set(fac.alarm_tones)
else
log.debug(log_header .. "alarm tones not a table or length mismatch")
valid = false
end
end end
return valid return valid
@@ -468,6 +811,8 @@ function iocontrol.update_unit_statuses(statuses)
valid = false valid = false
else else
local burn_rate_sum = 0.0 local burn_rate_sum = 0.0
local sna_count_sum = 0
local pu_rate, po_rate, po_pl_rate, po_am_rate, spent_rate = 0.0, 0.0, 0.0, 0.0, 0.0
-- get all unit statuses -- get all unit statuses
for i = 1, #statuses do for i = 1, #statuses do
@@ -476,7 +821,9 @@ function iocontrol.update_unit_statuses(statuses)
local unit = io.units[i] ---@type ioctl_unit local unit = io.units[i] ---@type ioctl_unit
local status = statuses[i] local status = statuses[i]
if type(status) ~= "table" or #status ~= 5 then local burn_rate = 0.0
if type(status) ~= "table" or #status ~= 6 then
log.debug(log_header .. "invalid status entry in unit statuses (not a table or invalid length)") log.debug(log_header .. "invalid status entry in unit statuses (not a table or invalid length)")
valid = false valid = false
else else
@@ -511,7 +858,8 @@ function iocontrol.update_unit_statuses(statuses)
-- if status hasn't been received, mek_status = {} -- if status hasn't been received, mek_status = {}
if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then
burn_rate_sum = burn_rate_sum + unit.reactor_data.mek_status.act_burn_rate burn_rate = unit.reactor_data.mek_status.act_burn_rate
burn_rate_sum = burn_rate_sum + burn_rate
end end
if unit.reactor_data.mek_status.status then if unit.reactor_data.mek_status.status then
@@ -558,6 +906,8 @@ function iocontrol.update_unit_statuses(statuses)
if type(rtu_statuses) == "table" then if type(rtu_statuses) == "table" then
-- boiler statuses -- boiler statuses
if type(rtu_statuses.boilers) == "table" then if type(rtu_statuses.boilers) == "table" then
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[i] == nil then
-- disconnected -- disconnected
@@ -567,40 +917,31 @@ 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 rtu_faulted = boiler[1] ---@type boolean local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
unit.boiler_data_tbl[id].formed = boiler[2] ---@type boolean local ps = unit.boiler_ps_tbl[id] ---@type psil
unit.boiler_data_tbl[id].state = boiler[3] ---@type table
unit.boiler_data_tbl[id].tanks = boiler[4] ---@type table
local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db local rtu_faulted = _record_multiblock_status(boiler, data, ps)
unit.boiler_ps_tbl[id].publish("formed", data.formed)
unit.boiler_ps_tbl[id].publish("faulted", rtu_faulted)
if rtu_faulted then if rtu_faulted then
unit.boiler_ps_tbl[id].publish("computed_status", 3) -- faulted ps.publish("computed_status", 3) -- faulted
elseif data.formed then elseif data.formed then
boil_sum = boil_sum + data.state.boil_rate
if data.state.boil_rate > 0 then if data.state.boil_rate > 0 then
unit.boiler_ps_tbl[id].publish("computed_status", 5) -- active ps.publish("computed_status", 5) -- active
else else
unit.boiler_ps_tbl[id].publish("computed_status", 4) -- idle ps.publish("computed_status", 4) -- idle
end end
else else
unit.boiler_ps_tbl[id].publish("computed_status", 2) -- not formed ps.publish("computed_status", 2) -- not formed
end
for key, val in pairs(unit.boiler_data_tbl[id].state) do
unit.boiler_ps_tbl[id].publish(key, val)
end
for key, val in pairs(unit.boiler_data_tbl[id].tanks) do
unit.boiler_ps_tbl[id].publish(key, val)
end end
else else
log.debug(util.c(log_header, "invalid boiler id ", id)) log.debug(util.c(log_header, "invalid boiler id ", id))
valid = false valid = false
end end
end end
unit.unit_ps.publish("boiler_boil_sum", boil_sum)
else else
log.debug(log_header .. "boiler list not a table") log.debug(log_header .. "boiler list not a table")
valid = false valid = false
@@ -608,6 +949,8 @@ function iocontrol.update_unit_statuses(statuses)
-- turbine statuses -- turbine statuses
if type(rtu_statuses.turbines) == "table" then if type(rtu_statuses.turbines) == "table" then
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[i] == nil then
-- disconnected -- disconnected
@@ -617,47 +960,93 @@ 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 rtu_faulted = turbine[1] ---@type boolean
unit.turbine_data_tbl[id].formed = turbine[2] ---@type boolean
unit.turbine_data_tbl[id].state = turbine[3] ---@type table
unit.turbine_data_tbl[id].tanks = turbine[4] ---@type table
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
unit.turbine_ps_tbl[id].publish("formed", data.formed) local rtu_faulted = _record_multiblock_status(turbine, data, ps)
unit.turbine_ps_tbl[id].publish("faulted", rtu_faulted)
if rtu_faulted then if rtu_faulted then
unit.turbine_ps_tbl[id].publish("computed_status", 3) -- faulted ps.publish("computed_status", 3) -- faulted
elseif data.formed then elseif data.formed then
flow_sum = flow_sum + data.state.flow_rate
if data.tanks.energy_fill >= 0.99 then if data.tanks.energy_fill >= 0.99 then
unit.turbine_ps_tbl[id].publish("computed_status", 6) -- trip ps.publish("computed_status", 6) -- trip
elseif data.state.flow_rate < 100 then elseif data.state.flow_rate < 100 then
unit.turbine_ps_tbl[id].publish("computed_status", 4) -- idle ps.publish("computed_status", 4) -- idle
else else
unit.turbine_ps_tbl[id].publish("computed_status", 5) -- active ps.publish("computed_status", 5) -- active
end end
else else
unit.turbine_ps_tbl[id].publish("computed_status", 2) -- not formed ps.publish("computed_status", 2) -- not formed
end
for key, val in pairs(unit.turbine_data_tbl[id].state) do
unit.turbine_ps_tbl[id].publish(key, val)
end
for key, val in pairs(unit.turbine_data_tbl[id].tanks) do
unit.turbine_ps_tbl[id].publish(key, val)
end end
else else
log.debug(util.c(log_header, "invalid turbine id ", id)) log.debug(util.c(log_header, "invalid turbine id ", id))
valid = false valid = false
end end
end end
unit.unit_ps.publish("turbine_flow_sum", flow_sum)
else else
log.debug(log_header .. "turbine list not a table") log.debug(log_header .. "turbine list not a table")
valid = false valid = false
end end
-- dynamic tank statuses
if type(rtu_statuses.tanks) == "table" then
for id = 1, #unit.tank_ps_tbl do
if rtu_statuses.tanks[i] == nil then
-- disconnected
unit.tank_ps_tbl[id].publish("computed_status", 1)
end
end
for id, tank in pairs(rtu_statuses.tanks) do
if type(unit.tank_data_tbl[id]) == "table" then
local data = unit.tank_data_tbl[id] ---@type dynamicv_session_db
local ps = unit.tank_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(tank, data, ps)
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.tanks.fill >= 0.99 then
ps.publish("computed_status", 6) -- full
elseif data.tanks.fill < 0.20 then
ps.publish("computed_status", 5) -- low
else
ps.publish("computed_status", 4) -- on-line
end
else
ps.publish("computed_status", 2) -- not formed
end
else
log.debug(util.c(log_header, "invalid dynamic tank id ", id))
valid = false
end
end
else
log.debug(log_header .. "dynamic tank list not a table")
valid = false
end
-- solar neutron activator status info
if type(rtu_statuses.sna) == "table" then
unit.num_snas = rtu_statuses.sna[1] ---@type integer
unit.sna_prod_rate = rtu_statuses.sna[2] ---@type number
unit.sna_peak_rate = rtu_statuses.sna[3] ---@type number
unit.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)
sna_count_sum = sna_count_sum + unit.num_snas
else
log.debug(log_header .. "sna statistic list not a table")
valid = false
end
-- environment detector status -- environment detector status
if type(rtu_statuses.rad_mon) == "table" then if type(rtu_statuses.rad_mon) == "table" then
if #rtu_statuses.rad_mon > 0 then if #rtu_statuses.rad_mon > 0 then
@@ -735,12 +1124,17 @@ function iocontrol.update_unit_statuses(statuses)
local unit_state = status[5] local unit_state = status[5]
if type(unit_state) == "table" then if type(unit_state) == "table" then
if #unit_state == 5 then if #unit_state == 6 then
unit.waste_mode = unit_state[5]
unit.waste_product = unit_state[6]
unit.unit_ps.publish("U_StatusLine1", unit_state[1]) unit.unit_ps.publish("U_StatusLine1", unit_state[1])
unit.unit_ps.publish("U_StatusLine2", unit_state[2]) unit.unit_ps.publish("U_StatusLine2", unit_state[2])
unit.unit_ps.publish("U_WasteMode", unit_state[3]) unit.unit_ps.publish("U_AutoReady", unit_state[3])
unit.unit_ps.publish("U_AutoReady", unit_state[4]) unit.unit_ps.publish("U_AutoDegraded", unit_state[4])
unit.unit_ps.publish("U_AutoDegraded", unit_state[5]) unit.unit_ps.publish("U_AutoWaste", unit.waste_mode == types.WASTE_MODE.AUTO)
unit.unit_ps.publish("U_WasteMode", unit.waste_mode)
unit.unit_ps.publish("U_WasteProduct", unit.waste_product)
else else
log.debug(log_header .. "unit state length mismatch") log.debug(log_header .. "unit state length mismatch")
valid = false valid = false
@@ -749,18 +1143,81 @@ function iocontrol.update_unit_statuses(statuses)
log.debug(log_header .. "unit state not a table") log.debug(log_header .. "unit state not a table")
valid = false valid = false
end end
-- valve states
local valve_states = status[6]
if type(valve_states) == "table" then
if #valve_states == 5 then
unit.unit_ps.publish("V_pu_conn", valve_states[1] > 0)
unit.unit_ps.publish("V_pu_state", valve_states[1] == 2)
unit.unit_ps.publish("V_po_conn", valve_states[2] > 0)
unit.unit_ps.publish("V_po_state", valve_states[2] == 2)
unit.unit_ps.publish("V_pl_conn", valve_states[3] > 0)
unit.unit_ps.publish("V_pl_state", valve_states[3] == 2)
unit.unit_ps.publish("V_am_conn", valve_states[4] > 0)
unit.unit_ps.publish("V_am_state", valve_states[4] == 2)
unit.unit_ps.publish("V_emc_conn", valve_states[5] > 0)
unit.unit_ps.publish("V_emc_state", valve_states[5] == 2)
else
log.debug(log_header .. "valve states length mismatch")
valid = false
end
else
log.debug(log_header .. "valve states not a table")
valid = false
end
-- determine waste production for this unit, add to statistics
local is_pu = unit.waste_product == types.WASTE_PRODUCT.PLUTONIUM
local waste_rate = burn_rate / 10.0
local u_spent_rate = waste_rate
local u_pu_rate = util.trinary(is_pu, waste_rate, 0.0)
local u_po_rate = util.trinary(not is_pu, math.min(waste_rate, unit.sna_prod_rate), 0.0)
unit.unit_ps.publish("pu_rate", u_pu_rate)
unit.unit_ps.publish("po_rate", u_po_rate)
unit.unit_ps.publish("sna_in", util.trinary(is_pu, 0, burn_rate))
if unit.waste_product == types.WASTE_PRODUCT.POLONIUM then
unit.unit_ps.publish("po_pl_rate", u_po_rate)
unit.unit_ps.publish("po_am_rate", 0)
po_pl_rate = po_pl_rate + u_po_rate
elseif unit.waste_product == types.WASTE_PRODUCT.ANTI_MATTER then
unit.unit_ps.publish("po_pl_rate", 0)
unit.unit_ps.publish("po_am_rate", u_po_rate)
po_am_rate = po_am_rate + u_po_rate
u_spent_rate = 0
else
unit.unit_ps.publish("po_pl_rate", 0)
unit.unit_ps.publish("po_am_rate", 0)
end
unit.unit_ps.publish("ws_rate", u_spent_rate)
pu_rate = pu_rate + u_pu_rate
po_rate = po_rate + u_po_rate
spent_rate = spent_rate + u_spent_rate
end end
end end
io.facility.ps.publish("burn_sum", burn_rate_sum) io.facility.ps.publish("burn_sum", burn_rate_sum)
io.facility.ps.publish("sna_count", sna_count_sum)
-- update alarm sounder io.facility.ps.publish("pu_rate", pu_rate)
sounder.eval(io.units) io.facility.ps.publish("po_rate", po_rate)
io.facility.ps.publish("po_pl_rate", po_pl_rate)
io.facility.ps.publish("po_am_rate", po_am_rate)
io.facility.ps.publish("spent_waste_rate", spent_rate)
end end
return valid return valid
end end
--#endregion
-- get the IO controller database -- get the IO controller database
function iocontrol.get_db() return io end function iocontrol.get_db() return io end

View File

@@ -11,6 +11,7 @@ local FAC_COMMAND = comms.FAC_COMMAND
local UNIT_COMMAND = comms.UNIT_COMMAND local UNIT_COMMAND = comms.UNIT_COMMAND
local PROCESS = types.PROCESS local PROCESS = types.PROCESS
local PRODUCT = types.WASTE_PRODUCT
---@class process_controller ---@class process_controller
local process = {} local process = {}
@@ -24,7 +25,9 @@ local self = {
burn_target = 0.0, burn_target = 0.0,
charge_target = 0.0, charge_target = 0.0,
gen_target = 0.0, gen_target = 0.0,
limits = {} limits = {},
waste_product = PRODUCT.PLUTONIUM,
pu_fallback = false
} }
} }
@@ -48,19 +51,23 @@ function process.init(iocontrol, coord_comms)
log.error("process.init(): failed to load coordinator settings file") log.error("process.init(): failed to load coordinator settings file")
end end
-- facility auto control configuration
local config = settings.get("PROCESS") ---@type coord_auto_config|nil 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 self.config.mode = config.mode
self.config.burn_target = config.burn_target self.config.burn_target = config.burn_target
self.config.charge_target = config.charge_target self.config.charge_target = config.charge_target
self.config.gen_target = config.gen_target self.config.gen_target = config.gen_target
self.config.limits = config.limits self.config.limits = config.limits
self.config.waste_product = config.waste_product
self.config.pu_fallback = config.pu_fallback
self.io.facility.ps.publish("process_mode", self.config.mode) self.io.facility.ps.publish("process_mode", self.config.mode)
self.io.facility.ps.publish("process_burn_target", self.config.burn_target) self.io.facility.ps.publish("process_burn_target", self.config.burn_target)
self.io.facility.ps.publish("process_charge_target", self.config.charge_target) self.io.facility.ps.publish("process_charge_target", self.config.charge_target)
self.io.facility.ps.publish("process_gen_target", self.config.gen_target) self.io.facility.ps.publish("process_gen_target", self.config.gen_target)
self.io.facility.ps.publish("process_waste_product", self.config.waste_product)
self.io.facility.ps.publish("process_pu_fallback", self.config.pu_fallback)
for id = 1, math.min(#self.config.limits, self.io.facility.num_units) do for id = 1, math.min(#self.config.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
@@ -68,20 +75,24 @@ function process.init(iocontrol, coord_comms)
end end
log.info("PROCESS: loaded auto control settings from coord.settings") log.info("PROCESS: loaded auto control settings from coord.settings")
-- notify supervisor of auto waste config
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, self.config.waste_product)
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, self.config.pu_fallback)
end end
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil -- unit waste states
local waste_modes = settings.get("WASTE_MODES") ---@type table|nil
if type(waste_mode) == "table" then if type(waste_modes) == "table" then
for id, mode in pairs(waste_mode) do for id, mode in pairs(waste_modes) do
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 waste mode settings from coord.settings") log.info("PROCESS: loaded unit waste mode settings from coord.settings")
end end
-- unit priority groups
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil local prio_groups = settings.get("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.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group) self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group)
@@ -137,7 +148,7 @@ end
-- set waste mode -- set waste mode
---@param id integer unit ID ---@param id integer unit ID
---@param mode integer waste mode ---@param mode integer waste mode
function process.set_waste(id, mode) function process.set_unit_waste(id, mode)
-- publish so that if it fails then it gets reset -- publish so that if it fails then it gets reset
self.io.units[id].unit_ps.publish("U_WasteMode", mode) self.io.units[id].unit_ps.publish("U_WasteMode", mode)
@@ -153,7 +164,7 @@ function process.set_waste(id, mode)
settings.set("WASTE_MODES", waste_mode) settings.set("WASTE_MODES", waste_mode)
if not settings.save("/coord.settings") then if not settings.save("/coord.settings") then
log.error("process.set_waste(): failed to save coordinator settings file") log.error("process.set_unit_waste(): failed to save coordinator settings file")
end end
end end
@@ -204,6 +215,24 @@ end
-- AUTO PROCESS CONTROL -- -- AUTO PROCESS CONTROL --
-------------------------- --------------------------
-- write auto process control to config file
local function _write_auto_config()
-- attempt to load settings
if not settings.load("/coord.settings") then
log.warning("process._write_auto_config(): failed to load coordinator settings file")
end
-- save config
settings.set("PROCESS", self.config)
local saved = settings.save("/coord.settings")
if not saved then
log.warning("process._write_auto_config(): failed to save coordinator settings file")
end
return not not saved
end
-- stop automatic process control -- stop automatic process control
function process.stop_auto() function process.stop_auto()
self.comms.send_fac_command(FAC_COMMAND.STOP) self.comms.send_fac_command(FAC_COMMAND.STOP)
@@ -216,6 +245,30 @@ function process.start_auto()
log.debug("PROCESS: START AUTO CTL") log.debug("PROCESS: START AUTO CTL")
end end
-- set automatic process control waste mode
---@param product WASTE_PRODUCT waste product for auto control
function process.set_process_waste(product)
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, product)
log.debug(util.c("PROCESS: SET WASTE ", product))
-- update config table and save
self.config.waste_product = product
_write_auto_config()
end
-- set automatic process control plutonium fallback
---@param enabled boolean whether to enable plutonium fallback
function process.set_pu_fallback(enabled)
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, enabled)
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
-- update config table and save
self.config.pu_fallback = enabled
_write_auto_config()
end
-- save process control settings -- save process control settings
---@param mode PROCESS control mode ---@param mode PROCESS control mode
---@param burn_target number burn rate target ---@param burn_target number burn rate target
@@ -223,29 +276,17 @@ end
---@param gen_target number generation rate target ---@param gen_target number generation rate target
---@param limits table unit burn rate limits ---@param limits table unit burn rate limits
function process.save(mode, burn_target, charge_target, gen_target, limits) function process.save(mode, burn_target, charge_target, gen_target, limits)
-- attempt to load settings log.debug("PROCESS: SAVE")
if not settings.load("/coord.settings") then
log.warning("process.save(): failed to load coordinator settings file")
end
-- config table -- update config table
self.config = { self.config.mode = mode
mode = mode, self.config.burn_target = burn_target
burn_target = burn_target, self.config.charge_target = charge_target
charge_target = charge_target, self.config.gen_target = gen_target
gen_target = gen_target, self.config.limits = limits
limits = limits
}
-- save config -- save config
settings.set("PROCESS", self.config) self.io.facility.save_cfg_ack(_write_auto_config())
local saved = settings.save("/coord.settings")
if not saved then
log.warning("process.save(): failed to save coordinator settings file")
end
self.io.facility.save_cfg_ack(saved)
end end
-- handle a start command acknowledgement -- handle a start command acknowledgement
@@ -258,16 +299,33 @@ function process.start_ack_handle(response)
self.config.charge_target = response[4] self.config.charge_target = response[4]
self.config.gen_target = response[5] self.config.gen_target = response[5]
for i = 1, #response[6] do for i = 1, math.min(#response[6], self.io.facility.num_units) do
self.config.limits[i] = response[6][i] self.config.limits[i] = response[6][i]
local unit = self.io.units[i] ---@type ioctl_unit
unit.unit_ps.publish("burn_limit", self.config.limits[i])
end end
self.io.facility.ps.publish("auto_mode", self.config.mode) self.io.facility.ps.publish("process_mode", self.config.mode)
self.io.facility.ps.publish("burn_target", self.config.burn_target) self.io.facility.ps.publish("process_burn_target", self.config.burn_target)
self.io.facility.ps.publish("charge_target", self.config.charge_target) self.io.facility.ps.publish("process_charge_target", self.config.charge_target)
self.io.facility.ps.publish("gen_target", self.config.gen_target) self.io.facility.ps.publish("process_gen_target", self.config.gen_target)
self.io.facility.start_ack(ack) self.io.facility.start_ack(ack)
end end
-- record waste product state after attempting to change it
---@param response WASTE_PRODUCT supervisor waste product state
function process.waste_ack_handle(response)
self.config.waste_product = response
self.io.facility.ps.publish("process_waste_product", response)
end
-- record plutonium fallback state after attempting to change it
---@param response boolean supervisor plutonium fallback state
function process.pu_fb_ack_handle(response)
self.config.pu_fallback = response
self.io.facility.ps.publish("process_pu_fallback", response)
end
return process return process

View File

@@ -5,8 +5,13 @@
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 style = require("coordinator.ui.style") local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local pgi = require("coordinator.ui.pgi")
local flow_view = require("coordinator.ui.layout.flow_view")
local panel_view = require("coordinator.ui.layout.front_panel")
local main_view = require("coordinator.ui.layout.main_view") local main_view = require("coordinator.ui.layout.main_view")
local unit_view = require("coordinator.ui.layout.unit_view") local unit_view = require("coordinator.ui.layout.unit_view")
@@ -21,10 +26,14 @@ local engine = {
monitors = nil, ---@type monitors_struct|nil monitors = nil, ---@type monitors_struct|nil
dmesg_window = nil, ---@type table|nil dmesg_window = nil, ---@type table|nil
ui_ready = false, ui_ready = false,
fp_ready = false,
ui = { ui = {
front_panel = nil, ---@type graphics_element|nil
main_display = nil, ---@type graphics_element|nil main_display = nil, ---@type graphics_element|nil
flow_display = nil, ---@type graphics_element|nil
unit_displays = {} unit_displays = {}
} },
disable_flow_view = false
} }
-- init a display to the "default", but set text scale to 0.5 -- init a display to the "default", but set text scale to 0.5
@@ -42,39 +51,44 @@ local function _init_display(monitor)
end end
end end
-- disable the flow view
---@param disable boolean
function renderer.legacy_disable_flow_view(disable)
engine.disable_flow_view = disable
end
-- link to the monitor peripherals -- link to the monitor peripherals
---@param monitors monitors_struct ---@param monitors monitors_struct
function renderer.set_displays(monitors) function renderer.set_displays(monitors)
engine.monitors = monitors engine.monitors = monitors
end
-- check if the renderer is configured to use a given monitor peripheral -- report to front panel as connected
---@nodiscard iocontrol.fp_monitor_state("main", engine.monitors.primary ~= nil)
---@param periph table peripheral iocontrol.fp_monitor_state("flow", engine.monitors.flow ~= nil)
---@return boolean is_used for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end
function renderer.is_monitor_used(periph)
if engine.monitors ~= nil then
if engine.monitors.primary == periph then
return true
else
for _, monitor in ipairs(engine.monitors.unit_displays) do
if monitor == periph then return true end
end
end
end
return false
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 monitor -- init primary and flow monitors
_init_display(engine.monitors.primary) _init_display(engine.monitors.primary)
if not engine.disable_flow_view then _init_display(engine.monitors.flow) end
-- init unit displays -- init unit displays
for _, monitor in ipairs(engine.monitors.unit_displays) do for _, monitor in ipairs(engine.monitors.unit_displays) do
_init_display(monitor) _init_display(monitor)
end end
-- init terminal
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
-- set overridden colors
for i = 1, #style.fp.colors do
term.setPaletteColor(style.fp.colors[i].c, style.fp.colors[i].hex)
end
end end
-- check main display width -- check main display width
@@ -85,6 +99,14 @@ function renderer.validate_main_display_width()
return w == 164 return w == 164
end 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 -- check display sizes
---@nodiscard ---@nodiscard
---@return boolean valid all unit display dimensions OK ---@return boolean valid all unit display dimensions OK
@@ -109,6 +131,51 @@ function renderer.init_dmesg()
log.direct_dmesg(engine.dmesg_window) log.direct_dmesg(engine.dmesg_window)
end end
-- start the coordinator front panel
function renderer.start_fp()
if not engine.fp_ready then
-- show front panel view on terminal
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root}
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
-- start flasher callback task
flasher.run()
-- report front panel as ready
engine.fp_ready = true
end
end
-- close out the front panel
function renderer.close_fp()
if engine.fp_ready then
if not engine.ui_ready then
-- stop blinking indicators
flasher.clear()
end
-- disable PGI
pgi.unlink()
-- hide to stop animation callbacks and clear root UI elements
engine.ui.front_panel.hide()
engine.ui.front_panel = nil
engine.fp_ready = false
-- restore colors
for i = 1, #style.colors do
local r, g, b = term.nativePaletteColor(style.colors[i].c)
term.setPaletteColor(style.colors[i].c, r, g, b)
end
-- reset terminal
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
end
end
-- start the coordinator GUI -- start the coordinator GUI
function renderer.start_ui() function renderer.start_ui()
if not engine.ui_ready then if not engine.ui_ready then
@@ -116,13 +183,21 @@ function renderer.start_ui()
engine.dmesg_window.setVisible(false) engine.dmesg_window.setVisible(false)
-- show main view on main monitor -- show main view on main monitor
engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root} if engine.monitors.primary ~= nil then
main_view(engine.ui.main_display) engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root}
main_view(engine.ui.main_display)
end
-- show flow view on flow monitor
if engine.monitors.flow ~= nil then
engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root}
flow_view(engine.ui.flow_display)
end
-- show unit views on unit displays -- show unit views on unit displays
for i = 1, #engine.monitors.unit_displays do for idx, display in pairs(engine.monitors.unit_displays) do
engine.ui.unit_displays[i] = DisplayBox{window=engine.monitors.unit_displays[i],fg_bg=style.root} engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
unit_view(engine.ui.unit_displays[i], i) unit_view(engine.ui.unit_displays[idx], idx)
end end
-- start flasher callback task -- start flasher callback task
@@ -135,18 +210,22 @@ end
-- close out the UI -- close out the UI
function renderer.close_ui() function renderer.close_ui()
-- stop blinking indicators if not engine.fp_ready then
flasher.clear() -- stop blinking indicators
flasher.clear()
end
-- hide to stop animation callbacks -- delete element trees
if engine.ui.main_display ~= nil then engine.ui.main_display.hide() end if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end
for _, display in ipairs(engine.ui.unit_displays) do display.hide() end if engine.ui.flow_display ~= nil then engine.ui.flow_display.delete() end
for _, display in pairs(engine.ui.unit_displays) do display.delete() end
-- report ui as not ready -- report ui as not ready
engine.ui_ready = false engine.ui_ready = false
-- clear root UI elements -- clear root UI elements
engine.ui.main_display = nil engine.ui.main_display = nil
engine.ui.flow_display = nil
engine.ui.unit_displays = {} engine.ui.unit_displays = {}
-- clear unit monitors -- clear unit monitors
@@ -157,22 +236,145 @@ function renderer.close_ui()
engine.dmesg_window.redraw() engine.dmesg_window.redraw()
end end
-- is the front panel ready?
---@nodiscard
---@return boolean ready
function renderer.fp_ready() return engine.fp_ready end
-- is the UI ready? -- is the UI ready?
---@nodiscard ---@nodiscard
---@return boolean ready ---@return boolean ready
function renderer.ui_ready() return engine.ui_ready end function renderer.ui_ready() return engine.ui_ready end
-- handle a touch event -- handle a monitor peripheral being disconnected
---@param event mouse_interaction ---@param device table monitor
function renderer.handle_mouse(event) ---@return boolean is_used if the monitor is one of the configured monitors
if engine.ui_ready then function renderer.handle_disconnect(device)
if event.monitor == engine.monitors.primary_name then local is_used = false
engine.ui.main_display.handle_mouse(event)
if engine.monitors ~= nil then
if engine.monitors.primary == device then
if engine.ui.main_display ~= nil then
-- delete element tree and clear root UI elements
engine.ui.main_display.delete()
end
is_used = true
engine.monitors.primary = nil
engine.ui.main_display = nil
iocontrol.fp_monitor_state("main", false)
elseif engine.monitors.flow == device then
if engine.ui.flow_display ~= nil then
-- delete element tree and clear root UI elements
engine.ui.flow_display.delete()
end
is_used = true
engine.monitors.flow = nil
engine.ui.flow_display = nil
iocontrol.fp_monitor_state("flow", false)
else else
for id, monitor in ipairs(engine.monitors.unit_name_map) do for idx, monitor in pairs(engine.monitors.unit_displays) do
if event.monitor == monitor then if monitor == device then
local layout = engine.ui.unit_displays[id] ---@type graphics_element if engine.ui.unit_displays[idx] ~= nil then
layout.handle_mouse(event) engine.ui.unit_displays[idx].delete()
end
is_used = true
engine.monitors.unit_displays[idx] = nil
engine.ui.unit_displays[idx] = nil
iocontrol.fp_monitor_state(idx, false)
break
end
end
end
end
return is_used
end
-- handle a monitor peripheral being reconnected
---@param name string monitor name
---@param device table monitor
---@return boolean is_used if the monitor is one of the configured monitors
function renderer.handle_reconnect(name, device)
local is_used = false
if engine.monitors ~= nil then
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()
engine.dmesg_window.reposition(1, 1, disp_x, disp_y, engine.monitors.primary)
if engine.ui_ready and (engine.ui.main_display == nil) then
engine.dmesg_window.setVisible(false)
engine.ui.main_display = DisplayBox{window=device,fg_bg=style.root}
main_view(engine.ui.main_display)
else
engine.dmesg_window.setVisible(true)
engine.dmesg_window.redraw()
end
iocontrol.fp_monitor_state("main", true)
elseif engine.monitors.flow_name == name then
is_used = true
_init_display(device)
engine.monitors.flow = device
if engine.ui_ready and (engine.ui.flow_display == nil) then
engine.ui.flow_display = DisplayBox{window=device,fg_bg=style.root}
flow_view(engine.ui.flow_display)
end
iocontrol.fp_monitor_state("flow", true)
else
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
if monitor == name then
is_used = true
_init_display(device)
engine.monitors.unit_displays[idx] = device
if engine.ui_ready and (engine.ui.unit_displays[idx] == nil) then
engine.ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
unit_view(engine.ui.unit_displays[idx], idx)
end
iocontrol.fp_monitor_state(idx, true)
break
end
end
end
end
return is_used
end
-- handle a touch event
---@param event mouse_interaction|nil
function renderer.handle_mouse(event)
if event ~= nil then
if engine.fp_ready and event.monitor == "terminal" then
engine.ui.front_panel.handle_mouse(event)
elseif engine.ui_ready then
if event.monitor == engine.monitors.primary_name then
engine.ui.main_display.handle_mouse(event)
elseif event.monitor == engine.monitors.flow_name then
engine.ui.flow_display.handle_mouse(event)
else
for id, monitor in ipairs(engine.monitors.unit_name_map) do
if event.monitor == monitor then
local layout = engine.ui.unit_displays[id] ---@type graphics_element
layout.handle_mouse(event)
break
end
end end
end end
end end

View File

@@ -1,16 +1,17 @@
local log = require("scada-common.log") 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 config = require("coordinator.config")
local iocontrol = require("coordinator.iocontrol")
local api = require("coordinator.session.api") local pocket = require("coordinator.session.pocket")
local apisessions = {} local apisessions = {}
local self = { local self = {
modem = nil, nic = nil,
next_id = 0, next_id = 0,
sessions = {} sessions = {}
} }
@@ -18,7 +19,7 @@ local self = {
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- handle a session output queue -- handle a session output queue
---@param session api_session_struct ---@param session pkt_session_struct
local function _api_handle_outq(session) local function _api_handle_outq(session)
-- record handler start time -- record handler start time
local handle_start = util.time() local handle_start = util.time()
@@ -31,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.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) self.nic.transmit(config.PKT_CHANNEL, 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
@@ -41,15 +42,15 @@ local function _api_handle_outq(session)
-- max 100ms spent processing queue -- max 100ms spent processing queue
if util.time() - handle_start > 100 then if util.time() - handle_start > 100 then
log.warning("API out queue handler exceeded 100ms queue process limit") log.warning("[API] out queue handler exceeded 100ms queue process limit")
log.warning(util.c("offending session: port ", session.r_port)) log.warning(util.c("[API] offending session: ", session))
break break
end end
end end
end end
-- cleanly close a session -- cleanly close a session
---@param session api_session_struct ---@param session pkt_session_struct
local function _shutdown(session) local function _shutdown(session)
session.open = false session.open = false
session.instance.close() session.instance.close()
@@ -58,77 +59,78 @@ 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.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) self.nic.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message)
end end
end end
log.debug(util.c("closed API session ", session.instance.get_id(), " on remote port ", session.r_port)) log.debug(util.c("[API] closed session ", session))
end end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
-- initialize apisessions -- initialize apisessions
---@param modem table ---@param nic nic
function apisessions.init(modem) function apisessions.init(nic)
self.modem = modem self.nic = nic
end
-- re-link the modem
---@param modem table
function apisessions.relink_modem(modem)
self.modem = modem
end end
-- find a session by remote port -- find a session by remote port
---@nodiscard ---@nodiscard
---@param port integer ---@param source_addr integer
---@return api_session_struct|nil ---@return pkt_session_struct|nil
function apisessions.find_session(port) function apisessions.find_session(source_addr)
for i = 1, #self.sessions do for i = 1, #self.sessions do
if self.sessions[i].r_port == port then return self.sessions[i] end if self.sessions[i].s_addr == source_addr then return self.sessions[i] end
end end
return nil return nil
end end
-- establish a new API session -- establish a new API session
---@nodiscard ---@nodiscard
---@param local_port integer ---@param source_addr integer
---@param remote_port integer
---@param version string ---@param version string
---@return integer session_id ---@return integer session_id
function apisessions.establish_session(local_port, remote_port, version) function apisessions.establish_session(source_addr, version)
---@class api_session_struct ---@class pkt_session_struct
local api_s = { local pkt_s = {
open = true, open = true,
version = version, version = version,
l_port = local_port, s_addr = source_addr,
r_port = remote_port,
in_queue = mqueue.new(), in_queue = mqueue.new(),
out_queue = mqueue.new(), out_queue = mqueue.new(),
instance = nil ---@type api_session instance = nil ---@type pkt_session
} }
api_s.instance = api.new_session(self.next_id, api_s.in_queue, api_s.out_queue, config.API_TIMEOUT) local id = self.next_id
table.insert(self.sessions, api_s)
log.debug(util.c("established new API session to ", remote_port, " with ID ", self.next_id)) pkt_s.instance = pocket.new_session(id, source_addr, pkt_s.in_queue, pkt_s.out_queue, config.API_TIMEOUT)
table.insert(self.sessions, pkt_s)
self.next_id = self.next_id + 1 local mt = {
---@param s pkt_session_struct
__tostring = function (s) return util.c("PKT [", id, "] (@", s.s_addr, ")") end
}
setmetatable(pkt_s, mt)
iocontrol.fp_pkt_connected(id, version, source_addr)
log.debug(util.c("[API] established new session: ", pkt_s))
self.next_id = id + 1
-- success -- success
return api_s.instance.get_id() return pkt_s.instance.get_id()
end end
-- attempt to identify which session's watchdog timer fired -- attempt to identify which session's watchdog timer fired
---@param timer_event number ---@param timer_event number
function apisessions.check_all_watchdogs(timer_event) function apisessions.check_all_watchdogs(timer_event)
for i = 1, #self.sessions do for i = 1, #self.sessions do
local session = self.sessions[i] ---@type api_session_struct local session = self.sessions[i] ---@type pkt_session_struct
if session.open then if session.open then
local triggered = session.instance.check_wd(timer_event) local triggered = session.instance.check_wd(timer_event)
if triggered then if triggered then
log.debug(util.c("watchdog closing API session ", session.instance.get_id(), log.debug(util.c("[API] watchdog closing session ", session, "..."))
" on remote port ", session.r_port, "..."))
_shutdown(session) _shutdown(session)
end end
end end
@@ -138,7 +140,7 @@ end
-- iterate all the API sessions -- iterate all the API sessions
function apisessions.iterate_all() function apisessions.iterate_all()
for i = 1, #self.sessions do for i = 1, #self.sessions do
local session = self.sessions[i] ---@type api_session_struct local session = self.sessions[i] ---@type pkt_session_struct
if session.open and session.instance.iterate() then if session.open and session.instance.iterate() then
_api_handle_outq(session) _api_handle_outq(session)
@@ -152,10 +154,9 @@ end
function apisessions.free_all_closed() function apisessions.free_all_closed()
local f = function (session) return session.open end local f = function (session) return session.open end
---@param session api_session_struct ---@param session pkt_session_struct
local on_delete = function (session) local on_delete = function (session)
log.debug(util.c("free'ing closed API session ", session.instance.get_id(), log.debug(util.c("[API] free'ing closed session ", session))
" on remote port ", session.r_port))
end end
util.filter_table(self.sessions, f, on_delete) util.filter_table(self.sessions, f, on_delete)
@@ -164,7 +165,7 @@ end
-- close all open connections -- close all open connections
function apisessions.close_all() function apisessions.close_all()
for i = 1, #self.sessions do for i = 1, #self.sessions do
local session = self.sessions[i] ---@type api_session_struct local session = self.sessions[i] ---@type pkt_session_struct
if session.open then _shutdown(session) end if session.open then _shutdown(session) end
end end

View File

@@ -1,16 +1,16 @@
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local log = require("scada-common.log") 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 api = {} local iocontrol = require("coordinator.iocontrol")
local pocket = {}
local PROTOCOL = comms.PROTOCOL local PROTOCOL = comms.PROTOCOL
-- local CAPI_TYPE = comms.CAPI_TYPE -- local CAPI_TYPE = comms.CAPI_TYPE
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local println = util.println
-- retry time constants in ms -- retry time constants in ms
-- local INITIAL_WAIT = 1500 -- local INITIAL_WAIT = 1500
-- local RETRY_PERIOD = 1000 -- local RETRY_PERIOD = 1000
@@ -21,8 +21,8 @@ local API_S_CMDS = {
local API_S_DATA = { local API_S_DATA = {
} }
api.API_S_CMDS = API_S_CMDS pocket.API_S_CMDS = API_S_CMDS
api.API_S_DATA = API_S_DATA pocket.API_S_DATA = API_S_DATA
local PERIODICS = { local PERIODICS = {
KEEP_ALIVE = 2000 KEEP_ALIVE = 2000
@@ -31,11 +31,12 @@ local PERIODICS = {
-- pocket API session -- pocket API session
---@nodiscard ---@nodiscard
---@param id integer session ID ---@param id integer session ID
---@param s_addr integer device source address
---@param in_queue mqueue in message queue ---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue ---@param out_queue mqueue out message queue
---@param timeout number communications timeout ---@param timeout number communications timeout
function api.new_session(id, in_queue, out_queue, timeout) function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
local log_header = "api_session(" .. id .. "): " local log_header = "pkt_session(" .. id .. "): "
local self = { local self = {
-- connection properties -- connection properties
@@ -61,13 +62,14 @@ function api.new_session(id, in_queue, out_queue, timeout)
} }
} }
---@class api_session ---@class pkt_session
local public = {} local public = {}
-- mark this API session as closed, stop watchdog -- mark this pocket session as closed, stop watchdog
local function _close() local function _close()
self.conn_watchdog.cancel() self.conn_watchdog.cancel()
self.connected = false self.connected = false
iocontrol.fp_pkt_disconnected(id)
end end
-- send a CAPI packet -- send a CAPI packet
@@ -92,7 +94,7 @@ function api.new_session(id, in_queue, out_queue, timeout)
local m_pkt = comms.mgmt_packet() local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg) m_pkt.make(msg_type, msg)
s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
out_queue.push_packet(s_pkt) out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
@@ -117,8 +119,6 @@ function api.new_session(id, in_queue, out_queue, timeout)
-- process packet -- process packet
if pkt.scada_frame.protocol() == PROTOCOL.COORD_API then if pkt.scada_frame.protocol() == PROTOCOL.COORD_API then
---@cast pkt capi_frame ---@cast pkt capi_frame
-- feed watchdog
self.conn_watchdog.feed()
-- handle packet by type -- handle packet by type
if pkt.type == nil then if pkt.type == nil then
@@ -136,11 +136,13 @@ function api.new_session(id, in_queue, out_queue, timeout)
self.last_rtt = srv_now - srv_start self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then if self.last_rtt > 750 then
log.warning(log_header .. "API KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)") log.warning(log_header .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
end end
-- log.debug(log_header .. "API RTT = " .. self.last_rtt .. "ms") -- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "API TT = " .. (srv_now - api_send) .. "ms") -- log.debug(log_header .. "PKT TT = " .. (srv_now - api_send) .. "ms")
iocontrol.fp_pkt_rtt(id, self.last_rtt)
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_header .. "SCADA keep alive packet length mismatch")
end end
@@ -173,7 +175,6 @@ function api.new_session(id, in_queue, out_queue, timeout)
function public.close() function public.close()
_close() _close()
_send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
println("connection to API session " .. id .. " closed by server")
log.info(log_header .. "session closed by server") log.info(log_header .. "session closed by server")
end end
@@ -212,7 +213,6 @@ function api.new_session(id, in_queue, out_queue, timeout)
-- exit if connection was closed -- exit if connection was closed
if not self.connected then if not self.connected then
println("connection to API session " .. id .. " closed by remote host")
log.info(log_header .. "session closed by remote host") log.info(log_header .. "session closed by remote host")
return self.connected return self.connected
end end
@@ -248,4 +248,4 @@ function api.new_session(id, in_queue, out_queue, timeout)
return public return public
end end
return api return pocket

View File

@@ -2,269 +2,25 @@
-- Alarm Sounder -- Alarm Sounder
-- --
local audio = require("scada-common.audio")
local log = require("scada-common.log") local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE
---@class sounder ---@class sounder
local sounder = {} local sounder = {}
-- note: max samples = 0x20000 (128 * 1024 samples)
local _2_PI = 2 * math.pi -- 2 whole pies, hope you're hungry
local _DRATE = 48000 -- 48kHz audio
local _MAX_VAL = 127 / 2 -- max signed integer in this 8-bit audio
local _05s_SAMPLES = 24000 -- half a second worth of samples
local test_alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
local alarm_ctl = { local alarm_ctl = {
speaker = nil, speaker = nil,
volume = 0.5, volume = 0.5,
playing = false, stream = audio.new_stream()
num_active = 0,
next_block = 1,
-- split audio up into 0.5s samples so specific components can be ended quicker
quad_buffer = { {}, {}, {}, {} }
} }
-- sounds modeled after https://www.e2s.com/references-and-guidelines/listen-and-download-alarm-tones
local T_340Hz_Int_2Hz = 1
local T_544Hz_440Hz_Alt = 2
local T_660Hz_Int_125ms = 3
local T_745Hz_Int_1Hz = 4
local T_800Hz_Int = 5
local T_800Hz_1000Hz_Alt = 6
local T_1000Hz_Int = 7
local T_1800Hz_Int_4Hz = 8
local TONES = {
{ active = false, component = { {}, {}, {}, {} } }, -- 340Hz @ 2Hz Intermittent
{ active = false, component = { {}, {}, {}, {} } }, -- 544Hz 100mS / 440Hz 400mS Alternating
{ active = false, component = { {}, {}, {}, {} } }, -- 660Hz @ 125ms On 125ms Off
{ active = false, component = { {}, {}, {}, {} } }, -- 745Hz @ 1Hz Intermittent
{ active = false, component = { {}, {}, {}, {} } }, -- 800Hz @ 0.25s On 1.75s Off
{ active = false, component = { {}, {}, {}, {} } }, -- 800/1000Hz @ 0.25s Alternating
{ active = false, component = { {}, {}, {}, {} } }, -- 1KHz 1s on, 1s off Intermittent
{ active = false, component = { {}, {}, {}, {} } } -- 1.8KHz @ 4Hz Intermittent
}
-- calculate how many samples are in the given number of milliseconds
---@nodiscard
---@param ms integer milliseconds
---@return integer samples
local function ms_to_samples(ms) return math.floor(ms * 48) end
--#region Tone Generation (the Maths)
-- 340Hz @ 2Hz Intermittent
local function gen_tone_1()
local t, dt = 0, _2_PI * 340 / _DRATE
for i = 1, _05s_SAMPLES do
local val = math.floor(math.sin(t) * _MAX_VAL)
TONES[1].component[1][i] = val
TONES[1].component[3][i] = val
TONES[1].component[2][i] = 0
TONES[1].component[4][i] = 0
t = (t + dt) % _2_PI
end
end
-- 544Hz 100mS / 440Hz 400mS Alternating
local function gen_tone_2()
local t1, dt1 = 0, _2_PI * 544 / _DRATE
local t2, dt2 = 0, _2_PI * 440 / _DRATE
local alternate_at = ms_to_samples(100)
for i = 1, _05s_SAMPLES do
local value
if i <= alternate_at then
value = math.floor(math.sin(t1) * _MAX_VAL)
t1 = (t1 + dt1) % _2_PI
else
value = math.floor(math.sin(t2) * _MAX_VAL)
t2 = (t2 + dt2) % _2_PI
end
TONES[2].component[1][i] = value
TONES[2].component[2][i] = value
TONES[2].component[3][i] = value
TONES[2].component[4][i] = value
end
end
-- 660Hz @ 125ms On 125ms Off
local function gen_tone_3()
local elapsed_samples = 0
local alternate_after = ms_to_samples(125)
local alternate_at = alternate_after
local mode = true
local t, dt = 0, _2_PI * 660 / _DRATE
for set = 1, 4 do
for i = 1, _05s_SAMPLES do
if mode then
local val = math.floor(math.sin(t) * _MAX_VAL)
TONES[3].component[set][i] = val
t = (t + dt) % _2_PI
else
t = 0
TONES[3].component[set][i] = 0
end
if elapsed_samples == alternate_at then
mode = not mode
alternate_at = elapsed_samples + alternate_after
end
elapsed_samples = elapsed_samples + 1
end
end
end
-- 745Hz @ 1Hz Intermittent
local function gen_tone_4()
local t, dt = 0, _2_PI * 745 / _DRATE
for i = 1, _05s_SAMPLES do
local val = math.floor(math.sin(t) * _MAX_VAL)
TONES[4].component[1][i] = val
TONES[4].component[3][i] = val
TONES[4].component[2][i] = 0
TONES[4].component[4][i] = 0
t = (t + dt) % _2_PI
end
end
-- 800Hz @ 0.25s On 1.75s Off
local function gen_tone_5()
local t, dt = 0, _2_PI * 800 / _DRATE
local stop_at = ms_to_samples(250)
for i = 1, _05s_SAMPLES do
local val = math.floor(math.sin(t) * _MAX_VAL)
if i > stop_at then
TONES[5].component[1][i] = val
else
TONES[5].component[1][i] = 0
end
TONES[5].component[2][i] = 0
TONES[5].component[3][i] = 0
TONES[5].component[4][i] = 0
t = (t + dt) % _2_PI
end
end
-- 1000/800Hz @ 0.25s Alternating
local function gen_tone_6()
local t1, dt1 = 0, _2_PI * 1000 / _DRATE
local t2, dt2 = 0, _2_PI * 800 / _DRATE
local alternate_at = ms_to_samples(250)
for i = 1, _05s_SAMPLES do
local val
if i <= alternate_at then
val = math.floor(math.sin(t1) * _MAX_VAL)
t1 = (t1 + dt1) % _2_PI
else
val = math.floor(math.sin(t2) * _MAX_VAL)
t2 = (t2 + dt2) % _2_PI
end
TONES[6].component[1][i] = val
TONES[6].component[2][i] = val
TONES[6].component[3][i] = val
TONES[6].component[4][i] = val
end
end
-- 1KHz 1s on, 1s off Intermittent
local function gen_tone_7()
local t, dt = 0, _2_PI * 1000 / _DRATE
for i = 1, _05s_SAMPLES do
local val = math.floor(math.sin(t) * _MAX_VAL)
TONES[7].component[1][i] = val
TONES[7].component[2][i] = val
TONES[7].component[3][i] = 0
TONES[7].component[4][i] = 0
t = (t + dt) % _2_PI
end
end
-- 1800Hz @ 4Hz Intermittent
local function gen_tone_8()
local t, dt = 0, _2_PI * 1800 / _DRATE
local off_at = ms_to_samples(250)
for i = 1, _05s_SAMPLES do
local val = 0
if i <= off_at then
val = math.floor(math.sin(t) * _MAX_VAL)
t = (t + dt) % _2_PI
end
TONES[8].component[1][i] = val
TONES[8].component[2][i] = val
TONES[8].component[3][i] = val
TONES[8].component[4][i] = val
end
end
--#endregion
-- hard audio limiter
---@nodiscard
---@param output number output level
---@return number limited -128.0 to 127.0
local function limit(output)
return math.max(-128, math.min(127, output))
end
-- zero the alarm audio buffer
local function zero()
for i = 1, 4 do
for s = 1, _05s_SAMPLES do alarm_ctl.quad_buffer[i][s] = 0 end
end
end
-- add an alarm to the output buffer
---@param alarm_idx integer tone ID
local function add(alarm_idx)
alarm_ctl.num_active = alarm_ctl.num_active + 1
TONES[alarm_idx].active = true
for i = 1, 4 do
for s = 1, _05s_SAMPLES do
alarm_ctl.quad_buffer[i][s] = limit(alarm_ctl.quad_buffer[i][s] + TONES[alarm_idx].component[i][s])
end
end
end
-- start audio or continue audio on buffer empty -- start audio or continue audio on buffer empty
---@return boolean success successfully added buffer to audio output ---@return boolean success successfully added buffer to audio output
local function play() local function play()
if not alarm_ctl.playing then if not alarm_ctl.playing then
alarm_ctl.playing = true alarm_ctl.playing = true
alarm_ctl.next_block = 1
return sounder.continue() return sounder.continue()
else else return true end
return true
end
end end
-- initialize the annunciator alarm system -- initialize the annunciator alarm system
@@ -273,23 +29,10 @@ end
function sounder.init(speaker, volume) function sounder.init(speaker, volume)
alarm_ctl.speaker = speaker alarm_ctl.speaker = speaker
alarm_ctl.speaker.stop() alarm_ctl.speaker.stop()
alarm_ctl.volume = volume alarm_ctl.volume = volume
alarm_ctl.playing = false alarm_ctl.stream.stop()
alarm_ctl.num_active = 0
alarm_ctl.next_block = 1
zero() audio.generate_tones()
-- generate tones
gen_tone_1()
gen_tone_2()
gen_tone_3()
gen_tone_4()
gen_tone_5()
gen_tone_6()
gen_tone_7()
gen_tone_8()
end end
-- reconnect the speaker peripheral -- reconnect the speaker peripheral
@@ -297,173 +40,40 @@ end
function sounder.reconnect(speaker) function sounder.reconnect(speaker)
alarm_ctl.speaker = speaker alarm_ctl.speaker = speaker
alarm_ctl.playing = false alarm_ctl.playing = false
alarm_ctl.next_block = 1 alarm_ctl.stream.stop()
alarm_ctl.num_active = 0
for id = 1, #TONES do TONES[id].active = false end
end end
-- check alarm state to enable/disable alarms -- set alarm tones
---@param units table|nil unit list or nil to use test mode ---@param states table alarm tone commands from supervisor
function sounder.eval(units) function sounder.set(states)
local changed = false -- set tone states
local any_active = false for id = 1, #states do alarm_ctl.stream.set_active(id, states[id]) end
local new_states = { false, false, false, false, false, false, false, false }
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
if units ~= nil then -- re-compute output if needed, then play audio if available
-- check all alarms for all units if alarm_ctl.stream.is_recompute_needed() then alarm_ctl.stream.compute_buffer() end
for i = 1, #units do if alarm_ctl.stream.has_next_block() then play() else sounder.stop() end
local unit = units[i] ---@type ioctl_unit
for id = 1, #unit.alarms do
alarms[id] = alarms[id] or (unit.alarms[id] == ALARM_STATE.TRIPPED)
end
end
else
alarms = test_alarms
end
-- containment breach is worst case CRITICAL alarm, this takes priority
if alarms[ALARM.ContainmentBreach] then
new_states[T_1800Hz_Int_4Hz] = true
else
-- critical damage is highest priority CRITICAL level alarm
if alarms[ALARM.CriticalDamage] then
new_states[T_660Hz_Int_125ms] = true
else
-- EMERGENCY level alarms + URGENT over temp
if alarms[ALARM.ReactorDamage] or alarms[ALARM.ReactorOverTemp] or alarms[ALARM.ReactorWasteLeak] then
new_states[T_544Hz_440Hz_Alt] = true
-- URGENT level turbine trip
elseif alarms[ALARM.TurbineTrip] then
new_states[T_745Hz_Int_1Hz] = true
-- URGENT level reactor lost
elseif alarms[ALARM.ReactorLost] then
new_states[T_340Hz_Int_2Hz] = true
-- TIMELY level alarms
elseif alarms[ALARM.ReactorHighTemp] or alarms[ALARM.ReactorHighWaste] or alarms[ALARM.RCSTransient] then
new_states[T_800Hz_Int] = true
end
end
-- check RPS transient URGENT level alarm
if alarms[ALARM.RPSTransient] then
new_states[T_1000Hz_Int] = true
-- disable really painful audio combination
new_states[T_340Hz_Int_2Hz] = false
end
end
-- radiation is a big concern, always play this CRITICAL level alarm if active
if alarms[ALARM.ContainmentRadiation] then
new_states[T_800Hz_1000Hz_Alt] = true
-- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled
-- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one
if new_states[T_1000Hz_Int] and alarms[ALARM.ReactorLost] then new_states[T_340Hz_Int_2Hz] = true end
-- it sounds *really* bad if this is in conjunction with these other tones, so disable them
new_states[T_745Hz_Int_1Hz] = false
new_states[T_800Hz_Int] = false
new_states[T_1000Hz_Int] = false
end
-- check if any changed, check if any active, update active flags
for id = 1, #TONES do
if new_states[id] ~= TONES[id].active then
TONES[id].active = new_states[id]
changed = true
end
if TONES[id].active then any_active = true end
end
-- zero and re-add tones if changed
if changed then
zero()
for id = 1, #TONES do
if TONES[id].active then add(id) end
end
end
if any_active then play() else sounder.stop() end
end end
-- stop all audio and clear output buffer -- stop all audio and clear output buffer
function sounder.stop() function sounder.stop()
alarm_ctl.playing = false alarm_ctl.playing = false
alarm_ctl.speaker.stop() alarm_ctl.speaker.stop()
alarm_ctl.next_block = 1 alarm_ctl.stream.stop()
alarm_ctl.num_active = 0
for id = 1, #TONES do TONES[id].active = false end
zero()
end end
-- continue audio on buffer empty -- continue audio on buffer empty
---@return boolean success successfully added buffer to audio output ---@return boolean success successfully added buffer to audio output
function sounder.continue() function sounder.continue()
local success = false
if alarm_ctl.playing then if alarm_ctl.playing then
if alarm_ctl.speaker ~= nil and #alarm_ctl.quad_buffer[alarm_ctl.next_block] > 0 then if alarm_ctl.speaker ~= nil and alarm_ctl.stream.has_next_block() then
local success = alarm_ctl.speaker.playAudio(alarm_ctl.quad_buffer[alarm_ctl.next_block], alarm_ctl.volume) success = alarm_ctl.speaker.playAudio(alarm_ctl.stream.get_next_block(), alarm_ctl.volume)
if not success then log.error("SOUNDER: error playing audio") end
alarm_ctl.next_block = alarm_ctl.next_block + 1
if alarm_ctl.next_block > 4 then alarm_ctl.next_block = 1 end
if not success then
log.debug("SOUNDER: error playing audio")
end
return success
else
return false
end
else
return false
end
end
--#region Test Functions
function sounder.test_1() add(1) play() end -- play tone T_340Hz_Int_2Hz
function sounder.test_2() add(2) play() end -- play tone T_544Hz_440Hz_Alt
function sounder.test_3() add(3) play() end -- play tone T_660Hz_Int_125ms
function sounder.test_4() add(4) play() end -- play tone T_745Hz_Int_1Hz
function sounder.test_5() add(5) play() end -- play tone T_800Hz_Int
function sounder.test_6() add(6) play() end -- play tone T_800Hz_1000Hz_Alt
function sounder.test_7() add(7) play() end -- play tone T_1000Hz_Int
function sounder.test_8() add(8) play() end -- play tone T_1800Hz_Int_4Hz
function sounder.test_breach(active) test_alarms[ALARM.ContainmentBreach] = active end ---@param active boolean
function sounder.test_rad(active) test_alarms[ALARM.ContainmentRadiation] = active end ---@param active boolean
function sounder.test_lost(active) test_alarms[ALARM.ReactorLost] = active end ---@param active boolean
function sounder.test_crit(active) test_alarms[ALARM.CriticalDamage] = active end ---@param active boolean
function sounder.test_dmg(active) test_alarms[ALARM.ReactorDamage] = active end ---@param active boolean
function sounder.test_overtemp(active) test_alarms[ALARM.ReactorOverTemp] = active end ---@param active boolean
function sounder.test_hightemp(active) test_alarms[ALARM.ReactorHighTemp] = active end ---@param active boolean
function sounder.test_wasteleak(active) test_alarms[ALARM.ReactorWasteLeak] = active end ---@param active boolean
function sounder.test_highwaste(active) test_alarms[ALARM.ReactorHighWaste] = active end ---@param active boolean
function sounder.test_rps(active) test_alarms[ALARM.RPSTransient] = active end ---@param active boolean
function sounder.test_rcs(active) test_alarms[ALARM.RCSTransient] = active end ---@param active boolean
function sounder.test_turbinet(active) test_alarms[ALARM.TurbineTrip] = active end ---@param active boolean
-- power rescaling limiter test
function sounder.test_power_scale()
local start = util.time_ms()
zero()
for id = 1, #TONES do
if TONES[id].active then
for i = 1, 4 do
for s = 1, _05s_SAMPLES do
alarm_ctl.quad_buffer[i][s] = limit(alarm_ctl.quad_buffer[i][s] +
(TONES[id].component[i][s] / math.sqrt(alarm_ctl.num_active)))
end
end
end end
end end
log.debug("SOUNDER: power rescale test took " .. (util.time_ms() - start) .. "ms") return success
end end
--#endregion
return sounder return sounder

View File

@@ -4,23 +4,25 @@
require("/initenv").init_env() require("/initenv").init_env()
local crash = require("scada-common.crash") local comms = require("scada-common.comms")
local log = require("scada-common.log") local crash = require("scada-common.crash")
local ppm = require("scada-common.ppm") local log = require("scada-common.log")
local tcallbackdsp = require("scada-common.tcallbackdsp") local network = require("scada-common.network")
local util = require("scada-common.util") local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local config = require("coordinator.config") local config = require("coordinator.config")
local coordinator = require("coordinator.coordinator") local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local renderer = require("coordinator.renderer") local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder") local sounder = require("coordinator.sounder")
local apisessions = require("coordinator.session.apisessions") local apisessions = require("coordinator.session.apisessions")
local COORDINATOR_VERSION = "v0.13.8" local COORDINATOR_VERSION = "v1.0.2"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@@ -29,7 +31,7 @@ local log_graphics = coordinator.log_graphics
local log_sys = coordinator.log_sys local log_sys = coordinator.log_sys
local log_boot = coordinator.log_boot local log_boot = coordinator.log_boot
local log_comms = coordinator.log_comms local log_comms = coordinator.log_comms
local log_comms_connecting = coordinator.log_comms_connecting local log_crypto = coordinator.log_crypto
---------------------------------------- ----------------------------------------
-- config validation -- config validation
@@ -37,9 +39,9 @@ local log_comms_connecting = coordinator.log_comms_connecting
local cfv = util.new_validator() local cfv = util.new_validator()
cfv.assert_port(config.SCADA_SV_PORT) cfv.assert_channel(config.SVR_CHANNEL)
cfv.assert_port(config.SCADA_SV_CTL_LISTEN) cfv.assert_channel(config.CRD_CHANNEL)
cfv.assert_port(config.SCADA_API_LISTEN) cfv.assert_channel(config.PKT_CHANNEL)
cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_int(config.TRUSTED_RANGE)
cfv.assert_type_num(config.SV_TIMEOUT) cfv.assert_type_num(config.SV_TIMEOUT)
cfv.assert_min(config.SV_TIMEOUT, 2) cfv.assert_min(config.SV_TIMEOUT, 2)
@@ -57,7 +59,7 @@ assert(cfv.valid(), "bad config file: missing/invalid fields")
-- log init -- log init
---------------------------------------- ----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE) log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
log.info("========================================") log.info("========================================")
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION) log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
@@ -78,8 +80,11 @@ local function main()
-- mount connected devices -- mount connected devices
ppm.mount_all() ppm.mount_all()
-- report versions/init fp PSIL
iocontrol.init_fp(COORDINATOR_VERSION, comms.version)
-- setup monitors -- setup monitors
local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS) local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS, config.DISABLE_FLOW_VIEW == true)
if not configured or monitors == nil then if not configured or monitors == nil then
println("startup> monitor setup failed") println("startup> monitor setup failed")
log.fatal("monitor configuration failed") log.fatal("monitor configuration failed")
@@ -87,6 +92,7 @@ local function main()
end end
-- init renderer -- init renderer
renderer.legacy_disable_flow_view(config.DISABLE_FLOW_VIEW == true)
renderer.set_displays(monitors) renderer.set_displays(monitors)
renderer.init_displays() renderer.init_displays()
@@ -94,6 +100,10 @@ local function main()
println("startup> main display must be 8 blocks wide") println("startup> main display must be 8 blocks wide")
log.fatal("main display not wide enough") log.fatal("main display not wide enough")
return 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 elseif not renderer.validate_unit_display_sizes() then
println("startup> one or more unit display dimensions incorrect; they must be 4x4 blocks") println("startup> one or more unit display dimensions incorrect; they must be 4x4 blocks")
log.fatal("unit display dimensions incorrect") log.fatal("unit display dimensions incorrect")
@@ -125,12 +135,19 @@ local function main()
sounder.init(speaker, config.SOUNDER_VOLUME) sounder.init(speaker, config.SOUNDER_VOLUME)
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)
end end
---------------------------------------- ----------------------------------------
-- setup communications -- setup communications
---------------------------------------- ----------------------------------------
-- message authentication init
if type(config.AUTH_KEY) == "string" then
local init_time = network.init_mac(config.AUTH_KEY)
log_crypto("HMAC init took " .. init_time .. "ms")
end
-- get the communications modem -- get the communications modem
local modem = ppm.get_wireless_modem() local modem = ppm.get_wireless_modem()
if modem == nil then if modem == nil then
@@ -140,6 +157,7 @@ local function main()
return return
else else
log_comms("wireless modem connected") log_comms("wireless modem connected")
iocontrol.fp_has_modem(true)
end end
-- create connection watchdog -- create connection watchdog
@@ -147,9 +165,10 @@ local function main()
conn_watchdog.cancel() conn_watchdog.cancel()
log.debug("startup> conn watchdog created") log.debug("startup> conn watchdog created")
-- start comms, open all channels -- create network interface then setup comms
local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.SCADA_SV_PORT, config.SCADA_SV_CTL_LISTEN, local nic = network.nic(modem)
config.SCADA_API_LISTEN, config.TRUSTED_RANGE, conn_watchdog) local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, config.NUM_UNITS, config.CRD_CHANNEL,
config.SVR_CHANNEL, config.PKT_CHANNEL, config.TRUSTED_RANGE, conn_watchdog)
log.debug("startup> comms init") log.debug("startup> comms init")
log_comms("comms initialized") log_comms("comms initialized")
@@ -158,78 +177,54 @@ local function main()
local loop_clock = util.new_clock(MAIN_CLOCK) local loop_clock = util.new_clock(MAIN_CLOCK)
---------------------------------------- ----------------------------------------
-- connect to the supervisor -- start front panel & UI start function
---------------------------------------- ----------------------------------------
-- attempt to connect to the supervisor or exit log_graphics("starting front panel UI...")
local function init_connect_sv()
local tick_waiting, task_done = log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SCADA_SV_PORT)
-- attempt to establish a connection with the supervisory computer local fp_ok, fp_message = pcall(renderer.start_fp)
if not coord_comms.sv_connect(60, tick_waiting, task_done) then if not fp_ok then
log_sys("supervisor connection failed, shutting down...") renderer.close_fp()
log.fatal("failed to connect to supervisor") log_graphics(util.c("front panel UI error: ", fp_message))
return false println_ts("front panel UI creation failed")
end log.fatal(util.c("front panel GUI render failed with error ", fp_message))
return true
end
if not init_connect_sv() then
println("startup> failed to connect to supervisor")
log_sys("system shutdown")
return return
else else log_graphics("front panel ready") end
log_sys("supervisor connected, proceeding to UI start")
end
---------------------------------------- -- start up the main UI
-- start the UI
----------------------------------------
-- start up the UI
---@return boolean ui_ok started ok ---@return boolean ui_ok started ok
local function init_start_ui() local function start_main_ui()
log_graphics("starting UI...") log_graphics("starting main UI...")
local draw_start = util.time_ms() local draw_start = util.time_ms()
local ui_ok, message = pcall(renderer.start_ui) local ui_ok, ui_message = pcall(renderer.start_ui)
if not ui_ok then if not ui_ok then
renderer.close_ui() renderer.close_ui()
log_graphics(util.c("UI crashed: ", message)) log_graphics(util.c("main UI error: ", ui_message))
println_ts("UI crashed") log.fatal(util.c("main GUI render failed with error ", ui_message))
log.fatal(util.c("GUI crashed with error ", message))
else else
log_graphics("first UI draw took " .. (util.time_ms() - draw_start) .. "ms") log_graphics("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
-- start clock
loop_clock.start()
end end
return ui_ok return ui_ok
end end
local ui_ok = init_start_ui()
---------------------------------------- ----------------------------------------
-- main event loop -- main event loop
---------------------------------------- ----------------------------------------
local link_failed = false
local ui_ok = true
local date_format = util.trinary(config.TIME_24_HOUR, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y") local date_format = util.trinary(config.TIME_24_HOUR, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y")
local no_modem = false -- start clock
loop_clock.start()
if ui_ok then log_sys("system started successfully")
-- start connection watchdog
conn_watchdog.feed()
log.debug("startup> conn watchdog started")
log_sys("system started successfully")
end
-- main event loop -- main event loop
while ui_ok do while true do
local event, param1, param2, param3, param4, param5 = util.pull_event() local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event -- handle event
@@ -239,33 +234,36 @@ local function main()
if type ~= nil and device ~= nil then if type ~= nil and device ~= nil then
if type == "modem" then if type == "modem" then
-- we only really care if this is our wireless modem -- we only really care if this is our wireless modem
if device == modem then -- if it is another modem, handle other peripheral losses separately
no_modem = true if nic.is_modem(device) then
nic.disconnect()
log_sys("comms modem disconnected") log_sys("comms modem disconnected")
println_ts("wireless modem disconnected!")
-- close out UI local other_modem = ppm.get_wireless_modem()
renderer.close_ui() if other_modem then
log_sys("found another wireless modem, using it for comms")
nic.connect(other_modem)
else
-- close out main UI
renderer.close_ui()
-- alert user to status -- alert user to status
log_sys("awaiting comms modem reconnect...") log_sys("awaiting comms modem reconnect...")
iocontrol.fp_has_modem(false)
end
else else
log_sys("non-comms modem disconnected") log_sys("non-comms modem disconnected")
end end
elseif type == "monitor" then elseif type == "monitor" then
if renderer.is_monitor_used(device) then if renderer.handle_disconnect(device) then
-- "halt and catch fire" style handling log_sys("lost a configured monitor")
local msg = "lost a configured monitor, system will now exit"
println_ts(msg)
log_sys(msg)
break
else else
log_sys("lost unused monitor, ignoring") log_sys("lost an unused monitor")
end end
elseif type == "speaker" then elseif type == "speaker" then
local msg = "lost alarm sounder speaker" log_sys("lost alarm sounder speaker")
println_ts(msg) iocontrol.fp_has_speaker(false)
log_sys(msg)
end end
end end
elseif event == "peripheral" then elseif event == "peripheral" then
@@ -273,34 +271,50 @@ local function main()
if type ~= nil and device ~= nil then if type ~= nil and device ~= nil then
if type == "modem" then if type == "modem" then
if device.isWireless() then if device.isWireless() and not nic.is_connected() then
-- reconnected modem -- reconnected modem
no_modem = false
modem = device
coord_comms.reconnect_modem(modem)
log_sys("comms modem reconnected") log_sys("comms modem reconnected")
println_ts("wireless modem reconnected.") nic.connect(device)
iocontrol.fp_has_modem(true)
-- re-init system elseif device.isWireless() then
if not init_connect_sv() then break end log.info("unused wireless modem reconnected")
ui_ok = init_start_ui()
else else
log_sys("wired modem reconnected") log_sys("wired modem reconnected")
end end
-- elseif type == "monitor" then elseif type == "monitor" then
-- not supported, system will exit on loss of in-use monitors if renderer.handle_reconnect(param1, device) then
log_sys(util.c("configured monitor ", param1, " reconnected"))
else
log_sys(util.c("unused monitor ", param1, " connected"))
end
elseif type == "speaker" then elseif type == "speaker" then
local msg = "alarm sounder speaker reconnected" log_sys("alarm sounder speaker reconnected")
println_ts(msg)
log_sys(msg)
sounder.reconnect(device) sounder.reconnect(device)
iocontrol.fp_has_speaker(true)
end end
end 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
-- toggle heartbeat
iocontrol.heartbeat()
-- maintain connection
if nic.is_connected() then
local ok, start_ui = coord_comms.try_connect()
if not ok then
link_failed = true
log_sys("supervisor connection failed, shutting down...")
log.fatal("failed to connect to supervisor")
break
elseif start_ui then
log_sys("supervisor connected, proceeding to main UI start")
ui_ok = start_main_ui()
if not ui_ok then break end
end
end
-- iterate sessions -- iterate sessions
apisessions.iterate_all() apisessions.iterate_all()
@@ -308,25 +322,19 @@ local function main()
apisessions.free_all_closed() apisessions.free_all_closed()
-- update date and time string for main display -- update date and time string for main display
iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format)) if coord_comms.is_linked() then
iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format))
end
loop_clock.start() loop_clock.start()
elseif conn_watchdog.is_timer(param1) then elseif conn_watchdog.is_timer(param1) then
-- supervisor watchdog timeout -- supervisor watchdog timeout
local msg = "supervisor server timeout" log_comms("supervisor server timeout")
log_comms(msg)
println_ts(msg)
-- close connection, UI, and stop sounder -- close connection, main UI, and stop sounder
coord_comms.close() coord_comms.close()
renderer.close_ui() renderer.close_ui()
sounder.stop() sounder.stop()
if not no_modem then
-- try to re-connect to the supervisor
if not init_connect_sv() then break end
ui_ok = init_start_ui()
end
else else
-- a non-clock/main watchdog timer event -- a non-clock/main watchdog timer event
@@ -334,31 +342,25 @@ local function main()
apisessions.check_all_watchdogs(param1) apisessions.check_all_watchdogs(param1)
-- notify timer callback dispatcher -- notify timer callback dispatcher
tcallbackdsp.handle(param1) tcd.handle(param1)
end end
elseif event == "modem_message" then elseif event == "modem_message" then
-- got a packet -- got a packet
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5) local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
coord_comms.handle_packet(packet)
-- check if it was a disconnect -- handle then check if it was a disconnect
if not coord_comms.is_linked() then if coord_comms.handle_packet(packet) then
log_comms("supervisor closed connection") log_comms("supervisor closed connection")
-- close connection, UI, and stop sounder -- close connection, main UI, and stop sounder
coord_comms.close() coord_comms.close()
renderer.close_ui() renderer.close_ui()
sounder.stop() sounder.stop()
if not no_modem then
-- try to re-connect to the supervisor
if not init_connect_sv() then break end
ui_ok = init_start_ui()
end
end end
elseif event == "monitor_touch" then elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
-- handle a monitor touch event event == "mouse_drag" or event == "mouse_scroll" then
renderer.handle_mouse(core.events.touch(param1, param2, param3)) -- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
elseif event == "speaker_audio_empty" then elseif event == "speaker_audio_empty" then
-- handle speaker buffer emptied -- handle speaker buffer emptied
sounder.continue() sounder.continue()
@@ -366,10 +368,17 @@ local function main()
-- check for termination request -- check for termination request
if event == "terminate" or ppm.should_terminate() then if event == "terminate" or ppm.should_terminate() then
println_ts("terminate requested, closing connections...") -- handle supervisor connection
log_comms("terminate requested, closing supervisor connection...") coord_comms.try_connect(true)
if coord_comms.is_linked() then
log_comms("terminate requested, closing supervisor connection...")
else link_failed = true end
coord_comms.close() coord_comms.close()
log_comms("supervisor connection closed") log_comms("supervisor connection closed")
-- handle API sessions
log_comms("closing api sessions...") log_comms("closing api sessions...")
apisessions.close_all() apisessions.close_all()
log_comms("api sessions closed") log_comms("api sessions closed")
@@ -378,15 +387,20 @@ local function main()
end end
renderer.close_ui() renderer.close_ui()
renderer.close_fp()
sounder.stop() sounder.stop()
log_sys("system shutdown") log_sys("system shutdown")
if link_failed then println_ts("failed to connect to supervisor") end
if not ui_ok then println_ts("main UI creation failed") end
println_ts("exited") println_ts("exited")
log.info("exited") log.info("exited")
end end
if not xpcall(main, crash.handler) then if not xpcall(main, crash.handler) then
pcall(renderer.close_ui) pcall(renderer.close_ui)
pcall(renderer.close_fp)
pcall(sounder.stop) pcall(sounder.stop)
crash.exit() crash.exit()
else else

View File

@@ -9,8 +9,8 @@ local DataIndicator = require("graphics.elements.indicators.data")
local StateIndicator = require("graphics.elements.indicators.state") local StateIndicator = require("graphics.elements.indicators.state")
local VerticalBar = require("graphics.elements.indicators.vbar") local VerticalBar = require("graphics.elements.indicators.vbar")
local cpair = core.graphics.cpair local cpair = core.cpair
local border = core.graphics.border local border = core.border
-- new boiler view -- new boiler view
---@param root graphics_element parent ---@param root graphics_element parent
@@ -27,9 +27,9 @@ local function new_view(root, x, y, ps)
local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=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="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg}
local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=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}
ps.subscribe("computed_status", status.update) status.register(ps, "computed_status", status.update)
ps.subscribe("temperature", temp.update) temp.register(ps, "temperature", temp.update)
ps.subscribe("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}
TextBox{parent=boiler,text="W",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg} TextBox{parent=boiler,text="W",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg}
@@ -41,10 +41,10 @@ local function new_view(root, x, y, ps)
local steam = VerticalBar{parent=boiler,x=27,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1} local steam = VerticalBar{parent=boiler,x=27,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
local ccool = VerticalBar{parent=boiler,x=28,y=1,fg_bg=cpair(colors.lightBlue,colors.gray),height=4,width=1} local ccool = VerticalBar{parent=boiler,x=28,y=1,fg_bg=cpair(colors.lightBlue,colors.gray),height=4,width=1}
ps.subscribe("hcool_fill", hcool.update) hcool.register(ps, "hcool_fill", hcool.update)
ps.subscribe("water_fill", water.update) water.register(ps, "water_fill", water.update)
ps.subscribe("steam_fill", steam.update) steam.register(ps, "steam_fill", steam.update)
ps.subscribe("ccool_fill", ccool.update) ccool.register(ps, "ccool_fill", ccool.update)
end end
return new_view return new_view

View File

@@ -13,10 +13,10 @@ local PowerIndicator = require("graphics.elements.indicators.power")
local StateIndicator = require("graphics.elements.indicators.state") local StateIndicator = require("graphics.elements.indicators.state")
local VerticalBar = require("graphics.elements.indicators.vbar") local VerticalBar = require("graphics.elements.indicators.vbar")
local cpair = core.graphics.cpair local cpair = core.cpair
local border = core.graphics.border local border = core.border
local TEXT_ALIGN = core.graphics.TEXT_ALIGN local TEXT_ALIGN = core.TEXT_ALIGN
-- new induction matrix view -- new induction matrix view
---@param root graphics_element parent ---@param root graphics_element parent
@@ -50,15 +50,15 @@ local function new_view(root, x, y, data, ps, id)
local avg_in = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="Avg. In: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg} local avg_in = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="Avg. In: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
local avg_out = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Avg. Out:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg} local avg_out = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Avg. Out:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
ps.subscribe("computed_status", status.update) status.register(ps, "computed_status", status.update)
ps.subscribe("energy", function (val) energy.update(util.joules_to_fe(val)) end) energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end)
ps.subscribe("max_energy", function (val) capacity.update(util.joules_to_fe(val)) end) capacity.register(ps, "max_energy", function (val) capacity.update(util.joules_to_fe(val)) end)
ps.subscribe("last_input", function (val) input.update(util.joules_to_fe(val)) end) input.register(ps, "last_input", function (val) input.update(util.joules_to_fe(val)) end)
ps.subscribe("last_output", function (val) output.update(util.joules_to_fe(val)) end) output.register(ps, "last_output", function (val) output.update(util.joules_to_fe(val)) end)
ps.subscribe("avg_charge", avg_chg.update) avg_chg.register(ps, "avg_charge", avg_chg.update)
ps.subscribe("avg_inflow", avg_in.update) avg_in.register(ps, "avg_inflow", avg_in.update)
ps.subscribe("avg_outflow", avg_out.update) avg_out.register(ps, "avg_outflow", avg_out.update)
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill:",unit="%",format="%8.2f",value=0,width=18,fg_bg=text_fg_bg} local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill:",unit="%",format="%8.2f",value=0,width=18,fg_bg=text_fg_bg}
@@ -68,10 +68,10 @@ local function new_view(root, x, y, data, ps, id)
TextBox{parent=rect,text="Transfer Capacity",x=11,y=17,height=1,width=17,fg_bg=label_fg_bg} TextBox{parent=rect,text="Transfer Capacity",x=11,y=17,height=1,width=17,fg_bg=label_fg_bg}
local trans_cap = PowerIndicator{parent=rect,x=19,y=18,lu_colors=lu_col,label="",format="%5.2f",rate=true,value=0,width=12,fg_bg=text_fg_bg} local trans_cap = PowerIndicator{parent=rect,x=19,y=18,lu_colors=lu_col,label="",format="%5.2f",rate=true,value=0,width=12,fg_bg=text_fg_bg}
ps.subscribe("cells", cells.update) cells.register(ps, "cells", cells.update)
ps.subscribe("providers", providers.update) providers.register(ps, "providers", providers.update)
ps.subscribe("energy_fill", function (val) fill.update(val * 100) end) fill.register(ps, "energy_fill", function (val) fill.update(val * 100) end)
ps.subscribe("transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end) trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end)
local charge = VerticalBar{parent=rect,x=2,y=2,fg_bg=cpair(colors.green,colors.gray),height=17,width=4} local charge = VerticalBar{parent=rect,x=2,y=2,fg_bg=cpair(colors.green,colors.gray),height=17,width=4}
local in_cap = VerticalBar{parent=rect,x=7,y=12,fg_bg=cpair(colors.red,colors.gray),height=7,width=1} local in_cap = VerticalBar{parent=rect,x=7,y=12,fg_bg=cpair(colors.red,colors.gray),height=7,width=1}
@@ -83,14 +83,12 @@ local function new_view(root, x, y, data, ps, id)
local function calc_saturation(val) local function calc_saturation(val)
if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then
return val / data.build.transfer_cap return val / data.build.transfer_cap
else else return 0 end
return 0
end
end end
ps.subscribe("energy_fill", charge.update) charge.register(ps, "energy_fill", charge.update)
ps.subscribe("last_input", function (val) in_cap.update(calc_saturation(val)) end) in_cap.register(ps, "last_input", function (val) in_cap.update(calc_saturation(val)) end)
ps.subscribe("last_output", function (val) out_cap.update(calc_saturation(val)) end) out_cap.register(ps, "last_output", function (val) out_cap.update(calc_saturation(val)) end)
end end
return new_view return new_view

View File

@@ -0,0 +1,48 @@
--
-- Pocket Connection Entry
--
local iocontrol = require("coordinator.iocontrol")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data")
local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.cpair
-- create a pocket list entry
---@param parent graphics_element parent
---@param id integer PKT session ID
local function init(parent, id)
local ps = iocontrol.get_db().fp.ps
-- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)}
local ps_prefix = "pkt_" .. id .. "_"
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)}
TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
pkt_addr.register(ps, ps_prefix .. "addr", pkt_addr.set_value)
TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1}
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value)
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1}
local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
pkt_rtt.register(ps, ps_prefix .. "rtt", pkt_rtt.update)
pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor)
return root
end
return init

View File

@@ -0,0 +1,360 @@
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light")
local RadIndicator = require("graphics.elements.indicators.rad")
local StateIndicator = require("graphics.elements.indicators.state")
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
local Checkbox = require("graphics.elements.controls.checkbox")
local HazardButton = require("graphics.elements.controls.hazard_button")
local RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.cpair
local border = core.border
local bw_fg_bg = style.bw_fg_bg
local lu_cpair = style.lu_colors
local hzd_fg_bg = style.hzd_fg_bg
local dis_colors = style.dis_colors
local ind_grn = style.ind_grn
local ind_yel = style.ind_yel
local ind_red = style.ind_red
local ind_wht = style.ind_wht
local period = core.flasher.PERIOD
-- new process control view
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y
local function new_view(root, x, y)
assert(root.get_height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units
local main = Div{parent=root,width=128,height=24,x=x,y=y}
local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,fg_bg=hzd_fg_bg}
facility.scram_ack = scram.on_response
facility.ack_alarms_ack = ack_a.on_response
local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=ind_grn}
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=ind_grn}
local sps = IndicatorLight{parent=main,label="SPS Connected",colors=ind_grn}
all_ok.register(facility.ps, "all_sys_ok", all_ok.update)
rad_mon.register(facility.ps, "rad_computed_status", rad_mon.update)
ind_mat.register(facility.induction_ps_tbl[1], "computed_status", function (status) ind_mat.update(status > 1) end)
sps.register(facility.sps_ps_tbl[1], "computed_status", function (status) sps.update(status > 1) end)
main.line_break()
local auto_ready = IndicatorLight{parent=main,label="Configured Units Ready",colors=ind_grn}
local auto_act = IndicatorLight{parent=main,label="Process Active",colors=ind_grn}
local auto_ramp = IndicatorLight{parent=main,label="Process Ramping",colors=ind_wht,flash=true,period=period.BLINK_250_MS}
local auto_sat = IndicatorLight{parent=main,label="Min/Max Burn Rate",colors=ind_yel}
auto_ready.register(facility.ps, "auto_ready", auto_ready.update)
auto_act.register(facility.ps, "auto_active", auto_act.update)
auto_ramp.register(facility.ps, "auto_ramping", auto_ramp.update)
auto_sat.register(facility.ps, "auto_saturated", auto_sat.update)
main.line_break()
local auto_scram = IndicatorLight{parent=main,label="Automatic SCRAM",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local matrix_dc = IndicatorLight{parent=main,label="Matrix Disconnected",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
local matrix_fill = IndicatorLight{parent=main,label="Matrix Charge High",colors=ind_red,flash=true,period=period.BLINK_500_MS}
local unit_crit = IndicatorLight{parent=main,label="Unit Critical Alarm",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local fac_rad_h = IndicatorLight{parent=main,label="Facility Radiation High",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local gen_fault = IndicatorLight{parent=main,label="Gen. Control Fault",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
auto_scram.register(facility.ps, "auto_scram", auto_scram.update)
matrix_dc.register(facility.ps, "as_matrix_dc", matrix_dc.update)
matrix_fill.register(facility.ps, "as_matrix_fill", matrix_fill.update)
unit_crit.register(facility.ps, "as_crit_alarm", unit_crit.update)
fac_rad_h.register(facility.ps, "as_radiation", fac_rad_h.update)
gen_fault.register(facility.ps, "as_gen_fault", gen_fault.update)
TextBox{parent=main,y=23,text="Radiation",height=1,width=13,fg_bg=style.label}
local radiation = RadIndicator{parent=main,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
radiation.register(facility.ps, "radiation", radiation.update)
TextBox{parent=main,x=15,y=23,text="Linked RTUs",height=1,width=11,fg_bg=style.label}
local rtu_count = DataIndicator{parent=main,x=15,y=24,label="",format="%11d",value=0,lu_colors=lu_cpair,width=11,fg_bg=bw_fg_bg}
rtu_count.register(facility.ps, "rtu_count", rtu_count.update)
---------------------
-- process control --
---------------------
local proc = Div{parent=main,width=103,height=24,x=27,y=1}
-----------------------------
-- process control targets --
-----------------------------
local targets = Div{parent=proc,width=31,height=24,x=1,y=1}
local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2}
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=burn_target,x=18,y=2,text="mB/t"}
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
b_target.register(facility.ps, "process_burn_target", b_target.set_value)
burn_sum.register(facility.ps, "burn_sum", burn_sum.update)
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=chg_target,x=18,y=2,text="MFE"}
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
c_target.register(facility.ps, "process_charge_target", c_target.set_value)
cur_charge.register(facility.induction_ps_tbl[1], "energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end)
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=gen_target,x=18,y=2,text="kFE/t"}
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
g_target.register(facility.ps, "process_gen_target", g_target.set_value)
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end)
-----------------
-- unit limits --
-----------------
local limit_div = Div{parent=proc,width=21,height=19,x=34,y=6}
local rate_limits = {}
for i = 1, 4 do
local unit
local tag_fg_bg = cpair(colors.gray,colors.white)
local lim_fg_bg = cpair(colors.lightGray,colors.white)
local ctl_fg = colors.lightGray
local cur_fg_bg = cpair(colors.lightGray,colors.white)
local cur_lu = colors.lightGray
if i <= facility.num_units then
unit = units[i] ---@type ioctl_unit
tag_fg_bg = cpair(colors.black,colors.lightBlue)
lim_fg_bg = bw_fg_bg
ctl_fg = colors.gray
cur_fg_bg = cpair(colors.black,colors.brown)
cur_lu = colors.black
end
local _y = ((i - 1) * 5) + 1
local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2}
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(ctl_fg,colors.white)}
local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=lim_fg_bg}
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1}
local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(cur_lu,cur_lu),width=14,fg_bg=cur_fg_bg}
if i <= facility.num_units then
rate_limits[i] = lim
rate_limits[i].register(unit.unit_ps, "max_burn", rate_limits[i].set_max)
rate_limits[i].register(unit.unit_ps, "burn_limit", rate_limits[i].set_value)
cur_burn.register(unit.unit_ps, "act_burn_rate", cur_burn.update)
else
lim.disable()
end
end
-------------------
-- unit statuses --
-------------------
local stat_div = Div{parent=proc,width=22,height=24,x=57,y=6}
for i = 1, 4 do
local tag_fg_bg = cpair(colors.gray, colors.white)
local ind_fg_bg = cpair(colors.lightGray, colors.white)
local ind_off = colors.lightGray
if i <= facility.num_units then
tag_fg_bg = cpair(colors.black, colors.cyan)
ind_fg_bg = bw_fg_bg
ind_off = colors.gray
end
local _y = ((i - 1) * 5) + 1
local unit_tag = Div{parent=stat_div,x=1,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Status",width=7,height=2}
local lights = Div{parent=stat_div,x=9,y=_y,width=14,height=4,fg_bg=ind_fg_bg}
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(colors.green,ind_off)}
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(colors.red,ind_off),flash=true,period=period.BLINK_250_MS}
if i <= facility.num_units then
local unit = units[i] ---@type ioctl_unit
ready.register(unit.unit_ps, "U_AutoReady", ready.update)
degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update)
end
end
-------------------------
-- controls and status --
-------------------------
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.white,colors.black),radio_bg=colors.purple}
mode.register(facility.ps, "process_mode", mode.set_value)
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value)
stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value)
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=cpair(colors.gray,colors.white)}
-- save the automatic process control configuration without starting
local function _save_cfg()
local limits = {}
for i = 1, #rate_limits do limits[i] = rate_limits[i].get_value() end
process.save(mode.get_value(), b_target.get_value(), c_target.get_value(), g_target.get_value(), limits)
end
-- start automatic control after saving process control settings
local function _start_auto()
_save_cfg()
process.start_auto()
end
local save = HazardButton{parent=auto_controls,x=2,y=2,text="SAVE",accent=colors.purple,dis_colors=dis_colors,callback=_save_cfg,fg_bg=hzd_fg_bg}
local start = HazardButton{parent=auto_controls,x=13,y=2,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=_start_auto,fg_bg=hzd_fg_bg}
local stop = HazardButton{parent=auto_controls,x=23,y=2,text="STOP",accent=colors.red,dis_colors=dis_colors,callback=process.stop_auto,fg_bg=hzd_fg_bg}
facility.start_ack = start.on_response
facility.stop_ack = stop.on_response
function facility.save_cfg_ack(ack)
tcd.dispatch(0.2, function () save.on_response(ack) end)
end
start.register(facility.ps, "auto_ready", function (ready)
if ready and (not facility.auto_active) then start.enable() else start.disable() end
end)
-- REGISTER_NOTE: for optimization/brevity, due to not deleting anything but the whole element tree when it comes
-- to the process control display and coordinator GUI as a whole, child elements will not directly be registered here
-- (preventing garbage collection until the parent 'proc' is deleted)
proc.register(facility.ps, "auto_active", function (active)
if active then
b_target.disable()
c_target.disable()
g_target.disable()
mode.disable()
start.disable()
for i = 1, #rate_limits do rate_limits[i].disable() end
else
b_target.enable()
c_target.enable()
g_target.enable()
mode.enable()
if facility.auto_ready then start.enable() end
for i = 1, #rate_limits do rate_limits[i].enable() end
end
end)
------------------------------
-- waste production control --
------------------------------
local waste_status = Div{parent=proc,width=24,height=4,x=57,y=1,}
for i = 1, facility.num_units do
local unit = units[i] ---@type ioctl_unit
TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8,height=1}
local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=ind_wht}
local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.waste.states_abbrv,value=1,min_width=6}
a_waste.register(unit.unit_ps, "U_AutoWaste", a_waste.update)
waste_m.register(unit.unit_ps, "U_WasteProduct", waste_m.update)
end
local waste_sel = Div{parent=proc,width=21,height=24,x=81,y=1}
TextBox{parent=waste_sel,text=" ",width=21,height=1,x=1,y=1,fg_bg=cpair(colors.black,colors.brown)}
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=TEXT_ALIGN.CENTER,width=21,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.brown)}
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17}
status.register(facility.ps, "current_waste_product", status.update)
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.white,colors.black),radio_bg=colors.brown}
local pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.green,colors.black)}
waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value)
pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value)
local fb_active = IndicatorLight{parent=rect,x=2,y=9,label="Fallback Active",colors=ind_wht}
fb_active.register(facility.ps, "pu_fallback_active", fb_active.update)
TextBox{parent=rect,x=2,y=11,text="Plutonium Rate",height=1,width=17,fg_bg=style.label}
local pu_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
TextBox{parent=rect,x=2,y=14,text="Polonium Rate",height=1,width=17,fg_bg=style.label}
local po_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
TextBox{parent=rect,x=2,y=17,text="Antimatter Rate",height=1,width=17,fg_bg=style.label}
local am_rate = DataIndicator{parent=rect,x=2,label="",unit="\xb5B/t",format="%12d",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
pu_rate.register(facility.ps, "pu_rate", pu_rate.update)
po_rate.register(facility.ps, "po_rate", po_rate.update)
am_rate.register(facility.ps, "am_rate", am_rate.update)
local sna_count = DataIndicator{parent=rect,x=2,y=20,label="Linked SNAs:",format="%4d",value=0,lu_colors=lu_cpair,width=17}
sna_count.register(facility.ps, "sna_count", sna_count.update)
end
return new_view

View File

@@ -1,267 +0,0 @@
local tcd = require("scada-common.tcallbackdsp")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light")
local RadIndicator = require("graphics.elements.indicators.rad")
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
local HazardButton = require("graphics.elements.controls.hazard_button")
local RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
local cpair = core.graphics.cpair
local border = core.graphics.border
local period = core.flasher.PERIOD
-- new process control view
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y
local function new_view(root, x, y)
assert(root.height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units
local bw_fg_bg = cpair(colors.black, colors.white)
local hzd_fg_bg = cpair(colors.white, colors.gray)
local lu_cpair = cpair(colors.gray, colors.gray)
local dis_colors = cpair(colors.white, colors.lightGray)
local main = Div{parent=root,width=104,height=24,x=x,y=y}
local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,fg_bg=hzd_fg_bg}
facility.scram_ack = scram.on_response
facility.ack_alarms_ack = ack_a.on_response
local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=cpair(colors.green,colors.red)}
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=cpair(colors.green,colors.gray)}
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
facility.ps.subscribe("all_sys_ok", all_ok.update)
facility.induction_ps_tbl[1].subscribe("computed_status", function (status) ind_mat.update(status > 1) end)
facility.ps.subscribe("rad_computed_status", rad_mon.update)
main.line_break()
local auto_ready = IndicatorLight{parent=main,label="Configured Units Ready",colors=cpair(colors.green,colors.red)}
local auto_act = IndicatorLight{parent=main,label="Process Active",colors=cpair(colors.green,colors.gray)}
local auto_ramp = IndicatorLight{parent=main,label="Process Ramping",colors=cpair(colors.white,colors.gray),flash=true,period=period.BLINK_250_MS}
local auto_sat = IndicatorLight{parent=main,label="Min/Max Burn Rate",colors=cpair(colors.yellow,colors.gray)}
facility.ps.subscribe("auto_ready", auto_ready.update)
facility.ps.subscribe("auto_active", auto_act.update)
facility.ps.subscribe("auto_ramping", auto_ramp.update)
facility.ps.subscribe("auto_saturated", auto_sat.update)
main.line_break()
local auto_scram = IndicatorLight{parent=main,label="Automatic SCRAM",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
local matrix_dc = IndicatorLight{parent=main,label="Matrix Disconnected",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
local matrix_fill = IndicatorLight{parent=main,label="Matrix Charge High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_500_MS}
local unit_crit = IndicatorLight{parent=main,label="Unit Critical Alarm",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
local fac_rad_h = IndicatorLight{parent=main,label="Facility Radiation High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
local gen_fault = IndicatorLight{parent=main,label="Gen. Control Fault",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
facility.ps.subscribe("auto_scram", auto_scram.update)
facility.ps.subscribe("as_matrix_dc", matrix_dc.update)
facility.ps.subscribe("as_matrix_fill", matrix_fill.update)
facility.ps.subscribe("as_crit_alarm", unit_crit.update)
facility.ps.subscribe("as_radiation", fac_rad_h.update)
facility.ps.subscribe("as_gen_fault", gen_fault.update)
TextBox{parent=main,y=23,text="Radiation",height=1,width=13,fg_bg=style.label}
local radiation = RadIndicator{parent=main,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
facility.ps.subscribe("radiation", radiation.update)
TextBox{parent=main,x=15,y=23,text="Linked RTUs",height=1,width=11,fg_bg=style.label}
local rtu_count = DataIndicator{parent=main,x=15,y=24,label="",format="%11d",value=0,lu_colors=lu_cpair,width=11,fg_bg=bw_fg_bg}
facility.ps.subscribe("rtu_count", rtu_count.update)
---------------------
-- process control --
---------------------
local proc = Div{parent=main,width=78,height=24,x=27,y=1}
-----------------------------
-- process control targets --
-----------------------------
local targets = Div{parent=proc,width=31,height=24,x=1,y=1}
local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2}
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=burn_target,x=18,y=2,text="mB/t"}
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
facility.ps.subscribe("process_burn_target", b_target.set_value)
facility.ps.subscribe("burn_sum", burn_sum.update)
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=chg_target,x=18,y=2,text="MFE"}
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
facility.ps.subscribe("process_charge_target", c_target.set_value)
facility.induction_ps_tbl[1].subscribe("energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end)
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=gen_target,x=18,y=2,text="kFE/t"}
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
facility.ps.subscribe("process_gen_target", g_target.set_value)
facility.induction_ps_tbl[1].subscribe("last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end)
-----------------
-- unit limits --
-----------------
local limit_div = Div{parent=proc,width=21,height=19,x=34,y=6}
local rate_limits = {}
for i = 1, facility.num_units do
local unit = units[i] ---@type ioctl_unit
local _y = ((i - 1) * 5) + 1
local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2}
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(colors.gray,colors.white)}
rate_limits[i] = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1}
unit.unit_ps.subscribe("max_burn", rate_limits[i].set_max)
unit.unit_ps.subscribe("burn_limit", rate_limits[i].set_value)
local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(colors.black,colors.black),width=14,fg_bg=cpair(colors.black,colors.brown)}
unit.unit_ps.subscribe("act_burn_rate", cur_burn.update)
end
-------------------
-- unit statuses --
-------------------
local stat_div = Div{parent=proc,width=38,height=19,x=57,y=6}
for i = 1, facility.num_units do
local unit = units[i] ---@type ioctl_unit
local _y = ((i - 1) * 5) + 1
local unit_tag = Div{parent=stat_div,x=1,y=_y,width=8,height=4,fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Status",width=7,height=2}
local lights = Div{parent=stat_div,x=9,y=_y,width=12,height=4,fg_bg=bw_fg_bg}
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(colors.green,colors.gray)}
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
unit.unit_ps.subscribe("U_AutoReady", ready.update)
unit.unit_ps.subscribe("U_AutoDegraded", degraded.update)
end
-------------------------
-- controls and status --
-------------------------
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.purple,colors.black),radio_bg=colors.gray}
facility.ps.subscribe("process_mode", mode.set_value)
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
facility.ps.subscribe("status_line_1", stat_line_1.set_value)
facility.ps.subscribe("status_line_2", stat_line_2.set_value)
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=cpair(colors.gray,colors.white)}
-- save the automatic process control configuration without starting
local function _save_cfg()
local limits = {}
for i = 1, #rate_limits do limits[i] = rate_limits[i].get_value() end
process.save(mode.get_value(), b_target.get_value(), c_target.get_value(), g_target.get_value(), limits)
end
-- start automatic control after saving process control settings
local function _start_auto()
_save_cfg()
process.start_auto()
end
local save = HazardButton{parent=auto_controls,x=2,y=2,text="SAVE",accent=colors.purple,dis_colors=dis_colors,callback=_save_cfg,fg_bg=hzd_fg_bg}
local start = HazardButton{parent=auto_controls,x=13,y=2,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=_start_auto,fg_bg=hzd_fg_bg}
local stop = HazardButton{parent=auto_controls,x=23,y=2,text="STOP",accent=colors.red,dis_colors=dis_colors,callback=process.stop_auto,fg_bg=hzd_fg_bg}
facility.start_ack = start.on_response
facility.stop_ack = stop.on_response
function facility.save_cfg_ack(ack)
tcd.dispatch(0.2, function () save.on_response(ack) end)
end
facility.ps.subscribe("auto_ready", function (ready)
if ready and (not facility.auto_active) then start.enable() else start.disable() end
end)
facility.ps.subscribe("auto_active", function (active)
if active then
b_target.disable()
c_target.disable()
g_target.disable()
mode.disable()
start.disable()
for i = 1, #rate_limits do
rate_limits[i].disable()
end
else
b_target.enable()
c_target.enable()
g_target.enable()
mode.enable()
if facility.auto_ready then start.enable() end
for i = 1, #rate_limits do
rate_limits[i].enable()
end
end
end)
end
return new_view

View File

@@ -11,8 +11,8 @@ local DataIndicator = require("graphics.elements.indicators.data")
local HorizontalBar = require("graphics.elements.indicators.hbar") local HorizontalBar = require("graphics.elements.indicators.hbar")
local StateIndicator = require("graphics.elements.indicators.state") local StateIndicator = require("graphics.elements.indicators.state")
local cpair = core.graphics.cpair local cpair = core.cpair
local border = core.graphics.border local border = core.border
-- create new reactor view -- create new reactor view
---@param root graphics_element parent ---@param root graphics_element parent
@@ -30,10 +30,10 @@ local function new_view(root, x, y, ps)
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}
ps.subscribe("computed_status", status.update) status.register(ps, "computed_status", status.update)
ps.subscribe("temp", core_temp.update) core_temp.register(ps, "temp", core_temp.update)
ps.subscribe("act_burn_rate", burn_r.update) burn_r.register(ps, "act_burn_rate", burn_r.update)
ps.subscribe("heating_rate", heating_r.update) heating_r.register(ps, "heating_rate", heating_r.update)
local reactor_fills = Rectangle{parent=root,border=border(1, colors.gray, true),width=24,height=7,x=(x + 29),y=y} local reactor_fills = Rectangle{parent=root,border=border(1, colors.gray, true),width=24,height=7,x=(x + 29),y=y}
@@ -47,7 +47,7 @@ local function new_view(root, x, y, ps)
local hcool = HorizontalBar{parent=reactor_fills,x=8,y=4,show_percent=true,bar_fg_bg=cpair(colors.white,colors.gray),height=1,width=14} local hcool = HorizontalBar{parent=reactor_fills,x=8,y=4,show_percent=true,bar_fg_bg=cpair(colors.white,colors.gray),height=1,width=14}
local waste = HorizontalBar{parent=reactor_fills,x=8,y=5,show_percent=true,bar_fg_bg=cpair(colors.brown,colors.gray),height=1,width=14} local waste = HorizontalBar{parent=reactor_fills,x=8,y=5,show_percent=true,bar_fg_bg=cpair(colors.brown,colors.gray),height=1,width=14}
ps.subscribe("ccool_type", function (type) ccool.register(ps, "ccool_type", function (type)
if type == types.FLUID.SODIUM then if type == types.FLUID.SODIUM then
ccool.recolor(cpair(colors.lightBlue, colors.gray)) ccool.recolor(cpair(colors.lightBlue, colors.gray))
else else
@@ -55,7 +55,7 @@ local function new_view(root, x, y, ps)
end end
end) end)
ps.subscribe("hcool_type", function (type) hcool.register(ps, "hcool_type", function (type)
if type == types.FLUID.SUPERHEATED_SODIUM then if type == types.FLUID.SUPERHEATED_SODIUM then
hcool.recolor(cpair(colors.orange, colors.gray)) hcool.recolor(cpair(colors.orange, colors.gray))
else else
@@ -63,10 +63,10 @@ local function new_view(root, x, y, ps)
end end
end) end)
ps.subscribe("fuel_fill", fuel.update) fuel.register(ps, "fuel_fill", fuel.update)
ps.subscribe("ccool_fill", ccool.update) ccool.register(ps, "ccool_fill", ccool.update)
ps.subscribe("hcool_fill", hcool.update) hcool.register(ps, "hcool_fill", hcool.update)
ps.subscribe("waste_fill", waste.update) waste.register(ps, "waste_fill", waste.update)
end end
return new_view return new_view

View File

@@ -12,8 +12,8 @@ local PowerIndicator = require("graphics.elements.indicators.power")
local StateIndicator = require("graphics.elements.indicators.state") local StateIndicator = require("graphics.elements.indicators.state")
local VerticalBar = require("graphics.elements.indicators.vbar") local VerticalBar = require("graphics.elements.indicators.vbar")
local cpair = core.graphics.cpair local cpair = core.cpair
local border = core.graphics.border local border = core.border
-- new turbine view -- new turbine view
---@param root graphics_element parent ---@param root graphics_element parent
@@ -30,9 +30,9 @@ local function new_view(root, x, y, ps)
local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg_bg} local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg_bg}
local flow_rate = DataIndicator{parent=turbine,x=5,y=4,lu_colors=lu_col,label="",unit="mB/t",format="%10.0f",value=0,commas=true,width=16,fg_bg=text_fg_bg} local flow_rate = DataIndicator{parent=turbine,x=5,y=4,lu_colors=lu_col,label="",unit="mB/t",format="%10.0f",value=0,commas=true,width=16,fg_bg=text_fg_bg}
ps.subscribe("computed_status", status.update) status.register(ps, "computed_status", status.update)
ps.subscribe("prod_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end) prod_rate.register(ps, "steam_input_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end)
ps.subscribe("flow_rate", flow_rate.update) flow_rate.register(ps, "flow_rate", flow_rate.update)
local steam = VerticalBar{parent=turbine,x=2,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1} local steam = VerticalBar{parent=turbine,x=2,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
local energy = VerticalBar{parent=turbine,x=3,y=1,fg_bg=cpair(colors.green,colors.gray),height=4,width=1} local energy = VerticalBar{parent=turbine,x=3,y=1,fg_bg=cpair(colors.green,colors.gray),height=4,width=1}
@@ -40,8 +40,8 @@ local function new_view(root, x, y, ps)
TextBox{parent=turbine,text="S",x=2,y=5,height=1,width=1,fg_bg=text_fg_bg} TextBox{parent=turbine,text="S",x=2,y=5,height=1,width=1,fg_bg=text_fg_bg}
TextBox{parent=turbine,text="E",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg} TextBox{parent=turbine,text="E",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg}
ps.subscribe("steam_fill", steam.update) steam.register(ps, "steam_fill", steam.update)
ps.subscribe("energy_fill", energy.update) energy.register(ps, "energy_fill", energy.update)
end end
return new_view return new_view

View File

@@ -26,69 +26,54 @@ local PushButton = require("graphics.elements.controls.push_button")
local RadioButton = require("graphics.elements.controls.radio_button") local RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric") local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.graphics.cpair local cpair = core.cpair
local border = core.graphics.border local border = core.border
local bw_fg_bg = style.bw_fg_bg
local lu_cpair = style.lu_colors
local hzd_fg_bg = style.hzd_fg_bg
local ind_grn = style.ind_grn
local ind_yel = style.ind_yel
local ind_red = style.ind_red
local ind_wht = style.ind_wht
local period = core.flasher.PERIOD local period = core.flasher.PERIOD
local waste_opts = {
{
text = "Auto",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.white, colors.gray)
},
{
text = "Pu",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.green)
},
{
text = "Po",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.cyan)
},
{
text = "AM",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.purple)
}
}
-- create a unit view -- create a unit view
---@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 unit = iocontrol.get_db().units[id] ---@type ioctl_unit
local f_ps = iocontrol.get_db().facility.ps local f_ps = iocontrol.get_db().facility.ps
local main = Div{parent=parent,x=1,y=1}
if unit == nil then return main end
local u_ps = unit.unit_ps local u_ps = unit.unit_ps
local b_ps = unit.boiler_ps_tbl local b_ps = unit.boiler_ps_tbl
local t_ps = unit.turbine_ps_tbl local t_ps = unit.turbine_ps_tbl
local main = Div{parent=parent,x=1,y=1}
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} TextBox{parent=main,text="Reactor Unit #" .. id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
local bw_fg_bg = cpair(colors.black, colors.white)
local hzd_fg_bg = cpair(colors.white, colors.gray)
local lu_cpair = cpair(colors.gray, colors.gray)
----------------------------- -----------------------------
-- main stats and core map -- -- main stats and core map --
----------------------------- -----------------------------
local core_map = CoreMap{parent=main,x=2,y=3,reactor_l=18,reactor_w=18} local core_map = CoreMap{parent=main,x=2,y=3,reactor_l=18,reactor_w=18}
u_ps.subscribe("temp", core_map.update) core_map.register(u_ps, "temp", core_map.update)
u_ps.subscribe("size", function (s) core_map.resize(s[1], s[2]) end) core_map.register(u_ps, "size", function (s) core_map.resize(s[1], s[2]) end)
TextBox{parent=main,x=12,y=22,text="Heating Rate",height=1,width=12,fg_bg=style.label} TextBox{parent=main,x=12,y=22,text="Heating Rate",height=1,width=12,fg_bg=style.label}
local heating_r = DataIndicator{parent=main,x=12,label="",format="%14.0f",value=0,unit="mB/t",commas=true,lu_colors=lu_cpair,width=19,fg_bg=bw_fg_bg} local heating_r = DataIndicator{parent=main,x=12,label="",format="%14.0f",value=0,unit="mB/t",commas=true,lu_colors=lu_cpair,width=19,fg_bg=bw_fg_bg}
u_ps.subscribe("heating_rate", heating_r.update) heating_r.register(u_ps, "heating_rate", heating_r.update)
TextBox{parent=main,x=12,y=25,text="Commanded Burn Rate",height=1,width=19,fg_bg=style.label} TextBox{parent=main,x=12,y=25,text="Commanded Burn Rate",height=1,width=19,fg_bg=style.label}
local burn_r = DataIndicator{parent=main,x=12,label="",format="%14.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=19,fg_bg=bw_fg_bg} local burn_r = DataIndicator{parent=main,x=12,label="",format="%14.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=19,fg_bg=bw_fg_bg}
u_ps.subscribe("burn_rate", burn_r.update) burn_r.register(u_ps, "burn_rate", burn_r.update)
TextBox{parent=main,text="F",x=2,y=22,width=1,height=1,fg_bg=style.label} TextBox{parent=main,text="F",x=2,y=22,width=1,height=1,fg_bg=style.label}
TextBox{parent=main,text="C",x=4,y=22,width=1,height=1,fg_bg=style.label} TextBox{parent=main,text="C",x=4,y=22,width=1,height=1,fg_bg=style.label}
@@ -102,12 +87,12 @@ local function init(parent, id)
local hcool = VerticalBar{parent=main,x=8,y=23,fg_bg=cpair(colors.white,colors.gray),height=4,width=1} local hcool = VerticalBar{parent=main,x=8,y=23,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
local waste = VerticalBar{parent=main,x=10,y=23,fg_bg=cpair(colors.brown,colors.gray),height=4,width=1} local waste = VerticalBar{parent=main,x=10,y=23,fg_bg=cpair(colors.brown,colors.gray),height=4,width=1}
u_ps.subscribe("fuel_fill", fuel.update) fuel.register(u_ps, "fuel_fill", fuel.update)
u_ps.subscribe("ccool_fill", ccool.update) ccool.register(u_ps, "ccool_fill", ccool.update)
u_ps.subscribe("hcool_fill", hcool.update) hcool.register(u_ps, "hcool_fill", hcool.update)
u_ps.subscribe("waste_fill", waste.update) waste.register(u_ps, "waste_fill", waste.update)
u_ps.subscribe("ccool_type", function (type) ccool.register(u_ps, "ccool_type", function (type)
if type == "mekanism:sodium" then if type == "mekanism:sodium" then
ccool.recolor(cpair(colors.lightBlue, colors.gray)) ccool.recolor(cpair(colors.lightBlue, colors.gray))
else else
@@ -115,7 +100,7 @@ local function init(parent, id)
end end
end) end)
u_ps.subscribe("hcool_type", function (type) hcool.register(u_ps, "hcool_type", function (type)
if type == "mekanism:superheated_sodium" then if type == "mekanism:superheated_sodium" then
hcool.recolor(cpair(colors.orange, colors.gray)) hcool.recolor(cpair(colors.orange, colors.gray))
else else
@@ -125,19 +110,19 @@ local function init(parent, id)
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 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}
u_ps.subscribe("temp", core_temp.update) core_temp.register(u_ps, "temp", core_temp.update)
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}
u_ps.subscribe("act_burn_rate", act_burn_r.update) act_burn_r.register(u_ps, "act_burn_rate", act_burn_r.update)
TextBox{parent=main,x=32,y=28,text="Damage",height=1,width=6,fg_bg=style.label} TextBox{parent=main,x=32,y=28,text="Damage",height=1,width=6,fg_bg=style.label}
local damage_p = DataIndicator{parent=main,x=32,label="",format="%11.0f",value=0,unit="%",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg} local damage_p = DataIndicator{parent=main,x=32,label="",format="%11.0f",value=0,unit="%",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
u_ps.subscribe("damage", damage_p.update) damage_p.register(u_ps, "damage", damage_p.update)
TextBox{parent=main,x=32,y=31,text="Radiation",height=1,width=21,fg_bg=style.label} TextBox{parent=main,x=32,y=31,text="Radiation",height=1,width=21,fg_bg=style.label}
local radiation = RadIndicator{parent=main,x=32,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg} local radiation = RadIndicator{parent=main,x=32,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
u_ps.subscribe("radiation", radiation.update) radiation.register(u_ps, "radiation", radiation.update)
------------------- -------------------
-- system status -- -- system status --
@@ -147,8 +132,8 @@ local function init(parent, id)
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg} local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)} local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
u_ps.subscribe("U_StatusLine1", stat_line_1.set_value) stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value)
u_ps.subscribe("U_StatusLine2", stat_line_2.set_value) stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value)
----------------- -----------------
-- annunciator -- -- annunciator --
@@ -160,48 +145,48 @@ local function init(parent, id)
-- connectivity -- connectivity
local plc_online = IndicatorLight{parent=annunciator,label="PLC Online",colors=cpair(colors.green,colors.red)} local plc_online = IndicatorLight{parent=annunciator,label="PLC Online",colors=cpair(colors.green,colors.red)}
local plc_hbeat = IndicatorLight{parent=annunciator,label="PLC Heartbeat",colors=cpair(colors.white,colors.gray)} local plc_hbeat = IndicatorLight{parent=annunciator,label="PLC Heartbeat",colors=ind_wht}
local rad_mon = TriIndicatorLight{parent=annunciator,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green} local rad_mon = TriIndicatorLight{parent=annunciator,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
u_ps.subscribe("PLCOnline", plc_online.update) plc_online.register(u_ps, "PLCOnline", plc_online.update)
u_ps.subscribe("PLCHeartbeat", plc_hbeat.update) plc_hbeat.register(u_ps, "PLCHeartbeat", plc_hbeat.update)
u_ps.subscribe("RadiationMonitor", rad_mon.update) rad_mon.register(u_ps, "RadiationMonitor", rad_mon.update)
annunciator.line_break() annunciator.line_break()
-- operating state -- operating state
local r_active = IndicatorLight{parent=annunciator,label="Active",colors=cpair(colors.green,colors.gray)} local r_active = IndicatorLight{parent=annunciator,label="Active",colors=ind_grn}
local r_auto = IndicatorLight{parent=annunciator,label="Automatic Control",colors=cpair(colors.white,colors.gray)} local r_auto = IndicatorLight{parent=annunciator,label="Automatic Control",colors=ind_wht}
u_ps.subscribe("status", r_active.update) r_active.register(u_ps, "status", r_active.update)
u_ps.subscribe("AutoControl", r_auto.update) r_auto.register(u_ps, "AutoControl", r_auto.update)
-- main unit transient/warning annunciator panel -- main unit transient/warning annunciator panel
local r_scram = IndicatorLight{parent=annunciator,label="Reactor SCRAM",colors=cpair(colors.red,colors.gray)} local r_scram = IndicatorLight{parent=annunciator,label="Reactor SCRAM",colors=ind_red}
local r_mscrm = IndicatorLight{parent=annunciator,label="Manual Reactor SCRAM",colors=cpair(colors.red,colors.gray)} local r_mscrm = IndicatorLight{parent=annunciator,label="Manual Reactor SCRAM",colors=ind_red}
local r_ascrm = IndicatorLight{parent=annunciator,label="Auto Reactor SCRAM",colors=cpair(colors.red,colors.gray)} local r_ascrm = IndicatorLight{parent=annunciator,label="Auto Reactor SCRAM",colors=ind_red}
local rad_wrn = IndicatorLight{parent=annunciator,label="Radiation Warning",colors=cpair(colors.yellow,colors.gray)} local rad_wrn = IndicatorLight{parent=annunciator,label="Radiation Warning",colors=ind_yel}
local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=cpair(colors.red,colors.gray)} local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=ind_red}
local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=cpair(colors.yellow,colors.gray)} local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=ind_yel}
local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=cpair(colors.yellow,colors.gray)} local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=ind_yel}
local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=cpair(colors.red,colors.gray)} local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=ind_red}
local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=cpair(colors.yellow,colors.gray)} local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=ind_yel}
local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=cpair(colors.yellow,colors.gray)} local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=ind_yel}
local r_wloc = IndicatorLight{parent=annunciator,label="Waste Line Occlusion",colors=cpair(colors.yellow,colors.gray)} local r_wloc = IndicatorLight{parent=annunciator,label="Waste Line Occlusion",colors=ind_yel}
local r_hsrt = IndicatorLight{parent=annunciator,label="Startup Rate High",colors=cpair(colors.yellow,colors.gray)} local r_hsrt = IndicatorLight{parent=annunciator,label="Startup Rate High",colors=ind_yel}
u_ps.subscribe("ReactorSCRAM", r_scram.update) r_scram.register(u_ps, "ReactorSCRAM", r_scram.update)
u_ps.subscribe("ManualReactorSCRAM", r_mscrm.update) r_mscrm.register(u_ps, "ManualReactorSCRAM", r_mscrm.update)
u_ps.subscribe("AutoReactorSCRAM", r_ascrm.update) r_ascrm.register(u_ps, "AutoReactorSCRAM", r_ascrm.update)
u_ps.subscribe("RadiationWarning", rad_wrn.update) rad_wrn.register(u_ps, "RadiationWarning", rad_wrn.update)
u_ps.subscribe("RCPTrip", r_rtrip.update) r_rtrip.register(u_ps, "RCPTrip", r_rtrip.update)
u_ps.subscribe("RCSFlowLow", r_cflow.update) r_cflow.register(u_ps, "RCSFlowLow", r_cflow.update)
u_ps.subscribe("CoolantLevelLow", r_clow.update) r_clow.register(u_ps, "CoolantLevelLow", r_clow.update)
u_ps.subscribe("ReactorTempHigh", r_temp.update) r_temp.register(u_ps, "ReactorTempHigh", r_temp.update)
u_ps.subscribe("ReactorHighDeltaT", r_rhdt.update) r_rhdt.register(u_ps, "ReactorHighDeltaT", r_rhdt.update)
u_ps.subscribe("FuelInputRateLow", r_firl.update) r_firl.register(u_ps, "FuelInputRateLow", r_firl.update)
u_ps.subscribe("WasteLineOcclusion", r_wloc.update) r_wloc.register(u_ps, "WasteLineOcclusion", r_wloc.update)
u_ps.subscribe("HighStartupRate", r_hsrt.update) r_hsrt.register(u_ps, "HighStartupRate", r_hsrt.update)
-- RPS annunciator panel -- RPS annunciator panel
@@ -209,27 +194,27 @@ local function init(parent, id)
local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9} local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9}
local rps_annunc = Div{parent=rps,width=31,height=10,x=2,y=1} local rps_annunc = Div{parent=rps,width=31,height=10,x=2,y=1}
local rps_trp = IndicatorLight{parent=rps_annunc,label="RPS Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} local rps_trp = IndicatorLight{parent=rps_annunc,label="RPS Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local rps_dmg = IndicatorLight{parent=rps_annunc,label="Damage Level High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} local rps_dmg = IndicatorLight{parent=rps_annunc,label="Damage Level High",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local rps_exh = IndicatorLight{parent=rps_annunc,label="Excess Heated Coolant",colors=cpair(colors.yellow,colors.gray)} local rps_exh = IndicatorLight{parent=rps_annunc,label="Excess Heated Coolant",colors=ind_yel}
local rps_exw = IndicatorLight{parent=rps_annunc,label="Excess Waste",colors=cpair(colors.yellow,colors.gray)} local rps_exw = IndicatorLight{parent=rps_annunc,label="Excess Waste",colors=ind_yel}
local rps_tmp = IndicatorLight{parent=rps_annunc,label="Core Temperature High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} local rps_tmp = IndicatorLight{parent=rps_annunc,label="Core Temperature High",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local rps_nof = IndicatorLight{parent=rps_annunc,label="No Fuel",colors=cpair(colors.yellow,colors.gray)} local rps_nof = IndicatorLight{parent=rps_annunc,label="No Fuel",colors=ind_yel}
local rps_loc = IndicatorLight{parent=rps_annunc,label="Coolant Level Low Low",colors=cpair(colors.yellow,colors.gray)} local rps_loc = IndicatorLight{parent=rps_annunc,label="Coolant Level Low Low",colors=ind_yel}
local rps_flt = IndicatorLight{parent=rps_annunc,label="PPM Fault",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS} local rps_flt = IndicatorLight{parent=rps_annunc,label="PPM Fault",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
local rps_tmo = IndicatorLight{parent=rps_annunc,label="Connection Timeout",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS} local rps_tmo = IndicatorLight{parent=rps_annunc,label="Connection Timeout",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
local rps_sfl = IndicatorLight{parent=rps_annunc,label="System Failure",colors=cpair(colors.orange,colors.gray),flash=true,period=period.BLINK_500_MS} local rps_sfl = IndicatorLight{parent=rps_annunc,label="System Failure",colors=cpair(colors.orange,colors.gray),flash=true,period=period.BLINK_500_MS}
u_ps.subscribe("rps_tripped", rps_trp.update) rps_trp.register(u_ps, "rps_tripped", rps_trp.update)
u_ps.subscribe("high_dmg", rps_dmg.update) rps_dmg.register(u_ps, "high_dmg", rps_dmg.update)
u_ps.subscribe("ex_hcool", rps_exh.update) rps_exh.register(u_ps, "ex_hcool", rps_exh.update)
u_ps.subscribe("ex_waste", rps_exw.update) rps_exw.register(u_ps, "ex_waste", rps_exw.update)
u_ps.subscribe("high_temp", rps_tmp.update) rps_tmp.register(u_ps, "high_temp", rps_tmp.update)
u_ps.subscribe("no_fuel", rps_nof.update) rps_nof.register(u_ps, "no_fuel", rps_nof.update)
u_ps.subscribe("low_cool", rps_loc.update) rps_loc.register(u_ps, "low_cool", rps_loc.update)
u_ps.subscribe("fault", rps_flt.update) rps_flt.register(u_ps, "fault", rps_flt.update)
u_ps.subscribe("timeout", rps_tmo.update) rps_tmo.register(u_ps, "timeout", rps_tmo.update)
u_ps.subscribe("sys_fail", rps_sfl.update) rps_sfl.register(u_ps, "sys_fail", rps_sfl.update)
-- cooling annunciator panel -- cooling annunciator panel
@@ -238,19 +223,19 @@ local function init(parent, id)
local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1} local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1}
local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7} local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7}
local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=cpair(colors.yellow,colors.gray)} local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=ind_yel}
local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=colors.gray,c2=colors.white,c3=colors.green} local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=colors.gray,c2=colors.white,c3=colors.green}
local c_cfm = IndicatorLight{parent=rcs_annunc,label="Coolant Feed Mismatch",colors=cpair(colors.yellow,colors.gray)} local c_cfm = IndicatorLight{parent=rcs_annunc,label="Coolant Feed Mismatch",colors=ind_yel}
local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=cpair(colors.yellow,colors.gray)} local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=ind_yel}
local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)} local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=ind_yel}
local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)} local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=ind_yel}
u_ps.subscribe("RCSFault", c_flt.update) c_flt.register(u_ps, "RCSFault", c_flt.update)
u_ps.subscribe("EmergencyCoolant", c_emg.update) c_emg.register(u_ps, "EmergencyCoolant", c_emg.update)
u_ps.subscribe("CoolantFeedMismatch", c_cfm.update) c_cfm.register(u_ps, "CoolantFeedMismatch", c_cfm.update)
u_ps.subscribe("BoilRateMismatch", c_brm.update) c_brm.register(u_ps, "BoilRateMismatch", c_brm.update)
u_ps.subscribe("SteamFeedMismatch", c_sfm.update) c_sfm.register(u_ps, "SteamFeedMismatch", c_sfm.update)
u_ps.subscribe("MaxWaterReturnFeed", c_mwrf.update) c_mwrf.register(u_ps, "MaxWaterReturnFeed", c_mwrf.update)
local available_space = 16 - (unit.num_boilers * 2 + unit.num_turbines * 4) local available_space = 16 - (unit.num_boilers * 2 + unit.num_turbines * 4)
@@ -266,12 +251,12 @@ 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=cpair(colors.red,colors.gray)} local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
b_ps[1].subscribe("WasterLevelLow", b1_wll.update) b1_wll.register(b_ps[1], "WasterLevelLow", 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=cpair(colors.yellow,colors.gray)} local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
b_ps[1].subscribe("HeatingRateLow", b1_hr.update) b1_hr.register(b_ps[1], "HeatingRateLow", b1_hr.update)
end end
if unit.num_boilers > 1 then if unit.num_boilers > 1 then
-- note, can't (shouldn't for sure...) have 0 turbines -- note, can't (shouldn't for sure...) have 0 turbines
@@ -282,12 +267,12 @@ local function init(parent, id)
end end
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=cpair(colors.red,colors.gray)} local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
b_ps[2].subscribe("WasterLevelLow", b2_wll.update) b2_wll.register(b_ps[2], "WasterLevelLow", 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=cpair(colors.yellow,colors.gray)} local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
b_ps[2].subscribe("HeatingRateLow", b2_hr.update) b2_hr.register(b_ps[2], "HeatingRateLow", b2_hr.update)
end end
-- turbine annunciator panels -- turbine annunciator panels
@@ -296,19 +281,19 @@ local function init(parent, id)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red} local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
t_ps[1].subscribe("SteamDumpOpen", t1_sdo.update) t1_sdo.register(t_ps[1], "SteamDumpOpen", t1_sdo.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)} local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
t_ps[1].subscribe("TurbineOverSpeed", t1_tos.update) t1_tos.register(t_ps[1], "TurbineOverSpeed", t1_tos.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS} local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
t_ps[1].subscribe("GeneratorTrip", t1_gtrp.update) t1_gtrp.register(t_ps[1], "GeneratorTrip", t1_gtrp.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
t_ps[1].subscribe("TurbineTrip", t1_trp.update) t1_trp.register(t_ps[1], "TurbineTrip", t1_trp.update)
if unit.num_turbines > 1 then if unit.num_turbines > 1 then
if (available_space > 2 and unit.num_turbines == 2) or available_space > 3 then if (available_space > 2 and unit.num_turbines == 2) or available_space > 3 then
@@ -317,19 +302,19 @@ local function init(parent, id)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red} local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
t_ps[2].subscribe("SteamDumpOpen", t2_sdo.update) t2_sdo.register(t_ps[2], "SteamDumpOpen", t2_sdo.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)} local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
t_ps[2].subscribe("TurbineOverSpeed", t2_tos.update) t2_tos.register(t_ps[2], "TurbineOverSpeed", t2_tos.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS} local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
t_ps[2].subscribe("GeneratorTrip", t2_gtrp.update) t2_gtrp.register(t_ps[2], "GeneratorTrip", t2_gtrp.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
t_ps[2].subscribe("TurbineTrip", t2_trp.update) t2_trp.register(t_ps[2], "TurbineTrip", t2_trp.update)
end end
if unit.num_turbines > 2 then if unit.num_turbines > 2 then
@@ -337,19 +322,19 @@ local function init(parent, id)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red} local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
t_ps[3].subscribe("SteamDumpOpen", t3_sdo.update) t3_sdo.register(t_ps[3], "SteamDumpOpen", t3_sdo.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)} local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
t_ps[3].subscribe("TurbineOverSpeed", t3_tos.update) t3_tos.register(t_ps[3], "TurbineOverSpeed", t3_tos.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS} local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
t_ps[3].subscribe("GeneratorTrip", t3_gtrp.update) t3_gtrp.register(t_ps[3], "GeneratorTrip", t3_gtrp.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
t_ps[3].subscribe("TurbineTrip", t3_trp.update) t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update)
end end
---------------------- ----------------------
@@ -363,10 +348,10 @@ local function init(parent, id)
TextBox{parent=burn_control,x=9,y=2,text="mB/t"} TextBox{parent=burn_control,x=9,y=2,text="mB/t"}
local set_burn = function () unit.set_burn(burn_rate.get_value()) end local set_burn = function () unit.set_burn(burn_rate.get_value()) end
local set_burn_btn = PushButton{parent=burn_control,x=14,y=2,text="SET",min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),dis_fg_bg=dis_colors,callback=set_burn} local set_burn_btn = PushButton{parent=burn_control,x=14,y=2,text="SET",min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=style.wh_gray,dis_fg_bg=dis_colors,callback=set_burn}
u_ps.subscribe("burn_rate", burn_rate.set_value) burn_rate.register(u_ps, "burn_rate", burn_rate.set_value)
u_ps.subscribe("max_burn", burn_rate.set_max) burn_rate.register(u_ps, "max_burn", burn_rate.set_max)
local start = HazardButton{parent=main,x=2,y=28,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=unit.start,fg_bg=hzd_fg_bg} local start = HazardButton{parent=main,x=2,y=28,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=unit.start,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=main,x=12,y=32,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=unit.ack_alarms,fg_bg=hzd_fg_bg} local ack_a = HazardButton{parent=main,x=12,y=32,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=unit.ack_alarms,fg_bg=hzd_fg_bg}
@@ -387,17 +372,20 @@ local function init(parent, id)
end end
end end
u_ps.subscribe("status", start_button_en_check) start.register(u_ps, "status", start_button_en_check)
u_ps.subscribe("rps_tripped", start_button_en_check) start.register(u_ps, "rps_tripped", start_button_en_check)
u_ps.subscribe("rps_tripped", function (active) if active then reset.enable() else reset.disable() end end) start.register(u_ps, "auto_group_id", start_button_en_check)
start.register(u_ps, "AutoControl", start_button_en_check)
reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=48} TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=48}
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49} local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1} local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1}
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=waste_opts,callback=unit.set_waste,min_width=6} local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.waste.unit_opts,callback=unit.set_waste,min_width=6}
u_ps.subscribe("U_WasteMode", waste_mode.set_value) waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)
---------------------- ----------------------
-- alarm management -- -- alarm management --
@@ -420,20 +408,20 @@ local function init(parent, id)
local a_clt = AlarmLight{parent=alarm_panel,x=6,label="RCS Transient",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS} local a_clt = AlarmLight{parent=alarm_panel,x=6,label="RCS Transient",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS}
local a_tbt = AlarmLight{parent=alarm_panel,x=6,label="Turbine Trip",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS} local a_tbt = AlarmLight{parent=alarm_panel,x=6,label="Turbine Trip",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
u_ps.subscribe("Alarm_1", a_brc.update) a_brc.register(u_ps, "Alarm_1", a_brc.update)
u_ps.subscribe("Alarm_2", a_rad.update) a_rad.register(u_ps, "Alarm_2", a_rad.update)
u_ps.subscribe("Alarm_4", a_dmg.update) a_dmg.register(u_ps, "Alarm_4", a_dmg.update)
u_ps.subscribe("Alarm_3", a_rcl.update) a_rcl.register(u_ps, "Alarm_3", a_rcl.update)
u_ps.subscribe("Alarm_5", a_rcd.update) a_rcd.register(u_ps, "Alarm_5", a_rcd.update)
u_ps.subscribe("Alarm_6", a_rot.update) a_rot.register(u_ps, "Alarm_6", a_rot.update)
u_ps.subscribe("Alarm_7", a_rht.update) a_rht.register(u_ps, "Alarm_7", a_rht.update)
u_ps.subscribe("Alarm_8", a_rwl.update) a_rwl.register(u_ps, "Alarm_8", a_rwl.update)
u_ps.subscribe("Alarm_9", a_rwh.update) a_rwh.register(u_ps, "Alarm_9", a_rwh.update)
u_ps.subscribe("Alarm_10", a_rps.update) a_rps.register(u_ps, "Alarm_10", a_rps.update)
u_ps.subscribe("Alarm_11", a_clt.update) a_clt.register(u_ps, "Alarm_11", a_clt.update)
u_ps.subscribe("Alarm_12", a_tbt.update) a_tbt.register(u_ps, "Alarm_12", a_tbt.update)
-- ack's and resets -- ack's and resets
@@ -487,56 +475,47 @@ local function init(parent, id)
local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.blue,colors.white),radio_bg=colors.gray} local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.blue,colors.white),radio_bg=colors.gray}
u_ps.subscribe("auto_group_id", function (gid) group.set_value(gid + 1) end) group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end)
auto_div.line_break() auto_div.line_break()
local function set_group() unit.set_group(group.get_value() - 1) end local function set_group() unit.set_group(group.get_value() - 1) end
local set_grp_btn = PushButton{parent=auto_div,text="SET",x=4,min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),dis_fg_bg=cpair(colors.gray,colors.white),callback=set_group} local set_grp_btn = PushButton{parent=auto_div,text="SET",x=4,min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=style.wh_gray,dis_fg_bg=cpair(colors.gray,colors.white),callback=set_group}
auto_div.line_break() auto_div.line_break()
TextBox{parent=auto_div,text="Prio. Group",height=1,width=11,fg_bg=style.label} TextBox{parent=auto_div,text="Prio. Group",height=1,width=11,fg_bg=style.label}
local auto_grp = TextBox{parent=auto_div,text="Manual",height=1,width=11,fg_bg=bw_fg_bg} local auto_grp = TextBox{parent=auto_div,text="Manual",height=1,width=11,fg_bg=bw_fg_bg}
u_ps.subscribe("auto_group", auto_grp.set_value) auto_grp.register(u_ps, "auto_group", auto_grp.set_value)
auto_div.line_break() auto_div.line_break()
local a_rdy = IndicatorLight{parent=auto_div,label="Ready",x=2,colors=cpair(colors.green,colors.gray)} local a_rdy = IndicatorLight{parent=auto_div,label="Ready",x=2,colors=ind_grn}
local a_stb = IndicatorLight{parent=auto_div,label="Standby",x=2,colors=cpair(colors.white,colors.gray),flash=true,period=period.BLINK_1000_MS} local a_stb = IndicatorLight{parent=auto_div,label="Standby",x=2,colors=ind_wht,flash=true,period=period.BLINK_1000_MS}
u_ps.subscribe("U_AutoReady", a_rdy.update) a_rdy.register(u_ps, "U_AutoReady", a_rdy.update)
-- update standby indicator -- update standby indicator
u_ps.subscribe("status", function (active) a_stb.register(u_ps, "status", function (active)
a_stb.update(unit.annunciator.AutoControl and (not active)) a_stb.update(unit.annunciator.AutoControl and (not active))
end) end)
a_stb.register(u_ps, "AutoControl", function (auto_active)
-- enable and disable controls based on group assignment
u_ps.subscribe("auto_group_id", function (gid)
start_button_en_check()
if gid == 0 then
burn_rate.enable()
set_burn_btn.enable()
else
burn_rate.disable()
set_burn_btn.disable()
end
end)
-- enable and disable controls based on auto control state (start button is handled separately)
u_ps.subscribe("AutoControl", function (auto_active)
start_button_en_check()
if auto_active then if auto_active then
a_stb.update(unit.reactor_data.mek_status.status == false) a_stb.update(unit.reactor_data.mek_status.status == false)
else a_stb.update(false) end else a_stb.update(false) end
end) end)
-- enable/disable controls based on group assignment (start button is separate)
burn_rate.register(u_ps, "auto_group_id", function (gid)
if gid == 0 then burn_rate.enable() else burn_rate.disable() end
end)
set_burn_btn.register(u_ps, "auto_group_id", function (gid)
if gid == 0 then set_burn_btn.enable() else set_burn_btn.disable() end
end)
-- can't change group if auto is engaged regardless of if this unit is part of auto control -- can't change group if auto is engaged regardless of if this unit is part of auto control
f_ps.subscribe("auto_active", function (auto_active) set_grp_btn.register(f_ps, "auto_active", function (auto_active)
if auto_active then set_grp_btn.disable() else set_grp_btn.enable() end if auto_active then set_grp_btn.disable() else set_grp_btn.enable() end
end) end)

View File

@@ -0,0 +1,225 @@
--
-- Basic Unit Flow Overview
--
local util = require("scada-common.util")
local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local PipeNetwork = require("graphics.elements.pipenet")
local TextBox = require("graphics.elements.textbox")
local Rectangle = require("graphics.elements.rectangle")
local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light")
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
local TEXT_ALIGN = core.TEXT_ALIGN
local sprintf = util.sprintf
local cpair = core.cpair
local border = core.border
local pipe = core.pipe
local wh_gray = style.wh_gray
local bw_fg_bg = style.bw_fg_bg
local text_c = style.text_colors
local lu_c = style.lu_colors
local ind_grn = style.ind_grn
local ind_wht = style.ind_wht
-- make a new unit flow window
---@param parent graphics_element parent
---@param x integer top left x
---@param y integer top left y
---@param wide boolean whether to render wide version
---@param unit ioctl_unit unit database entry
local function make(parent, x, y, wide, unit)
local height = 16
local v_start = 1 + ((unit.unit_id - 1) * 5)
local prv_start = 1 + ((unit.unit_id - 1) * 3)
local v_fields = { "pu", "po", "pl", "am" }
local v_names = {
sprintf("PV%02d-PU", v_start),
sprintf("PV%02d-PO", v_start + 1),
sprintf("PV%02d-PL", v_start + 2),
sprintf("PV%02d-AM", v_start + 3),
sprintf("PRV%02d", prv_start),
sprintf("PRV%02d", prv_start + 1),
sprintf("PRV%02d", prv_start + 2)
}
assert(parent.get_height() >= (y + height), "flow display not of sufficient vertical resolution (add an additional row of monitors) " .. y .. "," .. parent.get_height())
local function _wide(a, b) return util.trinary(wide, a, b) end
-- bounding box div
local root = Div{parent=parent,x=x,y=y,width=_wide(136, 114),height=height}
local lg_gray = cpair(colors.lightGray, colors.gray)
------------------
-- COOLING LOOP --
------------------
local reactor = Rectangle{parent=root,x=1,y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=reactor,y=1,text="FISSION REACTOR",alignment=TEXT_ALIGN.CENTER,height=1}
TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=TEXT_ALIGN.CENTER,height=1}
TextBox{parent=root,x=19,y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
TextBox{parent=root,x=3,y=5,text="\x19",width=1,height=1,fg_bg=lg_gray}
local rc_pipes = {}
local emc_x = 42 -- emergency coolant connection x point
if unit.num_boilers > 0 then
table.insert(rc_pipes, pipe(0, 1, _wide(28, 19), 1, colors.lightBlue, true))
table.insert(rc_pipes, pipe(0, 3, _wide(28, 19), 3, colors.orange, true))
table.insert(rc_pipes, pipe(_wide(46 ,39), 1, _wide(72,58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(_wide(46,39), 3, _wide(72,58), 3, colors.white, true))
else
emc_x = 3
table.insert(rc_pipes, pipe(0, 1, _wide(72,58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(0, 3, _wide(72,58), 3, colors.white, true))
end
if unit.has_tank then
table.insert(rc_pipes, pipe(emc_x, 1, emc_x, 0, colors.blue, true, true))
end
local prv_yo = math.max(3 - unit.num_turbines, 0)
for i = 1, unit.num_turbines do
local py = 2 * (i - 1) + prv_yo
table.insert(rc_pipes, pipe(_wide(92, 78), py, _wide(104, 83), py, colors.white, true))
end
PipeNetwork{parent=root,x=20,y=1,pipes=rc_pipes,bg=colors.lightGray}
if unit.num_boilers > 0 then
local cc_rate = DataIndicator{parent=root,x=_wide(25,22),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
local hc_rate = DataIndicator{parent=root,x=_wide(25,22),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
cc_rate.register(unit.unit_ps, "boiler_boil_sum", function (sum) cc_rate.update(sum * 10) end)
hc_rate.register(unit.unit_ps, "heating_rate", hc_rate.update)
local boiler = Rectangle{parent=root,x=_wide(47,40),y=1,border=border(1, colors.gray, true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=boiler,y=1,text="THERMO-ELECTRIC",alignment=TEXT_ALIGN.CENTER,height=1}
TextBox{parent=boiler,y=3,text="BOILERS",alignment=TEXT_ALIGN.CENTER,height=1}
TextBox{parent=root,x=_wide(47,40),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
TextBox{parent=root,x=_wide(65,58),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
local wt_rate = DataIndicator{parent=root,x=_wide(71,61),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
local st_rate = DataIndicator{parent=root,x=_wide(71,61),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
wt_rate.register(unit.unit_ps, "turbine_flow_sum", wt_rate.update)
st_rate.register(unit.unit_ps, "boiler_boil_sum", st_rate.update)
else
local wt_rate = DataIndicator{parent=root,x=28,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
local st_rate = DataIndicator{parent=root,x=28,y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
wt_rate.register(unit.unit_ps, "turbine_flow_sum", wt_rate.update)
st_rate.register(unit.unit_ps, "heating_rate", st_rate.update)
end
local turbine = Rectangle{parent=root,x=_wide(93,79),y=1,border=border(1, colors.gray, true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=turbine,y=1,text="STEAM TURBINE",alignment=TEXT_ALIGN.CENTER,height=1}
TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=TEXT_ALIGN.CENTER,height=1}
TextBox{parent=root,x=_wide(93,79),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
for i = 1, unit.num_turbines do
local ry = 1 + (2 * (i - 1)) + prv_yo
TextBox{parent=root,x=_wide(125,103),y=ry,text="\x10\x11\x7f",fg_bg=text_c,width=3,height=1}
local state = TriIndicatorLight{parent=root,x=_wide(129,107),y=ry,label=v_names[i+4],c1=colors.gray,c2=colors.yellow,c3=colors.red}
state.register(unit.turbine_ps_tbl[i], "SteamDumpOpen", state.update)
end
----------------------
-- WASTE PROCESSING --
----------------------
local waste = Div{parent=root,x=3,y=6}
local waste_pipes = {
pipe(0, 0, _wide(19, 16), 1, colors.brown, true),
pipe(_wide(14, 13), 1, _wide(19, 17), 5, colors.brown, true),
pipe(_wide(22, 19), 1, _wide(49, 45), 1, colors.brown, true),
pipe(_wide(22, 19), 5, _wide(28, 24), 5, colors.brown, true),
pipe(_wide(64, 53), 1, _wide(95, 81), 1, colors.green, true),
pipe(_wide(48, 43), 4, _wide(71, 61), 4, colors.cyan, true),
pipe(_wide(66, 57), 4, _wide(71, 61), 8, colors.cyan, true),
pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.cyan, true),
pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.cyan, true),
pipe(_wide(108, 94), 1, _wide(132, 110), 6, colors.black, true, true),
pipe(_wide(108, 94), 4, _wide(111, 95), 1, colors.black, true, true),
pipe(_wide(132, 110), 6, _wide(130, 108), 6, colors.black, true, true)
}
PipeNetwork{parent=waste,x=1,y=1,pipes=waste_pipes,bg=colors.lightGray}
local function _valve(vx, vy, n)
TextBox{parent=waste,x=vx,y=vy,text="\x10\x11",fg_bg=text_c,width=2,height=1}
local conn = IndicatorLight{parent=waste,x=vx-3,y=vy+1,label=v_names[n],colors=ind_grn}
local open = IndicatorLight{parent=waste,x=vx-3,y=vy+2,label="OPEN",colors=ind_wht}
conn.register(unit.unit_ps, util.c("V_", v_fields[n], "_conn"), conn.update)
open.register(unit.unit_ps, util.c("V_", v_fields[n], "_state"), open.update)
end
local function _machine(mx, my, name)
local l = string.len(name) + 2
TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=TEXT_ALIGN.CENTER,fg_bg=lg_gray,width=l,height=1}
TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=TEXT_ALIGN.CENTER,fg_bg=wh_gray,width=l,height=1}
end
local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg}
local pu_rate = DataIndicator{parent=waste,x=_wide(82,70),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
local po_rate = DataIndicator{parent=waste,x=_wide(52,45),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
local popl_rate = DataIndicator{parent=waste,x=_wide(82,70),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
local poam_rate = DataIndicator{parent=waste,x=_wide(82,70),y=10,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
local spent_rate = DataIndicator{parent=waste,x=_wide(117,99),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
waste_rate.register(unit.unit_ps, "act_burn_rate", waste_rate.update)
pu_rate.register(unit.unit_ps, "pu_rate", pu_rate.update)
po_rate.register(unit.unit_ps, "po_rate", po_rate.update)
popl_rate.register(unit.unit_ps, "po_pl_rate", popl_rate.update)
poam_rate.register(unit.unit_ps, "po_am_rate", poam_rate.update)
spent_rate.register(unit.unit_ps, "ws_rate", spent_rate.update)
_valve(_wide(21, 18), 2, 1)
_valve(_wide(21, 18), 6, 2)
_valve(_wide(73, 62), 5, 3)
_valve(_wide(73, 62), 9, 4)
_machine(_wide(51, 45), 1, "CENTRIFUGE \x1a");
_machine(_wide(97, 83), 1, "PRC [Pu] \x1a");
_machine(_wide(97, 83), 4, "PRC [Po] \x1a");
_machine(_wide(116, 94), 6, "SPENT WASTE \x1b")
TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=TEXT_ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=bw_fg_bg}
local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn}
local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c,label="CNT",unit="",format="%2d",value=0,width=7}
local sna_pk = DataIndicator{parent=sna_po,y=3,lu_colors=lu_c,label="PEAK",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max = DataIndicator{parent=sna_po,lu_colors=lu_c,label="MAX",unit="mB/t",format="%8.2f",value=0,width=17}
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c,label="IN",unit="mB/t",format="%9.2f",value=0,width=17}
sna_act.register(unit.unit_ps, "po_rate", function (r) sna_act.update(r > 0) end)
sna_cnt.register(unit.unit_ps, "sna_count", sna_cnt.update)
sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.update)
sna_max.register(unit.unit_ps, "sna_prod_rate", sna_max.update)
sna_in.register(unit.unit_ps, "sna_in", sna_in.update)
return root
end
return make

View File

@@ -14,9 +14,9 @@ local Div = require("graphics.elements.div")
local PipeNetwork = require("graphics.elements.pipenet") local PipeNetwork = require("graphics.elements.pipenet")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.textbox")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN local TEXT_ALIGN = core.TEXT_ALIGN
local pipe = core.graphics.pipe local pipe = core.pipe
-- make a new unit overview window -- make a new unit overview window
---@param parent graphics_element parent ---@param parent graphics_element parent
@@ -38,7 +38,7 @@ local function make(parent, x, y, unit)
height = 17 height = 17
end end
assert(parent.height() >= (y + height), "main display not of sufficient vertical resolution (add an additional row of monitors)") assert(parent.get_height() >= (y + height), "main display not of sufficient vertical resolution (add an additional row of monitors)")
-- bounding box div -- bounding box div
local root = Div{parent=parent,x=x,y=y,width=80,height=height} local root = Div{parent=parent,x=x,y=y,width=80,height=height}

View File

@@ -0,0 +1,381 @@
--
-- Flow Monitor GUI
--
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local unit_flow = require("coordinator.ui.components.unit_flow")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local PipeNetwork = require("graphics.elements.pipenet")
local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data")
local HorizontalBar = require("graphics.elements.indicators.hbar")
local IndicatorLight = require("graphics.elements.indicators.light")
local StateIndicator = require("graphics.elements.indicators.state")
local CONTAINER_MODE = types.CONTAINER_MODE
local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.cpair
local border = core.border
local pipe = core.pipe
local wh_gray = style.wh_gray
local bw_fg_bg = style.bw_fg_bg
local text_col = style.text_colors
local lu_col = style.lu_colors
-- create new flow view
---@param main graphics_element main displaybox
local function init(main)
local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units
local tank_defs = facility.tank_defs
local tank_list = facility.tank_list
-- window header message
local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
-- max length example: "01:23:45 AM - Wednesday, September 28 2022"
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
datetime.register(facility.ps, "date_time", datetime.set_value)
local po_pipes = {}
local water_pipes = {}
-- get the y offset for this unit index
---@param idx integer unit index
local function y_ofs(idx) return ((idx - 1) * 20) end
-- determinte facility tank start/end from the definitions list
---@param start_idx integer start index of table iteration
---@param end_idx integer end index of table iteration
local function find_fdef(start_idx, end_idx)
local first, last = 4, 0
for i = start_idx, end_idx do
if tank_defs[i] == 2 then
last = i
if i < first then first = i end
end
end
return first, last
end
if facility.tank_mode == 0 or facility.tank_mode == 8 then
-- (0) tanks belong to reactor units OR (8) 4 total facility tanks (A B C D)
for i = 1, facility.num_units do
if units[i].has_tank then
local y = y_ofs(i)
table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true))
table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true))
local u = units[i] ---@type ioctl_unit
local x = util.trinary(u.num_boilers == 0, 45, 84)
table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true))
end
end
else
-- setup connections for units with emergency coolant, always the same
for i = 1, #tank_defs do
if tank_defs[i] > 0 then
local y = y_ofs(i)
if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(1, y, 21, y, colors.blue, true))
else
table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true))
table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true))
end
local u = units[i] ---@type ioctl_unit
local x = util.trinary(u.num_boilers == 0, 45, 84)
table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true))
end
end
if facility.tank_mode == 1 then
-- (1) 1 total facility tank (A A A A)
local first_fdef, last_fdef = find_fdef(1, #tank_defs)
for i = 1, #tank_defs do
local y = y_ofs(i)
if i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
end
end
end
elseif facility.tank_mode == 2 then
-- (2) 2 total facility tanks (A A A B)
local first_fdef, last_fdef = find_fdef(1, math.min(3, #tank_defs))
for i = 1, #tank_defs do
local y = y_ofs(i)
if i == 4 then
if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
end
elseif i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
end
end
end
elseif facility.tank_mode == 3 then
-- (3) 2 total facility tanks (A A B B)
for _, a in pairs({ 1, 3 }) do
local b = a + 1
if tank_defs[a] == 2 then
table.insert(water_pipes, pipe(0, y_ofs(a), 1, y_ofs(a) + 6, colors.blue, true))
if tank_defs[b] == 2 then
table.insert(water_pipes, pipe(0, y_ofs(b) - 13, 1, y_ofs(b), colors.blue, true))
end
elseif tank_defs[b] == 2 then
table.insert(water_pipes, pipe(0, y_ofs(b), 1, y_ofs(b) + 6, colors.blue, true))
end
end
elseif facility.tank_mode == 4 then
-- (4) 2 total facility tanks (A B B B)
local first_fdef, last_fdef = find_fdef(2, #tank_defs)
for i = 1, #tank_defs do
local y = y_ofs(i)
if i == 1 then
if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
end
elseif i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
end
end
end
elseif facility.tank_mode == 5 then
-- (5) 3 total facility tanks (A A B C)
local first_fdef, last_fdef = find_fdef(1, math.min(2, #tank_defs))
for i = 1, #tank_defs do
local y = y_ofs(i)
if i == 3 or i == 4 then
if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
end
elseif i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
end
end
end
elseif facility.tank_mode == 6 then
-- (6) 3 total facility tanks (A B B C)
local first_fdef, last_fdef = find_fdef(2, math.min(3, #tank_defs))
for i = 1, #tank_defs do
local y = y_ofs(i)
if i == 1 or i == 4 then
if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
end
elseif i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
end
end
end
elseif facility.tank_mode == 7 then
-- (7) 3 total facility tanks (A B C C)
local first_fdef, last_fdef = find_fdef(3, #tank_defs)
for i = 1, #tank_defs do
local y = y_ofs(i)
if i == 1 or i == 2 then
if tank_defs[i] == 2 then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
end
elseif i == first_fdef then
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
elseif i < last_fdef then
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
end
end
end
end
end
local flow_x = 3
if #water_pipes > 0 then
flow_x = 25
PipeNetwork{parent=main,x=2,y=3,pipes=water_pipes,bg=colors.lightGray}
end
for i = 1, facility.num_units do
local y_offset = y_ofs(i)
unit_flow(main, flow_x, 5 + y_offset, #water_pipes == 0, units[i])
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.cyan, true, true))
end
PipeNetwork{parent=main,x=139,y=15,pipes=po_pipes,bg=colors.lightGray}
-----------------
-- tank valves --
-----------------
local next_f_id = 1
for i = 1, #tank_defs do
if tank_defs[i] > 0 then
local vy = 3 + y_ofs(i)
TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=cpair(colors.black,colors.lightGray),width=2,height=1}
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", i * 5),colors=style.ind_grn}
local open = IndicatorLight{parent=main,x=9,y=vy+2,label="OPEN",colors=style.ind_wht}
conn.register(units[i].unit_ps, "V_emc_conn", conn.update)
open.register(units[i].unit_ps, "V_emc_state", open.update)
end
end
-------------------
-- dynamic tanks --
-------------------
for i = 1, #tank_list do
if tank_list[i] > 0 then
local id = "U-" .. i
local f_id = next_f_id
if tank_list[i] == 2 then
id = "F-" .. next_f_id
next_f_id = next_f_id + 1
end
local y_offset = y_ofs(i)
local tank = Div{parent=main,x=3,y=7+y_offset,width=20,height=14}
TextBox{parent=tank,text=" ",height=1,x=1,y=1,fg_bg=cpair(colors.lightGray,colors.gray)}
TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.wh_gray}
local tank_box = Rectangle{parent=tank,border=border(1,colors.gray,true),width=20,height=12}
local status = StateIndicator{parent=tank_box,x=3,y=1,states=style.dtank.states,value=1,min_width=14}
TextBox{parent=tank_box,x=2,y=3,text="Fill",height=1,width=10,fg_bg=style.label}
local tank_pcnt = DataIndicator{parent=tank_box,x=10,y=3,label="",format="%5.2f",value=100,unit="%",lu_colors=lu_col,width=8,fg_bg=text_col}
local tank_amnt = DataIndicator{parent=tank_box,x=2,label="",format="%13d",value=0,unit="mB",lu_colors=lu_col,width=16,fg_bg=bw_fg_bg}
TextBox{parent=tank_box,x=2,y=6,text="Water Level",height=1,width=11,fg_bg=style.label}
local level = HorizontalBar{parent=tank_box,x=2,y=7,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=16}
TextBox{parent=tank_box,x=2,y=9,text="In/Out Mode",height=1,width=11,fg_bg=style.label}
local can_fill = IndicatorLight{parent=tank_box,x=2,y=10,label="FILL",colors=style.ind_wht}
local can_empty = IndicatorLight{parent=tank_box,x=10,y=10,label="EMPTY",colors=style.ind_wht}
local function _can_fill(mode)
can_fill.update((mode == CONTAINER_MODE.BOTH) or (mode == CONTAINER_MODE.FILL))
end
local function _can_empty(mode)
can_empty.update((mode == CONTAINER_MODE.BOTH) or (mode == CONTAINER_MODE.EMPTY))
end
if tank_list[i] == 1 then
status.register(units[i].tank_ps_tbl[1], "computed_status", status.update)
tank_pcnt.register(units[i].tank_ps_tbl[1], "fill", function (f) tank_pcnt.update(f * 100) end)
tank_amnt.register(units[i].tank_ps_tbl[1], "stored", function (sto) tank_amnt.update(sto.amount) end)
level.register(units[i].tank_ps_tbl[1], "fill", level.update)
can_fill.register(units[i].tank_ps_tbl[1], "container_mode", _can_fill)
can_empty.register(units[i].tank_ps_tbl[1], "container_mode", _can_empty)
else
status.register(facility.tank_ps_tbl[f_id], "computed_status", status.update)
tank_pcnt.register(facility.tank_ps_tbl[f_id], "fill", function (f) tank_pcnt.update(f * 100) end)
tank_amnt.register(facility.tank_ps_tbl[f_id], "stored", function (sto) tank_amnt.update(sto.amount) end)
level.register(facility.tank_ps_tbl[f_id], "fill", level.update)
can_fill.register(facility.tank_ps_tbl[f_id], "container_mode", _can_fill)
can_empty.register(facility.tank_ps_tbl[f_id], "container_mode", _can_empty)
end
end
end
---------
-- SPS --
---------
local sps = Div{parent=main,x=140,y=3,height=12}
TextBox{parent=sps,text=" ",width=24,height=1,x=1,y=1,fg_bg=cpair(colors.lightGray,colors.gray)}
TextBox{parent=sps,text="SPS",alignment=TEXT_ALIGN.CENTER,width=24,height=1,fg_bg=wh_gray}
local sps_box = Rectangle{parent=sps,border=border(1,colors.gray,true),width=24,height=10}
local status = StateIndicator{parent=sps_box,x=5,y=1,states=style.sps.states,value=1,min_width=14}
status.register(facility.sps_ps_tbl[1], "computed_status", status.update)
TextBox{parent=sps_box,x=2,y=3,text="Input Rate",height=1,width=10,fg_bg=style.label}
local sps_in = DataIndicator{parent=sps_box,x=2,label="",format="%15.3f",value=0,unit="mB/t",lu_colors=lu_col,width=20,fg_bg=bw_fg_bg}
sps_in.register(facility.ps, "po_am_rate", sps_in.update)
TextBox{parent=sps_box,x=2,y=6,text="Production Rate",height=1,width=15,fg_bg=style.label}
local sps_rate = DataIndicator{parent=sps_box,x=2,label="",format="%15d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=20,fg_bg=bw_fg_bg}
sps_rate.register(facility.sps_ps_tbl[1], "process_rate", function (r) sps_rate.update(r * 1000) end)
----------------
-- statistics --
----------------
TextBox{parent=main,x=145,y=16,text="PROC. WASTE",alignment=TEXT_ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
local pr_waste = Rectangle{parent=main,x=145,y=17,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=bw_fg_bg}
local pu = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17}
local po = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Po",unit="mB/t",format="%9.3f",value=0,width=17}
local popl = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="PoPl",unit="mB/t",format="%7.3f",value=0,width=17}
pu.register(facility.ps, "pu_rate", pu.update)
po.register(facility.ps, "po_rate", po.update)
popl.register(facility.ps, "po_pl_rate", popl.update)
TextBox{parent=main,x=145,y=23,text="SPENT WASTE",alignment=TEXT_ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
local sp_waste = Rectangle{parent=main,x=145,y=24,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg}
local sum_sp_waste = DataIndicator{parent=sp_waste,lu_colors=lu_col,label="SUM",unit="mB/t",format="%8.3f",value=0,width=17}
sum_sp_waste.register(facility.ps, "spent_waste_rate", sum_sp_waste.update)
end
return init

View File

@@ -0,0 +1,124 @@
--
-- Coordinator Front Panel GUI
--
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local pgi = require("coordinator.ui.pgi")
local style = require("coordinator.ui.style")
local pkt_entry = require("coordinator.ui.components.pkt_entry")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local TabBar = require("graphics.elements.controls.tabbar")
local LED = require("graphics.elements.indicators.led")
local RGBLED = require("graphics.elements.indicators.ledrgb")
local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.cpair
-- create new front panel view
---@param panel graphics_element main displaybox
---@param num_units integer number of units (number of unit monitors)
local function init(panel, num_units)
local ps = iocontrol.get_db().fp.ps
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.fp.header}
local page_div = Div{parent=panel,x=1,y=3}
--
-- system indicators
--
local main_page = Div{parent=page_div,x=1,y=1}
local system = Div{parent=main_page,width=14,height=17,x=2,y=2}
local status = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)}
status.update(true)
system.line_break()
heartbeat.register(ps, "heartbeat", heartbeat.update)
local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
system.line_break()
modem.register(ps, "has_modem", modem.update)
network.register(ps, "link_state", network.update)
local speaker = LED{parent=system,label="SPEAKER",colors=cpair(colors.green,colors.green_off)}
speaker.register(ps, "has_speaker", speaker.update)
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)}
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=cpair(colors.green,colors.green_off)}
main_monitor.register(ps, "main_monitor", main_monitor.update)
local flow_monitor = LED{parent=monitors,label="FLOW MONITOR",colors=cpair(colors.green,colors.green_off)}
flow_monitor.register(ps, "flow_monitor", flow_monitor.update)
monitors.line_break()
for i = 1, num_units do
local unit_monitor = LED{parent=monitors,label="UNIT "..i.." MONITOR",colors=cpair(colors.green,colors.green_off)}
unit_monitor.register(ps, "unit_monitor_" .. i, unit_monitor.update)
end
--
-- about footer
--
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=cpair(colors.lightGray,colors.ivory)}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
--
-- page handling
--
-- API page
local api_page = Div{parent=page_div,x=1,y=1,hidden=true}
local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=api_list,height=1,hidden=true} -- padding
-- assemble page panes
local panes = { main_page, api_page }
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
local tabs = {
{ name = "CRD", color = cpair(colors.black, colors.ivory) },
{ name = "API", color = cpair(colors.black, colors.ivory) },
}
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=cpair(colors.black,colors.white)}
-- link pocket API list management to PGI
pgi.link_elements(api_list, pkt_entry)
end
return init

View File

@@ -9,7 +9,7 @@ local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
local imatrix = require("coordinator.ui.components.imatrix") local imatrix = require("coordinator.ui.components.imatrix")
local process_ctl = require("coordinator.ui.components.processctl") local process_ctl = require("coordinator.ui.components.process_ctl")
local unit_overview = require("coordinator.ui.components.unit_overview") local unit_overview = require("coordinator.ui.components.unit_overview")
local core = require("graphics.core") local core = require("graphics.core")
@@ -18,9 +18,9 @@ local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.data")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.graphics.cpair local cpair = core.cpair
-- create new main view -- create new main view
---@param main graphics_element main displaybox ---@param main graphics_element main displaybox
@@ -32,10 +32,10 @@ local function init(main)
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=cpair(colors.lightGray, colors.white),width=12,fg_bg=style.header} local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=cpair(colors.lightGray, colors.white),width=12,fg_bg=style.header}
-- max length example: "01:23:45 AM - Wednesday, September 28 2022" -- max length example: "01:23:45 AM - Wednesday, September 28 2022"
local datetime = TextBox{parent=main,x=(header.width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header} local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
facility.ps.subscribe("sv_ping", ping.update) ping.register(facility.ps, "sv_ping", ping.update)
facility.ps.subscribe("date_time", datetime.set_value) datetime.register(facility.ps, "date_time", datetime.set_value)
local uo_1, uo_2, uo_3, uo_4 ---@type graphics_element local uo_1, uo_2, uo_3, uo_4 ---@type graphics_element
@@ -45,12 +45,12 @@ local function init(main)
-- unit overviews -- unit overviews
if facility.num_units >= 1 then if facility.num_units >= 1 then
uo_1 = unit_overview(main, 2, 3, units[1]) uo_1 = unit_overview(main, 2, 3, units[1])
row_1_height = uo_1.height() row_1_height = uo_1.get_height()
end end
if facility.num_units >= 2 then if facility.num_units >= 2 then
uo_2 = unit_overview(main, 84, 3, units[2]) uo_2 = unit_overview(main, 84, 3, units[2])
row_1_height = math.max(row_1_height, uo_2.height()) row_1_height = math.max(row_1_height, uo_2.get_height())
end end
cnc_y_start = cnc_y_start + row_1_height + 1 cnc_y_start = cnc_y_start + row_1_height + 1
@@ -60,11 +60,11 @@ local function init(main)
local row_2_offset = cnc_y_start local row_2_offset = cnc_y_start
uo_3 = unit_overview(main, 2, row_2_offset, units[3]) uo_3 = unit_overview(main, 2, row_2_offset, units[3])
cnc_y_start = row_2_offset + uo_3.height() + 1 cnc_y_start = row_2_offset + uo_3.get_height() + 1
if facility.num_units == 4 then if facility.num_units == 4 then
uo_4 = unit_overview(main, 84, row_2_offset, units[4]) uo_4 = unit_overview(main, 84, row_2_offset, units[4])
cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.height() + 1) cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.get_height() + 1)
end end
end end
@@ -73,11 +73,11 @@ local function init(main)
cnc_y_start = cnc_y_start cnc_y_start = cnc_y_start
-- induction matrix and process control interfaces are 24 tall + space needed for divider -- induction matrix and process control interfaces are 24 tall + space needed for divider
local cnc_bottom_align_start = main.height() - 26 local cnc_bottom_align_start = main.get_height() - 26
assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)") assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)")
TextBox{parent=main,y=cnc_bottom_align_start,text=util.strrep("\x8c", header.width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=cpair(colors.lightGray,colors.gray)} TextBox{parent=main,y=cnc_bottom_align_start,text=util.strrep("\x8c", header.get_width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=cpair(colors.lightGray,colors.gray)}
cnc_bottom_align_start = cnc_bottom_align_start + 2 cnc_bottom_align_start = cnc_bottom_align_start + 2

60
coordinator/ui/pgi.lua Normal file
View File

@@ -0,0 +1,60 @@
--
-- Protected Graphics Interface
--
local log = require("scada-common.log")
local util = require("scada-common.util")
local pgi = {}
local data = {
pkt_list = nil, ---@type nil|graphics_element
pkt_entry = nil, ---@type function
-- session entries
s_entries = { pkt = {} }
}
-- link list boxes
---@param pkt_list graphics_element pocket list element
---@param pkt_entry function pocket entry constructor
function pgi.link_elements(pkt_list, pkt_entry)
data.pkt_list = pkt_list
data.pkt_entry = pkt_entry
end
-- unlink all fields, disabling the PGI
function pgi.unlink()
data.pkt_list = nil
data.pkt_entry = nil
end
-- add a PKT entry to the PKT list
---@param session_id integer pocket session
function pgi.create_pkt_entry(session_id)
if data.pkt_list ~= nil and data.pkt_entry ~= nil then
local success, result = pcall(data.pkt_entry, data.pkt_list, session_id)
if success then
data.s_entries.pkt[session_id] = result
else
log.error(util.c("PGI: failed to create PKT entry (", result, ")"), true)
end
end
end
-- delete a PKT entry from the PKT list
---@param session_id integer pocket session
function pgi.delete_pkt_entry(session_id)
if data.s_entries.pkt[session_id] ~= nil then
local success, result = pcall(data.s_entries.pkt[session_id].delete)
data.s_entries.pkt[session_id] = nil
if not success then
log.error(util.c("PGI: failed to delete PKT entry (", result, ")"), true)
end
else
log.debug(util.c("PGI: tried to delete unknown PKT entry ", session_id))
end
end
return pgi

View File

@@ -6,10 +6,45 @@ local core = require("graphics.core")
local style = {} local style = {}
local cpair = core.graphics.cpair local cpair = core.cpair
-- GLOBAL -- -- GLOBAL --
-- add color mappings for front panel
colors.ivory = colors.pink
colors.yellow_hc = colors.purple
colors.red_off = colors.brown
colors.yellow_off = colors.magenta
colors.green_off = colors.lime
-- front panel styling
style.fp = {}
style.fp.root = cpair(colors.black, colors.ivory)
style.fp.header = cpair(colors.black, colors.lightGray)
style.fp.colors = {
{ c = colors.red, hex = 0xdf4949 }, -- RED ON
{ c = colors.orange, hex = 0xffb659 },
{ c = colors.yellow, hex = 0xf9fb53 }, -- YELLOW ON
{ c = colors.lime, hex = 0x16665a }, -- GREEN OFF
{ c = colors.green, hex = 0x6be551 }, -- GREEN ON
{ c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff },
{ c = colors.purple, hex = 0xb156ee }, -- YELLOW HIGH CONTRAST
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
-- { c = colors.white, hex = 0xdcd9ca },
{ c = colors.lightGray, hex = 0xb1b8b3 },
{ c = colors.gray, hex = 0x575757 },
-- { c = colors.black, hex = 0x191919 },
{ c = colors.brown, hex = 0x672223 } -- RED OFF
}
-- main GUI styling
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.label = cpair(colors.gray, colors.lightGray)
@@ -33,7 +68,22 @@ style.colors = {
-- { c = colors.brown, hex = 0x7f664c } -- { c = colors.brown, hex = 0x7f664c }
} }
-- MAIN LAYOUT -- -- COMMON COLOR PAIRS --
style.wh_gray = cpair(colors.white, colors.gray)
style.bw_fg_bg = cpair(colors.black, colors.white)
style.text_colors = cpair(colors.black, colors.lightGray)
style.lu_colors = cpair(colors.gray, colors.gray)
style.hzd_fg_bg = style.wh_gray
style.dis_colors = cpair(colors.white, colors.lightGray)
style.ind_grn = cpair(colors.green, colors.gray)
style.ind_yel = cpair(colors.yellow, colors.gray)
style.ind_red = cpair(colors.red, colors.gray)
style.ind_wht = style.wh_gray
-- UI COMPONENTS --
style.reactor = { style.reactor = {
-- reactor states -- reactor states
@@ -151,8 +201,121 @@ style.imatrix = {
{ {
color = cpair(colors.black, colors.yellow), color = cpair(colors.black, colors.yellow),
text = "HIGH CHARGE" text = "HIGH CHARGE"
}
}
}
style.sps = {
-- SPS states
states = {
{
color = cpair(colors.black, colors.yellow),
text = "OFF-LINE"
},
{
color = cpair(colors.black, colors.orange),
text = "NOT FORMED"
},
{
color = cpair(colors.black, colors.orange),
text = "RTU FAULT"
},
{
color = cpair(colors.white, colors.gray),
text = "IDLE"
},
{
color = cpair(colors.black, colors.green),
text = "ACTIVE"
}
}
}
style.dtank = {
-- dynamic tank states
states = {
{
color = cpair(colors.black, colors.yellow),
text = "OFF-LINE"
},
{
color = cpair(colors.black, colors.orange),
text = "NOT FORMED"
},
{
color = cpair(colors.black, colors.orange),
text = "RTU FAULT"
},
{
color = cpair(colors.black, colors.green),
text = "ONLINE"
},
{
color = cpair(colors.black, colors.yellow),
text = "LOW FILL"
},
{
color = cpair(colors.black, colors.green),
text = "FILLED"
}, },
} }
} }
style.waste = {
-- auto waste processing states
states = {
{
color = cpair(colors.black, colors.green),
text = "PLUTONIUM"
},
{
color = cpair(colors.black, colors.cyan),
text = "POLONIUM"
},
{
color = cpair(colors.black, colors.purple),
text = "ANTI MATTER"
}
},
states_abbrv = {
{
color = cpair(colors.black, colors.green),
text = "Pu"
},
{
color = cpair(colors.black, colors.cyan),
text = "Po"
},
{
color = cpair(colors.black, colors.purple),
text = "AM"
}
},
-- process radio button options
options = { "Plutonium", "Polonium", "Antimatter" },
-- unit waste selection
unit_opts = {
{
text = "Auto",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.white, colors.gray)
},
{
text = "Pu",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.green)
},
{
text = "Po",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.cyan)
},
{
text = "AM",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.purple)
}
}
}
return style return style

View File

@@ -1,96 +1,21 @@
-- --
-- Graphics Core Functions and Objects -- Graphics Core Types, Checks, and Constructors
-- --
local events = require("graphics.events")
local flasher = require("graphics.flasher")
local core = {} local core = {}
local flasher = require("graphics.flasher") core.version = "1.1.1"
core.flasher = flasher core.flasher = flasher
local events = {}
---@enum click_type
events.click_type = {
VIRTUAL = 0,
LEFT_BUTTON = 1,
RIGHT_BUTTON = 2,
MID_BUTTON = 3
}
---@class mouse_interaction
---@field monitor string
---@field button integer
---@field x integer
---@field y integer
-- create a new monitor touch mouse interaction event
---@nodiscard
---@param monitor string
---@param x integer
---@param y integer
---@return mouse_interaction
function events.touch(monitor, x, y)
return {
monitor = monitor,
button = events.click_type.LEFT_BUTTON,
x = x,
y = y
}
end
-- create a new mouse click mouse interaction event
---@nodiscard
---@param button click_type
---@param x integer
---@param y integer
---@return mouse_interaction
function events.click(button, x, y)
return {
monitor = "terminal",
button = button,
x = x,
y = y
}
end
-- create a new transposed mouse interaction event using the event's monitor/button fields
---@nodiscard
---@param event mouse_interaction
---@param new_x integer
---@param new_y integer
---@return mouse_interaction
function events.mouse_transposed(event, new_x, new_y)
return {
monitor = event.monitor,
button = event.button,
x = new_x,
y = new_y
}
end
-- create a new generic mouse interaction event
---@nodiscard
---@param monitor string
---@param button click_type
---@param x integer
---@param y integer
---@return mouse_interaction
function events.mouse_generic(monitor, button, x, y)
return {
monitor = monitor,
button = button,
x = x,
y = y
}
end
core.events = events core.events = events
local graphics = {} -- Core Types
---@enum TEXT_ALIGN ---@enum TEXT_ALIGN
graphics.TEXT_ALIGN = { core.TEXT_ALIGN = {
LEFT = 1, LEFT = 1,
CENTER = 2, CENTER = 2,
RIGHT = 3 RIGHT = 3
@@ -109,7 +34,7 @@ graphics.TEXT_ALIGN = {
---@param color color border color ---@param color color border color
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false ---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
---@return graphics_border ---@return graphics_border
function graphics.border(width, color, even) function core.border(width, color, even)
return { return {
width = width, width = width,
color = color, color = color,
@@ -130,7 +55,7 @@ end
---@param w integer ---@param w integer
---@param h integer ---@param h integer
---@return graphics_frame ---@return graphics_frame
function graphics.gframe(x, y, w, h) function core.gframe(x, y, w, h)
return { return {
x = x, x = x,
y = y, y = y,
@@ -154,7 +79,7 @@ end
---@param a color ---@param a color
---@param b color ---@param b color
---@return cpair ---@return cpair
function graphics.cpair(a, b) function core.cpair(a, b)
return { return {
-- color pairs -- color pairs
color_a = a, color_a = a,
@@ -191,7 +116,7 @@ end
---@param thin? boolean true for 1 subpixel, false (default) for 2 ---@param thin? boolean true for 1 subpixel, false (default) for 2
---@param align_tr? boolean false to align bottom left (default), true to align top right ---@param align_tr? boolean false to align bottom left (default), true to align top right
---@return pipe ---@return pipe
function graphics.pipe(x1, y1, x2, y2, color, thin, align_tr) function core.pipe(x1, y1, x2, y2, color, thin, align_tr)
return { return {
x1 = x1, x1 = x1,
y1 = y1, y1 = y1,
@@ -205,6 +130,4 @@ function graphics.pipe(x1, y1, x2, y2, color, thin, align_tr)
} }
end end
core.graphics = graphics
return core return core

View File

@@ -12,15 +12,16 @@ local element = {}
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer next line if omitted ---@field y? integer next line if omitted
---@field offset_x? integer 0 if omitted
---@field offset_y? integer 0 if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
---@alias graphics_args graphics_args_generic ---@alias graphics_args graphics_args_generic
---|waiting_args ---|waiting_args
---|app_button_args
---|checkbox_args
---|hazard_button_args ---|hazard_button_args
---|multi_button_args ---|multi_button_args
---|push_button_args ---|push_button_args
@@ -28,6 +29,7 @@ local element = {}
---|sidebar_args ---|sidebar_args
---|spinbox_args ---|spinbox_args
---|switch_button_args ---|switch_button_args
---|tabbar_args
---|alarm_indicator_light ---|alarm_indicator_light
---|core_map_args ---|core_map_args
---|data_indicator_args ---|data_indicator_args
@@ -45,38 +47,50 @@ local element = {}
---|colormap_args ---|colormap_args
---|displaybox_args ---|displaybox_args
---|div_args ---|div_args
---|listbox_args
---|multipane_args ---|multipane_args
---|pipenet_args ---|pipenet_args
---|rectangle_args ---|rectangle_args
---|textbox_args ---|textbox_args
---|tiling_args ---|tiling_args
---@class element_subscription
---@field ps psil ps used
---@field key string data key
---@field func function callback
-- a base graphics element, should not be created on its own -- a base graphics element, should not be created on its own
---@nodiscard ---@nodiscard
---@param args graphics_args arguments ---@param args graphics_args arguments
function element.new(args) ---@param child_offset_x? integer mouse event offset x
---@param child_offset_y? integer mouse event offset y
function element.new(args, child_offset_x, child_offset_y)
local self = { local self = {
id = -1, id = nil, ---@type element_id|nil
elem_type = debug.getinfo(2).name, elem_type = debug.getinfo(2).name,
define_completed = false, define_completed = false,
p_window = nil, ---@type table p_window = nil, ---@type table
position = { x = 1, y = 1 }, position = { x = 1, y = 1 }, ---@type coordinate_2d
child_offset = { x = 0, y = 0 }, bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1}, next_y = 1, -- next child y coordinate
next_y = 1, next_id = 0, -- next child ID
children = {}, subscriptions = {},
mt = {} mt = {}
} }
---@class graphics_template ---@class graphics_base
local protected = { local protected = {
enabled = true, enabled = true,
value = nil, ---@type any value = nil, ---@type any
window = nil, ---@type table window = nil, ---@type table
fg_bg = core.graphics.cpair(colors.white, colors.black), content_window = nil, ---@type table|nil
frame = core.graphics.gframe(1, 1, 1, 1) fg_bg = core.cpair(colors.white, colors.black),
frame = core.gframe(1, 1, 1, 1),
children = {}
} }
local name_brief = "graphics.element{" .. self.elem_type .. "}: "
-- element as string -- element as string
function self.mt.__tostring() function self.mt.__tostring()
return "graphics.element{" .. self.elem_type .. "} @ " .. tostring(self) return "graphics.element{" .. self.elem_type .. "} @ " .. tostring(self)
@@ -92,8 +106,8 @@ function element.new(args)
------------------------- -------------------------
-- prepare the template -- prepare the template
---@param offset_x integer x offset ---@param offset_x integer x offset for mouse events
---@param offset_y integer y offset ---@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)
-- get frame coordinates/size -- get frame coordinates/size
@@ -105,46 +119,28 @@ function element.new(args)
else else
local w, h = self.p_window.getSize() local w, h = self.p_window.getSize()
protected.frame.x = args.x or 1 protected.frame.x = args.x or 1
protected.frame.y = args.y or next_y
if args.parent ~= nil then
protected.frame.y = args.y or (next_y - offset_y)
else
protected.frame.y = args.y or next_y
end
protected.frame.w = args.width or w protected.frame.w = args.width or w
protected.frame.h = args.height or h protected.frame.h = args.height or h
end end
-- inner offsets
if args.offset_x ~= nil then self.child_offset.x = args.offset_x end
if args.offset_y ~= nil then self.child_offset.y = args.offset_y end
-- adjust window frame if applicable -- adjust window frame if applicable
local f = protected.frame local f = protected.frame
local x = f.x
local y = f.y
-- apply offsets
if args.parent ~= nil then if args.parent ~= nil then
-- constrain to parent inner width/height -- constrain to parent inner width/height
local w, h = self.p_window.getSize() local w, h = self.p_window.getSize()
f.w = math.min(f.w, w - ((2 * offset_x) + (f.x - 1))) f.w = math.min(f.w, w - (f.x - 1))
f.h = math.min(f.h, h - ((2 * offset_y) + (f.y - 1))) f.h = math.min(f.h, h - (f.y - 1))
-- offset x/y
f.x = x + offset_x
f.y = y + offset_y
end end
-- check frame -- check frame
assert(f.x >= 1, "graphics.element{" .. self.elem_type .. "}: frame x not >= 1") assert(f.x >= 1, name_brief .. "frame x not >= 1")
assert(f.y >= 1, "graphics.element{" .. self.elem_type .. "}: frame y not >= 1") assert(f.y >= 1, name_brief .. "frame y not >= 1")
assert(f.w >= 1, "graphics.element{" .. self.elem_type .. "}: frame width not >= 1") assert(f.w >= 1, name_brief .. "frame width not >= 1")
assert(f.h >= 1, "graphics.element{" .. self.elem_type .. "}: frame height not >= 1") assert(f.h >= 1, name_brief .. "frame height not >= 1")
-- create window -- create window
protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, true) protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, args.hidden ~= true)
-- init colors -- init colors
if args.fg_bg ~= nil then if args.fg_bg ~= nil then
@@ -161,15 +157,49 @@ function element.new(args)
-- record position -- record position
self.position.x, self.position.y = protected.window.getPosition() self.position.x, self.position.y = protected.window.getPosition()
-- calculate bounds -- shift per parent child offset
self.position.x = self.position.x + offset_x
self.position.y = self.position.y + offset_y
-- calculate mouse event bounds
self.bounds.x1 = self.position.x self.bounds.x1 = self.position.x
self.bounds.x2 = self.position.x + f.w - 1 self.bounds.x2 = self.position.x + f.w - 1
self.bounds.y1 = self.position.y self.bounds.y1 = self.position.y
self.bounds.y2 = self.position.y + f.h - 1 self.bounds.y2 = self.position.y + f.h - 1
end end
-- check if a coordinate relative to the parent is within the bounds of this element
---@param x integer
---@param y integer
function protected.in_window_bounds(x, y)
local in_x = x >= self.bounds.x1 and x <= self.bounds.x2
local in_y = y >= self.bounds.y1 and y <= self.bounds.y2
return in_x and in_y
end
-- check if a coordinate relative to this window is within the bounds of this element
---@param x integer
---@param y integer
function protected.in_frame_bounds(x, y)
local in_x = x >= 1 and x <= protected.frame.w
local in_y = y >= 1 and y <= protected.frame.h
return in_x and in_y
end
-- luacheck: push ignore
---@diagnostic disable: unused-local, unused-vararg ---@diagnostic disable: unused-local, unused-vararg
-- handle a child element having been added
---@param id element_id element identifier
---@param child graphics_element child element
function protected.on_added(id, child)
end
-- handle a child element having been removed
---@param id element_id element identifier
function protected.on_removed(id)
end
-- handle a mouse event -- handle a mouse event
---@param event mouse_interaction mouse interaction event ---@param event mouse_interaction mouse interaction event
function protected.handle_mouse(event) function protected.handle_mouse(event)
@@ -224,6 +254,7 @@ function element.new(args)
function protected.resize(...) function protected.resize(...)
end end
-- luacheck: pop
---@diagnostic enable: unused-local, unused-vararg ---@diagnostic enable: unused-local, unused-vararg
-- start animations -- start animations
@@ -239,6 +270,14 @@ function element.new(args)
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
function protected.get() return public, self.id end function protected.get() return public, self.id end
-- report completion of element instantiation and get the public interface
---@nodiscard
---@return graphics_element element, element_id id
function protected.complete()
if args.parent ~= nil then args.parent.__child_ready(self.id, public) end
return public, self.id
end
----------- -----------
-- SETUP -- -- SETUP --
----------- -----------
@@ -250,10 +289,11 @@ function element.new(args)
end end
-- check window -- check window
assert(self.p_window, "graphics.element{" .. self.elem_type .. "}: no parent window provided") assert(self.p_window, name_brief .. "no parent window provided")
-- prepare the template -- prepare the template
if args.parent == nil then if args.parent == nil then
self.id = args.id or "__ROOT__"
protected.prepare_template(0, 0, 1) protected.prepare_template(0, 0, 1)
else else
self.id = args.parent.__add_child(args.id, protected) self.id = args.parent.__add_child(args.id, protected)
@@ -265,57 +305,109 @@ function element.new(args)
-- get the window object -- get the window object
---@nodiscard ---@nodiscard
function public.window() return protected.window end function public.window() return protected.content_window or protected.window end
-- CHILD ELEMENTS -- -- delete this element (hide and unsubscribe from PSIL)
function public.delete()
local fg_bg = protected.fg_bg
if args.parent ~= nil then
-- grab parent fg/bg so we can clear cleanly as a child element
fg_bg = args.parent.get_fg_bg()
end
-- clear, hide, and stop animations
protected.window.setBackgroundColor(fg_bg.bkg)
protected.window.setTextColor(fg_bg.fgd)
protected.window.clear()
public.hide()
-- unsubscribe from PSIL
for i = 1, #self.subscriptions do
local s = self.subscriptions[i] ---@type element_subscription
s.ps.unsubscribe(s.key, s.func)
end
-- delete all children
for k, v in pairs(protected.children) do
v.delete()
protected.children[k] = nil
end
if args.parent ~= nil then
-- remove self from parent
args.parent.__remove_child(self.id)
end
end
-- ELEMENT TREE --
-- add a child element -- add a child element
---@nodiscard ---@nodiscard
---@param key string|nil id ---@param key string|nil id
---@param child graphics_template ---@param child graphics_base
---@return integer|string key ---@return integer|string key
function public.__add_child(key, child) function public.__add_child(key, child)
-- offset first automatic placement child.prepare_template(child_offset_x or 0, child_offset_y or 0, self.next_y)
if self.next_y <= self.child_offset.y then
self.next_y = self.child_offset.y + 1
end
child.prepare_template(self.child_offset.x, self.child_offset.y, self.next_y)
self.next_y = child.frame.y + child.frame.h self.next_y = child.frame.y + child.frame.h
local child_element = child.get() local child_element = child.get()
if key == nil then local id = key ---@type string|integer|nil
table.insert(self.children, child_element) if id == nil then
return #self.children id = self.next_id
else self.next_id = self.next_id + 1
self.children[key] = child_element
return key
end end
protected.children[id] = child_element
return id
end
-- remove a child element
---@param key element_id id
function public.__remove_child(key)
if protected.children[key] ~= nil then
protected.on_removed(key)
protected.children[key] = nil
end
end
-- actions to take upon a child element becoming ready (initial draw/construction completed)
---@param key element_id id
---@param child graphics_element
function public.__child_ready(key, child)
protected.on_added(key, child)
end end
-- get a child element -- get a child element
---@nodiscard ---@nodiscard
---@param id element_id
---@return graphics_element ---@return graphics_element
function public.get_child(key) return self.children[key] end function public.get_child(id) return protected.children[id] end
-- remove child -- remove a child element
---@param key string|integer ---@param id element_id
function public.remove(key) self.children[key] = nil end function public.remove(id)
if protected.children[id] ~= nil then
protected.children[id].delete()
protected.on_removed(id)
protected.children[id] = nil
end
end
-- attempt to get a child element by ID (does not include this element itself) -- attempt to get a child element by ID (does not include this element itself)
---@nodiscard ---@nodiscard
---@param id element_id ---@param id element_id
---@return graphics_element|nil element ---@return graphics_element|nil element
function public.get_element_by_id(id) function public.get_element_by_id(id)
if self.children[id] == nil then if protected.children[id] == nil then
for _, child in pairs(self.children) do for _, child in pairs(protected.children) do
local elem = child.get_element_by_id(id) local elem = child.get_element_by_id(id)
if elem ~= nil then return elem end if elem ~= nil then return elem end
end end
else else
return self.children[id] return protected.children[id]
end end
return nil return nil
@@ -354,14 +446,14 @@ function element.new(args)
-- get element width -- get element width
---@nodiscard ---@nodiscard
---@return integer width ---@return integer width
function public.width() function public.get_width()
return protected.frame.w return protected.frame.w
end end
-- get element height -- get element height
---@nodiscard ---@nodiscard
---@return integer height ---@return integer height
function public.height() function public.get_height()
return protected.frame.h return protected.frame.h
end end
@@ -414,22 +506,31 @@ function element.new(args)
protected.resize(...) protected.resize(...)
end end
-- reposition the element window<br>
-- 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 y integer y position relative to parent frame
function public.reposition(x, y)
protected.window.reposition(x, y)
end
-- FUNCTION CALLBACKS -- -- FUNCTION CALLBACKS --
-- handle a monitor touch or mouse click -- handle a monitor touch or mouse click if this element is visible
---@param event mouse_interaction mouse interaction event ---@param event mouse_interaction mouse interaction event
function public.handle_mouse(event) function public.handle_mouse(event)
local in_x = event.x >= self.bounds.x1 and event.x <= self.bounds.x2 if protected.window.isVisible() then
local in_y = event.y >= self.bounds.y1 and event.y <= self.bounds.y2 local x_ini, y_ini = event.initial.x, event.initial.y
if in_x and in_y then local ini_in = protected.in_window_bounds(x_ini, y_ini)
local event_T = core.events.mouse_transposed(event, (event.x - self.position.x) + 1, (event.y - self.position.y) + 1)
-- handle the touch event, transformed into the window frame if ini_in then
protected.handle_mouse(event_T) local event_T = core.events.mouse_transposed(event, self.position.x, self.position.y)
-- pass on touch event to children -- handle the mouse event then pass to children
for _, val in pairs(self.children) do val.handle_mouse(event_T) end protected.handle_mouse(event_T)
for _, child in pairs(protected.children) do child.handle_mouse(event_T) end
end
end end
end end
@@ -445,27 +546,71 @@ function element.new(args)
protected.response_callback(result) protected.response_callback(result)
end end
-- VISIBILITY -- -- register a callback with a PSIL, allowing for automatic unregister on delete<br>
-- do not use graphics elements directly with PSIL subscribe()
-- show the element ---@param ps psil PSIL to subscribe to
function public.show() ---@param key string key to subscribe to
protected.window.setVisible(true) ---@param func function function to link
protected.start_anim() function public.register(ps, key, func)
for _, child in pairs(self.children) do child.show() end table.insert(self.subscriptions, { ps = ps, key = key, func = func })
ps.subscribe(key, func)
end end
-- hide the element -- VISIBILITY & ANIMATIONS --
-- show the element and enables animations by default
---@param animate? boolean true (default) to automatically resume animations
function public.show(animate)
protected.window.setVisible(true)
if animate ~= false then public.animate_all() end
end
-- hide the element and disables animations<br>
-- this alone does not cause an element to be fully hidden, it only prevents updates from being shown<br>
---@see graphics_element.content_redraw
function public.hide() function public.hide()
protected.stop_anim() public.freeze_all() -- stop animations for efficiency/performance
for _, child in pairs(self.children) do child.hide() end
protected.window.setVisible(false) protected.window.setVisible(false)
end end
-- start/resume animation(s)
function public.animate()
protected.start_anim()
end
-- start/resume animation(s) for this element and all its children<br>
-- only animates if a window is visible
function public.animate_all()
if protected.window.isVisible() then
public.animate()
for _, child in pairs(protected.children) do child.animate_all() end
end
end
-- freeze animation(s)
function public.freeze()
protected.stop_anim()
end
-- freeze animation(s) for this element and all its children
function public.freeze_all()
public.freeze()
for _, child in pairs(protected.children) do child.freeze_all() end
end
-- re-draw the element -- re-draw the element
function public.redraw() function public.redraw()
protected.window.redraw() protected.window.redraw()
end end
-- if a content window is set, clears it then re-draws all children
function public.content_redraw()
if protected.content_window ~= nil then
protected.content_window.clear()
for _, child in pairs(protected.children) do child.redraw() end
end
end
return protected return protected
end end

View File

@@ -1,6 +1,6 @@
-- Loading/Waiting Animation Graphics Element -- Loading/Waiting Animation Graphics Element
local tcd = require("scada-common.tcallbackdsp") local tcd = require("scada-common.tcd")
local element = require("graphics.element") local element = require("graphics.element")
@@ -8,8 +8,9 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new waiting animation element -- new waiting animation element
---@param args waiting_args ---@param args waiting_args
@@ -102,7 +103,7 @@ local function waiting(args)
e.start_anim() e.start_anim()
return e.get() return e.complete()
end end
return waiting return waiting

View File

@@ -8,7 +8,8 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field hidden? boolean true to hide on initial draw
-- new color map -- new color map
---@param args colormap_args ---@param args colormap_args
@@ -27,7 +28,7 @@ local function colormap(args)
e.window.setCursorPos(1, 1) e.window.setCursorPos(1, 1)
e.window.blit(spaces, bkg, bkg) e.window.blit(spaces, bkg, bkg)
return e.get() return e.complete()
end end
return colormap return colormap

View File

@@ -0,0 +1,130 @@
-- App Button Graphics Element
local tcd = require("scada-common.tcd")
local core = require("graphics.core")
local element = require("graphics.element")
local CLICK_TYPE = core.events.CLICK_TYPE
---@class app_button_args
---@field text string app icon text
---@field title string app title text
---@field callback function function to call on touch
---@field app_fg_bg cpair app icon foreground/background colors
---@field active_fg_bg? cpair foreground/background colors when pressed
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new app button
---@param args app_button_args
---@return graphics_element element, element_id id
local function app_button(args)
assert(type(args.text) == "string", "graphics.elements.controls.app: text is a required field")
assert(type(args.title) == "string", "graphics.elements.controls.app: title is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.app: callback is a required field")
assert(type(args.app_fg_bg) == "table", "graphics.elements.controls.app: app_fg_bg is a required field")
args.height = 4
args.width = 5
-- create new graphics element base object
local e = element.new(args)
-- write app title, centered
e.window.setCursorPos(1, 4)
e.window.setCursorPos(math.floor((e.frame.w - string.len(args.title)) / 2) + 1, 4)
e.window.write(args.title)
-- draw the app button
local function draw()
local fgd = args.app_fg_bg.fgd
local bkg = args.app_fg_bg.bkg
if e.value then
fgd = args.active_fg_bg.fgd
bkg = args.active_fg_bg.bkg
end
-- draw icon
e.window.setCursorPos(1, 1)
e.window.setTextColor(fgd)
e.window.setBackgroundColor(bkg)
e.window.write("\x9f\x83\x83\x83")
e.window.setTextColor(bkg)
e.window.setBackgroundColor(fgd)
e.window.write("\x90")
e.window.setTextColor(fgd)
e.window.setBackgroundColor(bkg)
e.window.setCursorPos(1, 2)
e.window.write("\x95 ")
e.window.setTextColor(bkg)
e.window.setBackgroundColor(fgd)
e.window.write("\x95")
e.window.setCursorPos(1, 3)
e.window.write("\x82\x8f\x8f\x8f\x81")
-- write the icon text
e.window.setCursorPos(3, 2)
e.window.setTextColor(fgd)
e.window.setBackgroundColor(bkg)
e.window.write(args.text)
end
-- draw the app button as pressed (if active_fg_bg set)
local function show_pressed()
if e.enabled and args.active_fg_bg ~= nil then
e.value = true
e.window.setTextColor(args.active_fg_bg.fgd)
e.window.setBackgroundColor(args.active_fg_bg.bkg)
draw()
end
end
-- draw the app button as unpressed (if active_fg_bg set)
local function show_unpressed()
if e.enabled and args.active_fg_bg ~= nil then
e.value = false
e.window.setTextColor(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg)
draw()
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled then
if event.type == CLICK_TYPE.TAP then
show_pressed()
-- show as unpressed in 0.25 seconds
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
args.callback()
elseif event.type == CLICK_TYPE.DOWN then
show_pressed()
elseif event.type == CLICK_TYPE.UP then
show_unpressed()
if e.in_frame_bounds(event.current.x, event.current.y) then
args.callback()
end
end
end
end
-- set the value (true simulates pressing the app button)
---@param val boolean new value
function e.set_value(val)
if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
end
-- initial draw
draw()
return e.complete()
end
return app_button

View File

@@ -0,0 +1,85 @@
-- Checkbox Graphics Element
local core = require("graphics.core")
local element = require("graphics.element")
---@class checkbox_args
---@field label string checkbox text
---@field box_fg_bg cpair colors for checkbox
---@field callback function function to call on press
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new checkbox control
---@param args checkbox_args
---@return graphics_element element, element_id id
local function checkbox(args)
assert(type(args.label) == "string", "graphics.elements.controls.checkbox: label is a required field")
assert(type(args.box_fg_bg) == "table", "graphics.elements.controls.checkbox: box_fg_bg is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.checkbox: callback is a required field")
args.height = 1
args.width = 3 + string.len(args.label)
-- create new graphics element base object
local e = element.new(args)
e.value = false
-- show the button state
local function draw()
e.window.setCursorPos(1, 1)
if e.value then
-- show as selected
e.window.setTextColor(args.box_fg_bg.bkg)
e.window.setBackgroundColor(args.box_fg_bg.fgd)
e.window.write("\x88")
e.window.setTextColor(args.box_fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg)
e.window.write("\x95")
else
-- show as unselected
e.window.setTextColor(e.fg_bg.bkg)
e.window.setBackgroundColor(args.box_fg_bg.bkg)
e.window.write("\x88")
e.window.setTextColor(args.box_fg_bg.bkg)
e.window.setBackgroundColor(e.fg_bg.bkg)
e.window.write("\x95")
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled and core.events.was_clicked(event.type) then
e.value = not e.value
draw()
args.callback(e.value)
end
end
-- set the value
---@param val integer new value
function e.set_value(val)
e.value = val
draw()
end
-- write label text
e.window.setCursorPos(3, 1)
e.window.setTextColor(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg)
e.window.write(args.label)
-- initial draw
draw()
return e.complete()
end
return checkbox

View File

@@ -1,6 +1,6 @@
-- Hazard-bordered Button Graphics Element -- Hazard-bordered Button Graphics Element
local tcd = require("scada-common.tcallbackdsp") local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
@@ -14,8 +14,9 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new hazard button -- new hazard button
---@param args hazard_button_args ---@param args hazard_button_args
@@ -142,24 +143,25 @@ local function hazard_button(args)
-- handle mouse interaction -- handle mouse interaction
---@param event mouse_interaction mouse event ---@param event mouse_interaction mouse event
---@diagnostic disable-next-line: unused-local
function e.handle_mouse(event) function e.handle_mouse(event)
if e.enabled then if e.enabled then
-- change text color to indicate clicked if core.events.was_clicked(event.type) then
e.window.setTextColor(args.accent) -- change text color to indicate clicked
e.window.setCursorPos(3, 2) e.window.setTextColor(args.accent)
e.window.write(args.text) e.window.setCursorPos(3, 2)
e.window.write(args.text)
-- abort any other callbacks -- abort any other callbacks
tcd.abort(on_timeout) tcd.abort(on_timeout)
tcd.abort(on_success) tcd.abort(on_success)
tcd.abort(on_failure) tcd.abort(on_failure)
-- 1.5 second timeout -- 1.5 second timeout
tcd.dispatch(1.5, on_timeout) tcd.dispatch(1.5, on_timeout)
-- call the touch callback -- call the touch callback
args.callback() args.callback()
end
end end
end end
@@ -167,18 +169,13 @@ local function hazard_button(args)
---@param result boolean true for success, false for failure ---@param result boolean true for success, false for failure
function e.response_callback(result) function e.response_callback(result)
tcd.abort(on_timeout) tcd.abort(on_timeout)
if result then on_success() else on_failure(0) end
if result then
on_success()
else
on_failure(0)
end
end end
-- set the value (true simulates pressing the button) -- set the value (true simulates pressing the button)
---@param val boolean new value ---@param val boolean new value
function e.set_value(val) function e.set_value(val)
if val then e.handle_mouse(core.events.mouse_generic("", core.events.click_type.VIRTUAL, 1, 1)) end if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
end end
-- show the button as disabled -- show the button as disabled
@@ -202,7 +199,7 @@ local function hazard_button(args)
-- initial draw of border -- initial draw of border
draw_border(args.accent) draw_border(args.accent)
return e.get() return e.complete()
end end
return hazard_button return hazard_button

View File

@@ -2,13 +2,13 @@
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
---@class button_option ---@class button_option
---@field text string ---@field text string
---@field fg_bg cpair ---@field fg_bg cpair
---@field active_fg_bg cpair ---@field active_fg_bg cpair
---@field _lpad integer automatically calculated left pad
---@field _start_x integer starting touch x range (inclusive) ---@field _start_x integer starting touch x range (inclusive)
---@field _end_x integer ending touch x range (inclusive) ---@field _end_x integer ending touch x range (inclusive)
@@ -20,9 +20,10 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new multi button (latch selection, exclusively one button at a time) -- new multi button (latch selection, exclusively one button at a time)
---@param args multi_button_args ---@param args multi_button_args
@@ -62,9 +63,7 @@ local function multi_button(args)
local next_x = 2 local next_x = 2
for i = 1, #args.options do for i = 1, #args.options do
local opt = args.options[i] ---@type button_option local opt = args.options[i] ---@type button_option
local w = string.len(opt.text)
opt._lpad = math.floor((e.frame.w - w) / 2)
opt._start_x = next_x opt._start_x = next_x
opt._end_x = next_x + button_width - 1 opt._end_x = next_x + button_width - 1
@@ -92,20 +91,32 @@ local function multi_button(args)
end end
end end
-- check which button a given x is within
---@return integer|nil button index or nil if not within a button
local function which_button(x)
for i = 1, #args.options do
local opt = args.options[i] ---@type button_option
if x >= opt._start_x and x <= opt._end_x then return i end
end
return nil
end
-- handle mouse interaction -- handle mouse interaction
---@param event mouse_interaction mouse event ---@param event mouse_interaction mouse event
---@diagnostic disable-next-line: unused-local
function e.handle_mouse(event) function e.handle_mouse(event)
-- determine what was pressed -- if enabled and the button row was pressed...
if e.enabled and event.y == 1 then if e.enabled and core.events.was_clicked(event.type) then
for i = 1, #args.options do -- a button may have been pressed, which one was it?
local opt = args.options[i] ---@type button_option local button_ini = which_button(event.initial.x)
local button_cur = which_button(event.current.x)
if event.x >= opt._start_x and event.x <= opt._end_x then -- mouse up must always have started with a mouse down on the same button to count as a click
e.value = i -- tap always has identical coordinates, so this always passes for taps
draw() if button_ini == button_cur and button_cur ~= nil then
args.callback(e.value) e.value = button_cur
end draw()
args.callback(e.value)
end end
end end
end end
@@ -120,7 +131,7 @@ local function multi_button(args)
-- initial draw -- initial draw
draw() draw()
return e.get() return e.complete()
end end
return multi_button return multi_button

View File

@@ -1,10 +1,12 @@
-- Button Graphics Element -- Button Graphics Element
local tcd = require("scada-common.tcallbackdsp") 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 CLICK_TYPE = core.events.CLICK_TYPE
---@class push_button_args ---@class push_button_args
---@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
@@ -14,9 +16,10 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new push button -- new push button
---@param args push_button_args ---@param args push_button_args
@@ -24,6 +27,8 @@ local element = require("graphics.element")
local function push_button(args) local function push_button(args)
assert(type(args.text) == "string", "graphics.elements.controls.push_button: text is a required field") assert(type(args.text) == "string", "graphics.elements.controls.push_button: text is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.push_button: callback is a required field") assert(type(args.callback) == "function", "graphics.elements.controls.push_button: callback is a required field")
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
"graphics.elements.controls.push_button: min_width must be nil or a number > 0")
local text_width = string.len(args.text) local text_width = string.len(args.text)
@@ -47,38 +52,50 @@ local function push_button(args)
e.window.write(args.text) e.window.write(args.text)
end end
-- draw the button as pressed (if active_fg_bg set)
local function show_pressed()
if e.enabled and args.active_fg_bg ~= nil then
e.value = true
e.window.setTextColor(args.active_fg_bg.fgd)
e.window.setBackgroundColor(args.active_fg_bg.bkg)
draw()
end
end
-- draw the button as unpressed (if active_fg_bg set)
local function show_unpressed()
if e.enabled and args.active_fg_bg ~= nil then
e.value = false
e.window.setTextColor(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg)
draw()
end
end
-- handle mouse interaction -- handle mouse interaction
---@param event mouse_interaction mouse event ---@param event mouse_interaction mouse event
---@diagnostic disable-next-line: unused-local
function e.handle_mouse(event) function e.handle_mouse(event)
if e.enabled then if e.enabled then
if args.active_fg_bg ~= nil then if event.type == CLICK_TYPE.TAP then
-- show as pressed show_pressed()
e.value = true
e.window.setTextColor(args.active_fg_bg.fgd)
e.window.setBackgroundColor(args.active_fg_bg.bkg)
draw()
-- show as unpressed in 0.25 seconds -- show as unpressed in 0.25 seconds
tcd.dispatch(0.25, function () if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
e.value = false args.callback()
if e.enabled then elseif event.type == CLICK_TYPE.DOWN then
e.window.setTextColor(e.fg_bg.fgd) show_pressed()
e.window.setBackgroundColor(e.fg_bg.bkg) elseif event.type == CLICK_TYPE.UP then
end show_unpressed()
draw() if e.in_frame_bounds(event.current.x, event.current.y) then
end) args.callback()
end
end end
-- call the touch callback
args.callback()
end end
end end
-- set the value (true simulates pressing the button) -- set the value (true simulates pressing the button)
---@param val boolean new value ---@param val boolean new value
function e.set_value(val) function e.set_value(val)
if val then e.handle_mouse(core.events.mouse_generic("", core.events.click_type.VIRTUAL, 1, 1)) end if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
end end
-- show butten as enabled -- show butten as enabled
@@ -104,7 +121,7 @@ local function push_button(args)
-- initial draw -- initial draw
draw() draw()
return e.get() return e.complete()
end end
return push_button return push_button

View File

@@ -1,5 +1,6 @@
-- Radio Button Graphics Element -- Radio Button Graphics Element
local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
---@class radio_button_args ---@class radio_button_args
@@ -12,8 +13,9 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new radio button list (latch selection, exclusively one button at a time) -- new radio button list (latch selection, exclusively one button at a time)
---@param args radio_button_args ---@param args radio_button_args
@@ -82,10 +84,10 @@ local function radio_button(args)
-- 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)
-- determine what was pressed if e.enabled and core.events.was_clicked(event.type) and (event.initial.y == event.current.y) then
if e.enabled then -- determine what was pressed
if args.options[event.y] ~= nil then if args.options[event.current.y] ~= nil then
e.value = event.y e.value = event.current.y
draw() draw()
args.callback(e.value) args.callback(e.value)
end end
@@ -102,7 +104,7 @@ local function radio_button(args)
-- initial draw -- initial draw
draw() draw()
return e.get() return e.complete()
end end
return radio_button return radio_button

View File

@@ -1,9 +1,12 @@
-- Sidebar Graphics Element -- Sidebar Graphics Element
local tcd = require("scada-common.tcallbackdsp") local tcd = require("scada-common.tcd")
local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
local CLICK_TYPE = core.events.CLICK_TYPE
---@class sidebar_tab ---@class sidebar_tab
---@field char string character identifier ---@field char string character identifier
---@field color cpair tab colors (fg/bg) ---@field color cpair tab colors (fg/bg)
@@ -14,9 +17,10 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new sidebar tab selector -- new sidebar tab selector
---@param args sidebar_args ---@param args sidebar_args
@@ -39,7 +43,10 @@ local function sidebar(args)
-- show the button state -- show the button state
---@param pressed boolean if the currently selected tab should appear as actively pressed ---@param pressed boolean if the currently selected tab should appear as actively pressed
local function draw(pressed) ---@param pressed_idx? integer optional index to show as held (that is not yet selected)
local function draw(pressed, pressed_idx)
pressed_idx = pressed_idx or e.value
for i = 1, #args.tabs do for i = 1, #args.tabs do
local tab = args.tabs[i] ---@type sidebar_tab local tab = args.tabs[i] ---@type sidebar_tab
@@ -47,7 +54,7 @@ local function sidebar(args)
e.window.setCursorPos(1, y) e.window.setCursorPos(1, y)
if pressed and e.value == i then if pressed and i == pressed_idx then
e.window.setTextColor(e.fg_bg.fgd) e.window.setTextColor(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg) e.window.setBackgroundColor(e.fg_bg.bkg)
else else
@@ -74,16 +81,27 @@ local function sidebar(args)
function e.handle_mouse(event) function e.handle_mouse(event)
-- determine what was pressed -- determine what was pressed
if e.enabled then if e.enabled then
local idx = math.ceil(event.y / 3) local cur_idx = math.ceil(event.current.y / 3)
local ini_idx = math.ceil(event.initial.y / 3)
if args.tabs[idx] ~= nil then if args.tabs[cur_idx] ~= nil then
e.value = idx if event.type == CLICK_TYPE.TAP then
draw(true) e.value = cur_idx
draw(true)
-- show as unpressed in 0.25 seconds -- show as unpressed in 0.25 seconds
tcd.dispatch(0.25, function () draw(false) end) tcd.dispatch(0.25, function () draw(false) end)
args.callback(e.value)
args.callback(e.value) elseif event.type == CLICK_TYPE.DOWN then
draw(true, cur_idx)
elseif event.type == CLICK_TYPE.UP then
if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then
e.value = cur_idx
draw(false)
args.callback(e.value)
else draw(false) end
end
elseif event.type == CLICK_TYPE.UP then
draw(false)
end end
end end
end end
@@ -98,7 +116,7 @@ local function sidebar(args)
-- initial draw -- initial draw
draw(false) draw(false)
return e.get() return e.complete()
end end
return sidebar return sidebar

View File

@@ -2,6 +2,7 @@
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
---@class spinbox_args ---@class spinbox_args
@@ -15,8 +16,9 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new spinbox control (minimum value is 0) -- new spinbox control (minimum value is 0)
---@param args spinbox_args ---@param args spinbox_args
@@ -130,19 +132,22 @@ local function spinbox(args)
---@param event mouse_interaction mouse event ---@param event mouse_interaction mouse event
function e.handle_mouse(event) function e.handle_mouse(event)
-- only handle if on an increment or decrement arrow -- only handle if on an increment or decrement arrow
if e.enabled and event.x ~= dec_point_x then if e.enabled and core.events.was_clicked(event.type) and
local idx = util.trinary(event.x > dec_point_x, event.x - 1, event.x) (event.current.x ~= dec_point_x) and (event.current.y ~= 2) then
if digits[idx] ~= nil then if event.current.x == event.initial.x and event.current.y == event.initial.y then
if event.y == 1 then local idx = util.trinary(event.current.x > dec_point_x, event.current.x - 1, event.current.x)
-- increment if digits[idx] ~= nil then
digits[idx] = digits[idx] + 1 if event.current.y == 1 then
elseif event.y == 3 then -- increment
-- decrement digits[idx] = digits[idx] + 1
digits[idx] = digits[idx] - 1 elseif event.current.y == 3 then
end -- decrement
digits[idx] = digits[idx] - 1
end
update_value() update_value()
show_num() show_num()
end
end end
end end
end end
@@ -184,7 +189,7 @@ local function spinbox(args)
e.value = 0 e.value = 0
set_digits() set_digits()
return e.get() return e.complete()
end end
return spinbox return spinbox

View File

@@ -1,5 +1,6 @@
-- Button Graphics Element -- Button Graphics Element
local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
---@class switch_button_args ---@class switch_button_args
@@ -11,9 +12,10 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new switch button (latch high/low) -- new switch button (latch high/low)
---@param args switch_button_args ---@param args switch_button_args
@@ -22,13 +24,15 @@ local function switch_button(args)
assert(type(args.text) == "string", "graphics.elements.controls.switch_button: text is a required field") assert(type(args.text) == "string", "graphics.elements.controls.switch_button: text is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.switch_button: callback is a required field") assert(type(args.callback) == "function", "graphics.elements.controls.switch_button: callback is a required field")
assert(type(args.active_fg_bg) == "table", "graphics.elements.controls.switch_button: active_fg_bg is a required field") assert(type(args.active_fg_bg) == "table", "graphics.elements.controls.switch_button: active_fg_bg is a required field")
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
"graphics.elements.controls.switch_button: min_width must be nil or a number > 0")
-- single line
args.height = 1
-- determine widths
local text_width = string.len(args.text) local text_width = string.len(args.text)
args.width = math.max(text_width + 2, args.min_width)
-- single line height, calculate width
args.height = 1
args.min_width = args.min_width or 0
args.width = math.max(text_width, args.min_width)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
@@ -64,9 +68,8 @@ local function switch_button(args)
-- handle mouse interaction -- handle mouse interaction
---@param event mouse_interaction mouse event ---@param event mouse_interaction mouse event
---@diagnostic disable-next-line: unused-local
function e.handle_mouse(event) function e.handle_mouse(event)
if e.enabled then if e.enabled and core.events.was_clicked(event.type) then
-- toggle state -- toggle state
e.value = not e.value e.value = not e.value
draw_state() draw_state()
@@ -84,7 +87,7 @@ local function switch_button(args)
draw_state() draw_state()
end end
return e.get() return e.complete()
end end
return switch_button return switch_button

View File

@@ -0,0 +1,131 @@
-- Tab Bar Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
---@class tabbar_tab
---@field name string tab name
---@field color cpair tab colors (fg/bg)
---@field _start_x integer starting touch x range (inclusive)
---@field _end_x integer ending touch x range (inclusive)
---@class tabbar_args
---@field tabs table tab options
---@field callback function function to call on tab change
---@field min_width? integer text length + 2 if omitted
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new tab selector
---@param args tabbar_args
---@return graphics_element element, element_id id
local function tabbar(args)
assert(type(args.tabs) == "table", "graphics.elements.controls.tabbar: tabs is a required field")
assert(#args.tabs > 0, "graphics.elements.controls.tabbar: at least one tab is required")
assert(type(args.callback) == "function", "graphics.elements.controls.tabbar: callback is a required field")
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
"graphics.elements.controls.tabbar: min_width must be nil or a number > 0")
-- always 1 tall
args.height = 1
-- determine widths
local max_width = 1
for i = 1, #args.tabs do
local opt = args.tabs[i] ---@type tabbar_tab
if string.len(opt.name) > max_width then
max_width = string.len(opt.name)
end
end
local button_width = math.max(max_width, args.min_width or 0)
-- create new graphics element base object
local e = element.new(args)
assert(e.frame.w >= (button_width * #args.tabs), "graphics.elements.controls.tabbar: width insufficent to display all tabs")
-- default to 1st tab
e.value = 1
-- calculate required tab dimension information
local next_x = 1
for i = 1, #args.tabs do
local tab = args.tabs[i] ---@type tabbar_tab
tab._start_x = next_x
tab._end_x = next_x + button_width - 1
next_x = next_x + button_width
end
-- show the tab state
local function draw()
for i = 1, #args.tabs do
local tab = args.tabs[i] ---@type tabbar_tab
e.window.setCursorPos(tab._start_x, 1)
if e.value == i then
e.window.setTextColor(tab.color.fgd)
e.window.setBackgroundColor(tab.color.bkg)
else
e.window.setTextColor(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg)
end
e.window.write(util.pad(tab.name, button_width))
end
end
-- check which tab a given x is within
---@return integer|nil button index or nil if not within a tab
local function which_tab(x)
for i = 1, #args.tabs do
local tab = args.tabs[i] ---@type tabbar_tab
if x >= tab._start_x and x <= tab._end_x then return i end
end
return nil
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
-- determine what was pressed
if e.enabled and core.events.was_clicked(event.type) then
-- a button may have been pressed, which one was it?
local tab_ini = which_tab(event.initial.x)
local tab_cur = which_tab(event.current.x)
-- mouse up must always have started with a mouse down on the same tab to count as a click
-- tap always has identical coordinates, so this always passes for taps
if tab_ini == tab_cur and tab_cur ~= nil then
e.value = tab_cur
draw()
args.callback(e.value)
end
end
end
-- set the value
---@param val integer new value
function e.set_value(val)
e.value = val
draw()
end
-- initial draw
draw()
return e.complete()
end
return tabbar

View File

@@ -4,19 +4,22 @@ local element = require("graphics.element")
---@class displaybox_args ---@class displaybox_args
---@field window table ---@field window table
---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new root display box -- new root display box
---@nodiscard ---@nodiscard
---@param args displaybox_args ---@param args displaybox_args
---@return graphics_element element, element_id id
local function displaybox(args) local function displaybox(args)
-- create new graphics element base object -- create new graphics element base object
return element.new(args).get() return element.new(args).complete()
end end
return displaybox return displaybox

View File

@@ -6,11 +6,12 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new div element -- new div element
---@nodiscard ---@nodiscard
@@ -18,7 +19,7 @@ local element = require("graphics.element")
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function div(args) local function div(args)
-- create new graphics element base object -- create new graphics element base object
return element.new(args).get() return element.new(args).complete()
end end
return div return div

View File

@@ -16,8 +16,9 @@ local flasher = require("graphics.flasher")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new alarm indicator light -- new alarm indicator light
---@nodiscard ---@nodiscard
@@ -108,7 +109,7 @@ local function alarm_indicator_light(args)
e.on_update(1) e.on_update(1)
e.window.write(args.label) e.window.write(args.label)
return e.get() return e.complete()
end end
return alarm_indicator_light return alarm_indicator_light

View File

@@ -11,7 +11,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
-- new core map box -- new core map box
---@nodiscard ---@nodiscard
@@ -26,7 +26,7 @@ local function core_map(args)
args.height = 18 args.height = 18
-- inherit only foreground color -- inherit only foreground color
args.fg_bg = core.graphics.cpair(args.parent.get_fg_bg().fgd, colors.gray) args.fg_bg = core.cpair(args.parent.get_fg_bg().fgd, colors.gray)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
@@ -163,7 +163,7 @@ local function core_map(args)
-- initial draw -- initial draw
e.on_update(0) e.on_update(0)
return e.get() return e.complete()
end end
return core_map return core_map

View File

@@ -14,9 +14,10 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width integer length ---@field width integer length
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new data indicator -- new data indicator
---@nodiscard ---@nodiscard
@@ -43,8 +44,9 @@ local function data(args)
e.window.setCursorPos(1, 1) e.window.setCursorPos(1, 1)
e.window.write(args.label) e.window.write(args.label)
local label_len = string.len(args.label) local value_color = e.fg_bg.fgd
local data_start = 1 local label_len = string.len(args.label)
local data_start = 1
local clear_width = args.width local clear_width = args.width
if label_len > 0 then if label_len > 0 then
@@ -64,7 +66,7 @@ local function data(args)
-- write data -- write data
local data_str = util.sprintf(args.format, value) local data_str = util.sprintf(args.format, value)
e.window.setCursorPos(data_start, 1) e.window.setCursorPos(data_start, 1)
e.window.setTextColor(e.fg_bg.fgd) e.window.setTextColor(value_color)
if args.commas then if args.commas then
e.window.write(util.comma_format(data_str)) e.window.write(util.comma_format(data_str))
else else
@@ -84,10 +86,17 @@ local function data(args)
---@param val any new value ---@param val any new value
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- change the foreground color of the value, or all text if no label/unit colors provided
---@param c color
function e.recolor(c)
value_color = c
e.on_update(e.value)
end
-- initial value draw -- initial value draw
e.on_update(args.value) e.on_update(args.value)
return e.get() return e.complete()
end end
return data return data

View File

@@ -10,11 +10,12 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new horizontal bar -- new horizontal bar
---@nodiscard ---@nodiscard
@@ -119,7 +120,7 @@ local function hbar(args)
-- initialize to 0 -- initialize to 0
e.on_update(0) e.on_update(0)
return e.get() return e.complete()
end end
return hbar return hbar

View File

@@ -16,8 +16,9 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new icon indicator -- new icon indicator
---@nodiscard ---@nodiscard
@@ -68,7 +69,7 @@ local function icon(args)
-- initial icon draw -- initial icon draw
e.on_update(args.value or 1) e.on_update(args.value or 1)
return e.get() return e.complete()
end end
return icon return icon

View File

@@ -14,8 +14,9 @@ local flasher = require("graphics.flasher")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new indicator LED -- new indicator LED
---@nodiscard ---@nodiscard
@@ -94,7 +95,7 @@ local function indicator_led(args)
e.window.write(args.label) e.window.write(args.label)
end end
return e.get() return e.complete()
end end
return indicator_led return indicator_led

View File

@@ -16,8 +16,9 @@ local flasher = require("graphics.flasher")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new dual LED indicator light -- new dual LED indicator light
---@nodiscard ---@nodiscard
@@ -108,7 +109,7 @@ local function indicator_led_pair(args)
e.window.write(args.label) e.window.write(args.label)
end end
return e.get() return e.complete()
end end
return indicator_led_pair return indicator_led_pair

View File

@@ -9,8 +9,9 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new RGB LED indicator light -- new RGB LED indicator light
---@nodiscard ---@nodiscard
@@ -53,7 +54,7 @@ local function indicator_led_rgb(args)
e.window.write(args.label) e.window.write(args.label)
end end
return e.get() return e.complete()
end end
return indicator_led_rgb return indicator_led_rgb

View File

@@ -14,8 +14,9 @@ local flasher = require("graphics.flasher")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new indicator light -- new indicator light
---@nodiscard ---@nodiscard
@@ -92,7 +93,7 @@ local function indicator_light(args)
e.window.setCursorPos(3, 1) e.window.setCursorPos(3, 1)
e.window.write(args.label) e.window.write(args.label)
return e.get() return e.complete()
end end
return indicator_light return indicator_light

View File

@@ -13,9 +13,10 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width integer length ---@field width integer length
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new power indicator -- new power indicator
---@nodiscard ---@nodiscard
@@ -79,7 +80,7 @@ local function power(args)
-- initial value draw -- initial value draw
e.on_update(args.value) e.on_update(args.value)
return e.get() return e.complete()
end end
return power return power

View File

@@ -14,9 +14,10 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width integer length ---@field width integer length
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new radiation indicator -- new radiation indicator
---@nodiscard ---@nodiscard
@@ -84,7 +85,7 @@ local function rad(args)
-- initial value draw -- initial value draw
e.on_update(types.new_zero_radiation_reading()) e.on_update(types.new_zero_radiation_reading())
return e.get() return e.complete()
end end
return rad return rad

View File

@@ -15,9 +15,10 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field height? integer 1 if omitted, must be an odd number ---@field height? integer 1 if omitted, must be an odd number
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new state indicator -- new state indicator
---@nodiscard ---@nodiscard
@@ -74,7 +75,7 @@ local function state_indicator(args)
-- initial draw -- initial draw
e.on_update(args.value or 1) e.on_update(args.value or 1)
return e.get() return e.complete()
end end
return state_indicator return state_indicator

View File

@@ -16,8 +16,9 @@ local flasher = require("graphics.flasher")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new tri-state indicator light -- new tri-state indicator light
---@nodiscard ---@nodiscard
@@ -105,7 +106,7 @@ local function tristate_indicator_light(args)
e.on_update(1) e.on_update(1)
e.window.write(args.label) e.window.write(args.label)
return e.get() return e.complete()
end end
return tristate_indicator_light return tristate_indicator_light

View File

@@ -8,11 +8,12 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new vertical bar -- new vertical bar
---@nodiscard ---@nodiscard
@@ -99,7 +100,7 @@ local function vbar(args)
---@param val number 0.0 to 1.0 ---@param val number 0.0 to 1.0
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
return e.get() return e.complete()
end end
return vbar return vbar

View File

@@ -0,0 +1,283 @@
-- Scroll-able List Box Display Graphics Element
local tcd = require("scada-common.tcd")
local core = require("graphics.core")
local element = require("graphics.element")
local CLICK_TYPE = core.events.CLICK_TYPE
---@class listbox_args
---@field scroll_height integer height of internal scrolling container (must fit all elements vertically tiled)
---@field item_pad? integer spacing (lines) between items in the list (default 0)
---@field nav_fg_bg? cpair foreground/background colors for scroll arrows and bar area
---@field nav_active? cpair active colors for bar held down or arrow held down
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
---@class listbox_item
---@field id string|integer element ID
---@field e graphics_element element
---@field y integer y position
---@field h integer element height
-- new listbox element
---@nodiscard
---@param args listbox_args
---@return graphics_element element, element_id id
local function listbox(args)
-- create new graphics element base object
local e = element.new(args)
-- create content window for child elements
local scroll_frame = window.create(e.window, 1, 1, e.frame.w - 1, args.scroll_height, false)
e.content_window = scroll_frame
-- item list and scroll management
local list = {}
local item_pad = args.item_pad or 0
local scroll_offset = 0
local content_height = 0
local max_down_scroll = 0
-- bar control/tracking variables
local max_bar_height = e.frame.h - 2
local bar_height = 0 -- full height of bar
local bar_bounds = { 0, 0 } -- top and bottom of bar
local bar_is_scaled = false -- if the scrollbar doesn't have a 1:1 ratio with lines
local holding_bar = false -- bar is being held by mouse
local bar_grip_pos = 0 -- where the bar was gripped by mouse down
local mouse_last_y = 0 -- last reported y coordinate of drag
-- draw scroll bar arrows, optionally showing one of them as pressed
---@param pressed_arrow? 1|0|-1 arrow to show as pressed (1 = scroll up, 0 = neither, -1 = scroll down)
local function draw_arrows(pressed_arrow)
local nav_fg_bg = args.nav_fg_bg or e.fg_bg
local active_fg_bg = args.nav_active or nav_fg_bg
-- draw up/down arrows
if pressed_arrow == 1 then
e.window.setTextColor(active_fg_bg.fgd)
e.window.setBackgroundColor(active_fg_bg.bkg)
e.window.setCursorPos(e.frame.w, 1)
e.window.write("\x1e")
e.window.setTextColor(nav_fg_bg.fgd)
e.window.setBackgroundColor(nav_fg_bg.bkg)
e.window.setCursorPos(e.frame.w, e.frame.h)
e.window.write("\x1f")
elseif pressed_arrow == -1 then
e.window.setTextColor(nav_fg_bg.fgd)
e.window.setBackgroundColor(nav_fg_bg.bkg)
e.window.setCursorPos(e.frame.w, 1)
e.window.write("\x1e")
e.window.setTextColor(active_fg_bg.fgd)
e.window.setBackgroundColor(active_fg_bg.bkg)
e.window.setCursorPos(e.frame.w, e.frame.h)
e.window.write("\x1f")
else
e.window.setTextColor(nav_fg_bg.fgd)
e.window.setBackgroundColor(nav_fg_bg.bkg)
e.window.setCursorPos(e.frame.w, 1)
e.window.write("\x1e")
e.window.setCursorPos(e.frame.w, e.frame.h)
e.window.write("\x1f")
end
e.window.setTextColor(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg)
end
-- render the scroll bar and re-cacluate height & bounds
local function draw_bar()
local offset = 2 + math.abs(scroll_offset)
bar_height = math.min(max_bar_height + max_down_scroll, max_bar_height)
if bar_height < 1 then
bar_is_scaled = true
-- can't do a 1:1 ratio
-- use minimum size bar with scaled offset
local scroll_progress = scroll_offset / max_down_scroll
offset = 2 + math.floor(scroll_progress * (max_bar_height - 1))
bar_height = 1
else
bar_is_scaled = false
end
bar_bounds = { offset, (bar_height + offset) - 1 }
for i = 2, e.frame.h - 1 do
if (i >= offset and i < (bar_height + offset)) and (bar_height ~= max_bar_height) then
if args.nav_fg_bg ~= nil then
e.window.setBackgroundColor(args.nav_fg_bg.fgd)
else
e.window.setBackgroundColor(e.fg_bg.fgd)
end
else
if args.nav_fg_bg ~= nil then
e.window.setBackgroundColor(args.nav_fg_bg.bkg)
else
e.window.setBackgroundColor(e.fg_bg.bkg)
end
end
e.window.setCursorPos(e.frame.w, i)
e.window.write(" ")
end
e.window.setBackgroundColor(e.fg_bg.bkg)
end
-- update item y positions and move elements
local function update_positions()
local next_y = 1
scroll_frame.setVisible(false)
scroll_frame.setBackgroundColor(e.fg_bg.bkg)
scroll_frame.setTextColor(e.fg_bg.fgd)
scroll_frame.clear()
for i = 1, #list do
local item = list[i] ---@type listbox_item
item.y = next_y
next_y = next_y + item.h + item_pad
item.e.reposition(1, item.y)
item.e.show()
end
content_height = next_y
max_down_scroll = math.min(-1 * (content_height - (e.frame.h + 1 + item_pad)), 0)
if scroll_offset < max_down_scroll then scroll_offset = max_down_scroll end
scroll_frame.reposition(1, 1 + scroll_offset)
scroll_frame.setVisible(true)
draw_bar()
end
-- determine where to scroll to based on a scrollbar being dragged without a 1:1 relationship
---@param direction -1|1 negative 1 to scroll up by one, positive 1 to scroll down by one
local function scaled_bar_scroll(direction)
local scroll_progress = scroll_offset / max_down_scroll
local bar_position = math.floor(scroll_progress * (max_bar_height - 1))
-- check what moving the scroll bar up or down would mean for the scroll progress
scroll_progress = (bar_position + direction) / (max_bar_height - 1)
return math.max(math.floor(scroll_progress * max_down_scroll), max_down_scroll)
end
-- scroll down the list
local function scroll_down(scaled)
if scroll_offset > max_down_scroll then
if scaled then
scroll_offset = scaled_bar_scroll(1)
else
scroll_offset = scroll_offset - 1
end
update_positions()
end
end
-- scroll up the list
local function scroll_up(scaled)
if scroll_offset < 0 then
if scaled then
scroll_offset = scaled_bar_scroll(-1)
else
scroll_offset = scroll_offset + 1
end
update_positions()
end
end
-- handle a child element having been added to the list
---@param id element_id element identifier
---@param child graphics_element child element
function e.on_added(id, child)
table.insert(list, { id = id, e = child, y = 0, h = child.get_height() })
update_positions()
end
-- handle a child element having been removed from the list
---@param id element_id element identifier
function e.on_removed(id)
for idx, elem in ipairs(list) do
if elem.id == id then
table.remove(list, idx)
update_positions()
return
end
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled then
if event.type == CLICK_TYPE.TAP then
if event.current.x == e.frame.w then
if event.current.y == 1 or event.current.y < bar_bounds[1] then
draw_arrows(1)
scroll_up()
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
draw_arrows(-1)
scroll_down()
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
end
end
elseif event.type == CLICK_TYPE.DOWN then
if event.current.x == e.frame.w then
if event.current.y == 1 or event.current.y < bar_bounds[1] then
draw_arrows(1)
scroll_up()
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
draw_arrows(-1)
scroll_down()
else
-- clicked on bar
holding_bar = true
bar_grip_pos = event.current.y - bar_bounds[1]
mouse_last_y = event.current.y
end
end
elseif event.type == CLICK_TYPE.UP then
holding_bar = false
draw_arrows(0)
elseif event.type == CLICK_TYPE.DRAG then
if holding_bar then
-- if mouse is within vertical frame, including the grip point
if event.current.y > (1 + bar_grip_pos) and event.current.y <= ((e.frame.h - bar_height) + bar_grip_pos) then
if event.current.y < mouse_last_y then
scroll_up(bar_is_scaled)
elseif event.current.y > mouse_last_y then
scroll_down(bar_is_scaled)
end
mouse_last_y = event.current.y
end
end
elseif event.type == CLICK_TYPE.SCROLL_DOWN then
scroll_down()
elseif event.type == CLICK_TYPE.SCROLL_UP then
scroll_up()
end
end
end
draw_arrows(0)
draw_bar()
return e.complete()
end
return listbox

View File

@@ -7,11 +7,12 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new multipane element -- new multipane element
---@nodiscard ---@nodiscard
@@ -36,7 +37,7 @@ local function multipane(args)
e.set_value(1) e.set_value(1)
return e.get() return e.complete()
end end
return multipane return multipane

View File

@@ -11,7 +11,14 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field hidden? boolean true to hide on initial draw
---@class _pipe_map_entry
---@field atr boolean align top right (or bottom left for false)
---@field thin boolean thin pipe or not
---@field fg string foreground blit
---@field bg string background blit
-- new pipe network -- new pipe network
---@param args pipenet_args ---@param args pipenet_args
@@ -37,111 +44,273 @@ local function pipenet(args)
args.y = args.y or 1 args.y = args.y or 1
if args.bg ~= nil then if args.bg ~= nil then
args.fg_bg = core.graphics.cpair(args.bg, args.bg) args.fg_bg = core.cpair(args.bg, args.bg)
end end
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- draw all pipes -- determine if there are any thin pipes involved
local any_thin = false
for p = 1, #args.pipes do for p = 1, #args.pipes do
local pipe = args.pipes[p] ---@type pipe any_thin = args.pipes[p].thin
if any_thin then break end
end
local x = 1 + pipe.x1 if not any_thin then
local y = 1 + pipe.y1 -- draw all pipes
for p = 1, #args.pipes do
local pipe = args.pipes[p] ---@type pipe
local x_step = util.trinary(pipe.x1 >= pipe.x2, -1, 1) local x = 1 + pipe.x1
local y_step = util.trinary(pipe.y1 >= pipe.y2, -1, 1) local y = 1 + pipe.y1
e.window.setCursorPos(x, y) local x_step = util.trinary(pipe.x1 >= pipe.x2, -1, 1)
local y_step = util.trinary(pipe.y1 >= pipe.y2, -1, 1)
local c = core.graphics.cpair(pipe.color, e.fg_bg.bkg) if pipe.thin then
x_step = util.trinary(pipe.x1 == pipe.x2, 0, x_step)
y_step = util.trinary(pipe.y1 == pipe.y2, 0, y_step)
end
if pipe.align_tr then e.window.setCursorPos(x, y)
-- cross width then height
for i = 1, pipe.w do local c = core.cpair(pipe.color, e.fg_bg.bkg)
if pipe.thin then
if i == pipe.w then if pipe.align_tr then
-- corner -- cross width then height
if y_step > 0 then for i = 1, pipe.w do
e.window.blit("\x93", c.blit_bkg, c.blit_fgd) if pipe.thin then
if i == pipe.w then
-- corner
if y_step > 0 then
e.window.blit("\x93", c.blit_bkg, c.blit_fgd)
else
e.window.blit("\x8e", c.blit_fgd, c.blit_bkg)
end
else else
e.window.blit("\x8e", c.blit_fgd, c.blit_bkg) e.window.blit("\x8c", c.blit_fgd, c.blit_bkg)
end end
else else
if i == pipe.w and y_step > 0 then
-- corner
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
else
e.window.blit("\x8f", c.blit_fgd, c.blit_bkg)
end
end
x = x + x_step
e.window.setCursorPos(x, y)
end
-- back up one
x = x - x_step
for _ = 1, pipe.h - 1 do
y = y + y_step
e.window.setCursorPos(x, y)
if pipe.thin then
e.window.blit("\x95", c.blit_bkg, c.blit_fgd)
else
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
end
end
else
-- cross height then width
for i = 1, pipe.h do
if pipe.thin then
if i == pipe.h then
-- corner
if y_step < 0 then
e.window.blit("\x97", c.blit_bkg, c.blit_fgd)
elseif y_step > 0 then
e.window.blit("\x8d", c.blit_fgd, c.blit_bkg)
else
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg)
end
else
e.window.blit("\x95", c.blit_fgd, c.blit_bkg)
end
else
if i == pipe.h and y_step < 0 then
-- corner
e.window.blit("\x83", c.blit_bkg, c.blit_fgd)
else
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
end
end
y = y + y_step
e.window.setCursorPos(x, y)
end
-- back up one
y = y - y_step
for _ = 1, pipe.w - 1 do
x = x + x_step
e.window.setCursorPos(x, y)
if pipe.thin then
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg) e.window.blit("\x8c", c.blit_fgd, c.blit_bkg)
end
else
if i == pipe.w and y_step > 0 then
-- corner
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
else else
e.window.blit("\x8f", c.blit_fgd, c.blit_bkg)
end
end
x = x + x_step
e.window.setCursorPos(x, y)
end
-- back up one
x = x - x_step
for _ = 1, pipe.h - 1 do
y = y + y_step
e.window.setCursorPos(x, y)
if pipe.thin then
e.window.blit("\x95", c.blit_bkg, c.blit_fgd)
else
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
end
end
else
-- cross height then width
for i = 1, pipe.h do
if pipe.thin then
if i == pipe.h then
-- corner
if y_step < 0 then
e.window.blit("\x97", c.blit_bkg, c.blit_fgd)
else
e.window.blit("\x8d", c.blit_fgd, c.blit_bkg)
end
else
e.window.blit("\x95", c.blit_fgd, c.blit_bkg)
end
else
if i == pipe.h and y_step < 0 then
-- corner
e.window.blit("\x83", c.blit_bkg, c.blit_fgd) e.window.blit("\x83", c.blit_bkg, c.blit_fgd)
else
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
end end
end end
y = y + y_step
e.window.setCursorPos(x, y)
end end
end
else
-- build map if using thin pipes, easist way to check adjacent blocks (cannot 'cheat' like with standard width)
local map = {}
-- back up one -- allocate map
y = y - y_step for x = 1, args.width do
table.insert(map, {})
for _ = 1, args.height do table.insert(map[x], false) end
end
for _ = 1, pipe.w - 1 do -- build map
x = x + x_step for p = 1, #args.pipes do
e.window.setCursorPos(x, y) local pipe = args.pipes[p] ---@type pipe
if pipe.thin then local x = 1 + pipe.x1
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg) local y = 1 + pipe.y1
else
e.window.blit("\x83", c.blit_bkg, c.blit_fgd) local x_step = util.trinary(pipe.x1 >= pipe.x2, -1, 1)
local y_step = util.trinary(pipe.y1 >= pipe.y2, -1, 1)
local entry = { atr = pipe.align_tr, thin = pipe.thin, fg = colors.toBlit(pipe.color), bg = e.fg_bg.blit_bkg }
if pipe.align_tr then
-- cross width then height
for _ = 1, pipe.w do
map[x][y] = entry
x = x + x_step
end
x = x - x_step -- back up one
for _ = 1, pipe.h do
map[x][y] = entry
y = y + y_step
end
else
-- cross height then width
for _ = 1, pipe.h do
map[x][y] = entry
y = y + y_step
end
y = y - y_step -- back up one
for _ = 1, pipe.w do
map[x][y] = entry
x = x + x_step
end end
end end
end end
-- render
for x = 1, args.width do
for y = 1, args.height do
local entry = map[x][y] ---@type _pipe_map_entry|false
local char
local invert = false
if entry ~= false then
local function check(cx, cy)
return (map[cx] ~= nil) and (map[cx][cy] ~= nil) and (map[cx][cy] ~= false) and (map[cx][cy].fg == entry.fg)
end
if entry.thin then
if check(x - 1, y) then -- if left
if check(x, y - 1) then -- if above
if check(x + 1, y) then -- if right
if check(x, y + 1) then -- if below
char = util.trinary(entry.atr, "\x91", "\x9d")
invert = entry.atr
else -- not below
char = util.trinary(entry.atr, "\x8e", "\x8d")
end
else -- not right
if check(x, y + 1) then -- if below
char = util.trinary(entry.atr, "\x91", "\x95")
invert = entry.atr
else -- not below
char = util.trinary(entry.atr, "\x8e", "\x85")
end
end
elseif check(x, y + 1) then-- not above, if below
if check(x + 1, y) then -- if right
char = util.trinary(entry.atr, "\x93", "\x9c")
invert = entry.atr
else -- not right
char = util.trinary(entry.atr, "\x93", "\x94")
invert = entry.atr
end
else -- not above, not below
char = "\x8c"
end
elseif check(x + 1, y) then -- not left, if right
if check(x, y - 1) then -- if above
if check(x, y + 1) then -- if below
char = util.trinary(entry.atr, "\x95", "\x9d")
invert = entry.atr
else -- not below
char = util.trinary(entry.atr, "\x8a", "\x8d")
end
else -- not above
if check(x, y + 1) then -- if below
char = util.trinary(entry.atr, "\x97", "\x9c")
invert = entry.atr
else -- not below
char = "\x8c"
end
end
else -- not left, not right
char = "\x95"
invert = entry.atr
end
else
if check(x, y - 1) then -- above
-- not below and (if left or right)
if (not check(x, y + 1)) and (check(x - 1, y) or check(x + 1, y)) then
char = util.trinary(entry.atr, "\x8f", " ")
invert = not entry.atr
else -- not below w/ sides only
char = " "
invert = true
end
elseif check(x, y + 1) then -- not above, if below
-- if left or right
if (check(x - 1, y) or check(x + 1, y)) then
char = "\x83"
invert = true
else -- not left or right
char = " "
invert = true
end
else -- not above, not below
char = util.trinary(entry.atr, "\x8f", "\x83")
invert = not entry.atr
end
end
e.window.setCursorPos(x, y)
if invert then
e.window.blit(char, entry.bg, entry.fg)
else
e.window.blit(char, entry.fg, entry.bg)
end
end
end
end
end end
return e.get() return e.complete()
end end
return pipenet return pipenet

View File

@@ -11,11 +11,12 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new rectangle -- new rectangle
---@param args rectangle_args ---@param args rectangle_args
@@ -30,27 +31,35 @@ local function rectangle(args)
end end
-- offset children -- offset children
local offset_x = 0
local offset_y = 0
if args.border ~= nil then if args.border ~= nil then
args.offset_x = args.border.width offset_x = args.border.width
args.offset_y = args.border.width offset_y = args.border.width
-- slightly different y offset if the border is set to even -- slightly different y offset if the border is set to even
if args.border.even then if args.border.even then
local width_x2 = (2 * args.border.width) local width_x2 = (2 * args.border.width)
args.offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0) offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0)
end end
end end
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args, offset_x, offset_y)
-- create content window for child elements
e.content_window = window.create(e.window, 1 + offset_x, 1 + offset_y, e.frame.w - (2 * offset_x), e.frame.h - (2 * offset_y))
e.content_window.setBackgroundColor(e.fg_bg.bkg)
e.content_window.setTextColor(e.fg_bg.fgd)
e.content_window.clear()
-- draw bordered box if requested -- draw bordered box if requested
-- element constructor will have drawn basic colored rectangle regardless -- element constructor will have drawn basic colored rectangle regardless
if args.border ~= nil then if args.border ~= nil then
e.window.setCursorPos(1, 1) e.window.setCursorPos(1, 1)
local border_width = args.offset_x local border_width = offset_x
local border_height = args.offset_y local border_height = offset_y
local border_blit = colors.toBlit(args.border.color) local border_blit = colors.toBlit(args.border.color)
local width_x2 = border_width * 2 local width_x2 = border_width * 2
local inner_width = e.frame.w - width_x2 local inner_width = e.frame.w - width_x2
@@ -177,7 +186,7 @@ local function rectangle(args)
end end
end end
return e.get() return e.complete()
end end
return rectangle return rectangle

View File

@@ -5,7 +5,7 @@ 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")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN local TEXT_ALIGN = core.TEXT_ALIGN
---@class textbox_args ---@class textbox_args
---@field text string text to show ---@field text string text to show
@@ -13,11 +13,12 @@ local TEXT_ALIGN = core.graphics.TEXT_ALIGN
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new text box -- new text box
---@param args textbox_args ---@param args textbox_args
@@ -64,7 +65,7 @@ local function textbox(args)
display_text(val) display_text(val)
end end
return e.get() return e.complete()
end end
return textbox return textbox

View File

@@ -11,11 +11,12 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new tiling box -- new tiling box
---@param args tiling_args ---@param args tiling_args
@@ -81,7 +82,7 @@ local function tiling(args)
if inner_width % 2 == 0 then alternator = not alternator end if inner_width % 2 == 0 then alternator = not alternator end
end end
return e.get() return e.complete()
end end
return tiling return tiling

161
graphics/events.lua Normal file
View File

@@ -0,0 +1,161 @@
--
-- Graphics Events and Event Handlers
--
local util = require("scada-common.util")
local events = {}
---@enum CLICK_BUTTON
events.CLICK_BUTTON = {
GENERIC = 0,
LEFT_BUTTON = 1,
RIGHT_BUTTON = 2,
MID_BUTTON = 3
}
---@enum CLICK_TYPE
events.CLICK_TYPE = {
TAP = 1, -- screen tap (complete click)
DOWN = 2, -- button down
UP = 3, -- button up (completed a click)
DRAG = 4, -- mouse dragged
SCROLL_DOWN = 5, -- scroll down
SCROLL_UP = 6 -- scroll up
}
-- create a new 2D coordinate
---@param x integer
---@param y integer
---@return coordinate_2d
local function _coord2d(x, y) return { x = x, y = y } end
---@class mouse_interaction
---@field monitor string
---@field button CLICK_BUTTON
---@field type CLICK_TYPE
---@field initial coordinate_2d
---@field current coordinate_2d
local handler = {
-- left, right, middle button down tracking
button_down = {
_coord2d(0, 0),
_coord2d(0, 0),
_coord2d(0, 0)
}
}
-- create a new monitor touch mouse interaction event
---@nodiscard
---@param monitor string
---@param x integer
---@param y integer
---@return mouse_interaction
local function _monitor_touch(monitor, x, y)
return {
monitor = monitor,
button = events.CLICK_BUTTON.GENERIC,
type = events.CLICK_TYPE.TAP,
initial = _coord2d(x, y),
current = _coord2d(x, y)
}
end
-- create a new mouse button mouse interaction event
---@nodiscard
---@param button CLICK_BUTTON mouse button
---@param type CLICK_TYPE click type
---@param x1 integer initial x
---@param y1 integer initial y
---@param x2 integer current x
---@param y2 integer current y
---@return mouse_interaction
local function _mouse_event(button, type, x1, y1, x2, y2)
return {
monitor = "terminal",
button = button,
type = type,
initial = _coord2d(x1, y1),
current = _coord2d(x2, y2)
}
end
-- create a new generic mouse interaction event
---@nodiscard
---@param type CLICK_TYPE
---@param x integer
---@param y integer
---@return mouse_interaction
function events.mouse_generic(type, x, y)
return {
monitor = "",
button = events.CLICK_BUTTON.GENERIC,
type = type,
initial = _coord2d(x, y),
current = _coord2d(x, y)
}
end
-- create a new transposed mouse interaction event using the event's monitor/button fields
---@nodiscard
---@param event mouse_interaction
---@param elem_pos_x integer element's x position: new x = (event x - element x) + 1
---@param elem_pos_y integer element's y position: new y = (event y - element y) + 1
---@return mouse_interaction
function events.mouse_transposed(event, elem_pos_x, elem_pos_y)
return {
monitor = event.monitor,
button = event.button,
type = event.type,
initial = _coord2d((event.initial.x - elem_pos_x) + 1, (event.initial.y - elem_pos_y) + 1),
current = _coord2d((event.current.x - elem_pos_x) + 1, (event.current.y - elem_pos_y) + 1)
}
end
-- check if an event qualifies as a click (tap or up)
---@nodiscard
---@param t CLICK_TYPE
function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events.CLICK_TYPE.UP end
-- create a new mouse event to pass onto graphics renderer<br>
-- supports: mouse_click, mouse_up, mouse_drag, mouse_scroll, and monitor_touch
---@param event_type os_event OS event to handle
---@param opt integer|string button, scroll direction, or monitor for monitor touch
---@param x integer x coordinate
---@param y integer y coordinate
---@return mouse_interaction|nil
function events.new_mouse_event(event_type, opt, x, y)
if event_type == "mouse_click" then
---@cast opt 1|2|3
handler.button_down[opt] = _coord2d(x, y)
return _mouse_event(opt, events.CLICK_TYPE.DOWN, x, y, x, y)
elseif event_type == "mouse_up" then
---@cast opt 1|2|3
local initial = handler.button_down[opt] ---@type coordinate_2d
return _mouse_event(opt, events.CLICK_TYPE.UP, initial.x, initial.y, x, y)
elseif event_type == "monitor_touch" then
---@cast opt string
return _monitor_touch(opt, x, y)
elseif event_type == "mouse_drag" then
---@cast opt 1|2|3
local initial = handler.button_down[opt] ---@type coordinate_2d
return _mouse_event(opt, events.CLICK_TYPE.DRAG, initial.x, initial.y, x, y)
elseif event_type == "mouse_scroll" then
---@cast opt 1|-1
local scroll_direction = util.trinary(opt == 1, events.CLICK_TYPE.SCROLL_DOWN, events.CLICK_TYPE.SCROLL_UP)
return _mouse_event(events.CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y)
end
end
-- create a new key event to pass onto graphics renderer<br>
-- supports: char, key, and key_up
---@param event_type os_event
function events.new_key_event(event_type)
if event_type == "char" then
elseif event_type == "key" then
elseif event_type == "key_up" then
end
end
return events

View File

@@ -2,7 +2,7 @@
-- Indicator Light Flasher -- Indicator Light Flasher
-- --
local tcd = require("scada-common.tcallbackdsp") local tcd = require("scada-common.tcd")
local flasher = {} local flasher = {}
@@ -43,8 +43,10 @@ end
-- start/resume the flasher periodic -- start/resume the flasher periodic
function flasher.run() function flasher.run()
active = true if not active then
callback_250ms() active = true
callback_250ms()
end
end end
-- clear all blinking indicators and stop the flasher periodic -- clear all blinking indicators and stop the flasher periodic

View File

@@ -23,11 +23,11 @@ def dir_size(path):
return total return total
# get the version of an application at the provided path # get the version of an application at the provided path
def get_version(path, is_comms = False): def get_version(path, is_lib = False):
ver = "" ver = ""
string = "comms.version = \"" string = ".version = \""
if not is_comms: if not is_lib:
string = "_VERSION = \"" string = "_VERSION = \""
f = open(path, "r") f = open(path, "r")
@@ -48,7 +48,10 @@ def make_manifest(size):
"versions" : { "versions" : {
"installer" : get_version("./ccmsi.lua"), "installer" : get_version("./ccmsi.lua"),
"bootloader" : get_version("./startup.lua"), "bootloader" : get_version("./startup.lua"),
"common" : get_version("./scada-common/util.lua", True),
"comms" : get_version("./scada-common/comms.lua", True), "comms" : get_version("./scada-common/comms.lua", True),
"graphics" : get_version("./graphics/core.lua", True),
"lockbox" : get_version("./lockbox/init.lua", True),
"reactor-plc" : get_version("./reactor-plc/startup.lua"), "reactor-plc" : get_version("./reactor-plc/startup.lua"),
"rtu" : get_version("./rtu/startup.lua"), "rtu" : get_version("./rtu/startup.lua"),
"supervisor" : get_version("./supervisor/startup.lua"), "supervisor" : get_version("./supervisor/startup.lua"),
@@ -69,11 +72,11 @@ def make_manifest(size):
"pocket" : list_files("./pocket"), "pocket" : list_files("./pocket"),
}, },
"depends" : { "depends" : {
"reactor-plc" : [ "system", "common", "graphics" ], "reactor-plc" : [ "system", "common", "graphics", "lockbox" ],
"rtu" : [ "system", "common", "graphics" ], "rtu" : [ "system", "common", "graphics", "lockbox" ],
"supervisor" : [ "system", "common" ], "supervisor" : [ "system", "common", "graphics", "lockbox" ],
"coordinator" : [ "system", "common", "graphics" ], "coordinator" : [ "system", "common", "graphics", "lockbox" ],
"pocket" : [ "system", "common", "graphics" ] "pocket" : [ "system", "common", "graphics", "lockbox" ]
}, },
"sizes" : { "sizes" : {
# manifest file estimate # manifest file estimate
@@ -111,7 +114,7 @@ f.close()
if len(sys.argv) > 1 and sys.argv[1] == "shields": if len(sys.argv) > 1 and sys.argv[1] == "shields":
# write all the JSON files for shields.io # write all the JSON files for shields.io
for key, version in final_manifest["versions"].items(): for key, version in final_manifest["versions"].items():
f = open("./shields/" + key + ".json", "w") f = open("./deploy/" + key + ".json", "w")
if version.find("alpha") >= 0: if version.find("alpha") >= 0:
color = "yellow" color = "yellow"

File diff suppressed because one or more lines are too long

View File

@@ -1,415 +0,0 @@
local Array = require("lockbox.util.array");
local Bit = require("lockbox.util.bit");
local XOR = Bit.bxor;
local SBOX = {
[0] = 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16};
local ISBOX = {
[0] = 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D};
local ROW_SHIFT = { 1, 6, 11, 16, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, };
local IROW_SHIFT = { 1, 14, 11, 8, 5, 2, 15, 12, 9, 6, 3, 16, 13, 10, 7, 4, };
local ETABLE = {
[0] = 0x01, 0x03, 0x05, 0x0F, 0x11, 0x33, 0x55, 0xFF, 0x1A, 0x2E, 0x72, 0x96, 0xA1, 0xF8, 0x13, 0x35,
0x5F, 0xE1, 0x38, 0x48, 0xD8, 0x73, 0x95, 0xA4, 0xF7, 0x02, 0x06, 0x0A, 0x1E, 0x22, 0x66, 0xAA,
0xE5, 0x34, 0x5C, 0xE4, 0x37, 0x59, 0xEB, 0x26, 0x6A, 0xBE, 0xD9, 0x70, 0x90, 0xAB, 0xE6, 0x31,
0x53, 0xF5, 0x04, 0x0C, 0x14, 0x3C, 0x44, 0xCC, 0x4F, 0xD1, 0x68, 0xB8, 0xD3, 0x6E, 0xB2, 0xCD,
0x4C, 0xD4, 0x67, 0xA9, 0xE0, 0x3B, 0x4D, 0xD7, 0x62, 0xA6, 0xF1, 0x08, 0x18, 0x28, 0x78, 0x88,
0x83, 0x9E, 0xB9, 0xD0, 0x6B, 0xBD, 0xDC, 0x7F, 0x81, 0x98, 0xB3, 0xCE, 0x49, 0xDB, 0x76, 0x9A,
0xB5, 0xC4, 0x57, 0xF9, 0x10, 0x30, 0x50, 0xF0, 0x0B, 0x1D, 0x27, 0x69, 0xBB, 0xD6, 0x61, 0xA3,
0xFE, 0x19, 0x2B, 0x7D, 0x87, 0x92, 0xAD, 0xEC, 0x2F, 0x71, 0x93, 0xAE, 0xE9, 0x20, 0x60, 0xA0,
0xFB, 0x16, 0x3A, 0x4E, 0xD2, 0x6D, 0xB7, 0xC2, 0x5D, 0xE7, 0x32, 0x56, 0xFA, 0x15, 0x3F, 0x41,
0xC3, 0x5E, 0xE2, 0x3D, 0x47, 0xC9, 0x40, 0xC0, 0x5B, 0xED, 0x2C, 0x74, 0x9C, 0xBF, 0xDA, 0x75,
0x9F, 0xBA, 0xD5, 0x64, 0xAC, 0xEF, 0x2A, 0x7E, 0x82, 0x9D, 0xBC, 0xDF, 0x7A, 0x8E, 0x89, 0x80,
0x9B, 0xB6, 0xC1, 0x58, 0xE8, 0x23, 0x65, 0xAF, 0xEA, 0x25, 0x6F, 0xB1, 0xC8, 0x43, 0xC5, 0x54,
0xFC, 0x1F, 0x21, 0x63, 0xA5, 0xF4, 0x07, 0x09, 0x1B, 0x2D, 0x77, 0x99, 0xB0, 0xCB, 0x46, 0xCA,
0x45, 0xCF, 0x4A, 0xDE, 0x79, 0x8B, 0x86, 0x91, 0xA8, 0xE3, 0x3E, 0x42, 0xC6, 0x51, 0xF3, 0x0E,
0x12, 0x36, 0x5A, 0xEE, 0x29, 0x7B, 0x8D, 0x8C, 0x8F, 0x8A, 0x85, 0x94, 0xA7, 0xF2, 0x0D, 0x17,
0x39, 0x4B, 0xDD, 0x7C, 0x84, 0x97, 0xA2, 0xFD, 0x1C, 0x24, 0x6C, 0xB4, 0xC7, 0x52, 0xF6, 0x01};
local LTABLE = {
[0] = 0x00, 0x00, 0x19, 0x01, 0x32, 0x02, 0x1A, 0xC6, 0x4B, 0xC7, 0x1B, 0x68, 0x33, 0xEE, 0xDF, 0x03,
0x64, 0x04, 0xE0, 0x0E, 0x34, 0x8D, 0x81, 0xEF, 0x4C, 0x71, 0x08, 0xC8, 0xF8, 0x69, 0x1C, 0xC1,
0x7D, 0xC2, 0x1D, 0xB5, 0xF9, 0xB9, 0x27, 0x6A, 0x4D, 0xE4, 0xA6, 0x72, 0x9A, 0xC9, 0x09, 0x78,
0x65, 0x2F, 0x8A, 0x05, 0x21, 0x0F, 0xE1, 0x24, 0x12, 0xF0, 0x82, 0x45, 0x35, 0x93, 0xDA, 0x8E,
0x96, 0x8F, 0xDB, 0xBD, 0x36, 0xD0, 0xCE, 0x94, 0x13, 0x5C, 0xD2, 0xF1, 0x40, 0x46, 0x83, 0x38,
0x66, 0xDD, 0xFD, 0x30, 0xBF, 0x06, 0x8B, 0x62, 0xB3, 0x25, 0xE2, 0x98, 0x22, 0x88, 0x91, 0x10,
0x7E, 0x6E, 0x48, 0xC3, 0xA3, 0xB6, 0x1E, 0x42, 0x3A, 0x6B, 0x28, 0x54, 0xFA, 0x85, 0x3D, 0xBA,
0x2B, 0x79, 0x0A, 0x15, 0x9B, 0x9F, 0x5E, 0xCA, 0x4E, 0xD4, 0xAC, 0xE5, 0xF3, 0x73, 0xA7, 0x57,
0xAF, 0x58, 0xA8, 0x50, 0xF4, 0xEA, 0xD6, 0x74, 0x4F, 0xAE, 0xE9, 0xD5, 0xE7, 0xE6, 0xAD, 0xE8,
0x2C, 0xD7, 0x75, 0x7A, 0xEB, 0x16, 0x0B, 0xF5, 0x59, 0xCB, 0x5F, 0xB0, 0x9C, 0xA9, 0x51, 0xA0,
0x7F, 0x0C, 0xF6, 0x6F, 0x17, 0xC4, 0x49, 0xEC, 0xD8, 0x43, 0x1F, 0x2D, 0xA4, 0x76, 0x7B, 0xB7,
0xCC, 0xBB, 0x3E, 0x5A, 0xFB, 0x60, 0xB1, 0x86, 0x3B, 0x52, 0xA1, 0x6C, 0xAA, 0x55, 0x29, 0x9D,
0x97, 0xB2, 0x87, 0x90, 0x61, 0xBE, 0xDC, 0xFC, 0xBC, 0x95, 0xCF, 0xCD, 0x37, 0x3F, 0x5B, 0xD1,
0x53, 0x39, 0x84, 0x3C, 0x41, 0xA2, 0x6D, 0x47, 0x14, 0x2A, 0x9E, 0x5D, 0x56, 0xF2, 0xD3, 0xAB,
0x44, 0x11, 0x92, 0xD9, 0x23, 0x20, 0x2E, 0x89, 0xB4, 0x7C, 0xB8, 0x26, 0x77, 0x99, 0xE3, 0xA5,
0x67, 0x4A, 0xED, 0xDE, 0xC5, 0x31, 0xFE, 0x18, 0x0D, 0x63, 0x8C, 0x80, 0xC0, 0xF7, 0x70, 0x07};
local MIXTABLE = {
0x02, 0x03, 0x01, 0x01,
0x01, 0x02, 0x03, 0x01,
0x01, 0x01, 0x02, 0x03,
0x03, 0x01, 0x01, 0x02};
local IMIXTABLE = {
0x0E, 0x0B, 0x0D, 0x09,
0x09, 0x0E, 0x0B, 0x0D,
0x0D, 0x09, 0x0E, 0x0B,
0x0B, 0x0D, 0x09, 0x0E};
local RCON = {
[0] = 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a,
0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39,
0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a,
0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8,
0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef,
0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc,
0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b,
0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3,
0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94,
0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20,
0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35,
0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f,
0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04,
0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63,
0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd,
0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d};
local GMUL = function(A, B)
if(A == 0x01) then return B; end
if(B == 0x01) then return A; end
if(A == 0x00) then return 0; end
if(B == 0x00) then return 0; end
local LA = LTABLE[A];
local LB = LTABLE[B];
local sum = LA + LB;
if (sum > 0xFF) then sum = sum - 0xFF; end
return ETABLE[sum];
end
local byteSub = Array.substitute;
local shiftRow = Array.permute;
local mixCol = function(i, mix)
local out = {};
local a, b, c, d;
a = GMUL(i[ 1], mix[ 1]);
b = GMUL(i[ 2], mix[ 2]);
c = GMUL(i[ 3], mix[ 3]);
d = GMUL(i[ 4], mix[ 4]);
out[ 1] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[ 5]);
b = GMUL(i[ 2], mix[ 6]);
c = GMUL(i[ 3], mix[ 7]);
d = GMUL(i[ 4], mix[ 8]);
out[ 2] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[ 9]);
b = GMUL(i[ 2], mix[10]);
c = GMUL(i[ 3], mix[11]);
d = GMUL(i[ 4], mix[12]);
out[ 3] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[13]);
b = GMUL(i[ 2], mix[14]);
c = GMUL(i[ 3], mix[15]);
d = GMUL(i[ 4], mix[16]);
out[ 4] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 1]);
b = GMUL(i[ 6], mix[ 2]);
c = GMUL(i[ 7], mix[ 3]);
d = GMUL(i[ 8], mix[ 4]);
out[ 5] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 5]);
b = GMUL(i[ 6], mix[ 6]);
c = GMUL(i[ 7], mix[ 7]);
d = GMUL(i[ 8], mix[ 8]);
out[ 6] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 9]);
b = GMUL(i[ 6], mix[10]);
c = GMUL(i[ 7], mix[11]);
d = GMUL(i[ 8], mix[12]);
out[ 7] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[13]);
b = GMUL(i[ 6], mix[14]);
c = GMUL(i[ 7], mix[15]);
d = GMUL(i[ 8], mix[16]);
out[ 8] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 1]);
b = GMUL(i[10], mix[ 2]);
c = GMUL(i[11], mix[ 3]);
d = GMUL(i[12], mix[ 4]);
out[ 9] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 5]);
b = GMUL(i[10], mix[ 6]);
c = GMUL(i[11], mix[ 7]);
d = GMUL(i[12], mix[ 8]);
out[10] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 9]);
b = GMUL(i[10], mix[10]);
c = GMUL(i[11], mix[11]);
d = GMUL(i[12], mix[12]);
out[11] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[13]);
b = GMUL(i[10], mix[14]);
c = GMUL(i[11], mix[15]);
d = GMUL(i[12], mix[16]);
out[12] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 1]);
b = GMUL(i[14], mix[ 2]);
c = GMUL(i[15], mix[ 3]);
d = GMUL(i[16], mix[ 4]);
out[13] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 5]);
b = GMUL(i[14], mix[ 6]);
c = GMUL(i[15], mix[ 7]);
d = GMUL(i[16], mix[ 8]);
out[14] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 9]);
b = GMUL(i[14], mix[10]);
c = GMUL(i[15], mix[11]);
d = GMUL(i[16], mix[12]);
out[15] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[13]);
b = GMUL(i[14], mix[14]);
c = GMUL(i[15], mix[15]);
d = GMUL(i[16], mix[16]);
out[16] = XOR(XOR(a, b), XOR(c, d));
return out;
end
local keyRound = function(key, round)
local out = {};
out[ 1] = XOR(key[ 1], XOR(SBOX[key[14]], RCON[round]));
out[ 2] = XOR(key[ 2], SBOX[key[15]]);
out[ 3] = XOR(key[ 3], SBOX[key[16]]);
out[ 4] = XOR(key[ 4], SBOX[key[13]]);
out[ 5] = XOR(out[ 1], key[ 5]);
out[ 6] = XOR(out[ 2], key[ 6]);
out[ 7] = XOR(out[ 3], key[ 7]);
out[ 8] = XOR(out[ 4], key[ 8]);
out[ 9] = XOR(out[ 5], key[ 9]);
out[10] = XOR(out[ 6], key[10]);
out[11] = XOR(out[ 7], key[11]);
out[12] = XOR(out[ 8], key[12]);
out[13] = XOR(out[ 9], key[13]);
out[14] = XOR(out[10], key[14]);
out[15] = XOR(out[11], key[15]);
out[16] = XOR(out[12], key[16]);
return out;
end
local keyExpand = function(key)
local keys = {};
local temp = key;
keys[1] = temp;
for i = 1, 10 do
temp = keyRound(temp, i);
keys[i + 1] = temp;
end
return keys;
end
local addKey = Array.XOR;
local AES = {};
AES.blockSize = 16;
AES.encrypt = function(_key, block)
local key = keyExpand(_key);
--round 0
block = addKey(block, key[1]);
--round 1
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[2]);
--round 2
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[3]);
--round 3
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[4]);
--round 4
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[5]);
--round 5
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[6]);
--round 6
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[7]);
--round 7
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[8]);
--round 8
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[9]);
--round 9
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[10]);
--round 10
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = addKey(block, key[11]);
return block;
end
AES.decrypt = function(_key, block)
local key = keyExpand(_key);
--round 0
block = addKey(block, key[11]);
--round 1
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[10]);
block = mixCol(block, IMIXTABLE);
--round 2
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[9]);
block = mixCol(block, IMIXTABLE);
--round 3
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[8]);
block = mixCol(block, IMIXTABLE);
--round 4
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[7]);
block = mixCol(block, IMIXTABLE);
--round 5
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[6]);
block = mixCol(block, IMIXTABLE);
--round 6
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[5]);
block = mixCol(block, IMIXTABLE);
--round 7
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[4]);
block = mixCol(block, IMIXTABLE);
--round 8
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[3]);
block = mixCol(block, IMIXTABLE);
--round 9
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[2]);
block = mixCol(block, IMIXTABLE);
--round 10
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[1]);
return block;
end
return AES;

View File

@@ -1,462 +0,0 @@
local Array = require("lockbox.util.array");
local Bit = require("lockbox.util.bit");
local XOR = Bit.bxor;
local SBOX = {
[0] = 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16};
local ISBOX = {
[0] = 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D};
local ROW_SHIFT = { 1, 6, 11, 16, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, };
local IROW_SHIFT = { 1, 14, 11, 8, 5, 2, 15, 12, 9, 6, 3, 16, 13, 10, 7, 4, };
local ETABLE = {
[0] = 0x01, 0x03, 0x05, 0x0F, 0x11, 0x33, 0x55, 0xFF, 0x1A, 0x2E, 0x72, 0x96, 0xA1, 0xF8, 0x13, 0x35,
0x5F, 0xE1, 0x38, 0x48, 0xD8, 0x73, 0x95, 0xA4, 0xF7, 0x02, 0x06, 0x0A, 0x1E, 0x22, 0x66, 0xAA,
0xE5, 0x34, 0x5C, 0xE4, 0x37, 0x59, 0xEB, 0x26, 0x6A, 0xBE, 0xD9, 0x70, 0x90, 0xAB, 0xE6, 0x31,
0x53, 0xF5, 0x04, 0x0C, 0x14, 0x3C, 0x44, 0xCC, 0x4F, 0xD1, 0x68, 0xB8, 0xD3, 0x6E, 0xB2, 0xCD,
0x4C, 0xD4, 0x67, 0xA9, 0xE0, 0x3B, 0x4D, 0xD7, 0x62, 0xA6, 0xF1, 0x08, 0x18, 0x28, 0x78, 0x88,
0x83, 0x9E, 0xB9, 0xD0, 0x6B, 0xBD, 0xDC, 0x7F, 0x81, 0x98, 0xB3, 0xCE, 0x49, 0xDB, 0x76, 0x9A,
0xB5, 0xC4, 0x57, 0xF9, 0x10, 0x30, 0x50, 0xF0, 0x0B, 0x1D, 0x27, 0x69, 0xBB, 0xD6, 0x61, 0xA3,
0xFE, 0x19, 0x2B, 0x7D, 0x87, 0x92, 0xAD, 0xEC, 0x2F, 0x71, 0x93, 0xAE, 0xE9, 0x20, 0x60, 0xA0,
0xFB, 0x16, 0x3A, 0x4E, 0xD2, 0x6D, 0xB7, 0xC2, 0x5D, 0xE7, 0x32, 0x56, 0xFA, 0x15, 0x3F, 0x41,
0xC3, 0x5E, 0xE2, 0x3D, 0x47, 0xC9, 0x40, 0xC0, 0x5B, 0xED, 0x2C, 0x74, 0x9C, 0xBF, 0xDA, 0x75,
0x9F, 0xBA, 0xD5, 0x64, 0xAC, 0xEF, 0x2A, 0x7E, 0x82, 0x9D, 0xBC, 0xDF, 0x7A, 0x8E, 0x89, 0x80,
0x9B, 0xB6, 0xC1, 0x58, 0xE8, 0x23, 0x65, 0xAF, 0xEA, 0x25, 0x6F, 0xB1, 0xC8, 0x43, 0xC5, 0x54,
0xFC, 0x1F, 0x21, 0x63, 0xA5, 0xF4, 0x07, 0x09, 0x1B, 0x2D, 0x77, 0x99, 0xB0, 0xCB, 0x46, 0xCA,
0x45, 0xCF, 0x4A, 0xDE, 0x79, 0x8B, 0x86, 0x91, 0xA8, 0xE3, 0x3E, 0x42, 0xC6, 0x51, 0xF3, 0x0E,
0x12, 0x36, 0x5A, 0xEE, 0x29, 0x7B, 0x8D, 0x8C, 0x8F, 0x8A, 0x85, 0x94, 0xA7, 0xF2, 0x0D, 0x17,
0x39, 0x4B, 0xDD, 0x7C, 0x84, 0x97, 0xA2, 0xFD, 0x1C, 0x24, 0x6C, 0xB4, 0xC7, 0x52, 0xF6, 0x01};
local LTABLE = {
[0] = 0x00, 0x00, 0x19, 0x01, 0x32, 0x02, 0x1A, 0xC6, 0x4B, 0xC7, 0x1B, 0x68, 0x33, 0xEE, 0xDF, 0x03,
0x64, 0x04, 0xE0, 0x0E, 0x34, 0x8D, 0x81, 0xEF, 0x4C, 0x71, 0x08, 0xC8, 0xF8, 0x69, 0x1C, 0xC1,
0x7D, 0xC2, 0x1D, 0xB5, 0xF9, 0xB9, 0x27, 0x6A, 0x4D, 0xE4, 0xA6, 0x72, 0x9A, 0xC9, 0x09, 0x78,
0x65, 0x2F, 0x8A, 0x05, 0x21, 0x0F, 0xE1, 0x24, 0x12, 0xF0, 0x82, 0x45, 0x35, 0x93, 0xDA, 0x8E,
0x96, 0x8F, 0xDB, 0xBD, 0x36, 0xD0, 0xCE, 0x94, 0x13, 0x5C, 0xD2, 0xF1, 0x40, 0x46, 0x83, 0x38,
0x66, 0xDD, 0xFD, 0x30, 0xBF, 0x06, 0x8B, 0x62, 0xB3, 0x25, 0xE2, 0x98, 0x22, 0x88, 0x91, 0x10,
0x7E, 0x6E, 0x48, 0xC3, 0xA3, 0xB6, 0x1E, 0x42, 0x3A, 0x6B, 0x28, 0x54, 0xFA, 0x85, 0x3D, 0xBA,
0x2B, 0x79, 0x0A, 0x15, 0x9B, 0x9F, 0x5E, 0xCA, 0x4E, 0xD4, 0xAC, 0xE5, 0xF3, 0x73, 0xA7, 0x57,
0xAF, 0x58, 0xA8, 0x50, 0xF4, 0xEA, 0xD6, 0x74, 0x4F, 0xAE, 0xE9, 0xD5, 0xE7, 0xE6, 0xAD, 0xE8,
0x2C, 0xD7, 0x75, 0x7A, 0xEB, 0x16, 0x0B, 0xF5, 0x59, 0xCB, 0x5F, 0xB0, 0x9C, 0xA9, 0x51, 0xA0,
0x7F, 0x0C, 0xF6, 0x6F, 0x17, 0xC4, 0x49, 0xEC, 0xD8, 0x43, 0x1F, 0x2D, 0xA4, 0x76, 0x7B, 0xB7,
0xCC, 0xBB, 0x3E, 0x5A, 0xFB, 0x60, 0xB1, 0x86, 0x3B, 0x52, 0xA1, 0x6C, 0xAA, 0x55, 0x29, 0x9D,
0x97, 0xB2, 0x87, 0x90, 0x61, 0xBE, 0xDC, 0xFC, 0xBC, 0x95, 0xCF, 0xCD, 0x37, 0x3F, 0x5B, 0xD1,
0x53, 0x39, 0x84, 0x3C, 0x41, 0xA2, 0x6D, 0x47, 0x14, 0x2A, 0x9E, 0x5D, 0x56, 0xF2, 0xD3, 0xAB,
0x44, 0x11, 0x92, 0xD9, 0x23, 0x20, 0x2E, 0x89, 0xB4, 0x7C, 0xB8, 0x26, 0x77, 0x99, 0xE3, 0xA5,
0x67, 0x4A, 0xED, 0xDE, 0xC5, 0x31, 0xFE, 0x18, 0x0D, 0x63, 0x8C, 0x80, 0xC0, 0xF7, 0x70, 0x07};
local MIXTABLE = {
0x02, 0x03, 0x01, 0x01,
0x01, 0x02, 0x03, 0x01,
0x01, 0x01, 0x02, 0x03,
0x03, 0x01, 0x01, 0x02};
local IMIXTABLE = {
0x0E, 0x0B, 0x0D, 0x09,
0x09, 0x0E, 0x0B, 0x0D,
0x0D, 0x09, 0x0E, 0x0B,
0x0B, 0x0D, 0x09, 0x0E};
local RCON = {
[0] = 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a,
0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39,
0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a,
0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8,
0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef,
0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc,
0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b,
0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3,
0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94,
0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20,
0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35,
0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f,
0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04,
0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63,
0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd,
0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d};
local GMUL = function(A, B)
if(A == 0x01) then return B; end
if(B == 0x01) then return A; end
if(A == 0x00) then return 0; end
if(B == 0x00) then return 0; end
local LA = LTABLE[A];
local LB = LTABLE[B];
local sum = LA + LB;
if (sum > 0xFF) then sum = sum - 0xFF; end
return ETABLE[sum];
end
local byteSub = Array.substitute;
local shiftRow = Array.permute;
local mixCol = function(i, mix)
local out = {};
local a, b, c, d;
a = GMUL(i[ 1], mix[ 1]);
b = GMUL(i[ 2], mix[ 2]);
c = GMUL(i[ 3], mix[ 3]);
d = GMUL(i[ 4], mix[ 4]);
out[ 1] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[ 5]);
b = GMUL(i[ 2], mix[ 6]);
c = GMUL(i[ 3], mix[ 7]);
d = GMUL(i[ 4], mix[ 8]);
out[ 2] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[ 9]);
b = GMUL(i[ 2], mix[10]);
c = GMUL(i[ 3], mix[11]);
d = GMUL(i[ 4], mix[12]);
out[ 3] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[13]);
b = GMUL(i[ 2], mix[14]);
c = GMUL(i[ 3], mix[15]);
d = GMUL(i[ 4], mix[16]);
out[ 4] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 1]);
b = GMUL(i[ 6], mix[ 2]);
c = GMUL(i[ 7], mix[ 3]);
d = GMUL(i[ 8], mix[ 4]);
out[ 5] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 5]);
b = GMUL(i[ 6], mix[ 6]);
c = GMUL(i[ 7], mix[ 7]);
d = GMUL(i[ 8], mix[ 8]);
out[ 6] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 9]);
b = GMUL(i[ 6], mix[10]);
c = GMUL(i[ 7], mix[11]);
d = GMUL(i[ 8], mix[12]);
out[ 7] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[13]);
b = GMUL(i[ 6], mix[14]);
c = GMUL(i[ 7], mix[15]);
d = GMUL(i[ 8], mix[16]);
out[ 8] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 1]);
b = GMUL(i[10], mix[ 2]);
c = GMUL(i[11], mix[ 3]);
d = GMUL(i[12], mix[ 4]);
out[ 9] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 5]);
b = GMUL(i[10], mix[ 6]);
c = GMUL(i[11], mix[ 7]);
d = GMUL(i[12], mix[ 8]);
out[10] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 9]);
b = GMUL(i[10], mix[10]);
c = GMUL(i[11], mix[11]);
d = GMUL(i[12], mix[12]);
out[11] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[13]);
b = GMUL(i[10], mix[14]);
c = GMUL(i[11], mix[15]);
d = GMUL(i[12], mix[16]);
out[12] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 1]);
b = GMUL(i[14], mix[ 2]);
c = GMUL(i[15], mix[ 3]);
d = GMUL(i[16], mix[ 4]);
out[13] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 5]);
b = GMUL(i[14], mix[ 6]);
c = GMUL(i[15], mix[ 7]);
d = GMUL(i[16], mix[ 8]);
out[14] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 9]);
b = GMUL(i[14], mix[10]);
c = GMUL(i[15], mix[11]);
d = GMUL(i[16], mix[12]);
out[15] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[13]);
b = GMUL(i[14], mix[14]);
c = GMUL(i[15], mix[15]);
d = GMUL(i[16], mix[16]);
out[16] = XOR(XOR(a, b), XOR(c, d));
return out;
end
local keyRound = function(key, round)
local i = (round - 1) * 24;
local out = key;
out[25 + i] = XOR(key[ 1 + i], XOR(SBOX[key[22 + i]], RCON[round]));
out[26 + i] = XOR(key[ 2 + i], SBOX[key[23 + i]]);
out[27 + i] = XOR(key[ 3 + i], SBOX[key[24 + i]]);
out[28 + i] = XOR(key[ 4 + i], SBOX[key[21 + i]]);
out[29 + i] = XOR(out[25 + i], key[ 5 + i]);
out[30 + i] = XOR(out[26 + i], key[ 6 + i]);
out[31 + i] = XOR(out[27 + i], key[ 7 + i]);
out[32 + i] = XOR(out[28 + i], key[ 8 + i]);
out[33 + i] = XOR(out[29 + i], key[ 9 + i]);
out[34 + i] = XOR(out[30 + i], key[10 + i]);
out[35 + i] = XOR(out[31 + i], key[11 + i]);
out[36 + i] = XOR(out[32 + i], key[12 + i]);
out[37 + i] = XOR(out[33 + i], key[13 + i]);
out[38 + i] = XOR(out[34 + i], key[14 + i]);
out[39 + i] = XOR(out[35 + i], key[15 + i]);
out[40 + i] = XOR(out[36 + i], key[16 + i]);
out[41 + i] = XOR(out[37 + i], key[17 + i]);
out[42 + i] = XOR(out[38 + i], key[18 + i]);
out[43 + i] = XOR(out[39 + i], key[19 + i]);
out[44 + i] = XOR(out[40 + i], key[20 + i]);
out[45 + i] = XOR(out[41 + i], key[21 + i]);
out[46 + i] = XOR(out[42 + i], key[22 + i]);
out[47 + i] = XOR(out[43 + i], key[23 + i]);
out[48 + i] = XOR(out[44 + i], key[24 + i]);
return out;
end
local keyExpand = function(key)
local bytes = Array.copy(key);
for i = 1, 8 do
keyRound(bytes, i);
end
local keys = {};
keys[ 1] = Array.slice(bytes, 1, 16);
keys[ 2] = Array.slice(bytes, 17, 32);
keys[ 3] = Array.slice(bytes, 33, 48);
keys[ 4] = Array.slice(bytes, 49, 64);
keys[ 5] = Array.slice(bytes, 65, 80);
keys[ 6] = Array.slice(bytes, 81, 96);
keys[ 7] = Array.slice(bytes, 97, 112);
keys[ 8] = Array.slice(bytes, 113, 128);
keys[ 9] = Array.slice(bytes, 129, 144);
keys[10] = Array.slice(bytes, 145, 160);
keys[11] = Array.slice(bytes, 161, 176);
keys[12] = Array.slice(bytes, 177, 192);
keys[13] = Array.slice(bytes, 193, 208);
return keys;
end
local addKey = Array.XOR;
local AES = {};
AES.blockSize = 16;
AES.encrypt = function(_key, block)
local key = keyExpand(_key);
--round 0
block = addKey(block, key[1]);
--round 1
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[2]);
--round 2
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[3]);
--round 3
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[4]);
--round 4
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[5]);
--round 5
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[6]);
--round 6
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[7]);
--round 7
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[8]);
--round 8
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[9]);
--round 9
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[10]);
--round 10
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[11]);
--round 11
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[12]);
--round 12
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = addKey(block, key[13]);
return block;
end
AES.decrypt = function(_key, block)
local key = keyExpand(_key);
--round 0
block = addKey(block, key[13]);
--round 1
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[12]);
block = mixCol(block, IMIXTABLE);
--round 2
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[11]);
block = mixCol(block, IMIXTABLE);
--round 3
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[10]);
block = mixCol(block, IMIXTABLE);
--round 4
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[9]);
block = mixCol(block, IMIXTABLE);
--round 5
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[8]);
block = mixCol(block, IMIXTABLE);
--round 6
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[7]);
block = mixCol(block, IMIXTABLE);
--round 7
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[6]);
block = mixCol(block, IMIXTABLE);
--round 8
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[5]);
block = mixCol(block, IMIXTABLE);
--round 9
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[4]);
block = mixCol(block, IMIXTABLE);
--round 10
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[3]);
block = mixCol(block, IMIXTABLE);
--round 11
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[2]);
block = mixCol(block, IMIXTABLE);
--round 12
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[1]);
return block;
end
return AES;

View File

@@ -1,498 +0,0 @@
local Array = require("lockbox.util.array");
local Bit = require("lockbox.util.bit");
local XOR = Bit.bxor;
local SBOX = {
[0] = 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16};
local ISBOX = {
[0] = 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D};
local ROW_SHIFT = { 1, 6, 11, 16, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, };
local IROW_SHIFT = { 1, 14, 11, 8, 5, 2, 15, 12, 9, 6, 3, 16, 13, 10, 7, 4, };
local ETABLE = {
[0] = 0x01, 0x03, 0x05, 0x0F, 0x11, 0x33, 0x55, 0xFF, 0x1A, 0x2E, 0x72, 0x96, 0xA1, 0xF8, 0x13, 0x35,
0x5F, 0xE1, 0x38, 0x48, 0xD8, 0x73, 0x95, 0xA4, 0xF7, 0x02, 0x06, 0x0A, 0x1E, 0x22, 0x66, 0xAA,
0xE5, 0x34, 0x5C, 0xE4, 0x37, 0x59, 0xEB, 0x26, 0x6A, 0xBE, 0xD9, 0x70, 0x90, 0xAB, 0xE6, 0x31,
0x53, 0xF5, 0x04, 0x0C, 0x14, 0x3C, 0x44, 0xCC, 0x4F, 0xD1, 0x68, 0xB8, 0xD3, 0x6E, 0xB2, 0xCD,
0x4C, 0xD4, 0x67, 0xA9, 0xE0, 0x3B, 0x4D, 0xD7, 0x62, 0xA6, 0xF1, 0x08, 0x18, 0x28, 0x78, 0x88,
0x83, 0x9E, 0xB9, 0xD0, 0x6B, 0xBD, 0xDC, 0x7F, 0x81, 0x98, 0xB3, 0xCE, 0x49, 0xDB, 0x76, 0x9A,
0xB5, 0xC4, 0x57, 0xF9, 0x10, 0x30, 0x50, 0xF0, 0x0B, 0x1D, 0x27, 0x69, 0xBB, 0xD6, 0x61, 0xA3,
0xFE, 0x19, 0x2B, 0x7D, 0x87, 0x92, 0xAD, 0xEC, 0x2F, 0x71, 0x93, 0xAE, 0xE9, 0x20, 0x60, 0xA0,
0xFB, 0x16, 0x3A, 0x4E, 0xD2, 0x6D, 0xB7, 0xC2, 0x5D, 0xE7, 0x32, 0x56, 0xFA, 0x15, 0x3F, 0x41,
0xC3, 0x5E, 0xE2, 0x3D, 0x47, 0xC9, 0x40, 0xC0, 0x5B, 0xED, 0x2C, 0x74, 0x9C, 0xBF, 0xDA, 0x75,
0x9F, 0xBA, 0xD5, 0x64, 0xAC, 0xEF, 0x2A, 0x7E, 0x82, 0x9D, 0xBC, 0xDF, 0x7A, 0x8E, 0x89, 0x80,
0x9B, 0xB6, 0xC1, 0x58, 0xE8, 0x23, 0x65, 0xAF, 0xEA, 0x25, 0x6F, 0xB1, 0xC8, 0x43, 0xC5, 0x54,
0xFC, 0x1F, 0x21, 0x63, 0xA5, 0xF4, 0x07, 0x09, 0x1B, 0x2D, 0x77, 0x99, 0xB0, 0xCB, 0x46, 0xCA,
0x45, 0xCF, 0x4A, 0xDE, 0x79, 0x8B, 0x86, 0x91, 0xA8, 0xE3, 0x3E, 0x42, 0xC6, 0x51, 0xF3, 0x0E,
0x12, 0x36, 0x5A, 0xEE, 0x29, 0x7B, 0x8D, 0x8C, 0x8F, 0x8A, 0x85, 0x94, 0xA7, 0xF2, 0x0D, 0x17,
0x39, 0x4B, 0xDD, 0x7C, 0x84, 0x97, 0xA2, 0xFD, 0x1C, 0x24, 0x6C, 0xB4, 0xC7, 0x52, 0xF6, 0x01};
local LTABLE = {
[0] = 0x00, 0x00, 0x19, 0x01, 0x32, 0x02, 0x1A, 0xC6, 0x4B, 0xC7, 0x1B, 0x68, 0x33, 0xEE, 0xDF, 0x03,
0x64, 0x04, 0xE0, 0x0E, 0x34, 0x8D, 0x81, 0xEF, 0x4C, 0x71, 0x08, 0xC8, 0xF8, 0x69, 0x1C, 0xC1,
0x7D, 0xC2, 0x1D, 0xB5, 0xF9, 0xB9, 0x27, 0x6A, 0x4D, 0xE4, 0xA6, 0x72, 0x9A, 0xC9, 0x09, 0x78,
0x65, 0x2F, 0x8A, 0x05, 0x21, 0x0F, 0xE1, 0x24, 0x12, 0xF0, 0x82, 0x45, 0x35, 0x93, 0xDA, 0x8E,
0x96, 0x8F, 0xDB, 0xBD, 0x36, 0xD0, 0xCE, 0x94, 0x13, 0x5C, 0xD2, 0xF1, 0x40, 0x46, 0x83, 0x38,
0x66, 0xDD, 0xFD, 0x30, 0xBF, 0x06, 0x8B, 0x62, 0xB3, 0x25, 0xE2, 0x98, 0x22, 0x88, 0x91, 0x10,
0x7E, 0x6E, 0x48, 0xC3, 0xA3, 0xB6, 0x1E, 0x42, 0x3A, 0x6B, 0x28, 0x54, 0xFA, 0x85, 0x3D, 0xBA,
0x2B, 0x79, 0x0A, 0x15, 0x9B, 0x9F, 0x5E, 0xCA, 0x4E, 0xD4, 0xAC, 0xE5, 0xF3, 0x73, 0xA7, 0x57,
0xAF, 0x58, 0xA8, 0x50, 0xF4, 0xEA, 0xD6, 0x74, 0x4F, 0xAE, 0xE9, 0xD5, 0xE7, 0xE6, 0xAD, 0xE8,
0x2C, 0xD7, 0x75, 0x7A, 0xEB, 0x16, 0x0B, 0xF5, 0x59, 0xCB, 0x5F, 0xB0, 0x9C, 0xA9, 0x51, 0xA0,
0x7F, 0x0C, 0xF6, 0x6F, 0x17, 0xC4, 0x49, 0xEC, 0xD8, 0x43, 0x1F, 0x2D, 0xA4, 0x76, 0x7B, 0xB7,
0xCC, 0xBB, 0x3E, 0x5A, 0xFB, 0x60, 0xB1, 0x86, 0x3B, 0x52, 0xA1, 0x6C, 0xAA, 0x55, 0x29, 0x9D,
0x97, 0xB2, 0x87, 0x90, 0x61, 0xBE, 0xDC, 0xFC, 0xBC, 0x95, 0xCF, 0xCD, 0x37, 0x3F, 0x5B, 0xD1,
0x53, 0x39, 0x84, 0x3C, 0x41, 0xA2, 0x6D, 0x47, 0x14, 0x2A, 0x9E, 0x5D, 0x56, 0xF2, 0xD3, 0xAB,
0x44, 0x11, 0x92, 0xD9, 0x23, 0x20, 0x2E, 0x89, 0xB4, 0x7C, 0xB8, 0x26, 0x77, 0x99, 0xE3, 0xA5,
0x67, 0x4A, 0xED, 0xDE, 0xC5, 0x31, 0xFE, 0x18, 0x0D, 0x63, 0x8C, 0x80, 0xC0, 0xF7, 0x70, 0x07};
local MIXTABLE = {
0x02, 0x03, 0x01, 0x01,
0x01, 0x02, 0x03, 0x01,
0x01, 0x01, 0x02, 0x03,
0x03, 0x01, 0x01, 0x02};
local IMIXTABLE = {
0x0E, 0x0B, 0x0D, 0x09,
0x09, 0x0E, 0x0B, 0x0D,
0x0D, 0x09, 0x0E, 0x0B,
0x0B, 0x0D, 0x09, 0x0E};
local RCON = {
[0] = 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a,
0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39,
0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a,
0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8,
0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef,
0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc,
0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b,
0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3,
0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94,
0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20,
0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35,
0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f,
0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04,
0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63,
0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd,
0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d};
local GMUL = function(A, B)
if(A == 0x01) then return B; end
if(B == 0x01) then return A; end
if(A == 0x00) then return 0; end
if(B == 0x00) then return 0; end
local LA = LTABLE[A];
local LB = LTABLE[B];
local sum = LA + LB;
if (sum > 0xFF) then sum = sum - 0xFF; end
return ETABLE[sum];
end
local byteSub = Array.substitute;
local shiftRow = Array.permute;
local mixCol = function(i, mix)
local out = {};
local a, b, c, d;
a = GMUL(i[ 1], mix[ 1]);
b = GMUL(i[ 2], mix[ 2]);
c = GMUL(i[ 3], mix[ 3]);
d = GMUL(i[ 4], mix[ 4]);
out[ 1] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[ 5]);
b = GMUL(i[ 2], mix[ 6]);
c = GMUL(i[ 3], mix[ 7]);
d = GMUL(i[ 4], mix[ 8]);
out[ 2] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[ 9]);
b = GMUL(i[ 2], mix[10]);
c = GMUL(i[ 3], mix[11]);
d = GMUL(i[ 4], mix[12]);
out[ 3] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[13]);
b = GMUL(i[ 2], mix[14]);
c = GMUL(i[ 3], mix[15]);
d = GMUL(i[ 4], mix[16]);
out[ 4] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 1]);
b = GMUL(i[ 6], mix[ 2]);
c = GMUL(i[ 7], mix[ 3]);
d = GMUL(i[ 8], mix[ 4]);
out[ 5] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 5]);
b = GMUL(i[ 6], mix[ 6]);
c = GMUL(i[ 7], mix[ 7]);
d = GMUL(i[ 8], mix[ 8]);
out[ 6] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 9]);
b = GMUL(i[ 6], mix[10]);
c = GMUL(i[ 7], mix[11]);
d = GMUL(i[ 8], mix[12]);
out[ 7] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[13]);
b = GMUL(i[ 6], mix[14]);
c = GMUL(i[ 7], mix[15]);
d = GMUL(i[ 8], mix[16]);
out[ 8] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 1]);
b = GMUL(i[10], mix[ 2]);
c = GMUL(i[11], mix[ 3]);
d = GMUL(i[12], mix[ 4]);
out[ 9] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 5]);
b = GMUL(i[10], mix[ 6]);
c = GMUL(i[11], mix[ 7]);
d = GMUL(i[12], mix[ 8]);
out[10] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 9]);
b = GMUL(i[10], mix[10]);
c = GMUL(i[11], mix[11]);
d = GMUL(i[12], mix[12]);
out[11] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[13]);
b = GMUL(i[10], mix[14]);
c = GMUL(i[11], mix[15]);
d = GMUL(i[12], mix[16]);
out[12] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 1]);
b = GMUL(i[14], mix[ 2]);
c = GMUL(i[15], mix[ 3]);
d = GMUL(i[16], mix[ 4]);
out[13] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 5]);
b = GMUL(i[14], mix[ 6]);
c = GMUL(i[15], mix[ 7]);
d = GMUL(i[16], mix[ 8]);
out[14] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 9]);
b = GMUL(i[14], mix[10]);
c = GMUL(i[15], mix[11]);
d = GMUL(i[16], mix[12]);
out[15] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[13]);
b = GMUL(i[14], mix[14]);
c = GMUL(i[15], mix[15]);
d = GMUL(i[16], mix[16]);
out[16] = XOR(XOR(a, b), XOR(c, d));
return out;
end
local keyRound = function(key, round)
local i = (round - 1) * 32;
local out = key;
out[33 + i] = XOR(key[ 1 + i], XOR(SBOX[key[30 + i]], RCON[round]));
out[34 + i] = XOR(key[ 2 + i], SBOX[key[31 + i]]);
out[35 + i] = XOR(key[ 3 + i], SBOX[key[32 + i]]);
out[36 + i] = XOR(key[ 4 + i], SBOX[key[29 + i]]);
out[37 + i] = XOR(out[33 + i], key[ 5 + i]);
out[38 + i] = XOR(out[34 + i], key[ 6 + i]);
out[39 + i] = XOR(out[35 + i], key[ 7 + i]);
out[40 + i] = XOR(out[36 + i], key[ 8 + i]);
out[41 + i] = XOR(out[37 + i], key[ 9 + i]);
out[42 + i] = XOR(out[38 + i], key[10 + i]);
out[43 + i] = XOR(out[39 + i], key[11 + i]);
out[44 + i] = XOR(out[40 + i], key[12 + i]);
out[45 + i] = XOR(out[41 + i], key[13 + i]);
out[46 + i] = XOR(out[42 + i], key[14 + i]);
out[47 + i] = XOR(out[43 + i], key[15 + i]);
out[48 + i] = XOR(out[44 + i], key[16 + i]);
out[49 + i] = XOR(SBOX[out[45 + i]], key[17 + i]);
out[50 + i] = XOR(SBOX[out[46 + i]], key[18 + i]);
out[51 + i] = XOR(SBOX[out[47 + i]], key[19 + i]);
out[52 + i] = XOR(SBOX[out[48 + i]], key[20 + i]);
out[53 + i] = XOR(out[49 + i], key[21 + i]);
out[54 + i] = XOR(out[50 + i], key[22 + i]);
out[55 + i] = XOR(out[51 + i], key[23 + i]);
out[56 + i] = XOR(out[52 + i], key[24 + i]);
out[57 + i] = XOR(out[53 + i], key[25 + i]);
out[58 + i] = XOR(out[54 + i], key[26 + i]);
out[59 + i] = XOR(out[55 + i], key[27 + i]);
out[60 + i] = XOR(out[56 + i], key[28 + i]);
out[61 + i] = XOR(out[57 + i], key[29 + i]);
out[62 + i] = XOR(out[58 + i], key[30 + i]);
out[63 + i] = XOR(out[59 + i], key[31 + i]);
out[64 + i] = XOR(out[60 + i], key[32 + i]);
return out;
end
local keyExpand = function(key)
local bytes = Array.copy(key);
for i = 1, 7 do
keyRound(bytes, i);
end
local keys = {};
keys[ 1] = Array.slice(bytes, 1, 16);
keys[ 2] = Array.slice(bytes, 17, 32);
keys[ 3] = Array.slice(bytes, 33, 48);
keys[ 4] = Array.slice(bytes, 49, 64);
keys[ 5] = Array.slice(bytes, 65, 80);
keys[ 6] = Array.slice(bytes, 81, 96);
keys[ 7] = Array.slice(bytes, 97, 112);
keys[ 8] = Array.slice(bytes, 113, 128);
keys[ 9] = Array.slice(bytes, 129, 144);
keys[10] = Array.slice(bytes, 145, 160);
keys[11] = Array.slice(bytes, 161, 176);
keys[12] = Array.slice(bytes, 177, 192);
keys[13] = Array.slice(bytes, 193, 208);
keys[14] = Array.slice(bytes, 209, 224);
keys[15] = Array.slice(bytes, 225, 240);
return keys;
end
local addKey = Array.XOR;
local AES = {};
AES.blockSize = 16;
AES.encrypt = function(_key, block)
local key = keyExpand(_key);
--round 0
block = addKey(block, key[1]);
--round 1
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[2]);
--round 2
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[3]);
--round 3
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[4]);
--round 4
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[5]);
--round 5
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[6]);
--round 6
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[7]);
--round 7
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[8]);
--round 8
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[9]);
--round 9
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[10]);
--round 10
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[11]);
--round 11
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[12]);
--round 12
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[13]);
--round 13
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[14]);
--round 14
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = addKey(block, key[15]);
return block;
end
AES.decrypt = function(_key, block)
local key = keyExpand(_key);
--round 0
block = addKey(block, key[15]);
--round 1
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[14]);
block = mixCol(block, IMIXTABLE);
--round 2
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[13]);
block = mixCol(block, IMIXTABLE);
--round 3
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[12]);
block = mixCol(block, IMIXTABLE);
--round 4
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[11]);
block = mixCol(block, IMIXTABLE);
--round 5
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[10]);
block = mixCol(block, IMIXTABLE);
--round 6
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[9]);
block = mixCol(block, IMIXTABLE);
--round 7
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[8]);
block = mixCol(block, IMIXTABLE);
--round 8
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[7]);
block = mixCol(block, IMIXTABLE);
--round 9
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[6]);
block = mixCol(block, IMIXTABLE);
--round 10
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[5]);
block = mixCol(block, IMIXTABLE);
--round 11
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[4]);
block = mixCol(block, IMIXTABLE);
--round 12
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[3]);
block = mixCol(block, IMIXTABLE);
--round 13
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[2]);
block = mixCol(block, IMIXTABLE);
--round 14
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[1]);
return block;
end
return AES;

View File

@@ -1,164 +0,0 @@
local Array = require("lockbox.util.array");
local Stream = require("lockbox.util.stream");
local Queue = require("lockbox.util.queue");
local CBC = {};
CBC.Cipher = function()
local public = {};
local key;
local blockCipher;
local padding;
local inputQueue;
local outputQueue;
local iv;
public.setKey = function(keyBytes)
key = keyBytes;
return public;
end
public.setBlockCipher = function(cipher)
blockCipher = cipher;
return public;
end
public.setPadding = function(paddingMode)
padding = paddingMode;
return public;
end
public.init = function()
inputQueue = Queue();
outputQueue = Queue();
iv = nil;
return public;
end
public.update = function(messageStream)
local byte = messageStream();
while (byte ~= nil) do
inputQueue.push(byte);
if(inputQueue.size() >= blockCipher.blockSize) then
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
if(iv == nil) then
iv = block;
else
local out = Array.XOR(iv, block);
out = blockCipher.encrypt(key, out);
Array.writeToQueue(outputQueue, out);
iv = out;
end
end
byte = messageStream();
end
return public;
end
public.finish = function()
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
public.update(paddingStream);
return public;
end
public.getOutputQueue = function()
return outputQueue;
end
public.asHex = function()
return Stream.toHex(outputQueue.pop);
end
public.asBytes = function()
return Stream.toArray(outputQueue.pop);
end
return public;
end
CBC.Decipher = function()
local public = {};
local key;
local blockCipher;
local padding;
local inputQueue;
local outputQueue;
local iv;
public.setKey = function(keyBytes)
key = keyBytes;
return public;
end
public.setBlockCipher = function(cipher)
blockCipher = cipher;
return public;
end
public.setPadding = function(paddingMode)
padding = paddingMode;
return public;
end
public.init = function()
inputQueue = Queue();
outputQueue = Queue();
iv = nil;
return public;
end
public.update = function(messageStream)
local byte = messageStream();
while (byte ~= nil) do
inputQueue.push(byte);
if(inputQueue.size() >= blockCipher.blockSize) then
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
if(iv == nil) then
iv = block;
else
local out = block;
out = blockCipher.decrypt(key, out);
out = Array.XOR(iv, out);
Array.writeToQueue(outputQueue, out);
iv = block;
end
end
byte = messageStream();
end
return public;
end
public.finish = function()
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
public.update(paddingStream);
return public;
end
public.getOutputQueue = function()
return outputQueue;
end
public.asHex = function()
return Stream.toHex(outputQueue.pop);
end
public.asBytes = function()
return Stream.toArray(outputQueue.pop);
end
return public;
end
return CBC;

View File

@@ -1,163 +0,0 @@
local Array = require("lockbox.util.array");
local Stream = require("lockbox.util.stream");
local Queue = require("lockbox.util.queue");
local CFB = {};
CFB.Cipher = function()
local public = {};
local key;
local blockCipher;
local padding;
local inputQueue;
local outputQueue;
local iv;
public.setKey = function(keyBytes)
key = keyBytes;
return public;
end
public.setBlockCipher = function(cipher)
blockCipher = cipher;
return public;
end
public.setPadding = function(paddingMode)
padding = paddingMode;
return public;
end
public.init = function()
inputQueue = Queue();
outputQueue = Queue();
iv = nil;
return public;
end
public.update = function(messageStream)
local byte = messageStream();
while (byte ~= nil) do
inputQueue.push(byte);
if(inputQueue.size() >= blockCipher.blockSize) then
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
if(iv == nil) then
iv = block;
else
local out = iv;
out = blockCipher.encrypt(key, out);
out = Array.XOR(out, block);
Array.writeToQueue(outputQueue, out);
iv = out;
end
end
byte = messageStream();
end
return public;
end
public.finish = function()
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
public.update(paddingStream);
return public;
end
public.getOutputQueue = function()
return outputQueue;
end
public.asHex = function()
return Stream.toHex(outputQueue.pop);
end
public.asBytes = function()
return Stream.toArray(outputQueue.pop);
end
return public;
end
CFB.Decipher = function()
local public = {};
local key;
local blockCipher;
local padding;
local inputQueue;
local outputQueue;
local iv;
public.setKey = function(keyBytes)
key = keyBytes;
return public;
end
public.setBlockCipher = function(cipher)
blockCipher = cipher;
return public;
end
public.setPadding = function(paddingMode)
padding = paddingMode;
return public;
end
public.init = function()
inputQueue = Queue();
outputQueue = Queue();
iv = nil;
return public;
end
public.update = function(messageStream)
local byte = messageStream();
while (byte ~= nil) do
inputQueue.push(byte);
if(inputQueue.size() >= blockCipher.blockSize) then
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
if(iv == nil) then
iv = block;
else
local out = iv;
out = blockCipher.encrypt(key, out);
out = Array.XOR(out, block);
Array.writeToQueue(outputQueue, out);
iv = block;
end
end
byte = messageStream();
end
return public;
end
public.finish = function()
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
public.update(paddingStream);
return public;
end
public.getOutputQueue = function()
return outputQueue;
end
public.asHex = function()
return Stream.toHex(outputQueue.pop);
end
public.asBytes = function()
return Stream.toArray(outputQueue.pop);
end
return public;
end
return CFB;

View File

@@ -1,248 +0,0 @@
local Array = require("lockbox.util.array");
local Stream = require("lockbox.util.stream");
local Queue = require("lockbox.util.queue");
local Bit = require("lockbox.util.bit");
local AND = Bit.band;
local CTR = {};
CTR.Cipher = function()
local public = {};
local key;
local blockCipher;
local padding;
local inputQueue;
local outputQueue;
local iv;
public.setKey = function(keyBytes)
key = keyBytes;
return public;
end
public.setBlockCipher = function(cipher)
blockCipher = cipher;
return public;
end
public.setPadding = function(paddingMode)
padding = paddingMode;
return public;
end
public.init = function()
inputQueue = Queue();
outputQueue = Queue();
iv = nil;
return public;
end
local updateIV = function()
iv[16] = iv[16] + 1;
if iv[16] <= 0xFF then return; end
iv[16] = AND(iv[16], 0xFF);
iv[15] = iv[15] + 1;
if iv[15] <= 0xFF then return; end
iv[15] = AND(iv[15], 0xFF);
iv[14] = iv[14] + 1;
if iv[14] <= 0xFF then return; end
iv[14] = AND(iv[14], 0xFF);
iv[13] = iv[13] + 1;
if iv[13] <= 0xFF then return; end
iv[13] = AND(iv[13], 0xFF);
iv[12] = iv[12] + 1;
if iv[12] <= 0xFF then return; end
iv[12] = AND(iv[12], 0xFF);
iv[11] = iv[11] + 1;
if iv[11] <= 0xFF then return; end
iv[11] = AND(iv[11], 0xFF);
iv[10] = iv[10] + 1;
if iv[10] <= 0xFF then return; end
iv[10] = AND(iv[10], 0xFF);
iv[9] = iv[9] + 1;
if iv[9] <= 0xFF then return; end
iv[9] = AND(iv[9], 0xFF);
return;
end
public.update = function(messageStream)
local byte = messageStream();
while (byte ~= nil) do
inputQueue.push(byte);
if(inputQueue.size() >= blockCipher.blockSize) then
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
if(iv == nil) then
iv = block;
else
local out = iv;
out = blockCipher.encrypt(key, out);
out = Array.XOR(out, block);
Array.writeToQueue(outputQueue, out);
updateIV();
end
end
byte = messageStream();
end
return public;
end
public.finish = function()
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
public.update(paddingStream);
return public;
end
public.getOutputQueue = function()
return outputQueue;
end
public.asHex = function()
return Stream.toHex(outputQueue.pop);
end
public.asBytes = function()
return Stream.toArray(outputQueue.pop);
end
return public;
end
CTR.Decipher = function()
local public = {};
local key;
local blockCipher;
local padding;
local inputQueue;
local outputQueue;
local iv;
public.setKey = function(keyBytes)
key = keyBytes;
return public;
end
public.setBlockCipher = function(cipher)
blockCipher = cipher;
return public;
end
public.setPadding = function(paddingMode)
padding = paddingMode;
return public;
end
public.init = function()
inputQueue = Queue();
outputQueue = Queue();
iv = nil;
return public;
end
local updateIV = function()
iv[16] = iv[16] + 1;
if iv[16] <= 0xFF then return; end
iv[16] = AND(iv[16], 0xFF);
iv[15] = iv[15] + 1;
if iv[15] <= 0xFF then return; end
iv[15] = AND(iv[15], 0xFF);
iv[14] = iv[14] + 1;
if iv[14] <= 0xFF then return; end
iv[14] = AND(iv[14], 0xFF);
iv[13] = iv[13] + 1;
if iv[13] <= 0xFF then return; end
iv[13] = AND(iv[13], 0xFF);
iv[12] = iv[12] + 1;
if iv[12] <= 0xFF then return; end
iv[12] = AND(iv[12], 0xFF);
iv[11] = iv[11] + 1;
if iv[11] <= 0xFF then return; end
iv[11] = AND(iv[11], 0xFF);
iv[10] = iv[10] + 1;
if iv[10] <= 0xFF then return; end
iv[10] = AND(iv[10], 0xFF);
iv[9] = iv[9] + 1;
if iv[9] <= 0xFF then return; end
iv[9] = AND(iv[9], 0xFF);
return;
end
public.update = function(messageStream)
local byte = messageStream();
while (byte ~= nil) do
inputQueue.push(byte);
if(inputQueue.size() >= blockCipher.blockSize) then
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
if(iv == nil) then
iv = block;
else
local out = iv;
out = blockCipher.encrypt(key, out);
out = Array.XOR(out, block);
Array.writeToQueue(outputQueue, out);
updateIV();
end
end
byte = messageStream();
end
return public;
end
public.finish = function()
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
public.update(paddingStream);
return public;
end
public.getOutputQueue = function()
return outputQueue;
end
public.asHex = function()
return Stream.toHex(outputQueue.pop);
end
public.asBytes = function()
return Stream.toArray(outputQueue.pop);
end
return public;
end
return CTR;

View File

@@ -1,164 +0,0 @@
local Array = require("lockbox.util.array");
local Stream = require("lockbox.util.stream");
local Queue = require("lockbox.util.queue");
local OFB = {};
OFB.Cipher = function()
local public = {};
local key;
local blockCipher;
local padding;
local inputQueue;
local outputQueue;
local iv;
public.setKey = function(keyBytes)
key = keyBytes;
return public;
end
public.setBlockCipher = function(cipher)
blockCipher = cipher;
return public;
end
public.setPadding = function(paddingMode)
padding = paddingMode;
return public;
end
public.init = function()
inputQueue = Queue();
outputQueue = Queue();
iv = nil;
return public;
end
public.update = function(messageStream)
local byte = messageStream();
while (byte ~= nil) do
inputQueue.push(byte);
if(inputQueue.size() >= blockCipher.blockSize) then
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
if(iv == nil) then
iv = block;
else
local out = iv;
out = blockCipher.encrypt(key, out);
iv = out;
out = Array.XOR(out, block);
Array.writeToQueue(outputQueue, out);
end
end
byte = messageStream();
end
return public;
end
public.finish = function()
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
public.update(paddingStream);
return public;
end
public.getOutputQueue = function()
return outputQueue;
end
public.asHex = function()
return Stream.toHex(outputQueue.pop);
end
public.asBytes = function()
return Stream.toArray(outputQueue.pop);
end
return public;
end
OFB.Decipher = function()
local public = {};
local key;
local blockCipher;
local padding;
local inputQueue;
local outputQueue;
local iv;
public.setKey = function(keyBytes)
key = keyBytes;
return public;
end
public.setBlockCipher = function(cipher)
blockCipher = cipher;
return public;
end
public.setPadding = function(paddingMode)
padding = paddingMode;
return public;
end
public.init = function()
inputQueue = Queue();
outputQueue = Queue();
iv = nil;
return public;
end
public.update = function(messageStream)
local byte = messageStream();
while (byte ~= nil) do
inputQueue.push(byte);
if(inputQueue.size() >= blockCipher.blockSize) then
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
if(iv == nil) then
iv = block;
else
local out = iv;
out = blockCipher.encrypt(key, out);
iv = out;
out = Array.XOR(out, block);
Array.writeToQueue(outputQueue, out);
end
end
byte = messageStream();
end
return public;
end
public.finish = function()
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
public.update(paddingStream);
return public;
end
public.getOutputQueue = function()
return outputQueue;
end
public.asHex = function()
return Stream.toHex(outputQueue.pop);
end
public.asBytes = function()
return Stream.toArray(outputQueue.pop);
end
return public;
end
return OFB;

201
lockbox/digest/md5.lua Normal file
View File

@@ -0,0 +1,201 @@
require("lockbox").insecure();
local Bit = require("lockbox.util.bit");
local String = require("string");
local Math = require("math");
local Queue = require("lockbox.util.queue");
local SHIFT = {
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21};
local CONSTANTS = {
0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391};
local AND = Bit.band;
local OR = Bit.bor;
local NOT = Bit.bnot;
local XOR = Bit.bxor;
local LROT = Bit.lrotate;
local LSHIFT = Bit.lshift;
local RSHIFT = Bit.rshift;
--MD5 is little-endian
local bytes2word = function(b0, b1, b2, b3)
local i = b3; i = LSHIFT(i, 8);
i = OR(i, b2); i = LSHIFT(i, 8);
i = OR(i, b1); i = LSHIFT(i, 8);
i = OR(i, b0);
return i;
end
local word2bytes = function(word)
local b0, b1, b2, b3;
b0 = AND(word, 0xFF); word = RSHIFT(word, 8);
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
b3 = AND(word, 0xFF);
return b0, b1, b2, b3;
end
local dword2bytes = function(i)
local b4, b5, b6, b7 = word2bytes(Math.floor(i / 0x100000000));
local b0, b1, b2, b3 = word2bytes(i);
return b0, b1, b2, b3, b4, b5, b6, b7;
end
local F = function(x, y, z) return OR(AND(x, y), AND(NOT(x), z)); end
local G = function(x, y, z) return OR(AND(x, z), AND(y, NOT(z))); end
local H = function(x, y, z) return XOR(x, XOR(y, z)); end
local I = function(x, y, z) return XOR(y, OR(x, NOT(z))); end
local MD5 = function()
local queue = Queue();
local A = 0x67452301;
local B = 0xefcdab89;
local C = 0x98badcfe;
local D = 0x10325476;
local public = {};
local processBlock = function()
local a = A;
local b = B;
local c = C;
local d = D;
local X = {};
for i = 1, 16 do
X[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
end
for i = 0, 63 do
local f, g, temp;
if (0 <= i) and (i <= 15) then
f = F(b, c, d);
g = i;
elseif (16 <= i) and (i <= 31) then
f = G(b, c, d);
g = (5 * i + 1) % 16;
elseif (32 <= i) and (i <= 47) then
f = H(b, c, d);
g = (3 * i + 5) % 16;
elseif (48 <= i) and (i <= 63) then
f = I(b, c, d);
g = (7 * i) % 16;
end
temp = d;
d = c;
c = b;
b = b + LROT((a + f + CONSTANTS[i + 1] + X[g + 1]), SHIFT[i + 1]);
a = temp;
end
A = AND(A + a, 0xFFFFFFFF);
B = AND(B + b, 0xFFFFFFFF);
C = AND(C + c, 0xFFFFFFFF);
D = AND(D + d, 0xFFFFFFFF);
end
public.init = function()
queue.reset();
A = 0x67452301;
B = 0xefcdab89;
C = 0x98badcfe;
D = 0x10325476;
return public;
end
public.update = function(bytes)
for b in bytes do
queue.push(b);
if(queue.size() >= 64) then processBlock(); end
end
return public;
end
public.finish = function()
local bits = queue.getHead() * 8;
queue.push(0x80);
while ((queue.size() + 7) % 64) < 63 do
queue.push(0x00);
end
local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits);
queue.push(b0);
queue.push(b1);
queue.push(b2);
queue.push(b3);
queue.push(b4);
queue.push(b5);
queue.push(b6);
queue.push(b7);
while queue.size() > 0 do
processBlock();
end
return public;
end
public.asBytes = function()
local b0, b1, b2, b3 = word2bytes(A);
local b4, b5, b6, b7 = word2bytes(B);
local b8, b9, b10, b11 = word2bytes(C);
local b12, b13, b14, b15 = word2bytes(D);
return {b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15};
end
public.asHex = function()
local b0, b1, b2, b3 = word2bytes(A);
local b4, b5, b6, b7 = word2bytes(B);
local b8, b9, b10, b11 = word2bytes(C);
local b12, b13, b14, b15 = word2bytes(D);
return String.format("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15);
end
public.asString = function()
local b0, b1, b2, b3 = word2bytes(A);
local b4, b5, b6, b7 = word2bytes(B);
local b8, b9, b10, b11 = word2bytes(C);
local b12, b13, b14, b15 = word2bytes(D);
return string.pack(string.rep('B', 16),
b0, b1, b2, b3, b4, b5, b6, b7, b8,
b9, b10, b11, b12, b13, b14, b15
)
end
return public;
end
return MD5;

View File

@@ -1,5 +1,8 @@
local Lockbox = {}; local Lockbox = {};
-- cc-mek-scada lockbox version
Lockbox.version = "1.0"
--[[ --[[
package.path = "./?.lua;" package.path = "./?.lua;"
.. "./cipher/?.lua;" .. "./cipher/?.lua;"

View File

@@ -1,22 +0,0 @@
local ANSIX923Padding = function(blockSize, byteCount)
local paddingCount = blockSize - (byteCount % blockSize);
local bytesLeft = paddingCount;
local stream = function()
if bytesLeft > 1 then
bytesLeft = bytesLeft - 1;
return 0x00;
elseif bytesLeft > 0 then
bytesLeft = bytesLeft - 1;
return paddingCount;
else
return nil;
end
end
return stream;
end
return ANSIX923Padding;

View File

@@ -1,22 +0,0 @@
local ISOIEC7816Padding = function(blockSize, byteCount)
local paddingCount = blockSize - (byteCount % blockSize);
local bytesLeft = paddingCount;
local stream = function()
if bytesLeft == paddingCount then
bytesLeft = bytesLeft - 1;
return 0x80;
elseif bytesLeft > 0 then
bytesLeft = bytesLeft - 1;
return 0x00;
else
return nil;
end
end
return stream;
end
return ISOIEC7816Padding;

View File

@@ -1,18 +0,0 @@
local PKCS7Padding = function(blockSize, byteCount)
local paddingCount = blockSize - ((byteCount -1) % blockSize) + 1;
local bytesLeft = paddingCount;
local stream = function()
if bytesLeft > 0 then
bytesLeft = bytesLeft - 1;
return paddingCount;
else
return nil;
end
end
return stream;
end
return PKCS7Padding;

View File

@@ -1,19 +0,0 @@
local ZeroPadding = function(blockSize, byteCount)
local paddingCount = blockSize - ((byteCount -1) % blockSize) + 1;
local bytesLeft = paddingCount;
local stream = function()
if bytesLeft > 0 then
bytesLeft = bytesLeft - 1;
return 0x00;
else
return nil;
end
end
return stream;
end
return ZeroPadding;

View File

@@ -1,25 +1,19 @@
local ok, e -- modified (simplified) for ComputerCraft
ok = nil
if not ok then local ok, e = nil, nil
ok, e = pcall(require, "bit") -- the LuaJIT one ?
end
if not ok then if not ok then
ok, e = pcall(require, "bit32") -- Lua 5.2 ok, e = pcall(require, "bit32") -- Lua 5.2
end end
if not ok then if not ok then
ok, e = pcall(require, "bit.numberlua") -- for Lua 5.1, https://github.com/tst2005/lua-bit-numberlua/ ok, e = pcall(require, "bit")
end end
if not ok then if not ok then
error("no bitwise support found", 2) error("no bitwise support found", 2)
end end
assert(type(e) == "table", "invalid bit module") assert(type(e) == "table", "invalid bit module")
-- Workaround to support Lua 5.2 bit32 API with the LuaJIT bit one
if e.rol and not e.lrotate then
e.lrotate = e.rol
end
if e.ror and not e.rrotate then
e.rrotate = e.ror
end
return e return e

View File

@@ -1,15 +1,19 @@
local config = {} local config = {}
-- port of the SCADA supervisor -- supervisor comms channel
config.SCADA_SV_PORT = 16100 config.SVR_CHANNEL = 16240
-- port for SCADA coordinator API access -- coordinator comms channel
config.SCADA_API_PORT = 16200 config.CRD_CHANNEL = 16243
-- port to listen to incoming packets FROM servers -- pocket comms channel
config.LISTEN_PORT = 16201 config.PKT_CHANNEL = 16244
-- max trusted modem message distance (0 to disable check) -- max trusted modem message distance (0 to disable check)
config.TRUSTED_RANGE = 0 config.TRUSTED_RANGE = 0
-- time in seconds (>= 2) before assuming a remote device is no longer active -- time in seconds (>= 2) before assuming a remote device is no longer active
config.COMMS_TIMEOUT = 5 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 -- log path
config.LOG_PATH = "/log.txt" config.LOG_PATH = "/log.txt"
@@ -17,5 +21,7 @@ config.LOG_PATH = "/log.txt"
-- 0 = APPEND (adds to existing file on start) -- 0 = APPEND (adds to existing file on start)
-- 1 = NEW (replaces existing file on start) -- 1 = NEW (replaces existing file on start)
config.LOG_MODE = 0 config.LOG_MODE = 0
-- true to log verbose debug messages
config.LOG_DEBUG = false
return config return config

View File

@@ -1,35 +0,0 @@
--
-- Core I/O - Pocket Central I/O Management
--
local psil = require("scada-common.psil")
local coreio = {}
---@class pocket_core_io
local io = {
ps = psil.create()
}
---@enum POCKET_LINK_STATE
local LINK_STATE = {
UNLINKED = 0,
SV_LINK_ONLY = 1,
API_LINK_ONLY = 2,
LINKED = 3
}
coreio.LINK_STATE = LINK_STATE
-- get the core PSIL
function coreio.core_ps()
return io.ps
end
-- set network link state
---@param state POCKET_LINK_STATE
function coreio.report_link_state(state)
io.ps.publish("link_state", state)
end
return coreio

106
pocket/iocontrol.lua Normal file
View File

@@ -0,0 +1,106 @@
--
-- I/O Control for Pocket Integration with Supervisor & Coordinator
--
local psil = require("scada-common.psil")
local types = require("scada-common.types")
local ALARM = types.ALARM
local iocontrol = {}
---@class pocket_ioctl
local io = {
ps = psil.create()
}
---@enum POCKET_LINK_STATE
local LINK_STATE = {
UNLINKED = 0,
SV_LINK_ONLY = 1,
API_LINK_ONLY = 2,
LINKED = 3
}
---@enum NAV_PAGE
local NAV_PAGE = {
HOME = 1,
UNITS = 2,
REACTORS = 3,
BOILERS = 4,
TURBINES = 5,
DIAG = 6,
D_ALARMS = 7
}
iocontrol.LINK_STATE = LINK_STATE
iocontrol.NAV_PAGE = NAV_PAGE
-- initialize facility-independent components of pocket iocontrol
---@param comms pocket_comms
function iocontrol.init_core(comms)
---@class pocket_ioctl_diag
io.diag = {}
-- alarm testing
io.diag.tone_test = {
test_1 = function (state) comms.diag__set_alarm_tone(1, state) end,
test_2 = function (state) comms.diag__set_alarm_tone(2, state) end,
test_3 = function (state) comms.diag__set_alarm_tone(3, state) end,
test_4 = function (state) comms.diag__set_alarm_tone(4, state) end,
test_5 = function (state) comms.diag__set_alarm_tone(5, state) end,
test_6 = function (state) comms.diag__set_alarm_tone(6, state) end,
test_7 = function (state) comms.diag__set_alarm_tone(7, state) end,
test_8 = function (state) comms.diag__set_alarm_tone(8, state) end,
stop_tones = function () comms.diag__set_alarm_tone(0, false) end,
test_breach = function (state) comms.diag__set_alarm(ALARM.ContainmentBreach, state) end,
test_rad = function (state) comms.diag__set_alarm(ALARM.ContainmentRadiation, state) end,
test_lost = function (state) comms.diag__set_alarm(ALARM.ReactorLost, state) end,
test_crit = function (state) comms.diag__set_alarm(ALARM.CriticalDamage, state) end,
test_dmg = function (state) comms.diag__set_alarm(ALARM.ReactorDamage, state) end,
test_overtemp = function (state) comms.diag__set_alarm(ALARM.ReactorOverTemp, state) end,
test_hightemp = function (state) comms.diag__set_alarm(ALARM.ReactorHighTemp, state) end,
test_wasteleak = function (state) comms.diag__set_alarm(ALARM.ReactorWasteLeak, state) end,
test_highwaste = function (state) comms.diag__set_alarm(ALARM.ReactorHighWaste, state) end,
test_rps = function (state) comms.diag__set_alarm(ALARM.RPSTransient, state) end,
test_rcs = function (state) comms.diag__set_alarm(ALARM.RCSTransient, state) end,
test_turbinet = function (state) comms.diag__set_alarm(ALARM.TurbineTrip, state) end,
stop_alarms = function () comms.diag__set_alarm(0, false) end,
get_tone_states = function () comms.diag__get_alarm_tones() end,
ready_warn = nil, ---@type graphics_element
tone_buttons = {},
alarm_buttons = {},
tone_indicators = {} -- indicators to update from supervisor tone states
}
---@class pocket_nav
io.nav = {
page = NAV_PAGE.HOME, ---@type NAV_PAGE
sub_pages = { NAV_PAGE.HOME, NAV_PAGE.UNITS, NAV_PAGE.REACTORS, NAV_PAGE.BOILERS, NAV_PAGE.TURBINES, NAV_PAGE.DIAG },
tasks = {}
}
-- add a task to be performed periodically while on a given page
---@param page NAV_PAGE page to add task to
---@param task function function to execute
function io.nav.register_task(page, task)
if io.nav.tasks[page] == nil then io.nav.tasks[page] = {} end
table.insert(io.nav.tasks[page], task)
end
end
-- initialize facility-dependent components of pocket iocontrol
function iocontrol.init_fac() end
-- set network link state
---@param state POCKET_LINK_STATE
function iocontrol.report_link_state(state) io.ps.publish("link_state", state) end
-- get the IO controller database
function iocontrol.get_db() return io end
return iocontrol

View File

@@ -1,39 +1,40 @@
local comms = require("scada-common.comms") 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 coreio = require("pocket.coreio") local iocontrol = require("pocket.iocontrol")
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
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
-- local CAPI_TYPE = comms.CAPI_TYPE
local LINK_STATE = coreio.LINK_STATE local LINK_STATE = iocontrol.LINK_STATE
local pocket = {} local pocket = {}
-- pocket coordinator + supervisor communications -- pocket coordinator + supervisor communications
---@nodiscard ---@nodiscard
---@param version string pocket version ---@param version string pocket version
---@param modem table modem device ---@param nic nic network interface device
---@param local_port integer local pocket port ---@param pkt_channel integer pocket comms channel
---@param sv_port integer port of supervisor ---@param svr_channel integer supervisor access channel
---@param api_port integer port of coordinator API ---@param crd_channel integer coordinator access channel
---@param range integer trusted device connection range ---@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, modem, local_port, sv_port, api_port, range, sv_watchdog, api_watchdog) function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range, sv_watchdog, api_watchdog)
local self = { local self = {
sv = { sv = {
linked = false, linked = false,
addr = comms.BROADCAST,
seq_num = 0, seq_num = 0,
r_seq_num = nil, ---@type nil|integer r_seq_num = nil, ---@type nil|integer
last_est_ack = ESTABLISH_ACK.ALLOW last_est_ack = ESTABLISH_ACK.ALLOW
}, },
api = { api = {
linked = false, linked = false,
addr = comms.BROADCAST,
seq_num = 0, seq_num = 0,
r_seq_num = nil, ---@type nil|integer r_seq_num = nil, ---@type nil|integer
last_est_ack = ESTABLISH_ACK.ALLOW last_est_ack = ESTABLISH_ACK.ALLOW
@@ -45,13 +46,9 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure modem channels -- configure network channels
local function _conf_channels() nic.closeAll()
modem.closeAll() nic.open(pkt_channel)
modem.open(local_port)
end
_conf_channels()
-- send a management packet to the supervisor -- send a management packet to the supervisor
---@param msg_type SCADA_MGMT_TYPE ---@param msg_type SCADA_MGMT_TYPE
@@ -61,9 +58,9 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
local pkt = comms.mgmt_packet() local pkt = comms.mgmt_packet()
pkt.make(msg_type, msg) pkt.make(msg_type, msg)
s_pkt.make(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())
modem.transmit(sv_port, local_port, s_pkt.raw_sendable()) nic.transmit(svr_channel, pkt_channel, s_pkt)
self.sv.seq_num = self.sv.seq_num + 1 self.sv.seq_num = self.sv.seq_num + 1
end end
@@ -75,26 +72,12 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
local pkt = comms.mgmt_packet() local pkt = comms.mgmt_packet()
pkt.make(msg_type, msg) pkt.make(msg_type, msg)
s_pkt.make(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())
modem.transmit(api_port, local_port, s_pkt.raw_sendable()) nic.transmit(crd_channel, pkt_channel, s_pkt)
self.api.seq_num = self.api.seq_num + 1 self.api.seq_num = self.api.seq_num + 1
end end
-- send a packet to the coordinator API
-----@param msg_type CAPI_TYPE
-----@param msg table
-- local function _send_api(msg_type, msg)
-- local s_pkt = comms.scada_packet()
-- local pkt = comms.capi_packet()
-- pkt.make(msg_type, msg)
-- s_pkt.make(self.api.seq_num, PROTOCOL.COORD_API, pkt.raw_sendable())
-- modem.transmit(api_port, local_port, s_pkt.raw_sendable())
-- self.api.seq_num = self.api.seq_num + 1
-- end
-- attempt supervisor connection establishment -- attempt supervisor connection establishment
local function _send_sv_establish() local function _send_sv_establish()
_send_sv(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT }) _send_sv(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
@@ -122,17 +105,12 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
---@class pocket_comms ---@class pocket_comms
local public = {} local public = {}
-- reconnect a newly connected modem
---@param new_modem table
function public.reconnect_modem(new_modem)
modem = new_modem
_conf_channels()
end
-- close connection to the supervisor -- close connection to the supervisor
function public.close_sv() function public.close_sv()
sv_watchdog.cancel() sv_watchdog.cancel()
self.sv.linked = false self.sv.linked = false
self.sv.r_seq_num = nil
self.sv.addr = comms.BROADCAST
_send_sv(SCADA_MGMT_TYPE.CLOSE, {}) _send_sv(SCADA_MGMT_TYPE.CLOSE, {})
end end
@@ -140,6 +118,8 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
function public.close_api() function public.close_api()
api_watchdog.cancel() api_watchdog.cancel()
self.api.linked = false self.api.linked = false
self.api.r_seq_num = nil
self.api.addr = comms.BROADCAST
_send_crd(SCADA_MGMT_TYPE.CLOSE, {}) _send_crd(SCADA_MGMT_TYPE.CLOSE, {})
end end
@@ -152,7 +132,7 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
-- attempt to re-link if any of the dependent links aren't active -- attempt to re-link if any of the dependent links aren't active
function public.link_update() function public.link_update()
if not self.sv.linked then if not self.sv.linked then
coreio.report_link_state(util.trinary(self.api.linked, LINK_STATE.API_LINK_ONLY, LINK_STATE.UNLINKED)) iocontrol.report_link_state(util.trinary(self.api.linked, LINK_STATE.API_LINK_ONLY, LINK_STATE.UNLINKED))
if self.establish_delay_counter <= 0 then if self.establish_delay_counter <= 0 then
_send_sv_establish() _send_sv_establish()
@@ -161,7 +141,7 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
self.establish_delay_counter = self.establish_delay_counter - 1 self.establish_delay_counter = self.establish_delay_counter - 1
end end
elseif not self.api.linked then elseif not self.api.linked then
coreio.report_link_state(LINK_STATE.SV_LINK_ONLY) iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY)
if self.establish_delay_counter <= 0 then if self.establish_delay_counter <= 0 then
_send_api_establish() _send_api_establish()
@@ -171,10 +151,29 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
end end
else else
-- linked, all good! -- linked, all good!
coreio.report_link_state(LINK_STATE.LINKED) iocontrol.report_link_state(LINK_STATE.LINKED)
end end
end end
-- supervisor get active alarm tones
function public.diag__get_alarm_tones()
if self.sv.linked then _send_sv(SCADA_MGMT_TYPE.DIAG_TONE_GET, {}) end
end
-- supervisor test alarm tones by tone
---@param id TONE|0 tone ID, or 0 to stop all
---@param state boolean tone state
function public.diag__set_alarm_tone(id, state)
if self.sv.linked then _send_sv(SCADA_MGMT_TYPE.DIAG_TONE_SET, { id, state }) end
end
-- supervisor test alarm tones by alarm
---@param id ALARM|0 alarm ID, 0 to stop all
---@param state boolean alarm state
function public.diag__set_alarm(id, state)
if self.sv.linked then _send_sv(SCADA_MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end
end
-- parse a packet -- parse a packet
---@param side string ---@param side string
---@param sender integer ---@param sender integer
@@ -183,13 +182,10 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
---@param distance integer ---@param distance integer
---@return mgmt_frame|capi_frame|nil packet ---@return mgmt_frame|capi_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance) function public.parse_packet(side, sender, reply_to, message, distance)
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
local pkt = nil local pkt = nil
local s_pkt = comms.scada_packet()
-- parse packet as generic SCADA packet if s_pkt then
s_pkt.receive(side, sender, reply_to, message, distance)
if s_pkt.is_valid() then
-- get as SCADA management packet -- get as SCADA management packet
if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet() local mgmt_pkt = comms.mgmt_packet()
@@ -213,19 +209,26 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
-- handle a packet -- handle a packet
---@param packet mgmt_frame|capi_frame|nil ---@param packet mgmt_frame|capi_frame|nil
function public.handle_packet(packet) function public.handle_packet(packet)
if packet ~= nil then local diag = iocontrol.get_db().diag
local l_port = packet.scada_frame.local_port()
local r_port = packet.scada_frame.remote_port()
local protocol = packet.scada_frame.protocol()
if l_port ~= local_port then if packet ~= nil then
log.debug("received packet on unconfigured channel " .. l_port, true) local l_chan = packet.scada_frame.local_channel()
elseif r_port == api_port then local r_chan = packet.scada_frame.remote_channel()
local protocol = packet.scada_frame.protocol()
local src_addr = packet.scada_frame.src_addr()
if l_chan ~= pkt_channel then
log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == 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()
elseif self.connected and ((self.api.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then elseif self.connected and ((self.api.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
log.warning("sequence out-of-order: last = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) log.warning("sequence out-of-order (API): last = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
elseif self.api.linked and (src_addr ~= self.api.addr) then
log.debug("received packet from unknown computer " .. src_addr .. " while linked (API expected " .. self.api.addr ..
"); channel in use by another system?")
return return
else else
self.api.r_seq_num = packet.scada_frame.seq_num() self.api.r_seq_num = packet.scada_frame.seq_num()
@@ -234,11 +237,36 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
-- feed watchdog on valid sequence number -- feed watchdog on valid sequence number
api_watchdog.feed() api_watchdog.feed()
if protocol == PROTOCOL.COORD_API then if protocol == PROTOCOL.SCADA_MGMT then
---@cast packet capi_frame
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then if self.api.linked then
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back
if packet.length == 1 then
local timestamp = packet.data[1]
local trip_time = util.time() - timestamp
if trip_time > 750 then
log.warning("pocket coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
end
-- log.debug("pocket coordinator RTT = " .. trip_time .. "ms")
_send_api_keep_alive_ack(timestamp)
else
log.debug("coordinator SCADA keep alive packet length mismatch")
end
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
-- handle session close
api_watchdog.cancel()
self.api.linked = false
self.api.r_seq_num = nil
self.api.addr = comms.BROADCAST
log.info("coordinator server connection closed by remote host")
else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator")
end
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- connection with coordinator established -- connection with coordinator established
if packet.length == 1 then if packet.length == 1 then
local est_ack = packet.data[1] local est_ack = packet.data[1]
@@ -247,11 +275,12 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
log.info("coordinator connection established") log.info("coordinator connection established")
self.establish_delay_counter = 0 self.establish_delay_counter = 0
self.api.linked = true self.api.linked = true
self.api.addr = src_addr
if self.sv.linked then if self.sv.linked then
coreio.report_link_state(LINK_STATE.LINKED) iocontrol.report_link_state(LINK_STATE.LINKED)
else else
coreio.report_link_state(LINK_STATE.API_LINK_ONLY) iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY)
end end
elseif est_ack == ESTABLISH_ACK.DENY then elseif est_ack == ESTABLISH_ACK.DENY then
if self.api.last_est_ack ~= est_ack then if self.api.last_est_ack ~= est_ack then
@@ -273,43 +302,22 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
else else
log.debug("coordinator SCADA_MGMT establish packet length mismatch") log.debug("coordinator SCADA_MGMT establish packet length mismatch")
end end
elseif self.api.linked then
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back
if packet.length == 1 then
local timestamp = packet.data[1]
local trip_time = util.time() - timestamp
if trip_time > 750 then
log.warning("pocket coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
end
-- log.debug("pocket coordinator RTT = " .. trip_time .. "ms")
_send_api_keep_alive_ack(timestamp)
else
log.debug("coordinator SCADA keep alive packet length mismatch")
end
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
-- handle session close
api_watchdog.cancel()
self.api.linked = false
log.info("coordinator server connection closed by remote host")
else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator")
end
else else
log.debug("discarding coordinator non-link SCADA_MGMT packet before linked") log.debug("discarding coordinator non-link SCADA_MGMT packet before linked")
end end
else else
log.debug("illegal packet type " .. protocol .. " from coordinator", true) log.debug("illegal packet type " .. protocol .. " from coordinator", true)
end end
elseif r_port == sv_port then elseif r_chan == 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()
elseif self.connected and ((self.sv.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then elseif self.connected and ((self.sv.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
log.warning("sequence out-of-order: last = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) log.warning("sequence out-of-order (SVR): last = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
elseif self.sv.linked and (src_addr ~= self.sv.addr) then
log.debug("received packet from unknown computer " .. src_addr .. " while linked (SVR expected " .. self.sv.addr ..
"); channel in use by another system?")
return return
else else
self.sv.r_seq_num = packet.scada_frame.seq_num() self.sv.r_seq_num = packet.scada_frame.seq_num()
@@ -321,7 +329,79 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
-- handle packet -- handle packet
if protocol == PROTOCOL.SCADA_MGMT then if protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then if self.sv.linked then
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back
if packet.length == 1 then
local timestamp = packet.data[1]
local trip_time = util.time() - timestamp
if trip_time > 750 then
log.warning("pocket supervisor KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
end
-- log.debug("pocket supervisor RTT = " .. trip_time .. "ms")
_send_sv_keep_alive_ack(timestamp)
else
log.debug("supervisor SCADA keep alive packet length mismatch")
end
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
-- handle session close
sv_watchdog.cancel()
self.sv.linked = false
self.sv.r_seq_num = nil
self.sv.addr = comms.BROADCAST
log.info("supervisor server connection closed by remote host")
elseif packet.type == SCADA_MGMT_TYPE.DIAG_TONE_GET then
if packet.length == 8 then
for i = 1, #packet.data do
diag.tone_test.tone_indicators[i].update(packet.data[i] == true)
end
else
log.debug("supervisor SCADA diag alarm states packet length mismatch")
end
elseif packet.type == SCADA_MGMT_TYPE.DIAG_TONE_SET then
if packet.length == 1 and packet.data[1] == false then
diag.tone_test.ready_warn.set_value("testing denied")
log.debug("supervisor SCADA diag tone set failed")
elseif packet.length == 2 and type(packet.data[2]) == "table" then
local ready = packet.data[1]
local states = packet.data[2]
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready"))
for i = 1, #states do
if diag.tone_test.tone_buttons[i] ~= nil then
diag.tone_test.tone_buttons[i].set_value(states[i] == true)
diag.tone_test.tone_indicators[i].update(states[i] == true)
end
end
else
log.debug("supervisor SCADA diag tone set packet length/type mismatch")
end
elseif packet.type == SCADA_MGMT_TYPE.DIAG_ALARM_SET then
if packet.length == 1 and packet.data[1] == false then
diag.tone_test.ready_warn.set_value("testing denied")
log.debug("supervisor SCADA diag alarm set failed")
elseif packet.length == 2 and type(packet.data[2]) == "table" then
local ready = packet.data[1]
local states = packet.data[2]
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready"))
for i = 1, #states do
if diag.tone_test.alarm_buttons[i] ~= nil then
diag.tone_test.alarm_buttons[i].set_value(states[i] == true)
end
end
else
log.debug("supervisor SCADA diag alarm set packet length/type mismatch")
end
else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor")
end
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- connection with supervisor established -- connection with supervisor established
if packet.length == 1 then if packet.length == 1 then
local est_ack = packet.data[1] local est_ack = packet.data[1]
@@ -330,11 +410,12 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
log.info("supervisor connection established") log.info("supervisor connection established")
self.establish_delay_counter = 0 self.establish_delay_counter = 0
self.sv.linked = true self.sv.linked = true
self.sv.addr = src_addr
if self.api.linked then if self.api.linked then
coreio.report_link_state(LINK_STATE.LINKED) iocontrol.report_link_state(LINK_STATE.LINKED)
else else
coreio.report_link_state(LINK_STATE.SV_LINK_ONLY) iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY)
end end
elseif est_ack == ESTABLISH_ACK.DENY then elseif est_ack == ESTABLISH_ACK.DENY then
if self.sv.last_est_ack ~= est_ack then if self.sv.last_est_ack ~= est_ack then
@@ -356,31 +437,6 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
else else
log.debug("supervisor SCADA_MGMT establish packet length mismatch") log.debug("supervisor SCADA_MGMT establish packet length mismatch")
end end
elseif self.sv.linked then
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back
if packet.length == 1 then
local timestamp = packet.data[1]
local trip_time = util.time() - timestamp
if trip_time > 750 then
log.warning("pocket supervisor KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
end
-- log.debug("pocket supervisor RTT = " .. trip_time .. "ms")
_send_sv_keep_alive_ack(timestamp)
else
log.debug("supervisor SCADA keep alive packet length mismatch")
end
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
-- handle session close
sv_watchdog.cancel()
self.sv.linked = false
log.info("supervisor server connection closed by remote host")
else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor")
end
else else
log.debug("discarding supervisor non-link SCADA_MGMT packet before linked") log.debug("discarding supervisor non-link SCADA_MGMT packet before linked")
end end
@@ -388,7 +444,7 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
log.debug("illegal packet type " .. protocol .. " from supervisor", true) log.debug("illegal packet type " .. protocol .. " from supervisor", true)
end end
else else
log.debug("received packet from unconfigured channel " .. r_port, true) log.debug("received packet from unconfigured channel " .. r_chan, true)
end end
end end
end end

View File

@@ -70,9 +70,9 @@ end
function renderer.ui_ready() return ui.display ~= nil end function renderer.ui_ready() return ui.display ~= nil end
-- handle a mouse event -- handle a mouse event
---@param event mouse_interaction ---@param event mouse_interaction|nil
function renderer.handle_mouse(event) function renderer.handle_mouse(event)
if ui.display ~= nil then if ui.display ~= nil and event ~= nil then
ui.display.handle_mouse(event) ui.display.handle_mouse(event)
end end
end end

View File

@@ -4,20 +4,21 @@
require("/initenv").init_env() require("/initenv").init_env()
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
local log = require("scada-common.log") local log = require("scada-common.log")
local ppm = require("scada-common.ppm") local network = require("scada-common.network")
local tcallbackdsp = require("scada-common.tcallbackdsp") local ppm = require("scada-common.ppm")
local util = require("scada-common.util") local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local config = require("pocket.config") local config = require("pocket.config")
local coreio = require("pocket.coreio") 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 = "alpha-v0.2.6" local POCKET_VERSION = "v0.6.0-alpha"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@@ -28,9 +29,9 @@ local println_ts = util.println_ts
local cfv = util.new_validator() local cfv = util.new_validator()
cfv.assert_port(config.SCADA_SV_PORT) cfv.assert_channel(config.SVR_CHANNEL)
cfv.assert_port(config.SCADA_API_PORT) cfv.assert_channel(config.CRD_CHANNEL)
cfv.assert_port(config.LISTEN_PORT) cfv.assert_channel(config.PKT_CHANNEL)
cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_int(config.TRUSTED_RANGE)
cfv.assert_type_num(config.COMMS_TIMEOUT) cfv.assert_type_num(config.COMMS_TIMEOUT)
cfv.assert_min(config.COMMS_TIMEOUT, 2) cfv.assert_min(config.COMMS_TIMEOUT, 2)
@@ -43,7 +44,7 @@ assert(cfv.valid(), "bad config file: missing/invalid fields")
-- log init -- log init
---------------------------------------- ----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE) log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
log.info("========================================") log.info("========================================")
log.info("BOOTING pocket.startup " .. POCKET_VERSION) log.info("BOOTING pocket.startup " .. POCKET_VERSION)
@@ -67,7 +68,12 @@ local function main()
-- setup communications & clocks -- setup communications & clocks
---------------------------------------- ----------------------------------------
coreio.report_link_state(coreio.LINK_STATE.UNLINKED) -- message authentication init
if type(config.AUTH_KEY) == "string" then
network.init_mac(config.AUTH_KEY)
end
iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED)
-- get the communications modem -- get the communications modem
local modem = ppm.get_wireless_modem() local modem = ppm.get_wireless_modem()
@@ -88,15 +94,19 @@ local function main()
log.debug("startup> conn watchdogs created") log.debug("startup> conn watchdogs created")
-- start comms, open all channels -- create network interface then setup comms
local pocket_comms = pocket.comms(POCKET_VERSION, modem, config.LISTEN_PORT, config.SCADA_SV_PORT, local nic = network.nic(modem)
config.SCADA_API_PORT, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api) local pocket_comms = pocket.comms(POCKET_VERSION, nic, config.PKT_CHANNEL, config.SVR_CHANNEL,
config.CRD_CHANNEL, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api)
log.debug("startup> comms init") log.debug("startup> comms init")
-- base loop clock (2Hz, 10 ticks) -- base loop clock (2Hz, 10 ticks)
local MAIN_CLOCK = 0.5 local MAIN_CLOCK = 0.5
local loop_clock = util.new_clock(MAIN_CLOCK) local loop_clock = util.new_clock(MAIN_CLOCK)
-- init I/O control
iocontrol.init_core(pocket_comms)
---------------------------------------- ----------------------------------------
-- start the UI -- start the UI
---------------------------------------- ----------------------------------------
@@ -105,7 +115,7 @@ local function main()
if not ui_ok then if not ui_ok then
renderer.close_ui() renderer.close_ui()
println(util.c("UI error: ", message)) println(util.c("UI error: ", message))
log.error(util.c("startup> GUI crashed with error ", message)) log.error(util.c("startup> GUI render failed with error ", message))
else else
-- start clock -- start clock
loop_clock.start() loop_clock.start()
@@ -120,54 +130,64 @@ local function main()
conn_wd.sv.feed() conn_wd.sv.feed()
conn_wd.api.feed() conn_wd.api.feed()
log.debug("startup> conn watchdog started") log.debug("startup> conn watchdog started")
end
-- main event loop local io_db = iocontrol.get_db()
while ui_ok do local nav = io_db.nav
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event -- main event loop
if event == "timer" then while true do
if loop_clock.is_clock(param1) then local event, param1, param2, param3, param4, param5 = util.pull_event()
-- main loop tick
-- relink if necessary -- handle event
pocket_comms.link_update() if event == "timer" then
if loop_clock.is_clock(param1) then
-- main loop tick
loop_clock.start() -- relink if necessary
elseif conn_wd.sv.is_timer(param1) then pocket_comms.link_update()
-- supervisor watchdog timeout
log.info("supervisor server timeout") -- update any tasks for the active page
pocket_comms.close_sv() if (type(nav.tasks[nav.page]) == "table") then
elseif conn_wd.api.is_timer(param1) then for i = 1, #nav.tasks[nav.page] do
-- coordinator watchdog timeout nav.tasks[nav.page][i]()
log.info("coordinator api server timeout") end
pocket_comms.close_api() end
else
-- a non-clock/main watchdog timer event loop_clock.start()
-- notify timer callback dispatcher elseif conn_wd.sv.is_timer(param1) then
tcallbackdsp.handle(param1) -- supervisor watchdog timeout
log.info("supervisor server timeout")
pocket_comms.close_sv()
elseif conn_wd.api.is_timer(param1) then
-- coordinator watchdog timeout
log.info("coordinator api server timeout")
pocket_comms.close_api()
else
-- a non-clock/main watchdog timer event
-- notify timer callback dispatcher
tcd.handle(param1)
end
elseif event == "modem_message" then
-- got a packet
local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
pocket_comms.handle_packet(packet)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then
-- handle a monitor touch event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
end
-- check for termination request
if event == "terminate" or ppm.should_terminate() then
log.info("terminate requested, closing server connections...")
pocket_comms.close()
log.info("connections closed")
break
end end
elseif event == "modem_message" then
-- got a packet
local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
pocket_comms.handle_packet(packet)
elseif event == "mouse_click" then
-- handle a monitor touch event
renderer.handle_mouse(core.events.touch(param1, param2, param3))
end end
-- check for termination request renderer.close_ui()
if event == "terminate" or ppm.should_terminate() then
log.info("terminate requested, closing server connections...")
pocket_comms.close()
log.info("connections closed")
break
end
end end
renderer.close_ui()
println_ts("exited") println_ts("exited")
log.info("exited") log.info("exited")
end end

View File

@@ -11,9 +11,9 @@ local TextBox = require("graphics.elements.textbox")
local WaitingAnim = require("graphics.elements.animations.waiting") local WaitingAnim = require("graphics.elements.animations.waiting")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.graphics.cpair local cpair = core.cpair
-- create a waiting view -- create a waiting view
---@param parent graphics_element parent ---@param parent graphics_element parent
@@ -25,7 +25,7 @@ local function init(parent, y, is_api)
-- bounding box div -- bounding box div
local box = Div{parent=root,x=1,y=y,height=5} local box = Div{parent=root,x=1,y=y,height=5}
local waiting_x = math.floor(parent.width() / 2) - 1 local waiting_x = math.floor(parent.get_width() / 2) - 1
if is_api then if is_api then
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)} WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)}

View File

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

View File

@@ -2,17 +2,18 @@
-- Pocket GUI Root -- Pocket GUI Root
-- --
local coreio = require("pocket.coreio") local iocontrol = require("pocket.iocontrol")
local style = require("pocket.ui.style") local style = require("pocket.ui.style")
local conn_waiting = require("pocket.ui.components.conn_waiting") local conn_waiting = require("pocket.ui.components.conn_waiting")
local home_page = require("pocket.ui.components.home_page") local boiler_page = require("pocket.ui.pages.boiler_page")
local unit_page = require("pocket.ui.components.unit_page") local diag_page = require("pocket.ui.pages.diag_page")
local reactor_page = require("pocket.ui.components.reactor_page") local home_page = require("pocket.ui.pages.home_page")
local boiler_page = require("pocket.ui.components.boiler_page") local reactor_page = require("pocket.ui.pages.reactor_page")
local turbine_page = require("pocket.ui.components.turbine_page") local turbine_page = require("pocket.ui.pages.turbine_page")
local unit_page = require("pocket.ui.pages.unit_page")
local core = require("graphics.core") local core = require("graphics.core")
@@ -22,13 +23,19 @@ local TextBox = require("graphics.elements.textbox")
local Sidebar = require("graphics.elements.controls.sidebar") local Sidebar = require("graphics.elements.controls.sidebar")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN local LINK_STATE = iocontrol.LINK_STATE
local NAV_PAGE = iocontrol.NAV_PAGE
local cpair = core.graphics.cpair local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.cpair
-- create new main view -- create new main view
---@param main graphics_element main displaybox ---@param main graphics_element main displaybox
local function init(main) local function init(main)
local nav = iocontrol.get_db().nav
local ps = iocontrol.get_db().ps
-- window header message -- window header message
TextBox{parent=main,y=1,text="",alignment=TEXT_ALIGN.LEFT,height=1,fg_bg=style.header} TextBox{parent=main,y=1,text="",alignment=TEXT_ALIGN.LEFT,height=1,fg_bg=style.header}
@@ -45,10 +52,10 @@ local function init(main)
local root_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes=root_panes} local root_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes=root_panes}
coreio.core_ps().subscribe("link_state", function (state) root_pane.register(ps, "link_state", function (state)
if state == coreio.LINK_STATE.UNLINKED or state == coreio.LINK_STATE.API_LINK_ONLY then if state == LINK_STATE.UNLINKED or state == LINK_STATE.API_LINK_ONLY then
root_pane.set_value(1) root_pane.set_value(1)
elseif state == coreio.LINK_STATE.SV_LINK_ONLY then elseif state == LINK_STATE.SV_LINK_ONLY then
root_pane.set_value(2) root_pane.set_value(2)
else else
root_pane.set_value(3) root_pane.set_value(3)
@@ -81,19 +88,36 @@ local function init(main)
{ {
char = "T", char = "T",
color = cpair(colors.black,colors.white) color = cpair(colors.black,colors.white)
},
{
char = "D",
color = cpair(colors.black,colors.orange)
} }
} }
local pane_1 = home_page(page_div) local panes = { home_page(page_div), unit_page(page_div), reactor_page(page_div), boiler_page(page_div), turbine_page(page_div), diag_page(page_div) }
local pane_2 = unit_page(page_div)
local pane_3 = reactor_page(page_div)
local pane_4 = boiler_page(page_div)
local pane_5 = turbine_page(page_div)
local panes = { pane_1, pane_2, pane_3, pane_4, pane_5 }
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes} local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=page_pane.set_value} local function navigate_sidebar(page)
if page == 1 then
nav.page = nav.sub_pages[NAV_PAGE.HOME]
elseif page == 2 then
nav.page = nav.sub_pages[NAV_PAGE.UNITS]
elseif page == 3 then
nav.page = nav.sub_pages[NAV_PAGE.REACTORS]
elseif page == 4 then
nav.page = nav.sub_pages[NAV_PAGE.BOILERS]
elseif page == 5 then
nav.page = nav.sub_pages[NAV_PAGE.TURBINES]
elseif page == 6 then
nav.page = nav.sub_pages[NAV_PAGE.DIAG]
end
page_pane.set_value(page)
end
Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=navigate_sidebar}
end end
return init return init

View File

@@ -5,9 +5,9 @@ local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.textbox")
-- local cpair = core.graphics.cpair -- local cpair = core.cpair
local TEXT_ALIGN = core.graphics.TEXT_ALIGN local TEXT_ALIGN = core.TEXT_ALIGN
-- new boiler page view -- new boiler page view
---@param root graphics_element parent ---@param root graphics_element parent

View File

@@ -0,0 +1,147 @@
local iocontrol = require("pocket.iocontrol")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local IndicatorLight = require("graphics.elements.indicators.light")
local App = require("graphics.elements.controls.app")
local Checkbox = require("graphics.elements.controls.checkbox")
local PushButton = require("graphics.elements.controls.push_button")
local SwitchButton = require("graphics.elements.controls.switch_button")
local cpair = core.cpair
local NAV_PAGE = iocontrol.NAV_PAGE
local TEXT_ALIGN = core.TEXT_ALIGN
-- new diagnostics page view
---@param root graphics_element parent
local function new_view(root)
local db = iocontrol.get_db()
local main = Div{parent=root,x=1,y=1}
local diag_home = Div{parent=main,x=1,y=1}
TextBox{parent=diag_home,text="Diagnostic Apps",x=1,y=2,height=1,alignment=TEXT_ALIGN.CENTER}
local alarm_test = Div{parent=main,x=1,y=1}
local panes = { diag_home, alarm_test }
local page_pane = MultiPane{parent=main,x=1,y=1,panes=panes}
local function navigate_diag()
page_pane.set_value(1)
db.nav.page = NAV_PAGE.DIAG
db.nav.sub_pages[NAV_PAGE.DIAG] = NAV_PAGE.DIAG
end
local function navigate_alarm()
page_pane.set_value(2)
db.nav.page = NAV_PAGE.D_ALARMS
db.nav.sub_pages[NAV_PAGE.DIAG] = NAV_PAGE.D_ALARMS
end
------------------------
-- Alarm Testing Page --
------------------------
db.nav.register_task(NAV_PAGE.D_ALARMS, db.diag.tone_test.get_tone_states)
local ttest = db.diag.tone_test
local c_wht_gray = cpair(colors.white, colors.gray)
local c_red_gray = cpair(colors.red, colors.gray)
local c_yel_gray = cpair(colors.yellow, colors.gray)
local c_blue_gray = cpair(colors.blue, colors.gray)
local audio = Div{parent=alarm_test,x=1,y=1}
TextBox{parent=audio,y=1,text="Alarm Sounder Tests",height=1,alignment=TEXT_ALIGN.CENTER}
ttest.ready_warn = TextBox{parent=audio,y=2,text="",height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
PushButton{parent=audio,x=13,y=18,text="\x11 BACK",min_width=8,fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=c_wht_gray,callback=navigate_diag}
local tones = Div{parent=audio,x=2,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)}
TextBox{parent=tones,text="Tones",height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
local test_btns = {}
test_btns[1] = SwitchButton{parent=tones,text="TEST 1",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_1}
test_btns[2] = SwitchButton{parent=tones,text="TEST 2",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_2}
test_btns[3] = SwitchButton{parent=tones,text="TEST 3",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_3}
test_btns[4] = SwitchButton{parent=tones,text="TEST 4",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_4}
test_btns[5] = SwitchButton{parent=tones,text="TEST 5",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_5}
test_btns[6] = SwitchButton{parent=tones,text="TEST 6",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_6}
test_btns[7] = SwitchButton{parent=tones,text="TEST 7",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_7}
test_btns[8] = SwitchButton{parent=tones,text="TEST 8",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_8}
ttest.tone_buttons = test_btns
local function stop_all_tones()
for i = 1, #test_btns do test_btns[i].set_value(false) end
ttest.stop_tones()
end
PushButton{parent=tones,text="STOP",min_width=8,active_fg_bg=c_wht_gray,fg_bg=cpair(colors.black,colors.red),callback=stop_all_tones}
local alarms = Div{parent=audio,x=11,y=3,height=15,fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=alarms,text="Alarms (\x13)",height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
local alarm_btns = {}
alarm_btns[1] = Checkbox{parent=alarms,label="BREACH",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_breach}
alarm_btns[2] = Checkbox{parent=alarms,label="RADIATION",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_rad}
alarm_btns[3] = Checkbox{parent=alarms,label="RCT LOST",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_lost}
alarm_btns[4] = Checkbox{parent=alarms,label="CRIT DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_crit}
alarm_btns[5] = Checkbox{parent=alarms,label="DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_dmg}
alarm_btns[6] = Checkbox{parent=alarms,label="OVER TEMP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_overtemp}
alarm_btns[7] = Checkbox{parent=alarms,label="HIGH TEMP",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_hightemp}
alarm_btns[8] = Checkbox{parent=alarms,label="WASTE LEAK",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_wasteleak}
alarm_btns[9] = Checkbox{parent=alarms,label="WASTE HIGH",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_highwaste}
alarm_btns[10] = Checkbox{parent=alarms,label="RPS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rps}
alarm_btns[11] = Checkbox{parent=alarms,label="RCS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rcs}
alarm_btns[12] = Checkbox{parent=alarms,label="TURBINE TRP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_turbinet}
ttest.alarm_buttons = alarm_btns
local function stop_all_alarms()
for i = 1, #alarm_btns do alarm_btns[i].set_value(false) end
ttest.stop_alarms()
end
PushButton{parent=alarms,x=3,y=15,text="STOP \x13",min_width=8,fg_bg=cpair(colors.black,colors.red),active_fg_bg=c_wht_gray,callback=stop_all_alarms}
local states = Div{parent=audio,x=2,y=14,height=5,width=8}
TextBox{parent=states,text="States",height=1,alignment=TEXT_ALIGN.CENTER}
local t_1 = IndicatorLight{parent=states,label="1",colors=c_blue_gray}
local t_2 = IndicatorLight{parent=states,label="2",colors=c_blue_gray}
local t_3 = IndicatorLight{parent=states,label="3",colors=c_blue_gray}
local t_4 = IndicatorLight{parent=states,label="4",colors=c_blue_gray}
local t_5 = IndicatorLight{parent=states,x=6,y=2,label="5",colors=c_blue_gray}
local t_6 = IndicatorLight{parent=states,x=6,label="6",colors=c_blue_gray}
local t_7 = IndicatorLight{parent=states,x=6,label="7",colors=c_blue_gray}
local t_8 = IndicatorLight{parent=states,x=6,label="8",colors=c_blue_gray}
ttest.tone_indicators = { t_1, t_2, t_3, t_4, t_5, t_6, t_7, t_8 }
--------------
-- App List --
--------------
App{parent=diag_home,x=3,y=4,text="\x0f",title="Alarm",callback=navigate_alarm,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
App{parent=diag_home,x=10,y=4,text="\x1e",title="LoopT",callback=function()end,app_fg_bg=cpair(colors.black,colors.cyan)}
App{parent=diag_home,x=17,y=4,text="@",title="Comps",callback=function()end,app_fg_bg=cpair(colors.black,colors.orange)}
return main
end
return new_view

View File

@@ -0,0 +1,23 @@
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local App = require("graphics.elements.controls.app")
local cpair = core.cpair
-- new home page view
---@param root graphics_element parent
local function new_view(root)
local main = Div{parent=root,x=1,y=1}
App{parent=main,x=3,y=2,text="\x17",title="PRC",callback=function()end,app_fg_bg=cpair(colors.black,colors.purple)}
App{parent=main,x=10,y=2,text="\x15",title="CTL",callback=function()end,app_fg_bg=cpair(colors.black,colors.green)}
App{parent=main,x=17,y=2,text="\x08",title="DEV",callback=function()end,app_fg_bg=cpair(colors.black,colors.lightGray)}
App{parent=main,x=3,y=7,text="\x7f",title="Waste",callback=function()end,app_fg_bg=cpair(colors.black,colors.brown)}
App{parent=main,x=10,y=7,text="\xb6",title="Guide",callback=function()end,app_fg_bg=cpair(colors.black,colors.cyan)}
return main
end
return new_view

View File

@@ -5,9 +5,9 @@ local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.textbox")
-- local cpair = core.graphics.cpair -- local cpair = core.cpair
local TEXT_ALIGN = core.graphics.TEXT_ALIGN local TEXT_ALIGN = core.TEXT_ALIGN
-- new reactor page view -- new reactor page view
---@param root graphics_element parent ---@param root graphics_element parent

View File

@@ -5,9 +5,9 @@ local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.textbox")
-- local cpair = core.graphics.cpair -- local cpair = core.cpair
local TEXT_ALIGN = core.graphics.TEXT_ALIGN local TEXT_ALIGN = core.TEXT_ALIGN
-- new turbine page view -- new turbine page view
---@param root graphics_element parent ---@param root graphics_element parent

View File

@@ -5,9 +5,9 @@ local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.textbox")
-- local cpair = core.graphics.cpair -- local cpair = core.cpair
local TEXT_ALIGN = core.graphics.TEXT_ALIGN local TEXT_ALIGN = core.TEXT_ALIGN
-- new unit page view -- new unit page view
---@param root graphics_element parent ---@param root graphics_element parent

View File

@@ -6,7 +6,7 @@ local core = require("graphics.core")
local style = {} local style = {}
local cpair = core.graphics.cpair local cpair = core.cpair
-- GLOBAL -- -- GLOBAL --

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