Compare commits

..

335 Commits

Author SHA1 Message Date
Mikayla
b6617c140c Merge pull request #369 from MikaylaFischler/devel
2023.11.14 Release
2023-11-14 22:10:12 -05:00
Mikayla Fischler
1fdf012f65 properly clear peripherals and redstone when importing 2023-11-14 22:00:01 -05:00
Mikayla Fischler
8fe0321ac0 fixed RTU authkey check 2023-11-14 19:40:55 -05:00
Mikayla Fischler
4a2199fa13 readme update 2023-11-14 19:40:29 -05:00
Mikayla
69680a53a0 Merge pull request #368 from MikaylaFischler/145-graphical-configure-utilities
RTU configurator and other updates
2023-11-12 18:37:23 -05:00
Mikayla Fischler
785dea6545 #306 fixed incorrect screenflow and changed peripheral import validation symbols 2023-11-12 18:36:16 -05:00
Mikayla Fischler
885932afe1 don't try to log if log.init wasn't called 2023-11-12 18:35:46 -05:00
Mikayla Fischler
a38ccf3dcc #145 #306 improvements and fixes, better peripheral import 2023-11-12 18:27:24 -05:00
Mikayla Fischler
d7b1f9cc7e #306 #362 bugfixes 2023-11-12 16:55:24 -05:00
Mikayla Fischler
6e92097544 fixed util.concat handling of nil parameters 2023-11-12 16:06:16 -05:00
Mikayla Fischler
76403b4ddc cleanup and grammar 2023-11-12 15:38:25 -05:00
Mikayla Fischler
41ad8d8edb #306 prevent duplicate redstone inputs 2023-11-12 14:35:53 -05:00
Mikayla Fischler
68754977b0 cleanup and fixes 2023-11-12 14:21:48 -05:00
Mikayla Fischler
78ad6d5457 luacheck fix 2023-11-12 12:00:42 -05:00
Mikayla Fischler
cb049ebf41 #194 changed 'newer' to 'different' in ccmsi 2023-11-12 11:57:05 -05:00
Mikayla Fischler
f2f5c3201f #362 taking max of connected radiation monitors 2023-11-12 11:54:47 -05:00
Mikayla Fischler
1ba178eae8 #306 delete legacy RTU config 2023-11-06 10:22:52 -05:00
Mikayla Fischler
838f80c30c #306 #362 supervisor updates for RTU config changes 2023-11-06 10:21:42 -05:00
Mikayla Fischler
dc0408881e #145 use rsio.color_name on PLC configurator 2023-11-06 10:11:57 -05:00
Mikayla Fischler
1b5e8cb69c #306 RTU integration with new settings 2023-11-06 09:25:44 -05:00
Mikayla Fischler
9e13a3a467 added ability to view reactor PLC config after importing if cancelled before deleting 2023-11-05 13:23:45 -05:00
Mikayla Fischler
32653c3b8a param type change and added validator.assert 2023-11-05 13:23:22 -05:00
Mikayla Fischler
16258a2631 rtu configurator config import 2023-11-04 15:06:29 -04:00
Mikayla Fischler
4c646249ad plc configurator cleanup 2023-11-04 14:57:17 -04:00
Mikayla Fischler
45c8a8d8a9 added peripheral connections to rtu configurator 2023-11-04 13:29:38 -04:00
Mikayla Fischler
eff3444834 added type def to ppm and return a copy of the peripherals list rather than the table itself 2023-11-04 12:56:49 -04:00
Mikayla Fischler
25f68f338c bootloader cleanup and added license to installer downloads 2023-11-04 12:51:24 -04:00
Mikayla Fischler
7ef363a3c2 fixed installer typo 2023-11-04 12:49:54 -04:00
Mikayla Fischler
3065e2bece plc configurator clear settings when loading settings and show actual current settings on view 2023-11-04 12:49:14 -04:00
Mikayla Fischler
1075d66122 graphics bugfix with disabled input fields 2023-11-04 12:48:06 -04:00
Mikayla Fischler
d477b33774 fixed reposition not repositioning frame for mouse events 2023-10-21 13:58:42 -04:00
Mikayla Fischler
7b374f8618 rtu redstone configuration 2023-10-19 23:35:18 -04:00
Mikayla Fischler
4869c00c0e added side type alias and added some validation to RSIO 2023-10-19 23:22:04 -04:00
Mikayla Fischler
ff4a5a68d9 reactor PLC configurator emercoolcolor correction 2023-10-19 23:20:41 -04:00
Mikayla Fischler
d77a527b15 added text alignment to push buttons and added keyboard events to listbox 2023-10-19 23:20:04 -04:00
Mikayla Fischler
01caca48dc listbox improvements, tabbing while staying in frame (autoscroll) 2023-10-15 17:02:48 -04:00
Mikayla Fischler
43e545b6ae fixed unfocus all 2023-10-15 16:49:03 -04:00
Mikayla Fischler
8b65956dcc #306 base RTU configurator 2023-10-15 13:26:49 -04:00
Mikayla
7b522ae120 Merge pull request #360 from MikaylaFischler/devel
2023.10.14 Release
2023-10-14 19:46:33 -04:00
Mikayla Fischler
43d134d9ad comment clarity 2023-10-14 18:49:51 -04:00
Mikayla Fischler
24c787f47d #353 fixed auto lock not restoring on reconnect 2023-10-14 17:57:50 -04:00
Mikayla Fischler
f95ac8be8c #359 drop packets with nil distances if using trusted range feature 2023-10-14 12:17:25 -04:00
Mikayla Fischler
41442012c2 #357 min length of auth key and some cleanup, added config change log 2023-10-14 12:02:25 -04:00
Mikayla Fischler
6f1195dded fixed element assertion scope on frame validations 2023-10-14 11:40:36 -04:00
Mikayla Fischler
86e8feaabc #358 fixed non-networked PLC operation 2023-10-14 10:07:56 -04:00
Mikayla Fischler
670ca78a5b #356 fixed extract assert msg 2023-10-14 00:38:13 -04:00
Mikayla Fischler
73ceed0f60 #354 another reversion 2023-10-13 23:53:15 -04:00
Mikayla Fischler
8412270772 #354 some reversions 2023-10-13 23:48:30 -04:00
Mikayla Fischler
686b47898c #354 optimizations/minification 2023-10-08 12:47:51 -04:00
Mikayla
e03eaf2982 #354 type check functions 2023-10-07 20:57:24 +00:00
Mikayla
8b1775b0af Merge pull request #352 from MikaylaFischler/devel
2023.10.04 Hotfix
2023-10-04 20:03:06 -04:00
Mikayla
8bbd04d133 Update feature_request.md 2023-10-04 17:32:43 -04:00
Mikayla
26dc6ff6d1 #350 handle RPS_DISABLE ack packet on supervisor 2023-10-04 21:28:14 +00:00
Mikayla
24d190921d #349 F_ALARM_ANY rsio output added 2023-10-04 21:26:07 +00:00
Mikayla
4fec116e93 Merge pull request #348 from MikaylaFischler/devel
2023.10.03 Release
2023-10-04 00:11:47 -04:00
Mikayla Fischler
ef6fdaa3ac don't report settings files as not used 2023-10-03 23:39:14 -04:00
Mikayla
0160d4c53a Merge pull request #347 from MikaylaFischler/145-graphical-configure-utilities
#307 Reactor PLC Configurator
2023-10-03 23:18:02 -04:00
Mikayla Fischler
d2a1951b66 refactored TEXT_ALIGN to ALIGN 2023-10-03 23:16:46 -04:00
Mikayla Fischler
5d7a0b266a handle new settings file and not deleting legacy config 2023-10-03 23:11:52 -04:00
Mikayla Fischler
ebabd99f2b #307 fixes and cleanup 2023-10-03 22:52:13 -04:00
Mikayla Fischler
b5e0183e54 luacheck fix and added keys to luacheck globals 2023-10-01 19:16:44 -04:00
Mikayla Fischler
d450c9ca3e Merge branch 'devel' into 145-graphical-configure-utilities 2023-10-01 18:43:28 -04:00
Mikayla Fischler
894229831d #307 configure bugfixes and settings file rename 2023-10-01 17:12:59 -04:00
Mikayla Fischler
bfa24b3665 #307 PLC integration with new config storage 2023-10-01 17:10:16 -04:00
Mikayla Fischler
b1446637ad checkbox default val and radio type checks for set_value 2023-10-01 17:06:24 -04:00
Mikayla Fischler
02e9c09daf #307 configurator error reporting 2023-10-01 15:30:49 -04:00
Mikayla Fischler
21d5cb3858 #307 reactor PLC configurator 2023-10-01 00:21:46 -04:00
Mikayla Fischler
c0a602385d recycle log at <512B free 2023-10-01 00:20:19 -04:00
Mikayla Fischler
4d4dd4ed39 fix to redraw and improvements to hide() 2023-10-01 00:19:16 -04:00
Mikayla Fischler
3a5d69d96f improvements to number field 2023-10-01 00:18:57 -04:00
Mikayla Fischler
d38a2dea7c #344 renderer integration with new assertion handling 2023-09-30 13:31:41 -04:00
Mikayla Fischler
560d48084a #344 coordinator renderer assert handling 2023-09-30 12:19:04 -04:00
Mikayla Fischler
625feb3fd1 #344 graphics assertion overhaul 2023-09-30 11:46:47 -04:00
Mikayla Fischler
ed4180a072 #344 element redraws and shorter assert messages 2023-09-29 19:34:10 -04:00
Mikayla Fischler
70831b49d2 #344 2D radio button array 2023-09-24 22:27:39 -04:00
Mikayla Fischler
881a120d34 #145 more work on plc configurator 2023-09-23 20:22:02 -04:00
Mikayla Fischler
8ab1307b2b #344 include holding down keys for number fields 2023-09-23 16:50:54 -04:00
Mikayla Fischler
689d474796 #344 support hiding characters in text fields 2023-09-23 16:45:33 -04:00
Mikayla Fischler
18bcfb4014 #344 nav to start/end of fields 2023-09-23 15:30:53 -04:00
Mikayla Fischler
645a5f5137 #344 added focus navigation to checkboxes and radio buttons, refactor of enable handlers 2023-09-23 14:31:37 -04:00
Mikayla Fischler
1f9743efd0 #344 don't hide cursor at end of input length 2023-09-23 13:45:00 -04:00
Mikayla Fischler
9cef6e6175 #344 added double click events to event handlers 2023-09-23 12:58:09 -04:00
Mikayla Fischler
70b03896d5 added double click to custom events 2023-09-23 12:53:05 -04:00
Mikayla Fischler
f9d0ef60b4 #344 select all and improved input fields 2023-09-23 12:49:31 -04:00
Mikayla Fischler
09ab60f79d #344 double click support 2023-09-23 00:11:45 -04:00
Mikayla Fischler
d21604ea09 #344 improvements to text fields 2023-09-23 00:09:37 -04:00
Mikayla Fischler
611b048cb4 #307 work in progress PLC configurator 2023-09-20 00:02:30 -04:00
Mikayla Fischler
a2182d9566 #344 work in progress on text field & paste events, re-show number field on val/min/max changes 2023-09-20 00:02:21 -04:00
Mikayla Fischler
7a87499aa4 #344 radio button appearance changes 2023-09-20 00:02:05 -04:00
Mikayla Fischler
b173b72f21 #344 numeric field cleanup 2023-09-20 00:01:49 -04:00
Mikayla
29cc107ea5 #329 updated comment 2023-09-19 20:40:11 +00:00
Mikayla
c24766a4db #329 disable reactor rather than trip on auto control stop 2023-09-19 20:37:15 +00:00
Mikayla Fischler
29e910ba3c #342 added element focusing feature to graphics library 2023-09-16 21:08:28 -04:00
Mikayla Fischler
1cb240b1b0 improved ignoring mouse events for hidden elements 2023-09-05 15:32:45 -04:00
Mikayla Fischler
1525ed9d60 Merge branch 'devel' into 145-graphical-configure-utilities 2023-09-03 19:38:17 -04:00
Mikayla Fischler
b1c2c4d291 #339 added sum of raw waste stat to flow monitor 2023-09-03 18:07:34 -04:00
Mikayla Fischler
5585088e3a #338 resolved diagnostic warnings 2023-09-03 17:54:39 -04:00
Mikayla
b9073153b3 Merge pull request #341 from MikaylaFischler/300-code-footprint-cleanup-tasks
300 code footprint cleanup tasks
2023-09-01 22:52:47 -04:00
Mikayla Fischler
cb554e5d16 luacheck fixes 2023-09-01 22:51:02 -04:00
Mikayla Fischler
71d8b5ba0a #300 utilizing style file for common color pairs 2023-09-01 22:24:31 -04:00
Mikayla
e4f49e9949 #145 configure bootstrap command and size reduction of startup/initenv 2023-09-01 14:23:39 +00:00
Mikayla Fischler
3afc765f72 #300 graphics alias functions 2023-08-30 21:11:57 -04:00
Mikayla Fischler
048714817e #300 replaced util.strrep with string.rep 2023-08-30 19:30:46 -04:00
Mikayla
f267a4e569 #300 comms device type cleanup 2023-08-30 21:15:42 +00:00
Mikayla
cfc6479dd5 #300 comms cleanup 2023-08-30 20:45:48 +00:00
Mikayla
70f24edb53 Merge pull request #337 from MikaylaFischler/305-detailed-info-on-multi-condition-alarms
305 detailed info on multi condition alarms
2023-08-30 09:00:16 -04:00
Mikayla Fischler
31df4a7f7e removed unnecessary parentheses 2023-08-29 22:41:56 -04:00
Mikayla Fischler
ca49cf90b4 #305 improved log message clarity 2023-08-29 22:34:30 -04:00
Mikayla
785dbe9533 #305 print out cause of multi-condition alarms 2023-08-29 13:19:50 +00:00
Mikayla Fischler
a9d1bc2b50 #336 consolidated remove and purge into uninstall, added clarification on low space handling 2023-08-28 23:19:30 -04:00
Mikayla
f7766d8cba Merge pull request #334 from MikaylaFischler/devel
2023.08.27 Release
2023-08-27 14:02:43 -04:00
Mikayla Fischler
37f8b85924 #333 always set emergency coolant state 2023-08-27 13:42:25 -04:00
Mikayla Fischler
2ed28cf74d #324 fixed alarm sounder lag 2023-08-26 19:01:22 -04:00
Mikayla Fischler
17698b7fb4 #332 fixed turbine production rate on coordinator UI 2023-08-26 12:22:47 -04:00
Mikayla Fischler
386a33ffd8 #298 consistent log tags 2023-08-26 11:54:58 -04:00
Mikayla Fischler
b7d4468cea #327 close connections on timeout 2023-08-25 21:42:35 -04:00
Mikayla Fischler
8b0a5d529e #330 close coordinator comms on error exit 2023-08-25 21:02:24 -04:00
Mikayla Fischler
d18a93f7d2 #326 added commas to dynamic tank fill 2023-08-25 20:53:28 -04:00
Mikayla Fischler
89d1087b1c updated flow monitor to say boiler when 1, boilers when 2 2023-08-25 20:49:38 -04:00
Mikayla Fischler
d9e48f5cac #325 fixed coordinator unit overview height calcs 2023-08-25 20:02:59 -04:00
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 Fischler
4aba79f232 Merge branch 'devel' into front-panels 2023-05-20 09:44:26 -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
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
180 changed files with 14467 additions and 7867 deletions

View File

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

View File

@@ -28,4 +28,4 @@ jobs:
# --no-max-line-length = Disable warnings for long line lengths # --no-max-line-length = Disable warnings for long line lengths
# --exclude-files ... = Exclude lockbox library (external) and config files # --exclude-files ... = Exclude lockbox library (external) and config files
# --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os' # --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os'
args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http 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 keys 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

@@ -5,6 +5,7 @@
"colors", "colors",
"fs", "fs",
"http", "http",
"keys",
"parallel", "parallel",
"periphemu", "periphemu",
"peripheral", "peripheral",
@@ -24,6 +25,7 @@
}, },
"Lua.hint.setType": true, "Lua.hint.setType": true,
"Lua.diagnostics.disable": [ "Lua.diagnostics.disable": [
"duplicate-set-field" "duplicate-set-field",
"inject-field"
] ]
} }

View File

@@ -7,29 +7,20 @@ 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)
Mod Requirements: ### [Join](https://discord.gg/R9NSCkhcwt) the Discord!
- CC: Tweaked
- Mekanism v10.1+
Mod Recommendations: ![Discord](https://img.shields.io/discord/1129075839288496259)
- Advanced Peripherals (adds the capability to detect environmental radiation levels)
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 ## Released Component Versions
### Core ![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) ![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) ![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)
### 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) ![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) ![RTU](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Frtu.json)
@@ -37,11 +28,23 @@ There was also an apparent bug with boilers disconnecting and reconnecting when
![Coordinator](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fcoordinator.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) ![Pocket](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fpocket.json)
## Requirements
Mod Requirements:
- CC: Tweaked
- Mekanism v10.1+
Mod Recommendations:
- Advanced Peripherals (adds the capability to detect environmental radiation levels)
- Immersive Engineering (provides bundled redstone, though any mod containing bundled redstone will do)
v10.1+ is required due to the complete support of CC:Tweaked added in Mekanism v10.1
## 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 +89,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+!

795
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,98 @@ 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.11c"
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 cyan() term.setTextColor(colors.cyan) 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 +120,140 @@ 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)) and (val ~= "config.lua" ) then ---@fixme remove condition after migration to settings files
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") ---@fixme fix after migration to settings files?
lgray()
local ls = fs.list("/")
for _, val in pairs(ls) do
if fs.isDir(val) then
if tree[val] ~= nil then _clean_dir("/" .. val, tree[val]) end
if #fs.list(val) == 0 then fs.delete(val);println("deleted " .. val) end
elseif not _in_array(val, tree) and (string.find(val, ".settings") == nil) 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 available")
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.lua")
println(" update - update files EXCEPT for config/logs") println(" update - update files EXCEPT for config.lua")
println(" remove - delete files EXCEPT for config/logs") println(" uninstall - delete files INCLUDING config/logs")
println(" purge - delete files INCLUDING config/logs") white();println("<app>");lgray()
term.setTextColor(colors.white)
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", "uninstall" })
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,105 @@ 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(" (") cyan();print(value);white();println(" available)")
term.setTextColor(colors.cyan) else green();println(" (up to date)") end
print(value)
term.setTextColor(colors.white)
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") cyan();print(value);white();println(")")
term.setTextColor(colors.white)
print(" (latest ")
term.setTextColor(colors.cyan)
print(value)
term.setTextColor(colors.white)
println(")")
end end
end end
if manifest.versions.installer ~= local_manifest.versions.installer then
yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white()
end
elseif mode == "install" or mode == "update" then 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 uninstall 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 different version of the installer is available, it is recommended to update to it.");white() end
if update_installer or ask_y_n("Would you like to update now") then
lgray();println("GET ccmsi.lua")
local dl, err = http.get(repo_path .. "ccmsi.lua")
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 +387,46 @@ 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("NOTICE: Insufficient space available for a full cached download!");white()
println("WARNING: Insufficient space available for a full download!") lgray();println("Files can instead be downloaded one by one. If you are replacing a current install this may corrupt your install ONLY if it fails (such as a sudden network issue). If that occurs, you can still try again.")
term.setTextColor(colors.white) if mode == "update" then println("If installation still fails, delete this device's log file and/or any unrelated files you have on this computer then try again.") end
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.") white();
println("Do you wish to continue? (y/N)") if not ask_y_n("Do you wish to continue", false) then
println("Operation cancelled.")
local confirm = read()
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 +441,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 +462,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 +491,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,144 +505,103 @@ 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 == "uninstall" 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
elseif mode == "remove" and manifest.versions[app] == nil then
term.setTextColor(colors.red)
println(app .. " is not installed")
term.setTextColor(colors.white)
return return
end end
term.setTextColor(colors.orange) if manifest.versions[app] == nil then
if mode == "remove" then red();println("Error: '" .. app .. "' is not installed.")
println("removing all " .. app .. " files except for config.lua and log.txt...") return
elseif mode == "purge" then
println("purging all " .. app .. " files...")
end end
---@diagnostic disable-next-line: undefined-field orange();println("Uninstalling all " .. app .. " files...")
os.sleep(2)
-- ask for confirmation
if not ask_y_n("Continue", false) then return end
-- delete unused files first
clean(manifest)
local file_list = manifest.files local file_list = manifest.files
local dependencies = manifest.depends[app] local dependencies = manifest.depends[app]
local config_file = app .. "/config.lua"
table.insert(dependencies, app) table.insert(dependencies, app)
term.setTextColor(colors.lightGray) -- delete log file
local log_deleted = false
local settings_file = app .. ".settings"
local legacy_config_file = app .. "/config.lua"
-- delete log file if purging lgray()
if mode == "purge" and fs.exists(config_file) then if fs.exists(legacy_config_file) then
local log_deleted = pcall(function () log_deleted = pcall(function ()
local config = require(app .. ".config") local config = require(app .. ".config")
if fs.exists(config.LOG_PATH) then if fs.exists(config.LOG_PATH) then
fs.delete(config.LOG_PATH) fs.delete(config.LOG_PATH)
println("deleted log file " .. config.LOG_PATH) println("deleted log file " .. config.LOG_PATH)
end end
end) end)
elseif fs.exists(settings_file) and settings.load(settings_file) then
if not log_deleted then local log = settings.get("LogPath")
term.setTextColor(colors.red) if log ~= nil and fs.exists(log) then
println("failed to delete log file") log_deleted = true
term.setTextColor(colors.lightGray) fs.delete(log)
---@diagnostic disable-next-line: undefined-field println("deleted log file " .. log)
os.sleep(1)
end end
end end
-- delete all files except config unless purging if not log_deleted then
red();println("Failed to delete log file.")
white();println("press any key to continue...")
any_key();lgray()
end
-- delete all installed files
for _, dependency in pairs(dependencies) do for _, dependency in pairs(dependencies) do
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do for _, file in pairs(files) do
if mode == "purge" or file ~= config_file then if fs.exists(file) then fs.delete(file);println("deleted " .. file) end
if fs.exists(file) then
fs.delete(file)
println("deleted " .. file)
end
end
end end
-- delete folders that we should be deleteing local folder = files[1]
if mode == "purge" or dependency ~= app then while true do
local folder = files[1] local dir = fs.getDir(folder)
while true do if dir == "" or dir == ".." then break else folder = dir end
local dir = fs.getDir(folder) end
if dir == "" or dir == ".." then
break
else
folder = dir
end
end
if fs.isDir(folder) then if fs.isDir(folder) then
fs.delete(folder) fs.delete(folder)
println("deleted directory " .. folder) println("deleted directory " .. folder)
end
elseif dependency == app then
for _, folder in pairs(files) do
while true do
local dir = fs.getDir(folder)
if dir == "" or dir == ".." or dir == app then
break
else
folder = dir
end
end
if folder ~= app and fs.isDir(folder) then
fs.delete(folder)
println("deleted app subdirectory " .. folder)
end
end
end end
end end
-- only delete manifest if purging if fs.exists(settings_file) then
if mode == "purge" then fs.delete(settings_file)
fs.delete("install_manifest.json") println("deleted " .. settings_file)
println("deleted install_manifest.json")
else
-- remove all data from versions list to show nothing is installed
manifest.versions = {}
imfile = fs.open("install_manifest.json", "w")
imfile.write(textutils.serializeJSON(manifest))
imfile.close()
end end
term.setTextColor(colors.green) fs.delete("install_manifest.json")
println("done!") println("deleted install_manifest.json")
green();println("Done!")
end end
term.setTextColor(colors.white) white()

