Compare commits

...

165 Commits

Author SHA1 Message Date
Mikayla Fischler
d0d20b1299 #95 added boiler/turbine RTUs to supervisor, tons of RTU/MODBUS related bugfixes, adjusted annunciator conditions 2022-09-18 22:25:59 -04:00
Mikayla Fischler
88c34d8bca fixed acknowledge packets to use error flag, fixed 'static'-like function scope of modbus functions 2022-09-18 22:02:17 -04:00
Mikayla Fischler
3267e7ff13 #96 RTU starts unlinked now on main thread start 2022-09-17 17:04:57 -04:00
Mikayla Fischler
6686d8ea62 changed reactor status message text on main view 2022-09-13 16:08:11 -04:00
Mikayla Fischler
c47e0044b1 addresed monitor disconnect to-do, changed monitor requirement to minimum, fixed up connect/reconnect for #92 2022-09-13 16:07:21 -04:00
Mikayla Fischler
265368f9b2 fixed integrity % and changed to actual burn rate on main screen 2022-09-12 16:01:18 -04:00
Mikayla Fischler
70d9da847e graphics elements comments 2022-09-12 15:58:43 -04:00
Mikayla Fischler
1bf21564f9 #91 recoloring of horizontal and vertical bar indicators 2022-09-12 14:43:01 -04:00
Mikayla Fischler
cd6bb7376d #91 adjusted resizing logic for core map 2022-09-12 14:38:48 -04:00
Mikayla Fischler
e0ab2ade89 #91 support resizing core map per reactor dimension updates 2022-09-12 13:53:39 -04:00
Mikayla Fischler
10c53ac4b3 #91 get and set values for all controls/indicators and textbox 2022-09-12 12:59:28 -04:00
Mikayla Fischler
d9be5ccb47 #89 fixed up ui closing to be cleaner on restart 2022-09-10 22:08:29 -04:00
Mikayla Fischler
c14fc048a1 #88 not going to actually hold UI since that hides the PLC offline state and other offline indicators, instead should expose property update capability 2022-09-10 15:26:52 -04:00
Mikayla Fischler
4275c9d408 unit detail view in div and hide waiting indicator on connect 2022-09-10 15:15:24 -04:00
Mikayla Fischler
98c826e762 start/stop animations with show/hide and pass show/hide down children 2022-09-10 15:14:48 -04:00
Mikayla Fischler
dcf275784c removed debug print 2022-09-10 10:43:48 -04:00
Mikayla Fischler
6f3405949d #88 hold on rendering unit detail view until we get a status, added waiting animation 2022-09-10 10:42:56 -04:00
Mikayla Fischler
33695b2ed6 #74 #86 removed redundant overridden field (use rps_tripped) 2022-09-08 14:49:01 -04:00
Mikayla Fischler
350370a084 notify subscriber right away if there is already a value present 2022-09-08 12:19:19 -04:00
Mikayla Fischler
17954ef3d0 #86 supervisor fixes and changes for annunciator/units; send annunciator, fixed heartbeat, change to max return flow detection 2022-09-08 10:25:00 -04:00
Mikayla Fischler
c5ba95449f bugfix to trilight, change to test code in unit view 2022-09-08 10:22:11 -04:00
Mikayla Fischler
3621f53c45 #78 linked up the rest of the fields that we currently have, holding off on a few that are still WIP features 2022-09-07 11:10:20 -04:00
Mikayla Fischler
e084ae1eea removed redundant c_off from trilight 2022-09-07 10:42:12 -04:00
Mikayla Fischler
49605e5966 added tri-state indicator light 2022-09-07 10:39:51 -04:00
Mikayla Fischler
0f6b3fdd98 fixed incorrect comment 2022-09-07 10:25:48 -04:00
Mikayla Fischler
c2ac7fc973 #78 removed redundant device index from boiler/turbine ps keys 2022-09-07 10:25:22 -04:00
Mikayla Fischler
b53d2d6694 code cleanup and work on #78 linking for annunciator 2022-09-06 22:38:27 -04:00
Mikayla Fischler
117784500a #78 functional reactor stats on main view 2022-09-05 19:40:20 -04:00
Mikayla Fischler
397e311f1b #85 handling supervisor disconnected, bugfix with renderer 2022-09-05 16:24:57 -04:00
Mikayla Fischler
e456d34468 svsessions bugfixes 2022-09-05 16:23:03 -04:00
Mikayla Fischler
4359cc3e63 formatting 2022-09-05 16:21:59 -04:00
Mikayla Fischler
473763fd27 #78 removed use of data in graphics layouts since we don't have data at construct time 2022-09-05 16:04:32 -04:00
Mikayla Fischler
621adbbcbc #86 type bug fix 2022-09-05 11:49:23 -04:00
Mikayla Fischler
564b89d19c #78 linked up unit overview using psil 2022-09-03 13:10:51 -04:00
Mikayla Fischler
17fce01ff5 added rps_trip_cause type 2022-09-03 13:10:09 -04:00
Mikayla Fischler
f36b0c7e37 #85 version for reconnecting 2022-09-03 11:54:34 -04:00
Mikayla Fischler
5a8bba5108 #85 handle loss of supervisor conn or comms modem 2022-09-03 11:51:27 -04:00
Mikayla Fischler
c3f7407689 #86 work on supervisor/coordinator comms 2022-09-03 10:50:14 -04:00
Mikayla Fischler
d38e5ca5ec #86 send builds and statuses periodically 2022-08-28 12:57:36 -04:00
Mikayla Fischler
eadf5c488a #86 improvements to supervisor units, code cleanup 2022-08-28 12:12:30 -04:00
Mikayla Fischler
c985e90ec3 #73 test unit view completed, additional features held for after data integration is set 2022-08-28 11:52:43 -04:00
Mikayla Fischler
c80d861b28 #73 unit view reorganization 2022-08-16 13:56:42 -04:00
Mikayla Fischler
395c1ff9ce #73 add indicators for radiation monitor and boilers/turbines 2022-08-16 13:04:02 -04:00
Mikayla Fischler
8dac59fba4 #73 waste selection 2022-08-16 11:22:58 -04:00
Mikayla Fischler
7f011369c4 util pad function 2022-08-16 11:22:06 -04:00
Mikayla Fischler
3c2f631451 #73 additional indicators next to core map 2022-08-09 00:40:50 -04:00
Mikayla Fischler
02c3c5c53c fixed bug with textbox alignment 2022-08-09 00:40:02 -04:00
Mikayla Fischler
252c48a02c #73 core map changes 2022-08-02 11:46:21 -03:00
Mikayla Fischler
6b23a32744 renamed core_view to core_map 2022-08-01 13:11:20 -03:00
Mikayla Fischler
826114e5bf #73 core map and bugfixes 2022-08-01 13:05:39 -03:00
Mikayla Fischler
17dd35e6de bugfixes to tiling element 2022-08-01 10:30:53 -03:00
Mikayla Fischler
42c2b1bda1 coordinator use tcallbackdsp, #73 burn rate set button click effect, test blinks of lights 2022-07-28 12:10:52 -04:00
Mikayla Fischler
f5c703a8b3 fixed push button touch redraw 2022-07-28 11:42:22 -04:00
Mikayla Fischler
2918608326 #73 updated unit layout for graphics library changes 2022-07-28 11:17:58 -04:00
Mikayla Fischler
14b24678f9 #84 auto-incrementing x with line break function, removed need for get_offset by having parent prepare child template 2022-07-28 11:17:34 -04:00
Mikayla Fischler
f4f36b020b #84 recursive get element by id 2022-07-28 10:15:12 -04:00
Mikayla Fischler
f1a50990f2 #84 improved element creation process for adding children 2022-07-28 10:09:34 -04:00
Mikayla Fischler
01a364b5cf fixed bug with spinbox 2022-07-23 20:08:52 -04:00
Mikayla Fischler
fc14141321 #73 unit overview parent/child setup, fixed touch events by setting up children for elements 2022-07-23 20:08:37 -04:00
Mikayla Fischler
9b21a971fe #74 close supervisor connection on exit, start of touch event handling 2022-07-20 13:28:58 -04:00
Mikayla Fischler
1afafba501 wrap os.pullEventRaw to have return types 2022-07-19 15:18:11 -04:00
Mikayla Fischler
d6a201a45f #73 initial unit view 2022-07-19 14:03:02 -04:00
Mikayla Fischler
41cc6b9acc support for craftos-pc env by supporting modems instead of wireless modems for comms 2022-07-19 14:02:20 -04:00
Mikayla Fischler
2aedc015c8 correctly find mek 10.1+ fission reactors 2022-07-17 15:05:27 -04:00
Mikayla Fischler
c3d6d900a1 bugfixes to graphics elements 2022-07-16 13:25:07 -04:00
Mikayla Fischler
525dedb830 added missing RPS fields to supervisor session 2022-07-16 12:54:02 -04:00
Mikayla Fischler
88bf4d5653 #80 mek 10.1+ support for reactor plc 2022-07-15 09:58:04 -04:00
Mikayla Fischler
6643c7e6ed removed debug fg_bg set 2022-07-14 14:29:48 -04:00
Mikayla Fischler
bd1ab11686 #79 water cooling only support, dynamic height, changed 2 turbine 1 boiler layout 2022-07-14 13:47:39 -04:00
Mikayla Fischler
8704d845bd fixed bug with cpair blit_a/blit_b colors 2022-07-14 13:45:40 -04:00
Mikayla Fischler
6f61203db3 #72, #78 updated main view to adapt to facility configuration, initial use of pub/sub for main view 2022-07-10 16:19:04 -04:00
Mikayla Fischler
5a96818c97 #72 ui formatting 2022-07-09 13:43:38 -04:00
Mikayla Fischler
b25ebdf959 fixed supervisor keep alive periodics timing 2022-07-07 13:18:10 -04:00
Mikayla Fischler
4b60c038f4 removed debug prints 2022-07-07 00:37:58 -04:00
Mikayla Fischler
ea17ba41fe #74 supervisor-coordinator comms establish 2022-07-07 00:34:42 -04:00
Mikayla Fischler
39672fedb4 code cleanup 2022-07-05 23:49:48 -04:00
Mikayla Fischler
1444008479 #74 comms establish on boot 2022-07-05 23:48:01 -04:00
Mikayla Fischler
409e8083a7 dmesg working status animation 2022-07-05 23:47:13 -04:00
Mikayla Fischler
335e0f5ee9 gitignore for notes directory 2022-07-05 12:49:46 -04:00
Mikayla Fischler
9bd220cbb2 removed unused requires 2022-07-05 12:48:21 -04:00
Mikayla Fischler
33159bc677 main loop and work on #74 comms 2022-07-05 12:47:02 -04:00
Mikayla Fischler
bd33240515 #62 modifing color palette 2022-07-05 12:46:31 -04:00
Mikayla Fischler
f6708ca988 coordinator dmesg wrapper functions 2022-07-05 11:18:26 -04:00
Mikayla Fischler
ed0982a832 handle nil tag color 2022-07-05 11:18:07 -04:00
Mikayla Fischler
7ad115bc03 #72 unit overview layout completed 2022-07-02 17:24:52 -04:00
Mikayla Fischler
3048fbed8b moved pipenet to be basic element not an indicator 2022-07-02 15:09:35 -04:00
Mikayla Fischler
35c408883a fixes to pipes/pipenet 2022-07-02 15:08:24 -04:00
Mikayla Fischler
20a1fab611 #72 added pipes to main overview, changed text of reactor overview 2022-06-29 17:40:46 -04:00
Mikayla Fischler
ef73c52417 pipenet indicator instead of pipe indicator 2022-06-29 17:40:08 -04:00
Mikayla Fischler
01caf3d914 pipe indicator graphics element 2022-06-26 16:36:21 -04:00
Mikayla Fischler
f32cdf5563 ticked version and fixed wording 2022-06-25 18:39:29 -04:00
Mikayla Fischler
1188d2f7df #72 work on main layout, reactor and boiler views exist now 2022-06-25 16:21:57 -04:00
Mikayla Fischler
e137953f93 fixed vbar bugs 2022-06-25 16:20:58 -04:00
Mikayla Fischler
316b255a04 fixed hbar percentage position 2022-06-25 14:51:59 -04:00
Mikayla Fischler
6397f29d4f fixed offsets/inner width for real this time 2022-06-25 14:51:38 -04:00
Mikayla Fischler
47599b8ff6 fixes to offsets and width calculations, init hbar to 0 2022-06-25 14:27:15 -04:00
Mikayla Fischler
e54d5b3d85 #74 coordinator comms and work on database 2022-06-25 13:39:47 -04:00
Mikayla Fischler
cf6f0e3153 publisher-subscriber interconnect layer 2022-06-25 13:38:31 -04:00
Mikayla Fischler
d3f28a6882 #75 handle edge case on rectangle border width, renamed inner_* to offset_* 2022-06-19 11:35:17 -04:00
Mikayla Fischler
15595ca81b #75 offset children of rectangles with borders 2022-06-19 11:20:09 -04:00
Mikayla Fischler
5a3897572d fixed bug with single word strings in strwrap 2022-06-18 02:15:03 -04:00
Mikayla Fischler
e4b7f807fe commas in data indicators 2022-06-18 02:14:48 -04:00
Mikayla Fischler
9bd2229e27 improvements to rectangle graphics element even rendering 2022-06-18 01:33:45 -04:00
Mikayla Fischler
27038f64f7 SCRAM button graphics element 2022-06-16 12:17:41 -04:00
Mikayla Fischler
6980e73658 default to not even border 2022-06-16 11:31:52 -04:00
Mikayla Fischler
ea9e9288f7 bugfix to hbar 2022-06-16 11:29:47 -04:00
Mikayla Fischler
7f007e032d #62, #72 work on main layout, not using layout class, refactoring and bugfixes 2022-06-16 11:24:35 -04:00
Mikayla Fischler
971657c3d2 graphics library refactoring and bugfixes 2022-06-16 11:19:32 -04:00
Mikayla Fischler
b628472d81 #74 work on coordinator comms 2022-06-15 15:35:34 -04:00
Mikayla Fischler
2e4a533148 comments 2022-06-14 12:05:49 -04:00
Mikayla Fischler
13513a9ce6 #62 graphics layouts 2022-06-14 12:02:42 -04:00
Mikayla Fischler
3593493c98 #62 basic start of the UI 2022-06-11 17:58:29 -04:00
Mikayla Fischler
7dbc5594b0 #63 div graphics element 2022-06-11 17:09:14 -04:00
Mikayla Fischler
89437b2be9 #63 cleanup and assertions 2022-06-11 17:06:32 -04:00
Mikayla Fischler
4488a0594f #63 numeric spinbox element 2022-06-11 16:44:31 -04:00
Mikayla Fischler
3004902ce5 #63 bugfixes 2022-06-11 16:38:15 -04:00
Mikayla Fischler
0950fc045d #63 new indicators and fixed up old ones 2022-06-11 12:21:14 -04:00
Mikayla Fischler
dc867095fd util spaces function 2022-06-11 12:20:49 -04:00
Mikayla Fischler
1fa87132d6 #63 allow hbar to have variable height, other bar improvement 2022-06-09 11:59:55 -04:00
Mikayla Fischler
11e4d89b1d #63 vertical fill bar indicator 2022-06-09 10:18:37 -04:00
Mikayla Fischler
307883e6e7 #63 use util string wrap and support text alignment 2022-06-08 18:53:24 -04:00
Mikayla Fischler
1dad4bcf77 util string wrap function 2022-06-08 18:48:20 -04:00
Mikayla Fischler
bc844d21bd #63 use util.strrep where appropriate 2022-06-08 17:22:20 -04:00
Mikayla Fischler
d8bbe4b459 #63 added indicator icon/light, added util.strrep string repeater 2022-06-08 17:16:53 -04:00
Mikayla Fischler
6f645579f8 #63 removed gframe as an argument to buttons 2022-06-08 16:52:41 -04:00
Mikayla Fischler
ac607f9dc6 #63 latching button in addition to pushbutton 2022-06-08 16:21:49 -04:00
Mikayla Fischler
15bc816d7e #63 button control element 2022-06-08 14:48:17 -04:00
Mikayla Fischler
254e85f3ed timer callback dispatcher 2022-06-08 14:47:45 -04:00
Mikayla Fischler
9d107da8d9 #63 horizontal fill bar indicator 2022-06-08 14:16:05 -04:00
Mikayla Fischler
b99f57e480 #62 redrawing 2022-06-08 14:15:34 -04:00
Mikayla Fischler
2ac9bab92e #63 basketweave tiling pattern element 2022-06-08 13:18:14 -04:00
Mikayla Fischler
29c4c39d23 #62 uneven border support because rectangular pixels 2022-06-08 13:08:48 -04:00
Mikayla Fischler
8002698dd0 #63 rectangle construct asserts 2022-06-08 12:29:53 -04:00
Mikayla Fischler
ce227a175a #63 rectangle element 2022-06-08 12:27:28 -04:00
Mikayla Fischler
8ea75b9501 #62, #63 graphics primatives and added display boxes to renderer 2022-06-06 15:42:39 -04:00
Mikayla Fischler
285026c1fa docs cleanup 2022-06-06 15:40:08 -04:00
Mikayla Fischler
8b307ea030 alias for color type and added read() to globals 2022-06-05 23:24:18 -04:00
Mikayla Fischler
b75d482f4a use is_int in validator 2022-06-05 16:54:34 -04:00
Mikayla Fischler
ebcc911b81 #70 validate RTU advertisements on the supervisor 2022-06-05 16:53:36 -04:00
Mikayla Fischler
0bc0decbf2 util.is_int 2022-06-05 16:51:38 -04:00
Mikayla Fischler
1c819779c7 #69 config file validation 2022-06-05 15:09:02 -04:00
Mikayla Fischler
d6c8eb4d56 #68 check RTU unit configs while parsing 2022-06-05 14:49:50 -04:00
Mikayla Fischler
81345f5325 #71 validate frame data types 2022-06-05 13:22:36 -04:00
Mikayla Fischler
f0c97e8b70 #65 safe concat where appropriate 2022-06-05 11:16:25 -04:00
Mikayla Fischler
5068e47590 #67 turbine valve RTU supervisor session, bugfixes with redstone RTU session 2022-06-05 09:30:56 -04:00
Mikayla Fischler
c764506999 #67 boilerv RTU supervisor session, supervisor session cleanup 2022-06-04 17:59:24 -04:00
Mikayla Fischler
6d97d45227 #67 imatrix RTU supervisor session 2022-06-04 17:45:52 -04:00
Mikayla Fischler
e443beec19 #66 SNA RTU supervisor session 2022-06-04 16:25:23 -04:00
Mikayla Fischler
0f7e77b0cb #28 fixed addresses for RTU session 2022-06-04 15:36:47 -04:00
Mikayla Fischler
27a86cc893 #28 SPS RTU supervisor session 2022-06-04 15:33:04 -04:00
Mikayla Fischler
07574aa116 alignment and fixed has_build bugs 2022-06-04 15:00:50 -04:00
Mikayla Fischler
dcb517d1cb trailing case of not using TXN_TAGS 2022-06-04 11:23:06 -04:00
Mikayla Fischler
1242c5a81c use TXN_TAGS for consistency 2022-06-04 11:17:54 -04:00
Mikayla Fischler
5cba8ff9f1 #59 environment detector RTU 2022-06-04 11:11:35 -04:00
Mikayla Fischler
fc7b83a18a #28 #66 #59 new RTUs 2022-06-04 10:49:36 -04:00
Mikayla Fischler
3bb95eb441 #64 util code cleanup 2022-05-31 16:09:06 -04:00
Mikayla Fischler
341df1a739 simplification of initenv file 2022-05-31 16:05:05 -04:00
Mikayla Fischler
ccc5220ca8 util round and trinary 2022-05-31 15:55:40 -04:00
Mikayla Fischler
e52b76aa24 supervisor unit sessions now actually call txnctrl.cleanup 2022-05-31 15:40:17 -04:00
Mikayla Fischler
43d5c0f8ad #64 supervisor code cleanup 2022-05-31 15:36:17 -04:00
Mikayla Fischler
4ec07ca053 #64 rtu code cleanup and device bugfixes 2022-05-31 14:54:55 -04:00
Mikayla Fischler
1705d8993e #64 plc code cleanup 2022-05-31 14:14:17 -04:00
Mikayla Fischler
309ba06f8a #51 crypto system 2022-05-29 15:05:57 -04:00
Mikayla Fischler
e65a1bf6e1 #61 monitor configuration and init, render engine started, dmesg changes, ppm monitor listing changes 2022-05-29 14:34:09 -04:00
Mikayla Fischler
ff5b163c1d ppm patch to support multiple return value functions, changed lack of modem to emit fatal error 2022-05-29 14:26:40 -04:00
115 changed files with 11847 additions and 1118 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
_notes/

View File

@@ -8,6 +8,10 @@
"parallel",
"colors",
"textutils",
"shell"
"shell",
"settings",
"window",
"read",
"periphemu"
]
}

View File

@@ -0,0 +1,16 @@
local apisessions = {}
---@param packet capi_frame
function apisessions.handle_packet(packet)
end
function apisessions.check_all_watchdogs()
end
function apisessions.close_all()
end
function apisessions.free_all_closed()
end
return apisessions

24
coordinator/config.lua Normal file
View File

@@ -0,0 +1,24 @@
local config = {}
-- port of the SCADA supervisor
config.SCADA_SV_PORT = 16100
-- port to listen to incoming packets from supervisor
config.SCADA_SV_LISTEN = 16101
-- listen port for SCADA coordinator API access
config.SCADA_API_LISTEN = 16200
-- expected number of reactor units, used only to require that number of unit monitors
config.NUM_UNITS = 4
-- graphics color
config.RECOLOR = true
-- log path
config.LOG_PATH = "/log.txt"
-- log mode
-- 0 = APPEND (adds to existing file on start)
-- 1 = NEW (replaces existing file on start)
config.LOG_MODE = 0
-- crypto config
config.SECURE = true
-- must be common between all devices
config.PASSWORD = "testpassword!"
return config

View File

@@ -1,12 +1,459 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local apisessions = require("coordinator.apisessions")
local iocontrol = require("coordinator.iocontrol")
local dialog = require("coordinator.ui.dialog")
local coordinator = {}
-- coordinator communications
coordinator.coord_comms = function ()
local self = {
reactor_struct_cache = nil
local print = util.print
local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
local PROTOCOLS = comms.PROTOCOLS
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES
-- request the user to select a monitor
---@param names table available monitors
local function ask_monitor(names)
println("available monitors:")
for i = 1, #names do
print(" " .. names[i])
end
println("")
println("select a monitor or type c to cancel")
local iface = dialog.ask_options(names, "c")
if iface ~= false and iface ~= nil then
util.filter_table(names, function (x) return x ~= iface end)
end
return iface
end
-- configure monitor layout
---@param num_units integer number of units expected
function coordinator.configure_monitors(num_units)
---@class monitors_struct
local monitors = {
primary = nil,
primary_name = "",
unit_displays = {},
unit_name_map = {}
}
local monitors_avail = ppm.get_monitor_list()
local names = {}
-- get all interface names
for iface, _ in pairs(monitors_avail) do
table.insert(names, iface)
end
-- we need a certain number of monitors (1 per unit + 1 primary display)
if #names < num_units + 1 then
println("not enough monitors connected (need " .. num_units + 1 .. ")")
log.warning("insufficient monitors present (need " .. num_units + 1 .. ")")
return false
end
-- attempt to load settings
settings.load("/coord.settings")
---------------------
-- PRIMARY DISPLAY --
---------------------
local iface_primary_display = settings.get("PRIMARY_DISPLAY")
if not util.table_contains(names, iface_primary_display) then
println("primary display is not connected")
local response = dialog.ask_y_n("would you like to change it", true)
if response == false then return false end
iface_primary_display = nil
end
while iface_primary_display == nil and #names > 0 do
-- lets get a monitor
iface_primary_display = ask_monitor(names)
end
if iface_primary_display == false then return false end
settings.set("PRIMARY_DISPLAY", iface_primary_display)
util.filter_table(names, function (x) return x ~= iface_primary_display end)
monitors.primary = ppm.get_periph(iface_primary_display)
monitors.primary_name = iface_primary_display
-------------------
-- UNIT DISPLAYS --
-------------------
local unit_displays = settings.get("UNIT_DISPLAYS")
if unit_displays == nil then
unit_displays = {}
for i = 1, num_units do
local display = nil
while display == nil and #names > 0 do
-- lets get a monitor
println("please select monitor for unit " .. i)
display = ask_monitor(names)
end
if display == false then return false end
unit_displays[i] = display
end
else
-- make sure all displays are connected
for i = 1, num_units do
---@diagnostic disable-next-line: need-check-nil
local display = unit_displays[i]
if not util.table_contains(names, display) then
local response = dialog.ask_y_n("unit display " .. i .. " is not connected, would you like to change it?", true)
if response == false then return false end
display = nil
end
while display == nil and #names > 0 do
-- lets get a monitor
display = ask_monitor(names)
end
if display == false then return false end
unit_displays[i] = display
end
end
settings.set("UNIT_DISPLAYS", unit_displays)
settings.save("/coord.settings")
for i = 1, #unit_displays do
monitors.unit_displays[i] = ppm.get_periph(unit_displays[i])
monitors.unit_name_map[i] = unit_displays[i]
end
return true, monitors
end
-- dmesg print wrapper
---@param message string message
---@param dmesg_tag string tag
---@param working? boolean to use dmesg_working
---@return function? update, function? done
local function log_dmesg(message, dmesg_tag, working)
local colors = {
GRAPHICS = colors.green,
SYSTEM = colors.cyan,
BOOT = colors.blue,
COMMS = colors.purple
}
if working then
return log.dmesg_working(message, dmesg_tag, colors[dmesg_tag])
else
log.dmesg(message, dmesg_tag, colors[dmesg_tag])
end
end
function coordinator.log_graphics(message) log_dmesg(message, "GRAPHICS") end
function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end
function coordinator.log_boot(message) log_dmesg(message, "BOOT") end
function coordinator.log_comms(message) log_dmesg(message, "COMMS") end
---@param message string
---@return function update, function done
function coordinator.log_comms_connecting(message) return log_dmesg(message, "COMMS", true) end
-- coordinator communications
---@param version string
---@param modem table
---@param sv_port integer
---@param sv_listen integer
---@param api_listen integer
---@param sv_watchdog watchdog
function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_watchdog)
local self = {
sv_linked = false,
sv_seq_num = 0,
sv_r_seq_num = nil,
modem = modem,
connected = false
}
---@class coord_comms
local public = {}
-- PRIVATE FUNCTIONS --
-- open all channels
local function _open_channels()
if not self.modem.isOpen(sv_listen) then
self.modem.open(sv_listen)
end
if not self.modem.isOpen(api_listen) then
self.modem.open(api_listen)
end
end
-- open at construct time
_open_channels()
-- send a packet to the supervisor
---@param msg_type SCADA_MGMT_TYPES|SCADA_CRDN_TYPES
---@param msg table
local function _send_sv(protocol, msg_type, msg)
local s_pkt = comms.scada_packet()
local pkt = nil ---@type mgmt_packet|crdn_packet
if protocol == PROTOCOLS.SCADA_MGMT then
pkt = comms.mgmt_packet()
elseif protocol == PROTOCOLS.SCADA_CRDN then
pkt = comms.crdn_packet()
else
return
end
pkt.make(msg_type, msg)
s_pkt.make(self.sv_seq_num, protocol, pkt.raw_sendable())
self.modem.transmit(sv_port, sv_listen, s_pkt.raw_sendable())
self.sv_seq_num = self.sv_seq_num + 1
end
-- attempt connection establishment
local function _send_establish()
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.ESTABLISH, { version })
end
-- keep alive ack
---@param srv_time integer
local function _send_keep_alive_ack(srv_time)
_send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() })
end
-- PUBLIC FUNCTIONS --
-- reconnect a newly connected modem
---@param modem table
---@diagnostic disable-next-line: redefined-local
function public.reconnect_modem(modem)
self.modem = modem
_open_channels()
end
-- close the connection to the server
function public.close()
sv_watchdog.cancel()
self.sv_linked = false
_send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.CLOSE, {})
end
-- attempt to connect to the subervisor
---@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 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_CRDN_TYPES.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")
end
return self.sv_linked
end
-- parse a packet
---@param side string
---@param sender integer
---@param reply_to integer
---@param message any
---@param distance integer
---@return mgmt_frame|crdn_frame|capi_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance)
local pkt = nil
local s_pkt = comms.scada_packet()
-- parse packet as generic SCADA packet
s_pkt.receive(side, sender, reply_to, message, distance)
if s_pkt.is_valid() then
-- get as SCADA management packet
if s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get()
end
-- get as coordinator packet
elseif s_pkt.protocol() == PROTOCOLS.SCADA_CRDN then
local crdn_pkt = comms.crdn_packet()
if crdn_pkt.decode(s_pkt) then
pkt = crdn_pkt.get()
end
-- get as coordinator API packet
elseif s_pkt.protocol() == PROTOCOLS.COORD_API then
local capi_pkt = comms.capi_packet()
if capi_pkt.decode(s_pkt) then
pkt = capi_pkt.get()
end
else
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
end
end
return pkt
end
-- handle a packet
---@param packet mgmt_frame|crdn_frame|capi_frame
function public.handle_packet(packet)
if packet ~= nil then
local protocol = packet.scada_frame.protocol()
if protocol == PROTOCOLS.COORD_API then
apisessions.handle_packet(packet)
else
-- check sequence number
if self.sv_r_seq_num == nil then
self.sv_r_seq_num = packet.scada_frame.seq_num()
elseif self.connected and self.sv_r_seq_num >= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
else
self.sv_r_seq_num = packet.scada_frame.seq_num()
end
-- feed watchdog on valid sequence number
sv_watchdog.feed()
-- handle packet
if protocol == PROTOCOLS.SCADA_CRDN then
if packet.type == SCADA_CRDN_TYPES.ESTABLISH then
-- connection with supervisor established
if packet.length > 1 then
-- get configuration
---@class facility_conf
local conf = {
num_units = packet.data[1],
defs = {} -- boilers and turbines
}
if (packet.length - 1) == (conf.num_units * 2) then
-- record sequence of pairs of [#boilers, #turbines] per unit
for i = 2, packet.length do
table.insert(conf.defs, packet.data[i])
end
-- init io controller
iocontrol.init(conf)
self.sv_linked = true
else
log.debug("supervisor conn establish packet length mismatch")
end
else
log.debug("supervisor conn establish packet length mismatch")
end
elseif packet.type == SCADA_CRDN_TYPES.STRUCT_BUILDS then
-- record builds
if iocontrol.record_builds(packet.data) then
-- acknowledge receipt of builds
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.STRUCT_BUILDS, {})
else
log.error("received invalid build packet")
end
elseif packet.type == SCADA_CRDN_TYPES.UNIT_STATUSES then
-- update statuses
if not iocontrol.update_statuses(packet.data) then
log.error("received invalid unit statuses packet")
end
elseif packet.type == SCADA_CRDN_TYPES.COMMAND_UNIT then
elseif packet.type == SCADA_CRDN_TYPES.ALARM then
else
log.warning("received unknown SCADA_CRDN packet type " .. packet.type)
end
elseif protocol == PROTOCOLS.SCADA_MGMT then
if packet.type == SCADA_MGMT_TYPES.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 > 500 then
log.warning("coord KEEP_ALIVE trip time > 500ms (" .. trip_time .. "ms)")
end
-- log.debug("coord RTT = " .. trip_time .. "ms")
_send_keep_alive_ack(timestamp)
else
log.debug("SCADA keep alive packet length mismatch")
end
elseif packet.type == SCADA_MGMT_TYPES.CLOSE then
-- handle session close
sv_watchdog.cancel()
self.sv_linked = false
println_ts("server connection closed by remote host")
log.warning("server connection closed by remote host")
else
log.warning("received unknown SCADA_MGMT packet type " .. packet.type)
end
else
-- should be unreachable assuming packet is from parse_packet()
log.error("illegal packet type " .. protocol, true)
end
end
end
end
-- check if the coordinator is still linked to the supervisor
function public.is_linked() return self.sv_linked end
return public
end
return coordinator

285
coordinator/iocontrol.lua Normal file
View File

@@ -0,0 +1,285 @@
local psil = require("scada-common.psil")
local log = require("scada-common.log")
local iocontrol = {}
---@class ioctl
local io = {}
-- initialize the coordinator IO controller
---@param conf facility_conf configuration
function iocontrol.init(conf)
io.facility = {
scram = false,
num_units = conf.num_units,
ps = psil.create()
}
io.units = {}
for i = 1, conf.num_units do
---@class ioctl_entry
local entry = {
unit_id = i, ---@type integer
initialized = false,
num_boilers = 0,
num_turbines = 0,
control_state = false,
burn_rate_cmd = 0.0,
waste_control = 0,
---@fixme debug stubs to be linked into comms later?
start = function () print("UNIT " .. i .. ": start") end,
scram = function () print("UNIT " .. i .. ": SCRAM") end,
set_burn = function (rate) print("UNIT " .. i .. ": set burn rate to " .. rate) end,
reactor_ps = psil.create(),
reactor_data = {}, ---@type reactor_db
boiler_ps_tbl = {},
boiler_data_tbl = {},
turbine_ps_tbl = {},
turbine_data_tbl = {}
}
for _ = 1, conf.defs[(i * 2) - 1] do
local data = {} ---@type boiler_session_db|boilerv_session_db
table.insert(entry.boiler_ps_tbl, psil.create())
table.insert(entry.boiler_data_tbl, data)
end
for _ = 1, conf.defs[i * 2] do
local data = {} ---@type turbine_session_db|turbinev_session_db
table.insert(entry.turbine_ps_tbl, psil.create())
table.insert(entry.turbine_data_tbl, data)
end
entry.num_boilers = #entry.boiler_data_tbl
entry.num_turbines = #entry.turbine_data_tbl
table.insert(io.units, entry)
end
end
-- populate structure builds
---@param builds table
---@return boolean valid
function iocontrol.record_builds(builds)
if #builds ~= #io.units then
log.error("number of provided unit builds does not match expected number of units")
return false
else
for i = 1, #builds do
local unit = io.units[i] ---@type ioctl_entry
local build = builds[i]
-- reactor build
unit.reactor_data.mek_struct = build.reactor
for key, val in pairs(unit.reactor_data.mek_struct) do
unit.reactor_ps.publish(key, val)
end
-- boiler builds
for id, boiler in pairs(build.boilers) do
unit.boiler_data_tbl[id] = {
formed = boiler[2], ---@type boolean|nil
build = boiler[1] ---@type table
}
unit.boiler_ps_tbl[id].publish("formed", boiler[2])
for key, val in pairs(unit.boiler_data_tbl[id].build) do
unit.boiler_ps_tbl[id].publish(key, val)
end
end
-- turbine builds
for id, turbine in pairs(build.turbines) do
unit.turbine_data_tbl[id] = {
formed = turbine[2], ---@type boolean|nil
build = turbine[1] ---@type table
}
unit.turbine_ps_tbl[id].publish("formed", turbine[2])
for key, val in pairs(unit.turbine_data_tbl[id].build) do
unit.turbine_ps_tbl[id].publish(key, val)
end
end
end
end
return true
end
-- update unit statuses
---@param statuses table
---@return boolean valid
function iocontrol.update_statuses(statuses)
if #statuses ~= #io.units then
log.error("number of provided unit statuses does not match expected number of units")
return false
else
for i = 1, #statuses do
local unit = io.units[i] ---@type ioctl_entry
local status = statuses[i]
-- reactor PLC status
local reactor_status = status[1]
if #reactor_status == 0 then
unit.reactor_ps.publish("computed_status", 1) -- disconnected
else
local mek_status = reactor_status[1]
local rps_status = reactor_status[2]
local gen_status = reactor_status[3]
unit.reactor_data.last_status_update = gen_status[1]
unit.reactor_data.control_state = gen_status[2]
unit.reactor_data.rps_tripped = gen_status[3]
unit.reactor_data.rps_trip_cause = gen_status[4]
unit.reactor_data.degraded = gen_status[5]
unit.reactor_data.rps_status = rps_status ---@type rps_status
unit.reactor_data.mek_status = mek_status ---@type mek_status
if unit.reactor_data.mek_status.status then
unit.reactor_ps.publish("computed_status", 3) -- running
else
if unit.reactor_data.degraded then
unit.reactor_ps.publish("computed_status", 5) -- faulted
elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then
unit.reactor_ps.publish("computed_status", 4) -- SCRAM
else
unit.reactor_ps.publish("computed_status", 2) -- disabled
end
end
for key, val in pairs(unit.reactor_data) do
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
unit.reactor_ps.publish(key, val)
end
end
for key, val in pairs(unit.reactor_data.rps_status) do
unit.reactor_ps.publish(key, val)
end
for key, val in pairs(unit.reactor_data.mek_status) do
unit.reactor_ps.publish(key, val)
end
end
-- annunciator
local annunciator = status[2] ---@type annunciator
for key, val in pairs(annunciator) do
if key == "TurbineTrip" then
-- split up turbine trip table for all turbines and a general OR combination
local trips = val
local any = false
for id = 1, #trips do
any = any or trips[id]
unit.turbine_ps_tbl[id].publish(key, trips[id])
end
unit.reactor_ps.publish("TurbineTrip", any)
elseif key == "BoilerOnline" or key == "HeatingRateLow" then
-- split up array for all boilers
for id = 1, #val do
unit.boiler_ps_tbl[id].publish(key, val[id])
end
elseif key == "TurbineOnline" or key == "SteamDumpOpen" or key == "TurbineOverSpeed" then
-- split up array for all turbines
for id = 1, #val do
unit.turbine_ps_tbl[id].publish(key, val[id])
end
elseif type(val) == "table" then
-- we missed one of the tables?
log.error("unrecognized table found in annunciator list, this is a bug", true)
else
-- non-table fields
unit.reactor_ps.publish(key, val)
end
end
-- RTU statuses
local rtu_statuses = status[3]
-- boiler statuses
for id = 1, #unit.boiler_data_tbl do
if rtu_statuses.boilers[i] == nil then
-- disconnected
unit.boiler_ps_tbl[id].publish("computed_status", 1)
end
end
for id, boiler in pairs(rtu_statuses.boilers) do
unit.boiler_data_tbl[id].state = boiler[1] ---@type table
unit.boiler_data_tbl[id].tanks = boiler[2] ---@type table
local data = unit.boiler_data_tbl[id] ---@type boiler_session_db|boilerv_session_db
if data.state.boil_rate > 0 then
unit.boiler_ps_tbl[id].publish("computed_status", 3) -- active
else
unit.boiler_ps_tbl[id].publish("computed_status", 2) -- idle
end
for key, val in pairs(unit.boiler_data_tbl[id].state) do
unit.boiler_ps_tbl[id].publish(key, val)
end
for key, val in pairs(unit.boiler_data_tbl[id].tanks) do
unit.boiler_ps_tbl[id].publish(key, val)
end
end
-- turbine statuses
for id = 1, #unit.turbine_ps_tbl do
if rtu_statuses.turbines[i] == nil then
-- disconnected
unit.turbine_ps_tbl[id].publish("computed_status", 1)
end
end
for id, turbine in pairs(rtu_statuses.turbines) do
unit.turbine_data_tbl[id].state = turbine[1] ---@type table
unit.turbine_data_tbl[id].tanks = turbine[2] ---@type table
local data = unit.turbine_data_tbl[id] ---@type turbine_session_db|turbinev_session_db
if data.tanks.steam_fill >= 0.99 then
unit.turbine_ps_tbl[id].publish("computed_status", 4) -- trip
elseif data.state.flow_rate < 100 then
unit.turbine_ps_tbl[id].publish("computed_status", 2) -- idle
else
unit.turbine_ps_tbl[id].publish("computed_status", 3) -- active
end
for key, val in pairs(unit.turbine_data_tbl[id].state) do
unit.turbine_ps_tbl[id].publish(key, val)
end
for key, val in pairs(unit.turbine_data_tbl[id].tanks) do
unit.turbine_ps_tbl[id].publish(key, val)
end
end
end
end
return true
end
-- get the IO controller database
function iocontrol.get_db() return io end
return iocontrol