16
configure.lua Normal file
View File

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

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,6 +26,9 @@ 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

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,16 +13,17 @@ 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
local ESTABLISH_ACK = comms.ESTABLISH_ACK local ESTABLISH_ACK = comms.ESTABLISH_ACK
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE local CRDN_TYPE = comms.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, ---@type table|nil
primary_name = "", primary_name = "",
flow = nil, ---@type table|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,41 +245,41 @@ 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 MGMT_TYPE|CRDN_TYPE
---@param msg table ---@param msg table
local function _send_sv(protocol, msg_type, msg) local function _send_sv(protocol, msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
@@ -261,34 +294,35 @@ 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(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
local function _send_establish() local function _send_establish()
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRDN }) _send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRD })
end end
-- keep alive ack -- keep alive ack
---@param srv_time integer ---@param srv_time integer
local function _send_keep_alive_ack(srv_time) local function _send_keep_alive_ack(srv_time)
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) _send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
end end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
@@ -296,88 +330,84 @@ 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
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {}) self.sv_r_seq_num = nil
end iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {})
-- 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 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, 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
---@param config coord_auto_config configuration ---@param config coord_auto_config configuration
function public.send_auto_start(config) function public.send_auto_start(config)
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits
}) })
end end
@@ -385,9 +415,9 @@ 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, CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
end end
-- parse a packet -- parse a packet
@@ -396,15 +426,12 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
---@param reply_to integer ---@param reply_to integer
---@param message any ---@param message any
---@param distance integer ---@param distance integer
---@return mgmt_frame|crdn_frame|capi_frame|nil packet ---@return mgmt_frame|crdn_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()
@@ -417,12 +444,6 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
if crdn_pkt.decode(s_pkt) then if crdn_pkt.decode(s_pkt) then
pkt = crdn_pkt.get() pkt = crdn_pkt.get()
end end
-- get as coordinator API packet
elseif s_pkt.protocol() == PROTOCOL.COORD_API then
local capi_pkt = comms.capi_packet()
if capi_pkt.decode(s_pkt) then
pkt = capi_pkt.get()
end
else else
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true) log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
end end
@@ -432,40 +453,46 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
end end
-- handle a packet -- handle a packet
---@param packet mgmt_frame|crdn_frame|capi_frame|nil ---@param packet mgmt_frame|crdn_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)
---@cast packet capi_frame 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.SCADA_CRDN then
---@cast packet crdn_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 -- coordinator packet
if session ~= nil then if session ~= nil then
-- pass the packet onto the session handler -- pass the packet onto the session handler
session.in_queue.push_packet(packet) session.in_queue.push_packet(packet)
else else
-- any other packet should be session related, discard it -- any other packet should be session related, discard it
log.debug("discarding COORD_API packet without a known session") log.debug("discarding SCADA_CRDN packet without a known session")
end end
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
-- pass the packet onto the session handler -- pass the packet onto the session handler
session.in_queue.push_packet(packet) session.in_queue.push_packet(packet)
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then elseif packet.type == 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 +500,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
@@ -520,7 +547,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
if protocol == PROTOCOL.SCADA_CRDN then if protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame ---@cast packet crdn_frame
if self.sv_linked then if self.sv_linked then
if packet.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then if packet.type == CRDN_TYPE.INITIAL_BUILDS then
if packet.length == 2 then if packet.length == 2 then
-- record builds -- record builds
local fac_builds = iocontrol.record_facility_builds(packet.data[1]) local fac_builds = iocontrol.record_facility_builds(packet.data[1])
@@ -528,31 +555,31 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
if fac_builds and unit_builds then if fac_builds and unit_builds then
-- acknowledge receipt of builds -- acknowledge receipt of builds
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.INITIAL_BUILDS, {}) _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.INITIAL_BUILDS, {})
else else
log.debug("received invalid INITIAL_BUILDS packet") log.debug("received invalid INITIAL_BUILDS packet")
end end
else else
log.debug("INITIAL_BUILDS packet length mismatch") log.debug("INITIAL_BUILDS packet length mismatch")
end end
elseif packet.type == SCADA_CRDN_TYPE.FAC_BUILDS then elseif packet.type == CRDN_TYPE.FAC_BUILDS then
if packet.length == 1 then if packet.length == 1 then
-- record facility builds -- record facility builds
if iocontrol.record_facility_builds(packet.data[1]) then if iocontrol.record_facility_builds(packet.data[1]) then
-- acknowledge receipt of builds -- acknowledge receipt of builds
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_BUILDS, {}) _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_BUILDS, {})
else else
log.debug("received invalid FAC_BUILDS packet") log.debug("received invalid FAC_BUILDS packet")
end end
else else
log.debug("FAC_BUILDS packet length mismatch") log.debug("FAC_BUILDS packet length mismatch")
end end
elseif packet.type == SCADA_CRDN_TYPE.FAC_STATUS then elseif packet.type == CRDN_TYPE.FAC_STATUS then
-- update facility status -- update facility status
if not iocontrol.update_facility_status(packet.data) then if not iocontrol.update_facility_status(packet.data) then
log.debug("received invalid FAC_STATUS packet") log.debug("received invalid FAC_STATUS packet")
end end
elseif packet.type == SCADA_CRDN_TYPE.FAC_CMD then elseif packet.type == CRDN_TYPE.FAC_CMD then
-- facility command acknowledgement -- facility command acknowledgement
if packet.length >= 2 then if packet.length >= 2 then
local cmd = packet.data[1] local cmd = packet.data[1]
@@ -570,30 +597,34 @@ 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
else else
log.debug("SCADA_CRDN facility command ack packet length mismatch") log.debug("SCADA_CRDN facility command ack packet length mismatch")
end end
elseif packet.type == SCADA_CRDN_TYPE.UNIT_BUILDS then elseif packet.type == CRDN_TYPE.UNIT_BUILDS then
-- record builds -- record builds
if packet.length == 1 then if packet.length == 1 then
if iocontrol.record_unit_builds(packet.data[1]) then if iocontrol.record_unit_builds(packet.data[1]) then
-- acknowledge receipt of builds -- acknowledge receipt of builds
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_BUILDS, {}) _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_BUILDS, {})
else else
log.debug("received invalid UNIT_BUILDS packet") log.debug("received invalid UNIT_BUILDS packet")
end end
else else
log.debug("UNIT_BUILDS packet length mismatch") log.debug("UNIT_BUILDS packet length mismatch")
end end
elseif packet.type == SCADA_CRDN_TYPE.UNIT_STATUSES then elseif packet.type == CRDN_TYPE.UNIT_STATUSES then
-- update statuses -- update statuses
if not iocontrol.update_unit_statuses(packet.data) then if not iocontrol.update_unit_statuses(packet.data) then
log.debug("received invalid UNIT_STATUSES packet") log.debug("received invalid UNIT_STATUSES packet")
end end
elseif packet.type == SCADA_CRDN_TYPE.UNIT_CMD then elseif packet.type == CRDN_TYPE.UNIT_CMD then
-- unit command acknowledgement -- unit command acknowledgement
if packet.length == 3 then if packet.length == 3 then
local cmd = packet.data[1] local cmd = packet.data[1]
@@ -634,36 +665,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 == 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 == 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 == 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 +741,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 +762,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 +769,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

File diff suppressed because it is too large Load Diff

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,15 +5,22 @@
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")
local core = require("graphics.core")
local flasher = require("graphics.flasher") local flasher = require("graphics.flasher")
local DisplayBox = require("graphics.elements.displaybox") local DisplayBox = require("graphics.elements.displaybox")
---@class coord_renderer
local renderer = {} local renderer = {}
-- render engine -- render engine
@@ -21,10 +28,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 +53,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 +101,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,44 +133,123 @@ function renderer.init_dmesg()
log.direct_dmesg(engine.dmesg_window) log.direct_dmesg(engine.dmesg_window)
end end
-- start the coordinator GUI -- try to start the front panel
function renderer.start_ui() ---@return boolean success, any error_msg
function renderer.try_start_fp()
local status, msg = true, nil
if not engine.fp_ready then
-- show front panel view on terminal
status, msg = pcall(function ()
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root}
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
end)
if status then
-- start flasher callback task and report ready
flasher.run()
engine.fp_ready = true
else
-- report fail and close front panel
msg = core.extract_assert_msg(msg)
renderer.close_fp()
end
end
return status, msg
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
-- try to start the main GUI
---@return boolean success, any error_msg
function renderer.try_start_ui()
local status, msg = true, nil
if not engine.ui_ready then if not engine.ui_ready then
-- hide dmesg -- hide dmesg
engine.dmesg_window.setVisible(false) engine.dmesg_window.setVisible(false)
-- show main view on main monitor status, msg = pcall(function ()
engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root} -- show main view on main monitor
main_view(engine.ui.main_display) if engine.monitors.primary ~= nil then
engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root}
main_view(engine.ui.main_display)
end
-- show unit views on unit displays -- show flow view on flow monitor
for i = 1, #engine.monitors.unit_displays do if engine.monitors.flow ~= nil then
engine.ui.unit_displays[i] = DisplayBox{window=engine.monitors.unit_displays[i],fg_bg=style.root} engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root}
unit_view(engine.ui.unit_displays[i], i) flow_view(engine.ui.flow_display)
end
-- show unit views on unit displays
for idx, display in pairs(engine.monitors.unit_displays) do
engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
unit_view(engine.ui.unit_displays[idx], idx)
end
end)
if status then
-- start flasher callback task and report ready
flasher.run()
engine.ui_ready = true
else
-- report fail and close ui
msg = core.extract_assert_msg(msg)
renderer.close_ui()
end end
-- start flasher callback task
flasher.run()
-- report ui as ready
engine.ui_ready = true
end end
return status, msg
end 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
-- delete element trees -- delete element trees
if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end
for _, display in ipairs(engine.ui.unit_displays) do display.delete() 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 +260,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 monitor peripheral being disconnected
---@param device table monitor
---@return boolean is_used if the monitor is one of the configured monitors
function renderer.handle_disconnect(device)
local is_used = false
if engine.monitors ~= nil then
if engine.monitors.primary == device then
if engine.ui.main_display ~= nil then
-- delete element tree and clear root UI elements
engine.ui.main_display.delete()
end
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
for idx, monitor in pairs(engine.monitors.unit_displays) do
if monitor == device then
if engine.ui.unit_displays[idx] ~= nil then
engine.ui.unit_displays[idx].delete()
end
is_used = true
engine.monitors.unit_displays[idx] = nil
engine.ui.unit_displays[idx] = nil
iocontrol.fp_monitor_state(idx, false)
break
end
end
end
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 -- handle a touch event
---@param event mouse_interaction|nil ---@param event mouse_interaction|nil
function renderer.handle_mouse(event) function renderer.handle_mouse(event)
if engine.ui_ready and event ~= nil then if event ~= nil then
if event.monitor == engine.monitors.primary_name then if engine.fp_ready and event.monitor == "terminal" then
engine.ui.main_display.handle_mouse(event) engine.ui.front_panel.handle_mouse(event)
else elseif engine.ui_ready then
for id, monitor in ipairs(engine.monitors.unit_name_map) do if event.monitor == engine.monitors.primary_name then
if event.monitor == monitor then engine.ui.main_display.handle_mouse(event)
local layout = engine.ui.unit_displays[id] ---@type graphics_element elseif event.monitor == engine.monitors.flow_name then
layout.handle_mouse(event) 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,15 +1,15 @@
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 CRDN_TYPE = comms.CRDN_TYPE
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
local println = util.println
-- retry time constants in ms -- retry time constants in ms
-- local INITIAL_WAIT = 1500 -- local INITIAL_WAIT = 1500
@@ -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,45 +62,46 @@ 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 CRDN packet
-----@param msg_type CAPI_TYPE -----@param msg_type CRDN_TYPE
-----@param msg table -----@param msg table
-- local function _send(msg_type, msg) -- local function _send(msg_type, msg)
-- local s_pkt = comms.scada_packet() -- local s_pkt = comms.scada_packet()
-- local c_pkt = comms.capi_packet() -- local c_pkt = comms.crdn_packet()
-- c_pkt.make(msg_type, msg) -- c_pkt.make(msg_type, msg)
-- s_pkt.make(self.seq_num, PROTOCOL.COORD_API, c_pkt.raw_sendable()) -- s_pkt.make(self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable())
-- out_queue.push_packet(s_pkt) -- out_queue.push_packet(s_pkt)
-- self.seq_num = self.seq_num + 1 -- self.seq_num = self.seq_num + 1
-- end -- end
-- send a SCADA management packet -- send a SCADA management packet
---@param msg_type SCADA_MGMT_TYPE ---@param msg_type MGMT_TYPE
---@param msg table ---@param msg table
local function _send_mgmt(msg_type, msg) local function _send_mgmt(msg_type, msg)
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(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
end end
-- handle a packet -- handle a packet
---@param pkt mgmt_frame|capi_frame ---@param pkt mgmt_frame|crdn_frame
local function _handle_packet(pkt) local function _handle_packet(pkt)
-- check sequence number -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num == nil then
@@ -115,19 +117,17 @@ function api.new_session(id, in_queue, out_queue, timeout)
self.conn_watchdog.feed() self.conn_watchdog.feed()
-- process packet -- process packet
if pkt.scada_frame.protocol() == PROTOCOL.COORD_API then if pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
---@cast pkt capi_frame ---@cast pkt crdn_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
else else
log.debug(log_header .. "handler received unsupported CAPI packet type " .. pkt.type) log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type)
end end
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
---@cast pkt mgmt_frame ---@cast pkt mgmt_frame
if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then if pkt.type == MGMT_TYPE.KEEP_ALIVE then
-- keep alive reply -- keep alive reply
if pkt.length == 2 then if pkt.length == 2 then
local srv_start = pkt.data[1] local srv_start = pkt.data[1]
@@ -136,15 +136,17 @@ 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
elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session -- close the session
_close() _close()
else else
@@ -172,8 +174,7 @@ function api.new_session(id, in_queue, out_queue, timeout)
-- close the connection -- close the connection
function public.close() function public.close()
_close() _close()
_send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) _send_mgmt(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
@@ -229,7 +229,7 @@ function api.new_session(id, in_queue, out_queue, timeout)
periodics.keep_alive = periodics.keep_alive + elapsed periodics.keep_alive = periodics.keep_alive + elapsed
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
_send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() }) _send_mgmt(MGMT_TYPE.KEEP_ALIVE, { util.time() })
periodics.keep_alive = 0 periodics.keep_alive = 0
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.any_active() 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.15.1" local COORDINATOR_VERSION = "v1.0.17"
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)
@@ -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,52 @@ 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 = renderer.try_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...") log_graphics(util.c("front panel UI error: ", fp_message))
log.fatal("failed to connect to supervisor") println_ts("front panel UI creation failed")
return false log.fatal(util.c("front panel GUI render failed with error ", fp_message))
end
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 = renderer.try_start_ui()
if not ui_ok then if not ui_ok then
renderer.close_ui() log_graphics(util.c("main UI error: ", ui_message))
log_graphics(util.c("UI crashed: ", message)) log.fatal(util.c("main GUI render failed with error ", ui_message))
println_ts("UI crashed")
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 +232,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 +269,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 +320,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,30 +340,24 @@ 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" or event == "double_click" then
-- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) 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
@@ -366,10 +366,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 +385,23 @@ 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
-- close on error exit (such as UI error)
if coord_comms.is_linked() then coord_comms.close() 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

@@ -12,20 +12,19 @@ local VerticalBar = require("graphics.elements.indicators.vbar")
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
local text_fg_bg = style.text_colors
-- new boiler view -- new boiler view
---@param root graphics_element parent ---@param root graphics_element parent
---@param x integer top left x ---@param x integer top left x
---@param y integer top left y ---@param y integer top left y
---@param ps psil ps interface ---@param ps psil ps interface
local function new_view(root, x, y, ps) local function new_view(root, x, y, ps)
local boiler = Rectangle{parent=root,border=border(1, colors.gray, true),width=31,height=7,x=x,y=y} local boiler = Rectangle{parent=root,border=border(1,colors.gray,true),width=31,height=7,x=x,y=y}
local text_fg_bg = cpair(colors.black, colors.lightGray)
local lu_col = cpair(colors.gray, colors.gray)
local status = StateIndicator{parent=boiler,x=9,y=1,states=style.boiler.states,value=1,min_width=12} local status = StateIndicator{parent=boiler,x=9,y=1,states=style.boiler.states,value=1,min_width=12}
local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=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=style.lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg}
local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=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=style.lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg}
status.register(ps, "computed_status", status.update) status.register(ps, "computed_status", status.update)
temp.register(ps, "temperature", temp.update) temp.register(ps, "temperature", temp.update)

View File

@@ -16,7 +16,10 @@ local VerticalBar = require("graphics.elements.indicators.vbar")
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local text_fg_bg = style.text_colors
local lu_col = style.lu_colors
-- new induction matrix view -- new induction matrix view
---@param root graphics_element parent ---@param root graphics_element parent
@@ -31,14 +34,12 @@ local function new_view(root, x, y, data, ps, id)
local matrix = Div{parent=root,fg_bg=style.root,width=33,height=24,x=x,y=y} local matrix = Div{parent=root,fg_bg=style.root,width=33,height=24,x=x,y=y}
TextBox{parent=matrix,text=" ",width=33,height=1,x=1,y=1,fg_bg=cpair(colors.lightGray,colors.gray)} TextBox{parent=matrix,text=" ",width=33,height=1,x=1,y=1,fg_bg=style.lg_gray}
TextBox{parent=matrix,text=title,alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.gray)} TextBox{parent=matrix,text=title,alignment=ALIGN.CENTER,width=33,height=1,x=1,y=2,fg_bg=style.lg_gray}
local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3} local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3}
local text_fg_bg = cpair(colors.black, colors.lightGray)
local label_fg_bg = cpair(colors.gray, colors.lightGray) local label_fg_bg = cpair(colors.gray, colors.lightGray)
local lu_col = cpair(colors.gray, colors.gray)
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14} local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
local energy = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg_bg} local energy = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg_bg}
@@ -83,9 +84,7 @@ 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
charge.register(ps, "energy_fill", charge.update) charge.register(ps, "energy_fill", charge.update)

View File

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

@@ -1,4 +1,4 @@
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 iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
@@ -15,17 +15,31 @@ local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light") local IndicatorLight = require("graphics.elements.indicators.light")
local RadIndicator = require("graphics.elements.indicators.rad") local RadIndicator = require("graphics.elements.indicators.rad")
local StateIndicator = require("graphics.elements.indicators.state")
local TriIndicatorLight = require("graphics.elements.indicators.trilight") local TriIndicatorLight = require("graphics.elements.indicators.trilight")
local Checkbox = require("graphics.elements.controls.checkbox")
local HazardButton = require("graphics.elements.controls.hazard_button") local HazardButton = require("graphics.elements.controls.hazard_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.TEXT_ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
local border = core.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 dis_colors = style.dis_colors
local gry_wht = style.gray_white
local ind_grn = style.ind_grn
local ind_yel = style.ind_yel
local ind_red = style.ind_red
local ind_wht = style.ind_wht
local period = core.flasher.PERIOD local period = core.flasher.PERIOD
-- new process control view -- new process control view
@@ -33,17 +47,16 @@ local period = core.flasher.PERIOD
---@param x integer top left x ---@param x integer top left x
---@param y integer top left y ---@param y integer top left y
local function new_view(root, x, 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)") assert(root.get_height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
local black = cpair(colors.black, colors.black)
local blk_brn = cpair(colors.black, colors.brown)
local blk_pur = cpair(colors.black, colors.purple)
local facility = iocontrol.get_db().facility local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units local units = iocontrol.get_db().units
local bw_fg_bg = cpair(colors.black, colors.white) local main = Div{parent=root,width=128,height=24,x=x,y=y}
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 scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,fg_bg=hzd_fg_bg} local 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}
@@ -51,20 +64,22 @@ local function new_view(root, x, y)
facility.scram_ack = scram.on_response facility.scram_ack = scram.on_response
facility.ack_alarms_ack = ack_a.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 all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=ind_grn}
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} 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) all_ok.register(facility.ps, "all_sys_ok", all_ok.update)
ind_mat.register(facility.induction_ps_tbl[1], "computed_status", function (status) ind_mat.update(status > 1) end)
rad_mon.register(facility.ps, "rad_computed_status", rad_mon.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() main.line_break()
local auto_ready = IndicatorLight{parent=main,label="Configured Units Ready",colors=cpair(colors.green,colors.red)} local auto_ready = IndicatorLight{parent=main,label="Configured Units Ready",colors=ind_grn}
local auto_act = IndicatorLight{parent=main,label="Process Active",colors=cpair(colors.green,colors.gray)} local auto_act = IndicatorLight{parent=main,label="Process Active",colors=ind_grn}
local auto_ramp = IndicatorLight{parent=main,label="Process Ramping",colors=cpair(colors.white,colors.gray),flash=true,period=period.BLINK_250_MS} 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=cpair(colors.yellow,colors.gray)} 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_ready.register(facility.ps, "auto_ready", auto_ready.update)
auto_act.register(facility.ps, "auto_active", auto_act.update) auto_act.register(facility.ps, "auto_active", auto_act.update)
@@ -73,12 +88,12 @@ local function new_view(root, x, y)
main.line_break() 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 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=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_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=cpair(colors.red,colors.gray),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=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_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=cpair(colors.red,colors.gray),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=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_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) auto_scram.register(facility.ps, "auto_scram", auto_scram.update)
matrix_dc.register(facility.ps, "as_matrix_dc", matrix_dc.update) matrix_dc.register(facility.ps, "as_matrix_dc", matrix_dc.update)
@@ -99,7 +114,7 @@ local function new_view(root, x, y)
-- process control -- -- process control --
--------------------- ---------------------
local proc = Div{parent=main,width=78,height=24,x=27,y=1} local proc = Div{parent=main,width=103,height=24,x=27,y=1}
----------------------------- -----------------------------
-- process control targets -- -- process control targets --
@@ -107,35 +122,35 @@ local function new_view(root, x, y)
local targets = Div{parent=proc,width=31,height=24,x=1,y=1} 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)} local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=blk_pur}
TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2} 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 burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=gry_wht}
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg} local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
TextBox{parent=burn_target,x=18,y=2,text="mB/t"} 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)} local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
b_target.register(facility.ps, "process_burn_target", b_target.set_value) b_target.register(facility.ps, "process_burn_target", b_target.set_value)
burn_sum.register(facility.ps, "burn_sum", burn_sum.update) 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)} local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=blk_pur}
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2} 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 chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=gry_wht}
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg} local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
TextBox{parent=chg_target,x=18,y=2,text="MFE"} 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)} local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
c_target.register(facility.ps, "process_charge_target", c_target.set_value) c_target.register(facility.ps, "process_charge_target", c_target.set_value)
cur_charge.register(facility.induction_ps_tbl[1], "energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end) cur_charge.register(facility.induction_ps_tbl[1], "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)} local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur}
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2} TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)} local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=gry_wht}
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg} local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
TextBox{parent=gen_target,x=18,y=2,text="kFE/t"} 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)} local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
g_target.register(facility.ps, "process_gen_target", g_target.set_value) g_target.register(facility.ps, "process_gen_target", g_target.set_value)
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end) cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end)
@@ -148,46 +163,77 @@ local function new_view(root, x, y)
local rate_limits = {} local rate_limits = {}
for i = 1, facility.num_units do for i = 1, 4 do
local unit = units[i] ---@type ioctl_unit local unit
local tag_fg_bg = gry_wht
local lim_fg_bg = style.lg_white
local ctl_fg = colors.lightGray
local cur_fg_bg = style.lg_white
local cur_lu = colors.lightGray
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 = blk_brn
cur_lu = colors.black
end
local _y = ((i - 1) * 5) + 1 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)} 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} 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)} local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(ctl_fg,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} local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=lim_fg_bg}
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1} TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1}
rate_limits[i].register(unit.unit_ps, "max_burn", rate_limits[i].set_max) 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}
rate_limits[i].register(unit.unit_ps, "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)} 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) cur_burn.register(unit.unit_ps, "act_burn_rate", cur_burn.update)
else
lim.disable()
end
end end
------------------- -------------------
-- unit statuses -- -- unit statuses --
------------------- -------------------
local stat_div = Div{parent=proc,width=38,height=19,x=57,y=6} local stat_div = Div{parent=proc,width=22,height=24,x=57,y=6}
for i = 1, facility.num_units do for i = 1, 4 do
local unit = units[i] ---@type ioctl_unit local tag_fg_bg = gry_wht
local ind_fg_bg = style.lg_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 _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)} 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} 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 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,colors.gray)} 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,colors.gray),flash=true,period=period.BLINK_250_MS} local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(colors.red,ind_off),flash=true,period=period.BLINK_250_MS}
ready.register(unit.unit_ps, "U_AutoReady", ready.update) if i <= facility.num_units then
degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update) 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 end
------------------------- -------------------------
@@ -195,18 +241,18 @@ local function new_view(root, x, y)
------------------------- -------------------------
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" } 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} local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.gray,colors.white),select_color=colors.purple}
mode.register(facility.ps, "process_mode", mode.set_value) mode.register(facility.ps, "process_mode", mode.set_value)
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg} local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg} local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=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=31,height=1,alignment=ALIGN.CENTER,fg_bg=gry_wht}
stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value) stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value)
stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value) stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value)
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=cpair(colors.gray,colors.white)} local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=gry_wht}
-- save the automatic process control configuration without starting -- save the automatic process control configuration without starting
local function _save_cfg() local function _save_cfg()
@@ -261,6 +307,60 @@ local function new_view(root, x, y)
for i = 1, #rate_limits do rate_limits[i].enable() end for i = 1, #rate_limits do rate_limits[i].enable() end
end 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=blk_brn}
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.brown)}
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17}
status.register(facility.ps, "current_waste_product", status.update)
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.gray,colors.white),select_color=colors.brown}
local pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.green,colors.black)}
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 end
return new_view return new_view

View File

@@ -14,6 +14,9 @@ local StateIndicator = require("graphics.elements.indicators.state")
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
local text_fg_bg = style.text_colors
local lu_col = style.lu_colors
-- create new reactor view -- create new reactor view
---@param root graphics_element parent ---@param root graphics_element parent
---@param x integer top left x ---@param x integer top left x
@@ -22,9 +25,6 @@ local border = core.border
local function new_view(root, x, y, ps) local function new_view(root, x, y, ps)
local reactor = Rectangle{parent=root,border=border(1, colors.gray, true),width=30,height=7,x=x,y=y} local reactor = Rectangle{parent=root,border=border(1, colors.gray, true),width=30,height=7,x=x,y=y}
local text_fg_bg = cpair(colors.black, colors.lightGray)
local lu_col = cpair(colors.gray, colors.gray)
local status = StateIndicator{parent=reactor,x=6,y=1,states=style.reactor.states,value=1,min_width=16} local status = StateIndicator{parent=reactor,x=6,y=1,states=style.reactor.states,value=1,min_width=16}
local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit="K",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg} local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit="K",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg}
local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg} local 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}

View File