157
coordinator/renderer.lua Normal file
View File

@@ -0,0 +1,157 @@
local log = require("scada-common.log")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local main_view = require("coordinator.ui.layout.main_view")
local unit_view = require("coordinator.ui.layout.unit_view")
local renderer = {}
-- render engine
local engine = {
monitors = nil,
dmesg_window = nil,
ui_ready = false
}
-- UI layouts
local ui = {
main_layout = nil,
unit_layouts = {}
}
-- reset a display to the "default", but set text scale to 0.5
---@param monitor table monitor
---@param recolor? boolean override default color palette
local function _reset_display(monitor, recolor)
monitor.setTextScale(0.5)
monitor.setTextColor(colors.white)
monitor.setBackgroundColor(colors.black)
monitor.clear()
monitor.setCursorPos(1, 1)
if recolor then
-- set overridden colors
for i = 1, #style.colors do
monitor.setPaletteColor(style.colors[i].c, style.colors[i].hex)
end
else
-- reset all colors
for _, val in pairs(colors) do
-- colors api has constants and functions, just get color constants
if type(val) == "number" then
monitor.setPaletteColor(val, term.nativePaletteColor(val))
end
end
end
end
-- link to the monitor peripherals
---@param monitors monitors_struct
function renderer.set_displays(monitors)
engine.monitors = monitors
end
-- check if the renderer is configured to use a given monitor peripheral
---@param periph table peripheral
---@return boolean is_used
function renderer.is_monitor_used(periph)
if engine.monitors ~= nil then
if engine.monitors.primary == periph then
return true
else
for i = 1, #engine.monitors.unit_displays do
if engine.monitors.unit_displays[i] == periph then
return true
end
end
end
end
return false
end
-- reset all displays in use by the renderer
---@param recolor? boolean true to use color palette from style
function renderer.reset(recolor)
-- reset primary monitor
_reset_display(engine.monitors.primary, recolor)
-- reset unit displays
for _, monitor in pairs(engine.monitors.unit_displays) do
_reset_display(monitor, recolor)
end
end
-- initialize the dmesg output window
function renderer.init_dmesg()
local disp_x, disp_y = engine.monitors.primary.getSize()
engine.dmesg_window = window.create(engine.monitors.primary, 1, 1, disp_x, disp_y)
log.direct_dmesg(engine.dmesg_window)
end
-- start the coordinator GUI
function renderer.start_ui()
if not engine.ui_ready then
-- hide dmesg
engine.dmesg_window.setVisible(false)
-- show main view on main monitor
ui.main_layout = main_view(engine.monitors.primary)
-- show unit views on unit displays
for id, monitor in pairs(engine.monitors.unit_displays) do
table.insert(ui.unit_layouts, unit_view(monitor, id))
end
-- report ui as ready
engine.ui_ready = true
end
end
-- close out the UI
function renderer.close_ui()
if engine.ui_ready then
-- report ui as not ready
engine.ui_ready = false
-- hide to stop animation callbacks
ui.main_layout.hide()
for i = 1, #ui.unit_layouts do
ui.unit_layouts[i].hide()
engine.monitors.unit_displays[i].clear()
end
-- clear root UI elements
ui.main_layout = nil
ui.unit_layouts = {}
-- re-draw dmesg
engine.dmesg_window.setVisible(true)
engine.dmesg_window.redraw()
end
end
-- is the UI ready?
---@return boolean ready
function renderer.ui_ready() return engine.ui_ready end
-- handle a touch event
---@param event monitor_touch
function renderer.handle_touch(event)
if event.monitor == engine.monitors.primary_name then
ui.main_layout.handle_touch(event)
else
for id, monitor in pairs(engine.monitors.unit_name_map) do
if event.monitor == monitor then
local layout = ui.unit_layouts[id] ---@type graphics_element
layout.handle_touch(event)
end
end
end
end
return renderer

View File

@@ -4,34 +4,315 @@
require("/initenv").init_env()
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local tcallbackdsp = require("scada-common.tcallbackdsp")
local util = require("scada-common.util")
local config = require("coordinator.config")
local coordinator = require("coordinator.coordinator")
local core = require("graphics.core")
local COORDINATOR_VERSION = "alpha-v0.1.2"
local apisessions = require("coordinator.apisessions")
local config = require("coordinator.config")
local coordinator = require("coordinator.coordinator")
local renderer = require("coordinator.renderer")
local COORDINATOR_VERSION = "alpha-v0.4.12"
local print = util.print
local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
log.init("/log.txt", log.MODE.APPEND)
local log_graphics = coordinator.log_graphics
local log_sys = coordinator.log_sys
local log_boot = coordinator.log_boot
local log_comms = coordinator.log_comms
local log_comms_connecting = coordinator.log_comms_connecting
----------------------------------------
-- config validation
----------------------------------------
local cfv = util.new_validator()
cfv.assert_port(config.SCADA_SV_PORT)
cfv.assert_port(config.SCADA_SV_LISTEN)
cfv.assert_port(config.SCADA_API_LISTEN)
cfv.assert_type_int(config.NUM_UNITS)
cfv.assert_type_bool(config.RECOLOR)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
cfv.assert_type_bool(config.SECURE)
cfv.assert_type_str(config.PASSWORD)
assert(cfv.valid(), "bad config file: missing/invalid fields")
----------------------------------------
-- log init
----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE)
log.info("========================================")
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
log.info("========================================")
println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<")
----------------------------------------
-- system startup
----------------------------------------
-- mount connected devices
ppm.mount_all()
local modem = ppm.get_wireless_modem()
-- we need a modem
if modem == nil then
println("please connect a wireless modem")
-- setup monitors
local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS)
if not configured then
println("boot> monitor setup failed")
log.fatal("monitor configuration failed")
return
end
log.info("monitors ready, dmesg output incoming...")
-- init renderer
renderer.set_displays(monitors)
renderer.reset(config.RECOLOR)
renderer.init_dmesg()
log_graphics("displays connected and reset")
log_sys("system start on " .. os.date("%c"))
log_boot("starting " .. COORDINATOR_VERSION)
----------------------------------------
-- setup communications
----------------------------------------
-- get the communications modem
local modem = ppm.get_wireless_modem()
if modem == nil then
log_comms("wireless modem not found")
println("boot> wireless modem not found")
log.fatal("no wireless modem on startup")
return
else
log_comms("wireless modem connected")
end
-- create connection watchdog
local conn_watchdog = util.new_watchdog(5)
conn_watchdog.cancel()
log.debug("boot> conn watchdog created")
-- start comms, open all channels
local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.SCADA_SV_PORT, config.SCADA_SV_LISTEN, config.SCADA_API_LISTEN, conn_watchdog)
log.debug("boot> comms init")
log_comms("comms initialized")
-- base loop clock (2Hz, 10 ticks)
local MAIN_CLOCK = 0.5
local loop_clock = util.new_clock(MAIN_CLOCK)
----------------------------------------
-- connect to the supervisor
----------------------------------------
-- attempt to connect to the supervisor or exit
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
if not coord_comms.sv_connect(60, tick_waiting, task_done) then
log_comms("supervisor connection failed")
log.fatal("failed to connect to supervisor")
return false
end
return true
end
if not init_connect_sv() then
println("boot> failed to connect to supervisor")
log_sys("system shutdown")
return
else
log_sys("supervisor connected, proceeding to UI start")
end
----------------------------------------
-- start the UI
----------------------------------------
-- start up the UI
---@return boolean ui_ok started ok
local function init_start_ui()
log_graphics("starting UI...")
-- util.psleep(3)
local draw_start = util.time_ms()
local ui_ok, message = pcall(renderer.start_ui)
if not ui_ok then
renderer.close_ui()
log_graphics(util.c("UI crashed: ", message))
println_ts("UI crashed")
log.fatal(util.c("ui crashed with error ", message))
else
log_graphics("first UI draw took " .. (util.time_ms() - draw_start) .. "ms")
-- start clock
loop_clock.start()
end
return ui_ok
end
local ui_ok = init_start_ui()
----------------------------------------
-- main event loop
----------------------------------------
local no_modem = false
-- start connection watchdog
conn_watchdog.feed()
log.debug("boot> conn watchdog started")
log_sys("system started successfully")
-- event loop
-- ui_ok will never change in this loop, same as while true or exit if UI start failed
while ui_ok do
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event
if event == "peripheral_detach" then
local type, device = ppm.handle_unmount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
-- we only really care if this is our wireless modem
if device == modem then
no_modem = true
log_sys("comms modem disconnected")
println_ts("wireless modem disconnected!")
log.error("comms modem disconnected!")
-- close out UI
renderer.close_ui()
-- alert user to status
log_sys("awaiting comms modem reconnect...")
else
log_sys("non-comms modem disconnected")
log.warning("non-comms modem disconnected")
end
elseif type == "monitor" then
if renderer.is_monitor_used(device) then
-- "halt and catch fire" style handling
log_sys("lost a configured monitor, system will now exit")
break
else
log_sys("lost unused monitor, ignoring")
end
end
end
elseif event == "peripheral" then
local type, device = ppm.mount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
if device.isWireless() then
-- reconnected modem
no_modem = false
modem = device
coord_comms.reconnect_modem(modem)
log_sys("comms modem reconnected")
println_ts("wireless modem reconnected.")
-- re-init system
if not init_connect_sv() then break end
ui_ok = init_start_ui()
else
log_sys("wired modem reconnected")
end
elseif type == "monitor" then
-- not supported, system will exit on loss of in-use monitors
end
end
elseif event == "timer" then
if loop_clock.is_clock(param1) then
-- main loop tick
-- free any closed sessions
--apisessions.free_all_closed()
loop_clock.start()
elseif conn_watchdog.is_timer(param1) then
-- supervisor watchdog timeout
local msg = "supervisor server timeout"
log_comms(msg)
println_ts(msg)
log.warning(msg)
-- close connection and UI
coord_comms.close()
renderer.close_ui()
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
-- a non-clock/main watchdog timer event
--check API watchdogs
--apisessions.check_all_watchdogs(param1)
-- notify timer callback dispatcher
tcallbackdsp.handle(param1)
end
elseif event == "modem_message" then
-- got a packet
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
coord_comms.handle_packet(packet)
-- check if it was a disconnect
if not coord_comms.is_linked() then
log_comms("supervisor closed connection")
-- close connection and UI
coord_comms.close()
renderer.close_ui()
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
elseif event == "monitor_touch" then
-- handle a monitor touch event
renderer.handle_touch(core.events.touch(param1, param2, param3))
end
-- check for termination request
if event == "terminate" or ppm.should_terminate() then
println_ts("terminate requested, closing connections...")
log_comms("terminate requested, closing supervisor connection...")
coord_comms.close()
log_comms("supervisor connection closed")
log_comms("closing api sessions...")
apisessions.close_all()
log_comms("api sessions closed")
break
end
end
renderer.close_ui()
log_sys("system shutdown")
println_ts("exited")
log.info("exited")

View File

@@ -0,0 +1,49 @@
local core = require("graphics.core")
local style = require("coordinator.ui.style")
local DataIndicator = require("graphics.elements.indicators.data")
local StateIndicator = require("graphics.elements.indicators.state")
local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox")
local VerticalBar = require("graphics.elements.indicators.vbar")
local cpair = core.graphics.cpair
local border = core.graphics.border
-- new boiler view
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y
---@param ps psil ps interface
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 text_fg_bg = cpair(colors.black, colors.lightGray)
local lu_col = cpair(colors.gray, colors.gray)
local status = StateIndicator{parent=boiler,x=10,y=1,states=style.boiler.states,value=1,min_width=10}
local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg}
local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg}
ps.subscribe("computed_status", status.update)
ps.subscribe("temperature", temp.update)
ps.subscribe("boil_rate", boil_r.update)
TextBox{parent=boiler,text="H",x=2,y=5,height=1,width=1,fg_bg=text_fg_bg}
TextBox{parent=boiler,text="W",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg}
TextBox{parent=boiler,text="S",x=27,y=5,height=1,width=1,fg_bg=text_fg_bg}
TextBox{parent=boiler,text="C",x=28,y=5,height=1,width=1,fg_bg=text_fg_bg}
local hcool = VerticalBar{parent=boiler,x=2,y=1,fg_bg=cpair(colors.orange,colors.gray),height=4,width=1}
local water = VerticalBar{parent=boiler,x=3,y=1,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1}
local steam = VerticalBar{parent=boiler,x=27,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
local ccool = VerticalBar{parent=boiler,x=28,y=1,fg_bg=cpair(colors.lightBlue,colors.gray),height=4,width=1}
ps.subscribe("hcool_fill", hcool.update)
ps.subscribe("water_fill", water.update)
ps.subscribe("steam_fill", steam.update)
ps.subscribe("ccool_fill", ccool.update)
end
return new_view

View File

@@ -0,0 +1,63 @@
local util = require("scada-common.util")
local core = require("graphics.core")
local style = require("coordinator.ui.style")
local HorizontalBar = require("graphics.elements.indicators.hbar")
local DataIndicator = require("graphics.elements.indicators.data")
local StateIndicator = require("graphics.elements.indicators.state")
local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
local cpair = core.graphics.cpair
local border = core.graphics.border
-- create new reactor view
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y
---@param data reactor_db reactor data
---@param ps psil ps interface
local function new_view(root, x, y, data, ps)
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=8,y=1,states=style.reactor.states,value=1,min_width=14}
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.1f",value=0,width=26,fg_bg=text_fg_bg}
local heating_r = DataIndicator{parent=reactor,x=2,y=5,lu_colors=lu_col,label="Heating:",unit="mB/t",format="%12.0f",value=0,commas=true,width=26,fg_bg=text_fg_bg}
ps.subscribe("computed_status", status.update)
ps.subscribe("temp", core_temp.update)
ps.subscribe("act_burn_rate", burn_r.update)
ps.subscribe("heating_rate", heating_r.update)
local reactor_fills = Rectangle{parent=root,border=border(1, colors.gray, true),width=24,height=7,x=(x + 29),y=y}
TextBox{parent=reactor_fills,text="FUEL",x=2,y=1,height=1,fg_bg=text_fg_bg}
TextBox{parent=reactor_fills,text="COOL",x=2,y=2,height=1,fg_bg=text_fg_bg}
TextBox{parent=reactor_fills,text="HCOOL",x=2,y=4,height=1,fg_bg=text_fg_bg}
TextBox{parent=reactor_fills,text="WASTE",x=2,y=5,height=1,fg_bg=text_fg_bg}
-- local ccool_color = util.trinary(data.mek_status.ccool_type == "sodium", cpair(colors.lightBlue,colors.gray), cpair(colors.blue,colors.gray))
-- local hcool_color = util.trinary(data.mek_status.hcool_type == "superheated_sodium", cpair(colors.orange,colors.gray), cpair(colors.white,colors.gray))
local ccool_color = util.trinary(true, cpair(colors.lightBlue,colors.gray), cpair(colors.blue,colors.gray))
local hcool_color = util.trinary(true, cpair(colors.orange,colors.gray), cpair(colors.white,colors.gray))
local fuel = HorizontalBar{parent=reactor_fills,x=8,y=1,show_percent=true,bar_fg_bg=cpair(colors.black,colors.gray),height=1,width=14}
local ccool = HorizontalBar{parent=reactor_fills,x=8,y=2,show_percent=true,bar_fg_bg=ccool_color,height=1,width=14}
local hcool = HorizontalBar{parent=reactor_fills,x=8,y=4,show_percent=true,bar_fg_bg=hcool_color,height=1,width=14}
local waste = HorizontalBar{parent=reactor_fills,x=8,y=5,show_percent=true,bar_fg_bg=cpair(colors.brown,colors.gray),height=1,width=14}
ps.subscribe("fuel_fill", fuel.update)
ps.subscribe("ccool_fill", ccool.update)
ps.subscribe("hcool_fill", hcool.update)
ps.subscribe("waste_fill", waste.update)
end
return new_view

View File

@@ -0,0 +1,37 @@
local core = require("graphics.core")
local style = require("coordinator.ui.style")
local DataIndicator = require("graphics.elements.indicators.data")
local StateIndicator = require("graphics.elements.indicators.state")
local Rectangle = require("graphics.elements.rectangle")
local VerticalBar = require("graphics.elements.indicators.vbar")
local cpair = core.graphics.cpair
local border = core.graphics.border
-- new turbine view
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y
---@param ps psil ps interface
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 text_fg_bg = cpair(colors.black, colors.lightGray)
local lu_col = cpair(colors.gray, colors.gray)
local status = StateIndicator{parent=turbine,x=8,y=1,states=style.turbine.states,value=1,min_width=10}
local prod_rate = DataIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",unit="MFE",format="%10.2f",value=0,width=16,fg_bg=text_fg_bg}
local flow_rate = DataIndicator{parent=turbine,x=5,y=4,lu_colors=lu_col,label="",unit="mB/t",format="%10.0f",value=0,commas=true,width=16,fg_bg=text_fg_bg}
ps.subscribe("computed_status", status.update)
ps.subscribe("prod_rate", prod_rate.update)
ps.subscribe("flow_rate", flow_rate.update)
local steam = VerticalBar{parent=turbine,x=2,y=1,fg_bg=cpair(colors.white,colors.gray),height=5,width=2}
ps.subscribe("steam_fill", steam.update)
end
return new_view

View File

@@ -0,0 +1,289 @@
--
-- Reactor Unit SCADA Coordinator GUI
--
local tcallbackdsp = require("scada-common.tcallbackdsp")
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 ColorMap = require("graphics.elements.colormap")
local CoreMap = require("graphics.elements.indicators.coremap")
local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light")
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
local MultiButton = require("graphics.elements.controls.multi_button")
local PushButton = require("graphics.elements.controls.push_button")
local SCRAMButton = require("graphics.elements.controls.scram_button")
local StartButton = require("graphics.elements.controls.start_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
local cpair = core.graphics.cpair
-- create a unit view
---@param parent graphics_element parent
---@param id integer
local function init(parent, id)
local unit = iocontrol.get_db().units[id] ---@type ioctl_entry
local r_ps = unit.reactor_ps
local b_ps = unit.boiler_ps_tbl
local t_ps = unit.turbine_ps_tbl
local main = Div{parent=parent,x=1,y=1}
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
local scram_fg_bg = cpair(colors.white, colors.gray)
local lu_cpair = cpair(colors.gray, colors.gray)
-- main stats and core map --
---@todo need to be checking actual reactor dimensions somehow
local core_map = CoreMap{parent=main,x=2,y=3,reactor_l=18,reactor_w=18}
r_ps.subscribe("temp", core_map.update)
local stat_fg_bg = cpair(colors.black,colors.white)
TextBox{parent=main,x=21,y=3,text="Core Temp",height=1,fg_bg=style.label}
local core_temp = DataIndicator{parent=main,x=21,label="",format="%9.2f",value=0,unit="K",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg}
r_ps.subscribe("temp", core_temp.update)
main.line_break()
TextBox{parent=main,x=21,text="Burn Rate",height=1,width=12,fg_bg=style.label}
local act_burn_r = DataIndicator{parent=main,x=21,label="",format="%6.1f",value=0,unit="mB/t",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg}
r_ps.subscribe("act_burn_rate", act_burn_r.update)
main.line_break()
TextBox{parent=main,x=21,text="Commanded Burn Rate",height=2,width=12,fg_bg=style.label}
local burn_r = DataIndicator{parent=main,x=21,label="",format="%6.1f",value=0,unit="mB/t",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg}
r_ps.subscribe("burn_rate", burn_r.update)
main.line_break()
TextBox{parent=main,x=21,text="Heating Rate",height=1,width=12,fg_bg=style.label}
local heating_r = DataIndicator{parent=main,x=21,label="",format="%11.0f",value=0,unit="",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg}
r_ps.subscribe("heating_rate", heating_r.update)
main.line_break()
TextBox{parent=main,x=21,text="Containment Integrity",height=2,width=12,fg_bg=style.label}
local integ = DataIndicator{parent=main,x=21,label="",format="%9.0f",value=100,unit="%",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg}
r_ps.subscribe("damage", function (x) integ.update(100.0 - x) end)
main.line_break()
-- TextBox{parent=main,text="FL",x=21,y=19,height=1,width=2,fg_bg=style.label}
-- TextBox{parent=main,text="WS",x=24,y=19,height=1,width=2,fg_bg=style.label}
-- TextBox{parent=main,text="CL",x=28,y=19,height=1,width=2,fg_bg=style.label}
-- TextBox{parent=main,text="HC",x=31,y=19,height=1,width=2,fg_bg=style.label}
-- local fuel = VerticalBar{parent=main,x=21,y=12,fg_bg=cpair(colors.black,colors.gray),height=6,width=2}
-- local waste = VerticalBar{parent=main,x=24,y=12,fg_bg=cpair(colors.brown,colors.gray),height=6,width=2}
-- local ccool = VerticalBar{parent=main,x=28,y=12,fg_bg=cpair(colors.lightBlue,colors.gray),height=6,width=2}
-- local hcool = VerticalBar{parent=main,x=31,y=12,fg_bg=cpair(colors.orange,colors.gray),height=6,width=2}
-- annunciator --
local annunciator = Div{parent=main,x=34,y=3}
-- annunciator colors per IAEA-TECDOC-812 recommendations
-- connectivity/basic state
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 r_active = IndicatorLight{parent=annunciator,label="Active",colors=cpair(colors.green,colors.gray)}
---@todo auto control as info sent here
local r_auto = IndicatorLight{parent=annunciator,label="Auto Control",colors=cpair(colors.blue,colors.gray)}
r_ps.subscribe("PLCOnline", plc_online.update)
r_ps.subscribe("PLCHeartbeat", plc_hbeat.update)
r_ps.subscribe("status", r_active.update)
annunciator.line_break()
-- annunciator fields
local r_scram = IndicatorLight{parent=annunciator,label="Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
local r_mscrm = IndicatorLight{parent=annunciator,label="Manual Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=cpair(colors.red,colors.gray)}
local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=cpair(colors.yellow,colors.gray)}
local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=cpair(colors.red,colors.gray)}
local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=cpair(colors.yellow,colors.gray)}
local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=cpair(colors.yellow,colors.gray)}
local r_wloc = IndicatorLight{parent=annunciator,label="Waste Line Occlusion",colors=cpair(colors.yellow,colors.gray)}
local r_hsrt = IndicatorLight{parent=annunciator,label="High Startup Rate",colors=cpair(colors.yellow,colors.gray)}
r_ps.subscribe("ReactorSCRAM", r_scram.update)
r_ps.subscribe("ManualReactorSCRAM", r_mscrm.update)
r_ps.subscribe("RCPTrip", r_rtrip.update)
r_ps.subscribe("RCSFlowLow", r_cflow.update)
r_ps.subscribe("ReactorTempHigh", r_temp.update)
r_ps.subscribe("ReactorHighDeltaT", r_rhdt.update)
r_ps.subscribe("FuelInputRateLow", r_firl.update)
r_ps.subscribe("WasteLineOcclusion", r_wloc.update)
r_ps.subscribe("HighStartupRate", r_hsrt.update)
annunciator.line_break()
-- RPS
local rps_trp = IndicatorLight{parent=annunciator,label="RPS Trip",colors=cpair(colors.red,colors.gray)}
local rps_dmg = IndicatorLight{parent=annunciator,label="Damage Critical",colors=cpair(colors.yellow,colors.gray)}
local rps_exh = IndicatorLight{parent=annunciator,label="Excess Heated Coolant",colors=cpair(colors.yellow,colors.gray)}
local rps_exw = IndicatorLight{parent=annunciator,label="Excess Waste",colors=cpair(colors.yellow,colors.gray)}
local rps_tmp = IndicatorLight{parent=annunciator,label="High Core Temp",colors=cpair(colors.yellow,colors.gray)}
local rps_nof = IndicatorLight{parent=annunciator,label="No Fuel",colors=cpair(colors.yellow,colors.gray)}
local rps_noc = IndicatorLight{parent=annunciator,label="No Coolant",colors=cpair(colors.yellow,colors.gray)}
local rps_flt = IndicatorLight{parent=annunciator,label="PPM Fault",colors=cpair(colors.yellow,colors.gray)}
local rps_tmo = IndicatorLight{parent=annunciator,label="Timeout",colors=cpair(colors.yellow,colors.gray)}
r_ps.subscribe("rps_tripped", rps_trp.update)
r_ps.subscribe("dmg_crit", rps_dmg.update)
r_ps.subscribe("ex_hcool", rps_exh.update)
r_ps.subscribe("ex_waste", rps_exw.update)
r_ps.subscribe("high_temp", rps_tmp.update)
r_ps.subscribe("no_fuel", rps_nof.update)
r_ps.subscribe("no_cool", rps_noc.update)
r_ps.subscribe("fault", rps_flt.update)
r_ps.subscribe("timeout", rps_tmo.update)
annunciator.line_break()
-- cooling
local c_brm = IndicatorLight{parent=annunciator,label="Boil Rate Mismatch",colors=cpair(colors.yellow,colors.gray)}
local c_cfm = IndicatorLight{parent=annunciator,label="Coolant Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
local c_sfm = IndicatorLight{parent=annunciator,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
local c_mwrf = IndicatorLight{parent=annunciator,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)}
local c_tbnt = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray)}
r_ps.subscribe("BoilRateMismatch", c_brm.update)
r_ps.subscribe("CoolantFeedMismatch", c_cfm.update)
r_ps.subscribe("SteamFeedMismatch", c_sfm.update)
r_ps.subscribe("MaxWaterReturnFeed", c_mwrf.update)
r_ps.subscribe("TurbineTrip", c_tbnt.update)
annunciator.line_break()
-- machine-specific indicators
if unit.num_boilers > 0 then
TextBox{parent=main,x=32,y=34,text="B1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local b1_hr = IndicatorLight{parent=annunciator,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
b_ps[1].subscribe("HeatingRateLow", b1_hr.update)
end
if unit.num_boilers > 1 then
TextBox{parent=main,x=32,text="B2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local b2_hr = IndicatorLight{parent=annunciator,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
b_ps[2].subscribe("HeatingRateLow", b2_hr.update)
end
if unit.num_boilers > 0 then
main.line_break()
annunciator.line_break()
end
TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t1_sdo = TriIndicatorLight{parent=annunciator,label="Steam Dump Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
t_ps[1].subscribe("SteamDumpOpen", t1_sdo.update)
TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t1_tos = IndicatorLight{parent=annunciator,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
t_ps[1].subscribe("TurbineOverSpeed", t1_tos.update)
TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t1_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray)}
t_ps[1].subscribe("TurbineTrip", t1_trp.update)
main.line_break()
annunciator.line_break()
if unit.num_turbines > 1 then
TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t2_sdo = TriIndicatorLight{parent=annunciator,label="Steam Dump Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
t_ps[2].subscribe("SteamDumpOpen", t2_sdo.update)
TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t2_tos = IndicatorLight{parent=annunciator,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
t_ps[2].subscribe("TurbineOverSpeed", t2_tos.update)
TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t2_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray)}
t_ps[2].subscribe("TurbineTrip", t2_trp.update)
main.line_break()
annunciator.line_break()
end
if unit.num_turbines > 2 then
TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t3_sdo = TriIndicatorLight{parent=annunciator,label="Steam Dump Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
t_ps[3].subscribe("SteamDumpOpen", t3_sdo.update)
TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t3_tos = IndicatorLight{parent=annunciator,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
t_ps[3].subscribe("TurbineOverSpeed", t3_tos.update)
TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t3_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray)}
t_ps[3].subscribe("TurbineTrip", t3_trp.update)
annunciator.line_break()
end
---@todo radiation monitor
IndicatorLight{parent=annunciator,label="Radiation Monitor",colors=cpair(colors.green,colors.gray)}
IndicatorLight{parent=annunciator,label="Radiation Alarm",colors=cpair(colors.red,colors.gray)}
DataIndicator{parent=main,x=34,y=51,label="",format="%10.1f",value=0,unit="mSv/h",lu_colors=lu_cpair,width=18,fg_bg=stat_fg_bg}
-- reactor controls --
StartButton{parent=main,x=12,y=44,callback=unit.start,fg_bg=scram_fg_bg}
SCRAMButton{parent=main,x=22,y=44,callback=unit.scram,fg_bg=scram_fg_bg}
local burn_control = Div{parent=main,x=12,y=40,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,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=burn_control,x=9,y=2,text="mB/t"}
local set_burn = function () unit.set_burn(burn_rate.get_value()) end
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),callback=set_burn}
local 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.lime)
},
{
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)
}
}
---@todo waste selection
local waste_sel_f = function (s) print("waste: " .. s) end
local waste_sel = Div{parent=main,x=2,y=48,width=29,height=2,fg_bg=cpair(colors.black, colors.white)}
MultiButton{parent=waste_sel,x=1,y=1,options=opts,callback=waste_sel_f,min_width=6,fg_bg=cpair(colors.black, colors.white)}
TextBox{parent=waste_sel,text="Waste Processing",alignment=TEXT_ALIGN.CENTER,x=1,y=1,height=1}
---@fixme test code
main.line_break()
ColorMap{parent=main,x=2,y=51}
return main
end
return init

View File

@@ -0,0 +1,176 @@
--
-- Basic Unit Overview
--
local core = require("graphics.core")
local style = require("coordinator.ui.style")
local reactor_view = require("coordinator.ui.components.reactor")
local boiler_view = require("coordinator.ui.components.boiler")
local turbine_view = require("coordinator.ui.components.turbine")
local Div = require("graphics.elements.div")
local PipeNetwork = require("graphics.elements.pipenet")
local TextBox = require("graphics.elements.textbox")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
local cpair = core.graphics.cpair
local border = core.graphics.border
local pipe = core.graphics.pipe
-- make a new unit overview window
---@param parent graphics_element parent
---@param x integer top left x
---@param y integer top left y
---@param unit ioctl_entry unit database entry
local function make(parent, x, y, unit)
local height = 0
local num_boilers = #unit.boiler_data_tbl
local num_turbines = #unit.turbine_data_tbl
assert(num_boilers >= 0 and num_boilers <= 2, "minimum 0 boilers, maximum 2 boilers")
assert(num_turbines >= 1 and num_turbines <= 3, "minimum 1 turbine, maximum 3 turbines")
if num_boilers == 0 and num_turbines == 1 then
height = 9
elseif num_boilers == 1 and num_turbines <= 2 then
height = 17
else
height = 25
end
-- bounding box div
local root = Div{parent=parent,x=x,y=y,width=80,height=height}
-- unit header message
TextBox{parent=root,text="Unit #" .. unit.unit_id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
-------------
-- REACTOR --
-------------
reactor_view(root, 1, 3, unit.reactor_data, unit.reactor_ps)
if num_boilers > 0 then
local coolant_pipes = {}
if num_boilers >= 2 then
table.insert(coolant_pipes, pipe(0, 0, 11, 12, colors.lightBlue))
end
table.insert(coolant_pipes, pipe(0, 0, 11, 3, colors.lightBlue))
table.insert(coolant_pipes, pipe(2, 0, 11, 2, colors.orange))
if num_boilers >= 2 then
table.insert(coolant_pipes, pipe(2, 0, 11, 11, colors.orange))
end
PipeNetwork{parent=root,x=4,y=10,pipes=coolant_pipes,bg=colors.lightGray}
end
-------------
-- BOILERS --
-------------
if num_boilers >= 1 then boiler_view(root, 16, 11, unit.boiler_ps_tbl[1]) end
if num_boilers >= 2 then boiler_view(root, 16, 19, unit.boiler_ps_tbl[2]) end
--------------
-- TURBINES --
--------------
local t_idx = 1
local no_boilers = num_boilers == 0
if (num_turbines >= 3) or no_boilers or (num_boilers == 1 and num_turbines >= 2) then
turbine_view(root, 58, 3, unit.turbine_ps_tbl[t_idx])
t_idx = t_idx + 1
end
if (num_turbines >= 1 and not no_boilers) or num_turbines >= 2 then
turbine_view(root, 58, 11, unit.turbine_ps_tbl[t_idx])
t_idx = t_idx + 1
end
if (num_turbines >= 2 and num_boilers >= 2) or num_turbines >= 3 then
turbine_view(root, 58, 19, unit.turbine_ps_tbl[t_idx])
end
local steam_pipes_b = {}
if no_boilers then
table.insert(steam_pipes_b, pipe(0, 1, 3, 1, colors.white)) -- steam to turbine 1
table.insert(steam_pipes_b, pipe(0, 2, 3, 2, colors.blue)) -- water to turbine 1
if num_turbines >= 2 then
table.insert(steam_pipes_b, pipe(1, 2, 3, 9, colors.white)) -- steam to turbine 2
table.insert(steam_pipes_b, pipe(2, 3, 3, 10, colors.blue)) -- water to turbine 2
end
if num_turbines >= 3 then
table.insert(steam_pipes_b, pipe(1, 9, 3, 17, colors.white)) -- steam boiler 1 to turbine 1 junction end
table.insert(steam_pipes_b, pipe(2, 10, 3, 18, colors.blue)) -- water boiler 1 to turbine 1 junction start
end
else
-- boiler side pipes
local steam_pipes_a = {
-- boiler 1 steam/water pipes
pipe(0, 1, 6, 1, colors.white, false, true), -- steam boiler 1 to turbine junction
pipe(0, 2, 6, 2, colors.blue, false, true) -- water boiler 1 to turbine junction
}
if num_boilers >= 2 then
-- boiler 2 steam/water pipes
table.insert(steam_pipes_a, pipe(0, 9, 6, 9, colors.white, false, true)) -- steam boiler 2 to turbine junction
table.insert(steam_pipes_a, pipe(0, 10, 6, 10, colors.blue, false, true)) -- water boiler 2 to turbine junction
end
-- turbine side pipes
if num_turbines >= 3 or (num_boilers == 1 and num_turbines == 2) then
table.insert(steam_pipes_b, pipe(0, 9, 1, 2, colors.white, false, true)) -- steam boiler 1 to turbine 1 junction start
table.insert(steam_pipes_b, pipe(1, 1, 3, 1, colors.white, false, false)) -- steam boiler 1 to turbine 1 junction end
end
table.insert(steam_pipes_b, pipe(0, 9, 3, 9, colors.white, false, true)) -- steam boiler 1 to turbine 2
if num_turbines >= 3 or (num_boilers == 1 and num_turbines == 2) then
table.insert(steam_pipes_b, pipe(0, 10, 2, 3, colors.blue, false, true)) -- water boiler 1 to turbine 1 junction start
table.insert(steam_pipes_b, pipe(2, 2, 3, 2, colors.blue, false, false)) -- water boiler 1 to turbine 1 junction end
end
table.insert(steam_pipes_b, pipe(0, 10, 3, 10, colors.blue, false, true)) -- water boiler 1 to turbine 2
if num_turbines >= 3 or (num_turbines >= 2 and num_boilers >= 2) then
if num_boilers >= 2 then
table.insert(steam_pipes_b, pipe(0, 17, 1, 9, colors.white, false, true)) -- steam boiler 2 to turbine 2 junction
table.insert(steam_pipes_b, pipe(0, 17, 3, 17, colors.white, false, true)) -- steam boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(0, 18, 2, 10, colors.blue, false, true)) -- water boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(0, 18, 3, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
else
table.insert(steam_pipes_b, pipe(1, 17, 1, 9, colors.white, false, true)) -- steam boiler 2 to turbine 2 junction
table.insert(steam_pipes_b, pipe(1, 17, 3, 17, colors.white, false, true)) -- steam boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(2, 18, 2, 10, colors.blue, false, true)) -- water boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(2, 18, 3, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
end
elseif num_turbines == 1 and num_boilers >= 2 then
table.insert(steam_pipes_b, pipe(0, 17, 1, 9, colors.white, false, true)) -- steam boiler 2 to turbine 2 junction
table.insert(steam_pipes_b, pipe(0, 17, 1, 17, colors.white, false, true)) -- steam boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(0, 18, 2, 10, colors.blue, false, true)) -- water boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(0, 18, 2, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
end
PipeNetwork{parent=root,x=47,y=11,pipes=steam_pipes_a,bg=colors.lightGray}
end
PipeNetwork{parent=root,x=54,y=3,pipes=steam_pipes_b,bg=colors.lightGray}
return root
end
return make

View File

@@ -0,0 +1,33 @@
--
-- Reactor Unit SCADA Coordinator GUI
--
local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local WaitingAnim = require("graphics.elements.animations.waiting")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
local cpair = core.graphics.cpair
-- create a unit waiting view
---@param parent graphics_element parent
---@param y integer y offset
local function init(parent, y)
-- bounding box div
local root = Div{parent=parent,x=1,y=y,height=5}
local waiting_x = math.floor(parent.width() / 2) - 2
TextBox{parent=root,text="Waiting for status...",alignment=TEXT_ALIGN.CENTER,y=1,height=1,fg_bg=cpair(colors.black,style.root.bkg)}
WaitingAnim{parent=root,x=waiting_x,y=3,fg_bg=cpair(colors.blue,style.root.bkg)}
return root
end
return init

45
coordinator/ui/dialog.lua Normal file
View File

@@ -0,0 +1,45 @@
local completion = require("cc.completion")
local util = require("scada-common.util")
local print = util.print
local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
local dialog = {}
function dialog.ask_y_n(question, default)
print(question)
if default == true then
print(" (Y/n)? ")
else
print(" (y/N)? ")
end
local response = read(nil, nil)
if response == "" then
return default
elseif response == "Y" or response == "y" then
return true
elseif response == "N" or response == "n" then
return false
else
return nil
end
end
function dialog.ask_options(options, cancel)
print("> ")
local response = read(nil, nil, function(text) return completion.choice(text, options) end)
if response == cancel then return false end
if util.table_contains(options, response) then
return response
else return nil end
end
return dialog

View File

@@ -0,0 +1,47 @@
--
-- Main SCADA Coordinator GUI
--
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local unit_overview = require("coordinator.ui.components.unit_overview")
local core = require("graphics.core")
local DisplayBox = require("graphics.elements.displaybox")
local TextBox = require("graphics.elements.textbox")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
-- create new main view
---@param monitor table main viewscreen
local function init(monitor)
local main = DisplayBox{window=monitor,fg_bg=style.root}
-- window header message
TextBox{parent=main,text="Nuclear Generation Facility SCADA Coordinator",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
local db = iocontrol.get_db()
local uo_1, uo_2, uo_3, uo_4 ---@type graphics_element
-- unit overviews
if db.facility.num_units >= 1 then uo_1 = unit_overview(main, 2, 3, db.units[1]) end
if db.facility.num_units >= 2 then uo_2 = unit_overview(main, 84, 3, db.units[2]) end
if db.facility.num_units >= 3 then
-- base offset 3, spacing 1, max height of units 1 and 2
local row_2_offset = 3 + 1 + math.max(uo_1.height(), uo_2.height())
uo_3 = unit_overview(main, 2, row_2_offset, db.units[3])
if db.facility.num_units == 4 then uo_4 = unit_overview(main, 84, row_2_offset, db.units[4]) end
end
-- command & control
return main
end
return init

View File

@@ -0,0 +1,26 @@
--
-- Reactor Unit SCADA Coordinator GUI
--
local tcallbackdsp = require("scada-common.tcallbackdsp")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local unit_detail = require("coordinator.ui.components.unit_detail")
local DisplayBox = require("graphics.elements.displaybox")
-- create a unit view
---@param monitor table
---@param id integer
local function init(monitor, id)
local main = DisplayBox{window=monitor,fg_bg=style.root}
unit_detail(main, id)
return main
end
return init

101
coordinator/ui/style.lua Normal file
View File

@@ -0,0 +1,101 @@
local core = require("graphics.core")
local style = {}
local cpair = core.graphics.cpair
-- GLOBAL --
style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray)
style.label = cpair(colors.gray, colors.lightGray)
style.colors = {
{ c = colors.red, hex = 0xdf4949 },
{ c = colors.orange, hex = 0xffb659 },
{ c = colors.yellow, hex = 0xfffc79 },
{ c = colors.lime, hex = 0x64dd20 },
{ c = colors.green, hex = 0x4aee8a },
{ c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff },
{ c = colors.purple, hex = 0xb156ee },
{ c = colors.pink, hex = 0xf26ba2 },
{ c = colors.magenta, hex = 0xf9488a },
-- { c = colors.white, hex = 0xf0f0f0 },
{ c = colors.lightGray, hex = 0xcacaca },
{ c = colors.gray, hex = 0x575757 },
-- { c = colors.black, hex = 0x191919 },
-- { c = colors.brown, hex = 0x7f664c }
}
-- MAIN LAYOUT --
style.reactor = {
-- reactor states
states = {
{
color = cpair(colors.black, colors.yellow),
text = "PLC OFF-LINE"
},
{
color = cpair(colors.white, colors.gray),
text = "DISABLED"
},
{
color = cpair(colors.black, colors.green),
text = "ACTIVE"
},
{
color = cpair(colors.black, colors.red),
text = "SCRAMMED"
},
{
color = cpair(colors.black, colors.orange),
text = "PLC FAULT"
}
}
}
style.boiler = {
-- boiler states
states = {
{
color = cpair(colors.black, colors.yellow),
text = "OFF-LINE"
},
{
color = cpair(colors.white, colors.gray),
text = "IDLE"
},
{
color = cpair(colors.black, colors.green),
text = "ACTIVE"
}
}
}
style.turbine = {
-- turbine states
states = {
{
color = cpair(colors.black, colors.yellow),
text = "OFF-LINE"
},
{
color = cpair(colors.white, colors.gray),
text = "IDLE"
},
{
color = cpair(colors.black, colors.green),
text = "ACTIVE"
},
{
color = cpair(colors.black, colors.red),
text = "TRIP"
}
}
}
return style

146
graphics/core.lua Normal file
View File

@@ -0,0 +1,146 @@
--
-- Graphics Core Functions and Objects
--
local core = {}
local events = {}
---@class monitor_touch
---@field monitor string
---@field x integer
---@field y integer
-- create a new touch event definition
---@param monitor string
---@param x integer
---@param y integer
---@return monitor_touch
function events.touch(monitor, x, y)
return {
monitor = monitor,
x = x,
y = y
}
end
core.events = events
local graphics = {}
---@alias TEXT_ALIGN integer
graphics.TEXT_ALIGN = {
LEFT = 1,
CENTER = 2,
RIGHT = 3
}
---@class graphics_border
---@field width integer
---@field color color
---@field even boolean
---@alias element_id string|integer
-- create a new border definition
---@param width integer border width
---@param color color border color
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
---@return graphics_border
function graphics.border(width, color, even)
return {
width = width,
color = color,
even = even or false -- convert nil to false
}
end
---@class graphics_frame
---@field x integer
---@field y integer
---@field w integer
---@field h integer
-- create a new graphics frame definition
---@param x integer
---@param y integer
---@param w integer
---@param h integer
---@return graphics_frame
function graphics.gframe(x, y, w, h)
return {
x = x,
y = y,
w = w,
h = h
}
end
---@class cpair
---@field color_a color
---@field color_b color
---@field blit_a string
---@field blit_b string
---@field fgd color
---@field bkg color
---@field blit_fgd string
---@field blit_bkg string
-- create a new color pair definition
---@param a color
---@param b color
---@return cpair
function graphics.cpair(a, b)
return {
-- color pairs
color_a = a,
color_b = b,
blit_a = colors.toBlit(a),
blit_b = colors.toBlit(b),
-- aliases
fgd = a,
bkg = b,
blit_fgd = colors.toBlit(a),
blit_bkg = colors.toBlit(b)
}
end
---@class pipe
---@field x1 integer starting x, origin is 0
---@field y1 integer starting y, origin is 0
---@field x2 integer ending x, origin is 0
---@field y2 integer ending y, origin is 0
---@field w integer width
---@field h integer height
---@field color color pipe color
---@field thin boolean true for 1 subpixel, false (default) for 2
---@field align_tr boolean false to align bottom left (default), true to align top right
-- create a new pipe
--
-- note: pipe coordinate origin is (0, 0)
---@param x1 integer starting x, origin is 0
---@param y1 integer starting y, origin is 0
---@param x2 integer ending x, origin is 0
---@param y2 integer ending y, origin is 0
---@param color color pipe color
---@param thin? boolean true for 1 subpixel, false (default) for 2
---@param align_tr? boolean false to align bottom left (default), true to align top right
---@return pipe
function graphics.pipe(x1, y1, x2, y2, color, thin, align_tr)
return {
x1 = x1,
y1 = y1,
x2 = x2,
y2 = y2,
w = math.abs(x2 - x1) + 1,
h = math.abs(y2 - y1) + 1,
color = color,
thin = thin or false,
align_tr = align_tr or false
}
end
core.graphics = graphics
return core

342
graphics/element.lua Normal file
View File

@@ -0,0 +1,342 @@
--
-- Generic Graphics Element
--
local core = require("graphics.core")
local element = {}
---@class graphics_args_generic
---@field window? table
---@field parent? graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer next line if omitted
---@field offset_x? integer 0 if omitted
---@field offset_y? integer 0 if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
-- a base graphics element, should not be created on its own
---@param args graphics_args_generic arguments
function element.new(args)
local self = {
id = -1,
elem_type = debug.getinfo(2).name,
define_completed = false,
p_window = nil, ---@type table
position = { x = 1, y = 1 },
child_offset = { x = 0, y = 0 },
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1},
next_y = 1,
children = {},
mt = {}
}
---@class graphics_template
local protected = {
value = nil, ---@type any
window = nil, ---@type table
fg_bg = core.graphics.cpair(colors.white, colors.black),
frame = core.graphics.gframe(1, 1, 1, 1)
}
-- element as string
function self.mt.__tostring()
return "graphics.element{" .. self.elem_type .. "} @ " .. tostring(self)
end
---@class graphics_element
local public = {}
setmetatable(public, self.mt)
-------------------------
-- PROTECTED FUNCTIONS --
-------------------------
-- prepare the template
---@param offset_x integer x offset
---@param offset_y integer y offset
---@param next_y integer next line if no y was provided
function protected.prepare_template(offset_x, offset_y, next_y)
-- get frame coordinates/size
if args.gframe ~= nil then
protected.frame.x = args.gframe.x
protected.frame.y = args.gframe.y
protected.frame.w = args.gframe.w
protected.frame.h = args.gframe.h
else
local w, h = self.p_window.getSize()
protected.frame.x = args.x or 1
protected.frame.y = args.y or next_y
protected.frame.w = args.width or w
protected.frame.h = args.height or h
end
-- inner offsets
if args.offset_x ~= nil then self.child_offset.x = args.offset_x end
if args.offset_y ~= nil then self.child_offset.y = args.offset_y end
-- adjust window frame if applicable
local f = protected.frame
local x = f.x
local y = f.y
-- apply offsets
if args.parent ~= nil then
-- constrain to parent inner width/height
local w, h = self.p_window.getSize()
f.w = math.min(f.w, w - ((2 * offset_x) + (f.x - 1)))
f.h = math.min(f.h, h - ((2 * offset_y) + (f.y - 1)))
-- offset x/y
f.x = x + offset_x
f.y = y + offset_y
end
-- check frame
assert(f.x >= 1, "graphics.element{" .. self.elem_type .. "}: frame x not >= 1")
assert(f.y >= 1, "graphics.element{" .. self.elem_type .. "}: frame y not >= 1")
assert(f.w >= 1, "graphics.element{" .. self.elem_type .. "}: frame width not >= 1")
assert(f.h >= 1, "graphics.element{" .. self.elem_type .. "}: frame height not >= 1")
-- create window
protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, true)
-- init colors
if args.fg_bg ~= nil then
protected.fg_bg = args.fg_bg
elseif args.parent ~= nil then
protected.fg_bg = args.parent.get_fg_bg()
end
-- set colors
protected.window.setBackgroundColor(protected.fg_bg.bkg)
protected.window.setTextColor(protected.fg_bg.fgd)
protected.window.clear()
-- record position
self.position.x, self.position.y = protected.window.getPosition()
-- calculate bounds
self.bounds.x1 = self.position.x
self.bounds.x2 = self.position.x + f.w - 1
self.bounds.y1 = self.position.y
self.bounds.y2 = self.position.y + f.h - 1
end
-- handle a touch event
---@param event table monitor_touch event
function protected.handle_touch(event)
end
-- handle data value changes
function protected.on_update(...)
end
-- get value
function protected.get_value()
return protected.value
end
-- set value
---@param value any value to set
function protected.set_value(value)
return nil
end
-- custom recolor command, varies by element if implemented
---@vararg cpair|color color(s)
function protected.recolor(...)
end
-- custom resize command, varies by element if implemented
---@vararg integer sizing
function protected.resize(...)
end
-- start animations
function protected.start_anim()
end
-- stop animations
function protected.stop_anim()
end
-- get public interface
---@return graphics_element element, element_id id
function protected.get() return public, self.id end
-----------
-- SETUP --
-----------
-- get the parent window
self.p_window = args.window
if self.p_window == nil and args.parent ~= nil then
self.p_window = args.parent.window()
end
-- check window
assert(self.p_window, "graphics.element{" .. self.elem_type .. "}: no parent window provided")
-- prepare the template
if args.parent == nil then
protected.prepare_template(0, 0, 1)
else
self.id = args.parent.__add_child(args.id, protected)
end
----------------------
-- PUBLIC FUNCTIONS --
----------------------
-- get the window object
function public.window() return protected.window end
-- CHILD ELEMENTS --
-- add a child element
---@param key string|nil id
---@param child graphics_template
---@return integer|string key
function public.__add_child(key, child)
child.prepare_template(self.child_offset.x, self.child_offset.y, self.next_y)
self.next_y = child.frame.y + child.frame.h
local child_element = child.get()
if key == nil then
table.insert(self.children, child_element)
return #self.children
else
self.children[key] = child_element
return key
end
end
-- get a child element
---@return graphics_element
function public.get_child(key) return self.children[key] end
-- remove child
---@param key string|integer
function public.remove(key) self.children[key] = nil end
-- attempt to get a child element by ID (does not include this element itself)
---@param id element_id
---@return graphics_element|nil element
function public.get_element_by_id(id)
if self.children[id] == nil then
for _, child in pairs(self.children) do
local elem = child.get_element_by_id(id)
if elem ~= nil then return elem end
end
else
return self.children[id]
end
return nil
end
-- AUTO-PLACEMENT --
-- skip a line for automatically placed elements
function public.line_break() self.next_y = self.next_y + 1 end
-- PROPERTIES --
-- get the foreground/background colors
---@return cpair fg_bg
function public.get_fg_bg() return protected.fg_bg end
-- get element width
---@return integer width
function public.width()
return protected.frame.w
end
-- get element height
---@return integer height
function public.height()
return protected.frame.h
end
-- get the element value
---@return any value
function public.get_value()
return protected.get_value()
end
-- set the element value
---@param value any new value
function public.set_value(value)
protected.set_value(value)
end
-- resize attributes of the element value if supported
---@vararg number dimensions (element specific)
function public.resize(...)
protected.resize(...)
end
-- FUNCTION CALLBACKS --
-- handle a monitor touch
---@param event monitor_touch monitor touch event
function public.handle_touch(event)
local in_x = event.x >= self.bounds.x1 and event.x <= self.bounds.x2
local in_y = event.y >= self.bounds.y1 and event.y <= self.bounds.y2
if in_x and in_y then
local event_T = core.events.touch(event.monitor, (event.x - self.position.x) + 1, (event.y - self.position.y) + 1)
-- handle the touch event, transformed into the window frame
protected.handle_touch(event_T)
-- pass on touch event to children
for _, val in pairs(self.children) do val.handle_touch(event_T) end
end
end
-- draw the element given new data
---@vararg any new data
function public.update(...)
protected.on_update(...)
end
-- VISIBILITY --
-- show the element
function public.show()
protected.window.setVisible(true)
protected.start_anim()
for i = 1, #self.children do
self.children[i].show()
end
end
-- hide the element
function public.hide()
protected.stop_anim()
for i = 1, #self.children do
self.children[i].hide()
end
protected.window.setVisible(false)
end
-- re-draw the element
function public.redraw()
protected.window.redraw()
end
return protected
end
return element

View File

@@ -0,0 +1,108 @@
-- Loading/Waiting Animation Graphics Element
local tcd = require("scada-common.tcallbackdsp")
local element = require("graphics.element")
---@class waiting_args
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
-- new waiting animation element
---@param args waiting_args
---@return graphics_element element, element_id id
local function waiting(args)
local state = 0
local run_animation = false
args.width = 4
args.height = 3
-- create new graphics element base object
local e = element.new(args)
local blit_fg = e.fg_bg.blit_fgd
local blit_bg = e.fg_bg.blit_bkg
local blit_fg_2x = e.fg_bg.blit_fgd .. e.fg_bg.blit_fgd
local blit_bg_2x = e.fg_bg.blit_bkg .. e.fg_bg.blit_bkg
-- tick the animation
local function animate()
e.window.clear()
if state >= 0 and state < 7 then
-- top
e.window.setCursorPos(1 + math.floor(state / 2), 1)
if state % 2 == 0 then
e.window.blit("\x8f", blit_fg, blit_bg)
else
e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
end
-- bottom
e.window.setCursorPos(4 - math.ceil(state / 2), 3)
if state % 2 == 0 then
e.window.blit("\x8f", blit_fg, blit_bg)
else
e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
end
else
local st = state - 7
-- right
if st % 3 == 0 then
e.window.setCursorPos(4, 1 + math.floor(st / 3))
e.window.blit("\x83", blit_bg, blit_fg)
elseif st % 3 == 1 then
e.window.setCursorPos(4, 1 + math.floor(st / 3))
e.window.blit("\x8f", blit_bg, blit_fg)
e.window.setCursorPos(4, 2 + math.floor(st / 3))
e.window.blit("\x83", blit_fg, blit_bg)
else
e.window.setCursorPos(4, 2 + math.floor(st / 3))
e.window.blit("\x8f", blit_fg, blit_bg)
end
-- left
if st % 3 == 0 then
e.window.setCursorPos(1, 3 - math.floor(st / 3))
e.window.blit("\x83", blit_fg, blit_bg)
e.window.setCursorPos(1, 2 - math.floor(st / 3))
e.window.blit("\x8f", blit_bg, blit_fg)
elseif st % 3 == 1 then
e.window.setCursorPos(1, 2 - math.floor(st / 3))
e.window.blit("\x83", blit_bg, blit_fg)
else
e.window.setCursorPos(1, 2 - math.floor(st / 3))
e.window.blit("\x8f", blit_fg, blit_bg)
end
end
state = state + 1
if state >= 12 then state = 0 end
if run_animation then
tcd.dispatch(0.5, animate)
end
end
-- start the animation
function e.start_anim()
run_animation = true
animate()
end
-- stop the animation
function e.stop_anim()
run_animation = false
end
e.start_anim()
return e.get()
end
return waiting

View File

@@ -0,0 +1,33 @@
-- Color Map Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class colormap_args
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
-- new color map
---@param args colormap_args
---@return graphics_element element, element_id id
local function colormap(args)
local bkg = "008877FFCCEE114455DD9933BBAA2266"
local spaces = util.spaces(32)
args.width = 32
args.height = 1
-- create new graphics element base object
local e = element.new(args)
-- draw color map
e.window.setCursorPos(1, 1)
e.window.blit(spaces, bkg, bkg)
return e.get()
end
return colormap

View File

@@ -0,0 +1,121 @@
-- Button Graphics Element
local element = require("graphics.element")
local util = require("scada-common.util")
---@class button_option
---@field text string
---@field fg_bg cpair
---@field active_fg_bg cpair
---@field _lpad integer automatically calculated left pad
---@field _start_x integer starting touch x range (inclusive)
---@field _end_x integer ending touch x range (inclusive)
---@class multi_button_args
---@field options table button options
---@field callback function function to call on touch
---@field default? boolean default state, defaults to options[1]
---@field min_width? integer text length + 2 if omitted
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
-- new multi button (latch selection, exclusively one button at a time)
---@param args multi_button_args
---@return graphics_element element, element_id id
local function multi_button(args)
assert(type(args.options) == "table", "graphics.elements.controls.multi_button: options is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.multi_button: callback is a required field")
-- single line
args.height = 3
-- determine widths
local max_width = 1
for i = 1, #args.options do
local opt = args.options[i] ---@type button_option
if string.len(opt.text) > max_width then
max_width = string.len(opt.text)
end
end
local button_width = math.max(max_width, args.min_width or 1)
args.width = (button_width * #args.options) + #args.options + 1
-- create new graphics element base object
local e = element.new(args)
-- button state (convert nil to 1 if missing)
e.value = args.default or 1
-- calculate required button information
local next_x = 2
for i = 1, #args.options do
local opt = args.options[i] ---@type button_option
local w = string.len(opt.text)
opt._lpad = math.floor((e.frame.w - w) / 2)
opt._start_x = next_x
opt._end_x = next_x + button_width - 1
next_x = next_x + (button_width + 1)
end
-- show the button state
local function draw()
for i = 1, #args.options do
local opt = args.options[i] ---@type button_option
e.window.setCursorPos(opt._start_x, 2)
if e.value == i then
-- show as pressed
e.window.setTextColor(opt.active_fg_bg.fgd)
e.window.setBackgroundColor(opt.active_fg_bg.bkg)
else
-- show as unpressed
e.window.setTextColor(opt.fg_bg.fgd)
e.window.setBackgroundColor(opt.fg_bg.bkg)
end
e.window.write(util.pad(opt.text, button_width))
end
end
-- handle touch
---@param event monitor_touch monitor touch event
function e.handle_touch(event)
-- determine what was pressed
if event.y == 2 then
for i = 1, #args.options do
local opt = args.options[i] ---@type button_option
if event.x >= opt._start_x and event.x <= opt._end_x then
e.value = i
draw()
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()
args.callback(e.value)
end
-- initial draw
draw()
return e.get()
end
return multi_button

View File

@@ -0,0 +1,86 @@
-- Button Graphics Element
local tcd = require("scada-common.tcallbackdsp")
local core = require("graphics.core")
local element = require("graphics.element")
---@class push_button_args
---@field text string button text
---@field callback function function to call on touch
---@field min_width? integer text length + 2 if omitted
---@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 1 if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
-- new push button
---@param args push_button_args
---@return graphics_element element, element_id id
local function push_button(args)
assert(type(args.text) == "string", "graphics.elements.controls.push_button: text is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.push_button: callback is a required field")
-- single line
args.height = 1
args.min_width = args.min_width or 0
local text_width = string.len(args.text)
args.width = math.max(text_width + 2, args.min_width)
-- create new graphics element base object
local e = element.new(args)
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1
local v_pad = math.floor(e.frame.h / 2) + 1
-- draw the button
local function draw()
e.window.clear()
-- write the button text
e.window.setCursorPos(h_pad, v_pad)
e.window.write(args.text)
end
-- handle touch
---@param event monitor_touch monitor touch event
---@diagnostic disable-next-line: unused-local
function e.handle_touch(event)
if args.active_fg_bg ~= nil then
-- show as pressed
e.value = true
e.window.setTextColor(args.active_fg_bg.fgd)
e.window.setBackgroundColor(args.active_fg_bg.bkg)
draw()
-- show as unpressed in 0.25 seconds
tcd.dispatch(0.25, function ()
e.value = false
e.window.setTextColor(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg)
draw()
end)
end
-- call the touch callback
args.callback()
end
-- set the value
---@param val boolean new value
function e.set_value(val)
if val then e.handle_touch(core.events.touch("", 1, 1)) end
end
-- initial draw
draw()
return e.get()
end
return push_button

View File

@@ -0,0 +1,76 @@
-- SCRAM Button Graphics Element
local tcd = require("scada-common.tcallbackdsp")
local core = require("graphics.core")
local element = require("graphics.element")
---@class scram_button_args
---@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 1 if omitted
---@field fg_bg? cpair foreground/background colors
-- new scram button
---@param args scram_button_args
---@return graphics_element element, element_id id
local function scram_button(args)
assert(type(args.callback) == "function", "graphics.elements.controls.scram_button: callback is a required field")
-- static dimensions
args.height = 3
args.width = 9
-- create new graphics element base object
local e = element.new(args)
-- write the button text
e.window.setCursorPos(3, 2)
e.window.write("SCRAM")
-- draw border
-- top
e.window.setTextColor(colors.yellow)
e.window.setBackgroundColor(args.fg_bg.bkg)
e.window.setCursorPos(1, 1)
e.window.write("\x99\x89\x89\x89\x89\x89\x89\x89\x99")
-- center left
e.window.setCursorPos(1, 2)
e.window.setTextColor(args.fg_bg.bkg)
e.window.setBackgroundColor(colors.yellow)
e.window.write("\x99")
-- center right
e.window.setTextColor(args.fg_bg.bkg)
e.window.setBackgroundColor(colors.yellow)
e.window.setCursorPos(9, 2)
e.window.write("\x99")
-- bottom
e.window.setTextColor(colors.yellow)
e.window.setBackgroundColor(args.fg_bg.bkg)
e.window.setCursorPos(1, 3)
e.window.write("\x99\x98\x98\x98\x98\x98\x98\x98\x99")
-- handle touch
---@param event monitor_touch monitor touch event
---@diagnostic disable-next-line: unused-local
function e.handle_touch(event)
-- call the touch callback
args.callback()
end
-- set the value
---@param val boolean new value
function e.set_value(val)
if val then e.handle_touch(core.events.touch("", 1, 1)) end
end
return e.get()
end
return scram_button

View File

@@ -0,0 +1,145 @@
-- Spinbox Numeric Graphics Element
local element = require("graphics.element")
local util = require("scada-common.util")
---@class spinbox_args
---@field default? number default value, defaults to 0.0
---@field whole_num_precision integer number of whole number digits
---@field fractional_precision integer number of fractional digits
---@field arrow_fg_bg cpair arrow foreground/background colors
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
-- new spinbox control (minimum value is 0)
---@param args spinbox_args
---@return graphics_element element, element_id id
local function spinbox(args)
-- properties
local digits = {}
local wn_prec = args.whole_num_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")
assert(util.is_int(fr_prec), "graphics.element.controls.spinbox_numeric: fractional precision must be an integer")
local fmt = "%" .. (wn_prec + fr_prec + 1) .. "." .. fr_prec .. "f"
local fmt_init = "%0" .. (wn_prec + fr_prec + 1) .. "." .. fr_prec .. "f"
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")
-- determine widths
args.width = wn_prec + fr_prec + util.trinary(fr_prec > 0, 1, 0)
args.height = 3
-- create new graphics element base object
local e = element.new(args)
-- set initial value
e.value = args.default or 0.0
local initial_str = util.sprintf(fmt_init, e.value)
---@diagnostic disable-next-line: discard-returns
initial_str:gsub("%d", function (char) table.insert(digits, char) end)
-- draw the arrows
e.window.setBackgroundColor(args.arrow_fg_bg.bkg)
e.window.setTextColor(args.arrow_fg_bg.fgd)
e.window.setCursorPos(1, 1)
e.window.write(util.strrep("\x1e", wn_prec))
e.window.setCursorPos(1, 3)
e.window.write(util.strrep("\x1f", wn_prec))
if fr_prec > 0 then
e.window.setCursorPos(1 + wn_prec, 1)
e.window.write(" " .. util.strrep("\x1e", fr_prec))
e.window.setCursorPos(1 + wn_prec, 3)
e.window.write(" " .. util.strrep("\x1f", fr_prec))
end
-- zero the value
local function zero()
for i = 1, #digits do digits[i] = 0 end
e.value = 0
end
-- print out the current value
local function show_num()
e.window.setBackgroundColor(e.fg_bg.bkg)
e.window.setTextColor(e.fg_bg.fgd)
e.window.setCursorPos(1, 2)
e.window.write(util.sprintf(fmt, e.value))
end
-- update the value per digits table
local function update_value()
e.value = 0
for i = 1, #digits do
local pow = math.abs(wn_prec - i)
if i <= wn_prec then
e.value = e.value + (digits[i] * (10 ^ pow))
else
e.value = e.value + (digits[i] * (10 ^ -pow))
end
end
end
-- enforce numeric limits
local function enforce_limits()
-- min 0
if e.value < 0 then
zero()
-- max printable
elseif string.len(util.sprintf(fmt, e.value)) > args.width then
-- max out
for i = 1, #digits do digits[i] = 9 end
-- re-update value
update_value()
end
end
-- update value and show
local function parse_and_show()
update_value()
enforce_limits()
show_num()
end
-- init with the default value
show_num()
-- handle touch
---@param event monitor_touch monitor touch event
function e.handle_touch(event)
-- only handle if on an increment or decrement arrow
if event.x ~= dec_point_x then
local idx = util.trinary(event.x > dec_point_x, event.x - 1, event.x)
if event.y == 1 then
-- increment
digits[idx] = digits[idx] + 1
elseif event.y == 3 then
-- decrement
digits[idx] = digits[idx] - 1
end
parse_and_show()
end
end
-- set the value
---@param val number number to show
function e.set_value(val)
e.value = val
parse_and_show()
end
return e.get()
end
return spinbox

View File

@@ -0,0 +1,76 @@
-- SCRAM Button Graphics Element
local tcd = require("scada-common.tcallbackdsp")
local core = require("graphics.core")
local element = require("graphics.element")
---@class start_button_args
---@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 1 if omitted
---@field fg_bg? cpair foreground/background colors
-- new start button
---@param args start_button_args
---@return graphics_element element, element_id id
local function start_button(args)
assert(type(args.callback) == "function", "graphics.elements.controls.start_button: callback is a required field")
-- static dimensions
args.height = 3
args.width = 9
-- create new graphics element base object
local e = element.new(args)
-- write the button text
e.window.setCursorPos(3, 2)
e.window.write("START")
-- draw border
-- top
e.window.setTextColor(colors.orange)
e.window.setBackgroundColor(args.fg_bg.bkg)
e.window.setCursorPos(1, 1)
e.window.write("\x99\x89\x89\x89\x89\x89\x89\x89\x99")
-- center left
e.window.setCursorPos(1, 2)
e.window.setTextColor(args.fg_bg.bkg)
e.window.setBackgroundColor(colors.orange)
e.window.write("\x99")
-- center right
e.window.setTextColor(args.fg_bg.bkg)
e.window.setBackgroundColor(colors.orange)
e.window.setCursorPos(9, 2)
e.window.write("\x99")
-- bottom
e.window.setTextColor(colors.orange)
e.window.setBackgroundColor(args.fg_bg.bkg)
e.window.setCursorPos(1, 3)
e.window.write("\x99\x98\x98\x98\x98\x98\x98\x98\x99")
-- handle touch
---@param event monitor_touch monitor touch event
---@diagnostic disable-next-line: unused-local
function e.handle_touch(event)
-- call the touch callback
args.callback()
end
-- set the value
---@param val boolean new value
function e.set_value(val)
if val then e.handle_touch(core.events.touch("", 1, 1)) end
end
return e.get()
end
return start_button

View File

@@ -0,0 +1,88 @@
-- Button Graphics Element
local element = require("graphics.element")
---@class switch_button_args
---@field text string button text
---@field callback function function to call on touch
---@field default? boolean default state, defaults to off (false)
---@field min_width? integer text length + 2 if omitted
---@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 1 if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
-- new switch button (latch high/low)
---@param args switch_button_args
---@return graphics_element element, element_id id
local function switch_button(args)
assert(type(args.text) == "string", "graphics.elements.controls.switch_button: text is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.switch_button: callback is a required field")
assert(type(args.active_fg_bg) == "table", "graphics.elements.controls.switch_button: active_fg_bg is a required field")
-- single line
args.height = 1
-- determine widths
local text_width = string.len(args.text)
args.width = math.max(text_width + 2, args.min_width)
-- create new graphics element base object
local e = element.new(args)
-- button state (convert nil to false if missing)
e.value = args.default or false
local h_pad = math.floor((e.frame.w - text_width) / 2)
local v_pad = math.floor(e.frame.h / 2) + 1
-- show the button state
local function draw_state()
if e.value then
-- show as pressed
e.window.setTextColor(args.active_fg_bg.fgd)
e.window.setBackgroundColor(args.active_fg_bg.bkg)
else
-- show as unpressed
e.window.setTextColor(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg)
end
-- write the button text
e.window.setCursorPos(h_pad, v_pad)
e.window.write(args.text)
end
-- initial draw
draw_state()
-- handle touch
---@param event monitor_touch monitor touch event
---@diagnostic disable-next-line: unused-local
function e.handle_touch(event)
-- toggle state
e.value = not e.value
draw_state()
-- call the touch callback with state
args.callback(e.value)
end
-- set the value
---@param val boolean new value
function e.set_value(val)
-- set state
e.value = val
draw_state()
-- call the touch callback with state
args.callback(e.value)
end
return e.get()
end
return switch_button

View File

@@ -0,0 +1,21 @@
-- Root Display Box Graphics Element
local element = require("graphics.element")
---@class displaybox_args
---@field window table
---@field x? integer 1 if omitted
---@field y? integer 1 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
-- new root display box
---@param args displaybox_args
local function displaybox(args)
-- create new graphics element base object
return element.new(args).get()
end
return displaybox

23
graphics/elements/div.lua Normal file
View File

@@ -0,0 +1,23 @@
-- Div (Division, like in HTML) Graphics Element
local element = require("graphics.element")
---@class div_args
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 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
-- new div element
---@param args div_args
---@return graphics_element element, element_id id
local function div(args)
-- create new graphics element base object
return element.new(args).get()
end
return div

View File

@@ -0,0 +1,165 @@
-- Reactor Core View Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
---@class core_map_args
---@field reactor_l integer reactor length
---@field reactor_w integer reactor width
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
-- new core map box
---@param args core_map_args
---@return graphics_element element, element_id id
local function core_map(args)
assert(util.is_int(args.reactor_l), "graphics.elements.indicators.coremap: reactor_l is a required field")
assert(util.is_int(args.reactor_w), "graphics.elements.indicators.coremap: reactor_w is a required field")
-- require max dimensions
args.width = 18
args.height = 18
-- inherit only foreground color
args.fg_bg = core.graphics.cpair(args.parent.get_fg_bg().fgd, colors.gray)
-- create new graphics element base object
local e = element.new(args)
local alternator = true
local core_l = args.reactor_l - 2
local core_w = args.reactor_w - 2
local shift_x = 8 - math.floor(core_l / 2)
local shift_y = 8 - math.floor(core_w / 2)
local start_x = 2 + shift_x
local start_y = 2 + shift_y
local inner_width = core_l
local inner_height = core_w
-- create coordinate grid and frame
local function draw_frame()
e.window.setTextColor(colors.white)
for x = 0, (inner_width - 1) do
e.window.setCursorPos(x + start_x, 1)
e.window.write(util.sprintf("%X", x))
end
for y = 0, (inner_height - 1) do
e.window.setCursorPos(1, y + start_y)
e.window.write(util.sprintf("%X", y))
end
-- even out bottom edge
e.window.setTextColor(e.fg_bg.bkg)
e.window.setBackgroundColor(args.parent.get_fg_bg().bkg)
e.window.setCursorPos(1, e.frame.h)
e.window.write(util.strrep("\x8f", e.frame.w))
e.window.setTextColor(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg)
end
-- draw the core
---@param t number temperature in K
local function draw_core(t)
local i = 1
local back_c = "F"
local text_c = "8"
-- determine fuel assembly coloring
if t <= 300 then
-- gray
text_c = "8"
elseif t <= 350 then
-- blue
text_c = "3"
elseif t < 600 then
-- green
text_c = "D"
elseif t < 1000 then
-- yellow
text_c = "4"
-- back_c = "8"
elseif t < 1200 then
-- orange
text_c = "1"
elseif t < 1300 then
-- red
text_c = "E"
else
-- pink
text_c = "2"
end
-- draw pattern
for y = start_y, inner_height + (start_y - 1) do
e.window.setCursorPos(start_x, y)
for _ = 1, inner_width do
if alternator then
i = i + 1
e.window.blit("\x07", text_c, back_c)
else
e.window.blit("\x07", "7", "8")
end
alternator = not alternator
end
if inner_width % 2 == 0 then alternator = not alternator end
end
end
-- on state change
---@param temperature number temperature in Kelvin
function e.on_update(temperature)
e.value = temperature
draw_core(e.value)
end
-- set temperature to display
---@param val number degrees K
function e.set_value(val) e.on_update(val) end
-- resize reactor dimensions
---@param reactor_l integer reactor length (rendered in 2D top-down as width)
---@param reactor_w integer reactor width (rendered in 2D top-down as height)
function e.resize(reactor_l, reactor_w)
-- enforce possible dimensions
if reactor_l > 18 then reactor_l = 18 elseif reactor_l < 3 then reactor_l = 3 end
if reactor_w > 18 then reactor_w = 18 elseif reactor_w < 3 then reactor_w = 3 end
-- update dimensions
core_l = reactor_l - 2
core_w = reactor_w - 2
shift_x = 8 - math.floor(core_l / 2)
shift_y = 8 - math.floor(core_w / 2)
start_x = 2 + shift_x
start_y = 2 + shift_y
inner_width = core_l
inner_height = core_w
e.window.clear()
-- re-draw
draw_frame()
e.on_update(e.value)
end
-- initial (one-time except for resize()) frame draw
draw_frame()
-- initial draw
e.on_update(0)
return e.get()
end
return core_map

View File

@@ -0,0 +1,105 @@
-- Data Indicator Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
-- format a number string with commas as the thousands separator
--
-- subtracts from spaces at the start if present for each comma used
---@param num string number string
---@return string
local function comma_format(num)
local formatted = num
local commas = 0
local i = 1
while i > 0 do
formatted, i = formatted:gsub("^(%s-%d+)(%d%d%d)", '%1,%2')
if i > 0 then commas = commas + 1 end
end
local _, num_spaces = formatted:gsub(" %s-", "")
local remove = math.min(num_spaces, commas)
formatted = string.sub(formatted, remove + 1)
return formatted
end
---@class data_indicator_args
---@field label string indicator label
---@field unit? string indicator unit
---@field format string data format (lua string format)
---@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 value any default value
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field width integer length
---@field fg_bg? cpair foreground/background colors
-- new data indicator
---@param args data_indicator_args
---@return graphics_element element, element_id id
local function data(args)
assert(type(args.label) == "string", "graphics.elements.indicators.data: label is a required field")
assert(type(args.format) == "string", "graphics.elements.indicators.data: format is a required field")
assert(args.value ~= nil, "graphics.elements.indicators.data: value is a required field")
assert(util.is_int(args.width), "graphics.elements.indicators.data: width is a required field")
-- single line
args.height = 1
-- create new graphics element base object
local e = element.new(args)
-- label color
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 data_start = string.len(args.label) + 2
-- on state change
---@param value any new value
function e.on_update(value)
e.value = value
local data_str = util.sprintf(args.format, value)
-- write data
e.window.setCursorPos(data_start, 1)
e.window.setTextColor(e.fg_bg.fgd)
if args.commas then
e.window.write(comma_format(data_str))
else
e.window.write(data_str)
end
-- write label
if args.unit ~= nil then
if args.lu_colors ~= nil then
e.window.setTextColor(args.lu_colors.color_b)
end
e.window.write(" " .. args.unit)
end
end
-- set the value
---@param val any new value
function e.set_value(val) e.on_update(val) end
-- initial value draw
e.on_update(args.value)
return e.get()
end
return data

View File

@@ -0,0 +1,123 @@
-- Horizontal Bar Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class hbar_args
---@field show_percent? boolean whether or not to show the percent
---@field bar_fg_bg? cpair bar foreground/background colors if showing percent
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 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
-- new horizontal bar
---@param args hbar_args
---@return graphics_element element, element_id id
---@return graphics_element element, element_id id
local function hbar(args)
-- properties/state
local last_num_bars = -1
-- create new graphics element base object
local e = element.new(args)
-- 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)
assert(bar_width > 0, "graphics.elements.indicators.hbar: too small for bar")
-- determine bar colors
local bar_bkg = e.fg_bg.blit_bkg
local bar_fgd = e.fg_bg.blit_fgd
if args.bar_fg_bg ~= nil then
bar_bkg = args.bar_fg_bg.blit_bkg
bar_fgd = args.bar_fg_bg.blit_fgd
end
-- handle data changes
---@param fraction number 0.0 to 1.0
function e.on_update(fraction)
e.value = fraction
-- enforce minimum and maximum
if fraction < 0 then
fraction = 0.0
elseif fraction > 1 then
fraction = 1.0
end
-- compute number of bars
local num_bars = util.round(fraction * (bar_width * 2))
-- redraw bar if changed
if num_bars ~= last_num_bars then
last_num_bars = num_bars
local fgd = ""
local bkg = ""
local spaces = ""
-- fill percentage
for _ = 1, num_bars / 2 do
spaces = spaces .. " "
fgd = fgd .. bar_fgd
bkg = bkg .. bar_bkg
end
-- add fractional bar if needed
if num_bars % 2 == 1 then
spaces = spaces .. "\x95"
fgd = fgd .. bar_bkg
bkg = bkg .. bar_fgd
end
-- pad background
for _ = 1, ((bar_width * 2) - num_bars) / 2 do
spaces = spaces .. " "
fgd = fgd .. bar_bkg
bkg = bkg .. bar_bkg
end
-- draw bar
for y = 1, e.frame.h do
e.window.setCursorPos(1, y)
-- intentionally swapped fgd/bkg since we use spaces as fill, but they are the opposite
e.window.blit(spaces, bkg, fgd)
end
end
-- update percentage
if args.show_percent then
e.window.setCursorPos(bar_width + 2, math.max(1, math.ceil(e.frame.h / 2)))
e.window.write(util.sprintf("%3.0f%%", fraction * 100))
end
end
-- change bar color
---@param bar_fg_bg cpair new bar colors
function e.recolor(bar_fg_bg)
bar_bkg = bar_fg_bg.blit_bkg
bar_fgd = bar_fg_bg.blit_fgd
-- re-draw
last_num_bars = 0
e.on_update(e.value)
end
-- set the percentage value
---@param val number 0.0 to 1.0
function e.set_value(val) e.on_update(val) end
-- initialize to 0
e.on_update(0)
return e.get()
end
return hbar

View File