@@ -15,16 +15,16 @@ local VerticalBar = require("graphics.elements.indicators.vbar")
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
local text_fg_bg = style.text_colors
local lu_col = style.lu_colors
-- new turbine view -- new turbine view
---@param root graphics_element parent ---@param root graphics_element parent
---@param x integer top left x ---@param x integer top left x
---@param y integer top left y ---@param y integer top left y
---@param ps psil ps interface ---@param ps psil ps interface
local function new_view(root, x, y, ps) local function new_view(root, x, y, ps)
local turbine = Rectangle{parent=root,border=border(1, colors.gray, true),width=23,height=7,x=x,y=y} local turbine = Rectangle{parent=root,border=border(1,colors.gray,true),width=23,height=7,x=x,y=y}
local text_fg_bg = cpair(colors.black, colors.lightGray)
local lu_col = cpair(colors.gray, colors.gray)
local status = StateIndicator{parent=turbine,x=7,y=1,states=style.turbine.states,value=1,min_width=12} local status = StateIndicator{parent=turbine,x=7,y=1,states=style.turbine.states,value=1,min_width=12}
local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg_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}
@@ -32,7 +32,7 @@ local function new_view(root, x, y, ps)
status.register(ps, "computed_status", status.update) status.register(ps, "computed_status", status.update)
prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end) prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end)
flow_rate.register(ps, "flow_rate", flow_rate.update) flow_rate.register(ps, "steam_input_rate", flow_rate.update)
local steam = VerticalBar{parent=turbine,x=2,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1} local steam = VerticalBar{parent=turbine,x=2,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
local energy = VerticalBar{parent=turbine,x=3,y=1,fg_bg=cpair(colors.green,colors.gray),height=4,width=1} local energy = VerticalBar{parent=turbine,x=3,y=1,fg_bg=cpair(colors.green,colors.gray),height=4,width=1}

View File

@@ -2,6 +2,8 @@
-- Reactor Unit SCADA Coordinator GUI -- Reactor Unit SCADA Coordinator GUI
-- --
local types = require("scada-common.types")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
@@ -26,35 +28,24 @@ 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.TEXT_ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
local period = core.flasher.PERIOD 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 waste_opts = { local gry_wht = style.gray_white
{
text = "Auto", local ind_grn = style.ind_grn
fg_bg = cpair(colors.black, colors.lightGray), local ind_yel = style.ind_yel
active_fg_bg = cpair(colors.white, colors.gray) local ind_red = style.ind_red
}, local ind_wht = style.ind_wht
{
text = "Pu", local period = core.flasher.PERIOD
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
@@ -62,17 +53,16 @@ local waste_opts = {
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=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 --
@@ -108,7 +98,7 @@ local function init(parent, id)
waste.register(u_ps, "waste_fill", waste.update) waste.register(u_ps, "waste_fill", waste.update)
ccool.register(u_ps, "ccool_type", function (type) ccool.register(u_ps, "ccool_type", function (type)
if type == "mekanism:sodium" then if type == types.FLUID.SODIUM then
ccool.recolor(cpair(colors.lightBlue, colors.gray)) ccool.recolor(cpair(colors.lightBlue, colors.gray))
else else
ccool.recolor(cpair(colors.blue, colors.gray)) ccool.recolor(cpair(colors.blue, colors.gray))
@@ -116,7 +106,7 @@ local function init(parent, id)
end) end)
hcool.register(u_ps, "hcool_type", function (type) hcool.register(u_ps, "hcool_type", function (type)
if type == "mekanism: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
hcool.recolor(cpair(colors.white, colors.gray)) hcool.recolor(cpair(colors.white, colors.gray))
@@ -144,8 +134,8 @@ local function init(parent, id)
------------------- -------------------
local u_stat = Rectangle{parent=main,border=border(1,colors.gray,true),thin=true,width=33,height=4,x=46,y=3,fg_bg=bw_fg_bg} local u_stat = Rectangle{parent=main,border=border(1,colors.gray,true),thin=true,width=33,height=4,x=46,y=3,fg_bg=bw_fg_bg}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=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=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=ALIGN.CENTER,fg_bg=gry_wht}
stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value) stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value)
stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value) stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value)
@@ -160,7 +150,7 @@ 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}
plc_online.register(u_ps, "PLCOnline", plc_online.update) plc_online.register(u_ps, "PLCOnline", plc_online.update)
@@ -170,25 +160,25 @@ local function init(parent, id)
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}
r_active.register(u_ps, "status", r_active.update) r_active.register(u_ps, "status", r_active.update)
r_auto.register(u_ps, "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}
r_scram.register(u_ps, "ReactorSCRAM", r_scram.update) r_scram.register(u_ps, "ReactorSCRAM", r_scram.update)
r_mscrm.register(u_ps, "ManualReactorSCRAM", r_mscrm.update) r_mscrm.register(u_ps, "ManualReactorSCRAM", r_mscrm.update)
@@ -205,19 +195,19 @@ local function init(parent, id)
-- RPS annunciator panel -- RPS annunciator panel
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=8} TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=8}
local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9} local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9}
local rps_annunc = Div{parent=rps,width=31,height=10,x=2,y=1} local rps_annunc = Div{parent=rps,width=31,height=10,x=2,y=1}
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}
rps_trp.register(u_ps, "rps_tripped", rps_trp.update) rps_trp.register(u_ps, "rps_tripped", rps_trp.update)
@@ -233,17 +223,17 @@ local function init(parent, id)
-- cooling annunciator panel -- cooling annunciator panel
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=22} TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=22}
local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23} local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23}
local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1} local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1}
local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7} local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7}
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}
c_flt.register(u_ps, "RCSFault", c_flt.update) c_flt.register(u_ps, "RCSFault", c_flt.update)
c_emg.register(u_ps, "EmergencyCoolant", c_emg.update) c_emg.register(u_ps, "EmergencyCoolant", c_emg.update)
@@ -266,11 +256,11 @@ 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}
b1_wll.register(b_ps[1], "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}
b1_hr.register(b_ps[1], "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
@@ -282,11 +272,11 @@ 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}
b2_wll.register(b_ps[2], "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}
b2_hr.register(b_ps[2], "HeatingRateLow", b2_hr.update) b2_hr.register(b_ps[2], "HeatingRateLow", b2_hr.update)
end end
@@ -299,15 +289,15 @@ local function init(parent, id)
t1_sdo.register(t_ps[1], "SteamDumpOpen", t1_sdo.update) t1_sdo.register(t_ps[1], "SteamDumpOpen", t1_sdo.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=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}
t1_tos.register(t_ps[1], "TurbineOverSpeed", t1_tos.update) t1_tos.register(t_ps[1], "TurbineOverSpeed", t1_tos.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=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}
t1_gtrp.register(t_ps[1], "GeneratorTrip", t1_gtrp.update) t1_gtrp.register(t_ps[1], "GeneratorTrip", t1_gtrp.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=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}
t1_trp.register(t_ps[1], "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
@@ -320,15 +310,15 @@ local function init(parent, id)
t2_sdo.register(t_ps[2], "SteamDumpOpen", t2_sdo.update) t2_sdo.register(t_ps[2], "SteamDumpOpen", t2_sdo.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=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}
t2_tos.register(t_ps[2], "TurbineOverSpeed", t2_tos.update) t2_tos.register(t_ps[2], "TurbineOverSpeed", t2_tos.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=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}
t2_gtrp.register(t_ps[2], "GeneratorTrip", t2_gtrp.update) t2_gtrp.register(t_ps[2], "GeneratorTrip", t2_gtrp.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=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}
t2_trp.register(t_ps[2], "TurbineTrip", t2_trp.update) t2_trp.register(t_ps[2], "TurbineTrip", t2_trp.update)
end end
@@ -340,15 +330,15 @@ local function init(parent, id)
t3_sdo.register(t_ps[3], "SteamDumpOpen", t3_sdo.update) t3_sdo.register(t_ps[3], "SteamDumpOpen", t3_sdo.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=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}
t3_tos.register(t_ps[3], "TurbineOverSpeed", t3_tos.update) t3_tos.register(t_ps[3], "TurbineOverSpeed", t3_tos.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=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}
t3_gtrp.register(t_ps[3], "GeneratorTrip", t3_gtrp.update) t3_gtrp.register(t_ps[3], "GeneratorTrip", t3_gtrp.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=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}
t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update) t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update)
end end
@@ -356,14 +346,12 @@ local function init(parent, id)
-- reactor controls -- -- reactor controls --
---------------------- ----------------------
local dis_colors = cpair(colors.white, colors.lightGray) local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=gry_wht}
local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=cpair(colors.gray,colors.white)}
local burn_rate = SpinboxNumeric{parent=burn_control,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=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}
burn_rate.register(u_ps, "burn_rate", burn_rate.set_value) burn_rate.register(u_ps, "burn_rate", burn_rate.set_value)
burn_rate.register(u_ps, "max_burn", burn_rate.set_max) burn_rate.register(u_ps, "max_burn", burn_rate.set_max)
@@ -394,11 +382,11 @@ local function init(parent, id)
reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end) reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=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=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}
waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value) waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)
@@ -482,20 +470,20 @@ local function init(parent, id)
-- automatic control settings -- -- automatic control settings --
-------------------------------- --------------------------------
TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=TEXT_ALIGN.CENTER,width=13,height=1,x=32,y=36} TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,height=1,x=32,y=36}
local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37} local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37}
local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1} local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1}
local ctl_opts = { "Manual", "Primary", "Secondary", "Tertiary", "Backup" } local ctl_opts = { "Manual", "Primary", "Secondary", "Tertiary", "Backup" }
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.gray,colors.white),select_color=colors.purple}
group.register(u_ps, "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=gry_wht,callback=set_group}
auto_div.line_break() auto_div.line_break()
@@ -506,8 +494,8 @@ local function init(parent, id)
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}
a_rdy.register(u_ps, "U_AutoReady", a_rdy.update) a_rdy.register(u_ps, "U_AutoReady", a_rdy.update)

View File

@@ -0,0 +1,223 @@
--
-- 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 ALIGN = core.ALIGN
local sprintf = util.sprintf
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 lg_gray = style.lg_gray
local ind_grn = style.ind_grn
local ind_wht = style.ind_wht
-- make a new unit flow window
---@param parent graphics_element parent
---@param x integer top left x
---@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}
------------------
-- 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=ALIGN.CENTER,height=1}
TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=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=ALIGN.CENTER,height=1}
TextBox{parent=boiler,y=3,text=util.trinary(unit.num_boilers>1,"BOILERS","BOILER"),alignment=ALIGN.CENTER,height=1}
TextBox{parent=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=ALIGN.CENTER,height=1}
TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=ALIGN.CENTER,height=1}
TextBox{parent=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=ALIGN.CENTER,fg_bg=lg_gray,width=l,height=1}
TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=ALIGN.CENTER,fg_bg=wh_gray,width=l,height=1}
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=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,7 +14,7 @@ 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.TEXT_ALIGN local ALIGN = core.ALIGN
local pipe = core.pipe local pipe = core.pipe
@@ -34,17 +34,17 @@ local function make(parent, x, y, unit)
if num_boilers == 0 and num_turbines == 1 then if num_boilers == 0 and num_turbines == 1 then
height = 9 height = 9
elseif num_boilers == 1 and num_turbines <= 2 then elseif num_boilers <= 1 and num_turbines <= 2 then
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}
-- unit header message -- unit header message
TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
------------- -------------
-- REACTOR -- -- REACTOR --

View File

@@ -0,0 +1,387 @@
--
-- 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 ALIGN = core.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=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=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=text_col,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=style.lg_gray}
TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=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,commas=true,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=style.lg_gray}
TextBox{parent=sps,text="SPS",alignment=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="RAW WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
local raw_waste = Rectangle{parent=main,x=145,y=17,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg}
local sum_raw_waste = DataIndicator{parent=raw_waste,lu_colors=lu_col,label="SUM",unit="mB/t",format="%8.2f",value=0,width=17}
sum_raw_waste.register(facility.ps, "burn_sum", sum_raw_waste.update)
TextBox{parent=main,x=145,y=21,text="PROC. WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=bw_fg_bg}
local pu = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17}
local po = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Po",unit="mB/t",format="%9.3f",value=0,width=17}
local popl = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="PoPl",unit="mB/t",format="%7.3f",value=0,width=17}
pu.register(facility.ps, "pu_rate", pu.update)
po.register(facility.ps, "po_rate", po.update)
popl.register(facility.ps, "po_pl_rate", popl.update)
TextBox{parent=main,x=145,y=28,text="SPENT WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
local sp_waste = Rectangle{parent=main,x=145,y=29,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg}
local sum_sp_waste = DataIndicator{parent=sp_waste,lu_colors=lu_col,label="SUM",unit="mB/t",format="%8.3f",value=0,width=17}
sum_sp_waste.register(facility.ps, "spent_waste_rate", sum_sp_waste.update)
end
return init

View File

@@ -0,0 +1,126 @@
--
-- 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 ALIGN = core.ALIGN
local cpair = core.cpair
local led_grn = style.led_grn
-- 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=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=led_grn}
status.update(true)
system.line_break()
heartbeat.register(ps, "heartbeat", heartbeat.update)
local modem = LED{parent=system,label="MODEM",colors=led_grn}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
system.line_break()
modem.register(ps, "has_modem", modem.update)
network.register(ps, "link_state", network.update)
local speaker = LED{parent=system,label="SPEAKER",colors=led_grn}
speaker.register(ps, "has_speaker", speaker.update)
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=style.fp_label}
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=led_grn}
main_monitor.register(ps, "main_monitor", main_monitor.update)
local flow_monitor = LED{parent=monitors,label="FLOW MONITOR",colors=led_grn}
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=led_grn}
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=style.fp_label}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1}
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=style.fp_text,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 = style.fp_text },
{ name = "API", color = style.fp_text },
}
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=style.bw_fg_bg}
-- link pocket API list management to PGI
pgi.link_elements(api_list, pkt_entry)
end
return init

View File

@@ -2,14 +2,12 @@
-- Main SCADA Coordinator GUI -- Main SCADA Coordinator GUI
-- --
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
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 +16,7 @@ local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.data")
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.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
@@ -29,10 +25,10 @@ local function init(main)
local units = iocontrol.get_db().units local units = iocontrol.get_db().units
-- window header message -- window header message
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=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=style.lg_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=ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
ping.register(facility.ps, "sv_ping", ping.update) ping.register(facility.ps, "sv_ping", ping.update)
datetime.register(facility.ps, "date_time", datetime.set_value) datetime.register(facility.ps, "date_time", datetime.set_value)
@@ -45,12 +41,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 +56,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 +69,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=string.rep("\x8c", header.get_width()),alignment=ALIGN.CENTER,height=1,fg_bg=style.lg_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

@@ -10,6 +10,41 @@ 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,30 @@ 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.lg_gray = cpair(colors.lightGray, colors.gray)
style.lg_white = cpair(colors.lightGray, colors.white)
style.gray_white = cpair(colors.gray, colors.white)
style.ind_grn = cpair(colors.green, colors.gray)
style.ind_yel = cpair(colors.yellow, colors.gray)
style.ind_red = cpair(colors.red, colors.gray)
style.ind_wht = style.wh_gray
style.fp_text = cpair(colors.black, colors.ivory)
style.fp_label = cpair(colors.lightGray, colors.ivory)
style.led_grn = cpair(colors.green, colors.green_off)
-- UI COMPONENTS --
style.reactor = { style.reactor = {
-- reactor states -- reactor states
@@ -151,8 +209,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

@@ -7,17 +7,15 @@ local flasher = require("graphics.flasher")
local core = {} local core = {}
core.version = "2.0.7"
core.flasher = flasher core.flasher = flasher
core.events = events core.events = events
-- Core Types -- Core Types
---@enum TEXT_ALIGN ---@enum ALIGN
core.TEXT_ALIGN = { core.ALIGN = { LEFT = 1, CENTER = 2, RIGHT = 3 }
LEFT = 1,
CENTER = 2,
RIGHT = 3
}
---@class graphics_border ---@class graphics_border
---@field width integer ---@field width integer
@@ -33,11 +31,7 @@ core.TEXT_ALIGN = {
---@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 core.border(width, color, even) function core.border(width, color, even)
return { return { width = width, color = color, even = even or false }
width = width,
color = color,
even = even or false -- convert nil to false
}
end end
---@class graphics_frame ---@class graphics_frame
@@ -54,12 +48,7 @@ end
---@param h integer ---@param h integer
---@return graphics_frame ---@return graphics_frame
function core.gframe(x, y, w, h) function core.gframe(x, y, w, h)
return { return { x = x, y = y, w = w, h = h }
x = x,
y = y,
w = w,
h = h
}
end end
---@class cpair ---@class cpair
@@ -80,15 +69,9 @@ end
function core.cpair(a, b) function core.cpair(a, b)
return { return {
-- color pairs -- color pairs
color_a = a, color_a = a, color_b = b, blit_a = colors.toBlit(a), blit_b = colors.toBlit(b),
color_b = b,
blit_a = colors.toBlit(a),
blit_b = colors.toBlit(b),
-- aliases -- aliases
fgd = a, fgd = a, bkg = b, blit_fgd = colors.toBlit(a), blit_bkg = colors.toBlit(b)
bkg = b,
blit_fgd = colors.toBlit(a),
blit_bkg = colors.toBlit(b)
} }
end end
@@ -128,4 +111,215 @@ function core.pipe(x1, y1, x2, y2, color, thin, align_tr)
} }
end end
-- Assertion Handling
-- extract the custom element assert message, dropping the path to the element file
function core.extract_assert_msg(msg)
return string.sub(msg, (string.find(msg, "@") or 0) + 1)
end
-- Interactive Field Manager
---@param e graphics_base
---@param max_len any
---@param fg_bg any
---@param dis_fg_bg any
function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
local self = {
frame_start = 1,
visible_text = e.value,
cursor_pos = string.len(e.value) + 1,
selected_all = false
}
-- update visible text
local function _update_visible()
self.visible_text = string.sub(e.value, self.frame_start, self.frame_start + math.min(string.len(e.value), e.frame.w) - 1)
end
-- try shifting frame left
local function _try_lshift()
if self.frame_start > 1 then
self.frame_start = self.frame_start - 1
return true
end
end
-- try shifting frame right
local function _try_rshift()
if (self.frame_start + e.frame.w - 1) <= string.len(e.value) then
self.frame_start = self.frame_start + 1
return true
end
end
---@class ifield
local public = {}
-- censor the display (for private info, for example) with the provided character<br>
-- disable by passing no argument
---@param censor string? character to hide data with
function public.censor(censor)
if type(censor) == "string" and string.len(censor) == 1 then
self.censor = censor
else self.censor = nil end
public.show()
end
-- show the field
function public.show()
_update_visible()
if e.enabled then
e.w_set_bkg(fg_bg.bkg)
e.w_set_fgd(fg_bg.fgd)
elseif dis_fg_bg ~= nil then
e.w_set_bkg(dis_fg_bg.bkg)
e.w_set_fgd(dis_fg_bg.fgd)
end
-- clear and print
e.w_set_cur(1, 1)
e.w_write(string.rep(" ", e.frame.w))
e.w_set_cur(1, 1)
local function _write()
if self.censor then
e.w_write(string.rep(self.censor, string.len(self.visible_text)))
else
e.w_write(self.visible_text)
end
end
if e.is_focused() and e.enabled then
-- write text with cursor
if self.selected_all then
e.w_set_bkg(fg_bg.fgd)
e.w_set_fgd(fg_bg.bkg)
_write()
elseif self.cursor_pos >= (string.len(self.visible_text) + 1) then
-- write text with cursor at the end, no need to blit
_write()
e.w_set_fgd(colors.lightGray)
e.w_write("_")
else
local a, b = "", ""
if self.cursor_pos <= string.len(self.visible_text) then
a = fg_bg.blit_bkg
b = fg_bg.blit_fgd
end
local b_fgd = string.rep(fg_bg.blit_fgd, self.cursor_pos - 1) .. a .. string.rep(fg_bg.blit_fgd, string.len(self.visible_text) - self.cursor_pos)
local b_bkg = string.rep(fg_bg.blit_bkg, self.cursor_pos - 1) .. b .. string.rep(fg_bg.blit_bkg, string.len(self.visible_text) - self.cursor_pos)
if self.censor then
e.w_blit(string.rep(self.censor, string.len(self.visible_text)), b_fgd, b_bkg)
else
e.w_blit(self.visible_text, b_fgd, b_bkg)
end
end
else
self.selected_all = false
-- write text without cursor
_write()
end
end
-- move cursor to x
---@param x integer
function public.move_cursor(x)
self.selected_all = false
self.cursor_pos = math.min(x, string.len(self.visible_text) + 1)
public.show()
end
-- select all text
function public.select_all()
self.selected_all = true
public.show()
end
-- set field value
---@param val string
function public.set_value(val)
e.value = string.sub(val, 1, math.min(max_len, string.len(val)))
public.nav_end()
end
-- try to insert a character if there is space
---@param char string
function public.try_insert_char(char)
-- limit length
if string.len(e.value) >= max_len then return end
-- replace if selected all, insert otherwise
if self.selected_all then
self.selected_all = false
self.cursor_pos = 2
self.frame_start = 1
e.value = char
public.show()
else
e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 2) .. char .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value))
_update_visible()
public.nav_right()
end
end
-- remove charcter before cursor if there is anything to remove, or delete all if selected all
function public.backspace()
if self.selected_all then
self.selected_all = false
e.value = ""
self.cursor_pos = 1
self.frame_start = 1
public.show()
else
if self.frame_start + self.cursor_pos > 2 then
e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 3) .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value))
if self.cursor_pos > 1 then
self.cursor_pos = self.cursor_pos - 1
public.show()
elseif _try_lshift() then public.show() end
end
end
end
-- move cursor left by one
function public.nav_left()
if self.cursor_pos > 1 then
self.cursor_pos = self.cursor_pos - 1
public.show()
elseif _try_lshift() then public.show() end
end
-- move cursor right by one
function public.nav_right()
if self.cursor_pos < math.min(string.len(self.visible_text) + 1, e.frame.w) then
self.cursor_pos = self.cursor_pos + 1
public.show()
elseif _try_rshift() then public.show() end
end
-- move cursor to the start
function public.nav_start()
self.cursor_pos = 1
self.frame_start = 1
public.show()
end
-- move cursor to the end
function public.nav_end()
self.frame_start = math.max(1, string.len(e.value) - e.frame.w + 2)
_update_visible()
self.cursor_pos = string.len(self.visible_text) + 1
public.show()
end
return public
end
return core return core

File diff suppressed because it is too large Load Diff

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
@@ -35,49 +36,49 @@ local function waiting(args)
if state >= 0 and state < 7 then if state >= 0 and state < 7 then
-- top -- top
e.window.setCursorPos(1 + math.floor(state / 2), 1) e.w_set_cur(1 + math.floor(state / 2), 1)
if state % 2 == 0 then if state % 2 == 0 then
e.window.blit("\x8f", blit_fg, blit_bg) e.w_blit("\x8f", blit_fg, blit_bg)
else else
e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x) e.w_blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
end end
-- bottom -- bottom
e.window.setCursorPos(4 - math.ceil(state / 2), 3) e.w_set_cur(4 - math.ceil(state / 2), 3)
if state % 2 == 0 then if state % 2 == 0 then
e.window.blit("\x8f", blit_fg, blit_bg) e.w_blit("\x8f", blit_fg, blit_bg)
else else
e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x) e.w_blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
end end
else else
local st = state - 7 local st = state - 7
-- right -- right
if st % 3 == 0 then if st % 3 == 0 then
e.window.setCursorPos(4, 1 + math.floor(st / 3)) e.w_set_cur(4, 1 + math.floor(st / 3))
e.window.blit("\x83", blit_bg, blit_fg) e.w_blit("\x83", blit_bg, blit_fg)
elseif st % 3 == 1 then elseif st % 3 == 1 then
e.window.setCursorPos(4, 1 + math.floor(st / 3)) e.w_set_cur(4, 1 + math.floor(st / 3))
e.window.blit("\x8f", blit_bg, blit_fg) e.w_blit("\x8f", blit_bg, blit_fg)
e.window.setCursorPos(4, 2 + math.floor(st / 3)) e.w_set_cur(4, 2 + math.floor(st / 3))
e.window.blit("\x83", blit_fg, blit_bg) e.w_blit("\x83", blit_fg, blit_bg)
else else
e.window.setCursorPos(4, 2 + math.floor(st / 3)) e.w_set_cur(4, 2 + math.floor(st / 3))
e.window.blit("\x8f", blit_fg, blit_bg) e.w_blit("\x8f", blit_fg, blit_bg)
end end
-- left -- left
if st % 3 == 0 then if st % 3 == 0 then
e.window.setCursorPos(1, 3 - math.floor(st / 3)) e.w_set_cur(1, 3 - math.floor(st / 3))
e.window.blit("\x83", blit_fg, blit_bg) e.w_blit("\x83", blit_fg, blit_bg)
e.window.setCursorPos(1, 2 - math.floor(st / 3)) e.w_set_cur(1, 2 - math.floor(st / 3))
e.window.blit("\x8f", blit_bg, blit_fg) e.w_blit("\x8f", blit_bg, blit_fg)
elseif st % 3 == 1 then elseif st % 3 == 1 then
e.window.setCursorPos(1, 2 - math.floor(st / 3)) e.w_set_cur(1, 2 - math.floor(st / 3))
e.window.blit("\x83", blit_bg, blit_fg) e.w_blit("\x83", blit_bg, blit_fg)
else else
e.window.setCursorPos(1, 2 - math.floor(st / 3)) e.w_set_cur(1, 2 - math.floor(st / 3))
e.window.blit("\x8f", blit_fg, blit_bg) e.w_blit("\x8f", blit_fg, blit_bg)
end end
end end
@@ -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