@@ -0,0 +1,73 @@
-- Icon Indicator Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class icon_sym_color
---@field color cpair
---@field symbol string
---@class icon_indicator_args
---@field label string indicator label
---@field states table state color and symbol table
---@field value? integer default state, defaults to 1
---@field min_label_width? integer label length if omitted
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
-- new icon indicator
---@param args icon_indicator_args
---@return graphics_element element, element_id id
local function icon(args)
assert(type(args.label) == "string", "graphics.elements.indicators.icon: label is a required field")
assert(type(args.states) == "table", "graphics.elements.indicators.icon: states is a required field")
-- single line
args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4
-- create new graphics element base object
local e = element.new(args)
-- state blit strings
local state_blit_cmds = {}
for i = 1, #args.states do
local sym_color = args.states[i] ---@type icon_sym_color
table.insert(state_blit_cmds, {
text = " " .. sym_color.symbol .. " ",
fgd = util.strrep(sym_color.color.blit_fgd, 3),
bkg = util.strrep(sym_color.color.blit_bkg, 3)
})
end
-- write label and initial indicator light
e.window.setCursorPos(5, 1)
e.window.write(args.label)
-- on state change
---@param new_state integer indicator state
function e.on_update(new_state)
local blit_cmd = state_blit_cmds[new_state]
e.value = new_state
e.window.setCursorPos(1, 1)
e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
end
-- set indicator state
---@param val integer indicator state
function e.set_value(val) e.on_update(val) end
-- initial icon draw
e.on_update(args.value or 1)
return e.get()
end
return icon

View File

@@ -0,0 +1,54 @@
-- Indicator Light Graphics Element
local element = require("graphics.element")
---@class indicator_light_args
---@field label string indicator label
---@field colors cpair on/off colors (a/b respectively)
---@field min_label_width? integer label length if omitted
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
-- new indicator light
---@param args indicator_light_args
---@return graphics_element element, element_id id
local function indicator_light(args)
assert(type(args.label) == "string", "graphics.elements.indicators.light: label is a required field")
assert(type(args.colors) == "table", "graphics.elements.indicators.light: colors is a required field")
-- single line
args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
-- create new graphics element base object
local e = element.new(args)
-- on state change
---@param new_state boolean indicator state
function e.on_update(new_state)
e.value = new_state
e.window.setCursorPos(1, 1)
if new_state then
e.window.blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
else
e.window.blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
end
end
-- set indicator state
---@param val boolean indicator state
function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light
e.on_update(false)
e.window.write(args.label)
return e.get()
end
return indicator_light

View File

@@ -0,0 +1,79 @@
-- State (Text) Indicator Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class state_text_color
---@field color cpair
---@field text string
---@class state_indicator_args
---@field states table state color and text table
---@field value? integer default state, defaults to 1
---@field min_width? integer max state text length if omitted
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field height? integer 1 if omitted, must be an odd number
---@field fg_bg? cpair foreground/background colors
-- new state indicator
---@param args state_indicator_args
---@return graphics_element element, element_id id
local function state_indicator(args)
assert(type(args.states) == "table", "graphics.elements.indicators.state: states is a required field")
-- determine height
if util.is_int(args.height) then
assert(args.height % 2 == 1, "graphics.elements.indicators.state: height should be an odd number")
else
args.height = 1
end
-- initial guess at width
args.width = args.min_width or 1
-- state blit strings
local state_blit_cmds = {}
for i = 1, #args.states do
local state_def = args.states[i] ---@type state_text_color
-- re-determine width
if string.len(state_def.text) > args.width then
args.width = string.len(state_def.text)
end
local text = util.pad(state_def.text, args.width)
table.insert(state_blit_cmds, {
text = text,
fgd = util.strrep(state_def.color.blit_fgd, string.len(text)),
bkg = util.strrep(state_def.color.blit_bkg, string.len(text))
})
end
-- create new graphics element base object
local e = element.new(args)
-- on state change
---@param new_state integer indicator state
function e.on_update(new_state)
local blit_cmd = state_blit_cmds[new_state]
e.value = new_state
e.window.setCursorPos(1, 1)
e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
end
-- set indicator state
---@param val integer indicator state
function e.set_value(val) e.on_update(val) end
-- initial draw
e.on_update(args.value or 1)
return e.get()
end
return state_indicator

View File

@@ -0,0 +1,65 @@
-- Tri-State Indicator Light Graphics Element
local element = require("graphics.element")
---@class tristate_indicator_light_args
---@field label string indicator label
---@field c1 color color for state 1
---@field c2 color color for state 2
---@field c3 color color for state 3
---@field min_label_width? integer label length if omitted
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
-- new indicator light
---@param args tristate_indicator_light_args
---@return graphics_element element, element_id id
local function tristate_indicator_light(args)
assert(type(args.label) == "string", "graphics.elements.indicators.trilight: label is a required field")
assert(type(args.c1) == "number", "graphics.elements.indicators.trilight: c1 is a required field")
assert(type(args.c2) == "number", "graphics.elements.indicators.trilight: c2 is a required field")
assert(type(args.c3) == "number", "graphics.elements.indicators.trilight: c3 is a required field")
-- single line
args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
-- 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
local e = element.new(args)
-- on state change
---@param new_state integer indicator state
function e.on_update(new_state)
e.value = new_state
e.window.setCursorPos(1, 1)
if new_state == 2 then
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
elseif new_state == 3 then
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
else
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end
end
-- set indicator state
---@param val integer indicator state
function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light
e.on_update(0)
e.window.write(args.label)
return e.get()
end
return tristate_indicator_light

View File

@@ -0,0 +1,102 @@
-- Vertical Bar Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class vbar_args
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 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
-- new vertical bar
---@param args vbar_args
---@return graphics_element element, element_id id
local function vbar(args)
-- properties/state
local last_num_bars = -1
-- create new graphics element base object
local e = element.new(args)
-- blit strings
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 spaces = util.spaces(e.frame.w)
local one_third = util.strrep("\x8f", e.frame.w)
local two_thirds = util.strrep("\x83", e.frame.w)
-- handle data changes
---@param fraction number 0.0 to 1.0
function e.on_update(fraction)
e.value = fraction
-- enforce minimum and maximum
if fraction < 0 then
fraction = 0.0
elseif fraction > 1 then
fraction = 1.0
end
-- compute number of bars
local num_bars = util.round(fraction * (e.frame.h * 3))
-- redraw only if number of bars has changed
if num_bars ~= last_num_bars then
last_num_bars = num_bars
-- start bottom up
local y = e.frame.h
-- start at base of vertical bar
e.window.setCursorPos(1, y)
-- fill percentage
for _ = 1, num_bars / 3 do
e.window.blit(spaces, bkg, fgd)
y = y - 1
e.window.setCursorPos(1, y)
end
-- add fractional bar if needed
if num_bars % 3 == 1 then
e.window.blit(one_third, bkg, fgd)
y = y - 1
elseif num_bars % 3 == 2 then
e.window.blit(two_thirds, bkg, fgd)
y = y - 1
end
-- fill the rest blank
while y > 0 do
e.window.setCursorPos(1, y)
e.window.blit(spaces, fgd, bkg)
y = y - 1
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
e.on_update(e.value)
end
-- set the percentage value
---@param val number 0.0 to 1.0
function e.set_value(val) e.on_update(val) end
return e.get()
end
return vbar

View File

@@ -0,0 +1,147 @@
-- Pipe Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
---@class pipenet_args
---@field pipes table pipe list
---@field bg? color background color
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
-- new pipe network
---@param args pipenet_args
---@return graphics_element element, element_id id
local function pipenet(args)
assert(type(args.pipes) == "table", "graphics.elements.indicators.pipenet: pipes is a required field")
args.width = 0
args.height = 0
-- determine width/height
for i = 1, #args.pipes do
local pipe = args.pipes[i] ---@type pipe
local true_w = pipe.w + math.min(pipe.x1, pipe.x2)
local true_h = pipe.h + math.min(pipe.y1, pipe.y2)
if true_w > args.width then args.width = true_w end
if true_h > args.height then args.height = true_h end
end
args.x = args.x or 1
args.y = args.y or 1
if args.bg ~= nil then
args.fg_bg = core.graphics.cpair(args.bg, args.bg)
end
-- create new graphics element base object
local e = element.new(args)
-- draw all pipes
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)
e.window.setCursorPos(x, y)
local c = core.graphics.cpair(pipe.color, e.fg_bg.bkg)
if pipe.align_tr then
-- 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
-- back up one
x = x - x_step
for _ = 1, pipe.h - 1 do
y = y + y_step
e.window.setCursorPos(x, y)
if pipe.thin then
e.window.blit("\x95", c.blit_bkg, c.blit_fgd)
else
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
end
end
else
-- cross height then width
for i = 1, pipe.h do
if pipe.thin then
if i == pipe.h then
-- corner
if y_step < 0 then
e.window.blit("\x97", c.blit_bkg, c.blit_fgd)
else
e.window.blit("\x8d", c.blit_fgd, c.blit_bkg)
end
else
e.window.blit("\x95", c.blit_fgd, c.blit_bkg)
end
else
if i == pipe.h and y_step < 0 then
-- corner
e.window.blit("\x83", c.blit_bkg, c.blit_fgd)
else
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
end
end
y = y + y_step
e.window.setCursorPos(x, y)
end
-- back up one
y = y - y_step
for _ = 1, pipe.w - 1 do
x = x + x_step
e.window.setCursorPos(x, y)
if pipe.thin then
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg)
else
e.window.blit("\x83", c.blit_bkg, c.blit_fgd)
end
end
end
end
return e.get()
end
return pipenet

View File

@@ -0,0 +1,116 @@
-- Rectangle Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class rectangle_args
---@field border? graphics_border
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 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
-- new rectangle
---@param args rectangle_args
---@return graphics_element element, element_id id
local function rectangle(args)
-- offset children
if args.border ~= nil then
args.offset_x = args.border.width
args.offset_y = args.border.width
-- slightly different y offset if the border is set to even
if args.border.even then
local width_x2 = (2 * args.border.width)
args.offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0)
end
end
-- create new graphics element base object
local e = element.new(args)
-- draw bordered box if requested
-- element constructor will have drawn basic colored rectangle regardless
if args.border ~= nil then
e.window.setCursorPos(1, 1)
local border_width = args.offset_x
local border_height = args.offset_y
local border_blit = colors.toBlit(args.border.color)
local width_x2 = border_width * 2
local inner_width = e.frame.w - width_x2
-- check dimensions
assert(width_x2 <= e.frame.w, "graphics.elements.rectangle: border too thick for width")
assert(width_x2 <= e.frame.h, "graphics.elements.rectangle: border too thick for height")
-- form the basic line strings and top/bottom blit strings
local spaces = util.spaces(e.frame.w)
local blit_fg = util.strrep(e.fg_bg.blit_fgd, e.frame.w)
local blit_bg_sides = ""
local blit_bg_top_bot = util.strrep(border_blit, e.frame.w)
-- partial bars
local p_a = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width)
local p_b = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width)
local p_inv_fg = util.strrep(border_blit, border_width) .. util.strrep(e.fg_bg.blit_bkg, inner_width) ..
util.strrep(border_blit, border_width)
local p_inv_bg = util.strrep(e.fg_bg.blit_bkg, border_width) .. util.strrep(border_blit, inner_width) ..
util.strrep(e.fg_bg.blit_bkg, border_width)
-- form the body blit strings (sides are border, inside is normal)
for x = 1, e.frame.w do
-- edges get border color, center gets normal
if x <= border_width or x > (e.frame.w - border_width) then
blit_bg_sides = blit_bg_sides .. border_blit
else
blit_bg_sides = blit_bg_sides .. e.fg_bg.blit_bkg
end
end
-- draw rectangle with borders
for y = 1, e.frame.h do
e.window.setCursorPos(1, y)
if y <= border_height then
-- partial pixel fill
if args.border.even and y == border_height then
if width_x2 % 3 == 1 then
e.window.blit(p_b, p_inv_bg, p_inv_fg)
elseif width_x2 % 3 == 2 then
e.window.blit(p_a, p_inv_bg, p_inv_fg)
else
-- skip line
e.window.blit(spaces, blit_fg, blit_bg_sides)
end
else
e.window.blit(spaces, blit_fg, blit_bg_top_bot)
end
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 width_x2 % 3 == 1 then
e.window.blit(p_a, p_inv_fg, blit_bg_top_bot)
elseif width_x2 % 3 == 2 then
e.window.blit(p_b, p_inv_fg, blit_bg_top_bot)
else
-- skip line
e.window.blit(spaces, blit_fg, blit_bg_sides)
end
else
e.window.blit(spaces, blit_fg, blit_bg_top_bot)
end
else
e.window.blit(spaces, blit_fg, blit_bg_sides)
end
end
end
return e.get()
end
return rectangle

View File

@@ -0,0 +1,70 @@
-- Text Box Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
---@class textbox_args
---@field text string text to show
---@field alignment? TEXT_ALIGN text alignment, left by default
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 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
-- new text box
---@param args textbox_args
---@return graphics_element element, element_id id
local function textbox(args)
assert(type(args.text) == "string", "graphics.elements.textbox: text is a required field")
-- create new graphics element base object
local e = element.new(args)
local alignment = args.alignment or TEXT_ALIGN.LEFT
-- draw textbox
local function display_text(text)
e.value = text
local lines = util.strwrap(text, e.frame.w)
for i = 1, #lines do
if i > e.frame.h then break end
local len = string.len(lines[i])
-- use cursor position to align this line
if alignment == TEXT_ALIGN.CENTER then
e.window.setCursorPos(math.floor((e.frame.w - len) / 2) + 1, i)
elseif alignment == TEXT_ALIGN.RIGHT then
e.window.setCursorPos((e.frame.w - len) + 1, i)
else
e.window.setCursorPos(1, i)
end
e.window.write(lines[i])
end
end
display_text(args.text)
-- set the string value and re-draw the text
---@param val string value
function e.set_value(val)
e.window.clear()
display_text(val)
end
return e.get()
end
return textbox

View File

@@ -0,0 +1,87 @@
-- "Basketweave" Tiling Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class tiling_args
---@field fill_c cpair colors to fill with
---@field even? boolean whether to account for rectangular pixels
---@field border_c? color optional frame color
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 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
-- new tiling box
---@param args tiling_args
---@return graphics_element element, element_id id
local function tiling(args)
assert(type(args.fill_c) == "table", "graphics.elements.tiling: fill_c is a required field")
-- create new graphics element base object
local e = element.new(args)
-- draw tiling box
local fill_a = args.fill_c.blit_a
local fill_b = args.fill_c.blit_b
local even = args.even == true
local start_x = 1
local start_y = 1
local inner_width = math.floor(e.frame.w / util.trinary(even, 2, 1))
local inner_height = e.frame.h
local alternator = true
-- border
if args.border_c ~= nil then
e.window.setBackgroundColor(args.border_c)
e.window.clear()
start_x = 1 + util.trinary(even, 2, 1)
start_y = 2
inner_width = math.floor((e.frame.w - 2 * util.trinary(even, 2, 1)) / util.trinary(even, 2, 1))
inner_height = e.frame.h - 2
end
-- check dimensions
assert(inner_width > 0, "graphics.elements.tiling: inner_width <= 0")
assert(inner_height > 0, "graphics.elements.tiling: inner_height <= 0")
assert(start_x <= inner_width, "graphics.elements.tiling: start_x > inner_width")
assert(start_y <= inner_height, "graphics.elements.tiling: start_y > inner_height")
-- create pattern
for y = start_y, inner_height + (start_y - 1) do
e.window.setCursorPos(start_x, y)
for x = 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
end
if inner_width % 2 == 0 then alternator = not alternator end
end
return e.get()
end
return tiling

View File

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

22
lockbox/LICENSE Normal file
View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 James L.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

415
lockbox/cipher/aes128.lua Normal file
View File

@@ -0,0 +1,415 @@
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;

462
lockbox/cipher/aes192.lua Normal file
View File

@@ -0,0 +1,462 @@
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;

498
lockbox/cipher/aes256.lua Normal file
View File

@@ -0,0 +1,498 @@
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;

164
lockbox/cipher/mode/cbc.lua Normal file
View File

@@ -0,0 +1,164 @@
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;

163
lockbox/cipher/mode/cfb.lua Normal file
View File

@@ -0,0 +1,163 @@
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;

248
lockbox/cipher/mode/ctr.lua Normal file
View File

@@ -0,0 +1,248 @@
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;

164
lockbox/cipher/mode/ofb.lua Normal file
View File

@@ -0,0 +1,164 @@
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;

173
lockbox/digest/sha1.lua Normal file
View File

@@ -0,0 +1,173 @@
require("lockbox").insecure();
local Bit = require("lockbox.util.bit");
local String = require("string");
local Math = require("math");
local Queue = require("lockbox.util.queue");
local AND = Bit.band;
local OR = Bit.bor;
local XOR = Bit.bxor;
local LROT = Bit.lrotate;
local LSHIFT = Bit.lshift;
local RSHIFT = Bit.rshift;
--SHA1 is big-endian
local bytes2word = function(b0, b1, b2, b3)
local i = b0; i = LSHIFT(i, 8);
i = OR(i, b1); i = LSHIFT(i, 8);
i = OR(i, b2); i = LSHIFT(i, 8);
i = OR(i, b3);
return i;
end
local word2bytes = function(word)
local b0, b1, b2, b3;
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
b0 = AND(word, 0xFF);
return b0, b1, b2, b3;
end
local dword2bytes = function(i)
local b4, b5, b6, b7 = word2bytes(i);
local b0, b1, b2, b3 = word2bytes(Math.floor(i / 0x100000000));
return b0, b1, b2, b3, b4, b5, b6, b7;
end
local F = function(x, y, z) return XOR(z, AND(x, XOR(y, z))); end
local G = function(x, y, z) return XOR(x, XOR(y, z)); end
local H = function(x, y, z) return OR(AND(x, OR(y, z)), AND(y, z)); end
local I = function(x, y, z) return XOR(x, XOR(y, z)); end
local SHA1 = function()
local queue = Queue();
local h0 = 0x67452301;
local h1 = 0xEFCDAB89;
local h2 = 0x98BADCFE;
local h3 = 0x10325476;
local h4 = 0xC3D2E1F0;
local public = {};
local processBlock = function()
local a = h0;
local b = h1;
local c = h2;
local d = h3;
local e = h4;
local temp;
local k;
local w = {};
for i = 0, 15 do
w[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
end
for i = 16, 79 do
w[i] = LROT((XOR(XOR(w[i - 3], w[i - 8]), XOR(w[i - 14], w[i - 16]))), 1);
end
for i = 0, 79 do
if (i <= 19) then
temp = F(b, c, d);
k = 0x5A827999;
elseif (i <= 39) then
temp = G(b, c, d);
k = 0x6ED9EBA1;
elseif (i <= 59) then
temp = H(b, c, d);
k = 0x8F1BBCDC;
else
temp = I(b, c, d);
k = 0xCA62C1D6;
end
temp = LROT(a, 5) + temp + e + k + w[i];
e = d;
d = c;
c = LROT(b, 30);
b = a;
a = temp;
end
h0 = AND(h0 + a, 0xFFFFFFFF);
h1 = AND(h1 + b, 0xFFFFFFFF);
h2 = AND(h2 + c, 0xFFFFFFFF);
h3 = AND(h3 + d, 0xFFFFFFFF);
h4 = AND(h4 + e, 0xFFFFFFFF);
end
public.init = function()
queue.reset();
h0 = 0x67452301;
h1 = 0xEFCDAB89;
h2 = 0x98BADCFE;
h3 = 0x10325476;
h4 = 0xC3D2E1F0;
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(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
return {b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19};
end
public.asHex = function()
local b0, b1, b2, b3 = word2bytes(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
return String.format("%02x%02x%02x%02x%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, b16, b17, b18, b19);
end
return public;
end
return SHA1;

200
lockbox/digest/sha2_224.lua Normal file
View File

@@ -0,0 +1,200 @@
local Bit = require("lockbox.util.bit");
local String = require("string");
local Math = require("math");
local Queue = require("lockbox.util.queue");
local CONSTANTS = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 };
local fmt = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" ..
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
local AND = Bit.band;
local OR = Bit.bor;
local NOT = Bit.bnot;
local XOR = Bit.bxor;
local RROT = Bit.rrotate;
local LSHIFT = Bit.lshift;
local RSHIFT = Bit.rshift;
--SHA2 is big-endian
local bytes2word = function(b0, b1, b2, b3)
local i = b0; i = LSHIFT(i, 8);
i = OR(i, b1); i = LSHIFT(i, 8);
i = OR(i, b2); i = LSHIFT(i, 8);
i = OR(i, b3);
return i;
end
local word2bytes = function(word)
local b0, b1, b2, b3;
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
b0 = AND(word, 0xFF);
return b0, b1, b2, b3;
end
local dword2bytes = function(i)
local b4, b5, b6, b7 = word2bytes(i);
local b0, b1, b2, b3 = word2bytes(Math.floor(i / 0x100000000));
return b0, b1, b2, b3, b4, b5, b6, b7;
end
local SHA2_224 = function()
local queue = Queue();
local h0 = 0xc1059ed8;
local h1 = 0x367cd507;
local h2 = 0x3070dd17;
local h3 = 0xf70e5939;
local h4 = 0xffc00b31;
local h5 = 0x68581511;
local h6 = 0x64f98fa7;
local h7 = 0xbefa4fa4;
local public = {};
local processBlock = function()
local a = h0;
local b = h1;
local c = h2;
local d = h3;
local e = h4;
local f = h5;
local g = h6;
local h = h7;
local w = {};
for i = 0, 15 do
w[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
end
for i = 16, 63 do
local s0 = XOR(RROT(w[i - 15], 7), XOR(RROT(w[i - 15], 18), RSHIFT(w[i - 15], 3)));
local s1 = XOR(RROT(w[i - 2], 17), XOR(RROT(w[i - 2], 19), RSHIFT(w[i - 2], 10)));
w[i] = AND(w[i - 16] + s0 + w[i - 7] + s1, 0xFFFFFFFF);
end
for i = 0, 63 do
local s1 = XOR(RROT(e, 6), XOR(RROT(e, 11), RROT(e, 25)));
local ch = XOR(AND(e, f), AND(NOT(e), g));
local temp1 = h + s1 + ch + CONSTANTS[i + 1] + w[i];
local s0 = XOR(RROT(a, 2), XOR(RROT(a, 13), RROT(a, 22)));
local maj = XOR(AND(a, b), XOR(AND(a, c), AND(b, c)));
local temp2 = s0 + maj;
h = g;
g = f;
f = e;
e = d + temp1;
d = c;
c = b;
b = a;
a = temp1 + temp2;
end
h0 = AND(h0 + a, 0xFFFFFFFF);
h1 = AND(h1 + b, 0xFFFFFFFF);
h2 = AND(h2 + c, 0xFFFFFFFF);
h3 = AND(h3 + d, 0xFFFFFFFF);
h4 = AND(h4 + e, 0xFFFFFFFF);
h5 = AND(h5 + f, 0xFFFFFFFF);
h6 = AND(h6 + g, 0xFFFFFFFF);
h7 = AND(h7 + h, 0xFFFFFFFF);
end
public.init = function()
queue.reset();
h0 = 0xc1059ed8;
h1 = 0x367cd507;
h2 = 0x3070dd17;
h3 = 0xf70e5939;
h4 = 0xffc00b31;
h5 = 0x68581511;
h6 = 0x64f98fa7;
h7 = 0xbefa4fa4;
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(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
local b20, b21, b22, b23 = word2bytes(h5);
local b24, b25, b26, b27 = word2bytes(h6);
return { b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27};
end
public.asHex = function()
local b0, b1, b2, b3 = word2bytes(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
local b20, b21, b22, b23 = word2bytes(h5);
local b24, b25, b26, b27 = word2bytes(h6);
return String.format(fmt, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27);
end
return public;
end
return SHA2_224;

203
lockbox/digest/sha2_256.lua Normal file
View File

@@ -0,0 +1,203 @@
local Bit = require("lockbox.util.bit");
local String = require("string");
local Math = require("math");
local Queue = require("lockbox.util.queue");
local CONSTANTS = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 };
local fmt = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" ..
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
local AND = Bit.band;
local OR = Bit.bor;
local NOT = Bit.bnot;
local XOR = Bit.bxor;
local RROT = Bit.rrotate;
local LSHIFT = Bit.lshift;
local RSHIFT = Bit.rshift;
--SHA2 is big-endian
local bytes2word = function(b0, b1, b2, b3)
local i = b0; i = LSHIFT(i, 8);
i = OR(i, b1); i = LSHIFT(i, 8);
i = OR(i, b2); i = LSHIFT(i, 8);
i = OR(i, b3);
return i;
end
local word2bytes = function(word)
local b0, b1, b2, b3;
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
b0 = AND(word, 0xFF);
return b0, b1, b2, b3;
end
local dword2bytes = function(i)
local b4, b5, b6, b7 = word2bytes(i);
local b0, b1, b2, b3 = word2bytes(Math.floor(i / 0x100000000));
return b0, b1, b2, b3, b4, b5, b6, b7;
end
local SHA2_256 = function()
local queue = Queue();
local h0 = 0x6a09e667;
local h1 = 0xbb67ae85;
local h2 = 0x3c6ef372;
local h3 = 0xa54ff53a;
local h4 = 0x510e527f;
local h5 = 0x9b05688c;
local h6 = 0x1f83d9ab;
local h7 = 0x5be0cd19;
local public = {};
local processBlock = function()
local a = h0;
local b = h1;
local c = h2;
local d = h3;
local e = h4;
local f = h5;
local g = h6;
local h = h7;
local w = {};
for i = 0, 15 do
w[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
end
for i = 16, 63 do
local s0 = XOR(RROT(w[i - 15], 7), XOR(RROT(w[i - 15], 18), RSHIFT(w[i - 15], 3)));
local s1 = XOR(RROT(w[i - 2], 17), XOR(RROT(w[i - 2], 19), RSHIFT(w[i - 2], 10)));
w[i] = AND(w[i - 16] + s0 + w[i - 7] + s1, 0xFFFFFFFF);
end
for i = 0, 63 do
local s1 = XOR(RROT(e, 6), XOR(RROT(e, 11), RROT(e, 25)));
local ch = XOR(AND(e, f), AND(NOT(e), g));
local temp1 = h + s1 + ch + CONSTANTS[i + 1] + w[i];
local s0 = XOR(RROT(a, 2), XOR(RROT(a, 13), RROT(a, 22)));
local maj = XOR(AND(a, b), XOR(AND(a, c), AND(b, c)));
local temp2 = s0 + maj;
h = g;
g = f;
f = e;
e = d + temp1;
d = c;
c = b;
b = a;
a = temp1 + temp2;
end
h0 = AND(h0 + a, 0xFFFFFFFF);
h1 = AND(h1 + b, 0xFFFFFFFF);
h2 = AND(h2 + c, 0xFFFFFFFF);
h3 = AND(h3 + d, 0xFFFFFFFF);
h4 = AND(h4 + e, 0xFFFFFFFF);
h5 = AND(h5 + f, 0xFFFFFFFF);
h6 = AND(h6 + g, 0xFFFFFFFF);
h7 = AND(h7 + h, 0xFFFFFFFF);
end
public.init = function()
queue.reset();
h0 = 0x6a09e667;
h1 = 0xbb67ae85;
h2 = 0x3c6ef372;
h3 = 0xa54ff53a;
h4 = 0x510e527f;
h5 = 0x9b05688c;
h6 = 0x1f83d9ab;
h7 = 0x5be0cd19;
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(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
local b20, b21, b22, b23 = word2bytes(h5);
local b24, b25, b26, b27 = word2bytes(h6);
local b28, b29, b30, b31 = word2bytes(h7);
return { b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29, b30, b31};
end
public.asHex = function()
local b0, b1, b2, b3 = word2bytes(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
local b20, b21, b22, b23 = word2bytes(h5);
local b24, b25, b26, b27 = word2bytes(h6);
local b28, b29, b30, b31 = word2bytes(h7);
return String.format(fmt, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29, b30, b31);
end
return public;
end
return SHA2_256;

22
lockbox/init.lua Normal file
View File

@@ -0,0 +1,22 @@
local Lockbox = {};
--[[
package.path = "./?.lua;"
.. "./cipher/?.lua;"
.. "./digest/?.lua;"
.. "./kdf/?.lua;"
.. "./mac/?.lua;"
.. "./padding/?.lua;"
.. "./test/?.lua;"
.. "./util/?.lua;"
.. package.path;
--]]
Lockbox.ALLOW_INSECURE = true;
Lockbox.insecure = function()
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;

114
lockbox/kdf/pbkdf2.lua Normal file
View File

@@ -0,0 +1,114 @@
local Bit = require("lockbox.util.bit");
local Array = require("lockbox.util.array");
local Stream = require("lockbox.util.stream");
local Math = require("math");
local AND = Bit.band;
local RSHIFT = Bit.rshift;
local word2bytes = function(word)
local b0, b1, b2, b3;
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
b0 = AND(word, 0xFF);
return b0, b1, b2, b3;
end
local PBKDF2 = function()
local public = {};
local blockLen = 16;
local dKeyLen = 256;
local iterations = 4096;
local salt;
local password;
local PRF;
local dKey;
public.setBlockLen = function(len)
blockLen = len;
return public;
end
public.setDKeyLen = function(len)
dKeyLen = len
return public;
end
public.setIterations = function(iter)
iterations = iter;
return public;
end
public.setSalt = function(saltBytes)
salt = saltBytes;
return public;
end
public.setPassword = function(passwordBytes)
password = passwordBytes;
return public;
end
public.setPRF = function(prf)
PRF = prf;
return public;
end
local buildBlock = function(i)
local b0, b1, b2, b3 = word2bytes(i);
local ii = {b0, b1, b2, b3};
local s = Array.concat(salt, ii);
local out = {};
PRF.setKey(password);
for c = 1, iterations do
PRF.init()
.update(Stream.fromArray(s));
s = PRF.finish().asBytes();
if(c > 1) then
out = Array.XOR(out, s);
else
out = s;
end
end
return out;
end
public.finish = function()
local blocks = Math.ceil(dKeyLen / blockLen);
dKey = {};
for b = 1, blocks do
local block = buildBlock(b);
dKey = Array.concat(dKey, block);
end
if(Array.size(dKey) > dKeyLen) then dKey = Array.truncate(dKey, dKeyLen); end
return public;
end
public.asBytes = function()
return dKey;
end
public.asHex = function()
return Array.toHex(dKey);
end
return public;
end
return PBKDF2;

85
lockbox/mac/hmac.lua Normal file
View File

@@ -0,0 +1,85 @@
local Bit = require("lockbox.util.bit");
local Stream = require("lockbox.util.stream");
local Array = require("lockbox.util.array");
local XOR = Bit.bxor;
local HMAC = function()
local public = {};
local blockSize = 64;
local Digest = nil;
local outerPadding = {};
local innerPadding = {}
local digest;
public.setBlockSize = function(bytes)
blockSize = bytes;
return public;
end
public.setDigest = function(digestModule)
Digest = digestModule;
digest = Digest();
return public;
end
public.setKey = function(key)
local keyStream;
if(Array.size(key) > blockSize) then
keyStream = Stream.fromArray(Digest()
.update(Stream.fromArray(key))
.finish()
.asBytes());
else
keyStream = Stream.fromArray(key);
end
outerPadding = {};
innerPadding = {};
for i = 1, blockSize do
local byte = keyStream();
if byte == nil then byte = 0x00; end
outerPadding[i] = XOR(0x5C, byte);
innerPadding[i] = XOR(0x36, byte);
end
return public;
end
public.init = function()
digest.init()
.update(Stream.fromArray(innerPadding));
return public;
end
public.update = function(messageStream)
digest.update(messageStream);
return public;
end
public.finish = function()
local inner = digest.finish().asBytes();
digest.init()
.update(Stream.fromArray(outerPadding))
.update(Stream.fromArray(inner))
.finish();
return public;
end
public.asBytes = function()
return digest.asBytes();
end
public.asHex = function()
return digest.asHex();
end
return public;
end
return HMAC;

View File

@@ -0,0 +1,22 @@
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

@@ -0,0 +1,22 @@
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;

18
lockbox/padding/pkcs7.lua Normal file
View File

@@ -0,0 +1,18 @@
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;

19
lockbox/padding/zero.lua Normal file
View File

@@ -0,0 +1,19 @@
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;

211
lockbox/util/array.lua Normal file
View File

@@ -0,0 +1,211 @@
local String = require("string");
local Bit = require("lockbox.util.bit");
local Queue = require("lockbox.util.queue");
local XOR = Bit.bxor;
local Array = {};
Array.size = function(array)
return #array;
end
Array.fromString = function(string)
local bytes = {};
local i = 1;
local byte = String.byte(string, i);
while byte ~= nil do
bytes[i] = byte;
i = i + 1;
byte = String.byte(string, i);
end
return bytes;
end
Array.toString = function(bytes)
local chars = {};
local i = 1;
local byte = bytes[i];
while byte ~= nil do
chars[i] = String.char(byte);
i = i + 1;
byte = bytes[i];
end
return table.concat(chars, "");
end
Array.fromStream = function(stream)
local array = {};
local i = 1;
local byte = stream();
while byte ~= nil do
array[i] = byte;
i = i + 1;
byte = stream();
end
return array;
end
Array.readFromQueue = function(queue, size)
local array = {};
for i = 1, size do
array[i] = queue.pop();
end
return array;
end
Array.writeToQueue = function(queue, array)
local size = Array.size(array);
for i = 1, size do
queue.push(array[i]);
end
end
Array.toStream = function(array)
local queue = Queue();
local i = 1;
local byte = array[i];
while byte ~= nil do
queue.push(byte);
i = i + 1;
byte = array[i];
end
return queue.pop;
end
local fromHexTable = {};
for i = 0, 255 do
fromHexTable[String.format("%02X", i)] = i;
fromHexTable[String.format("%02x", i)] = i;
end
Array.fromHex = function(hex)
local array = {};
for i = 1, String.len(hex) / 2 do
local h = String.sub(hex, i * 2 - 1, i * 2);
array[i] = fromHexTable[h];
end
return array;
end
local toHexTable = {};
for i = 0, 255 do
toHexTable[i] = String.format("%02X", i);
end
Array.toHex = function(array)
local hex = {};
local i = 1;
local byte = array[i];
while byte ~= nil do
hex[i] = toHexTable[byte];
i = i + 1;
byte = array[i];
end
return table.concat(hex, "");
end
Array.concat = function(a, b)
local concat = {};
local out = 1;
local i = 1;
local byte = a[i];
while byte ~= nil do
concat[out] = byte;
i = i + 1;
out = out + 1;
byte = a[i];
end
i = 1;
byte = b[i];
while byte ~= nil do
concat[out] = byte;
i = i + 1;
out = out + 1;
byte = b[i];
end
return concat;
end
Array.truncate = function(a, newSize)
local x = {};
for i = 1, newSize do
x[i] = a[i];
end
return x;
end
Array.XOR = function(a, b)
local x = {};
for k, v in pairs(a) do
x[k] = XOR(v, b[k]);
end
return x;
end
Array.substitute = function(input, sbox)
local out = {};
for k, v in pairs(input) do
out[k] = sbox[v];
end
return out;
end
Array.permute = function(input, pbox)
local out = {};
for k, v in pairs(pbox) do
out[k] = input[v];
end
return out;
end
Array.copy = function(input)
local out = {};
for k, v in pairs(input) do
out[k] = v;
end
return out;
end
Array.slice = function(input, start, stop)
local out = {};
for i = start, stop do
out[i - start + 1] = input[i];
end
return out;
end
return Array;

25
lockbox/util/bit.lua Normal file
View File

@@ -0,0 +1,25 @@
local ok, e
ok = nil
if not ok then
ok, e = pcall(require, "bit") -- the LuaJIT one ?
end
if not ok then
ok, e = pcall(require, "bit32") -- Lua 5.2
end
if not ok then
ok, e = pcall(require, "bit.numberlua") -- for Lua 5.1, https://github.com/tst2005/lua-bit-numberlua/
end
if not ok then
error("no bitwise support found", 2)
end
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

47
lockbox/util/queue.lua Normal file
View File

@@ -0,0 +1,47 @@
local Queue = function()
local queue = {};
local tail = 0;
local head = 0;
local public = {};
public.push = function(obj)
queue[head] = obj;
head = head + 1;
return;
end
public.pop = function()
if tail < head
then
local obj = queue[tail];
queue[tail] = nil;
tail = tail + 1;
return obj;
else
return nil;
end
end
public.size = function()
return head - tail;
end
public.getHead = function()
return head;
end
public.getTail = function()
return tail;
end
public.reset = function()
queue = {};
head = 0;
tail = 0;
end
return public;
end
return Queue;

99
lockbox/util/stream.lua Normal file
View File

@@ -0,0 +1,99 @@
local Queue = require("lockbox.util.queue");
local String = require("string");
local Stream = {};
Stream.fromString = function(string)
local i = 0;
return function()
i = i + 1;
return String.byte(string, i);
end
end
Stream.toString = function(stream)
local array = {};
local i = 1;
local byte = stream();
while byte ~= nil do
array[i] = String.char(byte);
i = i + 1;
byte = stream();
end
return table.concat(array);
end
Stream.fromArray = function(array)
local queue = Queue();
local i = 1;
local byte = array[i];
while byte ~= nil do
queue.push(byte);
i = i + 1;
byte = array[i];
end
return queue.pop;
end
Stream.toArray = function(stream)
local array = {};
local i = 1;
local byte = stream();
while byte ~= nil do
array[i] = byte;
i = i + 1;
byte = stream();
end
return array;
end
local fromHexTable = {};
for i = 0, 255 do
fromHexTable[String.format("%02X", i)] = i;
fromHexTable[String.format("%02x", i)] = i;
end
Stream.fromHex = function(hex)
local queue = Queue();
for i = 1, String.len(hex) / 2 do
local h = String.sub(hex, i * 2 - 1, i * 2);
queue.push(fromHexTable[h]);
end
return queue.pop;
end
local toHexTable = {};
for i = 0, 255 do
toHexTable[i] = String.format("%02X", i);
end
Stream.toHex = function(stream)
local hex = {};
local i = 1;
local byte = stream();
while byte ~= nil do
hex[i] = toHexTable[byte];
i = i + 1;
byte = stream();
end
return table.concat(hex);
end
return Stream;

View File

@@ -3,7 +3,7 @@ local config = {}
-- set to false to run in offline mode (safety regulation only)
config.NETWORKED = true
-- unique reactor ID
config.REACTOR_ID = 1
config.REACTOR_ID = 1
-- port to send packets TO server
config.SERVER_PORT = 16000
-- port to listen to incoming packets FROM server

View File

@@ -1,8 +1,8 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local types = require("scada-common.types")
local util = require("scada-common.util")
local util = require("scada-common.util")
local plc = {}
@@ -23,7 +23,7 @@ local println_ts = util.println_ts
--- identifies dangerous states and SCRAMs reactor if warranted
---
--- autonomous from main SCADA supervisor/coordinator control
plc.rps_init = function (reactor)
function plc.rps_init(reactor)
local state_keys = {
dmg_crit = 1,
high_temp = 2,
@@ -41,7 +41,7 @@ plc.rps_init = function (reactor)
state = { false, false, false, false, false, false, false, false, false },
reactor_enabled = false,
tripped = false,
trip_cause = ""
trip_cause = "" ---@type rps_trip_cause
}
---@class rps
@@ -50,19 +50,19 @@ plc.rps_init = function (reactor)
-- PRIVATE FUNCTIONS --
-- set reactor access fault flag
local _set_fault = function ()
local function _set_fault()
if self.reactor.__p_last_fault() ~= "Terminated" then
self.state[state_keys.fault] = true
end
end
-- clear reactor access fault flag
local _clear_fault = function ()
local function _clear_fault()
self.state[state_keys.fault] = false
end
-- check for critical damage
local _damage_critical = function ()
local function _damage_critical()
local damage_percent = self.reactor.getDamagePercent()
if damage_percent == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
@@ -75,7 +75,7 @@ plc.rps_init = function (reactor)
end
-- check if the reactor is at a critically high temperature
local _high_temp = function ()
local function _high_temp()
-- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200
local temp = self.reactor.getTemperature()
if temp == ppm.ACCESS_FAULT then
@@ -89,7 +89,7 @@ plc.rps_init = function (reactor)
end
-- check if there is no coolant (<2% filled)
local _no_coolant = function ()
local function _no_coolant()
local coolant_filled = self.reactor.getCoolantFilledPercentage()
if coolant_filled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
@@ -102,7 +102,7 @@ plc.rps_init = function (reactor)
end
-- check for excess waste (>80% filled)
local _excess_waste = function ()
local function _excess_waste()
local w_filled = self.reactor.getWasteFilledPercentage()
if w_filled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
@@ -115,7 +115,7 @@ plc.rps_init = function (reactor)
end
-- check for heated coolant backup (>95% filled)
local _excess_heated_coolant = function ()
local function _excess_heated_coolant()
local hc_filled = self.reactor.getHeatedCoolantFilledPercentage()
if hc_filled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
@@ -128,7 +128,7 @@ plc.rps_init = function (reactor)
end
-- check if there is no fuel
local _insufficient_fuel = function ()
local function _insufficient_fuel()
local fuel = self.reactor.getFuel()
if fuel == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
@@ -144,28 +144,28 @@ plc.rps_init = function (reactor)
-- re-link a reactor after a peripheral re-connect
---@diagnostic disable-next-line: redefined-local
public.reconnect_reactor = function (reactor)
function public.reconnect_reactor(reactor)
self.reactor = reactor
end
-- trip for lost peripheral
public.trip_fault = function ()
function public.trip_fault()
_set_fault()
end
-- trip for a PLC comms timeout
public.trip_timeout = function ()
function public.trip_timeout()
self.state[state_keys.timeout] = true
end
-- manually SCRAM the reactor
public.trip_manual = function ()
function public.trip_manual()
self.state[state_keys.manual] = true
end
-- SCRAM the reactor now
---@return boolean success
public.scram = function ()
function public.scram()
log.info("RPS: reactor SCRAM")
self.reactor.scram()
@@ -180,7 +180,7 @@ plc.rps_init = function (reactor)
-- start the reactor
---@return boolean success
public.activate = function ()
function public.activate()
if not self.tripped then
log.info("RPS: reactor start")
@@ -198,7 +198,7 @@ plc.rps_init = function (reactor)
-- check all safety conditions
---@return boolean tripped, rps_status_t trip_status, boolean first_trip
public.check = function ()
function public.check()
local status = rps_status_t.ok
local was_tripped = self.tripped
local first_trip = false
@@ -259,12 +259,12 @@ plc.rps_init = function (reactor)
return self.tripped, status, first_trip
end
public.status = function () return self.state end
public.is_tripped = function () return self.tripped end
public.is_active = function () return self.reactor_enabled end
function public.status() return self.state end
function public.is_tripped() return self.tripped end
function public.is_active() return self.reactor_enabled end
-- reset the RPS
public.reset = function ()
function public.reset()
self.tripped = false
self.trip_cause = rps_status_t.ok
@@ -285,7 +285,7 @@ end
---@param reactor table
---@param rps rps
---@param conn_watchdog watchdog
plc.comms = function (id, version, modem, local_port, server_port, reactor, rps, conn_watchdog)
function plc.comms(id, version, modem, local_port, server_port, reactor, rps, conn_watchdog)
local self = {
id = id,
version = version,
@@ -316,7 +316,7 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps,
-- send an RPLC packet
---@param msg_type RPLC_TYPES
---@param msg string
local _send = function (msg_type, msg)
local function _send(msg_type, msg)
local s_pkt = comms.scada_packet()
local r_pkt = comms.rplc_packet()
@@ -330,7 +330,7 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps,
-- send a SCADA management packet
---@param msg_type SCADA_MGMT_TYPES
---@param msg string
local _send_mgmt = function (msg_type, msg)
local function _send_mgmt(msg_type, msg)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
@@ -343,7 +343,9 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps,
-- variable reactor status information, excluding heating rate
---@return table data_table, boolean faulted
local _reactor_status = function ()
local function _reactor_status()
local fuel = nil
local waste = nil
local coolant = nil
local hcoolant = nil
@@ -375,9 +377,9 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps,
function () data_table[5] = self.reactor.getDamagePercent() end,
function () data_table[6] = self.reactor.getBoilEfficiency() end,
function () data_table[7] = self.reactor.getEnvironmentalLoss() end,
function () data_table[8] = self.reactor.getFuel() end,
function () fuel = self.reactor.getFuel() end,
function () data_table[9] = self.reactor.getFuelFilledPercentage() end,
function () data_table[10] = self.reactor.getWaste() end,
function () waste = self.reactor.getWaste() end,
function () data_table[11] = self.reactor.getWasteFilledPercentage() end,
function () coolant = self.reactor.getCoolant() end,
function () data_table[14] = self.reactor.getCoolantFilledPercentage() end,
@@ -387,6 +389,18 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps,
parallel.waitForAll(table.unpack(tasks))
if type(fuel) == "table" then
data_table[8] = fuel.amount
elseif type(fuel) == "number" then
data_table[8] = fuel
end
if type(waste) == "table" then
data_table[10] = waste.amount
elseif type(waste) == "number" then
data_table[10] = waste
end
if coolant ~= nil then
data_table[12] = coolant.name
data_table[13] = coolant.amount
@@ -402,7 +416,7 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps,
-- update the status cache if changed
---@return boolean changed
local _update_status_cache = function ()
local function _update_status_cache()
local status, faulted = _reactor_status()
local changed = false
@@ -428,19 +442,19 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps,
-- keep alive ack
---@param srv_time integer
local _send_keep_alive_ack = function (srv_time)
local function _send_keep_alive_ack(srv_time)
_send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() })
end
-- general ack
---@param msg_type RPLC_TYPES
---@param succeeded boolean
local _send_ack = function (msg_type, succeeded)
local function _send_ack(msg_type, succeeded)
_send(msg_type, { succeeded })
end
-- send structure properties (these should not change, server will cache these)
local _send_struct = function ()
local function _send_struct()
local mek_data = { 0, 0, 0, 0, 0, 0, 0, 0 }
local tasks = {
@@ -468,7 +482,7 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps,
-- reconnect a newly connected modem
---@param modem table
---@diagnostic disable-next-line: redefined-local
public.reconnect_modem = function (modem)
function public.reconnect_modem(modem)
self.modem = modem
-- open modem
@@ -480,33 +494,33 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps,
-- reconnect a newly connected reactor
---@param reactor table
---@diagnostic disable-next-line: redefined-local
public.reconnect_reactor = function (reactor)
function public.reconnect_reactor(reactor)
self.reactor = reactor
self.status_cache = nil
end
-- unlink from the server
public.unlink = function ()
function public.unlink()
self.linked = false
self.r_seq_num = nil
self.status_cache = nil
end
-- close the connection to the server
public.close = function ()
function public.close()
self.conn_watchdog.cancel()
public.unlink()
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
end
-- attempt to establish link with supervisor
public.send_link_req = function ()
function public.send_link_req()
_send(RPLC_TYPES.LINK_REQ, { self.id, self.version })
end
-- send live status information
---@param degraded boolean
public.send_status = function (degraded)
function public.send_status(degraded)
if self.linked then
local mek_data = nil
@@ -517,7 +531,7 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps,
local sys_status = {
util.time(), -- timestamp
(not self.scrammed), -- requested control state
rps.is_tripped(), -- overridden
rps.is_tripped(), -- rps_tripped
degraded, -- degraded
self.reactor.getHeatingRate(), -- heating rate
mek_data -- mekanism status data
@@ -532,7 +546,7 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps,
end
-- send reactor protection system status
public.send_rps_status = function ()
function public.send_rps_status()
if self.linked then
_send(RPLC_TYPES.RPS_STATUS, rps.status())
end
@@ -540,7 +554,7 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps,
-- send reactor protection system alarm
---@param cause rps_status_t
public.send_rps_alarm = function (cause)
function public.send_rps_alarm(cause)
if self.linked then
local rps_alarm = {
cause,
@@ -558,7 +572,7 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps,
---@param message any
---@param distance integer
---@return rplc_frame|mgmt_frame|nil packet
public.parse_packet = function(side, sender, reply_to, message, distance)
function public.parse_packet(side, sender, reply_to, message, distance)
local pkt = nil
local s_pkt = comms.scada_packet()
@@ -590,7 +604,7 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps,
---@param packet rplc_frame|mgmt_frame
---@param plc_state plc_state
---@param setpoints setpoints
public.handle_packet = function (packet, plc_state, setpoints)
function public.handle_packet(packet, plc_state, setpoints)
if packet ~= nil then
-- check sequence number
if self.r_seq_num == nil then
@@ -738,7 +752,7 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps,
log.warning("PLC KEEP_ALIVE trip time > 500ms (" .. trip_time .. "ms)")
end
-- log.debug("RPLC RTT = ".. trip_time .. "ms")
-- log.debug("RPLC RTT = " .. trip_time .. "ms")
_send_keep_alive_ack(timestamp)
else
@@ -760,8 +774,8 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps,
end
end
public.is_scrammed = function () return self.scrammed end
public.is_linked = function () return self.linked end
function public.is_scrammed() return self.scrammed end
function public.is_linked() return self.linked end
return public
end

View File

@@ -4,22 +4,40 @@
require("/initenv").init_env()
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local config = require("reactor-plc.config")
local plc = require("reactor-plc.plc")
local config = require("reactor-plc.config")
local plc = require("reactor-plc.plc")
local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "alpha-v0.7.2"
local R_PLC_VERSION = "beta-v0.8.2"
local print = util.print
local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
----------------------------------------
-- config validation
----------------------------------------
local cfv = util.new_validator()
cfv.assert_type_bool(config.NETWORKED)
cfv.assert_type_int(config.REACTOR_ID)
cfv.assert_port(config.SERVER_PORT)
cfv.assert_port(config.LISTEN_PORT)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
assert(cfv.valid(), "bad config file: missing/invalid fields")
----------------------------------------
-- log init
----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE)
log.info("========================================")
@@ -27,6 +45,10 @@ log.info("BOOTING reactor-plc.startup " .. R_PLC_VERSION)
log.info("========================================")
println(">> Reactor PLC " .. R_PLC_VERSION .. " <<")
----------------------------------------
-- startup
----------------------------------------
-- mount connected devices
ppm.mount_all()
@@ -102,7 +124,7 @@ if __shared_memory.networked and smem_dev.modem == nil then
end
-- PLC init
local init = function ()
local function init()
if plc_state.init_ok then
-- just booting up, no fission allowed (neutrons stay put thanks)
smem_dev.reactor.scram()

View File

@@ -1,7 +1,7 @@
local log = require("scada-common.log")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local threads = {}
@@ -30,14 +30,14 @@ local MQ__COMM_CMD = {
-- main thread
---@param smem plc_shared_memory
---@param init function
threads.thread__main = function (smem, init)
function threads.thread__main(smem, init)
local public = {} ---@class thread
-- execute thread
public.exec = function ()
function public.exec()
log.debug("main thread init, clock inactive")
-- send status updates at 2Hz (every 10 server ticks) (every loop tick)
-- send status updates at 1Hz (every 20 server ticks) (every loop tick)
-- send link requests at 0.5Hz (every 40 server ticks) (every 4 loop ticks)
local LINK_TICKS = 4
local ticks_to_update = 0
@@ -55,8 +55,7 @@ threads.thread__main = function (smem, init)
local plc_comms = smem.plc_sys.plc_comms
local conn_watchdog = smem.plc_sys.conn_watchdog
---@diagnostic disable-next-line: undefined-field
local event, param1, param2, param3, param4, param5 = os.pullEventRaw()
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event
if event == "timer" and loop_clock.is_clock(param1) then
@@ -189,7 +188,7 @@ threads.thread__main = function (smem, init)
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
public.p_exec = function ()
function public.p_exec()
local plc_state = smem.plc_state
while not plc_state.shutdown do
@@ -215,11 +214,11 @@ end
-- RPS operation thread
---@param smem plc_shared_memory
threads.thread__rps = function (smem)
function threads.thread__rps(smem)
local public = {} ---@class thread
-- execute thread
public.exec = function ()
function public.exec()
log.debug("rps thread start")
-- load in from shared memory
@@ -332,7 +331,7 @@ threads.thread__rps = function (smem)
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
public.p_exec = function ()
function public.p_exec()
local plc_state = smem.plc_state
while not plc_state.shutdown do
@@ -354,11 +353,11 @@ end
-- communications sender thread
---@param smem plc_shared_memory
threads.thread__comms_tx = function (smem)
function threads.thread__comms_tx(smem)
local public = {} ---@class thread
-- execute thread
public.exec = function ()
function public.exec()
log.debug("comms tx thread start")
-- load in from shared memory
@@ -407,7 +406,7 @@ threads.thread__comms_tx = function (smem)
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
public.p_exec = function ()
function public.p_exec()
local plc_state = smem.plc_state
while not plc_state.shutdown do
@@ -428,11 +427,11 @@ end
-- communications handler thread
---@param smem plc_shared_memory
threads.thread__comms_rx = function (smem)
function threads.thread__comms_rx(smem)
local public = {} ---@class thread
-- execute thread
public.exec = function ()
function public.exec()
log.debug("comms rx thread start")
-- load in from shared memory
@@ -481,7 +480,7 @@ threads.thread__comms_rx = function (smem)
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
public.p_exec = function ()
function public.p_exec()
local plc_state = smem.plc_state
while not plc_state.shutdown do
@@ -502,11 +501,11 @@ end
-- apply setpoints
---@param smem plc_shared_memory
threads.thread__setpoint_control = function (smem)
function threads.thread__setpoint_control(smem)
local public = {} ---@class thread
-- execute thread
public.exec = function ()
function public.exec()
log.debug("setpoint control thread start")
-- load in from shared memory
@@ -605,7 +604,7 @@ threads.thread__setpoint_control = function (smem)
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
public.p_exec = function ()
function public.p_exec()
local plc_state = smem.plc_state
while not plc_state.shutdown do

View File

@@ -15,12 +15,12 @@ config.LOG_MODE = 0
-- RTU peripheral devices (named: side/network device name)
config.RTU_DEVICES = {
{
name = "boiler_1",
name = "boilerValve_0",
index = 1,
for_reactor = 1
},
{
name = "turbine_1",
name = "turbineValve_0",
index = 1,
for_reactor = 1
}

View File

@@ -4,11 +4,8 @@ local boiler_rtu = {}
-- create new boiler (mek 10.0) device
---@param boiler table
boiler_rtu.new = function (boiler)
local self = {
rtu = rtu.init_unit(),
boiler = boiler
}
function boiler_rtu.new(boiler)
local unit = rtu.init_unit()
-- discrete inputs --
-- none
@@ -18,34 +15,34 @@ boiler_rtu.new = function (boiler)
-- input registers --
-- build properties
self.rtu.connect_input_reg(self.boiler.getBoilCapacity)
self.rtu.connect_input_reg(self.boiler.getSteamCapacity)
self.rtu.connect_input_reg(self.boiler.getWaterCapacity)
self.rtu.connect_input_reg(self.boiler.getHeatedCoolantCapacity)
self.rtu.connect_input_reg(self.boiler.getCooledCoolantCapacity)
self.rtu.connect_input_reg(self.boiler.getSuperheaters)
self.rtu.connect_input_reg(self.boiler.getMaxBoilRate)
unit.connect_input_reg(boiler.getBoilCapacity)
unit.connect_input_reg(boiler.getSteamCapacity)
unit.connect_input_reg(boiler.getWaterCapacity)
unit.connect_input_reg(boiler.getHeatedCoolantCapacity)
unit.connect_input_reg(boiler.getCooledCoolantCapacity)
unit.connect_input_reg(boiler.getSuperheaters)
unit.connect_input_reg(boiler.getMaxBoilRate)
-- current state
self.rtu.connect_input_reg(self.boiler.getTemperature)
self.rtu.connect_input_reg(self.boiler.getBoilRate)
unit.connect_input_reg(boiler.getTemperature)
unit.connect_input_reg(boiler.getBoilRate)
-- tanks
self.rtu.connect_input_reg(self.boiler.getSteam)
self.rtu.connect_input_reg(self.boiler.getSteamNeeded)
self.rtu.connect_input_reg(self.boiler.getSteamFilledPercentage)
self.rtu.connect_input_reg(self.boiler.getWater)
self.rtu.connect_input_reg(self.boiler.getWaterNeeded)
self.rtu.connect_input_reg(self.boiler.getWaterFilledPercentage)
self.rtu.connect_input_reg(self.boiler.getHeatedCoolant)
self.rtu.connect_input_reg(self.boiler.getHeatedCoolantNeeded)
self.rtu.connect_input_reg(self.boiler.getHeatedCoolantFilledPercentage)
self.rtu.connect_input_reg(self.boiler.getCooledCoolant)
self.rtu.connect_input_reg(self.boiler.getCooledCoolantNeeded)
self.rtu.connect_input_reg(self.boiler.getCooledCoolantFilledPercentage)
unit.connect_input_reg(boiler.getSteam)
unit.connect_input_reg(boiler.getSteamNeeded)
unit.connect_input_reg(boiler.getSteamFilledPercentage)
unit.connect_input_reg(boiler.getWater)
unit.connect_input_reg(boiler.getWaterNeeded)
unit.connect_input_reg(boiler.getWaterFilledPercentage)
unit.connect_input_reg(boiler.getHeatedCoolant)
unit.connect_input_reg(boiler.getHeatedCoolantNeeded)
unit.connect_input_reg(boiler.getHeatedCoolantFilledPercentage)
unit.connect_input_reg(boiler.getCooledCoolant)
unit.connect_input_reg(boiler.getCooledCoolantNeeded)
unit.connect_input_reg(boiler.getCooledCoolantFilledPercentage)
-- holding registers --
-- none
return self.rtu.interface()
return unit.interface()
end
return boiler_rtu

View File

@@ -4,55 +4,52 @@ local boilerv_rtu = {}
-- create new boiler (mek 10.1+) device
---@param boiler table
boilerv_rtu.new = function (boiler)
local self = {
rtu = rtu.init_unit(),
boiler = boiler
}
function boilerv_rtu.new(boiler)
local unit = rtu.init_unit()
-- discrete inputs --
self.rtu.connect_di(self.boiler.isFormed)
unit.connect_di(boiler.isFormed)
-- coils --
-- none
-- input registers --
-- multiblock properties
self.rtu.connect_input_reg(self.boiler.getLength)
self.rtu.connect_input_reg(self.boiler.getWidth)
self.rtu.connect_input_reg(self.boiler.getHeight)
self.rtu.connect_input_reg(self.boiler.getMinPos)
self.rtu.connect_input_reg(self.boiler.getMaxPos)
unit.connect_input_reg(boiler.getLength)
unit.connect_input_reg(boiler.getWidth)
unit.connect_input_reg(boiler.getHeight)
unit.connect_input_reg(boiler.getMinPos)
unit.connect_input_reg(boiler.getMaxPos)
-- build properties
self.rtu.connect_input_reg(self.boiler.getBoilCapacity)
self.rtu.connect_input_reg(self.boiler.getSteamCapacity)
self.rtu.connect_input_reg(self.boiler.getWaterCapacity)
self.rtu.connect_input_reg(self.boiler.getHeatedCoolantCapacity)
self.rtu.connect_input_reg(self.boiler.getCooledCoolantCapacity)
self.rtu.connect_input_reg(self.boiler.getSuperheaters)
self.rtu.connect_input_reg(self.boiler.getMaxBoilRate)
self.rtu.connect_input_reg(self.boiler.getEnvironmentalLoss)
unit.connect_input_reg(boiler.getBoilCapacity)
unit.connect_input_reg(boiler.getSteamCapacity)
unit.connect_input_reg(boiler.getWaterCapacity)
unit.connect_input_reg(boiler.getHeatedCoolantCapacity)
unit.connect_input_reg(boiler.getCooledCoolantCapacity)
unit.connect_input_reg(boiler.getSuperheaters)
unit.connect_input_reg(boiler.getMaxBoilRate)
unit.connect_input_reg(boiler.getEnvironmentalLoss)
-- current state
self.rtu.connect_input_reg(self.boiler.getTemperature)
self.rtu.connect_input_reg(self.boiler.getBoilRate)
unit.connect_input_reg(boiler.getTemperature)
unit.connect_input_reg(boiler.getBoilRate)
-- tanks
self.rtu.connect_input_reg(self.boiler.getSteam)
self.rtu.connect_input_reg(self.boiler.getSteamNeeded)
self.rtu.connect_input_reg(self.boiler.getSteamFilledPercentage)
self.rtu.connect_input_reg(self.boiler.getWater)
self.rtu.connect_input_reg(self.boiler.getWaterNeeded)
self.rtu.connect_input_reg(self.boiler.getWaterFilledPercentage)
self.rtu.connect_input_reg(self.boiler.getHeatedCoolant)
self.rtu.connect_input_reg(self.boiler.getHeatedCoolantNeeded)
self.rtu.connect_input_reg(self.boiler.getHeatedCoolantFilledPercentage)
self.rtu.connect_input_reg(self.boiler.getCooledCoolant)
self.rtu.connect_input_reg(self.boiler.getCooledCoolantNeeded)
self.rtu.connect_input_reg(self.boiler.getCooledCoolantFilledPercentage)
unit.connect_input_reg(boiler.getSteam)
unit.connect_input_reg(boiler.getSteamNeeded)
unit.connect_input_reg(boiler.getSteamFilledPercentage)
unit.connect_input_reg(boiler.getWater)
unit.connect_input_reg(boiler.getWaterNeeded)
unit.connect_input_reg(boiler.getWaterFilledPercentage)
unit.connect_input_reg(boiler.getHeatedCoolant)
unit.connect_input_reg(boiler.getHeatedCoolantNeeded)
unit.connect_input_reg(boiler.getHeatedCoolantFilledPercentage)
unit.connect_input_reg(boiler.getCooledCoolant)
unit.connect_input_reg(boiler.getCooledCoolantNeeded)
unit.connect_input_reg(boiler.getCooledCoolantFilledPercentage)
-- holding registers --
-- none
return self.rtu.interface()
return unit.interface()
end
return boilerv_rtu

View File

@@ -4,17 +4,8 @@ local energymachine_rtu = {}
-- create new energy machine device
---@param machine table
energymachine_rtu.new = function (machine)
local self = {
rtu = rtu.init_unit(),
machine = machine
}
---@class rtu_device
local public = {}
-- get the RTU interface
public.rtu_interface = function () return self.rtu end
function energymachine_rtu.new(machine)
local unit = rtu.init_unit()
-- discrete inputs --
-- none
@@ -24,16 +15,16 @@ energymachine_rtu.new = function (machine)
-- input registers --
-- build properties
self.rtu.connect_input_reg(self.machine.getTotalMaxEnergy)
unit.connect_input_reg(machine.getTotalMaxEnergy)
-- containers
self.rtu.connect_input_reg(self.machine.getTotalEnergy)
self.rtu.connect_input_reg(self.machine.getTotalEnergyNeeded)
self.rtu.connect_input_reg(self.machine.getTotalEnergyFilledPercentage)
unit.connect_input_reg(machine.getTotalEnergy)
unit.connect_input_reg(machine.getTotalEnergyNeeded)
unit.connect_input_reg(machine.getTotalEnergyFilledPercentage)
-- holding registers --
-- none
return public
return unit.interface()
end
return energymachine_rtu

26
rtu/dev/envd_rtu.lua Normal file
View File

@@ -0,0 +1,26 @@
local rtu = require("rtu.rtu")
local envd_rtu = {}
-- create new environment detector device
---@param envd table
function envd_rtu.new(envd)
local unit = rtu.init_unit()
-- discrete inputs --
-- none
-- coils --
-- none
-- input registers --
unit.connect_input_reg(envd.getRadiation)
unit.connect_input_reg(envd.getRadiationRaw)
-- holding registers --
-- none
return unit.interface()
end
return envd_rtu

View File

@@ -4,42 +4,39 @@ local imatrix_rtu = {}
-- create new induction matrix (mek 10.1+) device
---@param imatrix table
imatrix_rtu.new = function (imatrix)
local self = {
rtu = rtu.init_unit(),
imatrix = imatrix
}
function imatrix_rtu.new(imatrix)
local unit = rtu.init_unit()
-- discrete inputs --
self.rtu.connect_di(self.boiler.isFormed)
unit.connect_di(imatrix.isFormed)
-- coils --
-- none
-- input registers --
-- multiblock properties
self.rtu.connect_input_reg(self.boiler.getLength)
self.rtu.connect_input_reg(self.boiler.getWidth)
self.rtu.connect_input_reg(self.boiler.getHeight)
self.rtu.connect_input_reg(self.boiler.getMinPos)
self.rtu.connect_input_reg(self.boiler.getMaxPos)
unit.connect_input_reg(imatrix.getLength)
unit.connect_input_reg(imatrix.getWidth)
unit.connect_input_reg(imatrix.getHeight)
unit.connect_input_reg(imatrix.getMinPos)
unit.connect_input_reg(imatrix.getMaxPos)
-- build properties
self.rtu.connect_input_reg(self.imatrix.getMaxEnergy)
self.rtu.connect_input_reg(self.imatrix.getTransferCap)
self.rtu.connect_input_reg(self.imatrix.getInstalledCells)
self.rtu.connect_input_reg(self.imatrix.getInstalledProviders)
-- containers
self.rtu.connect_input_reg(self.imatrix.getEnergy)
self.rtu.connect_input_reg(self.imatrix.getEnergyNeeded)
self.rtu.connect_input_reg(self.imatrix.getEnergyFilledPercentage)
unit.connect_input_reg(imatrix.getMaxEnergy)
unit.connect_input_reg(imatrix.getTransferCap)
unit.connect_input_reg(imatrix.getInstalledCells)
unit.connect_input_reg(imatrix.getInstalledProviders)
-- I/O rates
self.rtu.connect_input_reg(self.imatrix.getLastInput)
self.rtu.connect_input_reg(self.imatrix.getLastOutput)
unit.connect_input_reg(imatrix.getLastInput)
unit.connect_input_reg(imatrix.getLastOutput)
-- tanks
unit.connect_input_reg(imatrix.getEnergy)
unit.connect_input_reg(imatrix.getEnergyNeeded)
unit.connect_input_reg(imatrix.getEnergyFilledPercentage)
-- holding registers --
-- none
return self.rtu.interface()
return unit.interface()
end
return imatrix_rtu

View File

@@ -1,4 +1,4 @@
local rtu = require("rtu.rtu")
local rtu = require("rtu.rtu")
local rsio = require("scada-common.rsio")
local redstone_rtu = {}
@@ -8,13 +8,11 @@ local digital_write = rsio.digital_write
local digital_is_active = rsio.digital_is_active
-- create new redstone device
redstone_rtu.new = function ()
local self = {
rtu = rtu.init_unit()
}
function redstone_rtu.new()
local unit = rtu.init_unit()
-- get RTU interface
local interface = self.rtu.interface()
local interface = unit.interface()
---@class rtu_rs_device
--- extends rtu_device; fields added manually to please Lua diagnostics
@@ -31,7 +29,7 @@ redstone_rtu.new = function ()
-- link digital input
---@param side string
---@param color integer
public.link_di = function (side, color)
function public.link_di(side, color)
local f_read = nil
if color then
@@ -44,14 +42,14 @@ redstone_rtu.new = function ()
end
end
self.rtu.connect_di(f_read)
unit.connect_di(f_read)
end
-- link digital output
---@param channel RS_IO
---@param side string
---@param color integer
public.link_do = function (channel, side, color)
function public.link_do(channel, side, color)
local f_read = nil
local f_write = nil
@@ -81,13 +79,13 @@ redstone_rtu.new = function ()
end
end
self.rtu.connect_coil(f_read, f_write)
unit.connect_coil(f_read, f_write)
end
-- link analog input
---@param side string
public.link_ai = function (side)
self.rtu.connect_input_reg(
function public.link_ai(side)
unit.connect_input_reg(
function ()
return rs.getAnalogInput(side)
end
@@ -96,8 +94,8 @@ redstone_rtu.new = function ()
-- link analog output
---@param side string
public.link_ao = function (side)
self.rtu.connect_holding_reg(
function public.link_ao(side)
unit.connect_holding_reg(
function ()
return rs.getAnalogOutput(side)
end,

37
rtu/dev/sna_rtu.lua Normal file
View File

@@ -0,0 +1,37 @@
local rtu = require("rtu.rtu")
local sna_rtu = {}
-- create new solar neutron activator (sna) device
---@param sna table
function sna_rtu.new(sna)
local unit = rtu.init_unit()
-- discrete inputs --
-- none
-- coils --
-- none
-- input registers --
-- build properties
unit.connect_input_reg(sna.getInputCapacity)
unit.connect_input_reg(sna.getOutputCapacity)
-- current state
unit.connect_input_reg(sna.getProductionRate)
unit.connect_input_reg(sna.getPeakProductionRate)
-- tanks
unit.connect_input_reg(sna.getInput)
unit.connect_input_reg(sna.getInputNeeded)
unit.connect_input_reg(sna.getInputFilledPercentage)
unit.connect_input_reg(sna.getOutput)
unit.connect_input_reg(sna.getOutputNeeded)
unit.connect_input_reg(sna.getOutputFilledPercentage)
-- holding registers --
-- none
return unit.interface()
end
return sna_rtu

47
rtu/dev/sps_rtu.lua Normal file
View File

@@ -0,0 +1,47 @@
local rtu = require("rtu.rtu")
local sps_rtu = {}
-- create new super-critical phase shifter (sps) device
---@param sps table
function sps_rtu.new(sps)
local unit = rtu.init_unit()
-- discrete inputs --
unit.connect_di(sps.isFormed)
-- coils --
-- none
-- input registers --
-- multiblock properties
unit.connect_input_reg(sps.getLength)
unit.connect_input_reg(sps.getWidth)
unit.connect_input_reg(sps.getHeight)
unit.connect_input_reg(sps.getMinPos)
unit.connect_input_reg(sps.getMaxPos)
-- build properties
unit.connect_input_reg(sps.getCoils)
unit.connect_input_reg(sps.getInputCapacity)
unit.connect_input_reg(sps.getOutputCapacity)
unit.connect_input_reg(sps.getMaxEnergy)
-- current state
unit.connect_input_reg(sps.getProcessRate)
-- tanks
unit.connect_input_reg(sps.getInput)
unit.connect_input_reg(sps.getInputNeeded)
unit.connect_input_reg(sps.getInputFilledPercentage)
unit.connect_input_reg(sps.getOutput)
unit.connect_input_reg(sps.getOutputNeeded)
unit.connect_input_reg(sps.getOutputFilledPercentage)
unit.connect_input_reg(sps.getEnergy)
unit.connect_input_reg(sps.getEnergyNeeded)
unit.connect_input_reg(sps.getEnergyFilledPercentage)
-- holding registers --
-- none
return unit.interface()
end
return sps_rtu

View File

@@ -4,11 +4,8 @@ local turbine_rtu = {}
-- create new turbine (mek 10.0) device
---@param turbine table
turbine_rtu.new = function (turbine)
local self = {
rtu = rtu.init_unit(),
turbine = turbine
}
function turbine_rtu.new(turbine)
local unit = rtu.init_unit()
-- discrete inputs --
-- none
@@ -18,29 +15,29 @@ turbine_rtu.new = function (turbine)
-- input registers --
-- build properties
self.rtu.connect_input_reg(self.turbine.getBlades)
self.rtu.connect_input_reg(self.turbine.getCoils)
self.rtu.connect_input_reg(self.turbine.getVents)
self.rtu.connect_input_reg(self.turbine.getDispersers)
self.rtu.connect_input_reg(self.turbine.getCondensers)
self.rtu.connect_input_reg(self.turbine.getSteamCapacity)
self.rtu.connect_input_reg(self.turbine.getMaxFlowRate)
self.rtu.connect_input_reg(self.turbine.getMaxProduction)
self.rtu.connect_input_reg(self.turbine.getMaxWaterOutput)
unit.connect_input_reg(turbine.getBlades)
unit.connect_input_reg(turbine.getCoils)
unit.connect_input_reg(turbine.getVents)
unit.connect_input_reg(turbine.getDispersers)
unit.connect_input_reg(turbine.getCondensers)
unit.connect_input_reg(turbine.getSteamCapacity)
unit.connect_input_reg(turbine.getMaxFlowRate)
unit.connect_input_reg(turbine.getMaxProduction)
unit.connect_input_reg(turbine.getMaxWaterOutput)
-- current state
self.rtu.connect_input_reg(self.turbine.getFlowRate)
self.rtu.connect_input_reg(self.turbine.getProductionRate)
self.rtu.connect_input_reg(self.turbine.getLastSteamInputRate)
self.rtu.connect_input_reg(self.turbine.getDumpingMode)
unit.connect_input_reg(turbine.getFlowRate)
unit.connect_input_reg(turbine.getProductionRate)
unit.connect_input_reg(turbine.getLastSteamInputRate)
unit.connect_input_reg(turbine.getDumpingMode)
-- tanks
self.rtu.connect_input_reg(self.turbine.getSteam)
self.rtu.connect_input_reg(self.turbine.getSteamNeeded)
self.rtu.connect_input_reg(self.turbine.getSteamFilledPercentage)
unit.connect_input_reg(turbine.getSteam)
unit.connect_input_reg(turbine.getSteamNeeded)
unit.connect_input_reg(turbine.getSteamFilledPercentage)
-- holding registers --
-- none
return self.rtu.interface()
return unit.interface()
end
return turbine_rtu

View File

@@ -4,54 +4,51 @@ local turbinev_rtu = {}
-- create new turbine (mek 10.1+) device
---@param turbine table
turbinev_rtu.new = function (turbine)
local self = {
rtu = rtu.init_unit(),
turbine = turbine
}
function turbinev_rtu.new(turbine)
local unit = rtu.init_unit()
-- discrete inputs --
self.rtu.connect_di(self.boiler.isFormed)
unit.connect_di(turbine.isFormed)
-- coils --
self.rtu.connect_coil(function () self.turbine.incrementDumpingMode() end, function () end)
self.rtu.connect_coil(function () self.turbine.decrementDumpingMode() end, function () end)
unit.connect_coil(function () turbine.incrementDumpingMode() end, function () end)
unit.connect_coil(function () turbine.decrementDumpingMode() end, function () end)
-- input registers --
-- multiblock properties
self.rtu.connect_input_reg(self.boiler.getLength)
self.rtu.connect_input_reg(self.boiler.getWidth)
self.rtu.connect_input_reg(self.boiler.getHeight)
self.rtu.connect_input_reg(self.boiler.getMinPos)
self.rtu.connect_input_reg(self.boiler.getMaxPos)
unit.connect_input_reg(turbine.getLength)
unit.connect_input_reg(turbine.getWidth)
unit.connect_input_reg(turbine.getHeight)
unit.connect_input_reg(turbine.getMinPos)
unit.connect_input_reg(turbine.getMaxPos)
-- build properties
self.rtu.connect_input_reg(self.turbine.getBlades)
self.rtu.connect_input_reg(self.turbine.getCoils)
self.rtu.connect_input_reg(self.turbine.getVents)
self.rtu.connect_input_reg(self.turbine.getDispersers)
self.rtu.connect_input_reg(self.turbine.getCondensers)
self.rtu.connect_input_reg(self.turbine.getDumpingMode)
self.rtu.connect_input_reg(self.turbine.getSteamCapacity)
self.rtu.connect_input_reg(self.turbine.getMaxEnergy)
self.rtu.connect_input_reg(self.turbine.getMaxFlowRate)
self.rtu.connect_input_reg(self.turbine.getMaxWaterOutput)
self.rtu.connect_input_reg(self.turbine.getMaxProduction)
unit.connect_input_reg(turbine.getBlades)
unit.connect_input_reg(turbine.getCoils)
unit.connect_input_reg(turbine.getVents)
unit.connect_input_reg(turbine.getDispersers)
unit.connect_input_reg(turbine.getCondensers)
unit.connect_input_reg(turbine.getSteamCapacity)
unit.connect_input_reg(turbine.getMaxEnergy)
unit.connect_input_reg(turbine.getMaxFlowRate)
unit.connect_input_reg(turbine.getMaxProduction)
unit.connect_input_reg(turbine.getMaxWaterOutput)
-- current state
self.rtu.connect_input_reg(self.turbine.getFlowRate)
self.rtu.connect_input_reg(self.turbine.getProductionRate)
self.rtu.connect_input_reg(self.turbine.getLastSteamInputRate)
unit.connect_input_reg(turbine.getFlowRate)
unit.connect_input_reg(turbine.getProductionRate)
unit.connect_input_reg(turbine.getLastSteamInputRate)
unit.connect_input_reg(turbine.getDumpingMode)
-- tanks/containers
self.rtu.connect_input_reg(self.turbine.getSteam)
self.rtu.connect_input_reg(self.turbine.getSteamNeeded)
self.rtu.connect_input_reg(self.turbine.getSteamFilledPercentage)
self.rtu.connect_input_reg(self.turbine.getEnergy)
self.rtu.connect_input_reg(self.turbine.getEnergyNeeded)
self.rtu.connect_input_reg(self.turbine.getEnergyFilledPercentage)
unit.connect_input_reg(turbine.getSteam)
unit.connect_input_reg(turbine.getSteamNeeded)
unit.connect_input_reg(turbine.getSteamFilledPercentage)
unit.connect_input_reg(turbine.getEnergy)
unit.connect_input_reg(turbine.getEnergyNeeded)
unit.connect_input_reg(turbine.getEnergyFilledPercentage)
-- holding registers --
self.rtu.connect_holding_reg(self.turbine.setDumpingMode, self.turbine.getDumpingMode)
unit.connect_holding_reg(turbine.setDumpingMode, turbine.getDumpingMode)
return self.rtu.interface()
return unit.interface()
end
return turbinev_rtu

View File

@@ -9,7 +9,7 @@ local MODBUS_EXCODE = types.MODBUS_EXCODE
-- new modbus comms handler object
---@param rtu_dev rtu_device|rtu_rs_device RTU device
---@param use_parallel_read boolean whether or not to use parallel calls when reading
modbus.new = function (rtu_dev, use_parallel_read)
function modbus.new(rtu_dev, use_parallel_read)
local self = {
rtu = rtu_dev,
use_parallel = use_parallel_read
@@ -23,7 +23,7 @@ modbus.new = function (rtu_dev, use_parallel_read)
---@param c_addr_start integer
---@param count integer
---@return boolean ok, table readings
local _1_read_coils = function (c_addr_start, count)
local function _1_read_coils(c_addr_start, count)
local tasks = {}
local readings = {}
local access_fault = false
@@ -69,7 +69,7 @@ modbus.new = function (rtu_dev, use_parallel_read)
---@param di_addr_start integer
---@param count integer
---@return boolean ok, table readings
local _2_read_discrete_inputs = function (di_addr_start, count)
local function _2_read_discrete_inputs(di_addr_start, count)
local tasks = {}
local readings = {}
local access_fault = false
@@ -115,7 +115,7 @@ modbus.new = function (rtu_dev, use_parallel_read)
---@param hr_addr_start integer
---@param count integer
---@return boolean ok, table readings
local _3_read_multiple_holding_registers = function (hr_addr_start, count)
local function _3_read_multiple_holding_registers(hr_addr_start, count)
local tasks = {}
local readings = {}
local access_fault = false
@@ -161,7 +161,7 @@ modbus.new = function (rtu_dev, use_parallel_read)
---@param ir_addr_start integer
---@param count integer
---@return boolean ok, table readings
local _4_read_input_registers = function (ir_addr_start, count)
local function _4_read_input_registers(ir_addr_start, count)
local tasks = {}
local readings = {}
local access_fault = false
@@ -207,7 +207,7 @@ modbus.new = function (rtu_dev, use_parallel_read)
---@param c_addr integer
---@param value any
---@return boolean ok, MODBUS_EXCODE|nil
local _5_write_single_coil = function (c_addr, value)
local function _5_write_single_coil(c_addr, value)
local response = nil
local _, coils, _, _ = self.rtu.io_count()
local return_ok = c_addr <= coils
@@ -229,7 +229,7 @@ modbus.new = function (rtu_dev, use_parallel_read)
---@param hr_addr integer
---@param value any
---@return boolean ok, MODBUS_EXCODE|nil
local _6_write_single_holding_register = function (hr_addr, value)
local function _6_write_single_holding_register(hr_addr, value)
local response = nil
local _, _, _, hold_regs = self.rtu.io_count()
local return_ok = hr_addr <= hold_regs
@@ -251,7 +251,7 @@ modbus.new = function (rtu_dev, use_parallel_read)
---@param c_addr_start integer
---@param values any
---@return boolean ok, MODBUS_EXCODE|nil
local _15_write_multiple_coils = function (c_addr_start, values)
local function _15_write_multiple_coils(c_addr_start, values)
local response = nil
local _, coils, _, _ = self.rtu.io_count()
local count = #values
@@ -278,7 +278,7 @@ modbus.new = function (rtu_dev, use_parallel_read)
---@param hr_addr_start integer
---@param values any
---@return boolean ok, MODBUS_EXCODE|nil
local _16_write_multiple_holding_registers = function (hr_addr_start, values)
local function _16_write_multiple_holding_registers(hr_addr_start, values)
local response = nil
local _, _, _, hold_regs = self.rtu.io_count()
local count = #values
@@ -305,7 +305,7 @@ modbus.new = function (rtu_dev, use_parallel_read)
-- validate a request without actually executing it
---@param packet modbus_frame
---@return boolean return_code, modbus_packet reply
public.check_request = function (packet)
function public.check_request(packet)
local return_code = true
local response = { MODBUS_EXCODE.ACKNOWLEDGE }
@@ -332,10 +332,9 @@ modbus.new = function (rtu_dev, use_parallel_read)
-- default is to echo back
local func_code = packet.func_code
if not return_code then
-- echo back with error flag
func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
end
-- echo back with error flag, on success the "error" will be acknowledgement
func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
-- create reply
local reply = comms.modbus_packet()
@@ -347,7 +346,7 @@ modbus.new = function (rtu_dev, use_parallel_read)
-- handle a MODBUS TCP packet and generate a reply
---@param packet modbus_frame
---@return boolean return_code, modbus_packet reply
public.handle_packet = function (packet)
function public.handle_packet(packet)
local return_code = true
local response = nil
@@ -400,40 +399,40 @@ modbus.new = function (rtu_dev, use_parallel_read)
return return_code, reply
end
-- return a SERVER_DEVICE_BUSY error reply
---@return modbus_packet reply
public.reply__srv_device_busy = function (packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.SERVER_DEVICE_BUSY }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
-- return a NEG_ACKNOWLEDGE error reply
---@return modbus_packet reply
public.reply__neg_ack = function (packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.NEG_ACKNOWLEDGE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
-- return a GATEWAY_PATH_UNAVAILABLE error reply
---@return modbus_packet reply
public.reply__gw_unavailable = function (packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
return public
end
-- return a SERVER_DEVICE_BUSY error reply
---@return modbus_packet reply
function modbus.reply__srv_device_busy(packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.SERVER_DEVICE_BUSY }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
-- return a NEG_ACKNOWLEDGE error reply
---@return modbus_packet reply
function modbus.reply__neg_ack(packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.NEG_ACKNOWLEDGE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
-- return a GATEWAY_PATH_UNAVAILABLE error reply
---@return modbus_packet reply
function modbus.reply__gw_unavailable(packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
return modbus

View File

@@ -20,7 +20,7 @@ local print_ts = util.print_ts
local println_ts = util.println_ts
-- create a new RTU
rtu.init_unit = function ()
function rtu.init_unit()
local self = {
discrete_inputs = {},
coils = {},
@@ -38,13 +38,13 @@ rtu.init_unit = function ()
local protected = {}
-- refresh IO count
local _count_io = function ()
local function _count_io()
self.io_count_cache = { #self.discrete_inputs, #self.coils, #self.input_regs, #self.holding_regs }
end
-- return IO count
---@return integer discrete_inputs, integer coils, integer input_regs, integer holding_regs
public.io_count = function ()
function public.io_count()
return self.io_count_cache[1], self.io_count_cache[2], self.io_count_cache[3], self.io_count_cache[4]
end
@@ -53,7 +53,7 @@ rtu.init_unit = function ()
-- connect discrete input
---@param f function
---@return integer count count of discrete inputs
protected.connect_di = function (f)
function protected.connect_di(f)
insert(self.discrete_inputs, { read = f })
_count_io()
return #self.discrete_inputs
@@ -62,7 +62,7 @@ rtu.init_unit = function ()
-- read discrete input
---@param di_addr integer
---@return any value, boolean access_fault
public.read_di = function (di_addr)
function public.read_di(di_addr)
ppm.clear_fault()
local value = self.discrete_inputs[di_addr].read()
return value, ppm.is_faulted()
@@ -74,7 +74,7 @@ rtu.init_unit = function ()
---@param f_read function
---@param f_write function
---@return integer count count of coils
protected.connect_coil = function (f_read, f_write)
function protected.connect_coil(f_read, f_write)
insert(self.coils, { read = f_read, write = f_write })
_count_io()
return #self.coils
@@ -83,7 +83,7 @@ rtu.init_unit = function ()
-- read coil
---@param coil_addr integer
---@return any value, boolean access_fault
public.read_coil = function (coil_addr)
function public.read_coil(coil_addr)
ppm.clear_fault()
local value = self.coils[coil_addr].read()
return value, ppm.is_faulted()
@@ -93,7 +93,7 @@ rtu.init_unit = function ()
---@param coil_addr integer
---@param value any
---@return boolean access_fault
public.write_coil = function (coil_addr, value)
function public.write_coil(coil_addr, value)
ppm.clear_fault()
self.coils[coil_addr].write(value)
return ppm.is_faulted()
@@ -104,7 +104,7 @@ rtu.init_unit = function ()
-- connect input register
---@param f function
---@return integer count count of input registers
protected.connect_input_reg = function (f)
function protected.connect_input_reg(f)
insert(self.input_regs, { read = f })
_count_io()
return #self.input_regs
@@ -113,7 +113,7 @@ rtu.init_unit = function ()
-- read input register
---@param reg_addr integer
---@return any value, boolean access_fault
public.read_input_reg = function (reg_addr)
function public.read_input_reg(reg_addr)
ppm.clear_fault()
local value = self.input_regs[reg_addr].read()
return value, ppm.is_faulted()
@@ -125,7 +125,7 @@ rtu.init_unit = function ()
---@param f_read function
---@param f_write function
---@return integer count count of holding registers
protected.connect_holding_reg = function (f_read, f_write)
function protected.connect_holding_reg(f_read, f_write)
insert(self.holding_regs, { read = f_read, write = f_write })
_count_io()
return #self.holding_regs
@@ -134,7 +134,7 @@ rtu.init_unit = function ()
-- read holding register
---@param reg_addr integer
---@return any value, boolean access_fault
public.read_holding_reg = function (reg_addr)
function public.read_holding_reg(reg_addr)
ppm.clear_fault()
local value = self.holding_regs[reg_addr].read()
return value, ppm.is_faulted()
@@ -144,7 +144,7 @@ rtu.init_unit = function ()
---@param reg_addr integer
---@param value any
---@return boolean access_fault
public.write_holding_reg = function (reg_addr, value)
function public.write_holding_reg(reg_addr, value)
ppm.clear_fault()
self.holding_regs[reg_addr].write(value)
return ppm.is_faulted()
@@ -153,7 +153,7 @@ rtu.init_unit = function ()
-- public RTU device access
-- get the public interface to this RTU
protected.interface = function ()
function protected.interface()
return public
end
@@ -166,7 +166,7 @@ end
---@param local_port integer
---@param server_port integer
---@param conn_watchdog watchdog
rtu.comms = function (version, modem, local_port, server_port, conn_watchdog)
function rtu.comms(version, modem, local_port, server_port, conn_watchdog)
local self = {
version = version,
seq_num = 0,
@@ -193,7 +193,7 @@ rtu.comms = function (version, modem, local_port, server_port, conn_watchdog)
-- send a scada management packet
---@param msg_type SCADA_MGMT_TYPES
---@param msg table
local _send = function (msg_type, msg)
local function _send(msg_type, msg)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
@@ -206,7 +206,7 @@ rtu.comms = function (version, modem, local_port, server_port, conn_watchdog)
-- keep alive ack
---@param srv_time integer
local _send_keep_alive_ack = function (srv_time)
local function _send_keep_alive_ack(srv_time)
_send(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() })
end
@@ -214,7 +214,7 @@ rtu.comms = function (version, modem, local_port, server_port, conn_watchdog)
-- send a MODBUS TCP packet
---@param m_pkt modbus_packet
public.send_modbus = function (m_pkt)
function public.send_modbus(m_pkt)
local s_pkt = comms.scada_packet()
s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable())
self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable())
@@ -224,7 +224,7 @@ rtu.comms = function (version, modem, local_port, server_port, conn_watchdog)
-- reconnect a newly connected modem
---@param modem table
---@diagnostic disable-next-line: redefined-local
public.reconnect_modem = function (modem)
function public.reconnect_modem(modem)
self.modem = modem
-- open modem
@@ -235,14 +235,14 @@ rtu.comms = function (version, modem, local_port, server_port, conn_watchdog)
-- unlink from the server
---@param rtu_state rtu_state
public.unlink = function (rtu_state)
function public.unlink(rtu_state)
rtu_state.linked = false
self.r_seq_num = nil
end
-- close the connection to the server
---@param rtu_state rtu_state
public.close = function (rtu_state)
function public.close(rtu_state)
self.conn_watchdog.cancel()
public.unlink(rtu_state)
_send(SCADA_MGMT_TYPES.CLOSE, {})
@@ -250,7 +250,7 @@ rtu.comms = function (version, modem, local_port, server_port, conn_watchdog)
-- send capability advertisement
---@param units table
public.send_advertisement = function (units)
function public.send_advertisement(units)
local advertisement = { self.version }
for i = 1, #units do
@@ -282,7 +282,7 @@ rtu.comms = function (version, modem, local_port, server_port, conn_watchdog)
---@param message any
---@param distance integer
---@return modbus_frame|mgmt_frame|nil packet
public.parse_packet = function(side, sender, reply_to, message, distance)
function public.parse_packet(side, sender, reply_to, message, distance)
local pkt = nil
local s_pkt = comms.scada_packet()
@@ -314,7 +314,7 @@ rtu.comms = function (version, modem, local_port, server_port, conn_watchdog)
---@param packet modbus_frame|mgmt_frame
---@param units table
---@param rtu_state rtu_state
public.handle_packet = function(packet, units, rtu_state)
function public.handle_packet(packet, units, rtu_state)
if packet ~= nil then
-- check sequence number
if self.r_seq_num == nil then
@@ -353,7 +353,7 @@ rtu.comms = function (version, modem, local_port, server_port, conn_watchdog)
-- check if there are more than 3 active transactions
-- still queue the packet, but this may indicate a problem
if unit.pkt_queue.length() > 3 then
reply = unit.modbus_io.reply__srv_device_busy(packet)
reply = modbus.reply__srv_device_busy(packet)
log.debug("queueing new request with " .. unit.pkt_queue.length() ..
" transactions already in the queue" .. unit_dbg_tag)
end
@@ -383,7 +383,7 @@ rtu.comms = function (version, modem, local_port, server_port, conn_watchdog)
log.warning("RTU KEEP_ALIVE trip time > 500ms (" .. trip_time .. "ms)")
end
-- log.debug("RTU RTT = ".. trip_time .. "ms")
-- log.debug("RTU RTT = " .. trip_time .. "ms")
_send_keep_alive_ack(timestamp)
else

View File

@@ -4,27 +4,28 @@
require("/initenv").init_env()
local log = require("scada-common.log")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local config = require("rtu.config")
local modbus = require("rtu.modbus")
local rtu = require("rtu.rtu")
local config = require("rtu.config")
local modbus = require("rtu.modbus")
local rtu = require("rtu.rtu")
local threads = require("rtu.threads")
local redstone_rtu = require("rtu.dev.redstone_rtu")
local boiler_rtu = require("rtu.dev.boiler_rtu")
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
local redstone_rtu = require("rtu.dev.redstone_rtu")
local boiler_rtu = require("rtu.dev.boiler_rtu")
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
local energymachine_rtu = require("rtu.dev.energymachine_rtu")
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
local turbine_rtu = require("rtu.dev.turbine_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local envd_rtu = require("rtu.dev.envd_rtu")
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
local turbine_rtu = require("rtu.dev.turbine_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "alpha-v0.7.1"
local RTU_VERSION = "beta-v0.7.12"
local rtu_t = types.rtu_t
@@ -33,12 +34,30 @@ local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
----------------------------------------
-- config validation
----------------------------------------
local cfv = util.new_validator()
cfv.assert_port(config.SERVER_PORT)
cfv.assert_port(config.LISTEN_PORT)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
cfv.assert_type_table(config.RTU_DEVICES)
cfv.assert_type_table(config.RTU_REDSTONE)
assert(cfv.valid(), "bad config file: missing/invalid fields")
----------------------------------------
-- log init
----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE)
log.info("========================================")
log.info("BOOTING rtu.startup " .. RTU_VERSION)
log.info("========================================")
println(">> RTU " .. RTU_VERSION .. " <<")
println(">> RTU GATEWAY " .. RTU_VERSION .. " <<")
----------------------------------------
-- startup
@@ -80,7 +99,7 @@ local smem_sys = __shared_memory.rtu_sys
-- get modem
if smem_dev.modem == nil then
println("boot> wireless modem not found")
log.warning("no wireless modem on startup")
log.fatal("no wireless modem on startup")
return
end
@@ -93,194 +112,256 @@ local units = __shared_memory.rtu_sys.units
local rtu_redstone = config.RTU_REDSTONE
local rtu_devices = config.RTU_DEVICES
-- redstone interfaces
for entry_idx = 1, #rtu_redstone do
local rs_rtu = redstone_rtu.new()
local io_table = rtu_redstone[entry_idx].io
local io_reactor = rtu_redstone[entry_idx].for_reactor
-- configure RTU gateway based on config file definitions
local function configure()
-- redstone interfaces
for entry_idx = 1, #rtu_redstone do
local rs_rtu = redstone_rtu.new()
local io_table = rtu_redstone[entry_idx].io
local io_reactor = rtu_redstone[entry_idx].for_reactor
local capabilities = {}
log.debug("init> starting redstone RTU I/O linking for reactor " .. io_reactor .. "...")
local continue = true
for i = 1, #units do
local unit = units[i] ---@type rtu_unit_registry_entry
if unit.reactor == io_reactor and unit.type == rtu_t.redstone then
-- duplicate entry
log.warning("init> skipping definition block #" .. entry_idx .. " for reactor " .. io_reactor .. " with already defined redstone I/O")
continue = false
break
-- CHECK: reactor ID must be >= to 1
if (not util.is_int(io_reactor)) or (io_reactor <= 0) then
println(util.c("configure> redstone entry #", entry_idx, " : ", io_reactor, " isn't an integer >= 1"))
return false
end
end
if continue then
for i = 1, #io_table do
local valid = false
local conf = io_table[i]
-- CHECK: io table exists
if type(io_table) ~= "table" then
println(util.c("configure> redstone entry #", entry_idx, " no IO table found"))
return false
end
-- verify configuration
if rsio.is_valid_channel(conf.channel) and rsio.is_valid_side(conf.side) then
if conf.bundled_color then
valid = rsio.is_color(conf.bundled_color)
else
valid = true
end
end
local capabilities = {}
if not valid then
local message = "init> invalid redstone definition at index " .. i .. " in definition block #" .. entry_idx ..
" (for reactor " .. io_reactor .. ")"
println_ts(message)
log.debug(util.c("configure> starting redstone RTU I/O linking for reactor ", io_reactor, "..."))
local continue = true
-- check for duplicate entries
for i = 1, #units do
local unit = units[i] ---@type rtu_unit_registry_entry
if unit.reactor == io_reactor and unit.type == rtu_t.redstone then
-- duplicate entry
local message = util.c("configure> skipping definition block #", entry_idx, " for reactor ", io_reactor,
" with already defined redstone I/O")
println(message)
log.warning(message)
else
-- link redstone in RTU
local mode = rsio.get_io_mode(conf.channel)
if mode == rsio.IO_MODE.DIGITAL_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, conf.channel) then
log.warning("init> skipping duplicate input for channel " .. rsio.to_string(conf.channel) .. " on side " .. conf.side)
else
rs_rtu.link_di(conf.side, conf.bundled_color)
end
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
rs_rtu.link_do(conf.channel, conf.side, conf.bundled_color)
elseif mode == rsio.IO_MODE.ANALOG_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, conf.channel) then
log.warning("init> skipping duplicate input for channel " .. rsio.to_string(conf.channel) .. " on side " .. conf.side)
else
rs_rtu.link_ai(conf.side)
end
elseif mode == rsio.IO_MODE.ANALOG_OUT then
rs_rtu.link_ao(conf.side)
else
-- should be unreachable code, we already validated channels
log.error("init> fell through if chain attempting to identify IO mode", true)
break
end
table.insert(capabilities, conf.channel)
log.debug("init> linked redstone " .. #capabilities .. ": " .. rsio.to_string(conf.channel) .. " (" .. conf.side ..
") for reactor " .. io_reactor)
continue = false
break
end
end
---@class rtu_unit_registry_entry
local unit = {
name = "redstone_io",
type = rtu_t.redstone,
index = entry_idx,
reactor = io_reactor,
device = capabilities, -- use device field for redstone channels
rtu = rs_rtu,
modbus_io = modbus.new(rs_rtu, false),
pkt_queue = nil,
thread = nil
}
-- not a duplicate
if continue then
for i = 1, #io_table do
local valid = false
local conf = io_table[i]
table.insert(units, unit)
-- verify configuration
if rsio.is_valid_channel(conf.channel) and rsio.is_valid_side(conf.side) then
if conf.bundled_color then
valid = rsio.is_color(conf.bundled_color)
else
valid = true
end
end
log.debug("init> initialized RTU unit #" .. #units .. ": redstone_io (redstone) [1] for reactor " .. io_reactor)
end
end
if not valid then
local message = util.c("configure> invalid redstone definition at index ", i, " in definition block #", entry_idx,
" (for reactor ", io_reactor, ")")
println(message)
log.error(message)
return false
else
-- link redstone in RTU
local mode = rsio.get_io_mode(conf.channel)
if mode == rsio.IO_MODE.DIGITAL_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, conf.channel) then
local message = util.c("configure> skipping duplicate input for channel ", rsio.to_string(conf.channel), " on side ", conf.side)
println(message)
log.warning(message)
else
rs_rtu.link_di(conf.side, conf.bundled_color)
end
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
rs_rtu.link_do(conf.channel, conf.side, conf.bundled_color)
elseif mode == rsio.IO_MODE.ANALOG_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, conf.channel) then
local message = util.c("configure> skipping duplicate input for channel ", rsio.to_string(conf.channel), " on side ", conf.side)
println(message)
log.warning(message)
else
rs_rtu.link_ai(conf.side)
end
elseif mode == rsio.IO_MODE.ANALOG_OUT then
rs_rtu.link_ao(conf.side)
else
-- should be unreachable code, we already validated channels
log.error("configure> fell through if chain attempting to identify IO mode", true)
println("configure> encountered a software error, check logs")
return false
end
-- mounted peripherals
for i = 1, #rtu_devices do
local device = ppm.get_periph(rtu_devices[i].name)
table.insert(capabilities, conf.channel)
if device == nil then
local message = "init> '" .. rtu_devices[i].name .. "' not found"
println_ts(message)
log.warning(message)
else
local type = ppm.get_type(rtu_devices[i].name)
local rtu_iface = nil ---@type rtu_device
local rtu_type = ""
log.debug(util.c("configure> linked redstone ", #capabilities, ": ", rsio.to_string(conf.channel),
" (", conf.side, ") for reactor ", io_reactor))
end
end
if type == "boiler" then
-- boiler multiblock
rtu_type = rtu_t.boiler
rtu_iface = boiler_rtu.new(device)
elseif type == "boilerValve" then
-- boiler multiblock (10.1+)
rtu_type = rtu_t.boiler_valve
rtu_iface = boilerv_rtu.new(device)
elseif type == "turbine" then
-- turbine multiblock
rtu_type = rtu_t.turbine
rtu_iface = turbine_rtu.new(device)
elseif type == "turbineValve" then
-- turbine multiblock (10.1+)
rtu_type = rtu_t.turbine_valve
rtu_iface = turbinev_rtu.new(device)
elseif type == "mekanismMachine" then
-- assumed to be an induction matrix multiblock, pre Mekanism 10.1
-- also works with energy cubes
rtu_type = rtu_t.energy_machine
rtu_iface = energymachine_rtu.new(device)
elseif type == "inductionPort" then
-- induction matrix multiblock (10.1+)
rtu_type = rtu_t.induction_matrix
rtu_iface = imatrix_rtu.new(device)
else
local message = "init> device '" .. rtu_devices[i].name .. "' is not a known type (" .. type .. ")"
println_ts(message)
log.warning(message)
end
if rtu_iface ~= nil then
---@class rtu_unit_registry_entry
local rtu_unit = {
name = rtu_devices[i].name,
type = rtu_type,
index = rtu_devices[i].index,
reactor = rtu_devices[i].for_reactor,
device = device,
rtu = rtu_iface,
modbus_io = modbus.new(rtu_iface, true),
pkt_queue = mqueue.new(),
local unit = {
name = "redstone_io",
type = rtu_t.redstone,
index = entry_idx,
reactor = io_reactor,
device = capabilities, -- use device field for redstone channels
rtu = rs_rtu,
modbus_io = modbus.new(rs_rtu, false),
pkt_queue = nil,
thread = nil
}
rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
table.insert(units, unit)
table.insert(units, rtu_unit)
log.debug("init> initialized RTU unit #" .. #units .. ": " .. rtu_devices[i].name .. " (" .. rtu_type .. ") [" ..
rtu_devices[i].index .. "] for reactor " .. rtu_devices[i].for_reactor)
log.debug(util.c("init> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for reactor ", io_reactor))
end
end
-- mounted peripherals
for i = 1, #rtu_devices do
local name = rtu_devices[i].name
local index = rtu_devices[i].index
local for_reactor = rtu_devices[i].for_reactor
-- CHECK: name is a string
if type(name) ~= "string" then
println(util.c("configure> device entry #", i, ": device ", name, " isn't a string"))
return false
end
-- CHECK: index is an integer >= 1
if (not util.is_int(index)) or (index <= 0) then
println(util.c("configure> device entry #", i, ": index ", index, " isn't an integer >= 1"))
return false
end
-- CHECK: reactor is an integer >= 1
if (not util.is_int(for_reactor)) or (for_reactor <= 0) then
println(util.c("configure> device entry #", i, ": reactor ", for_reactor, " isn't an integer >= 1"))
return false
end
local device = ppm.get_periph(name)
if device == nil then
local message = util.c("configure> '", name, "' not found")
println(message)
log.fatal(message)
return false
else
local type = ppm.get_type(name)
local rtu_iface = nil ---@type rtu_device
local rtu_type = ""
if type == "boiler" then
-- boiler multiblock
rtu_type = rtu_t.boiler
rtu_iface = boiler_rtu.new(device)
elseif type == "boilerValve" then
-- boiler multiblock (10.1+)
rtu_type = rtu_t.boiler_valve
rtu_iface = boilerv_rtu.new(device)
elseif type == "turbine" then
-- turbine multiblock
rtu_type = rtu_t.turbine
rtu_iface = turbine_rtu.new(device)
elseif type == "turbineValve" then
-- turbine multiblock (10.1+)
rtu_type = rtu_t.turbine_valve
rtu_iface = turbinev_rtu.new(device)
elseif type == "mekanismMachine" then
-- assumed to be an induction matrix multiblock, pre Mekanism 10.1
-- also works with energy cubes
rtu_type = rtu_t.energy_machine
rtu_iface = energymachine_rtu.new(device)
elseif type == "inductionPort" then
-- induction matrix multiblock (10.1+)
rtu_type = rtu_t.induction_matrix
rtu_iface = imatrix_rtu.new(device)
elseif type == "environmentDetector" then
-- advanced peripherals environment detector
rtu_type = rtu_t.env_detector
rtu_iface = envd_rtu.new(device)
else
local message = util.c("configure> device '", name, "' is not a known type (", type, ")")
println_ts(message)
log.fatal(message)
return false
end
if rtu_iface ~= nil then
---@class rtu_unit_registry_entry
local rtu_unit = {
name = name,
type = rtu_type,
index = index,
reactor = for_reactor,
device = device,
rtu = rtu_iface,
modbus_io = modbus.new(rtu_iface, true),
pkt_queue = mqueue.new(),
thread = nil
}
rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
table.insert(units, rtu_unit)
log.debug(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", rtu_type, ") [", index, "] for reactor ", for_reactor))
end
end
end
-- we made it through all that trusting-user-to-write-a-config-file chaos
return true
end
----------------------------------------
-- start system
----------------------------------------
-- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(5)
log.debug("boot> conn watchdog started")
log.debug("boot> running configure()")
-- setup comms
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_sys.conn_watchdog)
log.debug("boot> comms init")
if configure() then
-- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(5)
log.debug("boot> conn watchdog started")
-- init threads
local main_thread = threads.thread__main(__shared_memory)
local comms_thread = threads.thread__comms(__shared_memory)
-- setup comms
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_sys.conn_watchdog)
log.debug("boot> comms init")
-- assemble thread list
local _threads = { main_thread.p_exec, comms_thread.p_exec }
for i = 1, #units do
if units[i].thread ~= nil then
table.insert(_threads, units[i].thread.p_exec)
-- init threads
local main_thread = threads.thread__main(__shared_memory)
local comms_thread = threads.thread__comms(__shared_memory)
-- assemble thread list
local _threads = { main_thread.p_exec, comms_thread.p_exec }
for i = 1, #units do
if units[i].thread ~= nil then
table.insert(_threads, units[i].thread.p_exec)
end
end
end
-- run threads
parallel.waitForAll(table.unpack(_threads))
-- run threads
parallel.waitForAll(table.unpack(_threads))
else
println("configuration failed, exiting...")
end
println_ts("exited")
log.info("exited")

View File

@@ -27,11 +27,11 @@ local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
-- main thread
---@param smem rtu_shared_memory
threads.thread__main = function (smem)
function threads.thread__main(smem)
local public = {} ---@class thread
-- execute thread
public.exec = function ()
function public.exec()
log.debug("main thread start")
-- main loop clock
@@ -44,13 +44,15 @@ threads.thread__main = function (smem)
local conn_watchdog = smem.rtu_sys.conn_watchdog
local units = smem.rtu_sys.units
-- start unlinked (in case of restart)
rtu_comms.unlink(rtu_state)
-- start clock
loop_clock.start()
-- event loop
while true do
---@diagnostic disable-next-line: undefined-field
local event, param1, param2, param3, param4, param5 = os.pullEventRaw()
local event, param1, param2, param3, param4, param5 = util.pull_event()
if event == "timer" and loop_clock.is_clock(param1) then
-- start next clock timer
@@ -155,7 +157,7 @@ threads.thread__main = function (smem)
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
public.p_exec = function ()
function public.p_exec()
local rtu_state = smem.rtu_state
while not rtu_state.shutdown do
@@ -176,11 +178,11 @@ end
-- communications handler thread
---@param smem rtu_shared_memory
threads.thread__comms = function (smem)
function threads.thread__comms(smem)
local public = {} ---@class thread
-- execute thread
public.exec = function ()
function public.exec()
log.debug("comms thread start")
-- load in from shared memory
@@ -227,7 +229,7 @@ threads.thread__comms = function (smem)
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
public.p_exec = function ()
function public.p_exec()
local rtu_state = smem.rtu_state
while not rtu_state.shutdown do
@@ -249,12 +251,12 @@ end
-- per-unit communications handler thread
---@param smem rtu_shared_memory
---@param unit rtu_unit_registry_entry
threads.thread__unit_comms = function (smem, unit)
function threads.thread__unit_comms(smem, unit)
local public = {} ---@class thread
-- execute thread
public.exec = function ()
log.debug("rtu unit thread start -> " .. unit.name .. "(" .. unit.type .. ")")
function public.exec()
log.debug("rtu unit thread start -> " .. unit.type .. "(" .. unit.name .. ")")
-- load in from shared memory
local rtu_state = smem.rtu_state
@@ -287,7 +289,7 @@ threads.thread__unit_comms = function (smem, unit)
-- check for termination request
if rtu_state.shutdown then
log.info("rtu unit thread exiting -> " .. unit.name .. "(" .. unit.type .. ")")
log.info("rtu unit thread exiting -> " .. unit.type .. "(" .. unit.name .. ")")
break
end
@@ -297,7 +299,7 @@ threads.thread__unit_comms = function (smem, unit)
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
public.p_exec = function ()
function public.p_exec()
local rtu_state = smem.rtu_state
while not rtu_state.shutdown do
@@ -307,7 +309,7 @@ threads.thread__unit_comms = function (smem, unit)
end
if not rtu_state.shutdown then
log.info("rtu unit thread " .. unit.name .. "(" .. unit.type .. ") restarting in 5 seconds...")
log.info(util.c("rtu unit thread ", unit.type, "(", unit.name, ") restarting in 5 seconds..."))
util.psleep(5)
end
end

View File

@@ -17,7 +17,7 @@ alarm.SEVERITY = SEVERITY
-- severity integer to string
---@param severity SEVERITY
alarm.severity_to_string = function (severity)
function alarm.severity_to_string(severity)
if severity == SEVERITY.INFO then
return "INFO"
elseif severity == SEVERITY.WARNING then
@@ -39,7 +39,7 @@ end
---@param severity SEVERITY
---@param device string
---@param message string
alarm.scada_alarm = function (severity, device, message)
function alarm.scada_alarm(severity, device, message)
local self = {
time = util.time(),
ts_string = os.date("[%H:%M:%S]"),
@@ -53,12 +53,12 @@ alarm.scada_alarm = function (severity, device, message)
-- format the alarm as a string
---@return string message
public.format = function ()
return self.ts_string .. " [" .. alarm.severity_to_string(self.severity) .. "] (" .. self.device ") >> " .. self.message
function public.format()
return self.ts_string .. " [" .. alarm.severity_to_string(self.severity) .. "] (" .. self.device .. ") >> " .. self.message
end
-- get alarm properties
public.properties = function ()
function public.properties()
return {
time = self.time,
severity = self.severity,

View File

@@ -2,7 +2,7 @@
-- Communications
--
local log = require("scada-common.log")
local log = require("scada-common.log")
local types = require("scada-common.types")
---@class comms
@@ -16,7 +16,7 @@ local PROTOCOLS = {
MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol
RPLC = 1, -- reactor PLC protocol
SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc
COORD_DATA = 3, -- data/control packets for coordinators to/from supervisory controllers
SCADA_CRDN = 3, -- data/control packets for coordinators to/from supervisory controllers
COORD_API = 4 -- data/control packets for pocket computers to/from coordinators
}
@@ -48,6 +48,20 @@ local SCADA_MGMT_TYPES = {
REMOTE_LINKED = 3 -- remote device linked
}
---@alias SCADA_CRDN_TYPES integer
local SCADA_CRDN_TYPES = {
ESTABLISH = 0, -- initial greeting
STRUCT_BUILDS = 1, -- mekanism structure builds
UNIT_STATUSES = 2, -- state of reactor units
COMMAND_UNIT = 3, -- command a reactor unit
ALARM = 4 -- alarm signaling
}
---@alias CAPI_TYPES integer
local CAPI_TYPES = {
ESTABLISH = 0 -- initial greeting
}
---@alias RTU_UNIT_TYPES integer
local RTU_UNIT_TYPES = {
REDSTONE = 0, -- redstone I/O
@@ -56,17 +70,21 @@ local RTU_UNIT_TYPES = {
TURBINE = 3, -- turbine
TURBINE_VALVE = 4, -- turbine, mekanism 10.1+
EMACHINE = 5, -- energy machine
IMATRIX = 6 -- induction matrix
IMATRIX = 6, -- induction matrix
SPS = 7, -- SPS
SNA = 8, -- SNA
ENV_DETECTOR = 9 -- environment detector
}
comms.PROTOCOLS = PROTOCOLS
comms.RPLC_TYPES = RPLC_TYPES
comms.RPLC_LINKING = RPLC_LINKING
comms.SCADA_MGMT_TYPES = SCADA_MGMT_TYPES
comms.SCADA_CRDN_TYPES = SCADA_CRDN_TYPES
comms.RTU_UNIT_TYPES = RTU_UNIT_TYPES
-- generic SCADA packet object
comms.scada_packet = function ()
function comms.scada_packet()
local self = {
modem_msg_in = nil,
valid = false,
@@ -84,7 +102,7 @@ comms.scada_packet = function ()
---@param seq_num integer
---@param protocol PROTOCOLS
---@param payload table
public.make = function (seq_num, protocol, payload)
function public.make(seq_num, protocol, payload)
self.valid = true
self.seq_num = seq_num
self.protocol = protocol
@@ -99,7 +117,7 @@ comms.scada_packet = function ()
---@param reply_to integer
---@param message any
---@param distance integer
public.receive = function (side, sender, reply_to, message, distance)
function public.receive(side, sender, reply_to, message, distance)
self.modem_msg_in = {
iface = side,
s_port = sender,
@@ -112,12 +130,15 @@ comms.scada_packet = function ()
if type(self.raw) == "table" then
if #self.raw >= 3 then
self.valid = true
self.seq_num = self.raw[1]
self.protocol = self.raw[2]
self.length = #self.raw[3]
self.payload = self.raw[3]
end
self.valid = type(self.seq_num) == "number" and
type(self.protocol) == "number" and
type(self.payload) == "table"
end
return self.valid
@@ -125,25 +146,25 @@ comms.scada_packet = function ()
-- public accessors --
public.modem_event = function () return self.modem_msg_in end
public.raw_sendable = function () return self.raw end
function public.modem_event() return self.modem_msg_in end
function public.raw_sendable() return self.raw end
public.local_port = function () return self.modem_msg_in.s_port end
public.remote_port = function () return self.modem_msg_in.r_port end
function public.local_port() return self.modem_msg_in.s_port end
function public.remote_port() return self.modem_msg_in.r_port end
public.is_valid = function () return self.valid end
function public.is_valid() return self.valid end
public.seq_num = function () return self.seq_num end
public.protocol = function () return self.protocol end
public.length = function () return self.length end
public.data = function () return self.payload end
function public.seq_num() return self.seq_num end
function public.protocol() return self.protocol end
function public.length() return self.length end
function public.data() return self.payload end
return public
end
-- MODBUS packet
-- modeled after MODBUS TCP packet
comms.modbus_packet = function ()
function comms.modbus_packet()
local self = {
frame = nil,
raw = nil,
@@ -162,24 +183,28 @@ comms.modbus_packet = function ()
---@param unit_id integer
---@param func_code MODBUS_FCODE
---@param data table
public.make = function (txn_id, unit_id, func_code, data)
self.txn_id = txn_id
self.length = #data
self.unit_id = unit_id
self.func_code = func_code
self.data = data
function public.make(txn_id, unit_id, func_code, data)
if type(data) == "table" then
self.txn_id = txn_id
self.length = #data
self.unit_id = unit_id
self.func_code = func_code
self.data = data
-- populate raw array
self.raw = { self.txn_id, self.unit_id, self.func_code }
for i = 1, self.length do
insert(self.raw, data[i])
-- populate raw array
self.raw = { self.txn_id, self.unit_id, self.func_code }
for i = 1, self.length do
insert(self.raw, data[i])
end
else
log.error("comms.modbus_packet.make(): data not table")
end
end
-- decode a MODBUS packet from a SCADA frame
---@param frame scada_packet
---@return boolean success
public.decode = function (frame)
function public.decode(frame)
if frame then
self.frame = frame
@@ -191,7 +216,11 @@ comms.modbus_packet = function ()
public.make(data[1], data[2], data[3], { table.unpack(data, 4, #data) })
end
return size_ok
local valid = type(self.txn_id) == "number" and
type(self.unit_id) == "number" and
type(self.func_code) == "number"
return size_ok and valid
else
log.debug("attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true)
return false
@@ -203,10 +232,10 @@ comms.modbus_packet = function ()
end
-- get raw to send
public.raw_sendable = function () return self.raw end
function public.raw_sendable() return self.raw end
-- get this packet as a frame with an immutable relation to this object
public.get = function ()
function public.get()
---@class modbus_frame
local frame = {
scada_frame = self.frame,
@@ -224,7 +253,7 @@ comms.modbus_packet = function ()
end
-- reactor PLC packet
comms.rplc_packet = function ()
function comms.rplc_packet()
local self = {
frame = nil,
raw = nil,
@@ -238,7 +267,7 @@ comms.rplc_packet = function ()
local public = {}
-- check that type is known
local _rplc_type_valid = function ()
local function _rplc_type_valid()
return self.type == RPLC_TYPES.LINK_REQ or
self.type == RPLC_TYPES.STATUS or
self.type == RPLC_TYPES.MEK_STRUCT or
@@ -254,24 +283,28 @@ comms.rplc_packet = function ()
---@param id integer
---@param packet_type RPLC_TYPES
---@param data table
public.make = function (id, packet_type, data)
-- packet accessor properties
self.id = id
self.type = packet_type
self.length = #data
self.data = data
function public.make(id, packet_type, data)
if type(data) == "table" then
-- packet accessor properties
self.id = id
self.type = packet_type
self.length = #data
self.data = data
-- populate raw array
self.raw = { self.id, self.type }
for i = 1, #data do
insert(self.raw, data[i])
-- populate raw array
self.raw = { self.id, self.type }
for i = 1, #data do
insert(self.raw, data[i])
end
else
log.error("comms.rplc_packet.make(): data not table")
end
end
-- decode an RPLC packet from a SCADA frame
---@param frame scada_packet
---@return boolean success
public.decode = function (frame)
function public.decode(frame)
if frame then
self.frame = frame
@@ -284,6 +317,8 @@ comms.rplc_packet = function ()
ok = _rplc_type_valid()
end
ok = ok and type(self.id) == "number"
return ok
else
log.debug("attempted RPLC parse of incorrect protocol " .. frame.protocol(), true)
@@ -296,10 +331,10 @@ comms.rplc_packet = function ()
end
-- get raw to send
public.raw_sendable = function () return self.raw end
function public.raw_sendable() return self.raw end
-- get this packet as a frame with an immutable relation to this object
public.get = function ()
function public.get()
---@class rplc_frame
local frame = {
scada_frame = self.frame,
@@ -316,7 +351,7 @@ comms.rplc_packet = function ()
end
-- SCADA management packet
comms.mgmt_packet = function ()
function comms.mgmt_packet()
local self = {
frame = nil,
raw = nil,
@@ -329,7 +364,7 @@ comms.mgmt_packet = function ()
local public = {}
-- check that type is known
local _scada_type_valid = function ()
local function _scada_type_valid()
return self.type == SCADA_MGMT_TYPES.KEEP_ALIVE or
self.type == SCADA_MGMT_TYPES.CLOSE or
self.type == SCADA_MGMT_TYPES.REMOTE_LINKED or
@@ -339,23 +374,27 @@ comms.mgmt_packet = function ()
-- make a SCADA management packet
---@param packet_type SCADA_MGMT_TYPES
---@param data table
public.make = function (packet_type, data)
-- packet accessor properties
self.type = packet_type
self.length = #data
self.data = data
function public.make(packet_type, data)
if type(data) == "table" then
-- packet accessor properties
self.type = packet_type
self.length = #data
self.data = data
-- populate raw array
self.raw = { self.type }
for i = 1, #data do
insert(self.raw, data[i])
-- populate raw array
self.raw = { self.type }
for i = 1, #data do
insert(self.raw, data[i])
end
else
log.error("comms.mgmt_packet.make(): data not table")
end
end
-- decode a SCADA management packet from a SCADA frame
---@param frame scada_packet
---@return boolean success
public.decode = function (frame)
function public.decode(frame)
if frame then
self.frame = frame
@@ -371,7 +410,7 @@ comms.mgmt_packet = function ()
return ok
else
log.debug("attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true)
return false
return false
end
else
log.debug("nil frame encountered", true)
@@ -380,10 +419,10 @@ comms.mgmt_packet = function ()
end
-- get raw to send
public.raw_sendable = function () return self.raw end
function public.raw_sendable() return self.raw end
-- get this packet as a frame with an immutable relation to this object
public.get = function ()
function public.get()
---@class mgmt_frame
local frame = {
scada_frame = self.frame,
@@ -399,8 +438,7 @@ comms.mgmt_packet = function ()
end
-- SCADA coordinator packet
-- @todo
comms.coord_packet = function ()
function comms.crdn_packet()
local self = {
frame = nil,
raw = nil,
@@ -409,49 +447,57 @@ comms.coord_packet = function ()
data = nil
}
---@class coord_packet
---@class crdn_packet
local public = {}
local _coord_type_valid = function ()
-- @todo
return false
-- check that type is known
local function _crdn_type_valid()
return self.type == SCADA_CRDN_TYPES.ESTABLISH or
self.type == SCADA_CRDN_TYPES.STRUCT_BUILDS or
self.type == SCADA_CRDN_TYPES.UNIT_STATUSES or
self.type == SCADA_CRDN_TYPES.COMMAND_UNIT or
self.type == SCADA_CRDN_TYPES.ALARM
end
-- make a coordinator packet
---@param packet_type any
---@param packet_type SCADA_CRDN_TYPES
---@param data table
public.make = function (packet_type, data)
-- packet accessor properties
self.type = packet_type
self.length = #data
self.data = data
function public.make(packet_type, data)
if type(data) == "table" then
-- packet accessor properties
self.type = packet_type
self.length = #data
self.data = data
-- populate raw array
self.raw = { self.type }
for i = 1, #data do
insert(self.raw, data[i])
-- populate raw array
self.raw = { self.type }
for i = 1, #data do
insert(self.raw, data[i])
end
else
log.error("comms.crdn_packet.make(): data not table")
end
end
-- decode a coordinator packet from a SCADA frame
---@param frame scada_packet
---@return boolean success
public.decode = function (frame)
function public.decode(frame)
if frame then
self.frame = frame
if frame.protocol() == PROTOCOLS.COORD_DATA then
if frame.protocol() == PROTOCOLS.SCADA_CRDN then
local ok = frame.length() >= 1
if ok then
local data = frame.data()
public.make(data[1], { table.unpack(data, 2, #data) })
ok = _coord_type_valid()
ok = _crdn_type_valid()
end
return ok
else
log.debug("attempted COORD_DATA parse of incorrect protocol " .. frame.protocol(), true)
log.debug("attempted SCADA_CRDN parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
@@ -461,11 +507,11 @@ comms.coord_packet = function ()
end
-- get raw to send
public.raw_sendable = function () return self.raw end
function public.raw_sendable() return self.raw end
-- get this packet as a frame with an immutable relation to this object
public.get = function ()
---@class coord_frame
function public.get()
---@class crdn_frame
local frame = {
scada_frame = self.frame,
type = self.type,
@@ -481,7 +527,7 @@ end
-- coordinator API (CAPI) packet
-- @todo
comms.capi_packet = function ()
function comms.capi_packet()
local self = {
frame = nil,
raw = nil,
@@ -493,31 +539,35 @@ comms.capi_packet = function ()
---@class capi_packet
local public = {}
local _coord_type_valid = function ()
local function _capi_type_valid()
-- @todo
return false
end
-- make a coordinator API packet
---@param packet_type any
---@param packet_type CAPI_TYPES
---@param data table
public.make = function (packet_type, data)
-- packet accessor properties
self.type = packet_type
self.length = #data
self.data = data
function public.make(packet_type, data)
if type(data) == "table" then
-- packet accessor properties
self.type = packet_type
self.length = #data
self.data = data
-- populate raw array
self.raw = { self.type }
for i = 1, #data do
insert(self.raw, data[i])
-- populate raw array
self.raw = { self.type }
for i = 1, #data do
insert(self.raw, data[i])
end
else
log.error("comms.capi_packet.make(): data not table")
end
end
-- decode a coordinator API packet from a SCADA frame
---@param frame scada_packet
---@return boolean success
public.decode = function (frame)
function public.decode(frame)
if frame then
self.frame = frame
@@ -527,7 +577,7 @@ comms.capi_packet = function ()
if ok then
local data = frame.data()
public.make(data[1], { table.unpack(data, 2, #data) })
ok = _coord_type_valid()
ok = _capi_type_valid()
end
return ok
@@ -542,10 +592,10 @@ comms.capi_packet = function ()
end
-- get raw to send
public.raw_sendable = function () return self.raw end
function public.raw_sendable() return self.raw end
-- get this packet as a frame with an immutable relation to this object
public.get = function ()
function public.get()
---@class capi_frame
local frame = {
scada_frame = self.frame,
@@ -563,7 +613,7 @@ end
-- convert rtu_t to RTU unit type
---@param type rtu_t
---@return RTU_UNIT_TYPES|nil
comms.rtu_t_to_unit_type = function (type)
function comms.rtu_t_to_unit_type(type)
if type == rtu_t.redstone then
return RTU_UNIT_TYPES.REDSTONE
elseif type == rtu_t.boiler then
@@ -586,7 +636,7 @@ end
-- convert RTU unit type to rtu_t
---@param utype RTU_UNIT_TYPES
---@return rtu_t|nil
comms.advert_type_to_rtu_t = function (utype)
function comms.advert_type_to_rtu_t(utype)
if utype == RTU_UNIT_TYPES.REDSTONE then
return rtu_t.redstone
elseif utype == RTU_UNIT_TYPES.BOILER then

245
scada-common/crypto.lua Normal file
View File

@@ -0,0 +1,245 @@
--
-- Cryptographic Communications Engine
--
local aes128 = require("lockbox.cipher.aes128")
local ctr_mode = require("lockbox.cipher.mode.ctr");
local sha1 = require("lockbox.digest.sha1");
local sha2_224 = require("lockbox.digest.sha2_224");
local sha2_256 = require("lockbox.digest.sha2_256");
local pbkdf2 = require("lockbox.kdf.pbkdf2")
local hmac = require("lockbox.mac.hmac")
local zero_pad = require("lockbox.padding.zero");
local stream = require("lockbox.util.stream")
local array = require("lockbox.util.array")
local log = require("scada-common.log")
local util = require("scada-common.util")
local crypto = {}
local c_eng = {
key = nil,
cipher = nil,
decipher = nil,
hmac = nil
}
---@alias hex string
-- initialize cryptographic system
function crypto.init(password, server_port)
local key_deriv = pbkdf2()
-- setup PBKDF2
-- the primary goal is to just turn our password into a 16 byte key
key_deriv.setPassword(password)
key_deriv.setSalt("salty_salt_at_" .. server_port)
key_deriv.setIterations(32)
key_deriv.setBlockLen(8)
key_deriv.setDKeyLen(16)
local start = util.time()
key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha2_256))
key_deriv.finish()
log.dmesg("pbkdf2: key derivation took " .. (util.time() - start) .. "ms", "CRYPTO", colors.yellow)
c_eng.key = array.fromHex(key_deriv.asHex())
-- initialize cipher
c_eng.cipher = ctr_mode.Cipher()
c_eng.cipher.setKey(c_eng.key)
c_eng.cipher.setBlockCipher(aes128)
c_eng.cipher.setPadding(zero_pad);
-- initialize decipher
c_eng.decipher = ctr_mode.Decipher()
c_eng.decipher.setKey(c_eng.key)
c_eng.decipher.setBlockCipher(aes128)
c_eng.decipher.setPadding(zero_pad);
-- initialize HMAC
c_eng.hmac = hmac()
c_eng.hmac.setBlockSize(64)
c_eng.hmac.setDigest(sha1)
c_eng.hmac.setKey(c_eng.key)
log.dmesg("init: completed in " .. (util.time() - start) .. "ms", "CRYPTO", colors.yellow)
end
-- encrypt plaintext
---@param plaintext string
---@return string initial_value, string ciphertext
function crypto.encrypt(plaintext)
local start = util.time()
-- initial value
local iv = {
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255)
}
log.debug("crypto.random: iv random took " .. (util.time() - start) .. "ms")
start = util.time()
c_eng.cipher.init()
c_eng.cipher.update(stream.fromArray(iv))
c_eng.cipher.update(stream.fromString(plaintext))
c_eng.cipher.finish()
local ciphertext = c_eng.cipher.asHex() ---@type hex
log.debug("crypto.encrypt: aes128-ctr-mode took " .. (util.time() - start) .. "ms")
log.debug("ciphertext: " .. util.strval(ciphertext))
return iv, ciphertext
end
-- decrypt ciphertext
---@param iv string CTR initial value
---@param ciphertext string ciphertext hex
---@return string plaintext
function crypto.decrypt(iv, ciphertext)
local start = util.time()
c_eng.decipher.init()
c_eng.decipher.update(stream.fromArray(iv))
c_eng.decipher.update(stream.fromHex(ciphertext))
c_eng.decipher.finish()
local plaintext_hex = c_eng.decipher.asHex() ---@type hex
local plaintext = stream.toString(stream.fromHex(plaintext_hex))
log.debug("crypto.decrypt: aes128-ctr-mode took " .. (util.time() - start) .. "ms")
log.debug("plaintext: " .. util.strval(plaintext))
return plaintext
end
-- generate HMAC of message
---@param message_hex string initial value concatenated with ciphertext
function crypto.hmac(message_hex)
local start = util.time()
c_eng.hmac.init()
c_eng.hmac.update(stream.fromHex(message_hex))
c_eng.hmac.finish()
local hash = c_eng.hmac.asHex() ---@type hex
log.debug("crypto.hmac: hmac-sha1 took " .. (util.time() - start) .. "ms")
log.debug("hmac: " .. util.strval(hash))
return hash
end
-- wrap a modem as a secure modem to send encrypted traffic
---@param modem table modem to wrap
function crypto.secure_modem(modem)
local self = {
modem = modem
}
---@class secure_modem
---@field open function
---@field isOpen function
---@field close function
---@field closeAll function
---@field isWireless function
---@field getNamesRemote function
---@field isPresentRemote function
---@field getTypeRemote function
---@field hasTypeRemote function
---@field getMethodsRemote function
---@field callRemote function
---@field getNameLocal function
local public = {}
-- wrap a modem
---@param modem table
---@diagnostic disable-next-line: redefined-local
function public.wrap(modem)
self.modem = modem
for key, func in pairs(self.modem) do
public[key] = func
end
end
-- wrap modem functions, then we replace transmit
public.wrap(self.modem)
-- send a packet with encryption
---@param channel integer
---@param reply_channel integer
---@param payload table packet raw_sendable
function public.transmit(channel, reply_channel, payload)
local plaintext = textutils.serialize(payload, { allow_repetitions = true, compact = true })
local iv, ciphertext = crypto.encrypt(plaintext)
---@diagnostic disable-next-line: redefined-local
local hmac = crypto.hmac(iv .. ciphertext)
self.modem.transmit(channel, reply_channel, { hmac, iv, ciphertext })
end
-- parse in a modem message as a network packet
---@param side string
---@param sender integer
---@param reply_to integer
---@param message any encrypted packet sent with secure_modem.transmit
---@param distance integer
---@return string side, integer sender, integer reply_to, any plaintext_message, integer distance
function public.receive(side, sender, reply_to, message, distance)
local body = ""
if type(message) == "table" then
if #message == 3 then
---@diagnostic disable-next-line: redefined-local
local hmac = message[1]
local iv = message[2]
local ciphertext = message[3]
local computed_hmac = crypto.hmac(iv .. ciphertext)
if hmac == computed_hmac then
-- message intact
local plaintext = crypto.decrypt(iv, ciphertext)
body = textutils.deserialize(plaintext)
if body == nil then
-- failed decryption
log.debug("crypto.secure_modem: decryption failed")
body = ""
end
else
-- something went wrong
log.debug("crypto.secure_modem: hmac mismatch violation")
end
end
end
return side, sender, reply_to, body, distance
end
return public
end
return crypto

View File

@@ -1,9 +1,9 @@
local util = require("scada-common.util")
--
-- File System Logger
--
local util = require("scada-common.util")
---@class log
local log = {}
@@ -32,7 +32,7 @@ local free_space = fs.getFreeSpace
---@param path string file path
---@param write_mode MODE
---@param dmesg_redirect? table terminal/window to direct dmesg to
log.init = function (path, write_mode, dmesg_redirect)
function log.init(path, write_mode, dmesg_redirect)
_log_sys.path = path
_log_sys.mode = write_mode
@@ -49,9 +49,15 @@ log.init = function (path, write_mode, dmesg_redirect)
end
end
-- direct dmesg output to a monitor/window
---@param window table window or terminal reference
function log.direct_dmesg(window)
_log_sys.dmesg_out = window
end
-- private log write function
---@param msg string
local _log = function (msg)
local function _log(msg)
local time_stamp = os.date("[%c] ")
local stamped = time_stamp .. util.strval(msg)
@@ -84,9 +90,20 @@ local _log = function (msg)
end
end
-- write a message to the dmesg output
---@param msg string message to write
local _write = function (msg)
-- dmesg style logging for boot because I like linux-y things
---@param msg string message
---@param tag? string log tag
---@param tag_color? integer log tag color
---@return dmesg_ts_coord coordinates line area to place working indicator
function log.dmesg(msg, tag, tag_color)
---@class dmesg_ts_coord
local ts_coord = { x1 = 2, x2 = 3, y = 1 }
msg = util.strval(msg)
tag = tag or ""
tag = util.strval(tag)
local t_stamp = string.format("%12.2f", os.clock())
local out = _log_sys.dmesg_out
local out_w, out_h = out.getSize()
@@ -116,11 +133,45 @@ local _write = function (msg)
end
end
-- start output with tag and time, assuming we have enough width for this to be on one line
local cur_x, cur_y = out.getCursorPos()
if cur_x > 1 then
if cur_y == out_h then
out.scroll(1)
out.setCursorPos(1, cur_y)
else
out.setCursorPos(1, cur_y + 1)
end
end
-- colored time
local initial_color = out.getTextColor()
out.setTextColor(colors.white)
out.write("[")
out.setTextColor(colors.lightGray)
out.write(t_stamp)
ts_coord.x2, ts_coord.y = out.getCursorPos()
ts_coord.x2 = ts_coord.x2 - 1
out.setTextColor(colors.white)
out.write("] ")
-- print optionally colored tag
if tag ~= "" then
out.write("[")
if tag_color then out.setTextColor(tag_color) end
out.write(tag)
out.setTextColor(colors.white)
out.write("] ")
end
out.setTextColor(initial_color)
-- output message
for i = 1, #lines do
local cur_x, cur_y = out.getCursorPos()
cur_x, cur_y = out.getCursorPos()
if cur_x > 1 then
if i > 1 and cur_x > 1 then
if cur_y == out_h then
out.scroll(1)
out.setCursorPos(1, cur_y)
@@ -131,21 +182,75 @@ local _write = function (msg)
out.write(lines[i])
end
_log(util.c("[", t_stamp, "] [", tag, "] ", msg))
return ts_coord
end
-- dmesg style logging for boot because I like linux-y things
-- print a dmesg message, but then show remaining seconds instead of timestamp
---@param msg string message
---@param show_term? boolean whether or not to show on terminal output
log.dmesg = function (msg, show_term)
local message = string.format("[%10.3f] ", os.clock()) .. util.strval(msg)
if show_term then _write(message) end
_log(message)
---@param tag? string log tag
---@param tag_color? integer log tag color
---@return function update, function done
function log.dmesg_working(msg, tag, tag_color)
local ts_coord = log.dmesg(msg, tag, tag_color)
local out = _log_sys.dmesg_out
local width = (ts_coord.x2 - ts_coord.x1) + 1
local initial_color = out.getTextColor()
local counter = 0
local function update(sec_remaining)
local time = util.sprintf("%ds", sec_remaining)
local available = width - (string.len(time) + 2)
local progress = ""
out.setCursorPos(ts_coord.x1, ts_coord.y)
out.write(" ")
if counter % 4 == 0 then
progress = "|"
elseif counter % 4 == 1 then
progress = "/"
elseif counter % 4 == 2 then
progress = "-"
elseif counter % 4 == 3 then
progress = "\\"
end
out.setTextColor(colors.blue)
out.write(progress)
out.setTextColor(colors.lightGray)
out.write(util.spaces(available) .. time)
out.setTextColor(initial_color)
counter = counter + 1
end
local function done(ok)
out.setCursorPos(ts_coord.x1, ts_coord.y)
if ok or ok == nil then
out.setTextColor(colors.green)
out.write(util.pad("DONE", width))
else
out.setTextColor(colors.red)
out.write(util.pad("FAIL", width))
end
out.setTextColor(initial_color)
end
return update, done
end
-- log debug messages
---@param msg string message
---@param trace? boolean include file trace
log.debug = function (msg, trace)
function log.debug(msg, trace)
if LOG_DEBUG then
local dbg_info = ""
@@ -166,20 +271,20 @@ end
-- log info messages
---@param msg string message
log.info = function (msg)
function log.info(msg)
_log("[INF] " .. util.strval(msg))
end
-- log warning messages
---@param msg string message
log.warning = function (msg)
function log.warning(msg)
_log("[WRN] " .. util.strval(msg))
end
-- log error messages
---@param msg string message
---@param trace? boolean include file trace
log.error = function (msg, trace)
function log.error(msg, trace)
local dbg_info = ""
if trace then
@@ -198,7 +303,7 @@ end
-- log fatal errors
---@param msg string message
log.fatal = function (msg)
function log.fatal(msg)
_log("[FTL] " .. util.strval(msg))
end

View File

@@ -14,7 +14,7 @@ local TYPE = {
mqueue.TYPE = TYPE
-- create a new message queue
mqueue.new = function ()
function mqueue.new()
local queue = {}
local insert = table.insert
@@ -32,44 +32,44 @@ mqueue.new = function ()
local public = {}
-- get queue length
public.length = function () return #queue end
function public.length() return #queue end
-- check if queue is empty
---@return boolean is_empty
public.empty = function () return #queue == 0 end
function public.empty() return #queue == 0 end
-- check if queue has contents
public.ready = function () return #queue ~= 0 end
function public.ready() return #queue ~= 0 end
-- push a new item onto the queue
---@param qtype TYPE
---@param message string
local _push = function (qtype, message)
local function _push(qtype, message)
insert(queue, { qtype = qtype, message = message })
end
-- push a command onto the queue
---@param message any
public.push_command = function (message)
function public.push_command(message)
_push(TYPE.COMMAND, message)
end
-- push data onto the queue
---@param key any
---@param value any
public.push_data = function (key, value)
function public.push_data(key, value)
_push(TYPE.DATA, { key = key, val = value })
end
-- push a packet onto the queue
---@param packet scada_packet|modbus_packet|rplc_packet|coord_packet|capi_packet
public.push_packet = function (packet)
---@param packet scada_packet|modbus_packet|rplc_packet|crdn_packet|capi_packet
function public.push_packet(packet)
_push(TYPE.PACKET, packet)
end
-- get an item off the queue
---@return queue_item|nil
public.pop = function ()
function public.pop()
if #queue > 0 then
return remove(queue, 1)
else

View File

@@ -1,9 +1,10 @@
local log = require("scada-common.log")
--
-- Protected Peripheral Manager
--
local log = require("scada-common.log")
local util = require("scada-common.util")
---@class ppm
local ppm = {}
@@ -32,7 +33,7 @@ local _ppm_sys = {
---
---assumes iface is a valid peripheral
---@param iface string CC peripheral interface
local peri_init = function (iface)
local function peri_init(iface)
local self = {
faulted = false,
last_fault = "",
@@ -47,7 +48,10 @@ local peri_init = function (iface)
for key, func in pairs(self.device) do
self.fault_counts[key] = 0
self.device[key] = function (...)
local status, result = pcall(func, ...)
local return_table = table.pack(pcall(func, ...))
local status = return_table[1]
table.remove(return_table, 1)
if status then
-- auto fault clear
@@ -56,8 +60,10 @@ local peri_init = function (iface)
self.fault_counts[key] = 0
return result
return table.unpack(return_table)
else
local result = return_table[1]
-- function failed
self.faulted = true
self.last_fault = result
@@ -71,7 +77,7 @@ local peri_init = function (iface)
count_str = " [" .. self.fault_counts[key] .. " total faults]"
end
log.error("PPM: protected " .. key .. "() -> " .. result .. count_str)
log.error(util.c("PPM: protected ", key, "() -> ", result, count_str))
end
self.fault_counts[key] = self.fault_counts[key] + 1
@@ -87,13 +93,13 @@ local peri_init = function (iface)
-- fault management functions
local clear_fault = function () self.faulted = false end
local get_last_fault = function () return self.last_fault end
local is_faulted = function () return self.faulted end
local is_ok = function () return not self.faulted end
local function clear_fault() self.faulted = false end
local function get_last_fault() return self.last_fault end
local function is_faulted() return self.faulted end
local function is_ok() return not self.faulted end
local enable_afc = function () self.auto_cf = true end
local disable_afc = function () self.auto_cf = false end
local function enable_afc() self.auto_cf = true end
local function disable_afc() self.auto_cf = false end
-- append to device functions
@@ -117,53 +123,53 @@ end
-- REPORTING --
-- silence error prints
ppm.disable_reporting = function ()
function ppm.disable_reporting()
_ppm_sys.mute = true
end
-- allow error prints
ppm.enable_reporting = function ()
function ppm.enable_reporting()
_ppm_sys.mute = false
end
-- FAULT MEMORY --
-- enable automatically clearing fault flag
ppm.enable_afc = function ()
function ppm.enable_afc()
_ppm_sys.auto_cf = true
end
-- disable automatically clearing fault flag
ppm.disable_afc = function ()
function ppm.disable_afc()
_ppm_sys.auto_cf = false
end
-- clear fault flag
ppm.clear_fault = function ()
function ppm.clear_fault()
_ppm_sys.faulted = false
end
-- check fault flag
ppm.is_faulted = function ()
function ppm.is_faulted()
return _ppm_sys.faulted
end
-- get the last fault message
ppm.get_last_fault = function ()
function ppm.get_last_fault()
return _ppm_sys.last_fault
end
-- TERMINATION --
-- if a caught error was a termination request
ppm.should_terminate = function ()
function ppm.should_terminate()
return _ppm_sys.terminate
end
-- MOUNTING --
-- mount all available peripherals (clears mounts first)
ppm.mount_all = function ()
function ppm.mount_all()
local ifaces = peripheral.getNames()
_ppm_sys.mounts = {}
@@ -171,7 +177,7 @@ ppm.mount_all = function ()
for i = 1, #ifaces do
_ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i])
log.info("PPM: found a " .. _ppm_sys.mounts[ifaces[i]].type .. " (" .. ifaces[i] .. ")")
log.info(util.c("PPM: found a ", _ppm_sys.mounts[ifaces[i]].type, " (", ifaces[i], ")"))
end
if #ifaces == 0 then
@@ -182,7 +188,7 @@ end
-- mount a particular device
---@param iface string CC peripheral interface
---@return string|nil type, table|nil device
ppm.mount = function (iface)
function ppm.mount(iface)
local ifaces = peripheral.getNames()
local pm_dev = nil
local pm_type = nil
@@ -194,7 +200,7 @@ ppm.mount = function (iface)
pm_type = _ppm_sys.mounts[iface].type
pm_dev = _ppm_sys.mounts[iface].dev
log.info("PPM: mount(" .. iface .. ") -> found a " .. pm_type)
log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type))
break
end
end
@@ -205,7 +211,7 @@ end
-- handle peripheral_detach event
---@param iface string CC peripheral interface
---@return string|nil type, table|nil device
ppm.handle_unmount = function (iface)
function ppm.handle_unmount(iface)
local pm_dev = nil
local pm_type = nil
@@ -216,9 +222,9 @@ ppm.handle_unmount = function (iface)
pm_type = lost_dev.type
pm_dev = lost_dev.dev
log.warning("PPM: lost device " .. pm_type .. " mounted to " .. iface)
log.warning(util.c("PPM: lost device ", pm_type, " mounted to ", iface))
else
log.error("PPM: lost device unknown to the PPM mounted to " .. iface)
log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface))
end
return pm_type, pm_dev
@@ -228,20 +234,20 @@ end
-- list all available peripherals
---@return table names
ppm.list_avail = function ()
function ppm.list_avail()
return peripheral.getNames()
end
-- list mounted peripherals
---@return table mounts
ppm.list_mounts = function ()
function ppm.list_mounts()
return _ppm_sys.mounts
end
-- get a mounted peripheral by side/interface
---@param iface string CC peripheral interface
---@return table|nil device function table
ppm.get_periph = function (iface)
function ppm.get_periph(iface)
if _ppm_sys.mounts[iface] then
return _ppm_sys.mounts[iface].dev
else return nil end
@@ -250,7 +256,7 @@ end
-- get a mounted peripheral type by side/interface
---@param iface string CC peripheral interface
---@return string|nil type
ppm.get_type = function (iface)
function ppm.get_type(iface)
if _ppm_sys.mounts[iface] then
return _ppm_sys.mounts[iface].type
else return nil end
@@ -259,7 +265,7 @@ end
-- get all mounted peripherals by type
---@param name string type name
---@return table devices device function tables
ppm.get_all_devices = function (name)
function ppm.get_all_devices(name)
local devices = {}
for _, data in pairs(_ppm_sys.mounts) do
@@ -274,7 +280,7 @@ end
-- get a mounted peripheral by type (if multiple, returns the first)
---@param name string type name
---@return table|nil device function table
ppm.get_device = function (name)
function ppm.get_device(name)
local device = nil
for side, data in pairs(_ppm_sys.mounts) do
@@ -291,17 +297,20 @@ end
-- get the fission reactor (if multiple, returns the first)
---@return table|nil reactor function table
ppm.get_fission_reactor = function ()
return ppm.get_device("fissionReactor")
function ppm.get_fission_reactor()
return ppm.get_device("fissionReactor") or ppm.get_device("fissionReactorLogicAdapter")
end
-- get the wireless modem (if multiple, returns the first)
--
-- if this is in a CraftOS emulated environment, wired modems will be used instead
---@return table|nil modem function table
ppm.get_wireless_modem = function ()
function ppm.get_wireless_modem()
local w_modem = nil
local emulated_env = periphemu ~= nil
for _, device in pairs(_ppm_sys.mounts) do
if device.type == "modem" and device.dev.isWireless() then
if device.type == "modem" and (emulated_env or device.dev.isWireless()) then
w_modem = device.dev
break
end
@@ -312,8 +321,16 @@ end
-- list all connected monitors
---@return table monitors
ppm.list_monitors = function ()
return ppm.get_all_devices("monitor")
function ppm.get_monitor_list()
local list = {}
for iface, device in pairs(_ppm_sys.mounts) do
if device.type == "monitor" then
list[iface] = device
end
end
return list
end
return ppm

57
scada-common/psil.lua Normal file
View File

@@ -0,0 +1,57 @@
--
-- Publisher-Subscriber Interconnect Layer
--
local psil = {}
-- instantiate a new PSI layer
function psil.create()
local self = {
ic = {}
}
-- allocate a new interconnect field
---@key string data key
local function alloc(key)
self.ic[key] = { subscribers = {}, value = nil }
end
---@class psil
local public = {}
-- subscribe to a data object in the interconnect
--
-- will call func() right away if a value is already avaliable
---@param key string data key
---@param func function function to call on change
function public.subscribe(key, func)
-- allocate new key if not found or notify if value is found
if self.ic[key] == nil then
alloc(key)
elseif self.ic[key].value ~= nil then
func(self.ic[key].value)
end
-- subscribe to key
table.insert(self.ic[key].subscribers, { notify = func })
end
-- publish data to a given key, passing it to all subscribers if it has changed
---@param key string data key
---@param value any data value
function public.publish(key, value)
if self.ic[key] == nil then alloc(key) end
if self.ic[key].value ~= value then
for i = 1, #self.ic[key].subscribers do
self.ic[key].subscribers[i].notify(value)
end
end
self.ic[key].value = value
end
return public
end
return psil

View File

@@ -2,6 +2,8 @@
-- Redstone I/O
--
local util = require("scada-common.util")
local rsio = {}
----------------------
@@ -77,7 +79,7 @@ rsio.IO = RS_IO
-- channel to string
---@param channel RS_IO
rsio.to_string = function (channel)
function rsio.to_string(channel)
local names = {
"F_SCRAM",
"R_SCRAM",
@@ -101,7 +103,7 @@ rsio.to_string = function (channel)
"R_PLC_TIMEOUT"
}
if type(channel) == "number" and channel > 0 and channel <= #names then
if util.is_int(channel) and channel > 0 and channel <= #names then
return names[channel]
else
return ""
@@ -160,7 +162,7 @@ local RS_DIO_MAP = {
-- get the mode of a channel
---@param channel RS_IO
---@return IO_MODE
rsio.get_io_mode = function (channel)
function rsio.get_io_mode(channel)
local modes = {
IO_MODE.DIGITAL_IN, -- F_SCRAM
IO_MODE.DIGITAL_IN, -- R_SCRAM
@@ -184,7 +186,7 @@ rsio.get_io_mode = function (channel)
IO_MODE.DIGITAL_OUT -- R_PLC_TIMEOUT
}
if type(channel) == "number" and channel > 0 and channel <= #modes then
if util.is_int(channel) and channel > 0 and channel <= #modes then
return modes[channel]
else
return IO_MODE.ANALOG_IN
@@ -200,14 +202,14 @@ local RS_SIDES = rs.getSides()
-- check if a channel is valid
---@param channel RS_IO
---@return boolean valid
rsio.is_valid_channel = function (channel)
return (type(channel) == "number") and (channel > 0) and (channel <= RS_IO.R_PLC_TIMEOUT)
function rsio.is_valid_channel(channel)
return util.is_int(channel) and (channel > 0) and (channel <= RS_IO.R_PLC_TIMEOUT)
end
-- check if a side is valid
---@param side string
---@return boolean valid
rsio.is_valid_side = function (side)
function rsio.is_valid_side(side)
if side ~= nil then
for i = 0, #RS_SIDES do
if RS_SIDES[i] == side then return true end
@@ -219,8 +221,8 @@ end
-- check if a color is a valid single color
---@param color integer
---@return boolean valid
rsio.is_color = function (color)
return (type(color) == "number") and (color > 0) and (_B_AND(color, (color - 1)) == 0);
function rsio.is_color(color)
return util.is_int(color) and (color > 0) and (_B_AND(color, (color - 1)) == 0);
end
-----------------
@@ -230,7 +232,7 @@ end
-- get digital IO level reading
---@param rs_value boolean
---@return IO_LVL
rsio.digital_read = function (rs_value)
function rsio.digital_read(rs_value)
if rs_value then
return IO_LVL.HIGH
else
@@ -242,8 +244,8 @@ end
---@param channel RS_IO
---@param level IO_LVL
---@return boolean
rsio.digital_write = function (channel, level)
if type(channel) ~= "number" or channel < RS_IO.F_ALARM or channel > RS_IO.R_PLC_TIMEOUT then
function rsio.digital_write(channel, level)
if (not util.is_int(channel)) or (channel < RS_IO.F_ALARM) or (channel > RS_IO.R_PLC_TIMEOUT) then
return false
else
return RS_DIO_MAP[channel]._f(level)
@@ -254,8 +256,8 @@ end
---@param channel RS_IO
---@param level IO_LVL
---@return boolean
rsio.digital_is_active = function (channel, level)
if type(channel) ~= "number" or channel > RS_IO.R_ENABLE then
function rsio.digital_is_active(channel, level)
if (not util.is_int(channel)) or (channel > RS_IO.R_ENABLE) then
return false
else
return RS_DIO_MAP[channel]._f(level)
@@ -271,7 +273,7 @@ end
---@param min number minimum of range
---@param max number maximum of range
---@return number value scaled reading (min to max)
rsio.analog_read = function (rs_value, min, max)
function rsio.analog_read(rs_value, min, max)
local value = rs_value / 15
return (value * (max - min)) + min
end
@@ -281,7 +283,7 @@ end
---@param min number minimum of range
---@param max number maximum of range
---@return number rs_value scaled redstone reading (0 to 15)
rsio.analog_write = function (value, min, max)
function rsio.analog_write(value, min, max)
local scaled_value = (value - min) / (max - min)
return scaled_value * 15
end

View File

@@ -0,0 +1,26 @@
--
-- Timer Callback Dispatcher
--
local tcallbackdsp = {}
local registry = {}
-- request a function to be called after the specified time
---@param time number seconds
---@param f function callback function
function tcallbackdsp.dispatch(time, f)
---@diagnostic disable-next-line: undefined-field
registry[os.startTimer(time)] = { callback = f }
end
-- lookup a timer event and execute the callback if found
---@param event integer timer event timer ID
function tcallbackdsp.handle(event)
if registry[event] ~= nil then
registry[event].callback()
registry[event] = nil
end
end
return tcallbackdsp

View File

@@ -22,6 +22,10 @@ local types = {}
---@field reactor integer
---@field rsio table|nil
-- ALIASES --
---@alias color integer
-- ENUMERATION TYPES --
---@alias TRI_FAIL integer
@@ -33,6 +37,52 @@ types.TRI_FAIL = {
-- STRING TYPES --
---@alias os_event
---| "alarm"
---| "char"
---| "computer_command"
---| "disk"
---| "disk_eject"
---| "http_check"
---| "http_failure"
---| "http_success"
---| "key"
---| "key_up"
---| "modem_message"
---| "monitor_resize"
---| "monitor_touch"
---| "mouse_click"
---| "mouse_drag"
---| "mouse_scroll"
---| "mouse_up"
---| "paste"
---| "peripheral"
---| "peripheral_detach"
---| "rednet_message"
---| "redstone"
---| "speaker_audio_empty"
---| "task_complete"
---| "term_resize"
---| "terminate"
---| "timer"
---| "turtle_inventory"
---| "websocket_closed"
---| "websocket_failure"
---| "websocket_message"
---| "websocket_success"
---@alias rps_trip_cause
---| "ok"
---| "dmg_crit"
---| "high_temp"
---| "no_coolant"
---| "full_waste"
---| "heated_coolant_backup"
---| "no_fuel"
---| "fault"
---| "timeout"
---| "manual"
---@alias rtu_t string
types.rtu_t = {
redstone = "redstone",
@@ -41,7 +91,10 @@ types.rtu_t = {
turbine = "turbine",
turbine_valve = "turbine_valve",
energy_machine = "emachine",
induction_matrix = "induction_matrix"
induction_matrix = "induction_matrix",
sps = "sps",
sna = "sna",
env_detector = "environment_detector"
}
---@alias rps_status_t string

View File

@@ -5,29 +5,40 @@
---@class util
local util = {}
-- OPERATORS --
-- trinary operator
---@param cond boolean condition
---@param a any return if true
---@param b any return if false
---@return any value
function util.trinary(cond, a, b)
if cond then return a else return b end
end
-- PRINT --
-- print
---@param message any
util.print = function (message)
function util.print(message)
term.write(tostring(message))
end
-- print line
---@param message any
util.println = function (message)
function util.println(message)
print(tostring(message))
end
-- timestamped print
---@param message any
util.print_ts = function (message)
function util.print_ts(message)
term.write(os.date("[%H:%M:%S] ") .. tostring(message))
end
-- timestamped print line
---@param message any
util.println_ts = function (message)
function util.println_ts(message)
print(os.date("[%H:%M:%S] ") .. tostring(message))
end
@@ -36,7 +47,7 @@ end
-- get a value as a string
---@param val any
---@return string
util.strval = function (val)
function util.strval(val)
local t = type(val)
if t == "table" or t == "function" then
return "[" .. tostring(val) .. "]"
@@ -45,10 +56,88 @@ util.strval = function (val)
end
end
-- repeat a string n times
---@param str string
---@param n integer
---@return string
function util.strrep(str, n)
local repeated = ""
for _ = 1, n do
repeated = repeated .. str
end
return repeated
end
-- repeat a space n times
---@param n integer
---@return string
function util.spaces(n)
return util.strrep(" ", n)
end
-- pad text to a minimum width
---@param str string text
---@param n integer minimum width
---@return string
function util.pad(str, n)
local len = string.len(str)
local lpad = math.floor((n - len) / 2)
local rpad = (n - len) - lpad
return util.spaces(lpad) .. str .. util.spaces(rpad)
end
-- wrap a string into a table of lines, supporting single dash splits
---@param str string
---@param limit integer line limit
---@return table lines
function util.strwrap(str, limit)
local lines = {}
local ln_start = 1
local first_break = str:find("([%-%s]+)")
if first_break ~= nil then
lines[1] = string.sub(str, 1, first_break - 1)
else
lines[1] = str
end
---@diagnostic disable-next-line: discard-returns
str:gsub("(%s+)()(%S+)()",
function(space, start, word, stop)
-- support splitting SINGLE DASH words
word:gsub("(%S+)(%-)()(%S+)()",
function (pre, dash, d_start, post, d_stop)
if (stop + d_stop) - ln_start <= limit then
-- do nothing, it will entirely fit
elseif ((start + d_start) + 1) - ln_start <= limit then
-- we can fit including the dash
lines[#lines] = lines[#lines] .. space .. pre .. dash
-- drop the space and replace the word with the post
space = ""
word = post
-- force a wrap
stop = limit + 1 + ln_start
-- change start position for new line start
start = start + d_start - 1
end
end)
-- can we append this or do we have to start a new line?
if stop - ln_start > limit then
-- starting new line
ln_start = start
lines[#lines + 1] = word
else lines[#lines] = lines[#lines] .. space .. word end
end)
return lines
end
-- concatenation with built-in to string
---@vararg any
---@return string
util.concat = function (...)
function util.concat(...)
local str = ""
for _, v in ipairs(arg) do
str = str .. util.strval(v)
@@ -56,41 +145,69 @@ util.concat = function (...)
return str
end
-- alias
util.c = util.concat
-- sprintf implementation
---@param format string
---@vararg any
util.sprintf = function (format, ...)
function util.sprintf(format, ...)
return string.format(format, table.unpack(arg))
end
-- MATH --
-- is a value an integer
---@param x any value
---@return boolean is_integer if the number is an integer
function util.is_int(x)
return type(x) == "number" and x == math.floor(x)
end
-- round a number to an integer
---@return integer rounded
function util.round(x)
return math.floor(x + 0.5)
end
-- TIME --
-- current time
---@return integer milliseconds
util.time_ms = function ()
function util.time_ms()
---@diagnostic disable-next-line: undefined-field
return os.epoch('local')
end
-- current time
---@return number seconds
util.time_s = function ()
function util.time_s()
---@diagnostic disable-next-line: undefined-field
return os.epoch('local') / 1000.0
end
-- current time
---@return integer milliseconds
util.time = function ()
function util.time()
return util.time_ms()
end
-- OS --
-- OS pull event raw wrapper with types
---@param target_event? string event to wait for
---@return os_event event, any param1, any param2, any param3, any param4, any param5
function util.pull_event(target_event)
---@diagnostic disable-next-line: undefined-field
return os.pullEventRaw(target_event)
end
-- PARALLELIZATION --
-- protected sleep call so we still are in charge of catching termination
---@param t integer seconds
--- EVENT_CONSUMER: this function consumes events
util.psleep = function (t)
function util.psleep(t)
---@diagnostic disable-next-line: undefined-field
pcall(os.sleep, t)
end
@@ -98,7 +215,7 @@ end
-- no-op to provide a brief pause (1 tick) to yield
---
--- EVENT_CONSUMER: this function consumes events
util.nop = function ()
function util.nop()
util.psleep(0.05)
end
@@ -106,8 +223,8 @@ end
---@param target_timing integer minimum amount of milliseconds to wait for
---@param last_update integer millisecond time of last update
---@return integer time_now
-- EVENT_CONSUMER: this function consumes events
util.adaptive_delay = function (target_timing, last_update)
--- EVENT_CONSUMER: this function consumes events
function util.adaptive_delay(target_timing, last_update)
local sleep_for = target_timing - (util.time() - last_update)
-- only if >50ms since worker loops already yield 0.05s
if sleep_for >= 50 then
@@ -124,7 +241,7 @@ end
---@param t table table to remove elements from
---@param f function should return false to delete an element when passed the element: f(elem) = true|false
---@param on_delete? function optional function to execute on deletion, passed the table element to be deleted as the parameter
util.filter_table = function (t, f, on_delete)
function util.filter_table(t, f, on_delete)
local move_to = 1
for i = 1, #t do
local element = t[i]
@@ -146,7 +263,7 @@ end
-- check if a table contains the provided element
---@param t table table to check
---@param element any element to check for
util.table_contains = function (t, element)
function util.table_contains(t, element)
for i = 1, #t do
if t[i] == element then return true end
end
@@ -191,7 +308,7 @@ end
---@param timeout number timeout duration
---
--- triggers a timer event if not fed within 'timeout' seconds
util.new_watchdog = function (timeout)
function util.new_watchdog(timeout)
---@diagnostic disable-next-line: undefined-field
local start_timer = os.startTimer
---@diagnostic disable-next-line: undefined-field
@@ -206,12 +323,12 @@ util.new_watchdog = function (timeout)
local public = {}
---@param timer number timer event timer ID
public.is_timer = function (timer)
function public.is_timer(timer)
return self.wd_timer == timer
end
-- satiate the beast
public.feed = function ()
function public.feed()
if self.wd_timer ~= nil then
cancel_timer(self.wd_timer)
end
@@ -219,7 +336,7 @@ util.new_watchdog = function (timeout)
end
-- cancel the watchdog
public.cancel = function ()
function public.cancel()
if self.wd_timer ~= nil then
cancel_timer(self.wd_timer)
end
@@ -234,7 +351,7 @@ end
---@param period number clock period
---
--- fires a timer event at the specified period, does not start at construct time
util.new_clock = function (period)
function util.new_clock(period)
---@diagnostic disable-next-line: undefined-field
local start_timer = os.startTimer
@@ -247,16 +364,46 @@ util.new_clock = function (period)
local public = {}
---@param timer number timer event timer ID
public.is_clock = function (timer)
function public.is_clock(timer)
return self.timer == timer
end
-- start the clock
public.start = function ()
function public.start()
self.timer = start_timer(self.period)
end
return public
end
-- create a new type validator
--
-- can execute sequential checks and check valid() to see if it is still valid
function util.new_validator()
local valid = true
---@class validator
local public = {}
function public.assert_type_bool(value) valid = valid and type(value) == "boolean" end
function public.assert_type_num(value) valid = valid and type(value) == "number" end
function public.assert_type_int(value) valid = valid and util.is_int(value) end
function public.assert_type_str(value) valid = valid and type(value) == "string" end
function public.assert_type_table(value) valid = valid and type(value) == "table" end
function public.assert_eq(check, expect) valid = valid and check == expect end
function public.assert_min(check, min) valid = valid and check >= min end
function public.assert_min_ex(check, min) valid = valid and check > min end
function public.assert_max(check, max) valid = valid and check <= max end
function public.assert_max_ex(check, max) valid = valid and check < max end
function public.assert_range(check, min, max) valid = valid and check >= min and check <= max end
function public.assert_range_ex(check, min, max) valid = valid and check > min and check < max end
function public.assert_port(port) valid = valid and type(port) == "number" and port >= 0 and port <= 65535 end
function public.valid() return valid end
return public
end
return util

View File

@@ -1,3 +1,278 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util")
local coordinator = {}
local PROTOCOLS = comms.PROTOCOLS
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES
local print = util.print
local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
-- retry time constants in ms
local INITIAL_WAIT = 1500
local RETRY_PERIOD = 1000
local PERIODICS = {
KEEP_ALIVE = 2000,
STATUS = 500
}
-- coordinator supervisor session
---@param id integer
---@param in_queue mqueue
---@param out_queue mqueue
---@param facility_units table
function coordinator.new_session(id, in_queue, out_queue, facility_units)
local log_header = "crdn_session(" .. id .. "): "
local self = {
id = id,
in_q = in_queue,
out_q = out_queue,
units = facility_units,
-- connection properties
seq_num = 0,
r_seq_num = nil,
connected = true,
conn_watchdog = util.new_watchdog(3),
last_rtt = 0,
-- periodic messages
periodics = {
last_update = 0,
keep_alive = 0,
status_packet = 0
},
-- when to next retry one of these messages
retry_times = {
builds_packet = (util.time() + 500)
},
-- message acknowledgements
acks = {
builds = true
}
}
-- mark this coordinator session as closed, stop watchdog
local function _close()
self.conn_watchdog.cancel()
self.connected = false
end
-- send a CRDN packet
---@param msg_type SCADA_CRDN_TYPES
---@param msg table
local function _send(msg_type, msg)
local s_pkt = comms.scada_packet()
local c_pkt = comms.crdn_packet()
c_pkt.make(msg_type, msg)
s_pkt.make(self.seq_num, PROTOCOLS.SCADA_CRDN, c_pkt.raw_sendable())
self.out_q.push_packet(s_pkt)
self.seq_num = self.seq_num + 1
end
-- send a SCADA management packet
---@param msg_type SCADA_MGMT_TYPES
---@param msg table
local function _send_mgmt(msg_type, msg)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg)
s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable())
self.out_q.push_packet(s_pkt)
self.seq_num = self.seq_num + 1
end
-- send unit builds
local function _send_builds()
self.acks.builds = false
local builds = {}
for i = 1, #self.units do
local unit = self.units[i] ---@type reactor_unit
builds[unit.get_id()] = unit.get_build()
end
_send(SCADA_CRDN_TYPES.STRUCT_BUILDS, builds)
end
-- send unit statuses
local function _send_status()
local status = {}
for i = 1, #self.units do
local unit = self.units[i] ---@type reactor_unit
status[unit.get_id()] = { unit.get_reactor_status(), unit.get_annunciator(), unit.get_rtu_statuses() }
end
_send(SCADA_CRDN_TYPES.UNIT_STATUSES, status)
end
-- handle a packet
---@param pkt crdn_frame
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
elseif self.r_seq_num >= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return
else
self.r_seq_num = pkt.scada_frame.seq_num()
end
-- feed watchdog
self.conn_watchdog.feed()
-- process packet
if pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then
if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
-- keep alive reply
if pkt.length == 2 then
local srv_start = pkt.data[1]
local coord_send = pkt.data[2]
local srv_now = util.time()
self.last_rtt = srv_now - srv_start
if self.last_rtt > 500 then
log.warning(log_header .. "COORD KEEP_ALIVE round trip time > 500ms (" .. self.last_rtt .. "ms)")
end
-- log.debug(log_header .. "COORD RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "COORD TT = " .. (srv_now - coord_send) .. "ms")
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then
-- close the session
_close()
else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end
elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_CRDN then
if pkt.type == SCADA_CRDN_TYPES.STRUCT_BUILDS then
-- acknowledgement to coordinator receiving builds
self.acks.builds = true
else
log.debug(log_header .. "handler received unexpected SCADA_CRDN packet type " .. pkt.type)
end
end
end
---@class coord_session
local public = {}
-- get the session ID
function public.get_id() return self.id end
-- check if a timer matches this session's watchdog
function public.check_wd(timer)
return self.conn_watchdog.is_timer(timer) and self.connected
end
-- close the connection
function public.close()
_close()
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
println("connection to coordinator " .. self.id .. " closed by server")
log.info(log_header .. "session closed by server")
end
-- iterate the session
---@return boolean connected
function public.iterate()
if self.connected then
------------------
-- handle queue --
------------------
local handle_start = util.time()
while self.in_q.ready() and self.connected do
-- get a new message to process
local message = self.in_q.pop()
if message ~= nil then
if message.qtype == mqueue.TYPE.PACKET then
-- handle a packet
_handle_packet(message.message)
elseif message.qtype == mqueue.TYPE.COMMAND then
-- handle instruction
elseif message.qtype == mqueue.TYPE.DATA then
-- instruction with body
end
end
-- max 100ms spent processing queue
if util.time() - handle_start > 100 then
log.warning(log_header .. "exceeded 100ms queue process limit")
break
end
end
-- exit if connection was closed
if not self.connected then
println("connection to coordinator " .. self.id .. " closed by remote host")
log.info(log_header .. "session closed by remote host")
return self.connected
end
----------------------
-- update periodics --
----------------------
local elapsed = util.time() - self.periodics.last_update
local periodics = self.periodics
-- keep alive
periodics.keep_alive = periodics.keep_alive + elapsed
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
_send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { util.time() })
periodics.keep_alive = 0
end
-- unit statuses to coordinator
periodics.status_packet = periodics.status_packet + elapsed
if periodics.status_packet >= PERIODICS.STATUS then
_send_status()
periodics.status_packet = 0
end
self.periodics.last_update = util.time()
---------------------
-- attempt retries --
---------------------
local rtimes = self.retry_times
-- builds packet retry
if not self.acks.builds then
if rtimes.builds_packet - util.time() <= 0 then
_send_builds()
rtimes.builds_packet = util.time() + RETRY_PERIOD
end
end
end
return self.connected
end
return public
end
return coordinator

View File

@@ -1,7 +1,7 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util")
local util = require("scada-common.util")
local plc = {}
@@ -33,7 +33,7 @@ plc.PLC_S_CMDS = PLC_S_CMDS
plc.PLC_S_DATA = PLC_S_DATA
local PERIODICS = {
KEEP_ALIVE = 2.0
KEEP_ALIVE = 2000
}
-- PLC supervisor session
@@ -41,7 +41,7 @@ local PERIODICS = {
---@param for_reactor integer
---@param in_queue mqueue
---@param out_queue mqueue
plc.new_session = function (id, for_reactor, in_queue, out_queue)
function plc.new_session(id, for_reactor, in_queue, out_queue)
local log_header = "plc_session(" .. id .. "): "
local self = {
@@ -86,10 +86,9 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
sDB = {
last_status_update = 0,
control_state = false,
overridden = false,
degraded = false,
rps_tripped = false,
rps_trip_cause = "ok",
rps_trip_cause = "ok", ---@type rps_trip_cause
---@class rps_status
rps_status = {
dmg_crit = false,
@@ -98,7 +97,9 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
high_temp = false,
no_fuel = false,
no_cool = false,
timed_out = false
fault = false,
timeout = false,
manual = false
},
---@class mek_status
mek_status = {
@@ -146,19 +147,21 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
-- copy in the RPS status
---@param rps_status table
local _copy_rps_status = function (rps_status)
local function _copy_rps_status(rps_status)
self.sDB.rps_status.dmg_crit = rps_status[1]
self.sDB.rps_status.ex_hcool = rps_status[2]
self.sDB.rps_status.ex_waste = rps_status[3]
self.sDB.rps_status.high_temp = rps_status[4]
self.sDB.rps_status.no_fuel = rps_status[5]
self.sDB.rps_status.no_cool = rps_status[6]
self.sDB.rps_status.timed_out = rps_status[7]
self.sDB.rps_status.fault = rps_status[7]
self.sDB.rps_status.timeout = rps_status[8]
self.sDB.rps_status.manual = rps_status[9]
end
-- copy in the reactor status
---@param mek_data table
local _copy_status = function (mek_data)
local function _copy_status(mek_data)
-- copy status information
self.sDB.mek_status.status = mek_data[1]
self.sDB.mek_status.burn_rate = mek_data[2]
@@ -191,7 +194,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
-- copy in the reactor structure
---@param mek_data table
local _copy_struct = function (mek_data)
local function _copy_struct(mek_data)
self.sDB.mek_struct.heat_cap = mek_data[1]
self.sDB.mek_struct.fuel_asm = mek_data[2]
self.sDB.mek_struct.fuel_sa = mek_data[3]
@@ -203,7 +206,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
end
-- mark this PLC session as closed, stop watchdog
local _close = function ()
local function _close()
self.plc_conn_watchdog.cancel()
self.connected = false
end
@@ -211,7 +214,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
-- send an RPLC packet
---@param msg_type RPLC_TYPES
---@param msg table
local _send = function (msg_type, msg)
local function _send(msg_type, msg)
local s_pkt = comms.scada_packet()
local r_pkt = comms.rplc_packet()
@@ -225,7 +228,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
-- send a SCADA management packet
---@param msg_type SCADA_MGMT_TYPES
---@param msg table
local _send_mgmt = function (msg_type, msg)
local function _send_mgmt(msg_type, msg)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
@@ -239,7 +242,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
-- get an ACK status
---@param pkt rplc_frame
---@return boolean|nil ack
local _get_ack = function (pkt)
local function _get_ack(pkt)
if pkt.length == 1 then
return pkt.data[1]
else
@@ -250,7 +253,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
-- handle a packet
---@param pkt rplc_frame
local _handle_packet = function (pkt)
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
@@ -278,7 +281,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
if pkt.length >= 5 then
self.sDB.last_status_update = pkt.data[1]
self.sDB.control_state = pkt.data[2]
self.sDB.overridden = pkt.data[3]
self.sDB.rps_tripped = pkt.data[3]
self.sDB.degraded = pkt.data[4]
self.sDB.mek_status.heating_rate = pkt.data[5]
@@ -338,7 +341,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
end
elseif pkt.type == RPLC_TYPES.RPS_STATUS then
-- RPS status packet received, copy data
if pkt.length == 7 then
if pkt.length == 9 then
local status = pcall(_copy_rps_status, pkt.data)
if status then
-- copied in RPS status data OK
@@ -351,8 +354,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
end
elseif pkt.type == RPLC_TYPES.RPS_ALARM then
-- RPS alarm
self.sDB.overridden = true
if pkt.length == 8 then
if pkt.length == 10 then
self.sDB.rps_tripped = true
self.sDB.rps_trip_cause = pkt.data[1]
local status = pcall(_copy_rps_status, { table.unpack(pkt.data, 2, #pkt.length) })
@@ -391,8 +393,8 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
log.warning(log_header .. "PLC KEEP_ALIVE round trip time > 500ms (" .. self.last_rtt .. "ms)")
end
-- log.debug(log_header .. "PLC RTT = ".. self.last_rtt .. "ms")
-- log.debug(log_header .. "PLC TT = ".. (srv_now - plc_send) .. "ms")
-- log.debug(log_header .. "PLC RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "PLC TT = " .. (srv_now - plc_send) .. "ms")
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
@@ -408,13 +410,13 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
-- PUBLIC FUNCTIONS --
-- get the session ID
public.get_id = function () return self.id end
function public.get_id() return self.id end
-- get the session database
public.get_db = function () return self.sDB end
function public.get_db() return self.sDB end
-- get the reactor structure
public.get_struct = function ()
function public.get_struct()
if self.received_struct then
return self.sDB.mek_struct
else
@@ -423,7 +425,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
end
-- get the reactor status
public.get_status = function ()
function public.get_status()
if self.received_status_cache then
return self.sDB.mek_status
else
@@ -432,29 +434,28 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
end
-- get the reactor RPS status
public.get_rps = function ()
function public.get_rps()
return self.sDB.rps_status
end
-- get the general status information
public.get_general_status = function ()
function public.get_general_status()
return {
last_status_update = self.sDB.last_status_update,
control_state = self.sDB.control_state,
overridden = self.sDB.overridden,
degraded = self.sDB.degraded,
rps_tripped = self.sDB.rps_tripped,
rps_trip_cause = self.sDB.rps_trip_cause
self.sDB.last_status_update,
self.sDB.control_state,
self.sDB.rps_tripped,
self.sDB.rps_trip_cause,
self.sDB.degraded
}
end
-- check if a timer matches this session's watchdog
public.check_wd = function (timer)
function public.check_wd(timer)
return self.plc_conn_watchdog.is_timer(timer) and self.connected
end
-- close the connection
public.close = function ()
function public.close()
_close()
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
println("connection to reactor " .. self.for_reactor .. " PLC closed by server")
@@ -463,7 +464,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue)
-- iterate the session
---@return boolean connected
public.iterate = function ()
function public.iterate()
if self.connected then
------------------
-- handle queue --

View File

@@ -1,14 +1,20 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local rsio = require("scada-common.rsio")
local util = require("scada-common.util")
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local rsio = require("scada-common.rsio")
local util = require("scada-common.util")
-- supervisor rtu sessions (svrs)
local svrs_boiler = require("supervisor.session.rtu.boiler")
local svrs_boiler = require("supervisor.session.rtu.boiler")
local svrs_boilerv = require("supervisor.session.rtu.boilerv")
local svrs_emachine = require("supervisor.session.rtu.emachine")
local svrs_envd = require("supervisor.session.rtu.envd")
local svrs_imatrix = require("supervisor.session.rtu.imatrix")
local svrs_redstone = require("supervisor.session.rtu.redstone")
local svrs_turbine = require("supervisor.session.rtu.turbine")
local svrs_sna = require("supervisor.session.rtu.sna")
local svrs_sps = require("supervisor.session.rtu.sps")
local svrs_turbine = require("supervisor.session.rtu.turbine")
local svrs_turbinev = require("supervisor.session.rtu.turbinev")
local rtu = {}
@@ -33,7 +39,7 @@ rtu.RTU_S_CMDS = RTU_S_CMDS
rtu.RTU_S_DATA = RTU_S_DATA
local PERIODICS = {
KEEP_ALIVE = 2.0
KEEP_ALIVE = 2000
}
---@class rs_session_command
@@ -46,13 +52,16 @@ local PERIODICS = {
---@param in_queue mqueue
---@param out_queue mqueue
---@param advertisement table
rtu.new_session = function (id, in_queue, out_queue, advertisement)
---@param facility_units table
function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units)
local log_header = "rtu_session(" .. id .. "): "
local self = {
id = id,
in_q = in_queue,
out_q = out_queue,
modbus_q = mqueue.new(),
f_units = facility_units,
advert = advertisement,
-- connection properties
seq_num = 0,
@@ -60,21 +69,36 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement)
connected = true,
rtu_conn_watchdog = util.new_watchdog(3),
last_rtt = 0,
-- periodic messages
periodics = {
last_update = 0,
keep_alive = 0
},
rs_io_q = {},
turbine_cmd_q = {},
turbine_cmd_capable = false,
units = {}
}
---@class rtu_session
local public = {}
local function _reset_config()
self.units = {}
self.rs_io_q = {}
self.turbine_cmd_q = {}
self.turbine_cmd_capable = false
end
-- parse the recorded advertisement and create unit sub-sessions
local _handle_advertisement = function ()
local function _handle_advertisement()
self.units = {}
self.rs_io_q = {}
for i = 1, #self.advert do
local unit = nil ---@type unit_session|nil
local rs_in_q = nil ---@type mqueue|nil
local unit = nil ---@type unit_session|nil
local rs_in_q = nil ---@type mqueue|nil
local tbv_in_q = nil ---@type mqueue|nil
---@type rtu_advertisement
local unit_advert = {
@@ -84,23 +108,67 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement)
rsio = self.advert[i][4]
}
local target_unit = self.f_units[unit_advert.reactor] ---@type reactor_unit
local u_type = unit_advert.type
-- create unit by type
-- validate unit advertisement
local advert_validator = util.new_validator()
advert_validator.assert_type_int(unit_advert.index)
advert_validator.assert_type_int(unit_advert.reactor)
if u_type == RTU_UNIT_TYPES.REDSTONE then
unit, rs_in_q = svrs_redstone.new(self.id, i, unit_advert, self.out_q)
advert_validator.assert_type_table(unit_advert.rsio)
end
if advert_validator.valid() then
advert_validator.assert_min(unit_advert.index, 1)
advert_validator.assert_min(unit_advert.reactor, 1)
if not advert_validator.valid() then u_type = false end
else
u_type = false
end
-- create unit by type
if u_type == false then
-- validation fail
elseif u_type == RTU_UNIT_TYPES.REDSTONE then
-- redstone
unit, rs_in_q = svrs_redstone.new(self.id, i, unit_advert, self.modbus_q)
elseif u_type == RTU_UNIT_TYPES.BOILER then
unit = svrs_boiler.new(self.id, i, unit_advert, self.out_q)
-- boiler
unit = svrs_boiler.new(self.id, i, unit_advert, self.modbus_q)
target_unit.add_boiler(unit)
elseif u_type == RTU_UNIT_TYPES.BOILER_VALVE then
-- @todo Mekanism 10.1+
-- boiler (Mekanism 10.1+)
unit = svrs_boilerv.new(self.id, i, unit_advert, self.modbus_q)
target_unit.add_boiler(unit)
elseif u_type == RTU_UNIT_TYPES.TURBINE then
unit = svrs_turbine.new(self.id, i, unit_advert, self.out_q)
-- turbine
unit = svrs_turbine.new(self.id, i, unit_advert, self.modbus_q)
target_unit.add_turbine(unit)
elseif u_type == RTU_UNIT_TYPES.TURBINE_VALVE then
-- @todo Mekanism 10.1+
-- turbine (Mekanism 10.1+)
unit, tbv_in_q = svrs_turbinev.new(self.id, i, unit_advert, self.modbus_q)
target_unit.add_turbine(unit)
self.turbine_cmd_capable = true
elseif u_type == RTU_UNIT_TYPES.EMACHINE then
unit = svrs_emachine.new(self.id, i, unit_advert, self.out_q)
-- mekanism [energy] machine
unit = svrs_emachine.new(self.id, i, unit_advert, self.modbus_q)
elseif u_type == RTU_UNIT_TYPES.IMATRIX then
-- @todo Mekanism 10.1+
-- induction matrix
unit = svrs_imatrix.new(self.id, i, unit_advert, self.modbus_q)
elseif u_type == RTU_UNIT_TYPES.SPS then
-- super-critical phase shifter
unit = svrs_sps.new(self.id, i, unit_advert, self.modbus_q)
elseif u_type == RTU_UNIT_TYPES.SNA then
-- solar neutron activator
unit = svrs_sna.new(self.id, i, unit_advert, self.modbus_q)
elseif u_type == RTU_UNIT_TYPES.ENV_DETECTOR then
-- environment detector
unit = svrs_envd.new(self.id, i, unit_advert, self.modbus_q)
else
log.error(log_header .. "bad advertisement: encountered unsupported RTU type")
end
@@ -108,21 +176,33 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement)
if unit ~= nil then
table.insert(self.units, unit)
if self.rs_io_q[unit_advert.reactor] == nil then
self.rs_io_q[unit_advert.reactor] = rs_in_q
else
self.units = {}
self.rs_io_q = {}
log.error(log_header .. "bad advertisement: duplicate redstone RTU for reactor " .. unit_advert.reactor)
break
if u_type == RTU_UNIT_TYPES.REDSTONE then
if self.rs_io_q[unit_advert.reactor] == nil then
self.rs_io_q[unit_advert.reactor] = rs_in_q
else
_reset_config()
log.error(log_header .. util.c("bad advertisement: duplicate redstone RTU for reactor " .. unit_advert.reactor))
break
end
elseif u_type == RTU_UNIT_TYPES.TURBINE_VALVE then
if self.turbine_cmd_q[unit_advert.reactor] == nil then
self.turbine_cmd_q[unit_advert.reactor] = {}
end
local queues = self.turbine_cmd_q[unit_advert.reactor]
if queues[unit_advert.index] == nil then
queues[unit_advert.index] = tbv_in_q
else
_reset_config()
log.error(log_header .. util.c("bad advertisement: duplicate turbine RTU (same index of ",
unit_advert.index, ") for reactor ", unit_advert.reactor))
break
end
end
else
self.units = {}
self.rs_io_q = {}
local type_string = comms.advert_type_to_rtu_t(u_type)
if type_string == nil then type_string = "unknown" end
_reset_config()
local type_string = util.strval(comms.advert_type_to_rtu_t(u_type))
log.error(log_header .. "bad advertisement: error occured while creating a unit (type is " .. type_string .. ")")
break
end
@@ -130,7 +210,7 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement)
end
-- mark this RTU session as closed, stop watchdog
local _close = function ()
local function _close()
self.rtu_conn_watchdog.cancel()
self.connected = false
@@ -140,10 +220,21 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement)
end
end
-- send a MODBUS packet
---@param m_pkt modbus_packet MODBUS packet
local function _send_modbus(m_pkt)
local s_pkt = comms.scada_packet()
s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable())
self.out_q.push_packet(s_pkt)
self.seq_num = self.seq_num + 1
end
-- send a SCADA management packet
---@param msg_type SCADA_MGMT_TYPES
---@param msg table
local _send_mgmt = function (msg_type, msg)
local function _send_mgmt(msg_type, msg)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
@@ -156,7 +247,7 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement)
-- handle a packet
---@param pkt modbus_frame|mgmt_frame
local _handle_packet = function (pkt)
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
@@ -190,8 +281,8 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement)
log.warning(log_header .. "RTU KEEP_ALIVE round trip time > 500ms (" .. self.last_rtt .. "ms)")
end
-- log.debug(log_header .. "RTU RTT = ".. self.last_rtt .. "ms")
-- log.debug(log_header .. "RTU TT = ".. (srv_now - rtu_send) .. "ms")
-- log.debug(log_header .. "RTU RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "RTU TT = " .. (srv_now - rtu_send) .. "ms")
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
@@ -212,16 +303,16 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement)
-- PUBLIC FUNCTIONS --
-- get the session ID
public.get_id = function () return self.id end
function public.get_id() return self.id end
-- check if a timer matches this session's watchdog
---@param timer number
public.check_wd = function (timer)
function public.check_wd(timer)
return self.rtu_conn_watchdog.is_timer(timer) and self.connected
end
-- close the connection
public.close = function ()
function public.close()
_close()
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
println(log_header .. "connection to RTU closed by server")
@@ -230,7 +321,7 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement)
-- iterate the session
---@return boolean connected
public.iterate = function ()
function public.iterate()
if self.connected then
------------------
-- handle queue --
@@ -314,11 +405,29 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement)
end
self.periodics.last_update = util.time()
----------------------------------------------
-- pass MODBUS packets on to main out queue --
----------------------------------------------
for _ = 1, self.modbus_q.length() do
-- get the next message
local msg = self.modbus_q.pop()
if msg ~= nil then
if msg.qtype == mqueue.TYPE.PACKET then
_send_modbus(msg.message)
end
end
end
end
return self.connected
end
-- handle initial advertisement
_handle_advertisement()
return public
end

View File

@@ -1,6 +1,6 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local types = require("scada-common.types")
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local types = require("scada-common.types")
local unit_session = require("supervisor.session.rtu.unit_session")
@@ -18,7 +18,7 @@ local TXN_TYPES = {
local TXN_TAGS = {
"boiler.build",
"boiler.state",
"boiler.tanks",
"boiler.tanks"
}
local PERIODICS = {
@@ -32,7 +32,7 @@ local PERIODICS = {
---@param unit_id integer
---@param advert rtu_advertisement
---@param out_queue mqueue
boiler.new = function (session_id, unit_id, advert, out_queue)
function boiler.new(session_id, unit_id, advert, out_queue)
-- type check
if advert.type ~= RTU_UNIT_TYPES.BOILER then
log.error("attempt to instantiate boiler RTU for type '" .. advert.type .. "'. this is a bug.")
@@ -47,7 +47,7 @@ boiler.new = function (session_id, unit_id, advert, out_queue)
periodics = {
next_build_req = 0,
next_state_req = 0,
next_tanks_req = 0,
next_tanks_req = 0
},
---@class boiler_session_db
db = {
@@ -86,19 +86,19 @@ boiler.new = function (session_id, unit_id, advert, out_queue)
-- PRIVATE FUNCTIONS --
-- query the build of the device
local _request_build = function ()
local function _request_build()
-- read input registers 1 through 7 (start = 1, count = 7)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 7 })
end
-- query the state of the device
local _request_state = function ()
local function _request_state()
-- read input registers 8 through 9 (start = 8, count = 2)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 8, 2 })
end
-- query the tanks of the device
local _request_tanks = function ()
local function _request_tanks()
-- read input registers 10 through 21 (start = 10, count = 12)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 10, 12 })
end
@@ -107,51 +107,52 @@ boiler.new = function (session_id, unit_id, advert, out_queue)
-- handle a packet
---@param m_pkt modbus_frame
public.handle_packet = function (m_pkt)
local txn_type = self.session.try_resolve(m_pkt.txn_id)
function public.handle_packet(m_pkt)
local txn_type = self.session.try_resolve(m_pkt)
if txn_type == false then
-- nothing to do
elseif txn_type == TXN_TYPES.BUILD then
-- build response
-- load in data if correct length
if m_pkt.length == 7 then
self.db.build.boil_cap = m_pkt.data[1]
self.db.build.steam_cap = m_pkt.data[2]
self.db.build.water_cap = m_pkt.data[3]
self.db.build.hcoolant_cap = m_pkt.data[4]
self.db.build.ccoolant_cap = m_pkt.data[5]
self.db.build.superheaters = m_pkt.data[6]
self.db.build.boil_cap = m_pkt.data[1]
self.db.build.steam_cap = m_pkt.data[2]
self.db.build.water_cap = m_pkt.data[3]
self.db.build.hcoolant_cap = m_pkt.data[4]
self.db.build.ccoolant_cap = m_pkt.data[5]
self.db.build.superheaters = m_pkt.data[6]
self.db.build.max_boil_rate = m_pkt.data[7]
self.has_build = true
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.build)")
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.STATE then
-- state response
-- load in data if correct length
if m_pkt.length == 2 then
self.db.state.temperature = m_pkt.data[1]
self.db.state.boil_rate = m_pkt.data[2]
self.db.state.boil_rate = m_pkt.data[2]
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.state)")
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.TANKS then
-- tanks response
-- load in data if correct length
if m_pkt.length == 12 then
self.db.tanks.steam = m_pkt.data[1]
self.db.tanks.steam = m_pkt.data[1]
self.db.tanks.steam_need = m_pkt.data[2]
self.db.tanks.steam_fill = m_pkt.data[3]
self.db.tanks.water = m_pkt.data[4]
self.db.tanks.water = m_pkt.data[4]
self.db.tanks.water_need = m_pkt.data[5]
self.db.tanks.water_fill = m_pkt.data[6]
self.db.tanks.hcool = m_pkt.data[7]
self.db.tanks.hcool = m_pkt.data[7]
self.db.tanks.hcool_need = m_pkt.data[8]
self.db.tanks.hcool_fill = m_pkt.data[9]
self.db.tanks.ccool = m_pkt.data[10]
self.db.tanks.ccool = m_pkt.data[10]
self.db.tanks.ccool_need = m_pkt.data[11]
self.db.tanks.ccool_fill = m_pkt.data[12]
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.tanks)")
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == nil then
log.error(log_tag .. "unknown transaction reply")
@@ -162,8 +163,8 @@ boiler.new = function (session_id, unit_id, advert, out_queue)
-- update this runner
---@param time_now integer milliseconds
public.update = function (time_now)
if not self.periodics.has_build and self.periodics.next_build_req <= time_now then
function public.update(time_now)
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
@@ -177,10 +178,12 @@ boiler.new = function (session_id, unit_id, advert, out_queue)
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
self.session.post_update()
end
-- get the unit session database
public.get_db = function () return self.db end
function public.get_db() return self.db end
return public
end

View File

@@ -0,0 +1,230 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local unit_session = require("supervisor.session.rtu.unit_session")
local boilerv = {}
local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
local MODBUS_FCODE = types.MODBUS_FCODE
local TXN_TYPES = {
FORMED = 1,
BUILD = 2,
STATE = 3,
TANKS = 4
}
local TXN_TAGS = {
"boilerv.formed",
"boilerv.build",
"boilerv.state",
"boilerv.tanks"
}
local PERIODICS = {
FORMED = 2000,
BUILD = 1000,
STATE = 500,
TANKS = 1000
}
-- create a new boilerv rtu session runner
---@param session_id integer
---@param unit_id integer
---@param advert rtu_advertisement
---@param out_queue mqueue
function boilerv.new(session_id, unit_id, advert, out_queue)
-- type check
if advert.type ~= RTU_UNIT_TYPES.BOILER_VALVE then
log.error("attempt to instantiate boilerv RTU for type '" .. advert.type .. "'. this is a bug.")
return nil
end
local log_tag = "session.rtu(" .. session_id .. ").boilerv(" .. advert.index .. "): "
local self = {
session = unit_session.new(unit_id, advert, out_queue, log_tag, TXN_TAGS),
has_build = false,
periodics = {
next_formed_req = 0,
next_build_req = 0,
next_state_req = 0,
next_tanks_req = 0
},
---@class boilerv_session_db
db = {
formed = false,
build = {
length = 0,
width = 0,
height = 0,
min_pos = { x = 0, y = 0, z = 0 }, ---@type coordinate
max_pos = { x = 0, y = 0, z = 0 }, ---@type coordinate
boil_cap = 0.0,
steam_cap = 0,
water_cap = 0,
hcoolant_cap = 0,
ccoolant_cap = 0,
superheaters = 0,
max_boil_rate = 0.0,
env_loss = 0.0
},
state = {
temperature = 0.0,
boil_rate = 0.0
},
tanks = {
steam = { type = "mekanism:empty_gas", amount = 0 }, ---@type tank_fluid
steam_need = 0,
steam_fill = 0.0,
water = { type = "mekanism:empty_gas", amount = 0 }, ---@type tank_fluid
water_need = 0,
water_fill = 0.0,
hcool = { type = "mekanism:empty_gas", amount = 0 }, ---@type tank_fluid
hcool_need = 0,
hcool_fill = 0.0,
ccool = { type = "mekanism:empty_gas", amount = 0 }, ---@type tank_fluid
ccool_need = 0,
ccool_fill = 0.0
}
}
}
local public = self.session.get()
-- PRIVATE FUNCTIONS --
-- query if the multiblock is formed
local function _request_formed()
-- read discrete input 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 })
end
-- query the build of the device
local function _request_build()
-- read input registers 1 through 13 (start = 1, count = 13)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 13 })
end
-- query the state of the device
local function _request_state()
-- read input registers 14 through 15 (start = 14, count = 2)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 14, 2 })
end
-- query the tanks of the device
local function _request_tanks()
-- read input registers 16 through 27 (start = 16, count = 12)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 16, 12 })
end
-- PUBLIC FUNCTIONS --
-- handle a packet
---@param m_pkt modbus_frame
function public.handle_packet(m_pkt)
local txn_type = self.session.try_resolve(m_pkt)
if txn_type == false then
-- nothing to do
elseif txn_type == TXN_TYPES.FORMED then
-- formed response
-- load in data if correct length
if m_pkt.length == 1 then
self.db.formed = m_pkt.data[1]
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.BUILD then
-- build response
-- load in data if correct length
if m_pkt.length == 13 then
self.db.build.length = m_pkt.data[1]
self.db.build.width = m_pkt.data[2]
self.db.build.height = m_pkt.data[3]
self.db.build.min_pos = m_pkt.data[4]
self.db.build.max_pos = m_pkt.data[5]
self.db.build.boil_cap = m_pkt.data[6]
self.db.build.steam_cap = m_pkt.data[7]
self.db.build.water_cap = m_pkt.data[8]
self.db.build.hcoolant_cap = m_pkt.data[9]
self.db.build.ccoolant_cap = m_pkt.data[10]
self.db.build.superheaters = m_pkt.data[11]
self.db.build.max_boil_rate = m_pkt.data[12]
self.db.build.env_loss = m_pkt.data[13]
self.has_build = true
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.STATE then
-- state response
-- load in data if correct length
if m_pkt.length == 2 then
self.db.state.temperature = m_pkt.data[1]
self.db.state.boil_rate = m_pkt.data[2]
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.TANKS then
-- tanks response
-- load in data if correct length
if m_pkt.length == 12 then
self.db.tanks.steam = m_pkt.data[1]
self.db.tanks.steam_need = m_pkt.data[2]
self.db.tanks.steam_fill = m_pkt.data[3]
self.db.tanks.water = m_pkt.data[4]
self.db.tanks.water_need = m_pkt.data[5]
self.db.tanks.water_fill = m_pkt.data[6]
self.db.tanks.hcool = m_pkt.data[7]
self.db.tanks.hcool_need = m_pkt.data[8]
self.db.tanks.hcool_fill = m_pkt.data[9]
self.db.tanks.ccool = m_pkt.data[10]
self.db.tanks.ccool_need = m_pkt.data[11]
self.db.tanks.ccool_fill = m_pkt.data[12]
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == nil then
log.error(log_tag .. "unknown transaction reply")
else
log.error(log_tag .. "unknown transaction type " .. txn_type)
end
end
-- update this runner
---@param time_now integer milliseconds
function public.update(time_now)
if self.periodics.next_formed_req <= time_now then
_request_formed()
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
if self.db.formed then
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
if self.periodics.next_state_req <= time_now then
_request_state()
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
if self.periodics.next_tanks_req <= time_now then
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
end
self.session.post_update()
end
-- get the unit session database
function public.get_db() return self.db end
return public
end
return boilerv

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