@@ -1,21 +1,20 @@
-- Color Map Graphics Element -- Color Map Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element") local element = require("graphics.element")
---@class colormap_args ---@class colormap_args
---@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
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function colormap(args) local function colormap(args)
local bkg = "008877FFCCEE114455DD9933BBAA2266" local bkg = "008877FFCCEE114455DD9933BBAA2266"
local spaces = util.spaces(32) local spaces = string.rep(" ", 32)
args.width = 32 args.width = 32
args.height = 1 args.height = 1
@@ -24,10 +23,15 @@ local function colormap(args)
local e = element.new(args) local e = element.new(args)
-- draw color map -- draw color map
e.window.setCursorPos(1, 1) function e.redraw()
e.window.blit(spaces, bkg, bkg) e.w_set_cur(1, 1)
e.w_blit(spaces, bkg, bkg)
end
return e.get() -- initial draw
e.redraw()
return e.complete()
end end
return colormap return colormap

View File

@@ -0,0 +1,132 @@
-- App Button Graphics Element
local tcd = require("scada-common.tcd")
local core = require("graphics.core")
local element = require("graphics.element")
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@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)
element.assert(type(args.text) == "string", "text is a required field")
element.assert(type(args.title) == "string", "title is a required field")
element.assert(type(args.callback) == "function", "callback is a required field")
element.assert(type(args.app_fg_bg) == "table", "app_fg_bg is a required field")
args.height = 4
args.width = 5
-- create new graphics element base object
local e = element.new(args)
-- 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.w_set_cur(1, 1)
e.w_set_fgd(fgd)
e.w_set_bkg(bkg)
e.w_write("\x9f\x83\x83\x83")
e.w_set_fgd(bkg)
e.w_set_bkg(fgd)
e.w_write("\x90")
e.w_set_fgd(fgd)
e.w_set_bkg(bkg)
e.w_set_cur(1, 2)
e.w_write("\x95 ")
e.w_set_fgd(bkg)
e.w_set_bkg(fgd)
e.w_write("\x95")
e.w_set_cur(1, 3)
e.w_write("\x82\x8f\x8f\x8f\x81")
-- write the icon text
e.w_set_cur(3, 2)
e.w_set_fgd(fgd)
e.w_set_bkg(bkg)
e.w_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.w_set_fgd(args.active_fg_bg.fgd)
e.w_set_bkg(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.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(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 == MOUSE_CLICK.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 == MOUSE_CLICK.DOWN then
show_pressed()
elseif event.type == MOUSE_CLICK.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.MOUSE_CLICK.UP, 1, 1)) end
end
-- element redraw
function e.redraw()
e.w_set_cur(math.floor((e.frame.w - string.len(args.title)) / 2) + 1, 4)
e.w_write(args.title)
draw()
end
-- initial draw
e.redraw()
return e.complete()
end
return app_button

View File

@@ -0,0 +1,121 @@
-- 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 default? boolean default value
---@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)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.box_fg_bg) == "table", "box_fg_bg is a required field")
args.can_focus = true
args.height = 1
args.width = 3 + string.len(args.label)
-- create new graphics element base object
local e = element.new(args)
e.value = args.default == true
-- show the button state
local function draw()
e.w_set_cur(1, 1)
if e.value then
-- show as selected
e.w_set_fgd(args.box_fg_bg.bkg)
e.w_set_bkg(args.box_fg_bg.fgd)
e.w_write("\x88")
e.w_set_fgd(args.box_fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
e.w_write("\x95")
else
-- show as unselected
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(args.box_fg_bg.bkg)
e.w_write("\x88")
e.w_set_fgd(args.box_fg_bg.bkg)
e.w_set_bkg(e.fg_bg.bkg)
e.w_write("\x95")
end
end
-- write label text
local function draw_label()
if e.enabled and e.is_focused() then
e.w_set_cur(3, 1)
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(e.fg_bg.fgd)
e.w_write(args.label)
else
e.w_set_cur(3, 1)
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
e.w_write(args.label)
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) and e.in_frame_bounds(event.current.x, event.current.y) then
e.value = not e.value
draw()
if type(args.callback) == "function" then args.callback(e.value) end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == core.events.KEY_CLICK.DOWN then
if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then
e.value = not e.value
draw()
if type(args.callback) == "function" then args.callback(e.value) end
end
end
end
-- set the value
---@param val integer new value
function e.set_value(val)
e.value = val
draw()
end
-- handle focus
e.on_focused = draw_label
e.on_unfocused = draw_label
-- handle enable
e.on_enabled = draw_label
e.on_disabled = draw_label
-- element redraw
function e.redraw()
draw()
draw_label()
end
-- initial draw
e.redraw()
return e.complete()
end
return checkbox

View File

@@ -1,7 +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 core = require("graphics.core") local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
@@ -14,54 +13,50 @@ 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
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function hazard_button(args) local function hazard_button(args)
assert(type(args.text) == "string", "graphics.elements.controls.hazard_button: text is a required field") element.assert(type(args.text) == "string", "text is a required field")
assert(type(args.accent) == "number", "graphics.elements.controls.hazard_button: accent is a required field") element.assert(type(args.accent) == "number", "accent is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.hazard_button: callback is a required field") element.assert(type(args.callback) == "function", "callback is a required field")
-- static dimensions
args.height = 3 args.height = 3
args.width = string.len(args.text) + 4 args.width = string.len(args.text) + 4
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- write the button text
e.window.setCursorPos(3, 2)
e.window.write(args.text)
-- draw border -- draw border
---@param accent color accent color ---@param accent color accent color
local function draw_border(accent) local function draw_border(accent)
-- top -- top
e.window.setTextColor(accent) e.w_set_fgd(accent)
e.window.setBackgroundColor(args.fg_bg.bkg) e.w_set_bkg(args.fg_bg.bkg)
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
e.window.write("\x99" .. util.strrep("\x89", args.width - 2) .. "\x99") e.w_write("\x99" .. string.rep("\x89", args.width - 2) .. "\x99")
-- center left -- center left
e.window.setCursorPos(1, 2) e.w_set_cur(1, 2)
e.window.setTextColor(args.fg_bg.bkg) e.w_set_fgd(args.fg_bg.bkg)
e.window.setBackgroundColor(accent) e.w_set_bkg(accent)
e.window.write("\x99") e.w_write("\x99")
-- center right -- center right
e.window.setTextColor(args.fg_bg.bkg) e.w_set_fgd(args.fg_bg.bkg)
e.window.setBackgroundColor(accent) e.w_set_bkg(accent)
e.window.setCursorPos(args.width, 2) e.w_set_cur(args.width, 2)
e.window.write("\x99") e.w_write("\x99")
-- bottom -- bottom
e.window.setTextColor(accent) e.w_set_fgd(accent)
e.window.setBackgroundColor(args.fg_bg.bkg) e.w_set_bkg(args.fg_bg.bkg)
e.window.setCursorPos(1, 3) e.w_set_cur(1, 3)
e.window.write("\x99" .. util.strrep("\x98", args.width - 2) .. "\x99") e.w_write("\x99" .. string.rep("\x98", args.width - 2) .. "\x99")
end end
-- on request timeout: recursively calls itself to double flash button text -- on request timeout: recursively calls itself to double flash button text
@@ -72,9 +67,9 @@ local function hazard_button(args)
if n == 0 then if n == 0 then
-- go back off -- go back off
e.window.setTextColor(args.fg_bg.fgd) e.w_set_fgd(args.fg_bg.fgd)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
end end
if n >= 4 then if n >= 4 then
@@ -82,18 +77,18 @@ local function hazard_button(args)
elseif n % 2 == 0 then elseif n % 2 == 0 then
-- toggle text color on after 0.25 seconds -- toggle text color on after 0.25 seconds
tcd.dispatch(0.25, function () tcd.dispatch(0.25, function ()
e.window.setTextColor(args.accent) e.w_set_fgd(args.accent)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
on_timeout(n + 1) on_timeout(n + 1)
on_timeout(n + 1) on_timeout(n + 1)
end) end)
elseif n % 1 then elseif n % 1 then
-- toggle text color off after 0.25 seconds -- toggle text color off after 0.25 seconds
tcd.dispatch(0.25, function () tcd.dispatch(0.25, function ()
e.window.setTextColor(args.fg_bg.fgd) e.w_set_fgd(args.fg_bg.fgd)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
on_timeout(n + 1) on_timeout(n + 1)
end) end)
end end
@@ -101,9 +96,9 @@ local function hazard_button(args)
-- blink routine for success indication -- blink routine for success indication
local function on_success() local function on_success()
e.window.setTextColor(args.fg_bg.fgd) e.w_set_fgd(args.fg_bg.fgd)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
end end
-- blink routine for failure indication -- blink routine for failure indication
@@ -114,9 +109,9 @@ local function hazard_button(args)
if n == 0 then if n == 0 then
-- go back off -- go back off
e.window.setTextColor(args.fg_bg.fgd) e.w_set_fgd(args.fg_bg.fgd)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
end end
if n >= 2 then if n >= 2 then
@@ -124,17 +119,17 @@ local function hazard_button(args)
elseif n % 2 == 0 then elseif n % 2 == 0 then
-- toggle text color on after 0.5 seconds -- toggle text color on after 0.5 seconds
tcd.dispatch(0.5, function () tcd.dispatch(0.5, function ()
e.window.setTextColor(args.accent) e.w_set_fgd(args.accent)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
on_failure(n + 1) on_failure(n + 1)
end) end)
elseif n % 1 then elseif n % 1 then
-- toggle text color off after 0.25 seconds -- toggle text color off after 0.25 seconds
tcd.dispatch(0.25, function () tcd.dispatch(0.25, function ()
e.window.setTextColor(args.fg_bg.fgd) e.w_set_fgd(args.fg_bg.fgd)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
on_failure(n + 1) on_failure(n + 1)
end) end)
end end
@@ -146,9 +141,9 @@ local function hazard_button(args)
if e.enabled then if e.enabled then
if core.events.was_clicked(event.type) then if core.events.was_clicked(event.type) then
-- change text color to indicate clicked -- change text color to indicate clicked
e.window.setTextColor(args.accent) e.w_set_fgd(args.accent)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
-- abort any other callbacks -- abort any other callbacks
tcd.abort(on_timeout) tcd.abort(on_timeout)
@@ -158,7 +153,6 @@ local function hazard_button(args)
-- 1.5 second timeout -- 1.5 second timeout
tcd.dispatch(1.5, on_timeout) tcd.dispatch(1.5, on_timeout)
-- call the touch callback
args.callback() args.callback()
end end
end end
@@ -174,31 +168,39 @@ local function hazard_button(args)
-- 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.UP, 1, 1)) end if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
end end
-- show the button as disabled -- show the button as disabled
function e.disable() function e.on_disabled()
if args.dis_colors then if args.dis_colors then
draw_border(args.dis_colors.color_a) draw_border(args.dis_colors.color_a)
e.window.setTextColor(args.dis_colors.color_b) e.w_set_fgd(args.dis_colors.color_b)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
end end
end end
-- show the button as enabled -- show the button as enabled
function e.enable() function e.on_enabled()
draw_border(args.accent) draw_border(args.accent)
e.window.setTextColor(args.fg_bg.fgd) e.w_set_fgd(args.fg_bg.fgd)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
end end
-- initial draw of border -- element redraw
draw_border(args.accent) function e.redraw()
-- write the button text and draw border
e.w_set_cur(3, 2)
e.w_write(args.text)
draw_border(args.accent)
end
return e.get() -- initial draw
e.redraw()
return e.complete()
end end
return hazard_button return hazard_button

View File

@@ -20,21 +20,20 @@ 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
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function multi_button(args) local function multi_button(args)
assert(type(args.options) == "table", "graphics.elements.controls.multi_button: options is a required field") element.assert(type(args.options) == "table", "options is a required field")
assert(#args.options > 0, "graphics.elements.controls.multi_button: at least one option is required") element.assert(#args.options > 0, "at least one option is required")
assert(type(args.callback) == "function", "graphics.elements.controls.multi_button: callback is a required field") element.assert(type(args.callback) == "function", "callback is a required field")
assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
"graphics.elements.controls.multi_button: default must be nil or a number > 0") element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
"graphics.elements.controls.multi_button: min_width must be nil or a number > 0")
-- single line -- single line
args.height = 1 args.height = 1
@@ -70,23 +69,23 @@ local function multi_button(args)
end end
-- show the button state -- show the button state
local function draw() function e.redraw()
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
e.window.setCursorPos(opt._start_x, 1) e.w_set_cur(opt._start_x, 1)
if e.value == i then if e.value == i then
-- show as pressed -- show as pressed
e.window.setTextColor(opt.active_fg_bg.fgd) e.w_set_fgd(opt.active_fg_bg.fgd)
e.window.setBackgroundColor(opt.active_fg_bg.bkg) e.w_set_bkg(opt.active_fg_bg.bkg)
else else
-- show as unpressed -- show as unpressed
e.window.setTextColor(opt.fg_bg.fgd) e.w_set_fgd(opt.fg_bg.fgd)
e.window.setBackgroundColor(opt.fg_bg.bkg) e.w_set_bkg(opt.fg_bg.bkg)
end end
e.window.write(util.pad(opt.text, button_width)) e.w_write(util.pad(opt.text, button_width))
end end
end end
@@ -114,7 +113,7 @@ local function multi_button(args)
-- tap always has identical coordinates, so this always passes for taps -- tap always has identical coordinates, so this always passes for taps
if button_ini == button_cur and button_cur ~= nil then if button_ini == button_cur and button_cur ~= nil then
e.value = button_cur e.value = button_cur
draw() e.redraw()
args.callback(e.value) args.callback(e.value)
end end
end end
@@ -124,13 +123,13 @@ local function multi_button(args)
---@param val integer new value ---@param val integer new value
function e.set_value(val) function e.set_value(val)
e.value = val e.value = val
draw() e.redraw()
end end
-- initial draw -- initial draw
draw() e.redraw()
return e.get() return e.complete()
end end
return multi_button return multi_button

View File

@@ -1,37 +1,43 @@
-- 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 local ALIGN = core.ALIGN
local MOUSE_CLICK = core.events.MOUSE_CLICK
local KEY_CLICK = core.events.KEY_CLICK
---@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
---@field min_width? integer text length if omitted ---@field min_width? integer text length if omitted
---@field alignment? ALIGN text align if min width > length
---@field active_fg_bg? cpair foreground/background colors when pressed ---@field active_fg_bg? cpair foreground/background colors when pressed
---@field dis_fg_bg? cpair foreground/background colors when disabled ---@field dis_fg_bg? cpair foreground/background colors when disabled
---@field parent graphics_element ---@field parent graphics_element
---@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
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
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") element.assert(type(args.text) == "string", "text is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.push_button: callback is a required field") element.assert(type(args.callback) == "function", "callback is a required field")
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
"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)
local alignment = args.alignment or ALIGN.CENTER
-- single line height, calculate width -- set automatic settings
args.can_focus = true
args.height = 1 args.height = 1
args.min_width = args.min_width or 0 args.min_width = args.min_width or 0
args.width = math.max(text_width, args.min_width) args.width = math.max(text_width, args.min_width)
@@ -39,25 +45,31 @@ local function push_button(args)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1 local h_pad = 1
local v_pad = math.floor(e.frame.h / 2) + 1 local v_pad = math.floor(e.frame.h / 2) + 1
if alignment == ALIGN.CENTER then
h_pad = math.floor((e.frame.w - text_width) / 2) + 1
elseif alignment == ALIGN.RIGHT then
h_pad = (e.frame.w - text_width) + 1
end
-- draw the button -- draw the button
local function draw() function e.redraw()
e.window.clear() e.window.clear()
-- write the button text -- write the button text
e.window.setCursorPos(h_pad, v_pad) e.w_set_cur(h_pad, v_pad)
e.window.write(args.text) e.w_write(args.text)
end end
-- draw the button as pressed (if active_fg_bg set) -- draw the button as pressed (if active_fg_bg set)
local function show_pressed() local function show_pressed()
if e.enabled and args.active_fg_bg ~= nil then if e.enabled and args.active_fg_bg ~= nil then
e.value = true e.value = true
e.window.setTextColor(args.active_fg_bg.fgd) e.w_set_fgd(args.active_fg_bg.fgd)
e.window.setBackgroundColor(args.active_fg_bg.bkg) e.w_set_bkg(args.active_fg_bg.bkg)
draw() e.redraw()
end end
end end
@@ -65,9 +77,9 @@ local function push_button(args)
local function show_unpressed() local function show_unpressed()
if e.enabled and args.active_fg_bg ~= nil then if e.enabled and args.active_fg_bg ~= nil then
e.value = false e.value = false
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
draw() e.redraw()
end end
end end
@@ -75,52 +87,67 @@ local function push_button(args)
---@param event mouse_interaction mouse event ---@param event mouse_interaction mouse event
function e.handle_mouse(event) function e.handle_mouse(event)
if e.enabled then if e.enabled then
if event.type == CLICK_TYPE.TAP then if event.type == MOUSE_CLICK.TAP then
show_pressed() show_pressed()
-- show as unpressed in 0.25 seconds -- show as unpressed in 0.25 seconds
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
args.callback() args.callback()
elseif event.type == CLICK_TYPE.DOWN then elseif event.type == MOUSE_CLICK.DOWN then
show_pressed() show_pressed()
elseif event.type == CLICK_TYPE.UP then elseif event.type == MOUSE_CLICK.UP then
show_unpressed() show_unpressed()
if e.in_bounds(event.current.x, event.current.y) then if e.in_frame_bounds(event.current.x, event.current.y) then
args.callback() args.callback()
end end
end end
end end
end end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == KEY_CLICK.DOWN then
if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then
args.callback()
e.defocus()
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.UP, 1, 1)) end if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
end end
-- show butten as enabled -- show butten as enabled
function e.enable() function e.on_enabled()
if args.dis_fg_bg ~= nil then if args.dis_fg_bg ~= nil then
e.value = false e.value = false
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
draw() e.redraw()
end end
end end
-- show button as disabled -- show button as disabled
function e.disable() function e.on_disabled()
if args.dis_fg_bg ~= nil then if args.dis_fg_bg ~= nil then
e.value = false e.value = false
e.window.setTextColor(args.dis_fg_bg.fgd) e.w_set_fgd(args.dis_fg_bg.fgd)
e.window.setBackgroundColor(args.dis_fg_bg.bkg) e.w_set_bkg(args.dis_fg_bg.bkg)
draw() e.redraw()
end end
end end
-- initial draw -- handle focus
draw() e.on_focused = show_pressed
e.on_unfocused = show_unpressed
return e.get() -- initial draw
e.redraw()
return e.complete()
end end
return push_button return push_button

View File

@@ -0,0 +1,203 @@
-- 2D Radio Button Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
---@class radio_2d_args
---@field rows integer
---@field columns integer
---@field options table
---@field radio_colors cpair radio button colors (inner & outer)
---@field select_color? color color for radio button when selected
---@field color_map? table colors for each radio button when selected
---@field disable_color? color color for radio button when disabled
---@field disable_fg_bg? cpair text colors when disabled
---@field default? integer default state, defaults to options[1]
---@field callback? function function to call on touch
---@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 2D radio button list (latch selection, exclusively one color at a time)
---@param args radio_2d_args
---@return graphics_element element, element_id id
local function radio_2d_button(args)
element.assert(type(args.options) == "table" and #args.options > 0, "options should be a table with length >= 1")
element.assert(util.is_int(args.rows) and util.is_int(args.columns), "rows/columns must be integers")
element.assert((args.rows * args.columns) >= #args.options, "rows x columns size insufficient for provided number of options")
element.assert(type(args.radio_colors) == "table", "radio_colors is a required field")
element.assert(type(args.select_color) == "number" or type(args.color_map) == "table", "select_color or color_map is required")
element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
local array = {}
local col_widths = {}
local next_idx = 1
local total_width = 0
local max_rows = 1
local focused_opt = 1
local focus_x, focus_y = 1, 1
-- build table to display
for col = 1, args.columns do
local max_width = 0
array[col] = {}
for row = 1, args.rows do
local len = string.len(args.options[next_idx])
if len > max_width then max_width = len end
if row > max_rows then max_rows = row end
table.insert(array[col], { text = args.options[next_idx], id = next_idx, x_1 = 1 + total_width, x_2 = 2 + total_width + len })
next_idx = next_idx + 1
if next_idx > #args.options then break end
end
table.insert(col_widths, max_width + 3)
total_width = total_width + max_width + 3
if next_idx > #args.options then break end
end
args.can_focus = true
args.width = total_width
args.height = max_rows
-- create new graphics element base object
local e = element.new(args)
-- selected option (convert nil to 1 if missing)
e.value = args.default or 1
-- draw the element
function e.redraw()
local col_x = 1
local radio_color_b = util.trinary(type(args.disable_color) == "number" and not e.enabled, args.disable_color, args.radio_colors.color_b)
for col = 1, #array do
for row = 1, #array[col] do
local opt = array[col][row]
local select_color = args.select_color
if type(args.color_map) == "table" and args.color_map[opt.id] then
select_color = args.color_map[opt.id]
end
local inner_color = util.trinary((e.value == opt.id) and e.enabled, radio_color_b, args.radio_colors.color_a)
local outer_color = util.trinary((e.value == opt.id) and e.enabled, select_color, radio_color_b)
e.w_set_cur(col_x, row)
e.w_set_fgd(inner_color)
e.w_set_bkg(outer_color)
e.w_write("\x88")
e.w_set_fgd(outer_color)
e.w_set_bkg(e.fg_bg.bkg)
e.w_write("\x95")
if opt.id == focused_opt then
focus_x, focus_y = row, col
end
-- write button text
if opt.id == focused_opt and e.is_focused() and e.enabled then
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(e.fg_bg.fgd)
elseif type(args.disable_fg_bg) == "table" and not e.enabled then
e.w_set_fgd(args.disable_fg_bg.fgd)
e.w_set_bkg(args.disable_fg_bg.bkg)
else
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
end
e.w_write(opt.text)
end
col_x = col_x + col_widths[col]
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) and (event.initial.y == event.current.y) then
-- determine what was pressed
for _, row in ipairs(array) do
local elem = row[event.current.y]
if elem ~= nil and event.initial.x >= elem.x_1 and event.initial.x <= elem.x_2 and event.current.x >= elem.x_1 and event.current.x <= elem.x_2 then
e.value = elem.id
focused_opt = elem.id
e.redraw()
if type(args.callback) == "function" then args.callback(e.value) end
break
end
end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == core.events.KEY_CLICK.DOWN or event.type == core.events.KEY_CLICK.HELD then
if event.type == core.events.KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then
e.value = focused_opt
e.redraw()
if type(args.callback) == "function" then args.callback(e.value) end
elseif event.key == keys.down then
if focused_opt < #args.options then
focused_opt = focused_opt + 1
e.redraw()
end
elseif event.key == keys.up then
if focused_opt > 1 then
focused_opt = focused_opt - 1
e.redraw()
end
elseif event.key == keys.right then
if array[focus_y + 1] and array[focus_y + 1][focus_x] then
focused_opt = array[focus_y + 1][focus_x].id
else focused_opt = array[1][focus_x].id end
e.redraw()
elseif event.key == keys.left then
if array[focus_y - 1] and array[focus_y - 1][focus_x] then
focused_opt = array[focus_y - 1][focus_x].id
e.redraw()
elseif array[#array][focus_x] then
focused_opt = array[#array][focus_x].id
e.redraw()
end
end
end
end
-- set the value
---@param val integer new value
function e.set_value(val)
if type(val) == "number" and val > 0 and val <= #args.options then
e.value = val
e.redraw()
end
end
-- handle focus & enable
e.on_focused = e.redraw
e.on_unfocused = e.redraw
e.on_enabled = e.redraw
e.on_disabled = e.redraw
-- initial draw
e.redraw()
return e.complete()
end
return radio_2d_button

View File

@@ -1,35 +1,36 @@
-- Radio Button Graphics Element -- Radio Button Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
local KEY_CLICK = core.events.KEY_CLICK
---@class radio_button_args ---@class radio_button_args
---@field options table button options ---@field options table button options
---@field callback function function to call on touch ---@field radio_colors cpair radio button colors (inner & outer)
---@field radio_colors cpair colors for radio button center dot when active (a) or inactive (b) ---@field select_color color color for radio button border when selected
---@field radio_bg color background color of radio button
---@field default? integer default state, defaults to options[1] ---@field default? integer default state, defaults to options[1]
---@field min_width? integer text length + 2 if omitted ---@field min_width? integer text length + 2 if omitted
---@field callback? function function to call on touch
---@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
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function radio_button(args) local function radio_button(args)
assert(type(args.options) == "table", "graphics.elements.controls.radio_button: options is a required field") element.assert(type(args.options) == "table", "options is a required field")
assert(#args.options > 0, "graphics.elements.controls.radio_button: at least one option is required") element.assert(#args.options > 0, "at least one option is required")
assert(type(args.callback) == "function", "graphics.elements.controls.radio_button: callback is a required field") element.assert(type(args.radio_colors) == "table", "radio_colors is a required field")
assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), element.assert(type(args.select_color) == "number", "select_color is a required field")
"graphics.elements.controls.radio_button: default must be nil or a number > 0") element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
"graphics.elements.controls.radio_button: min_width must be nil or a number > 0")
-- one line per option
args.height = #args.options
-- determine widths -- determine widths
local max_width = 1 local max_width = 1
@@ -42,41 +43,47 @@ local function radio_button(args)
local button_text_width = math.max(max_width, args.min_width or 0) local button_text_width = math.max(max_width, args.min_width or 0)
-- set automatic args
args.can_focus = true
args.width = button_text_width + 2 args.width = button_text_width + 2
args.height = #args.options -- one line per option
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
local focused_opt = 1
-- button state (convert nil to 1 if missing) -- button state (convert nil to 1 if missing)
e.value = args.default or 1 e.value = args.default or 1
-- show the button state -- show the button state
local function draw() function e.redraw()
for i = 1, #args.options do for i = 1, #args.options do
local opt = args.options[i] ---@type string local opt = args.options[i] ---@type string
e.window.setCursorPos(1, i) local inner_color = util.trinary(e.value == i, args.radio_colors.color_b, args.radio_colors.color_a)
local outer_color = util.trinary(e.value == i, args.select_color, args.radio_colors.color_b)
if e.value == i then e.w_set_cur(1, i)
-- show as selected
e.window.setTextColor(args.radio_colors.color_a)
e.window.setBackgroundColor(args.radio_bg)
else
-- show as unselected
e.window.setTextColor(args.radio_colors.color_b)
e.window.setBackgroundColor(args.radio_bg)
end
e.window.write("\x88") e.w_set_fgd(inner_color)
e.w_set_bkg(outer_color)
e.w_write("\x88")
e.window.setTextColor(args.radio_bg) e.w_set_fgd(outer_color)
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
e.window.write("\x95") e.w_write("\x95")
-- write button text -- write button text
e.window.setTextColor(e.fg_bg.fgd) if i == focused_opt and e.is_focused() and e.enabled then
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_fgd(e.fg_bg.bkg)
e.window.write(opt) e.w_set_bkg(e.fg_bg.fgd)
else
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
end
e.w_write(opt)
end end
end end
@@ -87,8 +94,31 @@ local function radio_button(args)
-- determine what was pressed -- determine what was pressed
if args.options[event.current.y] ~= nil then if args.options[event.current.y] ~= nil then
e.value = event.current.y e.value = event.current.y
draw() focused_opt = e.value
args.callback(e.value) e.redraw()
if type(args.callback) == "function" then args.callback(e.value) end
end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
if event.type == KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then
e.value = focused_opt
e.redraw()
if type(args.callback) == "function" then args.callback(e.value) end
elseif event.key == keys.down then
if focused_opt < #args.options then
focused_opt = focused_opt + 1
e.redraw()
end
elseif event.key == keys.up then
if focused_opt > 1 then
focused_opt = focused_opt - 1
e.redraw()
end
end end
end end
end end
@@ -96,14 +126,22 @@ local function radio_button(args)
-- set the value -- set the value
---@param val integer new value ---@param val integer new value
function e.set_value(val) function e.set_value(val)
e.value = val if type(val) == "number" and val > 0 and val <= #args.options then
draw() e.value = val
e.redraw()
end
end end
-- initial draw -- handle focus & enable
draw() e.on_focused = e.redraw
e.on_unfocused = e.redraw
e.on_enabled = e.redraw
e.on_disabled = e.redraw
return e.get() -- initial draw
e.redraw()
return e.complete()
end end
return radio_button return radio_button

View File

@@ -1,11 +1,12 @@
-- Sidebar Graphics Element -- Sidebar Graphics Element
local tcd = require("scada-common.tcallbackdsp") local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
local CLICK_TYPE = core.events.CLICK_TYPE local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class sidebar_tab ---@class sidebar_tab
---@field char string character identifier ---@field char string character identifier
@@ -17,33 +18,37 @@ local CLICK_TYPE = core.events.CLICK_TYPE
---@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
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function sidebar(args) local function sidebar(args)
assert(type(args.tabs) == "table", "graphics.elements.controls.sidebar: tabs is a required field") element.assert(type(args.tabs) == "table", "tabs is a required field")
assert(#args.tabs > 0, "graphics.elements.controls.sidebar: at least one tab is required") element.assert(#args.tabs > 0, "at least one tab is required")
assert(type(args.callback) == "function", "graphics.elements.controls.sidebar: callback is a required field") element.assert(type(args.callback) == "function", "callback is a required field")
-- always 3 wide
args.width = 3 args.width = 3
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
assert(e.frame.h >= (#args.tabs * 3), "graphics.elements.controls.sidebar: height insufficent to display all tabs") element.assert(e.frame.h >= (#args.tabs * 3), "height insufficent to display all tabs")
-- default to 1st tab -- default to 1st tab
e.value = 1 e.value = 1
local was_pressed = false
-- 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
---@param pressed_idx? integer optional index to show as held (that is not yet selected) ---@param pressed_idx? integer optional index to show as held (that is not yet selected)
local function draw(pressed, pressed_idx) local function draw(pressed, pressed_idx)
pressed = util.trinary(pressed == nil, was_pressed, pressed)
was_pressed = pressed
pressed_idx = pressed_idx or e.value pressed_idx = pressed_idx or e.value
for i = 1, #args.tabs do for i = 1, #args.tabs do
@@ -51,27 +56,23 @@ local function sidebar(args)
local y = ((i - 1) * 3) + 1 local y = ((i - 1) * 3) + 1
e.window.setCursorPos(1, y) e.w_set_cur(1, y)
if pressed and i == pressed_idx then if pressed and i == pressed_idx then
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
else else
e.window.setTextColor(tab.color.fgd) e.w_set_fgd(tab.color.fgd)
e.window.setBackgroundColor(tab.color.bkg) e.w_set_bkg(tab.color.bkg)
end end
e.window.write(" ") e.w_write(" ")
e.window.setCursorPos(1, y + 1) e.w_set_cur(1, y + 1)
if e.value == i then if e.value == i then
-- show as selected e.w_write(" " .. tab.char .. "\x10")
e.window.write(" " .. tab.char .. "\x10") else e.w_write(" " .. tab.char .. " ") end
else e.w_set_cur(1, y + 2)
-- show as unselected e.w_write(" ")
e.window.write(" " .. tab.char .. " ")
end
e.window.setCursorPos(1, y + 2)
e.window.write(" ")
end end
end end
@@ -84,22 +85,22 @@ local function sidebar(args)
local ini_idx = math.ceil(event.initial.y / 3) local ini_idx = math.ceil(event.initial.y / 3)
if args.tabs[cur_idx] ~= nil then if args.tabs[cur_idx] ~= nil then
if event.type == CLICK_TYPE.TAP then if event.type == MOUSE_CLICK.TAP then
e.value = cur_idx e.value = cur_idx
draw(true) draw(true)
-- show as unpressed in 0.25 seconds -- show as unpressed in 0.25 seconds
tcd.dispatch(0.25, function () draw(false) end) tcd.dispatch(0.25, function () draw(false) end)
args.callback(e.value) args.callback(e.value)
elseif event.type == CLICK_TYPE.DOWN then elseif event.type == MOUSE_CLICK.DOWN then
draw(true, cur_idx) draw(true, cur_idx)
elseif event.type == CLICK_TYPE.UP then elseif event.type == MOUSE_CLICK.UP then
if cur_idx == ini_idx and e.in_bounds(event.current.x, event.current.y) then if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then
e.value = cur_idx e.value = cur_idx
draw(false) draw(false)
args.callback(e.value) args.callback(e.value)
else draw(false) end else draw(false) end
end end
elseif event.type == CLICK_TYPE.UP then elseif event.type == MOUSE_CLICK.UP then
draw(false) draw(false)
end end
end end
@@ -112,10 +113,12 @@ local function sidebar(args)
draw(false) draw(false)
end end
-- initial draw -- element redraw
draw(false) e.redraw = draw
return e.get() e.redraw()
return e.complete()
end end
return sidebar return sidebar

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 spinbox control (minimum value is 0) -- new spinbox control (minimum value is 0)
---@param args spinbox_args ---@param args spinbox_args
@@ -28,8 +29,8 @@ local function spinbox(args)
local wn_prec = args.whole_num_precision local wn_prec = args.whole_num_precision
local fr_prec = args.fractional_precision local fr_prec = args.fractional_precision
assert(util.is_int(wn_prec), "graphics.element.controls.spinbox_numeric: whole number precision must be an integer") element.assert(util.is_int(wn_prec), "whole number precision must be an integer")
assert(util.is_int(fr_prec), "graphics.element.controls.spinbox_numeric: fractional precision must be an integer") element.assert(util.is_int(fr_prec), "fractional precision must be an integer")
local fmt, fmt_init ---@type string, string local fmt, fmt_init ---@type string, string
@@ -43,7 +44,7 @@ local function spinbox(args)
local dec_point_x = args.whole_num_precision + 1 local dec_point_x = args.whole_num_precision + 1
assert(type(args.arrow_fg_bg) == "table", "graphics.element.spinbox_numeric: arrow_fg_bg is a required field") element.assert(type(args.arrow_fg_bg) == "table", "arrow_fg_bg is a required field")
-- determine widths -- determine widths
args.width = wn_prec + fr_prec + util.trinary(fr_prec > 0, 1, 0) args.width = wn_prec + fr_prec + util.trinary(fr_prec > 0, 1, 0)
@@ -57,22 +58,20 @@ local function spinbox(args)
-- draw the arrows -- draw the arrows
local function draw_arrows(color) local function draw_arrows(color)
e.window.setBackgroundColor(args.arrow_fg_bg.bkg) e.w_set_bkg(args.arrow_fg_bg.bkg)
e.window.setTextColor(color) e.w_set_fgd(color)
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
e.window.write(util.strrep("\x1e", wn_prec)) e.w_write(string.rep("\x1e", wn_prec))
e.window.setCursorPos(1, 3) e.w_set_cur(1, 3)
e.window.write(util.strrep("\x1f", wn_prec)) e.w_write(string.rep("\x1f", wn_prec))
if fr_prec > 0 then if fr_prec > 0 then
e.window.setCursorPos(1 + wn_prec, 1) e.w_set_cur(1 + wn_prec, 1)
e.window.write(" " .. util.strrep("\x1e", fr_prec)) e.w_write(" " .. string.rep("\x1e", fr_prec))
e.window.setCursorPos(1 + wn_prec, 3) e.w_set_cur(1 + wn_prec, 3)
e.window.write(" " .. util.strrep("\x1f", fr_prec)) e.w_write(" " .. string.rep("\x1f", fr_prec))
end end
end end
draw_arrows(args.arrow_fg_bg.fgd)
-- populate digits from current value -- populate digits from current value
local function set_digits() local function set_digits()
local initial_str = util.sprintf(fmt_init, e.value) local initial_str = util.sprintf(fmt_init, e.value)
@@ -118,15 +117,12 @@ local function spinbox(args)
end end
-- draw -- draw
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.window.setCursorPos(1, 2) e.w_set_cur(1, 2)
e.window.write(util.sprintf(fmt, e.value)) e.w_write(util.sprintf(fmt, e.value))
end end
-- init with the default value
show_num()
-- 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)
@@ -137,10 +133,8 @@ local function spinbox(args)
local idx = util.trinary(event.current.x > dec_point_x, event.current.x - 1, event.current.x) local idx = util.trinary(event.current.x > dec_point_x, event.current.x - 1, event.current.x)
if digits[idx] ~= nil then if digits[idx] ~= nil then
if event.current.y == 1 then if event.current.y == 1 then
-- increment
digits[idx] = digits[idx] + 1 digits[idx] = digits[idx] + 1
elseif event.current.y == 3 then elseif event.current.y == 3 then
-- decrement
digits[idx] = digits[idx] - 1 digits[idx] = digits[idx] - 1
end end
@@ -175,20 +169,21 @@ local function spinbox(args)
end end
-- enable this input -- enable this input
function e.enable() function e.on_enabled() draw_arrows(args.arrow_fg_bg.fgd) end
draw_arrows(args.arrow_fg_bg.fgd)
end
-- disable this input -- disable this input
function e.disable() function e.on_disabled() draw_arrows(args.arrow_disable or colors.lightGray) end
draw_arrows(args.arrow_disable or colors.lightGray)
-- element redraw
function e.redraw()
show_num()
draw_arrows(util.trinary(e.enabled, args.arrow_fg_bg.fgd, args.arrow_disable or colors.lightGray))
end end
-- default to zero, init digits table -- initial draw
e.value = 0 e.redraw()
set_digits()
return e.get() return e.complete()
end end
return spinbox return spinbox

View File

@@ -12,23 +12,22 @@ 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
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function switch_button(args) local function switch_button(args)
assert(type(args.text) == "string", "graphics.elements.controls.switch_button: text is a required field") element.assert(type(args.text) == "string", "text is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.switch_button: callback is a required field") element.assert(type(args.callback) == "function", "callback is a required field")
assert(type(args.active_fg_bg) == "table", "graphics.elements.controls.switch_button: active_fg_bg is a required field") element.assert(type(args.active_fg_bg) == "table", "active_fg_bg is a required field")
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
"graphics.elements.controls.switch_button: min_width must be nil or a number > 0")
local text_width = string.len(args.text) local text_width = string.len(args.text)
-- single line height, calculate width
args.height = 1 args.height = 1
args.min_width = args.min_width or 0 args.min_width = args.min_width or 0
args.width = math.max(text_width, args.min_width) args.width = math.max(text_width, args.min_width)
@@ -36,44 +35,32 @@ local function switch_button(args)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- button state (convert nil to false if missing)
e.value = args.default or false e.value = args.default or false
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1 local h_pad = math.floor((e.frame.w - text_width) / 2) + 1
local v_pad = math.floor(e.frame.h / 2) + 1 local v_pad = math.floor(e.frame.h / 2) + 1
-- show the button state -- show the button state
local function draw_state() function e.redraw()
if e.value then if e.value then
-- show as pressed e.w_set_fgd(args.active_fg_bg.fgd)
e.window.setTextColor(args.active_fg_bg.fgd) e.w_set_bkg(args.active_fg_bg.bkg)
e.window.setBackgroundColor(args.active_fg_bg.bkg)
else else
-- show as unpressed e.w_set_fgd(e.fg_bg.fgd)
e.window.setTextColor(e.fg_bg.fgd) e.w_set_bkg(e.fg_bg.bkg)
e.window.setBackgroundColor(e.fg_bg.bkg)
end end
-- clear to redraw background
e.window.clear() e.window.clear()
e.w_set_cur(h_pad, v_pad)
-- write the button text e.w_write(args.text)
e.window.setCursorPos(h_pad, v_pad)
e.window.write(args.text)
end end
-- initial draw
draw_state()
-- 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)
if e.enabled and core.events.was_clicked(event.type) then if e.enabled and core.events.was_clicked(event.type) then
-- toggle state
e.value = not e.value e.value = not e.value
draw_state() e.redraw()
-- call the touch callback with state
args.callback(e.value) args.callback(e.value)
end end
end end
@@ -81,12 +68,14 @@ local function switch_button(args)
-- set the value -- set the value
---@param val boolean new value ---@param val boolean new value
function e.set_value(val) function e.set_value(val)
-- set state
e.value = val e.value = val
draw_state() e.redraw()
end end
return e.get() -- initial draw
e.redraw()
return e.complete()
end end
return switch_button return switch_button

View File

@@ -18,21 +18,20 @@ 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 fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new tab selector -- new tab selector
---@param args tabbar_args ---@param args tabbar_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function tabbar(args) local function tabbar(args)
assert(type(args.tabs) == "table", "graphics.elements.controls.tabbar: tabs is a required field") element.assert(type(args.tabs) == "table", "tabs is a required field")
assert(#args.tabs > 0, "graphics.elements.controls.tabbar: at least one tab is required") element.assert(#args.tabs > 0, "at least one tab is required")
assert(type(args.callback) == "function", "graphics.elements.controls.tabbar: callback is a required field") element.assert(type(args.callback) == "function", "callback is a required field")
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
"graphics.elements.controls.tabbar: min_width must be nil or a number > 0")
-- always 1 tall
args.height = 1 args.height = 1
-- determine widths -- determine widths
@@ -49,7 +48,7 @@ local function tabbar(args)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
assert(e.frame.w >= (button_width * #args.tabs), "graphics.elements.controls.tabbar: width insufficent to display all tabs") element.assert(e.frame.w >= (button_width * #args.tabs), "width insufficent to display all tabs")
-- default to 1st tab -- default to 1st tab
e.value = 1 e.value = 1
@@ -66,21 +65,21 @@ local function tabbar(args)
end end
-- show the tab state -- show the tab state
local function draw() function e.redraw()
for i = 1, #args.tabs do for i = 1, #args.tabs do
local tab = args.tabs[i] ---@type tabbar_tab local tab = args.tabs[i] ---@type tabbar_tab
e.window.setCursorPos(tab._start_x, 1) e.w_set_cur(tab._start_x, 1)
if e.value == i then if e.value == i then
e.window.setTextColor(tab.color.fgd) e.w_set_fgd(tab.color.fgd)
e.window.setBackgroundColor(tab.color.bkg) e.w_set_bkg(tab.color.bkg)
else else
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
end end
e.window.write(util.pad(tab.name, button_width)) e.w_write(util.pad(tab.name, button_width))
end end
end end
@@ -108,7 +107,7 @@ local function tabbar(args)
-- tap always has identical coordinates, so this always passes for taps -- tap always has identical coordinates, so this always passes for taps
if tab_ini == tab_cur and tab_cur ~= nil then if tab_ini == tab_cur and tab_cur ~= nil then
e.value = tab_cur e.value = tab_cur
draw() e.redraw()
args.callback(e.value) args.callback(e.value)
end end
end end
@@ -118,13 +117,13 @@ local function tabbar(args)
---@param val integer new value ---@param val integer new value
function e.set_value(val) function e.set_value(val)
e.value = val e.value = val
draw() e.redraw()
end end
-- initial draw -- initial draw
draw() e.redraw()
return e.get() return e.complete()
end end
return tabbar 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

@@ -0,0 +1,159 @@
-- Numeric Value Entry Graphics Element
local core = require("graphics.core")
local element = require("graphics.element")
local KEY_CLICK = core.events.KEY_CLICK
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class number_field_args
---@field default? number default value, defaults to 0
---@field min? number minimum, forced on unfocus
---@field max? number maximum, forced on unfocus
---@field max_digits? integer maximum number of digits, defaults to width
---@field allow_decimal? boolean true to allow decimals
---@field allow_negative? boolean true to allow negative numbers
---@field dis_fg_bg? cpair foreground/background colors when disabled
---@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 numeric entry field
---@param args number_field_args
---@return graphics_element element, element_id id
local function number_field(args)
args.height = 1
args.can_focus = true
-- create new graphics element base object
local e = element.new(args)
local has_decimal = false
args.max_digits = args.max_digits or e.frame.w
-- set initial value
e.value = "" .. (args.default or 0)
-- make an interactive field manager
local ifield = core.new_ifield(e, args.max_digits, args.fg_bg, args.dis_fg_bg)
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
-- only handle if on an increment or decrement arrow
if e.enabled then
if core.events.was_clicked(event.type) then
e.take_focus()
if event.type == MOUSE_CLICK.UP then
ifield.move_cursor(event.current.x)
end
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
ifield.select_all()
end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == KEY_CLICK.CHAR and string.len(e.value) < args.max_digits then
if tonumber(event.name) then
if e.value == 0 then e.value = "" end
ifield.try_insert_char(event.name)
end
elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
if (event.key == keys.backspace or event.key == keys.delete) and (string.len(e.value) > 0) then
ifield.backspace()
has_decimal = string.find(e.value, "%.") ~= nil
elseif (event.key == keys.period or event.key == keys.numPadDecimal) and (not has_decimal) and args.allow_decimal then
has_decimal = true
ifield.try_insert_char(".")
elseif (event.key == keys.minus or event.key == keys.numPadSubtract) and (string.len(e.value) == 0) and args.allow_negative then
ifield.set_value("-")
elseif event.key == keys.left then
ifield.nav_left()
elseif event.key == keys.right then
ifield.nav_right()
elseif event.key == keys.a and event.ctrl then
ifield.select_all()
elseif event.key == keys.home or event.key == keys.up then
ifield.nav_start()
elseif event.key == keys["end"] or event.key == keys.down then
ifield.nav_end()
end
end
end
-- set the value (must be a number)
---@param val number number to show
function e.set_value(val)
if tonumber(val) then ifield.set_value("" .. tonumber(val)) end
end
-- set minimum input value
---@param min integer minimum allowed value
function e.set_min(min)
args.min = min
e.on_unfocused()
end
-- set maximum input value
---@param max integer maximum allowed value
function e.set_max(max)
args.max = max
e.on_unfocused()
end
-- replace text with pasted text if its a number
---@param text string string pasted
function e.handle_paste(text)
if tonumber(text) then
ifield.set_value("" .. tonumber(text))
else
ifield.set_value("0")
end
end
-- handle unfocused
function e.on_unfocused()
local val = tonumber(e.value)
local max = tonumber(args.max)
local min = tonumber(args.min)
if type(val) == "number" then
if type(args.max) == "number" and val > max then
e.value = "" .. max
ifield.nav_start()
elseif type(args.min) == "number" and val < min then
e.value = "" .. min
ifield.nav_start()
else
e.value = "" .. val
ifield.nav_end()
end
else
e.value = ""
end
ifield.show()
end
-- handle focus (not unfocus), enable, and redraw with show()
e.on_focused = ifield.show
e.on_enabled = ifield.show
e.on_disabled = ifield.show
e.redraw = ifield.show
-- initial draw
e.redraw()
return e.complete()
end
return number_field

View File

@@ -0,0 +1,105 @@
-- Text Value Entry Graphics Element
local core = require("graphics.core")
local element = require("graphics.element")
local KEY_CLICK = core.events.KEY_CLICK
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class text_field_args
---@field value? string initial value
---@field max_len? integer maximum string length
---@field censor? string character to replace text with when printing to screen
---@field dis_fg_bg? cpair foreground/background colors when disabled
---@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 text entry field
---@param args text_field_args
---@return graphics_element element, element_id id, function censor_ctl
local function text_field(args)
args.height = 1
args.can_focus = true
-- create new graphics element base object
local e = element.new(args)
-- set initial value
e.value = args.value or ""
-- make an interactive field manager
local ifield = core.new_ifield(e, args.max_len or e.frame.w, args.fg_bg, args.dis_fg_bg)
ifield.censor(args.censor)
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
-- only handle if on an increment or decrement arrow
if e.enabled then
if core.events.was_clicked(event.type) then
e.take_focus()
if event.type == MOUSE_CLICK.UP then
ifield.move_cursor(event.current.x)
end
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
ifield.select_all()
end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == KEY_CLICK.CHAR then
ifield.try_insert_char(event.name)
elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
if (event.key == keys.backspace or event.key == keys.delete) then
ifield.backspace()
elseif event.key == keys.left then
ifield.nav_left()
elseif event.key == keys.right then
ifield.nav_right()
elseif event.key == keys.a and event.ctrl then
ifield.select_all()
elseif event.key == keys.home or event.key == keys.up then
ifield.nav_start()
elseif event.key == keys["end"] or event.key == keys.down then
ifield.nav_end()
end
end
end
-- set the value
---@param val string string to set
function e.set_value(val)
ifield.set_value(val)
end
-- replace text with pasted text
---@param text string string to set
function e.handle_paste(text)
ifield.set_value(text)
end
-- handle focus, enable, and redraw with show()
e.on_focused = ifield.show
e.on_unfocused = ifield.show
e.on_enabled = ifield.show
e.on_disabled = ifield.show
e.redraw = ifield.show
-- initial draw
e.redraw()
local elem, id = e.complete()
return elem, id, ifield.censor
end
return text_field

View File

@@ -16,21 +16,22 @@ 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
---@param args alarm_indicator_light ---@param args alarm_indicator_light
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function alarm_indicator_light(args) local function alarm_indicator_light(args)
assert(type(args.label) == "string", "graphics.elements.indicators.alight: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.c1) == "number", "graphics.elements.indicators.alight: c1 is a required field") element.assert(type(args.c1) == "number", "c1 is a required field")
assert(type(args.c2) == "number", "graphics.elements.indicators.alight: c2 is a required field") element.assert(type(args.c2) == "number", "c2 is a required field")
assert(type(args.c3) == "number", "graphics.elements.indicators.alight: c3 is a required field") element.assert(type(args.c3) == "number", "c3 is a required field")
if args.flash then if args.flash then
assert(util.is_int(args.period), "graphics.elements.indicators.alight: period is a required field if flash is enabled") element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
end end
-- single line -- single line
@@ -50,19 +51,21 @@ local function alarm_indicator_light(args)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = 1
-- called by flasher when enabled -- called by flasher when enabled
local function flash_callback() local function flash_callback()
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if flash_on then if flash_on then
if e.value == 2 then if e.value == 2 then
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
end end
else else
if e.value == 3 then if e.value == 3 then
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
else else
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end end
end end
@@ -75,7 +78,7 @@ local function alarm_indicator_light(args)
local was_off = e.value ~= 2 local was_off = e.value ~= 2
e.value = new_state e.value = new_state
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if args.flash then if args.flash then
if was_off and (new_state == 2) then if was_off and (new_state == 2) then
@@ -86,17 +89,17 @@ local function alarm_indicator_light(args)
flasher.stop(flash_callback) flasher.stop(flash_callback)
if new_state == 3 then if new_state == 3 then
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
else else
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end end
end end
elseif new_state == 2 then elseif new_state == 2 then
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
elseif new_state == 3 then elseif new_state == 3 then
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
else else
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end end
end end
@@ -104,11 +107,16 @@ local function alarm_indicator_light(args)
---@param val integer indicator state ---@param val integer indicator state
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light -- draw label and indicator light
e.on_update(1) function e.redraw()
e.window.write(args.label) e.on_update(e.value)
e.w_write(args.label)
end
return e.get() -- initial draw
e.redraw()
return e.complete()
end end
return alarm_indicator_light return alarm_indicator_light

View File

@@ -11,15 +11,15 @@ 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
---@param args core_map_args ---@param args core_map_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function core_map(args) local function core_map(args)
assert(util.is_int(args.reactor_l), "graphics.elements.indicators.coremap: reactor_l is a required field") element.assert(util.is_int(args.reactor_l), "reactor_l is a required field")
assert(util.is_int(args.reactor_w), "graphics.elements.indicators.coremap: reactor_w is a required field") element.assert(util.is_int(args.reactor_w), "reactor_w is a required field")
-- require max dimensions -- require max dimensions
args.width = 18 args.width = 18
@@ -31,6 +31,8 @@ local function core_map(args)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = 0
local alternator = true local alternator = true
local core_l = args.reactor_l - 2 local core_l = args.reactor_l - 2
@@ -47,25 +49,25 @@ local function core_map(args)
-- create coordinate grid and frame -- create coordinate grid and frame
local function draw_frame() local function draw_frame()
e.window.setTextColor(colors.white) e.w_set_fgd(colors.white)
for x = 0, (inner_width - 1) do for x = 0, (inner_width - 1) do
e.window.setCursorPos(x + start_x, 1) e.w_set_cur(x + start_x, 1)
e.window.write(util.sprintf("%X", x)) e.w_write(util.sprintf("%X", x))
end end
for y = 0, (inner_height - 1) do for y = 0, (inner_height - 1) do
e.window.setCursorPos(1, y + start_y) e.w_set_cur(1, y + start_y)
e.window.write(util.sprintf("%X", y)) e.w_write(util.sprintf("%X", y))
end end
-- even out bottom edge -- even out bottom edge
e.window.setTextColor(e.fg_bg.bkg) e.w_set_fgd(e.fg_bg.bkg)
e.window.setBackgroundColor(args.parent.get_fg_bg().bkg) e.w_set_bkg(args.parent.get_fg_bg().bkg)
e.window.setCursorPos(1, e.frame.h) e.w_set_cur(1, e.frame.h)
e.window.write(util.strrep("\x8f", e.frame.w)) e.w_write(string.rep("\x8f", e.frame.w))
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
end end
-- draw the core -- draw the core
@@ -102,13 +104,13 @@ local function core_map(args)
-- draw pattern -- draw pattern
for y = start_y, inner_height + (start_y - 1) do for y = start_y, inner_height + (start_y - 1) do
e.window.setCursorPos(start_x, y) e.w_set_cur(start_x, y)
for _ = 1, inner_width do for _ = 1, inner_width do
if alternator then if alternator then
i = i + 1 i = i + 1
e.window.blit("\x07", text_c, back_c) e.w_blit("\x07", text_c, back_c)
else else
e.window.blit("\x07", "7", "8") e.w_blit("\x07", "7", "8")
end end
alternator = not alternator alternator = not alternator
@@ -157,13 +159,16 @@ local function core_map(args)
e.on_update(e.value) e.on_update(e.value)
end end
-- initial (one-time except for resize()) frame draw -- redraw both frame and core
draw_frame() function e.redraw()
draw_frame()
draw_core(e.value)
end
-- initial draw -- initial draw
e.on_update(0) e.redraw()
return e.get() return e.complete()
end end
return core_map return core_map

View File

@@ -14,37 +14,31 @@ 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
---@param args data_indicator_args ---@param args data_indicator_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function data(args) local function data(args)
assert(type(args.label) == "string", "graphics.elements.indicators.data: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.format) == "string", "graphics.elements.indicators.data: format is a required field") element.assert(type(args.format) == "string", "format is a required field")
assert(args.value ~= nil, "graphics.elements.indicators.data: value is a required field") element.assert(args.value ~= nil, "value is a required field")
assert(util.is_int(args.width), "graphics.elements.indicators.data: width is a required field") element.assert(util.is_int(args.width), "width is a required field")
-- single line
args.height = 1 args.height = 1
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- label color e.value = args.value
if args.lu_colors ~= nil then
e.window.setTextColor(args.lu_colors.color_a)
end
-- write label local value_color = e.fg_bg.fgd
e.window.setCursorPos(1, 1) local label_len = string.len(args.label)
e.window.write(args.label) 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
@@ -58,25 +52,25 @@ local function data(args)
e.value = value e.value = value
-- clear old data and label -- clear old data and label
e.window.setCursorPos(data_start, 1) e.w_set_cur(data_start, 1)
e.window.write(util.spaces(clear_width)) e.w_write(util.spaces(clear_width))
-- 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.w_set_cur(data_start, 1)
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(value_color)
if args.commas then if args.commas then
e.window.write(util.comma_format(data_str)) e.w_write(util.comma_format(data_str))
else else
e.window.write(data_str) e.w_write(data_str)
end end
-- write label -- write label
if args.unit ~= nil then if args.unit ~= nil then
if args.lu_colors ~= nil then if args.lu_colors ~= nil then
e.window.setTextColor(args.lu_colors.color_b) e.w_set_fgd(args.lu_colors.color_b)
end end
e.window.write(" " .. args.unit) e.w_write(" " .. args.unit)
end end
end end
@@ -84,10 +78,26 @@ 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
-- initial value draw -- change the foreground color of the value, or all text if no label/unit colors provided
e.on_update(args.value) ---@param c color
function e.recolor(c)
value_color = c
e.on_update(e.value)
end
return e.get() -- element redraw
function e.redraw()
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
e.w_set_cur(1, 1)
e.w_write(args.label)
e.on_update(e.value)
end
-- initial draw
e.redraw()
return e.complete()
end end
return data return data

View File

@@ -10,27 +10,29 @@ 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
---@param args hbar_args ---@param args hbar_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function hbar(args) local function hbar(args)
-- properties/state
local last_num_bars = -1
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = 0.0
-- bar width is width - 5 characters for " 100%" if showing percent -- bar width is width - 5 characters for " 100%" if showing percent
local bar_width = util.trinary(args.show_percent, e.frame.w - 5, e.frame.w) local bar_width = util.trinary(args.show_percent, e.frame.w - 5, e.frame.w)
assert(bar_width > 0, "graphics.elements.indicators.hbar: too small for bar") element.assert(bar_width > 0, "too small for bar")
local last_num_bars = -1
-- determine bar colors -- determine bar colors
local bar_bkg = e.fg_bg.blit_bkg local bar_bkg = e.fg_bg.blit_bkg
@@ -86,16 +88,16 @@ local function hbar(args)
-- draw bar -- draw bar
for y = 1, e.frame.h do for y = 1, e.frame.h do
e.window.setCursorPos(1, y) e.w_set_cur(1, y)
-- intentionally swapped fgd/bkg since we use spaces as fill, but they are the opposite -- intentionally swapped fgd/bkg since we use spaces as fill, but they are the opposite
e.window.blit(spaces, bkg, fgd) e.w_blit(spaces, bkg, fgd)
end end
end end
-- update percentage -- update percentage
if args.show_percent then if args.show_percent then
e.window.setCursorPos(bar_width + 2, math.max(1, math.ceil(e.frame.h / 2))) e.w_set_cur(bar_width + 2, math.max(1, math.ceil(e.frame.h / 2)))
e.window.write(util.sprintf("%3.0f%%", fraction * 100)) e.w_write(util.sprintf("%3.0f%%", fraction * 100))
end end
end end
@@ -104,22 +106,23 @@ local function hbar(args)
function e.recolor(bar_fg_bg) function e.recolor(bar_fg_bg)
bar_bkg = bar_fg_bg.blit_bkg bar_bkg = bar_fg_bg.blit_bkg
bar_fgd = bar_fg_bg.blit_fgd bar_fgd = bar_fg_bg.blit_fgd
e.redraw()
-- re-draw
last_num_bars = 0
if type(e.value) == "number" then
e.on_update(e.value)
end
end end
-- set the percentage value -- set the percentage value
---@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
-- initialize to 0 -- element redraw
e.on_update(0) function e.redraw()
last_num_bars = -1
e.on_update(e.value)
end
return e.get() -- initial draw
e.redraw()
return e.complete()
end end
return hbar return hbar

View File

@@ -1,7 +1,5 @@
-- Icon Indicator Graphics Element -- Icon Indicator Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element") local element = require("graphics.element")
---@class icon_sym_color ---@class icon_sym_color
@@ -16,26 +14,26 @@ 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
---@param args icon_indicator_args ---@param args icon_indicator_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function icon(args) local function icon(args)
assert(type(args.label) == "string", "graphics.elements.indicators.icon: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.states) == "table", "graphics.elements.indicators.icon: states is a required field") element.assert(type(args.states) == "table", "states is a required field")
-- single line
args.height = 1 args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4 args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = args.value or 1
-- state blit strings -- state blit strings
local state_blit_cmds = {} local state_blit_cmds = {}
for i = 1, #args.states do for i = 1, #args.states do
@@ -43,32 +41,36 @@ local function icon(args)
table.insert(state_blit_cmds, { table.insert(state_blit_cmds, {
text = " " .. sym_color.symbol .. " ", text = " " .. sym_color.symbol .. " ",
fgd = util.strrep(sym_color.color.blit_fgd, 3), fgd = string.rep(sym_color.color.blit_fgd, 3),
bkg = util.strrep(sym_color.color.blit_bkg, 3) bkg = string.rep(sym_color.color.blit_bkg, 3)
}) })
end end
-- write label and initial indicator light
e.window.setCursorPos(5, 1)
e.window.write(args.label)
-- on state change -- on state change
---@param new_state integer indicator state ---@param new_state integer indicator state
function e.on_update(new_state) function e.on_update(new_state)
local blit_cmd = state_blit_cmds[new_state] local blit_cmd = state_blit_cmds[new_state]
e.value = new_state e.value = new_state
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg) e.w_blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
end end
-- set indicator state -- set indicator state
---@param val integer indicator state ---@param val integer indicator state
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- initial icon draw -- element redraw
e.on_update(args.value or 1) function e.redraw()
e.w_set_cur(5, 1)
e.w_write(args.label)
return e.get() e.on_update(e.value)
end
-- initial draw
e.redraw()
return e.complete()
end end
return icon return icon

View File

@@ -14,41 +14,40 @@ 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
---@param args indicator_led_args ---@param args indicator_led_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function indicator_led(args) local function indicator_led(args)
assert(type(args.label) == "string", "graphics.elements.indicators.led: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.colors) == "table", "graphics.elements.indicators.led: colors is a required field") element.assert(type(args.colors) == "table", "colors is a required field")
if args.flash then if args.flash then
assert(util.is_int(args.period), "graphics.elements.indicators.led: period is a required field if flash is enabled") element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
end end
-- single line
args.height = 1 args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2 args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
-- flasher state
local flash_on = true local flash_on = true
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = false
-- called by flasher when enabled -- called by flasher when enabled
local function flash_callback() local function flash_callback()
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if flash_on then if flash_on then
e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg) e.w_blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
else else
e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg) e.w_blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
end end
flash_on = not flash_on flash_on = not flash_on
@@ -60,8 +59,8 @@ local function indicator_led(args)
flash_on = true flash_on = true
flasher.start(flash_callback, args.period) flasher.start(flash_callback, args.period)
else else
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg) e.w_blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
end end
end end
@@ -72,8 +71,8 @@ local function indicator_led(args)
flasher.stop(flash_callback) flasher.stop(flash_callback)
end end
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg) e.w_blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
end end
-- on state change -- on state change
@@ -87,14 +86,19 @@ local function indicator_led(args)
---@param val boolean indicator state ---@param val boolean indicator state
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light -- draw label and indicator light
e.on_update(false) function e.redraw()
if string.len(args.label) > 0 then e.on_update(e.value)
e.window.setCursorPos(3, 1) if string.len(args.label) > 0 then
e.window.write(args.label) e.w_set_cur(3, 1)
e.w_write(args.label)
end
end end
return e.get() -- initial draw
e.redraw()
return e.complete()
end end
return indicator_led return indicator_led

View File

@@ -16,33 +16,29 @@ 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
---@param args indicator_led_pair_args ---@param args indicator_led_pair_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function indicator_led_pair(args) local function indicator_led_pair(args)
assert(type(args.label) == "string", "graphics.elements.indicators.ledpair: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.off) == "number", "graphics.elements.indicators.ledpair: off is a required field") element.assert(type(args.off) == "number", "off is a required field")
assert(type(args.c1) == "number", "graphics.elements.indicators.ledpair: c1 is a required field") element.assert(type(args.c1) == "number", "c1 is a required field")
assert(type(args.c2) == "number", "graphics.elements.indicators.ledpair: c2 is a required field") element.assert(type(args.c2) == "number", "c2 is a required field")
if args.flash then if args.flash then
assert(util.is_int(args.period), "graphics.elements.indicators.ledpair: period is a required field if flash is enabled") element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
end end
-- single line
args.height = 1 args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2 args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
-- flasher state
local flash_on = true local flash_on = true
-- blit translations
local co = colors.toBlit(args.off) local co = colors.toBlit(args.off)
local c1 = colors.toBlit(args.c1) local c1 = colors.toBlit(args.c1)
local c2 = colors.toBlit(args.c2) local c2 = colors.toBlit(args.c2)
@@ -50,21 +46,20 @@ local function indicator_led_pair(args)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- init value for initial check in on_update
e.value = 1 e.value = 1
-- called by flasher when enabled -- called by flasher when enabled
local function flash_callback() local function flash_callback()
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if flash_on then if flash_on then
if e.value == 2 then if e.value == 2 then
e.window.blit("\x8c", c1, e.fg_bg.blit_bkg) e.w_blit("\x8c", c1, e.fg_bg.blit_bkg)
elseif e.value == 3 then elseif e.value == 3 then
e.window.blit("\x8c", c2, e.fg_bg.blit_bkg) e.w_blit("\x8c", c2, e.fg_bg.blit_bkg)
end end
else else
e.window.blit("\x8c", co, e.fg_bg.blit_bkg) e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
end end
flash_on = not flash_on flash_on = not flash_on
@@ -76,7 +71,7 @@ local function indicator_led_pair(args)
local was_off = e.value <= 1 local was_off = e.value <= 1
e.value = new_state e.value = new_state
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if args.flash then if args.flash then
if was_off and (new_state > 1) then if was_off and (new_state > 1) then
@@ -85,15 +80,14 @@ local function indicator_led_pair(args)
elseif new_state <= 1 then elseif new_state <= 1 then
flash_on = false flash_on = false
flasher.stop(flash_callback) flasher.stop(flash_callback)
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
end end
elseif new_state == 2 then elseif new_state == 2 then
e.window.blit("\x8c", c1, e.fg_bg.blit_bkg) e.w_blit("\x8c", c1, e.fg_bg.blit_bkg)
elseif new_state == 3 then elseif new_state == 3 then
e.window.blit("\x8c", c2, e.fg_bg.blit_bkg) e.w_blit("\x8c", c2, e.fg_bg.blit_bkg)
else else
e.window.blit("\x8c", co, e.fg_bg.blit_bkg) e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
end end
end end
@@ -101,14 +95,19 @@ local function indicator_led_pair(args)
---@param val integer indicator state ---@param val integer indicator state
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light -- draw label and indicator light
e.on_update(1) function e.redraw()
if string.len(args.label) > 0 then e.on_update(e.value)
e.window.setCursorPos(3, 1) if string.len(args.label) > 0 then
e.window.write(args.label) e.w_set_cur(3, 1)
e.w_write(args.label)
end
end end
return e.get() -- initial draw
e.redraw()
return e.complete()
end end
return indicator_led_pair return indicator_led_pair

View File

@@ -9,36 +9,33 @@ 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
---@param args indicator_led_rgb_args ---@param args indicator_led_rgb_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function indicator_led_rgb(args) local function indicator_led_rgb(args)
assert(type(args.label) == "string", "graphics.elements.indicators.ledrgb: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.colors) == "table", "graphics.elements.indicators.ledrgb: colors is a required field") element.assert(type(args.colors) == "table", "colors is a required field")
-- single line
args.height = 1 args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2 args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- init value for initial check in on_update
e.value = 1 e.value = 1
-- on state change -- on state change
---@param new_state integer indicator state ---@param new_state integer indicator state
function e.on_update(new_state) function e.on_update(new_state)
e.value = new_state e.value = new_state
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if type(args.colors[new_state]) == "number" then if type(args.colors[new_state]) == "number" then
e.window.blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg) e.w_blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg)
end end
end end
@@ -46,14 +43,19 @@ local function indicator_led_rgb(args)
---@param val integer indicator state ---@param val integer indicator state
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light -- draw label and indicator light
e.on_update(1) function e.redraw()
if string.len(args.label) > 0 then e.on_update(e.value)
e.window.setCursorPos(3, 1) if string.len(args.label) > 0 then
e.window.write(args.label) e.w_set_cur(3, 1)
e.w_write(args.label)
end
end end
return e.get() -- initial draw
e.redraw()
return e.complete()
end end
return indicator_led_rgb return indicator_led_rgb

View File

@@ -14,41 +14,40 @@ 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
---@param args indicator_light_args ---@param args indicator_light_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function indicator_light(args) local function indicator_light(args)
assert(type(args.label) == "string", "graphics.elements.indicators.light: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.colors) == "table", "graphics.elements.indicators.light: colors is a required field") element.assert(type(args.colors) == "table", "colors is a required field")
if args.flash then if args.flash then
assert(util.is_int(args.period), "graphics.elements.indicators.light: period is a required field if flash is enabled") element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
end end
-- single line
args.height = 1 args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
-- flasher state
local flash_on = true local flash_on = true
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = false
-- called by flasher when enabled -- called by flasher when enabled
local function flash_callback() local function flash_callback()
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if flash_on then if flash_on then
e.window.blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
else else
e.window.blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
end end
flash_on = not flash_on flash_on = not flash_on
@@ -60,8 +59,8 @@ local function indicator_light(args)
flash_on = true flash_on = true
flasher.start(flash_callback, args.period) flasher.start(flash_callback, args.period)
else else
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
e.window.blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
end end
end end
@@ -72,8 +71,8 @@ local function indicator_light(args)
flasher.stop(flash_callback) flasher.stop(flash_callback)
end end
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
e.window.blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
end end
-- on state change -- on state change
@@ -87,12 +86,17 @@ local function indicator_light(args)
---@param val boolean indicator state ---@param val boolean indicator state
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light -- draw label and indicator light
e.on_update(false) function e.redraw()
e.window.setCursorPos(3, 1) e.on_update(false)
e.window.write(args.label) e.w_set_cur(3, 1)
e.w_write(args.label)
end
return e.get() -- initial draw
e.redraw()
return e.complete()
end end
return indicator_light return indicator_light

View File

@@ -9,39 +9,31 @@ local element = require("graphics.element")
---@field format string power format override (lua string format) ---@field format string power format override (lua string format)
---@field rate boolean? whether to append /t to the end (power per tick) ---@field rate boolean? whether to append /t to the end (power per tick)
---@field lu_colors? cpair label foreground color (a), unit foreground color (b) ---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
---@field value any default value ---@field value number default value
---@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
---@param args power_indicator_args ---@param args power_indicator_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function power(args) local function power(args)
assert(args.value ~= nil, "graphics.elements.indicators.power: value is a required field") element.assert(type(args.value) == "number", "value is a required field")
assert(util.is_int(args.width), "graphics.elements.indicators.power: width is a required field") element.assert(util.is_int(args.width), "width is a required field")
-- single line
args.height = 1 args.height = 1
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- label color e.value = args.value
if args.lu_colors ~= nil then
e.window.setTextColor(args.lu_colors.color_a)
end
-- write label local data_start = 0
e.window.setCursorPos(1, 1)
e.window.write(args.label)
local data_start = string.len(args.label) + 2
if string.len(args.label) == 0 then data_start = 1 end
-- on state change -- on state change
---@param value any new value ---@param value any new value
@@ -51,13 +43,13 @@ local function power(args)
local data_str, unit = util.power_format(value, false, args.format) local data_str, unit = util.power_format(value, false, args.format)
-- write data -- write data
e.window.setCursorPos(data_start, 1) e.w_set_cur(data_start, 1)
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.window.write(util.comma_format(data_str)) e.w_write(util.comma_format(data_str))
-- write unit -- write unit
if args.lu_colors ~= nil then if args.lu_colors ~= nil then
e.window.setTextColor(args.lu_colors.color_b) e.w_set_fgd(args.lu_colors.color_b)
end end
-- append per tick if rate is set -- append per tick if rate is set
@@ -69,17 +61,29 @@ local function power(args)
if unit == "FE" then unit = "FE " end if unit == "FE" then unit = "FE " end
end end
e.window.write(" " .. unit) e.w_write(" " .. unit)
end end
-- set the value -- set the value
---@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
-- initial value draw -- element redraw
e.on_update(args.value) function e.redraw()
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
e.w_set_cur(1, 1)
e.w_write(args.label)
return e.get() data_start = string.len(args.label) + 2
if string.len(args.label) == 0 then data_start = 1 end
e.on_update(e.value)
end
-- initial draw
e.redraw()
return e.complete()
end end
return power return power

View File

@@ -10,37 +10,30 @@ local element = require("graphics.element")
---@field format string data format (lua string format) ---@field format string data format (lua string format)
---@field commas? boolean whether to use commas if a number is given (default to false) ---@field commas? boolean whether to use commas if a number is given (default to false)
---@field lu_colors? cpair label foreground color (a), unit foreground color (b) ---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
---@field value any default value ---@field value? radiation_reading default value
---@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
---@param args rad_indicator_args ---@param args rad_indicator_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function rad(args) local function rad(args)
assert(type(args.label) == "string", "graphics.elements.indicators.rad: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.format) == "string", "graphics.elements.indicators.rad: format is a required field") element.assert(type(args.format) == "string", "format is a required field")
assert(util.is_int(args.width), "graphics.elements.indicators.rad: width is a required field") element.assert(util.is_int(args.width), "width is a required field")
-- single line
args.height = 1 args.height = 1
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- label color e.value = args.value or types.new_zero_radiation_reading()
if args.lu_colors ~= nil then
e.window.setTextColor(args.lu_colors.color_a)
end
-- write label
e.window.setCursorPos(1, 1)
e.window.write(args.label)
local label_len = string.len(args.label) local label_len = string.len(args.label)
local data_start = 1 local data_start = 1
@@ -57,34 +50,43 @@ local function rad(args)
e.value = value.radiation e.value = value.radiation
-- clear old data and label -- clear old data and label
e.window.setCursorPos(data_start, 1) e.w_set_cur(data_start, 1)
e.window.write(util.spaces(clear_width)) e.w_write(util.spaces(clear_width))
-- write data -- write data
local data_str = util.sprintf(args.format, e.value) local data_str = util.sprintf(args.format, e.value)
e.window.setCursorPos(data_start, 1) e.w_set_cur(data_start, 1)
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
if args.commas then if args.commas then
e.window.write(util.comma_format(data_str)) e.w_write(util.comma_format(data_str))
else else
e.window.write(data_str) e.w_write(data_str)
end end
-- write unit -- write unit
if args.lu_colors ~= nil then if args.lu_colors ~= nil then
e.window.setTextColor(args.lu_colors.color_b) e.w_set_fgd(args.lu_colors.color_b)
end end
e.window.write(" " .. value.unit) e.w_write(" " .. value.unit)
end end
-- set the value -- set the value
---@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
-- initial value draw -- element redraw
e.on_update(types.new_zero_radiation_reading()) function e.redraw()
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
e.w_set_cur(1, 1)
e.w_write(args.label)
return e.get() e.on_update(e.value)
end
-- initial draw
e.redraw()
return e.complete()
end end
return rad return rad

View File

@@ -15,25 +15,22 @@ 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
---@param args state_indicator_args ---@param args state_indicator_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function state_indicator(args) local function state_indicator(args)
assert(type(args.states) == "table", "graphics.elements.indicators.state: states is a required field") element.assert(type(args.states) == "table", "states is a required field")
-- determine height
if util.is_int(args.height) then if util.is_int(args.height) then
assert(args.height % 2 == 1, "graphics.elements.indicators.state: height should be an odd number") element.assert(args.height % 2 == 1, "height should be an odd number")
else else args.height = 1 end
args.height = 1
end
-- initial guess at width
args.width = args.min_width or 1 args.width = args.min_width or 1
-- state blit strings -- state blit strings
@@ -41,7 +38,6 @@ local function state_indicator(args)
for i = 1, #args.states do for i = 1, #args.states do
local state_def = args.states[i] ---@type state_text_color local state_def = args.states[i] ---@type state_text_color
-- re-determine width
if string.len(state_def.text) > args.width then if string.len(state_def.text) > args.width then
args.width = string.len(state_def.text) args.width = string.len(state_def.text)
end end
@@ -50,21 +46,28 @@ local function state_indicator(args)
table.insert(state_blit_cmds, { table.insert(state_blit_cmds, {
text = text, text = text,
fgd = util.strrep(state_def.color.blit_fgd, string.len(text)), fgd = string.rep(state_def.color.blit_fgd, string.len(text)),
bkg = util.strrep(state_def.color.blit_bkg, string.len(text)) bkg = string.rep(state_def.color.blit_bkg, string.len(text))
}) })
end end
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = args.value or 1
-- element redraw
function e.redraw()
local blit_cmd = state_blit_cmds[e.value]
e.w_set_cur(1, 1)
e.w_blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
end
-- on state change -- on state change
---@param new_state integer indicator state ---@param new_state integer indicator state
function e.on_update(new_state) function e.on_update(new_state)
local blit_cmd = state_blit_cmds[new_state]
e.value = new_state e.value = new_state
e.window.setCursorPos(1, 1) e.redraw()
e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
end end
-- set indicator state -- set indicator state
@@ -72,9 +75,9 @@ local function state_indicator(args)
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- initial draw -- initial draw
e.on_update(args.value or 1) e.redraw()
return e.get() return e.complete()
end end
return state_indicator return state_indicator

View File

@@ -16,55 +16,50 @@ 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
---@param args tristate_indicator_light_args ---@param args tristate_indicator_light_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function tristate_indicator_light(args) local function tristate_indicator_light(args)
assert(type(args.label) == "string", "graphics.elements.indicators.trilight: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.c1) == "number", "graphics.elements.indicators.trilight: c1 is a required field") element.assert(type(args.c1) == "number", "c1 is a required field")
assert(type(args.c2) == "number", "graphics.elements.indicators.trilight: c2 is a required field") element.assert(type(args.c2) == "number", "c2 is a required field")
assert(type(args.c3) == "number", "graphics.elements.indicators.trilight: c3 is a required field") element.assert(type(args.c3) == "number", "c3 is a required field")
if args.flash then if args.flash then
assert(util.is_int(args.period), "graphics.elements.indicators.trilight: period is a required field if flash is enabled") element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
end end
-- single line
args.height = 1 args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
-- flasher state
local flash_on = true
-- blit translations
local c1 = colors.toBlit(args.c1)
local c2 = colors.toBlit(args.c2)
local c3 = colors.toBlit(args.c3)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- init value for initial check in on_update
e.value = 1 e.value = 1
local flash_on = true
local c1 = colors.toBlit(args.c1)
local c2 = colors.toBlit(args.c2)
local c3 = colors.toBlit(args.c3)
-- called by flasher when enabled -- called by flasher when enabled
local function flash_callback() local function flash_callback()
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if flash_on then if flash_on then
if e.value == 2 then if e.value == 2 then
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
elseif e.value == 3 then elseif e.value == 3 then
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
end end
else else
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end end
flash_on = not flash_on flash_on = not flash_on
@@ -76,7 +71,7 @@ local function tristate_indicator_light(args)
local was_off = e.value <= 1 local was_off = e.value <= 1
e.value = new_state e.value = new_state
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if args.flash then if args.flash then
if was_off and (new_state > 1) then if was_off and (new_state > 1) then
@@ -86,14 +81,14 @@ local function tristate_indicator_light(args)
flash_on = false flash_on = false
flasher.stop(flash_callback) flasher.stop(flash_callback)
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end end
elseif new_state == 2 then elseif new_state == 2 then
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
elseif new_state == 3 then elseif new_state == 3 then
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
else else
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end end
end end
@@ -101,11 +96,16 @@ local function tristate_indicator_light(args)
---@param val integer indicator state ---@param val integer indicator state
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light -- draw light and label
e.on_update(1) function e.redraw()
e.window.write(args.label) e.on_update(1)
e.w_write(args.label)
end
return e.get() -- initial draw
e.redraw()
return e.complete()
end end
return tristate_indicator_light return tristate_indicator_light

View File

@@ -8,29 +8,30 @@ 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
---@param args vbar_args ---@param args vbar_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function vbar(args) local function vbar(args)
-- properties/state
local last_num_bars = -1
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- blit strings e.value = 0.0
local fgd = util.strrep(e.fg_bg.blit_fgd, e.frame.w)
local bkg = util.strrep(e.fg_bg.blit_bkg, e.frame.w) local last_num_bars = -1
local fgd = string.rep(e.fg_bg.blit_fgd, e.frame.w)
local bkg = string.rep(e.fg_bg.blit_bkg, e.frame.w)
local spaces = util.spaces(e.frame.w) local spaces = util.spaces(e.frame.w)
local one_third = util.strrep("\x8f", e.frame.w) local one_third = string.rep("\x8f", e.frame.w)
local two_thirds = util.strrep("\x83", e.frame.w) local two_thirds = string.rep("\x83", e.frame.w)
-- handle data changes -- handle data changes
---@param fraction number 0.0 to 1.0 ---@param fraction number 0.0 to 1.0
@@ -51,55 +52,56 @@ local function vbar(args)
if num_bars ~= last_num_bars then if num_bars ~= last_num_bars then
last_num_bars = num_bars last_num_bars = num_bars
-- start bottom up
local y = e.frame.h local y = e.frame.h
e.w_set_cur(1, y)
-- start at base of vertical bar
e.window.setCursorPos(1, y)
-- fill percentage -- fill percentage
for _ = 1, num_bars / 3 do for _ = 1, num_bars / 3 do
e.window.blit(spaces, bkg, fgd) e.w_blit(spaces, bkg, fgd)
y = y - 1 y = y - 1
e.window.setCursorPos(1, y) e.w_set_cur(1, y)
end end
-- add fractional bar if needed -- add fractional bar if needed
if num_bars % 3 == 1 then if num_bars % 3 == 1 then
e.window.blit(one_third, bkg, fgd) e.w_blit(one_third, bkg, fgd)
y = y - 1 y = y - 1
elseif num_bars % 3 == 2 then elseif num_bars % 3 == 2 then
e.window.blit(two_thirds, bkg, fgd) e.w_blit(two_thirds, bkg, fgd)
y = y - 1 y = y - 1
end end
-- fill the rest blank -- fill the rest blank
while y > 0 do while y > 0 do
e.window.setCursorPos(1, y) e.w_set_cur(1, y)
e.window.blit(spaces, fgd, bkg) e.w_blit(spaces, fgd, bkg)
y = y - 1 y = y - 1
end end
end end
end end
-- change bar color
---@param fg_bg cpair new bar colors
function e.recolor(fg_bg)
fgd = util.strrep(fg_bg.blit_fgd, e.frame.w)
bkg = util.strrep(fg_bg.blit_bkg, e.frame.w)
-- re-draw
last_num_bars = 0
if type(e.value) == "number" then
e.on_update(e.value)
end
end
-- set the percentage value -- set the percentage value
---@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() -- element redraw
function e.redraw()
last_num_bars = -1
e.on_update(e.value)
end
-- change bar color
---@param fg_bg cpair new bar colors
function e.recolor(fg_bg)
fgd = string.rep(fg_bg.blit_fgd, e.frame.w)
bkg = string.rep(fg_bg.blit_bkg, e.frame.w)
e.redraw()
end
-- initial draw
e.redraw()
return e.complete()
end end
return vbar return vbar

View File

@@ -0,0 +1,343 @@
-- Scroll-able List Box Display Graphics Element
local tcd = require("scada-common.tcd")
local core = require("graphics.core")
local element = require("graphics.element")
local KEY_CLICK = core.events.KEY_CLICK
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@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)
args.can_focus = true
-- 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.w_set_fgd(active_fg_bg.fgd)
e.w_set_bkg(active_fg_bg.bkg)
e.w_set_cur(e.frame.w, 1)
e.w_write("\x1e")
e.w_set_fgd(nav_fg_bg.fgd)
e.w_set_bkg(nav_fg_bg.bkg)
e.w_set_cur(e.frame.w, e.frame.h)
e.w_write("\x1f")
elseif pressed_arrow == -1 then
e.w_set_fgd(nav_fg_bg.fgd)
e.w_set_bkg(nav_fg_bg.bkg)
e.w_set_cur(e.frame.w, 1)
e.w_write("\x1e")
e.w_set_fgd(active_fg_bg.fgd)
e.w_set_bkg(active_fg_bg.bkg)
e.w_set_cur(e.frame.w, e.frame.h)
e.w_write("\x1f")
else
e.w_set_fgd(nav_fg_bg.fgd)
e.w_set_bkg(nav_fg_bg.bkg)
e.w_set_cur(e.frame.w, 1)
e.w_write("\x1e")
e.w_set_cur(e.frame.w, e.frame.h)
e.w_write("\x1f")
end
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(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.w_set_bkg(args.nav_fg_bg.fgd)
else
e.w_set_bkg(e.fg_bg.fgd)
end
else
if args.nav_fg_bg ~= nil then
e.w_set_bkg(args.nav_fg_bg.bkg)
else
e.w_set_bkg(e.fg_bg.bkg)
end
end
e.w_set_cur(e.frame.w, i)
if e.is_focused() then e.w_write("\x7f") else e.w_write(" ") end
end
e.w_set_bkg(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)
-- shift mouse events
e.mouse_window_shift.y = scroll_offset
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 focus
e.on_focused = draw_bar
e.on_unfocused = draw_bar
-- handle a child in the list being focused, make sure it is visible
function e.on_child_focused(child)
for i = 1, #list do
local item = list[i] ---@type listbox_item
if item.e == child then
if (item.y + scroll_offset) <= 0 then
scroll_offset = 1 - item.y
update_positions()
draw_bar()
elseif (item.y + scroll_offset) == 1 then
-- do nothing, it's right at the top (if the bottom doesn't fit we can't easily fix that)
elseif ((item.h + item.y - 1) + scroll_offset) > e.frame.h then
scroll_offset = 1 - ((item.h + item.y) - e.frame.h)
update_positions()
draw_bar()
end
return
end
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled then
if event.type == MOUSE_CLICK.TAP then
if event.current.x == e.frame.w then
if event.current.y == 1 or event.current.y < bar_bounds[1] then
scroll_up()
if event.current.y == 1 then
draw_arrows(1)
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
end
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
scroll_down()
if event.current.y == e.frame.h then
draw_arrows(-1)
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
end
end
end
elseif event.type == MOUSE_CLICK.DOWN then
if event.current.x == e.frame.w then
if event.current.y == 1 or event.current.y < bar_bounds[1] then
scroll_up()
if event.current.y == 1 then draw_arrows(1) end
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
scroll_down()
if event.current.y == e.frame.h then draw_arrows(-1) end
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 == MOUSE_CLICK.UP then
holding_bar = false
draw_arrows(0)
elseif event.type == MOUSE_CLICK.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 == MOUSE_CLICK.SCROLL_DOWN then
scroll_down()
elseif event.type == MOUSE_CLICK.SCROLL_UP then
scroll_up()
end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
if event.key == keys.up then
scroll_up()
elseif event.key == keys.down then
scroll_down()
elseif event.key == keys.home then
scroll_offset = 0
update_positions()
elseif event.key == keys["end"] then
scroll_offset = max_down_scroll
update_positions()
end
end
end
-- element redraw
function e.redraw()
draw_arrows(0)
draw_bar()
end
-- initial draw
e.redraw()
return e.complete()
end
return listbox

View File

@@ -7,36 +7,44 @@ 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
---@param args multipane_args ---@param args multipane_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function multipane(args) local function multipane(args)
assert(type(args.panes) == "table", "graphics.elements.multipane: panes is a required field") element.assert(type(args.panes) == "table", "panes is a required field")
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = 1
-- show the selected pane
function e.redraw()
for i = 1, #args.panes do args.panes[i].hide() end
args.panes[e.value].show()
end
-- select which pane is shown -- select which pane is shown
---@param value integer pane to show ---@param value integer pane to show
function e.set_value(value) function e.set_value(value)
if (e.value ~= value) and (value > 0) and (value <= #args.panes) then if (e.value ~= value) and (value > 0) and (value <= #args.panes) then
e.value = value e.value = value
e.redraw()
for i = 1, #args.panes do args.panes[i].hide() end
args.panes[value].show()
end end
end end
e.set_value(1) -- initial draw
e.redraw()
return e.get() return e.complete()
end end
return multipane return multipane

View File

@@ -11,18 +11,24 @@ 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
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function pipenet(args) local function pipenet(args)
assert(type(args.pipes) == "table", "graphics.elements.indicators.pipenet: pipes is a required field") element.assert(type(args.pipes) == "table", "pipes is a required field")
args.width = 0 args.width = 0
args.height = 0 args.height = 0
-- determine width/height
for i = 1, #args.pipes do for i = 1, #args.pipes do
local pipe = args.pipes[i] ---@type pipe local pipe = args.pipes[i] ---@type pipe
@@ -43,105 +49,283 @@ local function pipenet(args)
-- 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 -- draw all pipes by drawing out lines
local y = 1 + pipe.y1 local function vector_draw()
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.cpair(pipe.color, e.fg_bg.bkg) if pipe.thin then
x_step = util.trinary(pipe.x1 == pipe.x2, 0, x_step)
if pipe.align_tr then y_step = util.trinary(pipe.y1 == pipe.y2, 0, y_step)
-- cross width then height
for i = 1, pipe.w do
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
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
e.window.blit("\x8f", c.blit_fgd, c.blit_bkg)
end
end
x = x + x_step
e.window.setCursorPos(x, y)
end end
-- back up one e.w_set_cur(x, y)
x = x - x_step
for _ = 1, pipe.h - 1 do local c = core.cpair(pipe.color, e.fg_bg.bkg)
y = y + y_step
e.window.setCursorPos(x, y)
if pipe.thin then if pipe.align_tr then
e.window.blit("\x95", c.blit_bkg, c.blit_fgd) -- cross width then height
else for i = 1, pipe.w do
e.window.blit(" ", c.blit_bkg, c.blit_fgd) if pipe.thin then
if i == pipe.w then
-- corner
if y_step > 0 then
e.w_blit("\x93", c.blit_bkg, c.blit_fgd)
else
e.w_blit("\x8e", c.blit_fgd, c.blit_bkg)
end
else
e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
end
else
if i == pipe.w and y_step > 0 then
-- corner
e.w_blit(" ", c.blit_bkg, c.blit_fgd)
else
e.w_blit("\x8f", c.blit_fgd, c.blit_bkg)
end
end
x = x + x_step
e.w_set_cur(x, y)
end end
-- back up one
x = x - x_step
for _ = 1, pipe.h - 1 do
y = y + y_step
e.w_set_cur(x, y)
if pipe.thin then
e.w_blit("\x95", c.blit_bkg, c.blit_fgd)
else
e.w_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.w_blit("\x97", c.blit_bkg, c.blit_fgd)
elseif y_step > 0 then
e.w_blit("\x8d", c.blit_fgd, c.blit_bkg)
else
e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
end
else
e.w_blit("\x95", c.blit_fgd, c.blit_bkg)
end
else
if i == pipe.h and y_step < 0 then
-- corner
e.w_blit("\x83", c.blit_bkg, c.blit_fgd)
else
e.w_blit(" ", c.blit_bkg, c.blit_fgd)
end
end
y = y + y_step
e.w_set_cur(x, y)
end
-- back up one
y = y - y_step
for _ = 1, pipe.w - 1 do
x = x + x_step
e.w_set_cur(x, y)
if pipe.thin then
e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
else
e.w_blit("\x83", c.blit_bkg, c.blit_fgd)
end
end
end
end
end
-- draw a particular map cell
---@param map table 2D cell map
---@param x integer x coord
---@param y integer y coord
local function draw_map_cell(map, x, y)
local entry = map[x][y] ---@type _pipe_map_entry already confirmed not false
local char
local invert = false
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 end
else else
-- cross height then width if check(x, y - 1) then -- above
for i = 1, pipe.h do -- not below and (if left or right)
if pipe.thin then if (not check(x, y + 1)) and (check(x - 1, y) or check(x + 1, y)) then
if i == pipe.h then char = util.trinary(entry.atr, "\x8f", " ")
-- corner invert = not entry.atr
if y_step < 0 then else -- not below w/ sides only
e.window.blit("\x97", c.blit_bkg, c.blit_fgd) char = " "
else invert = true
e.window.blit("\x8d", c.blit_fgd, c.blit_bkg) end
end elseif check(x, y + 1) then -- not above, if below
else -- if left or right
e.window.blit("\x95", c.blit_fgd, c.blit_bkg) if (check(x - 1, y) or check(x + 1, y)) then
end char = "\x83"
else invert = true
if i == pipe.h and y_step < 0 then else -- not left or right
-- corner char = " "
e.window.blit("\x83", c.blit_bkg, c.blit_fgd) invert = true
else end
e.window.blit(" ", c.blit_bkg, c.blit_fgd) else -- not above, not below
end char = util.trinary(entry.atr, "\x8f", "\x83")
invert = not entry.atr
end
end
e.w_set_cur(x, y)
if invert then
e.w_blit(char, entry.bg, entry.fg)
else
e.w_blit(char, entry.fg, entry.bg)
end
end
-- draw all pipes by assembling and marking up a 2D map<br>
-- this is an easy way to check adjacent blocks, which is required to properly draw thin pipes
local function map_draw()
local map = {}
for x = 1, args.width do
table.insert(map, {})
for _ = 1, args.height do table.insert(map[x], false) end
end
-- build map
for p = 1, #args.pipes do
local pipe = args.pipes[p] ---@type pipe
local x = 1 + pipe.x1
local y = 1 + pipe.y1
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 end
y = y + y_step x = x - x_step -- back up one
e.window.setCursorPos(x, y)
end
-- back up one for _ = 1, pipe.h do
y = y - y_step 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
for _ = 1, pipe.w - 1 do y = y - y_step -- back up one
x = x + x_step
e.window.setCursorPos(x, y)
if pipe.thin then for _ = 1, pipe.w do
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg) map[x][y] = entry
else x = x + x_step
e.window.blit("\x83", c.blit_bkg, c.blit_fgd)
end end
end end
end end
-- render
for x = 1, args.width do
for y = 1, args.height do
if map[x][y] ~= false then draw_map_cell(map, x, y) end
end
end
end end
return e.get() -- element redraw
function e.redraw()
if any_thin then map_draw() else vector_draw() end
end
-- initial draw
e.redraw()
return e.complete()
end end
return pipenet return pipenet

View File

@@ -11,17 +11,18 @@ 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
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function rectangle(args) local function rectangle(args)
assert(args.border ~= nil or args.thin ~= true, "graphics.elements.rectangle: thin requires border to be provided") element.assert(args.border ~= nil or args.thin ~= true, "thin requires border to be provided")
-- if thin, then width will always need to be 1 -- if thin, then width will always need to be 1
if args.thin == true then if args.thin == true then
@@ -30,76 +31,84 @@ 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.w_set_cur(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
-- check dimensions -- check dimensions
assert(width_x2 <= e.frame.w, "graphics.elements.rectangle: border too thick for width") element.assert(width_x2 <= e.frame.w, "border too thick for width")
assert(width_x2 <= e.frame.h, "graphics.elements.rectangle: border too thick for height") element.assert(width_x2 <= e.frame.h, "border too thick for height")
-- form the basic line strings and top/bottom blit strings -- form the basic line strings and top/bottom blit strings
local spaces = util.spaces(e.frame.w) local spaces = util.spaces(e.frame.w)
local blit_fg = util.strrep(e.fg_bg.blit_fgd, e.frame.w) local blit_fg = string.rep(e.fg_bg.blit_fgd, e.frame.w)
local blit_fg_sides = blit_fg local blit_fg_sides = blit_fg
local blit_bg_sides = "" local blit_bg_sides = ""
local blit_bg_top_bot = util.strrep(border_blit, e.frame.w) local blit_bg_top_bot = string.rep(border_blit, e.frame.w)
-- partial bars -- partial bars
local p_a, p_b, p_s local p_a, p_b, p_s
if args.thin == true then if args.thin == true then
if args.even_inner == true then if args.even_inner == true then
p_a = "\x9c" .. util.strrep("\x8c", inner_width) .. "\x93" p_a = "\x9c" .. string.rep("\x8c", inner_width) .. "\x93"
p_b = "\x8d" .. util.strrep("\x8c", inner_width) .. "\x8e" p_b = "\x8d" .. string.rep("\x8c", inner_width) .. "\x8e"
else else
p_a = "\x97" .. util.strrep("\x83", inner_width) .. "\x94" p_a = "\x97" .. string.rep("\x83", inner_width) .. "\x94"
p_b = "\x8a" .. util.strrep("\x8f", inner_width) .. "\x85" p_b = "\x8a" .. string.rep("\x8f", inner_width) .. "\x85"
end end
p_s = "\x95" .. util.spaces(inner_width) .. "\x95" p_s = "\x95" .. util.spaces(inner_width) .. "\x95"
else else
if args.even_inner == true then if args.even_inner == true then
p_a = util.strrep("\x83", inner_width + width_x2) p_a = string.rep("\x83", inner_width + width_x2)
p_b = util.strrep("\x8f", inner_width + width_x2) p_b = string.rep("\x8f", inner_width + width_x2)
else else
p_a = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width) p_a = util.spaces(border_width) .. string.rep("\x8f", inner_width) .. util.spaces(border_width)
p_b = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width) p_b = util.spaces(border_width) .. string.rep("\x83", inner_width) .. util.spaces(border_width)
end end
p_s = spaces p_s = spaces
end end
local p_inv_fg = util.strrep(border_blit, border_width) .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. local p_inv_fg = string.rep(border_blit, border_width) .. string.rep(e.fg_bg.blit_bkg, inner_width) ..
util.strrep(border_blit, border_width) string.rep(border_blit, border_width)
local p_inv_bg = util.strrep(e.fg_bg.blit_bkg, border_width) .. util.strrep(border_blit, inner_width) .. local p_inv_bg = string.rep(e.fg_bg.blit_bkg, border_width) .. string.rep(border_blit, inner_width) ..
util.strrep(e.fg_bg.blit_bkg, border_width) string.rep(e.fg_bg.blit_bkg, border_width)
if args.thin == true then if args.thin == true then
p_inv_fg = e.fg_bg.blit_bkg .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. util.strrep(border_blit, border_width) p_inv_fg = e.fg_bg.blit_bkg .. string.rep(e.fg_bg.blit_bkg, inner_width) .. string.rep(border_blit, border_width)
p_inv_bg = border_blit .. util.strrep(border_blit, inner_width) .. util.strrep(e.fg_bg.blit_bkg, border_width) p_inv_bg = border_blit .. string.rep(border_blit, inner_width) .. string.rep(e.fg_bg.blit_bkg, border_width)
blit_fg_sides = border_blit .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. e.fg_bg.blit_bkg blit_fg_sides = border_blit .. string.rep(e.fg_bg.blit_bkg, inner_width) .. e.fg_bg.blit_bkg
end end
-- form the body blit strings (sides are border, inside is normal) -- form the body blit strings (sides are border, inside is normal)
@@ -117,67 +126,72 @@ local function rectangle(args)
end end
-- draw rectangle with borders -- draw rectangle with borders
for y = 1, e.frame.h do function e.redraw()
e.window.setCursorPos(1, y) for y = 1, e.frame.h do
-- top border e.w_set_cur(1, y)
if y <= border_height then -- top border
-- partial pixel fill if y <= border_height then
if args.border.even and y == border_height then -- partial pixel fill
if args.thin == true then if args.border.even and y == border_height then
e.window.blit(p_a, p_inv_bg, p_inv_fg) if args.thin == true then
else e.w_blit(p_a, p_inv_bg, p_inv_fg)
local _fg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), p_inv_bg) else
local _bg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg) local _fg = util.trinary(args.even_inner == true, string.rep(e.fg_bg.blit_bkg, e.frame.w), p_inv_bg)
local _bg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
if width_x2 % 3 == 1 then if width_x2 % 3 == 1 then
e.window.blit(p_b, _fg, _bg) e.w_blit(p_b, _fg, _bg)
elseif width_x2 % 3 == 2 then elseif width_x2 % 3 == 2 then
e.window.blit(p_a, _fg, _bg) e.w_blit(p_a, _fg, _bg)
else else
-- skip line -- skip line
e.window.blit(spaces, blit_fg, blit_bg_sides) e.w_blit(spaces, blit_fg, blit_bg_sides)
end end
end
else
e.window.blit(spaces, blit_fg, blit_bg_top_bot)
end
-- bottom border
elseif y > (e.frame.h - border_width) then
-- partial pixel fill
if args.border.even and y == ((e.frame.h - border_width) + 1) then
if args.thin == true then
if args.even_inner == true then
e.window.blit(p_b, blit_bg_top_bot, util.strrep(e.fg_bg.blit_bkg, e.frame.w))
else
e.window.blit(p_b, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
end end
else else
local _fg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg) e.w_blit(spaces, blit_fg, blit_bg_top_bot)
local _bg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot) end
-- bottom border
if width_x2 % 3 == 1 then elseif y > (e.frame.h - border_width) then
e.window.blit(p_a, _fg, _bg) -- partial pixel fill
elseif width_x2 % 3 == 2 then if args.border.even and y == ((e.frame.h - border_width) + 1) then
e.window.blit(p_b, _fg, _bg) if args.thin == true then
if args.even_inner == true then
e.w_blit(p_b, blit_bg_top_bot, string.rep(e.fg_bg.blit_bkg, e.frame.w))
else
e.w_blit(p_b, string.rep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
end
else else
-- skip line local _fg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
e.window.blit(spaces, blit_fg, blit_bg_sides) local _bg = util.trinary(args.even_inner == true, string.rep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
if width_x2 % 3 == 1 then
e.w_blit(p_a, _fg, _bg)
elseif width_x2 % 3 == 2 then
e.w_blit(p_b, _fg, _bg)
else
-- skip line
e.w_blit(spaces, blit_fg, blit_bg_sides)
end
end end
else
e.w_blit(spaces, blit_fg, blit_bg_top_bot)
end end
else else
e.window.blit(spaces, blit_fg, blit_bg_top_bot) if args.thin == true then
end e.w_blit(p_s, blit_fg_sides, blit_bg_sides)
else else
if args.thin == true then e.w_blit(p_s, blit_fg, blit_bg_sides)
e.window.blit(p_s, blit_fg_sides, blit_bg_sides) end
else
e.window.blit(p_s, blit_fg, blit_bg_sides)
end end
end end
end end
-- initial draw of border
e.redraw()
end end
return e.get() return e.complete()
end end
return rectangle return rectangle

View File

@@ -5,37 +5,39 @@ 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.TEXT_ALIGN local ALIGN = core.ALIGN
---@class textbox_args ---@class textbox_args
---@field text string text to show ---@field text string text to show
---@field alignment? TEXT_ALIGN text alignment, left by default ---@field alignment? ALIGN text alignment, left by default
---@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
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function textbox(args) local function textbox(args)
assert(type(args.text) == "string", "graphics.elements.textbox: text is a required field") element.assert(type(args.text) == "string", "text is a required field")
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
local alignment = args.alignment or TEXT_ALIGN.LEFT e.value = args.text
local alignment = args.alignment or ALIGN.LEFT
-- draw textbox -- draw textbox
function e.redraw()
e.window.clear()
local function display_text(text) local lines = util.strwrap(e.value, e.frame.w)
e.value = text
local lines = util.strwrap(text, e.frame.w)
for i = 1, #lines do for i = 1, #lines do
if i > e.frame.h then break end if i > e.frame.h then break end
@@ -43,28 +45,29 @@ local function textbox(args)
local len = string.len(lines[i]) local len = string.len(lines[i])
-- use cursor position to align this line -- use cursor position to align this line
if alignment == TEXT_ALIGN.CENTER then if alignment == ALIGN.CENTER then
e.window.setCursorPos(math.floor((e.frame.w - len) / 2) + 1, i) e.w_set_cur(math.floor((e.frame.w - len) / 2) + 1, i)
elseif alignment == TEXT_ALIGN.RIGHT then elseif alignment == ALIGN.RIGHT then
e.window.setCursorPos((e.frame.w - len) + 1, i) e.w_set_cur((e.frame.w - len) + 1, i)
else else
e.window.setCursorPos(1, i) e.w_set_cur(1, i)
end end
e.window.write(lines[i]) e.w_write(lines[i])
end end
end end
display_text(args.text)
-- set the string value and re-draw the text -- set the string value and re-draw the text
---@param val string value ---@param val string value
function e.set_value(val) function e.set_value(val)
e.window.clear() e.value = val
display_text(val) e.redraw()
end end
return e.get() -- initial draw
e.redraw()
return e.complete()
end end
return textbox return textbox

View File

@@ -11,23 +11,22 @@ 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
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function tiling(args) local function tiling(args)
assert(type(args.fill_c) == "table", "graphics.elements.tiling: fill_c is a required field") element.assert(type(args.fill_c) == "table", "fill_c is a required field")
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- draw tiling box
local fill_a = args.fill_c.blit_a local fill_a = args.fill_c.blit_a
local fill_b = args.fill_c.blit_b local fill_b = args.fill_c.blit_b
@@ -37,13 +36,9 @@ local function tiling(args)
local start_y = 1 local start_y = 1
local inner_width = math.floor(e.frame.w / util.trinary(even, 2, 1)) local inner_width = math.floor(e.frame.w / util.trinary(even, 2, 1))
local inner_height = e.frame.h local inner_height = e.frame.h
local alternator = true
-- border -- border
if args.border_c ~= nil then if args.border_c ~= nil then
e.window.setBackgroundColor(args.border_c)
e.window.clear()
start_x = 1 + util.trinary(even, 2, 1) start_x = 1 + util.trinary(even, 2, 1)
start_y = 2 start_y = 2
@@ -52,36 +47,49 @@ local function tiling(args)
end end
-- check dimensions -- check dimensions
assert(inner_width > 0, "graphics.elements.tiling: inner_width <= 0") element.assert(inner_width > 0, "inner_width <= 0")
assert(inner_height > 0, "graphics.elements.tiling: inner_height <= 0") element.assert(inner_height > 0, "inner_height <= 0")
assert(start_x <= inner_width, "graphics.elements.tiling: start_x > inner_width") element.assert(start_x <= inner_width, "start_x > inner_width")
assert(start_y <= inner_height, "graphics.elements.tiling: start_y > inner_height") element.assert(start_y <= inner_height, "start_y > inner_height")
-- create pattern -- draw tiling box
for y = start_y, inner_height + (start_y - 1) do function e.redraw()
e.window.setCursorPos(start_x, y) local alternator = true
for _ = 1, inner_width do
if alternator then
if even then
e.window.blit(" ", "00", fill_a .. fill_a)
else
e.window.blit(" ", "0", fill_a)
end
else
if even then
e.window.blit(" ", "00", fill_b .. fill_b)
else
e.window.blit(" ", "0", fill_b)
end
end
alternator = not alternator if args.border_c ~= nil then
e.w_set_bkg(args.border_c)
e.window.clear()
end end
if inner_width % 2 == 0 then alternator = not alternator end -- draw pattern
for y = start_y, inner_height + (start_y - 1) do
e.w_set_cur(start_x, y)
for _ = 1, inner_width do
if alternator then
if even then
e.w_blit(" ", "00", fill_a .. fill_a)
else
e.w_blit(" ", "0", fill_a)
end
else
if even then
e.w_blit(" ", "00", fill_b .. fill_b)
else
e.w_blit(" ", "0", fill_b)
end
end
alternator = not alternator
end
if inner_width % 2 == 0 then alternator = not alternator end
end
end end
return e.get() -- initial draw
e.redraw()
return e.complete()
end end
return tiling return tiling

View File

@@ -4,46 +4,77 @@
local util = require("scada-common.util") local util = require("scada-common.util")
local DOUBLE_CLICK_MS = 500
local events = {} local events = {}
---@enum CLICK_BUTTON ---@enum CLICK_BUTTON
events.CLICK_BUTTON = { local CLICK_BUTTON = {
GENERIC = 0, GENERIC = 0,
LEFT_BUTTON = 1, LEFT_BUTTON = 1,
RIGHT_BUTTON = 2, RIGHT_BUTTON = 2,
MID_BUTTON = 3 MID_BUTTON = 3
} }
---@enum CLICK_TYPE events.CLICK_BUTTON = CLICK_BUTTON
events.CLICK_TYPE = {
---@enum MOUSE_CLICK
local MOUSE_CLICK = {
TAP = 1, -- screen tap (complete click) TAP = 1, -- screen tap (complete click)
DOWN = 2, -- button down DOWN = 2, -- button down
UP = 3, -- button up (completed a click) UP = 3, -- button up (completed a click)
DRAG = 4, -- mouse dragged DRAG = 4, -- mouse dragged
SCROLL_DOWN = 5, -- scroll down SCROLL_DOWN = 5, -- scroll down
SCROLL_UP = 6 -- scroll up SCROLL_UP = 6, -- scroll up
DOUBLE_CLICK = 7 -- double left click
} }
events.MOUSE_CLICK = MOUSE_CLICK
---@enum KEY_CLICK
local KEY_CLICK = {
DOWN = 1,
HELD = 2,
UP = 3,
CHAR = 4
}
events.KEY_CLICK = KEY_CLICK
-- create a new 2D coordinate -- create a new 2D coordinate
---@param x integer ---@param x integer
---@param y integer ---@param y integer
---@return coordinate_2d ---@return coordinate_2d
local function _coord2d(x, y) return { x = x, y = y } end local function _coord2d(x, y) return { x = x, y = y } end
events.new_coord_2d = _coord2d
---@class mouse_interaction ---@class mouse_interaction
---@field monitor string ---@field monitor string
---@field button CLICK_BUTTON ---@field button CLICK_BUTTON
---@field type CLICK_TYPE ---@field type MOUSE_CLICK
---@field initial coordinate_2d ---@field initial coordinate_2d
---@field current coordinate_2d ---@field current coordinate_2d
---@class key_interaction
---@field type KEY_CLICK
---@field key number key code
---@field name string key character name
---@field shift boolean shift held
---@field ctrl boolean ctrl held
---@field alt boolean alt held
local handler = { local handler = {
-- left, right, middle button down tracking -- left, right, middle button down tracking
button_down = { button_down = { _coord2d(0, 0), _coord2d(0, 0), _coord2d(0, 0) },
_coord2d(0, 0), -- keyboard modifiers
_coord2d(0, 0), shift = false,
_coord2d(0, 0) alt = false,
} ctrl = false,
-- double click tracking
dc_start = 0,
dc_step = 1,
dc_coord = _coord2d(0, 0)
} }
-- create a new monitor touch mouse interaction event -- create a new monitor touch mouse interaction event
@@ -55,8 +86,8 @@ local handler = {
local function _monitor_touch(monitor, x, y) local function _monitor_touch(monitor, x, y)
return { return {
monitor = monitor, monitor = monitor,
button = events.CLICK_BUTTON.GENERIC, button = CLICK_BUTTON.GENERIC,
type = events.CLICK_TYPE.TAP, type = MOUSE_CLICK.TAP,
initial = _coord2d(x, y), initial = _coord2d(x, y),
current = _coord2d(x, y) current = _coord2d(x, y)
} }
@@ -65,7 +96,7 @@ end
-- create a new mouse button mouse interaction event -- create a new mouse button mouse interaction event
---@nodiscard ---@nodiscard
---@param button CLICK_BUTTON mouse button ---@param button CLICK_BUTTON mouse button
---@param type CLICK_TYPE click type ---@param type MOUSE_CLICK click type
---@param x1 integer initial x ---@param x1 integer initial x
---@param y1 integer initial y ---@param y1 integer initial y
---@param x2 integer current x ---@param x2 integer current x
@@ -83,14 +114,14 @@ end
-- create a new generic mouse interaction event -- create a new generic mouse interaction event
---@nodiscard ---@nodiscard
---@param type CLICK_TYPE ---@param type MOUSE_CLICK
---@param x integer ---@param x integer
---@param y integer ---@param y integer
---@return mouse_interaction ---@return mouse_interaction
function events.mouse_generic(type, x, y) function events.mouse_generic(type, x, y)
return { return {
monitor = "", monitor = "",
button = events.CLICK_BUTTON.GENERIC, button = CLICK_BUTTON.GENERIC,
type = type, type = type,
initial = _coord2d(x, y), initial = _coord2d(x, y),
current = _coord2d(x, y) current = _coord2d(x, y)
@@ -115,8 +146,8 @@ end
-- check if an event qualifies as a click (tap or up) -- check if an event qualifies as a click (tap or up)
---@nodiscard ---@nodiscard
---@param t CLICK_TYPE ---@param t MOUSE_CLICK
function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events.CLICK_TYPE.UP end function events.was_clicked(t) return t == MOUSE_CLICK.TAP or t == MOUSE_CLICK.UP end
-- create a new mouse event to pass onto graphics renderer<br> -- create a new mouse event to pass onto graphics renderer<br>
-- supports: mouse_click, mouse_up, mouse_drag, mouse_scroll, and monitor_touch -- supports: mouse_click, mouse_up, mouse_drag, mouse_scroll, and monitor_touch
@@ -126,35 +157,97 @@ function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events.
---@param y integer y coordinate ---@param y integer y coordinate
---@return mouse_interaction|nil ---@return mouse_interaction|nil
function events.new_mouse_event(event_type, opt, x, y) function events.new_mouse_event(event_type, opt, x, y)
local h = handler
if event_type == "mouse_click" then if event_type == "mouse_click" then
---@cast opt 1|2|3 ---@cast opt 1|2|3
handler.button_down[opt] = _coord2d(x, y)
return _mouse_event(opt, events.CLICK_TYPE.DOWN, x, y, x, y) local init = true
if opt == 1 and (h.dc_step % 2) == 1 then
if h.dc_step ~= 1 and h.dc_coord.x == x and h.dc_coord.y == y and (util.time_ms() - h.dc_start) < DOUBLE_CLICK_MS then
init = false
h.dc_step = h.dc_step + 1
end
end
if init then
h.dc_start = util.time_ms()
h.dc_coord = _coord2d(x, y)
h.dc_step = 2
end
h.button_down[opt] = _coord2d(x, y)
return _mouse_event(opt, MOUSE_CLICK.DOWN, x, y, x, y)
elseif event_type == "mouse_up" then elseif event_type == "mouse_up" then
---@cast opt 1|2|3 ---@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) if opt == 1 and (h.dc_step % 2) == 0 and h.dc_coord.x == x and h.dc_coord.y == y and
(util.time_ms() - h.dc_start) < DOUBLE_CLICK_MS then
if h.dc_step == 4 then
util.push_event("double_click", 1, x, y)
h.dc_step = 1
else h.dc_step = h.dc_step + 1 end
else h.dc_step = 1 end
local initial = h.button_down[opt] ---@type coordinate_2d
return _mouse_event(opt, MOUSE_CLICK.UP, initial.x, initial.y, x, y)
elseif event_type == "monitor_touch" then elseif event_type == "monitor_touch" then
---@cast opt string ---@cast opt string
return _monitor_touch(opt, x, y) return _monitor_touch(opt, x, y)
elseif event_type == "mouse_drag" then elseif event_type == "mouse_drag" then
---@cast opt 1|2|3 ---@cast opt 1|2|3
local initial = handler.button_down[opt] ---@type coordinate_2d local initial = h.button_down[opt] ---@type coordinate_2d
return _mouse_event(opt, events.CLICK_TYPE.DRAG, initial.x, initial.y, x, y) return _mouse_event(opt, MOUSE_CLICK.DRAG, initial.x, initial.y, x, y)
elseif event_type == "mouse_scroll" then elseif event_type == "mouse_scroll" then
---@cast opt 1|-1 ---@cast opt 1|-1
local scroll_direction = util.trinary(opt == 1, events.CLICK_TYPE.SCROLL_DOWN, events.CLICK_TYPE.SCROLL_UP) local scroll_direction = util.trinary(opt == 1, MOUSE_CLICK.SCROLL_DOWN, MOUSE_CLICK.SCROLL_UP)
return _mouse_event(events.CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y) return _mouse_event(CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y)
elseif event_type == "double_click" then
return _mouse_event(CLICK_BUTTON.LEFT_BUTTON, MOUSE_CLICK.DOUBLE_CLICK, x, y, x, y)
end end
end end
-- create a new key event to pass onto graphics renderer<br> -- create a new keyboard interaction event
---@nodiscard
---@param click_type KEY_CLICK key click type
---@param key integer|string keyboard key code or character for 'char' event
---@return key_interaction
local function _key_event(click_type, key)
local name = key
if type(key) == "number" then name = keys.getName(key) end
return { type = click_type, key = key, name = name, shift = handler.shift, ctrl = handler.ctrl, alt = handler.alt }
end
-- create a new keyboard event to pass onto graphics renderer<br>
-- supports: char, key, and key_up -- supports: char, key, and key_up
---@param event_type os_event ---@param event_type os_event OS event to handle
function events.new_key_event(event_type) ---@param key integer keyboard key code
---@param held boolean? if the key is being held (for 'key' event)
---@return key_interaction|nil
function events.new_key_event(event_type, key, held)
if event_type == "char" then if event_type == "char" then
return _key_event(KEY_CLICK.CHAR, key)
elseif event_type == "key" then elseif event_type == "key" then
if key == keys.leftShift or key == keys.rightShift then
handler.shift = true
elseif key == keys.leftCtrl or key == keys.rightCtrl then
handler.ctrl = true
elseif key == keys.leftAlt or key == keys.rightAlt then
handler.alt = true
else
return _key_event(util.trinary(held, KEY_CLICK.HELD, KEY_CLICK.DOWN), key)
end
elseif event_type == "key_up" then elseif event_type == "key_up" then
if key == keys.leftShift or key == keys.rightShift then
handler.shift = false
elseif key == keys.leftCtrl or key == keys.rightCtrl then
handler.ctrl = false
elseif key == keys.leftAlt or key == keys.rightAlt then
handler.alt = false
else
return _key_event(KEY_CLICK.UP, key)
end
end end
end end

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"),
@@ -57,7 +60,7 @@ def make_manifest(size):
}, },
"files" : { "files" : {
# common files # common files
"system" : [ "initenv.lua", "startup.lua" ], "system" : [ "initenv.lua", "startup.lua", "configure.lua", "LICENSE" ],
"common" : list_files("./scada-common"), "common" : list_files("./scada-common"),
"graphics" : list_files("./graphics"), "graphics" : list_files("./graphics"),
"lockbox" : list_files("./lockbox"), "lockbox" : list_files("./lockbox"),
@@ -69,17 +72,17 @@ 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
"manifest" : size, "manifest" : size,
# common files # common files
"system" : os.path.getsize("initenv.lua") + os.path.getsize("startup.lua"), "system" : os.path.getsize("initenv.lua") + os.path.getsize("startup.lua") + os.path.getsize("configure.lua"),
"common" : dir_size("./scada-common"), "common" : dir_size("./scada-common"),
"graphics" : dir_size("./graphics"), "graphics" : dir_size("./graphics"),
"lockbox" : dir_size("./lockbox"), "lockbox" : dir_size("./lockbox"),
@@ -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"

View File

@@ -1,18 +1,10 @@
--
-- Initialize the Post-Boot Module Environment
--
return { return {
-- initialize booted environment -- initialize booted environment
init_env = function () init_env = function ()
local _require = require("cc.require") local _require, _env = require("cc.require"), setmetatable({}, { __index = _ENV })
local _env = setmetatable({}, { __index = _ENV }) -- overwrite require/package globals
require, package = _require.make(_env, "/")
-- overwrite require/package globals -- reset terminal
require, package = _require.make(_env, "/") term.clear(); term.setCursorPos(1, 1)
end
-- reset terminal
term.clear()
term.setCursorPos(1, 1)
end
} }

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;

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

@@ -0,0 +1,199 @@
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,3 @@
require("lockbox").insecure();
local Bit = require("lockbox.util.bit"); local Bit = require("lockbox.util.bit");
local String = require("string"); local String = require("string");
local Math = require("math"); local Math = require("math");

View File

@@ -1,22 +1,6 @@
local Lockbox = {}; local Lockbox = {}
--[[ -- cc-mek-scada lockbox version
package.path = "./?.lua;" Lockbox.version = "1.1"
.. "./cipher/?.lua;"
.. "./digest/?.lua;"
.. "./kdf/?.lua;"
.. "./mac/?.lua;"
.. "./padding/?.lua;"
.. "./test/?.lua;"
.. "./util/?.lua;"
.. package.path;
--]]
Lockbox.ALLOW_INSECURE = true;
Lockbox.insecure = function() return Lockbox
assert(Lockbox.ALLOW_INSECURE,
"This module is insecure! It should not be used in production." ..
"If you really want to use it, set Lockbox.ALLOW_INSECURE to true before importing it");
end
return Lockbox;

View File

@@ -8,7 +8,7 @@ local HMAC = function()
local public = {}; local public = {};
local blockSize = 64; local blockSize = 64;
local Digest = nil; local Digest;
local outerPadding = {}; local outerPadding = {};
local innerPadding = {} local innerPadding = {}
local digest; local digest;

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,4 +1,3 @@
local String = require("string"); local String = require("string");
local Bit = require("lockbox.util.bit"); local Bit = require("lockbox.util.bit");
local Queue = require("lockbox.util.queue"); local Queue = require("lockbox.util.queue");

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"

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 MGMT_TYPE = comms.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,76 +46,58 @@ 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 MGMT_TYPE
---@param msg table ---@param msg table
local function _send_sv(msg_type, msg) local function _send_sv(msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
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
-- send a management packet to the coordinator -- send a management packet to the coordinator
---@param msg_type SCADA_MGMT_TYPE ---@param msg_type MGMT_TYPE
---@param msg table ---@param msg table
local function _send_crd(msg_type, msg) local function _send_crd(msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
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(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
end end
-- attempt coordinator API connection establishment -- attempt coordinator API connection establishment
local function _send_api_establish() local function _send_api_establish()
_send_crd(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT }) _send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
end end
-- keep alive ack to supervisor -- keep alive ack to supervisor
---@param srv_time integer ---@param srv_time integer
local function _send_sv_keep_alive_ack(srv_time) local function _send_sv_keep_alive_ack(srv_time)
_send_sv(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) _send_sv(MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
end end
-- keep alive ack to coordinator -- keep alive ack to coordinator
---@param srv_time integer ---@param srv_time integer
local function _send_api_keep_alive_ack(srv_time) local function _send_api_keep_alive_ack(srv_time)
_send_crd(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) _send_crd(MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
end end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
@@ -122,25 +105,22 @@ 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
_send_sv(SCADA_MGMT_TYPE.CLOSE, {}) self.sv.r_seq_num = nil
self.sv.addr = comms.BROADCAST
_send_sv(MGMT_TYPE.CLOSE, {})
end end
-- close connection to coordinator API server -- close connection to coordinator API server
function public.close_api() function public.close_api()
api_watchdog.cancel() api_watchdog.cancel()
self.api.linked = false self.api.linked = false
_send_crd(SCADA_MGMT_TYPE.CLOSE, {}) self.api.r_seq_num = nil
self.api.addr = comms.BROADCAST
_send_crd(MGMT_TYPE.CLOSE, {})
end end
-- close the connections to the servers -- close the connections to the servers
@@ -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,36 +151,52 @@ 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(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(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(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
---@param reply_to integer ---@param reply_to integer
---@param message any ---@param message any
---@param distance integer ---@param distance integer
---@return mgmt_frame|capi_frame|nil packet ---@return mgmt_frame|crdn_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()
if mgmt_pkt.decode(s_pkt) then if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get() pkt = mgmt_pkt.get()
end end
-- get as coordinator API packet -- get as coordinator packet
elseif s_pkt.protocol() == PROTOCOL.COORD_API then elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then
local capi_pkt = comms.capi_packet() local crdn_pkt = comms.crdn_packet()
if capi_pkt.decode(s_pkt) then if crdn_pkt.decode(s_pkt) then
pkt = capi_pkt.get() pkt = crdn_pkt.get()
end end
else else
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true) log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
@@ -211,21 +207,28 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w
end end
-- handle a packet -- handle a packet
---@param packet mgmt_frame|capi_frame|nil ---@param packet mgmt_frame|crdn_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 == 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 == 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 == 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 == 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 == 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 == 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 == 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 == 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 == 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

@@ -5,18 +5,23 @@
local main_view = require("pocket.ui.main") local main_view = require("pocket.ui.main")
local style = require("pocket.ui.style") local style = require("pocket.ui.style")
local core = require("graphics.core")
local flasher = require("graphics.flasher") local flasher = require("graphics.flasher")
local DisplayBox = require("graphics.elements.displaybox") local DisplayBox = require("graphics.elements.displaybox")
---@class pocket_renderer
local renderer = {} local renderer = {}
local ui = { local ui = {
display = nil display = nil
} }
-- start the pocket GUI -- try to start the pocket GUI
function renderer.start_ui() ---@return boolean success, any error_msg
function renderer.try_start_ui()
local status, msg = true, nil
if ui.display == nil then if ui.display == nil then
-- reset screen -- reset screen
term.setTextColor(colors.white) term.setTextColor(colors.white)
@@ -30,12 +35,22 @@ function renderer.start_ui()
end end
-- init front panel view -- init front panel view
ui.display = DisplayBox{window=term.current(),fg_bg=style.root} status, msg = pcall(function ()
main_view(ui.display) ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
main_view(ui.display)
end)
-- start flasher callback task if status then
flasher.run() -- start flasher callback task
flasher.run()
else
-- report fail and close ui
msg = core.extract_assert_msg(msg)
renderer.close_ui()
end
end end
return status, msg
end end
-- close out the UI -- close out the UI

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.3.1" local POCKET_VERSION = "v0.6.3-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)
@@ -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,24 +94,27 @@ 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
---------------------------------------- ----------------------------------------
local ui_ok, message = pcall(renderer.start_ui) local ui_ok, message = renderer.try_start_ui()
if not ui_ok then if not ui_ok then
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 +129,65 @@ 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" or
event == "double_click" then
-- handle a monitor touch event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
end
-- check for termination request
if event == "terminate" or ppm.should_terminate() then
log.info("terminate requested, closing server connections...")
pocket_comms.close()
log.info("connections closed")
break
end end
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 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,7 +11,7 @@ local TextBox = require("graphics.elements.textbox")
local WaitingAnim = require("graphics.elements.animations.waiting") local WaitingAnim = require("graphics.elements.animations.waiting")
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
@@ -25,14 +25,14 @@ 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)}
TextBox{parent=box,text="Connecting to API",alignment=TEXT_ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)} TextBox{parent=box,text="Connecting to API",alignment=ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)}
else else
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.green,style.root.bkg)} WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.green,style.root.bkg)}
TextBox{parent=box,text="Connecting to Supervisor",alignment=TEXT_ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)} TextBox{parent=box,text="Connecting to Supervisor",alignment=ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)}
end end
return root return root

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