Compare commits

...

81 Commits

Author SHA1 Message Date
Mikayla
acb7b5b4cb update README.md with new installer pastebin 2023-07-16 21:29:41 -04:00
Mikayla
9bd79dacad Merge pull request #281 from MikaylaFischler/devel
2023.07.16 Release
2023-07-16 21:25:00 -04:00
Mikayla Fischler
c544d140bf installer key handling improvements 2023-07-16 21:21:33 -04:00
Mikayla Fischler
353cb3622b improved installer any key detection 2023-07-16 21:11:27 -04:00
Mikayla Fischler
b54f15bad6 #274 bugfixes and optimizations 2023-07-16 21:07:37 -04:00
Mikayla Fischler
4d9783beca fixed installer clean bug 2023-07-16 20:58:34 -04:00
Mikayla Fischler
5529774b0e changed installer press enter to continue to any key, fixed some text colors 2023-07-16 20:53:39 -04:00
Mikayla Fischler
2a541ef3fe #274 cleanup functionality added to installer 2023-07-16 19:42:20 -04:00
Mikayla Fischler
e1b4d72ef8 updated legacy install manifest 2023-07-15 13:33:51 -04:00
Mikayla Fischler
6a0992c7a4 removed unused variable 2023-07-15 13:33:18 -04:00
Mikayla Fischler
cff7c724be #272 fixed bug with transmitting unit dynamic tank table 2023-07-15 13:31:48 -04:00
Mikayla Fischler
47bda73afe #272 basic dynamic tank data in supervisor and coordinator 2023-07-15 13:16:36 -04:00
Mikayla
8daedc109c added discord info to readme 2023-07-13 13:50:17 -04:00
Mikayla Fischler
a164c18a50 removed unused fields from dynamic tank rtu 2023-07-13 12:19:25 -04:00
Mikayla Fischler
4d663ada8d added high contrast yellow to rtu/plc/coord front panels 2023-07-12 13:38:37 -04:00
Mikayla Fischler
084a153a79 #268 fixed incorrect info print on extra wireless modem connection 2023-07-11 21:06:47 -04:00
Mikayla Fischler
4ed6ec1c63 correctly print new messages without overwrites in dmesg even if a prior message is a progress one 2023-07-11 21:01:24 -04:00
Mikayla Fischler
d3c2ba7bee update install manifest 2023-07-11 20:32:37 -04:00
Mikayla Fischler
55ff9dad4b #249 coordinator handle monitor disconnects/reconnects 2023-07-11 20:32:10 -04:00
Mikayla Fischler
0d6022f5e3 fixed always reporting failure to connect to supervisor even when inaccurate 2023-07-11 18:31:53 -04:00
Mikayla Fischler
8b136d78a8 #268 better handling of wireless modem peripherals 2023-07-11 18:22:09 -04:00
Mikayla Fischler
a5214730ef #260 added dynamic tank RTU 2023-07-11 17:27:03 -04:00
Mikayla Fischler
9f3ad3caf0 removed PLC establish packet handling when already linked 2023-07-11 16:17:24 -04:00
Mikayla
9bb2a99be5 Merge pull request #279 from MikaylaFischler/265-coordinator-front-panel
265 coordinator front panel
2023-07-11 15:37:17 -04:00
Mikayla Fischler
65ace26258 corrected comments 2023-07-11 15:36:41 -04:00
Mikayla Fischler
61d975d13f updated error messages for consistency 2023-07-11 15:15:44 -04:00
Mikayla Fischler
1d7d6e9817 update legacy install manifest 2023-07-11 13:38:21 -04:00
Mikayla Fischler
a2e0999cea combine coordinator supervisor connection event loop with main loop 2023-07-11 13:32:26 -04:00
Mikayla Fischler
1edee7f64b updated graphics comments 2023-07-09 23:42:44 -04:00
Mikayla Fischler
df61ec2c62 #265 coordinator front panel 2023-07-09 23:31:56 -04:00
Mikayla Fischler
bf7a316b04 don't start flasher if already started 2023-07-09 23:24:41 -04:00
Mikayla Fischler
96c4444184 corrected some comments 2023-07-09 23:22:24 -04:00
Mikayla Fischler
59eac62c33 #270 validate reactor PLC status packet types 2023-07-08 18:07:40 -04:00
Mikayla
ab193db153 Merge pull request #277 from MikaylaFischler/25-process-waste-control
25 process waste control
2023-07-08 17:12:23 -04:00
Mikayla Fischler
7d65bba589 fixes/cleanups for pull request 2023-07-08 17:11:51 -04:00
Mikayla Fischler
dcef5a96f0 removed unused function 2023-07-08 16:57:41 -04:00
Mikayla Fischler
ba0900ac65 #25 sna/sps integration, plutonium fallback, waste rate reporting 2023-07-08 16:57:13 -04:00
Mikayla Fischler
8f54e95519 #25 continued WIP waste control, main view updated and unit fields modified 2023-07-06 01:36:06 -04:00
Mikayla Fischler
7b9824b6f9 added checkbox graphics element 2023-07-01 19:40:33 -04:00
Mikayla Fischler
b6835fc7d1 #276 updated readme 2023-06-29 16:54:46 -04:00
Mikayla
bc5a94cd3b Update README.md 2023-06-29 12:57:25 -04:00
Mikayla
2a3d868402 Merge pull request #273 from MikaylaFischler/devel
2023.06.29 Release
2023-06-29 12:33:12 -04:00
Mikayla Fischler
b998634da1 installer fixes 2023-06-29 12:29:30 -04:00
Mikayla
5225380523 Merge pull request #271 from MikaylaFischler/51-hmac-message-authentication
HMAC Message Authentication
2023-06-27 19:09:38 -04:00
Mikayla Fischler
0e7ea7102c removed extra verbose comment in configs 2023-06-27 19:08:33 -04:00
Mikayla Fischler
8924ba4e99 cleanup and luacheck fixes 2023-06-27 19:05:51 -04:00
Mikayla Fischler
a8071db08e #51 send serialized data to properly MAC 2023-06-27 18:36:16 -04:00
Mikayla Fischler
fb3c7ded06 updated lockbox benchmark 2023-06-26 20:44:55 -04:00
Mikayla Fischler
f6b0a49904 added graphics version to crash dump 2023-06-26 14:03:36 -04:00
Mikayla Fischler
bfbbfb164b #51 include versioned lockbox in installer, reduced installer file size 2023-06-25 17:53:02 -04:00
Mikayla Fischler
57763702ff #51 init mac component from config key 2023-06-25 14:00:18 -04:00
Mikayla Fischler
f469754bb7 #51 network file cleanup 2023-06-25 13:06:03 -04:00
Mikayla Fischler
336662de62 #51 nic integration with rtu and supervisor 2023-06-25 12:59:38 -04:00
Mikayla Fischler
9073009eb0 #51 usage of nic.receive and some cleanup 2023-06-23 14:12:41 -04:00
Mikayla Fischler
ffac6996ed #51 PLC changes for new networking 2023-06-23 13:52:24 -04:00
Mikayla Fischler
da3c92b3bf Merge branch 'devel' into 51-hmac-message-authentication 2023-06-22 16:05:46 -04:00
Mikayla
712c7a8f3b #266 added health check to ppm and strengthened reliability of RTU hw state reporting 2023-06-22 19:46:17 +00:00
Mikayla
737afe586d renamed lockbox benchmark 2023-06-22 14:22:32 +00:00
Mikayla
d69796b607 lockbox benchmark cleanup 2023-06-22 14:21:00 +00:00
Mikayla
1cdf66a8c3 #51 WIP network interface controller 2023-06-21 23:04:39 +00:00
Mikayla
282c7db3eb Merge branch 'devel' into 51-hmac-message-authentication 2023-06-18 19:23:56 +00:00
Mikayla Fischler
a02529b9f7 #263 fixed bug with supervisor group map length not matching number of reactors 2023-06-18 15:19:01 -04:00
Mikayla Fischler
af38025f50 #262 don't ever abort RTU unit parsing on error, just skip 2023-06-18 14:26:38 -04:00
Mikayla Fischler
b28e4d1e95 #258 installer bugfix 2023-06-18 14:04:49 -04:00
Mikayla Fischler
75dfa3ae73 #258 luacheck fix 2023-06-18 13:16:28 -04:00
Mikayla Fischler
4a3455fa60 #258 luacheck fix 2023-06-18 13:13:34 -04:00
Mikayla Fischler
a2fa6570dc #258 installer improvement 2023-06-18 13:12:34 -04:00
Mikayla Fischler
aef8281ad6 #258 more installer fixes 2023-06-18 01:19:00 -04:00
Mikayla Fischler
d42327a20d #258 bugfixes 2023-06-18 01:09:46 -04:00
Mikayla Fischler
49db75f34d #258 installer bugfix 2023-06-18 01:04:40 -04:00
Mikayla Fischler
bc87030491 #258 installer improvements and test change to graphics version 2023-06-18 00:48:06 -04:00
Mikayla Fischler
9266d7d8e1 #258 versioned graphics component 2023-06-18 00:40:01 -04:00
Mikayla
ef5567ad46 #51 hmac verification 2023-06-11 18:26:55 +00:00
Mikayla Fischler
302f3d913f unlikely to use ldoc due to incompatibilities with vscode lua extension luadocs 2023-06-08 11:39:07 -04:00
Mikayla Fischler
650b9c1811 #244 luadoc actions fixes 2023-06-08 10:58:06 -04:00
Mikayla Fischler
543ac8c9fe updated ldoc version 2023-06-08 10:50:39 -04:00
Mikayla Fischler
7f19f76c0b update comment to force re-run 2023-06-08 10:46:35 -04:00
Mikayla
8d76c86309 fixed pages.yml format error 2023-06-08 10:42:49 -04:00
Mikayla Fischler
a4be6a6dde #244 luadoc in github actions 2023-06-08 10:41:44 -04:00
Mikayla Fischler
8b926a0978 #257 tick supervisor version to force installers to re-pull graphics 2023-06-07 21:42:21 -04:00
Mikayla Fischler
775ffc8094 added graphics to supervisor depends 2023-06-07 21:25:42 -04:00
113 changed files with 3734 additions and 3995 deletions

View File

@@ -1,5 +1,5 @@
# Simple workflow for deploying static content to GitHub Pages # Deploy installation manifests and shields versions
name: Deploy Installation Manifests and Versions name: Deploy Installation Data
on: on:
workflow_dispatch: workflow_dispatch:

2
.gitignore vendored
View File

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

View File

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

652
ccmsi.lua
View File

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

View File

@@ -11,6 +11,10 @@ config.TRUSTED_RANGE = 0
-- time in seconds (>= 2) before assuming a remote device is no longer active -- time in seconds (>= 2) before assuming a remote device is no longer active
config.SV_TIMEOUT = 5 config.SV_TIMEOUT = 5
config.API_TIMEOUT = 5 config.API_TIMEOUT = 5
-- facility authentication key (do NOT use one of your passwords)
-- this enables verifying that messages are authentic
-- all devices on the same network must use the same key
-- config.AUTH_KEY = "SCADAfacility123"
-- expected number of reactor units, used only to require that number of unit monitors -- expected number of reactor units, used only to require that number of unit monitors
config.NUM_UNITS = 4 config.NUM_UNITS = 4

View File

@@ -2,6 +2,7 @@ local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local util = require("scada-common.util") local util = require("scada-common.util")
local types = require("scada-common.types")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process") local process = require("coordinator.process")
@@ -12,7 +13,6 @@ local dialog = require("coordinator.ui.dialog")
local print = util.print local print = util.print
local println = util.println local println = util.println
local println_ts = util.println_ts
local PROTOCOL = comms.PROTOCOL local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE local DEVICE_TYPE = comms.DEVICE_TYPE
@@ -22,6 +22,8 @@ local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE
local UNIT_COMMAND = comms.UNIT_COMMAND local UNIT_COMMAND = comms.UNIT_COMMAND
local FAC_COMMAND = comms.FAC_COMMAND local FAC_COMMAND = comms.FAC_COMMAND
local LINK_TIMEOUT = 60.0
local coordinator = {} local coordinator = {}
-- request the user to select a monitor -- request the user to select a monitor
@@ -183,7 +185,8 @@ local function log_dmesg(message, dmesg_tag, working)
GRAPHICS = colors.green, GRAPHICS = colors.green,
SYSTEM = colors.cyan, SYSTEM = colors.cyan,
BOOT = colors.blue, BOOT = colors.blue,
COMMS = colors.purple COMMS = colors.purple,
CRYPTO = colors.yellow
} }
if working then if working then
@@ -197,6 +200,7 @@ function coordinator.log_graphics(message) log_dmesg(message, "GRAPHICS") end
function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end
function coordinator.log_boot(message) log_dmesg(message, "BOOT") end function coordinator.log_boot(message) log_dmesg(message, "BOOT") end
function coordinator.log_comms(message) log_dmesg(message, "COMMS") end function coordinator.log_comms(message) log_dmesg(message, "COMMS") end
function coordinator.log_crypto(message) log_dmesg(message, "CRYPTO") end
-- log a message for communications connecting, providing access to progress indication control functions -- log a message for communications connecting, providing access to progress indication control functions
---@nodiscard ---@nodiscard
@@ -212,38 +216,37 @@ end
-- coordinator communications -- coordinator communications
---@nodiscard ---@nodiscard
---@param version string coordinator version ---@param version string coordinator version
---@param modem table modem device ---@param nic nic network interface device
---@param crd_channel integer port of configured supervisor ---@param crd_channel integer port of configured supervisor
---@param svr_channel integer listening port for supervisor replys ---@param svr_channel integer listening port for supervisor replys
---@param pkt_channel integer listening port for pocket API ---@param pkt_channel integer listening port for pocket API
---@param range integer trusted device connection range ---@param range integer trusted device connection range
---@param sv_watchdog watchdog ---@param sv_watchdog watchdog
function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel, range, sv_watchdog) function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel, range, sv_watchdog)
local self = { local self = {
sv_linked = false, sv_linked = false,
sv_addr = comms.BROADCAST, sv_addr = comms.BROADCAST,
sv_seq_num = 0, sv_seq_num = 0,
sv_r_seq_num = nil, sv_r_seq_num = nil,
sv_config_err = false, sv_config_err = false,
connected = false,
last_est_ack = ESTABLISH_ACK.ALLOW, last_est_ack = ESTABLISH_ACK.ALLOW,
last_api_est_acks = {} last_api_est_acks = {},
est_start = 0,
est_last = 0,
est_tick_waiting = nil,
est_task_done = nil
} }
comms.set_trusted_range(range) comms.set_trusted_range(range)
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure modem channels -- configure network channels
local function _conf_channels() nic.closeAll()
modem.closeAll() nic.open(crd_channel)
modem.open(crd_channel)
end
_conf_channels() -- link nic to apisessions
apisessions.init(nic)
-- link modem to apisessions
apisessions.init(modem)
-- send a packet to the supervisor -- send a packet to the supervisor
---@param msg_type SCADA_MGMT_TYPE|SCADA_CRDN_TYPE ---@param msg_type SCADA_MGMT_TYPE|SCADA_CRDN_TYPE
@@ -263,7 +266,7 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel
pkt.make(msg_type, msg) pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable()) s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable())
modem.transmit(svr_channel, crd_channel, s_pkt.raw_sendable()) nic.transmit(svr_channel, crd_channel, s_pkt)
self.sv_seq_num = self.sv_seq_num + 1 self.sv_seq_num = self.sv_seq_num + 1
end end
@@ -277,7 +280,7 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel
m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, { ack }) m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, { ack })
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
modem.transmit(pkt_channel, crd_channel, s_pkt.raw_sendable()) nic.transmit(pkt_channel, crd_channel, s_pkt)
self.last_api_est_acks[packet.src_addr()] = ack self.last_api_est_acks[packet.src_addr()] = ack
end end
@@ -297,12 +300,61 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel
---@class coord_comms ---@class coord_comms
local public = {} local public = {}
-- reconnect a newly connected modem -- try to connect to the supervisor if not already linked
---@param new_modem table ---@param abort boolean? true to print out cancel info if not linked (use on program terminate)
function public.reconnect_modem(new_modem) ---@return boolean ok, boolean start_ui
modem = new_modem function public.try_connect(abort)
apisessions.relink_modem(new_modem) local ok = true
_conf_channels() local start_ui = false
if not self.sv_linked then
if self.est_tick_waiting == nil then
self.est_start = util.time_s()
self.est_last = self.est_start
self.est_tick_waiting, self.est_task_done =
coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. svr_channel)
_send_establish()
else
self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (util.time_s() - self.est_start)))
end
if abort or (util.time_s() - self.est_start) >= LINK_TIMEOUT then
self.est_task_done(false)
if abort then
coordinator.log_comms("supervisor connection attempt cancelled by user")
elseif self.sv_config_err then
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
elseif not self.sv_linked then
if self.last_est_ack == ESTABLISH_ACK.DENY then
coordinator.log_comms("supervisor connection attempt denied")
elseif self.last_est_ack == ESTABLISH_ACK.COLLISION then
coordinator.log_comms("supervisor connection failed due to collision")
elseif self.last_est_ack == ESTABLISH_ACK.BAD_VERSION then
coordinator.log_comms("supervisor connection failed due to version mismatch")
else
coordinator.log_comms("supervisor connection failed with no valid response")
end
end
ok = false
elseif self.sv_config_err then
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
ok = false
elseif (util.time_s() - self.est_last) > 1.0 then
_send_establish()
self.est_last = util.time_s()
end
elseif self.est_tick_waiting ~= nil then
self.est_task_done(true)
self.est_tick_waiting = nil
self.est_task_done = nil
start_ui = true
end
return ok, start_ui
end end
-- close the connection to the server -- close the connection to the server
@@ -311,71 +363,15 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel
self.sv_addr = comms.BROADCAST self.sv_addr = comms.BROADCAST
self.sv_linked = false self.sv_linked = false
self.sv_r_seq_num = nil self.sv_r_seq_num = nil
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {}) _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {})
end end
-- attempt to connect to the subervisor
---@nodiscard
---@param timeout_s number timeout in seconds
---@param tick_dmesg_waiting function callback to tick dmesg waiting
---@param task_done function callback to show done on dmesg
---@return boolean sv_linked true if connected, false otherwise
--- EVENT_CONSUMER: this function consumes events
function public.sv_connect(timeout_s, tick_dmesg_waiting, task_done)
local clock = util.new_clock(1)
local start = util.time_s()
local terminated = false
_send_establish()
clock.start()
while (util.time_s() - start) < timeout_s and (not self.sv_linked) and (not self.sv_config_err) do
local event, p1, p2, p3, p4, p5 = util.pull_event()
if event == "timer" and clock.is_clock(p1) then
-- timed out attempt, try again
tick_dmesg_waiting(math.max(0, timeout_s - (util.time_s() - start)))
_send_establish()
clock.start()
elseif event == "timer" then
-- keep checking watchdog timers
apisessions.check_all_watchdogs(p1)
elseif event == "modem_message" then
-- handle message
local packet = public.parse_packet(p1, p2, p3, p4, p5)
public.handle_packet(packet)
elseif event == "terminate" then
terminated = true
break
end
end
task_done(self.sv_linked)
if terminated then
coordinator.log_comms("supervisor connection attempt cancelled by user")
elseif self.sv_config_err then
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
elseif not self.sv_linked then
if self.last_est_ack == ESTABLISH_ACK.DENY then
coordinator.log_comms("supervisor connection attempt denied")
elseif self.last_est_ack == ESTABLISH_ACK.COLLISION then
coordinator.log_comms("supervisor connection failed due to collision")
elseif self.last_est_ack == ESTABLISH_ACK.BAD_VERSION then
coordinator.log_comms("supervisor connection failed due to version mismatch")
else
coordinator.log_comms("supervisor connection failed with no valid response")
end
end
return self.sv_linked
end
-- send a facility command -- send a facility command
---@param cmd FAC_COMMAND command ---@param cmd FAC_COMMAND command
function public.send_fac_command(cmd) ---@param option any? optional option options for the optional options (like waste mode)
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd }) function public.send_fac_command(cmd, option)
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd, option })
end end
-- send the auto process control configuration with a start command -- send the auto process control configuration with a start command
@@ -389,7 +385,7 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel
-- send a unit command -- send a unit command
---@param cmd UNIT_COMMAND command ---@param cmd UNIT_COMMAND command
---@param unit integer unit ID ---@param unit integer unit ID
---@param option any? optional option options for the optional options (like burn rate) (does option still look like a word?) ---@param option any? optional option options for the optional options (like burn rate)
function public.send_unit_command(cmd, unit, option) function public.send_unit_command(cmd, unit, option)
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_CMD, { cmd, unit, option }) _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
end end
@@ -402,13 +398,10 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel
---@param distance integer ---@param distance integer
---@return mgmt_frame|crdn_frame|capi_frame|nil packet ---@return mgmt_frame|crdn_frame|capi_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance) function public.parse_packet(side, sender, reply_to, message, distance)
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
local pkt = nil local pkt = nil
local s_pkt = comms.scada_packet()
-- parse packet as generic SCADA packet if s_pkt then
s_pkt.receive(side, sender, reply_to, message, distance)
if s_pkt.is_valid() then
-- get as SCADA management packet -- get as SCADA management packet
if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet() local mgmt_pkt = comms.mgmt_packet()
@@ -437,7 +430,10 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel
-- handle a packet -- handle a packet
---@param packet mgmt_frame|crdn_frame|capi_frame|nil ---@param packet mgmt_frame|crdn_frame|capi_frame|nil
---@return boolean close_ui
function public.handle_packet(packet) function public.handle_packet(packet)
local was_linked = self.sv_linked
if packet ~= nil then if packet ~= nil then
local l_chan = packet.scada_frame.local_channel() local l_chan = packet.scada_frame.local_channel()
local r_chan = packet.scada_frame.remote_channel() local r_chan = packet.scada_frame.remote_channel()
@@ -447,7 +443,9 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel
if l_chan ~= crd_channel then if l_chan ~= crd_channel then
log.debug("received packet on unconfigured channel " .. l_chan, true) log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == pkt_channel then elseif r_chan == pkt_channel then
if protocol == PROTOCOL.COORD_API then if not self.sv_linked then
log.debug("discarding pocket API packet before linked to supervisor")
elseif protocol == PROTOCOL.COORD_API then
---@cast packet capi_frame ---@cast packet capi_frame
-- look for an associated session -- look for an associated session
local session = apisessions.find_session(src_addr) local session = apisessions.find_session(src_addr)
@@ -486,7 +484,6 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel
elseif dev_type == DEVICE_TYPE.PKT then elseif dev_type == DEVICE_TYPE.PKT then
-- pocket linking request -- pocket linking request
local id = apisessions.establish_session(src_addr, firmware_v) local id = apisessions.establish_session(src_addr, firmware_v)
println(util.c("[API] pocket (", firmware_v, ") [@", src_addr, "] \xbb connected"))
coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id)) coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id))
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW) _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW)
@@ -509,12 +506,12 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel
-- check sequence number -- check sequence number
if self.sv_r_seq_num == nil then if self.sv_r_seq_num == nil then
self.sv_r_seq_num = packet.scada_frame.seq_num() self.sv_r_seq_num = packet.scada_frame.seq_num()
elseif self.connected and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then elseif self.sv_linked and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return return false
elseif self.sv_linked and src_addr ~= self.sv_addr then elseif self.sv_linked and src_addr ~= self.sv_addr then
log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?") log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?")
return return false
else else
self.sv_r_seq_num = packet.scada_frame.seq_num() self.sv_r_seq_num = packet.scada_frame.seq_num()
end end
@@ -576,6 +573,10 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel
end end
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
iocontrol.get_db().facility.ack_alarms_ack(ack) iocontrol.get_db().facility.ack_alarms_ack(ack)
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
process.waste_ack_handle(packet.data[2])
elseif cmd == FAC_COMMAND.SET_PU_FB then
process.pu_fb_ack_handle(packet.data[2])
else else
log.debug(util.c("received facility command ack with unknown command ", cmd)) log.debug(util.c("received facility command ack with unknown command ", cmd))
end end
@@ -640,70 +641,7 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel
end end
elseif protocol == PROTOCOL.SCADA_MGMT then elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then if self.sv_linked then
-- connection with supervisor established
if packet.length == 2 then
local est_ack = packet.data[1]
local config = packet.data[2]
if est_ack == ESTABLISH_ACK.ALLOW then
if type(config) == "table" and #config > 1 then
-- get configuration
---@class facility_conf
local conf = {
num_units = config[1], ---@type integer
defs = {} -- boilers and turbines
}
if (#config - 1) == (conf.num_units * 2) then
-- record sequence of pairs of [#boilers, #turbines] per unit
for i = 2, #config do
table.insert(conf.defs, config[i])
end
-- init io controller
iocontrol.init(conf, public)
self.sv_addr = src_addr
self.sv_linked = true
self.sv_config_err = false
else
self.sv_config_err = true
log.warning("invalid supervisor configuration definitions received, establish failed")
end
else
log.debug("invalid supervisor configuration table received, establish failed")
end
else
log.debug("SCADA_MGMT establish packet reply (len = 2) unsupported")
end
self.last_est_ack = est_ack
elseif packet.length == 1 then
local est_ack = packet.data[1]
if est_ack == ESTABLISH_ACK.DENY then
if self.last_est_ack ~= est_ack then
log.info("supervisor connection denied")
end
elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.last_est_ack ~= est_ack then
log.warning("supervisor connection denied due to collision")
end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.last_est_ack ~= est_ack then
log.warning("supervisor comms version mismatch")
end
else
log.debug("SCADA_MGMT establish packet reply (len = 1) unsupported")
end
self.last_est_ack = est_ack
else
log.debug("SCADA_MGMT establish packet length mismatch")
end
elseif self.sv_linked then
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back -- keep alive request received, echo back
if packet.length == 1 then if packet.length == 1 then
@@ -728,11 +666,83 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel
self.sv_addr = comms.BROADCAST self.sv_addr = comms.BROADCAST
self.sv_linked = false self.sv_linked = false
self.sv_r_seq_num = nil self.sv_r_seq_num = nil
println_ts("server connection closed by remote host") iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
log.info("server connection closed by remote host") log.info("server connection closed by remote host")
else else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type) log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
end end
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- connection with supervisor established
if packet.length == 2 then
local est_ack = packet.data[1]
local config = packet.data[2]
if est_ack == ESTABLISH_ACK.ALLOW then
-- reset to disconnected before validating
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
if type(config) == "table" and #config > 1 then
-- get configuration
---@class facility_conf
local conf = {
num_units = config[1], ---@type integer
defs = {} -- boilers and turbines
}
if (#config - 1) == (conf.num_units * 2) then
-- record sequence of pairs of [#boilers, #turbines] per unit
for i = 2, #config do
table.insert(conf.defs, config[i])
end
-- init io controller
iocontrol.init(conf, public)
self.sv_addr = src_addr
self.sv_linked = true
self.sv_r_seq_num = nil
self.sv_config_err = false
iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED)
else
self.sv_config_err = true
log.warning("invalid supervisor configuration definitions received, establish failed")
end
else
log.debug("invalid supervisor configuration table received, establish failed")
end
else
log.debug("SCADA_MGMT establish packet reply (len = 2) unsupported")
end
self.last_est_ack = est_ack
elseif packet.length == 1 then
local est_ack = packet.data[1]
if est_ack == ESTABLISH_ACK.DENY then
if self.last_est_ack ~= est_ack then
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DENIED)
log.info("supervisor connection denied")
end
elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.last_est_ack ~= est_ack then
iocontrol.fp_link_state(types.PANEL_LINK_STATE.COLLISION)
log.warning("supervisor connection denied due to collision")
end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.last_est_ack ~= est_ack then
iocontrol.fp_link_state(types.PANEL_LINK_STATE.BAD_VERSION)
log.warning("supervisor comms version mismatch")
end
else
log.debug("SCADA_MGMT establish packet reply (len = 1) unsupported")
end
self.last_est_ack = est_ack
else
log.debug("SCADA_MGMT establish packet length mismatch")
end
else else
log.debug("discarding non-link SCADA_MGMT packet before linked") log.debug("discarding non-link SCADA_MGMT packet before linked")
end end
@@ -743,6 +753,8 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel
log.debug("received packet for unknown channel " .. r_chan, true) log.debug("received packet for unknown channel " .. r_chan, true)
end end
end end
return was_linked and not self.sv_linked
end end
-- check if the coordinator is still linked to the supervisor -- check if the coordinator is still linked to the supervisor

View File

@@ -10,9 +10,15 @@ local util = require("scada-common.util")
local process = require("coordinator.process") local process = require("coordinator.process")
local sounder = require("coordinator.sounder") local sounder = require("coordinator.sounder")
local pgi = require("coordinator.ui.pgi")
local ALARM_STATE = types.ALARM_STATE local ALARM_STATE = types.ALARM_STATE
local PROCESS = types.PROCESS local PROCESS = types.PROCESS
-- nominal RTT is ping (0ms to 10ms usually) + 500ms for CRD main loop tick
local WARN_RTT = 1000 -- 2x as long as expected w/ 0 ping
local HIGH_RTT = 1500 -- 3.33x as long as expected w/ 0 ping
local iocontrol = {} local iocontrol = {}
---@class ioctl ---@class ioctl
@@ -27,6 +33,19 @@ local function __generic_ack(success) end
-- luacheck: unused args -- luacheck: unused args
-- initialize front panel PSIL
---@param firmware_v string coordinator version
---@param comms_v string comms version
function iocontrol.init_fp(firmware_v, comms_v)
---@class ioctl_front_panel
io.fp = {
ps = psil.create()
}
io.fp.ps.publish("version", firmware_v)
io.fp.ps.publish("comms_version", comms_v)
end
-- initialize the coordinator IO controller -- initialize the coordinator IO controller
---@param conf facility_conf configuration ---@param conf facility_conf configuration
---@param comms coord_comms comms reference ---@param comms coord_comms comms reference
@@ -52,6 +71,10 @@ function iocontrol.init(conf, comms)
gen_fault = false gen_fault = false
}, },
---@type WASTE_PRODUCT
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
auto_pu_fallback_active = false,
radiation = types.new_zero_radiation_reading(), radiation = types.new_zero_radiation_reading(),
save_cfg_ack = __generic_ack, save_cfg_ack = __generic_ack,
@@ -65,16 +88,21 @@ function iocontrol.init(conf, comms)
induction_ps_tbl = {}, induction_ps_tbl = {},
induction_data_tbl = {}, induction_data_tbl = {},
sps_ps_tbl = {},
sps_data_tbl = {},
tank_ps_tbl = {},
tank_data_tbl = {},
env_d_ps = psil.create(), env_d_ps = psil.create(),
env_d_data = {} env_d_data = {}
} }
-- create induction tables (currently only 1 is supported) -- create induction and SPS tables (currently only 1 of each is supported)
for _ = 1, conf.num_units do table.insert(io.facility.induction_ps_tbl, psil.create())
local data = {} ---@type imatrix_session_db table.insert(io.facility.induction_data_tbl, {})
table.insert(io.facility.induction_ps_tbl, psil.create()) table.insert(io.facility.sps_ps_tbl, psil.create())
table.insert(io.facility.induction_data_tbl, data) table.insert(io.facility.sps_data_tbl, {})
end
io.units = {} io.units = {}
for i = 1, conf.num_units do for i = 1, conf.num_units do
@@ -87,11 +115,15 @@ function iocontrol.init(conf, comms)
num_boilers = 0, num_boilers = 0,
num_turbines = 0, num_turbines = 0,
num_snas = 0,
control_state = false, control_state = false,
burn_rate_cmd = 0.0, burn_rate_cmd = 0.0,
waste_control = 0,
radiation = types.new_zero_radiation_reading(), radiation = types.new_zero_radiation_reading(),
sna_prod_rate = 0.0,
waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
waste_product = types.WASTE_PRODUCT.PLUTONIUM,
-- auto control group -- auto control group
a_group = 0, a_group = 0,
@@ -100,10 +132,10 @@ function iocontrol.init(conf, comms)
scram = function () process.scram(i) end, scram = function () process.scram(i) end,
reset_rps = function () process.reset_rps(i) end, reset_rps = function () process.reset_rps(i) end,
ack_alarms = function () process.ack_all_alarms(i) end, ack_alarms = function () process.ack_all_alarms(i) end,
set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate
set_waste = function (mode) process.set_waste(i, mode) end, ---@param mode integer waste processing mode set_waste = function (mode) process.set_unit_waste(i, mode) end, ---@param mode WASTE_MODE waste processing mode
set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0 set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0 for manual
start_ack = __generic_ack, start_ack = __generic_ack,
scram_ack = __generic_ack, scram_ack = __generic_ack,
@@ -152,7 +184,10 @@ function iocontrol.init(conf, comms)
boiler_data_tbl = {}, boiler_data_tbl = {},
turbine_ps_tbl = {}, turbine_ps_tbl = {},
turbine_data_tbl = {} turbine_data_tbl = {},
tank_ps_tbl = {},
tank_data_tbl = {}
} }
-- create boiler tables -- create boiler tables
@@ -179,6 +214,92 @@ function iocontrol.init(conf, comms)
process.init(io, comms) process.init(io, comms)
end end
--#region Front Panel PSIL
-- toggle heartbeat indicator
function iocontrol.heartbeat() io.fp.ps.toggle("heartbeat") end
-- report presence of the wireless modem
---@param has_modem boolean
function iocontrol.fp_has_modem(has_modem) io.fp.ps.publish("has_modem", has_modem) end
-- report presence of the speaker
---@param has_speaker boolean
function iocontrol.fp_has_speaker(has_speaker) io.fp.ps.publish("has_speaker", has_speaker) end
-- report supervisor link state
---@param state integer
function iocontrol.fp_link_state(state) io.fp.ps.publish("link_state", state) end
-- report monitor connection state
---@param id integer unit ID or 0 for main
function iocontrol.fp_monitor_state(id, connected)
local name = "main_monitor"
if id > 0 then name = "unit_monitor_" .. id end
io.fp.ps.publish(name, connected)
end
-- report PKT firmware version and PKT session connection state
---@param session_id integer PKT session
---@param fw string firmware version
---@param s_addr integer PKT computer ID
function iocontrol.fp_pkt_connected(session_id, fw, s_addr)
io.fp.ps.publish("pkt_" .. session_id .. "_fw", fw)
io.fp.ps.publish("pkt_" .. session_id .. "_addr", util.sprintf("@ C% 3d", s_addr))
pgi.create_pkt_entry(session_id)
end
-- report PKT session disconnected
---@param session_id integer PKT session
function iocontrol.fp_pkt_disconnected(session_id)
pgi.delete_pkt_entry(session_id)
end
-- transmit PKT session RTT
---@param session_id integer PKT session
---@param rtt integer round trip time
function iocontrol.fp_pkt_rtt(session_id, rtt)
io.fp.ps.publish("pkt_" .. session_id .. "_rtt", rtt)
if rtt > HIGH_RTT then
io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.red)
elseif rtt > WARN_RTT then
io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.yellow_hc)
else
io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.green)
end
end
--#endregion
--#region Builds
-- record and publish multiblock RTU build data
---@param id integer
---@param entry table
---@param data_tbl table
---@param ps_tbl table
---@param create boolean? true to create an entry if non exists, false to fail on missing
---@return boolean ok true if data saved, false if invalid ID
local function _record_multiblock_build(id, entry, data_tbl, ps_tbl, create)
local exists = type(data_tbl[id]) == "table"
if exists or create then
if not exists then
ps_tbl[id] = psil.create()
data_tbl[id] = {}
end
data_tbl[id].formed = entry[1] ---@type boolean
data_tbl[id].build = entry[2] ---@type table
ps_tbl[id].publish("formed", entry[1])
for key, val in pairs(data_tbl[id].build) do ps_tbl[id].publish(key, val) end
end
return exists or (create == true)
end
-- populate facility structure builds -- populate facility structure builds
---@param build table ---@param build table
---@return boolean valid ---@return boolean valid
@@ -191,21 +312,29 @@ function iocontrol.record_facility_builds(build)
-- induction matricies -- induction matricies
if type(build.induction) == "table" then if type(build.induction) == "table" then
for id, matrix in pairs(build.induction) do for id, matrix in pairs(build.induction) do
if type(fac.induction_data_tbl[id]) == "table" then if not _record_multiblock_build(id, matrix, fac.induction_data_tbl, fac.induction_ps_tbl) then
fac.induction_data_tbl[id].formed = matrix[1] ---@type boolean
fac.induction_data_tbl[id].build = matrix[2] ---@type table
fac.induction_ps_tbl[id].publish("formed", matrix[1])
for key, val in pairs(fac.induction_data_tbl[id].build) do
fac.induction_ps_tbl[id].publish(key, val)
end
else
log.debug(util.c("iocontrol.record_facility_builds: invalid induction matrix id ", id)) log.debug(util.c("iocontrol.record_facility_builds: invalid induction matrix id ", id))
valid = false valid = false
end end
end end
end end
-- SPS
if type(build.sps) == "table" then
for id, sps in pairs(build.sps) do
if not _record_multiblock_build(id, sps, fac.sps_data_tbl, fac.sps_ps_tbl) then
log.debug(util.c("iocontrol.record_facility_builds: invalid SPS id ", id))
valid = false
end
end
end
-- dynamic tanks
if type(build.tanks) == "table" then
for id, tank in pairs(build.tanks) do
_record_multiblock_build(id, tank, fac.tank_data_tbl, fac.tank_ps_tbl, true)
end
end
else else
log.debug("facility builds not a table") log.debug("facility builds not a table")
valid = false valid = false
@@ -249,16 +378,7 @@ function iocontrol.record_unit_builds(builds)
-- boiler builds -- boiler builds
if type(build.boilers) == "table" then if type(build.boilers) == "table" then
for b_id, boiler in pairs(build.boilers) do for b_id, boiler in pairs(build.boilers) do
if type(unit.boiler_data_tbl[b_id]) == "table" then if not _record_multiblock_build(b_id, boiler, unit.boiler_data_tbl, unit.boiler_ps_tbl) then
unit.boiler_data_tbl[b_id].formed = boiler[1] ---@type boolean
unit.boiler_data_tbl[b_id].build = boiler[2] ---@type table
unit.boiler_ps_tbl[b_id].publish("formed", boiler[1])
for key, val in pairs(unit.boiler_data_tbl[b_id].build) do
unit.boiler_ps_tbl[b_id].publish(key, val)
end
else
log.debug(util.c(log_header, "invalid boiler id ", b_id)) log.debug(util.c(log_header, "invalid boiler id ", b_id))
valid = false valid = false
end end
@@ -268,27 +388,49 @@ function iocontrol.record_unit_builds(builds)
-- turbine builds -- turbine builds
if type(build.turbines) == "table" then if type(build.turbines) == "table" then
for t_id, turbine in pairs(build.turbines) do for t_id, turbine in pairs(build.turbines) do
if type(unit.turbine_data_tbl[t_id]) == "table" then if not _record_multiblock_build(t_id, turbine, unit.turbine_data_tbl, unit.turbine_ps_tbl) then
unit.turbine_data_tbl[t_id].formed = turbine[1] ---@type boolean
unit.turbine_data_tbl[t_id].build = turbine[2] ---@type table
unit.turbine_ps_tbl[t_id].publish("formed", turbine[1])
for key, val in pairs(unit.turbine_data_tbl[t_id].build) do
unit.turbine_ps_tbl[t_id].publish(key, val)
end
else
log.debug(util.c(log_header, "invalid turbine id ", t_id)) log.debug(util.c(log_header, "invalid turbine id ", t_id))
valid = false valid = false
end end
end end
end end
-- dynamic tank builds
if type(build.tanks) == "table" then
for d_id, d_tank in pairs(build.tanks) do
_record_multiblock_build(d_id, d_tank, unit.tank_data_tbl, unit.tank_ps_tbl, true)
end
end
end end
end end
return valid return valid
end end
--#endregion
--#region Statuses
-- record and publish multiblock status data
---@param entry any
---@param data imatrix_session_db|sps_session_db|dynamicv_session_db|turbinev_session_db|boilerv_session_db
---@param ps psil
---@return boolean is_faulted
local function _record_multiblock_status(entry, data, ps)
local is_faulted = entry[1] ---@type boolean
data.formed = entry[2] ---@type boolean
data.state = entry[3] ---@type table
data.tanks = entry[4] ---@type table
ps.publish("formed", data.formed)
ps.publish("faulted", is_faulted)
for key, val in pairs(data.state) do ps.publish(key, val) end
for key, val in pairs(data.tanks) do ps.publish(key, val) end
return is_faulted
end
-- update facility status -- update facility status
---@param status table ---@param status table
---@return boolean valid ---@return boolean valid
@@ -306,7 +448,7 @@ function iocontrol.update_facility_status(status)
local ctl_status = status[1] local ctl_status = status[1]
if type(ctl_status) == "table" and #ctl_status == 14 then if type(ctl_status) == "table" and #ctl_status == 16 then
fac.all_sys_ok = ctl_status[1] fac.all_sys_ok = ctl_status[1]
fac.auto_ready = ctl_status[2] fac.auto_ready = ctl_status[2]
@@ -354,6 +496,12 @@ function iocontrol.update_facility_status(status)
io.units[i].unit_ps.publish("auto_group", names[group_map[i] + 1]) io.units[i].unit_ps.publish("auto_group", names[group_map[i] + 1])
end end
end end
fac.auto_current_waste_product = ctl_status[15]
fac.auto_pu_fallback_active = ctl_status[16]
fac.ps.publish("current_waste_product", fac.auto_current_waste_product)
fac.ps.publish("pu_fallback_active", fac.auto_pu_fallback_active)
else else
log.debug(log_header .. "control status not a table or length mismatch") log.debug(log_header .. "control status not a table or length mismatch")
valid = false valid = false
@@ -390,36 +538,23 @@ function iocontrol.update_facility_status(status)
for id, matrix in pairs(rtu_statuses.induction) do for id, matrix in pairs(rtu_statuses.induction) do
if type(fac.induction_data_tbl[id]) == "table" then if type(fac.induction_data_tbl[id]) == "table" then
local rtu_faulted = matrix[1] ---@type boolean local data = fac.induction_data_tbl[id] ---@type imatrix_session_db
fac.induction_data_tbl[id].formed = matrix[2] ---@type boolean local ps = fac.induction_ps_tbl[id] ---@type psil
fac.induction_data_tbl[id].state = matrix[3] ---@type table
fac.induction_data_tbl[id].tanks = matrix[4] ---@type table
local data = fac.induction_data_tbl[id] ---@type imatrix_session_db local rtu_faulted = _record_multiblock_status(matrix, data, ps)
fac.induction_ps_tbl[id].publish("formed", data.formed) if rtu_faulted then
fac.induction_ps_tbl[id].publish("faulted", rtu_faulted) ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.formed then if data.tanks.energy_fill >= 0.99 then
if rtu_faulted then ps.publish("computed_status", 6) -- full
fac.induction_ps_tbl[id].publish("computed_status", 3) -- faulted
elseif data.tanks.energy_fill >= 0.99 then
fac.induction_ps_tbl[id].publish("computed_status", 6) -- full
elseif data.tanks.energy_fill <= 0.01 then elseif data.tanks.energy_fill <= 0.01 then
fac.induction_ps_tbl[id].publish("computed_status", 5) -- empty ps.publish("computed_status", 5) -- empty
else else
fac.induction_ps_tbl[id].publish("computed_status", 4) -- on-line ps.publish("computed_status", 4) -- on-line
end end
else else
fac.induction_ps_tbl[id].publish("computed_status", 2) -- not formed ps.publish("computed_status", 2) -- not formed
end
for key, val in pairs(fac.induction_data_tbl[id].state) do
fac.induction_ps_tbl[id].publish(key, val)
end
for key, val in pairs(fac.induction_data_tbl[id].tanks) do
fac.induction_ps_tbl[id].publish(key, val)
end end
else else
log.debug(util.c(log_header, "invalid induction matrix id ", id)) log.debug(util.c(log_header, "invalid induction matrix id ", id))
@@ -430,6 +565,82 @@ function iocontrol.update_facility_status(status)
valid = false valid = false
end end
-- SPS statuses
if type(rtu_statuses.sps) == "table" then
for id = 1, #fac.sps_ps_tbl do
if rtu_statuses.sps[id] == nil then
-- disconnected
fac.sps_ps_tbl[id].publish("computed_status", 1)
end
end
for id, sps in pairs(rtu_statuses.sps) do
if type(fac.sps_data_tbl[id]) == "table" then
local data = fac.sps_data_tbl[id] ---@type sps_session_db
local ps = fac.sps_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(sps, data, ps)
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.state.process_rate > 0 then
ps.publish("computed_status", 5) -- active
else
ps.publish("computed_status", 4) -- idle
end
else
ps.publish("computed_status", 2) -- not formed
end
io.facility.ps.publish("am_rate", data.state.process_rate * 1000)
else
log.debug(util.c(log_header, "invalid sps id ", id))
end
end
else
log.debug(log_header .. "sps list not a table")
valid = false
end
-- dynamic tank statuses
if type(rtu_statuses.tanks) == "table" then
for id = 1, #fac.tank_ps_tbl do
if rtu_statuses.tanks[id] == nil then
-- disconnected
fac.tank_ps_tbl[id].publish("computed_status", 1)
end
end
for id, tank in pairs(rtu_statuses.tanks) do
if type(fac.tank_data_tbl[id]) == "table" then
local data = fac.tank_data_tbl[id] ---@type dynamicv_session_db
local ps = fac.tank_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(tank, data, ps)
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.tanks.fill >= 0.99 then
ps.publish("computed_status", 6) -- full
elseif data.tanks.fill < 0.20 then
ps.publish("computed_status", 5) -- low
else
ps.publish("computed_status", 4) -- on-line
end
else
ps.publish("computed_status", 2) -- not formed
end
else
log.debug(util.c(log_header, "invalid dynamic tank id ", id))
end
end
else
log.debug(log_header .. "dyanmic tank list not a table")
valid = false
end
-- environment detector status -- environment detector status
if type(rtu_statuses.rad_mon) == "table" then if type(rtu_statuses.rad_mon) == "table" then
if #rtu_statuses.rad_mon > 0 then if #rtu_statuses.rad_mon > 0 then
@@ -472,6 +683,9 @@ function iocontrol.update_unit_statuses(statuses)
valid = false valid = false
else else
local burn_rate_sum = 0.0 local burn_rate_sum = 0.0
local sna_count_sum = 0
local pu_rate = 0.0
local po_rate = 0.0
-- get all unit statuses -- get all unit statuses
for i = 1, #statuses do for i = 1, #statuses do
@@ -480,6 +694,8 @@ function iocontrol.update_unit_statuses(statuses)
local unit = io.units[i] ---@type ioctl_unit local unit = io.units[i] ---@type ioctl_unit
local status = statuses[i] local status = statuses[i]
local burn_rate = 0.0
if type(status) ~= "table" or #status ~= 5 then if type(status) ~= "table" or #status ~= 5 then
log.debug(log_header .. "invalid status entry in unit statuses (not a table or invalid length)") log.debug(log_header .. "invalid status entry in unit statuses (not a table or invalid length)")
valid = false valid = false
@@ -515,7 +731,8 @@ function iocontrol.update_unit_statuses(statuses)
-- if status hasn't been received, mek_status = {} -- if status hasn't been received, mek_status = {}
if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then
burn_rate_sum = burn_rate_sum + unit.reactor_data.mek_status.act_burn_rate burn_rate = unit.reactor_data.mek_status.act_burn_rate
burn_rate_sum = burn_rate_sum + burn_rate
end end
if unit.reactor_data.mek_status.status then if unit.reactor_data.mek_status.status then
@@ -571,34 +788,21 @@ function iocontrol.update_unit_statuses(statuses)
for id, boiler in pairs(rtu_statuses.boilers) do for id, boiler in pairs(rtu_statuses.boilers) do
if type(unit.boiler_data_tbl[id]) == "table" then if type(unit.boiler_data_tbl[id]) == "table" then
local rtu_faulted = boiler[1] ---@type boolean local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
unit.boiler_data_tbl[id].formed = boiler[2] ---@type boolean local ps = unit.boiler_ps_tbl[id] ---@type psil
unit.boiler_data_tbl[id].state = boiler[3] ---@type table
unit.boiler_data_tbl[id].tanks = boiler[4] ---@type table
local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db local rtu_faulted = _record_multiblock_status(boiler, data, ps)
unit.boiler_ps_tbl[id].publish("formed", data.formed)
unit.boiler_ps_tbl[id].publish("faulted", rtu_faulted)
if rtu_faulted then if rtu_faulted then
unit.boiler_ps_tbl[id].publish("computed_status", 3) -- faulted ps.publish("computed_status", 3) -- faulted
elseif data.formed then elseif data.formed then
if data.state.boil_rate > 0 then if data.state.boil_rate > 0 then
unit.boiler_ps_tbl[id].publish("computed_status", 5) -- active ps.publish("computed_status", 5) -- active
else else
unit.boiler_ps_tbl[id].publish("computed_status", 4) -- idle ps.publish("computed_status", 4) -- idle
end end
else else
unit.boiler_ps_tbl[id].publish("computed_status", 2) -- not formed ps.publish("computed_status", 2) -- not formed
end
for key, val in pairs(unit.boiler_data_tbl[id].state) do
unit.boiler_ps_tbl[id].publish(key, val)
end
for key, val in pairs(unit.boiler_data_tbl[id].tanks) do
unit.boiler_ps_tbl[id].publish(key, val)
end end
else else
log.debug(util.c(log_header, "invalid boiler id ", id)) log.debug(util.c(log_header, "invalid boiler id ", id))
@@ -621,36 +825,23 @@ function iocontrol.update_unit_statuses(statuses)
for id, turbine in pairs(rtu_statuses.turbines) do for id, turbine in pairs(rtu_statuses.turbines) do
if type(unit.turbine_data_tbl[id]) == "table" then if type(unit.turbine_data_tbl[id]) == "table" then
local rtu_faulted = turbine[1] ---@type boolean
unit.turbine_data_tbl[id].formed = turbine[2] ---@type boolean
unit.turbine_data_tbl[id].state = turbine[3] ---@type table
unit.turbine_data_tbl[id].tanks = turbine[4] ---@type table
local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db
local ps = unit.turbine_ps_tbl[id] ---@type psil
unit.turbine_ps_tbl[id].publish("formed", data.formed) local rtu_faulted = _record_multiblock_status(turbine, data, ps)
unit.turbine_ps_tbl[id].publish("faulted", rtu_faulted)
if rtu_faulted then if rtu_faulted then
unit.turbine_ps_tbl[id].publish("computed_status", 3) -- faulted ps.publish("computed_status", 3) -- faulted
elseif data.formed then elseif data.formed then
if data.tanks.energy_fill >= 0.99 then if data.tanks.energy_fill >= 0.99 then
unit.turbine_ps_tbl[id].publish("computed_status", 6) -- trip ps.publish("computed_status", 6) -- trip
elseif data.state.flow_rate < 100 then elseif data.state.flow_rate < 100 then
unit.turbine_ps_tbl[id].publish("computed_status", 4) -- idle ps.publish("computed_status", 4) -- idle
else else
unit.turbine_ps_tbl[id].publish("computed_status", 5) -- active ps.publish("computed_status", 5) -- active
end end
else else
unit.turbine_ps_tbl[id].publish("computed_status", 2) -- not formed ps.publish("computed_status", 2) -- not formed
end
for key, val in pairs(unit.turbine_data_tbl[id].state) do
unit.turbine_ps_tbl[id].publish(key, val)
end
for key, val in pairs(unit.turbine_data_tbl[id].tanks) do
unit.turbine_ps_tbl[id].publish(key, val)
end end
else else
log.debug(util.c(log_header, "invalid turbine id ", id)) log.debug(util.c(log_header, "invalid turbine id ", id))
@@ -662,6 +853,58 @@ function iocontrol.update_unit_statuses(statuses)
valid = false valid = false
end end
-- dynamic tank statuses
if type(rtu_statuses.tanks) == "table" then
for id = 1, #unit.tank_ps_tbl do
if rtu_statuses.tanks[i] == nil then
-- disconnected
unit.tank_ps_tbl[id].publish("computed_status", 1)
end
end
for id, tank in pairs(rtu_statuses.tanks) do
if type(unit.tank_data_tbl[id]) == "table" then
local data = unit.tank_data_tbl[id] ---@type dynamicv_session_db
local ps = unit.tank_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(tank, data, ps)
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.tanks.fill >= 0.99 then
ps.publish("computed_status", 6) -- full
elseif data.tanks.fill < 0.20 then
ps.publish("computed_status", 5) -- low
else
ps.publish("computed_status", 5) -- active
end
else
ps.publish("computed_status", 2) -- not formed
end
else
log.debug(util.c(log_header, "invalid dynamic tank id ", id))
valid = false
end
end
else
log.debug(log_header .. "dynamic tank list not a table")
valid = false
end
-- solar neutron activator status info
if type(rtu_statuses.sna) == "table" then
unit.num_snas = rtu_statuses.sna[1] ---@type integer
unit.sna_prod_rate = rtu_statuses.sna[2] ---@type number
unit.unit_ps.publish("sna_prod_rate", unit.sna_prod_rate)
sna_count_sum = sna_count_sum + unit.num_snas
else
log.debug(log_header .. "sna statistic list not a table")
valid = false
end
-- environment detector status -- environment detector status
if type(rtu_statuses.rad_mon) == "table" then if type(rtu_statuses.rad_mon) == "table" then
if #rtu_statuses.rad_mon > 0 then if #rtu_statuses.rad_mon > 0 then
@@ -739,12 +982,17 @@ function iocontrol.update_unit_statuses(statuses)
local unit_state = status[5] local unit_state = status[5]
if type(unit_state) == "table" then if type(unit_state) == "table" then
if #unit_state == 5 then if #unit_state == 6 then
unit.waste_mode = unit_state[5]
unit.waste_product = unit_state[6]
unit.unit_ps.publish("U_StatusLine1", unit_state[1]) unit.unit_ps.publish("U_StatusLine1", unit_state[1])
unit.unit_ps.publish("U_StatusLine2", unit_state[2]) unit.unit_ps.publish("U_StatusLine2", unit_state[2])
unit.unit_ps.publish("U_WasteMode", unit_state[3]) unit.unit_ps.publish("U_AutoReady", unit_state[3])
unit.unit_ps.publish("U_AutoReady", unit_state[4]) unit.unit_ps.publish("U_AutoDegraded", unit_state[4])
unit.unit_ps.publish("U_AutoDegraded", unit_state[5]) unit.unit_ps.publish("U_AutoWaste", unit.waste_mode == types.WASTE_MODE.AUTO)
unit.unit_ps.publish("U_WasteMode", unit.waste_mode)
unit.unit_ps.publish("U_WasteProduct", unit.waste_product)
else else
log.debug(log_header .. "unit state length mismatch") log.debug(log_header .. "unit state length mismatch")
valid = false valid = false
@@ -753,10 +1001,18 @@ function iocontrol.update_unit_statuses(statuses)
log.debug(log_header .. "unit state not a table") log.debug(log_header .. "unit state not a table")
valid = false valid = false
end end
-- determine waste production for this unit, add to statistics
local is_pu = unit.waste_product == types.WASTE_PRODUCT.PLUTONIUM
pu_rate = pu_rate + util.trinary(is_pu, burn_rate / 10.0, 0.0)
po_rate = po_rate + util.trinary(not is_pu, math.min(burn_rate / 10.0, unit.sna_prod_rate), 0.0)
end end
end end
io.facility.ps.publish("burn_sum", burn_rate_sum) io.facility.ps.publish("burn_sum", burn_rate_sum)
io.facility.ps.publish("sna_count", sna_count_sum)
io.facility.ps.publish("pu_rate", pu_rate)
io.facility.ps.publish("po_rate", po_rate)
-- update alarm sounder -- update alarm sounder
sounder.eval(io.units) sounder.eval(io.units)
@@ -765,6 +1021,8 @@ function iocontrol.update_unit_statuses(statuses)
return valid return valid
end end
--#endregion
-- get the IO controller database -- get the IO controller database
function iocontrol.get_db() return io end function iocontrol.get_db() return io end

View File

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

View File

@@ -5,8 +5,12 @@
local log = require("scada-common.log") local log = require("scada-common.log")
local util = require("scada-common.util") local util = require("scada-common.util")
local style = require("coordinator.ui.style") local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local pgi = require("coordinator.ui.pgi")
local panel_view = require("coordinator.ui.layout.front_panel")
local main_view = require("coordinator.ui.layout.main_view") local main_view = require("coordinator.ui.layout.main_view")
local unit_view = require("coordinator.ui.layout.unit_view") local unit_view = require("coordinator.ui.layout.unit_view")
@@ -21,7 +25,9 @@ local engine = {
monitors = nil, ---@type monitors_struct|nil monitors = nil, ---@type monitors_struct|nil
dmesg_window = nil, ---@type table|nil dmesg_window = nil, ---@type table|nil
ui_ready = false, ui_ready = false,
fp_ready = false,
ui = { ui = {
front_panel = nil, ---@type graphics_element|nil
main_display = nil, ---@type graphics_element|nil main_display = nil, ---@type graphics_element|nil
unit_displays = {} unit_displays = {}
} }
@@ -46,24 +52,10 @@ end
---@param monitors monitors_struct ---@param monitors monitors_struct
function renderer.set_displays(monitors) function renderer.set_displays(monitors)
engine.monitors = monitors engine.monitors = monitors
end
-- check if the renderer is configured to use a given monitor peripheral -- report to front panel as connected
---@nodiscard iocontrol.fp_monitor_state(0, true)
---@param periph table peripheral for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end
---@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 _, monitor in ipairs(engine.monitors.unit_displays) do
if monitor == periph then return true end
end
end
end
return false
end end
-- init all displays in use by the renderer -- init all displays in use by the renderer
@@ -75,6 +67,17 @@ function renderer.init_displays()
for _, monitor in ipairs(engine.monitors.unit_displays) do for _, monitor in ipairs(engine.monitors.unit_displays) do
_init_display(monitor) _init_display(monitor)
end end
-- init terminal
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
-- set overridden colors
for i = 1, #style.fp.colors do
term.setPaletteColor(style.fp.colors[i].c, style.fp.colors[i].hex)
end
end end
-- check main display width -- check main display width
@@ -109,6 +112,51 @@ function renderer.init_dmesg()
log.direct_dmesg(engine.dmesg_window) log.direct_dmesg(engine.dmesg_window)
end end
-- start the coordinator front panel
function renderer.start_fp()
if not engine.fp_ready then
-- show front panel view on terminal
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root}
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
-- start flasher callback task
flasher.run()
-- report front panel as ready
engine.fp_ready = true
end
end
-- close out the front panel
function renderer.close_fp()
if engine.fp_ready then
if not engine.ui_ready then
-- stop blinking indicators
flasher.clear()
end
-- disable PGI
pgi.unlink()
-- hide to stop animation callbacks and clear root UI elements
engine.ui.front_panel.hide()
engine.ui.front_panel = nil
engine.fp_ready = false
-- restore colors
for i = 1, #style.colors do
local r, g, b = term.nativePaletteColor(style.colors[i].c)
term.setPaletteColor(style.colors[i].c, r, g, b)
end
-- reset terminal
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
end
end
-- start the coordinator GUI -- start the coordinator GUI
function renderer.start_ui() function renderer.start_ui()
if not engine.ui_ready then if not engine.ui_ready then
@@ -116,13 +164,15 @@ function renderer.start_ui()
engine.dmesg_window.setVisible(false) engine.dmesg_window.setVisible(false)
-- show main view on main monitor -- show main view on main monitor
engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root} if engine.monitors.primary ~= nil then
main_view(engine.ui.main_display) engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root}
main_view(engine.ui.main_display)
end
-- show unit views on unit displays -- show unit views on unit displays
for i = 1, #engine.monitors.unit_displays do for idx, display in pairs(engine.monitors.unit_displays) do
engine.ui.unit_displays[i] = DisplayBox{window=engine.monitors.unit_displays[i],fg_bg=style.root} engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
unit_view(engine.ui.unit_displays[i], i) unit_view(engine.ui.unit_displays[idx], idx)
end end
-- start flasher callback task -- start flasher callback task
@@ -135,12 +185,14 @@ end
-- close out the UI -- close out the UI
function renderer.close_ui() function renderer.close_ui()
-- stop blinking indicators if not engine.fp_ready then
flasher.clear() -- stop blinking indicators
flasher.clear()
end
-- delete element trees -- delete element trees
if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end
for _, display in ipairs(engine.ui.unit_displays) do display.delete() end for _, display in pairs(engine.ui.unit_displays) do display.delete() end
-- report ui as not ready -- report ui as not ready
engine.ui_ready = false engine.ui_ready = false
@@ -157,22 +209,121 @@ function renderer.close_ui()
engine.dmesg_window.redraw() engine.dmesg_window.redraw()
end end
-- is the front panel ready?
---@nodiscard
---@return boolean ready
function renderer.fp_ready() return engine.fp_ready end
-- is the UI ready? -- is the UI ready?
---@nodiscard ---@nodiscard
---@return boolean ready ---@return boolean ready
function renderer.ui_ready() return engine.ui_ready end function renderer.ui_ready() return engine.ui_ready end
-- handle a monitor peripheral being disconnected
---@param device table monitor
---@return boolean is_used if the monitor is one of the configured monitors
function renderer.handle_disconnect(device)
local is_used = false
if engine.monitors ~= nil then
if engine.monitors.primary == device then
if engine.ui.main_display ~= nil then
-- delete element tree and clear root UI elements
engine.ui.main_display.delete()
end
is_used = true
engine.monitors.primary = nil
engine.ui.main_display = nil
iocontrol.fp_monitor_state(0, false)
else
for idx, monitor in pairs(engine.monitors.unit_displays) do
if monitor == device then
if engine.ui.unit_displays[idx] ~= nil then
engine.ui.unit_displays[idx].delete()
end
is_used = true
engine.monitors.unit_displays[idx] = nil
engine.ui.unit_displays[idx] = nil
iocontrol.fp_monitor_state(idx, false)
break
end
end
end
end
return is_used
end
-- handle a monitor peripheral being reconnected
---@param name string monitor name
---@param device table monitor
---@return boolean is_used if the monitor is one of the configured monitors
function renderer.handle_reconnect(name, device)
local is_used = false
if engine.monitors ~= nil then
if engine.monitors.primary_name == name then
is_used = true
_init_display(device)
engine.monitors.primary = device
local disp_x, disp_y = engine.monitors.primary.getSize()
engine.dmesg_window.reposition(1, 1, disp_x, disp_y, engine.monitors.primary)
if engine.ui_ready and (engine.ui.main_display == nil) then
engine.dmesg_window.setVisible(false)
engine.ui.main_display = DisplayBox{window=device,fg_bg=style.root}
main_view(engine.ui.main_display)
else
engine.dmesg_window.setVisible(true)
engine.dmesg_window.redraw()
end
iocontrol.fp_monitor_state(0, true)
else
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
if monitor == name then
is_used = true
_init_display(device)
engine.monitors.unit_displays[idx] = device
if engine.ui_ready and (engine.ui.unit_displays[idx] == nil) then
engine.ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
unit_view(engine.ui.unit_displays[idx], idx)
end
iocontrol.fp_monitor_state(idx, true)
break
end
end
end
end
return is_used
end
-- handle a touch event -- handle a touch event
---@param event mouse_interaction|nil ---@param event mouse_interaction|nil
function renderer.handle_mouse(event) function renderer.handle_mouse(event)
if engine.ui_ready and event ~= nil then if event ~= nil then
if event.monitor == engine.monitors.primary_name then if engine.fp_ready and event.monitor == "terminal" then
engine.ui.main_display.handle_mouse(event) engine.ui.front_panel.handle_mouse(event)
else elseif engine.ui_ready then
for id, monitor in ipairs(engine.monitors.unit_name_map) do if event.monitor == engine.monitors.primary_name then
if event.monitor == monitor then engine.ui.main_display.handle_mouse(event)
local layout = engine.ui.unit_displays[id] ---@type graphics_element else
layout.handle_mouse(event) for id, monitor in ipairs(engine.monitors.unit_name_map) do
if event.monitor == monitor then
local layout = engine.ui.unit_displays[id] ---@type graphics_element
layout.handle_mouse(event)
break
end
end end
end end
end end

View File

@@ -1,16 +1,17 @@
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util") local util = require("scada-common.util")
local config = require("coordinator.config") local config = require("coordinator.config")
local iocontrol = require("coordinator.iocontrol")
local pocket = require("coordinator.session.pocket") local pocket = require("coordinator.session.pocket")
local apisessions = {} local apisessions = {}
local self = { local self = {
modem = nil, nic = nil,
next_id = 0, next_id = 0,
sessions = {} sessions = {}
} }
@@ -31,7 +32,7 @@ local function _api_handle_outq(session)
if msg ~= nil then if msg ~= nil then
if msg.qtype == mqueue.TYPE.PACKET then if msg.qtype == mqueue.TYPE.PACKET then
-- handle a packet to be sent -- handle a packet to be sent
self.modem.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message.raw_sendable()) self.nic.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message)
elseif msg.qtype == mqueue.TYPE.COMMAND then elseif msg.qtype == mqueue.TYPE.COMMAND then
-- handle instruction/notification -- handle instruction/notification
elseif msg.qtype == mqueue.TYPE.DATA then elseif msg.qtype == mqueue.TYPE.DATA then
@@ -58,7 +59,7 @@ local function _shutdown(session)
while session.out_queue.ready() do while session.out_queue.ready() do
local msg = session.out_queue.pop() local msg = session.out_queue.pop()
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then
self.modem.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message.raw_sendable()) self.nic.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message)
end end
end end
@@ -68,15 +69,9 @@ end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
-- initialize apisessions -- initialize apisessions
---@param modem table ---@param nic nic
function apisessions.init(modem) function apisessions.init(nic)
self.modem = modem self.nic = nic
end
-- re-link the modem
---@param modem table
function apisessions.relink_modem(modem)
self.modem = modem
end end
-- find a session by remote port -- find a session by remote port
@@ -118,6 +113,7 @@ function apisessions.establish_session(source_addr, version)
setmetatable(pkt_s, mt) setmetatable(pkt_s, mt)
iocontrol.fp_pkt_connected(id, version, source_addr)
log.debug(util.c("[API] established new session: ", pkt_s)) log.debug(util.c("[API] established new session: ", pkt_s))
self.next_id = id + 1 self.next_id = id + 1

View File

@@ -1,7 +1,9 @@
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util") local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local pocket = {} local pocket = {}
@@ -9,8 +11,6 @@ local PROTOCOL = comms.PROTOCOL
-- local CAPI_TYPE = comms.CAPI_TYPE -- local CAPI_TYPE = comms.CAPI_TYPE
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local println = util.println
-- retry time constants in ms -- retry time constants in ms
-- local INITIAL_WAIT = 1500 -- local INITIAL_WAIT = 1500
-- local RETRY_PERIOD = 1000 -- local RETRY_PERIOD = 1000
@@ -69,6 +69,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
local function _close() local function _close()
self.conn_watchdog.cancel() self.conn_watchdog.cancel()
self.connected = false self.connected = false
iocontrol.fp_pkt_disconnected(id)
end end
-- send a CAPI packet -- send a CAPI packet
@@ -140,6 +141,8 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
-- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms") -- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "PKT TT = " .. (srv_now - api_send) .. "ms") -- log.debug(log_header .. "PKT TT = " .. (srv_now - api_send) .. "ms")
iocontrol.fp_pkt_rtt(id, self.last_rtt)
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_header .. "SCADA keep alive packet length mismatch")
end end
@@ -172,7 +175,6 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
function public.close() function public.close()
_close() _close()
_send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
println("connection to pocket session " .. id .. " closed by server")
log.info(log_header .. "session closed by server") log.info(log_header .. "session closed by server")
end end
@@ -211,7 +213,6 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
-- exit if connection was closed -- exit if connection was closed
if not self.connected then if not self.connected then
println("connection to pocket session " .. id .. " closed by remote host")
log.info(log_header .. "session closed by remote host") log.info(log_header .. "session closed by remote host")
return self.connected return self.connected
end end

View File

@@ -4,8 +4,10 @@
require("/initenv").init_env() require("/initenv").init_env()
local comms = require("scada-common.comms")
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
local log = require("scada-common.log") local log = require("scada-common.log")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd") local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
@@ -20,7 +22,7 @@ local sounder = require("coordinator.sounder")
local apisessions = require("coordinator.session.apisessions") local apisessions = require("coordinator.session.apisessions")
local COORDINATOR_VERSION = "v0.16.1" local COORDINATOR_VERSION = "v0.21.0"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@@ -29,7 +31,7 @@ local log_graphics = coordinator.log_graphics
local log_sys = coordinator.log_sys local log_sys = coordinator.log_sys
local log_boot = coordinator.log_boot local log_boot = coordinator.log_boot
local log_comms = coordinator.log_comms local log_comms = coordinator.log_comms
local log_comms_connecting = coordinator.log_comms_connecting local log_crypto = coordinator.log_crypto
---------------------------------------- ----------------------------------------
-- config validation -- config validation
@@ -78,6 +80,9 @@ local function main()
-- mount connected devices -- mount connected devices
ppm.mount_all() ppm.mount_all()
-- report versions/init fp PSIL
iocontrol.init_fp(COORDINATOR_VERSION, comms.version)
-- setup monitors -- setup monitors
local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS) local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS)
if not configured or monitors == nil then if not configured or monitors == nil then
@@ -125,12 +130,19 @@ local function main()
sounder.init(speaker, config.SOUNDER_VOLUME) sounder.init(speaker, config.SOUNDER_VOLUME)
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms") log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
log_sys("annunciator alarm configured") log_sys("annunciator alarm configured")
iocontrol.fp_has_speaker(true)
end end
---------------------------------------- ----------------------------------------
-- setup communications -- setup communications
---------------------------------------- ----------------------------------------
-- message authentication init
if type(config.AUTH_KEY) == "string" then
local init_time = network.init_mac(config.AUTH_KEY)
log_crypto("HMAC init took " .. init_time .. "ms")
end
-- get the communications modem -- get the communications modem
local modem = ppm.get_wireless_modem() local modem = ppm.get_wireless_modem()
if modem == nil then if modem == nil then
@@ -140,6 +152,7 @@ local function main()
return return
else else
log_comms("wireless modem connected") log_comms("wireless modem connected")
iocontrol.fp_has_modem(true)
end end
-- create connection watchdog -- create connection watchdog
@@ -147,8 +160,9 @@ local function main()
conn_watchdog.cancel() conn_watchdog.cancel()
log.debug("startup> conn watchdog created") log.debug("startup> conn watchdog created")
-- start comms, open all channels -- create network interface then setup comms
local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.CRD_CHANNEL, config.SVR_CHANNEL, local nic = network.nic(modem)
local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, config.CRD_CHANNEL, config.SVR_CHANNEL,
config.PKT_CHANNEL, config.TRUSTED_RANGE, conn_watchdog) config.PKT_CHANNEL, config.TRUSTED_RANGE, conn_watchdog)
log.debug("startup> comms init") log.debug("startup> comms init")
log_comms("comms initialized") log_comms("comms initialized")
@@ -158,78 +172,54 @@ local function main()
local loop_clock = util.new_clock(MAIN_CLOCK) local loop_clock = util.new_clock(MAIN_CLOCK)
---------------------------------------- ----------------------------------------
-- connect to the supervisor -- start front panel & UI start function
---------------------------------------- ----------------------------------------
-- attempt to connect to the supervisor or exit log_graphics("starting front panel UI...")
local function init_connect_sv()
local tick_waiting, task_done = log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SVR_CHANNEL)
-- attempt to establish a connection with the supervisory computer local fp_ok, fp_message = pcall(renderer.start_fp)
if not coord_comms.sv_connect(60, tick_waiting, task_done) then if not fp_ok then
log_sys("supervisor connection failed, shutting down...") renderer.close_fp()
log.fatal("failed to connect to supervisor") log_graphics(util.c("front panel UI error: ", fp_message))
return false println_ts("front panel UI creation failed")
end log.fatal(util.c("front panel GUI render failed with error ", fp_message))
return true
end
if not init_connect_sv() then
println("startup> failed to connect to supervisor")
log_sys("system shutdown")
return return
else else log_graphics("front panel ready") end
log_sys("supervisor connected, proceeding to UI start")
end
---------------------------------------- -- start up the main UI
-- start the UI
----------------------------------------
-- start up the UI
---@return boolean ui_ok started ok ---@return boolean ui_ok started ok
local function init_start_ui() local function start_main_ui()
log_graphics("starting UI...") log_graphics("starting main UI...")
local draw_start = util.time_ms() local draw_start = util.time_ms()
local ui_ok, message = pcall(renderer.start_ui) local ui_ok, ui_message = pcall(renderer.start_ui)
if not ui_ok then if not ui_ok then
renderer.close_ui() renderer.close_ui()
log_graphics(util.c("UI crashed: ", message)) log_graphics(util.c("main UI error: ", ui_message))
println_ts("UI crashed") log.fatal(util.c("main GUI render failed with error ", ui_message))
log.fatal(util.c("GUI crashed with error ", message))
else else
log_graphics("first UI draw took " .. (util.time_ms() - draw_start) .. "ms") log_graphics("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
-- start clock
loop_clock.start()
end end
return ui_ok return ui_ok
end end
local ui_ok = init_start_ui()
---------------------------------------- ----------------------------------------
-- main event loop -- main event loop
---------------------------------------- ----------------------------------------
local link_failed = false
local ui_ok = true
local date_format = util.trinary(config.TIME_24_HOUR, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y") local date_format = util.trinary(config.TIME_24_HOUR, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y")
local no_modem = false -- start clock
loop_clock.start()
if ui_ok then log_sys("system started successfully")
-- start connection watchdog
conn_watchdog.feed()
log.debug("startup> conn watchdog started")
log_sys("system started successfully")
end
-- main event loop -- main event loop
while ui_ok do while true do
local event, param1, param2, param3, param4, param5 = util.pull_event() local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event -- handle event
@@ -239,33 +229,36 @@ local function main()
if type ~= nil and device ~= nil then if type ~= nil and device ~= nil then
if type == "modem" then if type == "modem" then
-- we only really care if this is our wireless modem -- we only really care if this is our wireless modem
if device == modem then -- if it is another modem, handle other peripheral losses separately
no_modem = true if nic.is_modem(device) then
nic.disconnect()
log_sys("comms modem disconnected") log_sys("comms modem disconnected")
println_ts("wireless modem disconnected!")
-- close out UI local other_modem = ppm.get_wireless_modem()
renderer.close_ui() if other_modem then
log_sys("found another wireless modem, using it for comms")
nic.connect(other_modem)
else
-- close out main UI
renderer.close_ui()
-- alert user to status -- alert user to status
log_sys("awaiting comms modem reconnect...") log_sys("awaiting comms modem reconnect...")
iocontrol.fp_has_modem(false)
end
else else
log_sys("non-comms modem disconnected") log_sys("non-comms modem disconnected")
end end
elseif type == "monitor" then elseif type == "monitor" then
if renderer.is_monitor_used(device) then if renderer.handle_disconnect(device) then
-- "halt and catch fire" style handling log_sys("lost a configured monitor")
local msg = "lost a configured monitor, system will now exit"
println_ts(msg)
log_sys(msg)
break
else else
log_sys("lost unused monitor, ignoring") log_sys("lost an unused monitor")
end end
elseif type == "speaker" then elseif type == "speaker" then
local msg = "lost alarm sounder speaker" log_sys("lost alarm sounder speaker")
println_ts(msg) iocontrol.fp_has_speaker(false)
log_sys(msg)
end end
end end
elseif event == "peripheral" then elseif event == "peripheral" then
@@ -273,34 +266,50 @@ local function main()
if type ~= nil and device ~= nil then if type ~= nil and device ~= nil then
if type == "modem" then if type == "modem" then
if device.isWireless() then if device.isWireless() and not nic.is_connected() then
-- reconnected modem -- reconnected modem
no_modem = false
modem = device
coord_comms.reconnect_modem(modem)
log_sys("comms modem reconnected") log_sys("comms modem reconnected")
println_ts("wireless modem reconnected.") nic.connect(device)
iocontrol.fp_has_modem(true)
-- re-init system elseif device.isWireless() then
if not init_connect_sv() then break end log.info("unused wireless modem reconnected")
ui_ok = init_start_ui()
else else
log_sys("wired modem reconnected") log_sys("wired modem reconnected")
end end
-- elseif type == "monitor" then elseif type == "monitor" then
-- not supported, system will exit on loss of in-use monitors if renderer.handle_reconnect(param1, device) then
log_sys(util.c("configured monitor ", param1, " reconnected"))
else
log_sys(util.c("unused monitor ", param1, " connected"))
end
elseif type == "speaker" then elseif type == "speaker" then
local msg = "alarm sounder speaker reconnected" log_sys("alarm sounder speaker reconnected")
println_ts(msg)
log_sys(msg)
sounder.reconnect(device) sounder.reconnect(device)
iocontrol.fp_has_speaker(true)
end end
end end
elseif event == "timer" then elseif event == "timer" then
if loop_clock.is_clock(param1) then if loop_clock.is_clock(param1) then
-- main loop tick -- main loop tick
-- toggle heartbeat
iocontrol.heartbeat()
-- maintain connection
if nic.is_connected() then
local ok, start_ui = coord_comms.try_connect()
if not ok then
link_failed = true
log_sys("supervisor connection failed, shutting down...")
log.fatal("failed to connect to supervisor")
break
elseif start_ui then
log_sys("supervisor connected, proceeding to main UI start")
ui_ok = start_main_ui()
if not ui_ok then break end
end
end
-- iterate sessions -- iterate sessions
apisessions.iterate_all() apisessions.iterate_all()
@@ -308,25 +317,19 @@ local function main()
apisessions.free_all_closed() apisessions.free_all_closed()
-- update date and time string for main display -- update date and time string for main display
iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format)) if coord_comms.is_linked() then
iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format))
end
loop_clock.start() loop_clock.start()
elseif conn_watchdog.is_timer(param1) then elseif conn_watchdog.is_timer(param1) then
-- supervisor watchdog timeout -- supervisor watchdog timeout
local msg = "supervisor server timeout" log_comms("supervisor server timeout")
log_comms(msg)
println_ts(msg)
-- close connection, UI, and stop sounder -- close connection, main UI, and stop sounder
coord_comms.close() coord_comms.close()
renderer.close_ui() renderer.close_ui()
sounder.stop() sounder.stop()
if not no_modem then
-- try to re-connect to the supervisor
if not init_connect_sv() then break end
ui_ok = init_start_ui()
end
else else
-- a non-clock/main watchdog timer event -- a non-clock/main watchdog timer event
@@ -339,25 +342,19 @@ local function main()
elseif event == "modem_message" then elseif event == "modem_message" then
-- got a packet -- got a packet
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5) local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
coord_comms.handle_packet(packet)
-- check if it was a disconnect -- handle then check if it was a disconnect
if not coord_comms.is_linked() then if coord_comms.handle_packet(packet) then
log_comms("supervisor closed connection") log_comms("supervisor closed connection")
-- close connection, UI, and stop sounder -- close connection, main UI, and stop sounder
coord_comms.close() coord_comms.close()
renderer.close_ui() renderer.close_ui()
sounder.stop() sounder.stop()
if not no_modem then
-- try to re-connect to the supervisor
if not init_connect_sv() then break end
ui_ok = init_start_ui()
end
end end
elseif event == "monitor_touch" then elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
-- handle a monitor touch event event == "mouse_drag" or event == "mouse_scroll" then
-- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
elseif event == "speaker_audio_empty" then elseif event == "speaker_audio_empty" then
-- handle speaker buffer emptied -- handle speaker buffer emptied
@@ -366,10 +363,17 @@ local function main()
-- check for termination request -- check for termination request
if event == "terminate" or ppm.should_terminate() then if event == "terminate" or ppm.should_terminate() then
println_ts("terminate requested, closing connections...") -- handle supervisor connection
log_comms("terminate requested, closing supervisor connection...") coord_comms.try_connect(true)
if coord_comms.is_linked() then
log_comms("terminate requested, closing supervisor connection...")
else link_failed = true end
coord_comms.close() coord_comms.close()
log_comms("supervisor connection closed") log_comms("supervisor connection closed")
-- handle API sessions
log_comms("closing api sessions...") log_comms("closing api sessions...")
apisessions.close_all() apisessions.close_all()
log_comms("api sessions closed") log_comms("api sessions closed")
@@ -378,15 +382,20 @@ local function main()
end end
renderer.close_ui() renderer.close_ui()
renderer.close_fp()
sounder.stop() sounder.stop()
log_sys("system shutdown") log_sys("system shutdown")
if link_failed then println_ts("failed to connect to supervisor") end
if not ui_ok then println_ts("main UI creation failed") end
println_ts("exited") println_ts("exited")
log.info("exited") log.info("exited")
end end
if not xpcall(main, crash.handler) then if not xpcall(main, crash.handler) then
pcall(renderer.close_ui) pcall(renderer.close_ui)
pcall(renderer.close_fp)
pcall(sounder.stop) pcall(sounder.stop)
crash.exit() crash.exit()
else else

View File

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

View File

@@ -15,8 +15,10 @@ local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light") local IndicatorLight = require("graphics.elements.indicators.light")
local RadIndicator = require("graphics.elements.indicators.rad") local RadIndicator = require("graphics.elements.indicators.rad")
local StateIndicator = require("graphics.elements.indicators.state")
local TriIndicatorLight = require("graphics.elements.indicators.trilight") local TriIndicatorLight = require("graphics.elements.indicators.trilight")
local Checkbox = require("graphics.elements.controls.checkbox")
local HazardButton = require("graphics.elements.controls.hazard_button") local HazardButton = require("graphics.elements.controls.hazard_button")
local RadioButton = require("graphics.elements.controls.radio_button") local RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric") local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
@@ -43,7 +45,7 @@ local function new_view(root, x, y)
local lu_cpair = cpair(colors.gray, colors.gray) local lu_cpair = cpair(colors.gray, colors.gray)
local dis_colors = cpair(colors.white, colors.lightGray) local dis_colors = cpair(colors.white, colors.lightGray)
local main = Div{parent=root,width=104,height=24,x=x,y=y} local main = Div{parent=root,width=128,height=24,x=x,y=y}
local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,fg_bg=hzd_fg_bg} local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,fg_bg=hzd_fg_bg} local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,fg_bg=hzd_fg_bg}
@@ -52,12 +54,14 @@ local function new_view(root, x, y)
facility.ack_alarms_ack = ack_a.on_response facility.ack_alarms_ack = ack_a.on_response
local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=cpair(colors.green,colors.red)} local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=cpair(colors.green,colors.red)}
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=cpair(colors.green,colors.gray)}
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green} local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=cpair(colors.green,colors.gray)}
local sps = IndicatorLight{parent=main,label="SPS Connected",colors=cpair(colors.green,colors.gray)}
all_ok.register(facility.ps, "all_sys_ok", all_ok.update) all_ok.register(facility.ps, "all_sys_ok", all_ok.update)
ind_mat.register(facility.induction_ps_tbl[1], "computed_status", function (status) ind_mat.update(status > 1) end)
rad_mon.register(facility.ps, "rad_computed_status", rad_mon.update) rad_mon.register(facility.ps, "rad_computed_status", rad_mon.update)
ind_mat.register(facility.induction_ps_tbl[1], "computed_status", function (status) ind_mat.update(status > 1) end)
sps.register(facility.sps_ps_tbl[1], "computed_status", function (status) sps.update(status > 1) end)
main.line_break() main.line_break()
@@ -99,7 +103,7 @@ local function new_view(root, x, y)
-- process control -- -- process control --
--------------------- ---------------------
local proc = Div{parent=main,width=78,height=24,x=27,y=1} local proc = Div{parent=main,width=103,height=24,x=27,y=1}
----------------------------- -----------------------------
-- process control targets -- -- process control targets --
@@ -148,46 +152,77 @@ local function new_view(root, x, y)
local rate_limits = {} local rate_limits = {}
for i = 1, facility.num_units do for i = 1, 4 do
local unit = units[i] ---@type ioctl_unit local unit
local tag_fg_bg = cpair(colors.gray,colors.white)
local lim_fg_bg = cpair(colors.lightGray,colors.white)
local ctl_fg = colors.lightGray
local cur_fg_bg = cpair(colors.lightGray,colors.white)
local cur_lu = colors.lightGray
if i <= facility.num_units then
unit = units[i] ---@type ioctl_unit
tag_fg_bg = cpair(colors.black,colors.lightBlue)
lim_fg_bg = bw_fg_bg
ctl_fg = colors.gray
cur_fg_bg = cpair(colors.black,colors.brown)
cur_lu = colors.black
end
local _y = ((i - 1) * 5) + 1 local _y = ((i - 1) * 5) + 1
local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=cpair(colors.black,colors.lightBlue)} local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2} TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2}
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(colors.gray,colors.white)} local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(ctl_fg,colors.white)}
rate_limits[i] = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg} local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=lim_fg_bg}
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1} TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1}
rate_limits[i].register(unit.unit_ps, "max_burn", rate_limits[i].set_max) local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(cur_lu,cur_lu),width=14,fg_bg=cur_fg_bg}
rate_limits[i].register(unit.unit_ps, "burn_limit", rate_limits[i].set_value)
local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(colors.black,colors.black),width=14,fg_bg=cpair(colors.black,colors.brown)} if i <= facility.num_units then
rate_limits[i] = lim
rate_limits[i].register(unit.unit_ps, "max_burn", rate_limits[i].set_max)
rate_limits[i].register(unit.unit_ps, "burn_limit", rate_limits[i].set_value)
cur_burn.register(unit.unit_ps, "act_burn_rate", cur_burn.update) cur_burn.register(unit.unit_ps, "act_burn_rate", cur_burn.update)
else
lim.disable()
end
end end
------------------- -------------------
-- unit statuses -- -- unit statuses --
------------------- -------------------
local stat_div = Div{parent=proc,width=38,height=19,x=57,y=6} local stat_div = Div{parent=proc,width=22,height=24,x=57,y=6}
for i = 1, facility.num_units do for i = 1, 4 do
local unit = units[i] ---@type ioctl_unit local tag_fg_bg = cpair(colors.gray,colors.white)
local ind_fg_bg = cpair(colors.lightGray,colors.white)
local ind_off = colors.lightGray
if i <= facility.num_units then
tag_fg_bg = cpair(colors.black,colors.cyan)
ind_fg_bg = bw_fg_bg
ind_off = colors.gray
end
local _y = ((i - 1) * 5) + 1 local _y = ((i - 1) * 5) + 1
local unit_tag = Div{parent=stat_div,x=1,y=_y,width=8,height=4,fg_bg=cpair(colors.black,colors.lightBlue)} local unit_tag = Div{parent=stat_div,x=1,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Status",width=7,height=2} TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Status",width=7,height=2}
local lights = Div{parent=stat_div,x=9,y=_y,width=12,height=4,fg_bg=bw_fg_bg} local lights = Div{parent=stat_div,x=9,y=_y,width=14,height=4,fg_bg=ind_fg_bg}
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(colors.green,colors.gray)} local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(colors.green,ind_off)}
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(colors.red,ind_off),flash=true,period=period.BLINK_250_MS}
ready.register(unit.unit_ps, "U_AutoReady", ready.update) if i <= facility.num_units then
degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update) local unit = units[i] ---@type ioctl_unit
ready.register(unit.unit_ps, "U_AutoReady", ready.update)
degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update)
end
end end
------------------------- -------------------------
@@ -195,7 +230,7 @@ local function new_view(root, x, y)
------------------------- -------------------------
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" } local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.purple,colors.black),radio_bg=colors.gray} local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.white,colors.black),radio_bg=colors.purple}
mode.register(facility.ps, "process_mode", mode.set_value) mode.register(facility.ps, "process_mode", mode.set_value)
@@ -261,6 +296,60 @@ local function new_view(root, x, y)
for i = 1, #rate_limits do rate_limits[i].enable() end for i = 1, #rate_limits do rate_limits[i].enable() end
end end
end) end)
------------------------------
-- waste production control --
------------------------------
local waste_status = Div{parent=proc,width=24,height=4,x=57,y=1,}
for i = 1, facility.num_units do
local unit = units[i] ---@type ioctl_unit
TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8,height=1}
local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=cpair(colors.white,colors.gray)}
local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.waste.states_abbrv,value=1,min_width=6}
a_waste.register(unit.unit_ps, "U_AutoWaste", a_waste.update)
waste_m.register(unit.unit_ps, "U_WasteProduct", waste_m.update)
end
local waste_sel = Div{parent=proc,width=21,height=24,x=81,y=1}
TextBox{parent=waste_sel,text=" ",width=21,height=1,x=1,y=1,fg_bg=cpair(colors.black,colors.brown)}
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=TEXT_ALIGN.CENTER,width=21,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.brown)}
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17}
status.register(facility.ps, "current_waste_product", status.update)
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.white,colors.black),radio_bg=colors.brown}
local pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.green,colors.black)}
waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value)
pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value)
local fb_active = IndicatorLight{parent=rect,x=2,y=9,label="Fallback Active",colors=cpair(colors.white,colors.gray)}
fb_active.register(facility.ps, "pu_fallback_active", fb_active.update)
TextBox{parent=rect,x=2,y=11,text="Plutonium Rate",height=1,width=17,fg_bg=style.label}
local pu_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
TextBox{parent=rect,x=2,y=14,text="Polonium Rate",height=1,width=17,fg_bg=style.label}
local po_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
TextBox{parent=rect,x=2,y=17,text="Antimatter Rate",height=1,width=17,fg_bg=style.label}
local am_rate = DataIndicator{parent=rect,x=2,label="",unit="\xb5B/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
pu_rate.register(facility.ps, "pu_rate", pu_rate.update)
po_rate.register(facility.ps, "po_rate", po_rate.update)
am_rate.register(facility.ps, "am_rate", am_rate.update)
local sna_count = DataIndicator{parent=rect,x=2,y=20,label="Linked SNAs:",format="%4d",value=0,lu_colors=lu_cpair,width=17}
sna_count.register(facility.ps, "sna_count", sna_count.update)
end end
return new_view return new_view

View File

@@ -33,41 +33,21 @@ local border = core.border
local period = core.flasher.PERIOD local period = core.flasher.PERIOD
local waste_opts = {
{
text = "Auto",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.white, colors.gray)
},
{
text = "Pu",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.green)
},
{
text = "Po",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.cyan)
},
{
text = "AM",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.purple)
}
}
-- create a unit view -- create a unit view
---@param parent graphics_element parent ---@param parent graphics_element parent
---@param id integer ---@param id integer
local function init(parent, id) local function init(parent, id)
local unit = iocontrol.get_db().units[id] ---@type ioctl_unit local unit = iocontrol.get_db().units[id] ---@type ioctl_unit
local f_ps = iocontrol.get_db().facility.ps local f_ps = iocontrol.get_db().facility.ps
local main = Div{parent=parent,x=1,y=1}
if unit == nil then return main end
local u_ps = unit.unit_ps local u_ps = unit.unit_ps
local b_ps = unit.boiler_ps_tbl local b_ps = unit.boiler_ps_tbl
local t_ps = unit.turbine_ps_tbl local t_ps = unit.turbine_ps_tbl
local main = Div{parent=parent,x=1,y=1}
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} TextBox{parent=main,text="Reactor Unit #" .. id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
local bw_fg_bg = cpair(colors.black, colors.white) local bw_fg_bg = cpair(colors.black, colors.white)
@@ -398,7 +378,7 @@ local function init(parent, id)
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49} local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1} local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1}
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=waste_opts,callback=unit.set_waste,min_width=6} local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.waste.unit_opts,callback=unit.set_waste,min_width=6}
waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value) waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)

View File

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

View File

@@ -9,7 +9,7 @@ local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
local imatrix = require("coordinator.ui.components.imatrix") local imatrix = require("coordinator.ui.components.imatrix")
local process_ctl = require("coordinator.ui.components.processctl") local process_ctl = require("coordinator.ui.components.process_ctl")
local unit_overview = require("coordinator.ui.components.unit_overview") local unit_overview = require("coordinator.ui.components.unit_overview")
local core = require("graphics.core") local core = require("graphics.core")

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

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

View File

@@ -10,6 +10,41 @@ local cpair = core.cpair
-- GLOBAL -- -- GLOBAL --
-- add color mappings for front panel
colors.ivory = colors.pink
colors.yellow_hc = colors.purple
colors.red_off = colors.brown
colors.yellow_off = colors.magenta
colors.green_off = colors.lime
-- front panel styling
style.fp = {}
style.fp.root = cpair(colors.black, colors.ivory)
style.fp.header = cpair(colors.black, colors.lightGray)
style.fp.colors = {
{ c = colors.red, hex = 0xdf4949 }, -- RED ON
{ c = colors.orange, hex = 0xffb659 },
{ c = colors.yellow, hex = 0xf9fb53 }, -- YELLOW ON
{ c = colors.lime, hex = 0x16665a }, -- GREEN OFF
{ c = colors.green, hex = 0x6be551 }, -- GREEN ON
{ c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff },
{ c = colors.purple, hex = 0xb156ee }, -- YELLOW HIGH CONTRAST
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
-- { c = colors.white, hex = 0xdcd9ca },
{ c = colors.lightGray, hex = 0xb1b8b3 },
{ c = colors.gray, hex = 0x575757 },
-- { c = colors.black, hex = 0x191919 },
{ c = colors.brown, hex = 0x672223 } -- RED OFF
}
-- main GUI styling
style.root = cpair(colors.black, colors.lightGray) style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray) style.header = cpair(colors.white, colors.gray)
style.label = cpair(colors.gray, colors.lightGray) style.label = cpair(colors.gray, colors.lightGray)
@@ -151,7 +186,90 @@ style.imatrix = {
{ {
color = cpair(colors.black, colors.yellow), color = cpair(colors.black, colors.yellow),
text = "HIGH CHARGE" text = "HIGH CHARGE"
}
}
}
style.sps = {
-- SPS states
states = {
{
color = cpair(colors.black, colors.yellow),
text = "OFF-LINE"
}, },
{
color = cpair(colors.black, colors.orange),
text = "NOT FORMED"
},
{
color = cpair(colors.black, colors.orange),
text = "RTU FAULT"
},
{
color = cpair(colors.black, colors.gray),
text = "IDLE"
},
{
color = cpair(colors.black, colors.green),
text = "ACTIVE"
}
}
}
style.waste = {
-- auto waste processing states
states = {
{
color = cpair(colors.black, colors.green),
text = "PLUTONIUM"
},
{
color = cpair(colors.black, colors.cyan),
text = "POLONIUM"
},
{
color = cpair(colors.black, colors.purple),
text = "ANTI MATTER"
}
},
states_abbrv = {
{
color = cpair(colors.black, colors.green),
text = "Pu"
},
{
color = cpair(colors.black, colors.cyan),
text = "Po"
},
{
color = cpair(colors.black, colors.purple),
text = "AM"
}
},
-- process radio button options
options = { "Plutonium", "Polonium", "Antimatter" },
-- unit waste selection
unit_opts = {
{
text = "Auto",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.white, colors.gray)
},
{
text = "Pu",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.green)
},
{
text = "Po",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.cyan)
},
{
text = "AM",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.purple)
}
} }
} }

View File

@@ -7,6 +7,8 @@ local flasher = require("graphics.flasher")
local core = {} local core = {}
core.version = "1.0.1"
core.flasher = flasher core.flasher = flasher
core.events = events core.events = events

View File

@@ -20,6 +20,7 @@ local element = {}
---@alias graphics_args graphics_args_generic ---@alias graphics_args graphics_args_generic
---|waiting_args ---|waiting_args
---|checkbox_args
---|hazard_button_args ---|hazard_button_args
---|multi_button_args ---|multi_button_args
---|push_button_args ---|push_button_args

View File

@@ -8,7 +8,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -8,7 +8,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new color map -- new color map

View File

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

View File

@@ -14,7 +14,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -20,7 +20,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -16,7 +16,7 @@ local CLICK_TYPE = core.events.CLICK_TYPE
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -13,7 +13,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -17,7 +17,7 @@ local CLICK_TYPE = core.events.CLICK_TYPE
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -16,7 +16,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -12,7 +12,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -18,7 +18,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -6,7 +6,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height

View File

@@ -16,7 +16,7 @@ local flasher = require("graphics.flasher")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -11,7 +11,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
-- new core map box -- new core map box
---@nodiscard ---@nodiscard

View File

@@ -14,7 +14,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width integer length ---@field width integer length
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -10,7 +10,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height

View File

@@ -16,7 +16,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -14,7 +14,7 @@ local flasher = require("graphics.flasher")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -16,7 +16,7 @@ local flasher = require("graphics.flasher")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -9,7 +9,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -14,7 +14,7 @@ local flasher = require("graphics.flasher")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -13,7 +13,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width integer length ---@field width integer length
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -14,7 +14,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width integer length ---@field width integer length
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -15,7 +15,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field height? integer 1 if omitted, must be an odd number ---@field height? integer 1 if omitted, must be an odd number
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -16,7 +16,7 @@ local flasher = require("graphics.flasher")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@@ -8,7 +8,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height

View File

@@ -15,7 +15,7 @@ local CLICK_TYPE = core.events.CLICK_TYPE
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height

View File

@@ -7,7 +7,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height

View File

@@ -11,7 +11,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
-- new pipe network -- new pipe network

View File

@@ -11,7 +11,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height

View File

@@ -13,7 +13,7 @@ local TEXT_ALIGN = core.TEXT_ALIGN
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height

View File

@@ -11,7 +11,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted ---@field width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height

View File

@@ -43,8 +43,10 @@ end
-- start/resume the flasher periodic -- start/resume the flasher periodic
function flasher.run() function flasher.run()
active = true if not active then
callback_250ms() active = true
callback_250ms()
end
end end
-- clear all blinking indicators and stop the flasher periodic -- clear all blinking indicators and stop the flasher periodic

View File

@@ -23,11 +23,11 @@ def dir_size(path):
return total return total
# get the version of an application at the provided path # get the version of an application at the provided path
def get_version(path, is_comms = False): def get_version(path, is_lib = False):
ver = "" ver = ""
string = "comms.version = \"" string = ".version = \""
if not is_comms: if not is_lib:
string = "_VERSION = \"" string = "_VERSION = \""
f = open(path, "r") f = open(path, "r")
@@ -49,6 +49,8 @@ def make_manifest(size):
"installer" : get_version("./ccmsi.lua"), "installer" : get_version("./ccmsi.lua"),
"bootloader" : get_version("./startup.lua"), "bootloader" : get_version("./startup.lua"),
"comms" : get_version("./scada-common/comms.lua", True), "comms" : get_version("./scada-common/comms.lua", True),
"graphics" : get_version("./graphics/core.lua", True),
"lockbox" : get_version("./lockbox/init.lua", True),
"reactor-plc" : get_version("./reactor-plc/startup.lua"), "reactor-plc" : get_version("./reactor-plc/startup.lua"),
"rtu" : get_version("./rtu/startup.lua"), "rtu" : get_version("./rtu/startup.lua"),
"supervisor" : get_version("./supervisor/startup.lua"), "supervisor" : get_version("./supervisor/startup.lua"),
@@ -69,11 +71,11 @@ def make_manifest(size):
"pocket" : list_files("./pocket"), "pocket" : list_files("./pocket"),
}, },
"depends" : { "depends" : {
"reactor-plc" : [ "system", "common", "graphics" ], "reactor-plc" : [ "system", "common", "graphics", "lockbox" ],
"rtu" : [ "system", "common", "graphics" ], "rtu" : [ "system", "common", "graphics", "lockbox" ],
"supervisor" : [ "system", "common" ], "supervisor" : [ "system", "common", "graphics", "lockbox" ],
"coordinator" : [ "system", "common", "graphics" ], "coordinator" : [ "system", "common", "graphics", "lockbox" ],
"pocket" : [ "system", "common", "graphics" ] "pocket" : [ "system", "common", "graphics", "lockbox" ]
}, },
"sizes" : { "sizes" : {
# manifest file estimate # manifest file estimate

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,10 @@ config.PKT_CHANNEL = 16244
config.TRUSTED_RANGE = 0 config.TRUSTED_RANGE = 0
-- time in seconds (>= 2) before assuming a remote device is no longer active -- time in seconds (>= 2) before assuming a remote device is no longer active
config.COMMS_TIMEOUT = 5 config.COMMS_TIMEOUT = 5
-- facility authentication key (do NOT use one of your passwords)
-- this enables verifying that messages are authentic
-- all devices on the same network must use the same key
-- config.AUTH_KEY = "SCADAfacility123"
-- log path -- log path
config.LOG_PATH = "/log.txt" config.LOG_PATH = "/log.txt"

View File

@@ -17,14 +17,14 @@ local pocket = {}
-- pocket coordinator + supervisor communications -- pocket coordinator + supervisor communications
---@nodiscard ---@nodiscard
---@param version string pocket version ---@param version string pocket version
---@param modem table modem device ---@param nic nic network interface device
---@param pkt_channel integer pocket comms channel ---@param pkt_channel integer pocket comms channel
---@param svr_channel integer supervisor access channel ---@param svr_channel integer supervisor access channel
---@param crd_channel integer coordinator access channel ---@param crd_channel integer coordinator access channel
---@param range integer trusted device connection range ---@param range integer trusted device connection range
---@param sv_watchdog watchdog ---@param sv_watchdog watchdog
---@param api_watchdog watchdog ---@param api_watchdog watchdog
function pocket.comms(version, modem, pkt_channel, svr_channel, crd_channel, range, sv_watchdog, api_watchdog) function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range, sv_watchdog, api_watchdog)
local self = { local self = {
sv = { sv = {
linked = false, linked = false,
@@ -47,13 +47,9 @@ function pocket.comms(version, modem, pkt_channel, svr_channel, crd_channel, ran
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure modem channels -- configure network channels
local function _conf_channels() nic.closeAll()
modem.closeAll() nic.open(pkt_channel)
modem.open(pkt_channel)
end
_conf_channels()
-- send a management packet to the supervisor -- send a management packet to the supervisor
---@param msg_type SCADA_MGMT_TYPE ---@param msg_type SCADA_MGMT_TYPE
@@ -65,7 +61,7 @@ function pocket.comms(version, modem, pkt_channel, svr_channel, crd_channel, ran
pkt.make(msg_type, msg) pkt.make(msg_type, msg)
s_pkt.make(self.sv.addr, self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) s_pkt.make(self.sv.addr, self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
modem.transmit(svr_channel, pkt_channel, s_pkt.raw_sendable()) nic.transmit(svr_channel, pkt_channel, s_pkt)
self.sv.seq_num = self.sv.seq_num + 1 self.sv.seq_num = self.sv.seq_num + 1
end end
@@ -79,7 +75,7 @@ function pocket.comms(version, modem, pkt_channel, svr_channel, crd_channel, ran
pkt.make(msg_type, msg) pkt.make(msg_type, msg)
s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
modem.transmit(crd_channel, pkt_channel, s_pkt.raw_sendable()) nic.transmit(crd_channel, pkt_channel, s_pkt)
self.api.seq_num = self.api.seq_num + 1 self.api.seq_num = self.api.seq_num + 1
end end
@@ -93,7 +89,7 @@ function pocket.comms(version, modem, pkt_channel, svr_channel, crd_channel, ran
-- pkt.make(msg_type, msg) -- pkt.make(msg_type, msg)
-- s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.COORD_API, pkt.raw_sendable()) -- s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.COORD_API, pkt.raw_sendable())
-- modem.transmit(crd_channel, pkt_channel, s_pkt.raw_sendable()) -- nic.transmit(crd_channel, pkt_channel, s_pkt)
-- self.api.seq_num = self.api.seq_num + 1 -- self.api.seq_num = self.api.seq_num + 1
-- end -- end
@@ -124,13 +120,6 @@ function pocket.comms(version, modem, pkt_channel, svr_channel, crd_channel, ran
---@class pocket_comms ---@class pocket_comms
local public = {} local public = {}
-- reconnect a newly connected modem
---@param new_modem table
function public.reconnect_modem(new_modem)
modem = new_modem
_conf_channels()
end
-- close connection to the supervisor -- close connection to the supervisor
function public.close_sv() function public.close_sv()
sv_watchdog.cancel() sv_watchdog.cancel()
@@ -189,13 +178,10 @@ function pocket.comms(version, modem, pkt_channel, svr_channel, crd_channel, ran
---@param distance integer ---@param distance integer
---@return mgmt_frame|capi_frame|nil packet ---@return mgmt_frame|capi_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance) function public.parse_packet(side, sender, reply_to, message, distance)
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
local pkt = nil local pkt = nil
local s_pkt = comms.scada_packet()
-- parse packet as generic SCADA packet if s_pkt then
s_pkt.receive(side, sender, reply_to, message, distance)
if s_pkt.is_valid() then
-- get as SCADA management packet -- get as SCADA management packet
if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet() local mgmt_pkt = comms.mgmt_packet()

View File

@@ -6,6 +6,7 @@ require("/initenv").init_env()
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
local log = require("scada-common.log") local log = require("scada-common.log")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd") local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
@@ -17,7 +18,7 @@ local coreio = require("pocket.coreio")
local pocket = require("pocket.pocket") local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer") local renderer = require("pocket.renderer")
local POCKET_VERSION = "alpha-v0.4.5" local POCKET_VERSION = "alpha-v0.5.2"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@@ -67,6 +68,11 @@ local function main()
-- setup communications & clocks -- setup communications & clocks
---------------------------------------- ----------------------------------------
-- message authentication init
if type(config.AUTH_KEY) == "string" then
network.init_mac(config.AUTH_KEY)
end
coreio.report_link_state(coreio.LINK_STATE.UNLINKED) coreio.report_link_state(coreio.LINK_STATE.UNLINKED)
-- get the communications modem -- get the communications modem
@@ -88,8 +94,9 @@ local function main()
log.debug("startup> conn watchdogs created") log.debug("startup> conn watchdogs created")
-- start comms, open all channels -- create network interface then setup comms
local pocket_comms = pocket.comms(POCKET_VERSION, modem, config.PKT_CHANNEL, config.SVR_CHANNEL, local nic = network.nic(modem)
local pocket_comms = pocket.comms(POCKET_VERSION, nic, config.PKT_CHANNEL, config.SVR_CHANNEL,
config.CRD_CHANNEL, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api) config.CRD_CHANNEL, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api)
log.debug("startup> comms init") log.debug("startup> comms init")
@@ -105,7 +112,7 @@ local function main()
if not ui_ok then if not ui_ok then
renderer.close_ui() renderer.close_ui()
println(util.c("UI error: ", message)) println(util.c("UI error: ", message))
log.error(util.c("startup> GUI crashed with error ", message)) log.error(util.c("startup> GUI render failed with error ", message))
else else
-- start clock -- start clock
loop_clock.start() loop_clock.start()

View File

@@ -17,6 +17,10 @@ config.PLC_CHANNEL = 16241
config.TRUSTED_RANGE = 0 config.TRUSTED_RANGE = 0
-- time in seconds (>= 2) before assuming a remote device is no longer active -- time in seconds (>= 2) before assuming a remote device is no longer active
config.COMMS_TIMEOUT = 5 config.COMMS_TIMEOUT = 5
-- facility authentication key (do NOT use one of your passwords)
-- this enables verifying that messages are authentic
-- all devices on the same network must use the same key
-- config.AUTH_KEY = "SCADAfacility123"
-- log path -- log path
config.LOG_PATH = "/log.txt" config.LOG_PATH = "/log.txt"

View File

@@ -1,5 +1,5 @@
-- --
-- Main SCADA Coordinator GUI -- Reactor PLC Front Panel GUI
-- --
local types = require("scada-common.types") local types = require("scada-common.types")
@@ -28,7 +28,7 @@ local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
-- create new main view -- create new front panel view
---@param panel graphics_element main displaybox ---@param panel graphics_element main displaybox
local function init(panel) local function init(panel)
local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}

View File

@@ -12,6 +12,7 @@ local cpair = core.cpair
-- remap global colors -- remap global colors
colors.ivory = colors.pink colors.ivory = colors.pink
colors.yellow_hc = colors.purple
colors.red_off = colors.brown colors.red_off = colors.brown
colors.yellow_off = colors.magenta colors.yellow_off = colors.magenta
colors.green_off = colors.lime colors.green_off = colors.lime
@@ -28,7 +29,7 @@ style.colors = {
{ c = colors.cyan, hex = 0x34bac8 }, { c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 }, { c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff }, { c = colors.blue, hex = 0x0096ff },
{ c = colors.purple, hex = 0xb156ee }, { c = colors.purple, hex = 0xb156ee }, -- YELLOW HIGH CONTRAST
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY { c = colors.pink, hex = 0xdcd9ca }, -- IVORY
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF { c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
-- { c = colors.white, hex = 0xdcd9ca }, -- { c = colors.white, hex = 0xdcd9ca },

View File

@@ -445,14 +445,14 @@ end
---@nodiscard ---@nodiscard
---@param id integer reactor ID ---@param id integer reactor ID
---@param version string PLC version ---@param version string PLC version
---@param modem table modem device ---@param nic nic network interface device
---@param plc_channel integer PLC comms channel ---@param plc_channel integer PLC comms channel
---@param svr_channel integer supervisor server channel ---@param svr_channel integer supervisor server channel
---@param range integer trusted device connection range ---@param range integer trusted device connection range
---@param reactor table reactor device ---@param reactor table reactor device
---@param rps rps RPS reference ---@param rps rps RPS reference
---@param conn_watchdog watchdog watchdog reference ---@param conn_watchdog watchdog watchdog reference
function plc.comms(id, version, modem, plc_channel, svr_channel, range, reactor, rps, conn_watchdog) function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, rps, conn_watchdog)
local self = { local self = {
sv_addr = comms.BROADCAST, sv_addr = comms.BROADCAST,
seq_num = 0, seq_num = 0,
@@ -470,13 +470,9 @@ function plc.comms(id, version, modem, plc_channel, svr_channel, range, reactor,
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure modem channels -- configure network channels
local function _conf_channels() nic.closeAll()
modem.closeAll() nic.open(plc_channel)
modem.open(plc_channel)
end
_conf_channels()
-- send an RPLC packet -- send an RPLC packet
---@param msg_type RPLC_TYPE ---@param msg_type RPLC_TYPE
@@ -488,7 +484,7 @@ function plc.comms(id, version, modem, plc_channel, svr_channel, range, reactor,
r_pkt.make(id, msg_type, msg) r_pkt.make(id, msg_type, msg)
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable()) s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable())
modem.transmit(svr_channel, plc_channel, s_pkt.raw_sendable()) nic.transmit(svr_channel, plc_channel, s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
end end
@@ -502,7 +498,7 @@ function plc.comms(id, version, modem, plc_channel, svr_channel, range, reactor,
m_pkt.make(msg_type, msg) m_pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
modem.transmit(svr_channel, plc_channel, s_pkt.raw_sendable()) nic.transmit(svr_channel, plc_channel, s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
end end
@@ -639,7 +635,7 @@ function plc.comms(id, version, modem, plc_channel, svr_channel, range, reactor,
parallel.waitForAll(table.unpack(tasks)) parallel.waitForAll(table.unpack(tasks))
if not reactor.__p_is_faulted() then if reactor.__p_is_ok() then
_send(RPLC_TYPE.MEK_STRUCT, mek_data) _send(RPLC_TYPE.MEK_STRUCT, mek_data)
self.resend_build = false self.resend_build = false
end end
@@ -650,13 +646,6 @@ function plc.comms(id, version, modem, plc_channel, svr_channel, range, reactor,
---@class plc_comms ---@class plc_comms
local public = {} local public = {}
-- reconnect a newly connected modem
---@param new_modem table
function public.reconnect_modem(new_modem)
modem = new_modem
_conf_channels()
end
-- reconnect a newly connected reactor -- reconnect a newly connected reactor
---@param new_reactor table ---@param new_reactor table
function public.reconnect_reactor(new_reactor) function public.reconnect_reactor(new_reactor)
@@ -743,13 +732,10 @@ function plc.comms(id, version, modem, plc_channel, svr_channel, range, reactor,
---@param distance integer ---@param distance integer
---@return rplc_frame|mgmt_frame|nil packet ---@return rplc_frame|mgmt_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance) function public.parse_packet(side, sender, reply_to, message, distance)
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
local pkt = nil local pkt = nil
local s_pkt = comms.scada_packet()
-- parse packet as generic SCADA packet if s_pkt then
s_pkt.receive(side, sender, reply_to, message, distance)
if s_pkt.is_valid() then
-- get as RPLC packet -- get as RPLC packet
if s_pkt.protocol() == PROTOCOL.RPLC then if s_pkt.protocol() == PROTOCOL.RPLC then
local rplc_pkt = comms.rplc_packet() local rplc_pkt = comms.rplc_packet()
@@ -836,7 +822,7 @@ function plc.comms(id, version, modem, plc_channel, svr_channel, range, reactor,
success = true success = true
else else
reactor.setBurnRate(burn_rate) reactor.setBurnRate(burn_rate)
success = not reactor.__p_is_faulted() success = reactor.__p_is_ok()
end end
else else
log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate) log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate)
@@ -943,47 +929,7 @@ function plc.comms(id, version, modem, plc_channel, svr_channel, range, reactor,
---@cast packet mgmt_frame ---@cast packet mgmt_frame
-- if linked, only accept packets from configured supervisor -- if linked, only accept packets from configured supervisor
if self.linked then if self.linked then
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- link request confirmation
if packet.length == 1 then
log.debug("received unsolicited establish response")
local est_ack = packet.data[1]
if est_ack == ESTABLISH_ACK.ALLOW then
self.status_cache = nil
_send_struct()
public.send_status(plc_state.no_reactor, plc_state.reactor_formed)
log.debug("re-sent initial status data due to re-establish")
else
if est_ack == ESTABLISH_ACK.DENY then
println_ts("received unsolicited link denial, unlinking")
log.warning("unsolicited establish request denied")
elseif est_ack == ESTABLISH_ACK.COLLISION then
println_ts("received unsolicited link collision, unlinking")
log.warning("unsolicited establish request collision")
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
println_ts("received unsolicited link version mismatch, unlinking")
log.warning("unsolicited establish request version mismatch")
else
println_ts("invalid unsolicited link response")
log.debug("unsolicited unknown establish request response")
end
-- unlink
self.sv_addr = comms.BROADCAST
self.linked = false
end
-- clear this since this is for something that was unsolicited
self.last_est_ack = ESTABLISH_ACK.ALLOW
-- report link state
databus.tx_link_state(est_ack + 1)
else
log.debug("SCADA_MGMT establish packet length mismatch")
end
elseif packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back -- keep alive request received, echo back
if packet.length == 1 and type(packet.data[1]) == "number" then if packet.length == 1 and type(packet.data[1]) == "number" then
local timestamp = packet.data[1] local timestamp = packet.data[1]

View File

@@ -8,6 +8,7 @@ local comms = require("scada-common.comms")
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio") local rsio = require("scada-common.rsio")
local util = require("scada-common.util") local util = require("scada-common.util")
@@ -18,7 +19,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer") local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads") local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "v1.4.6" local R_PLC_VERSION = "v1.5.5"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@@ -79,6 +80,11 @@ local function main()
-- mount connected devices -- mount connected devices
ppm.mount_all() ppm.mount_all()
-- message authentication init
if type(config.AUTH_KEY) == "string" then
network.init_mac(config.AUTH_KEY)
end
-- shared memory across threads -- shared memory across threads
---@class plc_shared_memory ---@class plc_shared_memory
local __shared_memory = { local __shared_memory = {
@@ -88,13 +94,13 @@ local function main()
-- PLC system state flags -- PLC system state flags
---@class plc_state ---@class plc_state
plc_state = { plc_state = {
init_ok = true, init_ok = true,
fp_ok = false, fp_ok = false,
shutdown = false, shutdown = false,
degraded = false, degraded = true,
reactor_formed = true, reactor_formed = true,
no_reactor = false, no_reactor = true,
no_modem = false no_modem = true
}, },
-- control setpoints -- control setpoints
@@ -113,6 +119,7 @@ local function main()
-- system objects -- system objects
plc_sys = { plc_sys = {
rps = nil, ---@type rps rps = nil, ---@type rps
nic = nil, ---@type nic
plc_comms = nil, ---@type plc_comms plc_comms = nil, ---@type plc_comms
conn_watchdog = nil ---@type watchdog conn_watchdog = nil ---@type watchdog
}, },
@@ -130,14 +137,17 @@ local function main()
local plc_state = __shared_memory.plc_state local plc_state = __shared_memory.plc_state
-- initial state evaluation
plc_state.no_reactor = smem_dev.reactor == nil
plc_state.no_modem = smem_dev.modem == nil
-- we need a reactor, can at least do some things even if it isn't formed though -- we need a reactor, can at least do some things even if it isn't formed though
if smem_dev.reactor == nil then if plc_state.no_reactor then
println("init> fission reactor not found"); println("init> fission reactor not found");
log.warning("init> no reactor on startup") log.warning("init> no reactor on startup")
plc_state.init_ok = false plc_state.init_ok = false
plc_state.degraded = true plc_state.degraded = true
plc_state.no_reactor = true
elseif not smem_dev.reactor.isFormed() then elseif not smem_dev.reactor.isFormed() then
println("init> fission reactor not formed"); println("init> fission reactor not formed");
log.warning("init> reactor logic adapter present, but reactor is not formed") log.warning("init> reactor logic adapter present, but reactor is not formed")
@@ -147,7 +157,7 @@ local function main()
end end
-- modem is required if networked -- modem is required if networked
if __shared_memory.networked and smem_dev.modem == nil then if __shared_memory.networked and plc_state.no_modem then
println("init> wireless modem not found") println("init> wireless modem not found")
log.warning("init> no wireless modem on startup") log.warning("init> no wireless modem on startup")
@@ -158,7 +168,6 @@ local function main()
plc_state.init_ok = false plc_state.init_ok = false
plc_state.degraded = true plc_state.degraded = true
plc_state.no_modem = true
end end
-- print a log message to the terminal as long as the UI isn't running -- print a log message to the terminal as long as the UI isn't running
@@ -181,7 +190,7 @@ local function main()
renderer.close_ui() renderer.close_ui()
println_ts(util.c("UI error: ", message)) println_ts(util.c("UI error: ", message))
println("init> running without front panel") println("init> running without front panel")
log.error(util.c("GUI crashed with error ", message)) log.error(util.c("front panel GUI render failed with error ", message))
log.info("init> running in headless mode without front panel") log.info("init> running in headless mode without front panel")
end end
end end
@@ -196,8 +205,9 @@ local function main()
smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT) smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT)
log.debug("init> conn watchdog started") log.debug("init> conn watchdog started")
-- start comms -- create network interface then setup comms
smem_sys.plc_comms = plc.comms(config.REACTOR_ID, R_PLC_VERSION, smem_dev.modem, config.PLC_CHANNEL, config.SVR_CHANNEL, smem_sys.nic = network.nic(smem_dev.modem)
smem_sys.plc_comms = plc.comms(config.REACTOR_ID, R_PLC_VERSION, smem_sys.nic, config.PLC_CHANNEL, config.SVR_CHANNEL,
config.TRUSTED_RANGE, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog) config.TRUSTED_RANGE, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
log.debug("init> comms init") log.debug("init> comms init")
else else

View File

@@ -59,6 +59,7 @@ function threads.thread__main(smem, init)
while true do while true do
-- get plc_sys fields (may have been set late due to degraded boot) -- get plc_sys fields (may have been set late due to degraded boot)
local rps = smem.plc_sys.rps local rps = smem.plc_sys.rps
local nic = smem.plc_sys.nic
local plc_comms = smem.plc_sys.plc_comms local plc_comms = smem.plc_sys.plc_comms
local conn_watchdog = smem.plc_sys.conn_watchdog local conn_watchdog = smem.plc_sys.conn_watchdog
@@ -66,6 +67,7 @@ function threads.thread__main(smem, init)
-- handle event -- handle event
if event == "timer" and loop_clock.is_clock(param1) then if event == "timer" and loop_clock.is_clock(param1) then
-- note: loop clock is only running if init_ok = true
-- blink heartbeat indicator -- blink heartbeat indicator
databus.heartbeat() databus.heartbeat()
@@ -75,7 +77,7 @@ function threads.thread__main(smem, init)
loop_clock.start() loop_clock.start()
-- send updated data -- send updated data
if not plc_state.no_modem then if nic.is_connected() then
if plc_comms.is_linked() then if plc_comms.is_linked() then
smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS) smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS)
else else
@@ -114,7 +116,7 @@ function threads.thread__main(smem, init)
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM) smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
-- determine if we are still in a degraded state -- determine if we are still in a degraded state
if not networked or not plc_state.no_modem then if (not networked) or nic.is_connected() then
plc_state.degraded = false plc_state.degraded = false
end end
@@ -144,7 +146,7 @@ function threads.thread__main(smem, init)
-- update indicators -- update indicators
databus.tx_hw_status(plc_state) databus.tx_hw_status(plc_state)
elseif event == "modem_message" and networked and plc_state.init_ok and not plc_state.no_modem then elseif event == "modem_message" and networked and plc_state.init_ok and nic.is_connected() then
-- got a packet -- got a packet
local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5) local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5)
if packet ~= nil then if packet ~= nil then
@@ -171,18 +173,25 @@ function threads.thread__main(smem, init)
plc_state.degraded = true plc_state.degraded = true
elseif networked and type == "modem" then elseif networked and type == "modem" then
-- we only care if this is our wireless modem -- we only care if this is our wireless modem
if device == plc_dev.modem then if nic.is_modem(device) then
nic.disconnect()
println_ts("comms modem disconnected!") println_ts("comms modem disconnected!")
log.error("comms modem disconnected") log.warning("comms modem disconnected")
plc_state.no_modem = true local other_modem = ppm.get_wireless_modem()
if other_modem then
log.info("found another wireless modem, using it for comms")
nic.connect(other_modem)
else
plc_state.no_modem = true
plc_state.degraded = true
if plc_state.init_ok then if plc_state.init_ok then
-- try to scram reactor if it is still connected -- try to scram reactor if it is still connected
smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM) smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
end
end end
plc_state.degraded = true
else else
log.warning("non-comms modem disconnected") log.warning("non-comms modem disconnected")
end end
@@ -199,12 +208,11 @@ function threads.thread__main(smem, init)
if type == "fissionReactorLogicAdapter" then if type == "fissionReactorLogicAdapter" then
-- reconnected reactor -- reconnected reactor
plc_dev.reactor = device plc_dev.reactor = device
plc_state.no_reactor = false
println_ts("reactor reconnected.") println_ts("reactor reconnected.")
log.info("reactor reconnected") log.info("reactor reconnected")
plc_state.no_reactor = false
-- we need to assume formed here as we cannot check in this main loop -- we need to assume formed here as we cannot check in this main loop
-- RPS will identify if it isn't and this will get set false later -- RPS will identify if it isn't and this will get set false later
plc_state.reactor_formed = true plc_state.reactor_formed = true
@@ -227,22 +235,22 @@ function threads.thread__main(smem, init)
rps.reset() rps.reset()
end end
elseif networked and type == "modem" then elseif networked and type == "modem" then
if device.isWireless() then if device.isWireless() and not nic.is_connected() then
-- reconnected modem -- reconnected modem
plc_dev.modem = device plc_dev.modem = device
plc_state.no_modem = false
if plc_state.init_ok then if plc_state.init_ok then nic.connect(device) end
plc_comms.reconnect_modem(plc_dev.modem)
end
println_ts("wireless modem reconnected.") println_ts("wireless modem reconnected.")
log.info("comms modem reconnected") log.info("comms modem reconnected")
plc_state.no_modem = false
-- determine if we are still in a degraded state -- determine if we are still in a degraded state
if not plc_state.no_reactor then if not plc_state.no_reactor then
plc_state.degraded = false plc_state.degraded = false
end end
elseif device.isWireless() then
log.info("unused wireless modem reconnected")
else else
log.info("wired modem reconnected") log.info("wired modem reconnected")
end end
@@ -709,9 +717,7 @@ function threads.thread__setpoint_control(smem)
end end
-- if ramping completed or was aborted, reset last burn setpoint so that if it is requested again it will be re-attempted -- if ramping completed or was aborted, reset last burn setpoint so that if it is requested again it will be re-attempted
if not setpoints.burn_rate_en then if not setpoints.burn_rate_en then last_burn_sp = 0 end
last_burn_sp = 0
end
end end
-- check for termination request -- check for termination request

View File

@@ -10,6 +10,10 @@ config.RTU_CHANNEL = 16242
config.TRUSTED_RANGE = 0 config.TRUSTED_RANGE = 0
-- time in seconds (>= 2) before assuming a remote device is no longer active -- time in seconds (>= 2) before assuming a remote device is no longer active
config.COMMS_TIMEOUT = 5 config.COMMS_TIMEOUT = 5
-- facility authentication key (do NOT use one of your passwords)
-- this enables verifying that messages are authentic
-- all devices on the same network must use the same key
-- config.AUTH_KEY = "SCADAfacility123"
-- log path -- log path
config.LOG_PATH = "/log.txt" config.LOG_PATH = "/log.txt"

View File

@@ -2,7 +2,7 @@ local rtu = require("rtu.rtu")
local boilerv_rtu = {} local boilerv_rtu = {}
-- create new boiler (mek 10.1+) device -- create new boiler device
---@nodiscard ---@nodiscard
---@param boiler table ---@param boiler table
---@return rtu_device interface, boolean faulted ---@return rtu_device interface, boolean faulted
@@ -10,6 +10,7 @@ function boilerv_rtu.new(boiler)
local unit = rtu.init_unit() local unit = rtu.init_unit()
-- disable auto fault clearing -- disable auto fault clearing
boiler.__p_clear_fault()
boiler.__p_disable_afc() boiler.__p_disable_afc()
-- discrete inputs -- -- discrete inputs --

48
rtu/dev/dynamicv_rtu.lua Normal file
View File

@@ -0,0 +1,48 @@
local rtu = require("rtu.rtu")
local dynamicv_rtu = {}
-- create new dynamic tank device
---@nodiscard
---@param dynamic_tank table
---@return rtu_device interface, boolean faulted
function dynamicv_rtu.new(dynamic_tank)
local unit = rtu.init_unit()
-- disable auto fault clearing
dynamic_tank.__p_clear_fault()
dynamic_tank.__p_disable_afc()
-- discrete inputs --
unit.connect_di(dynamic_tank.isFormed)
-- coils --
unit.connect_coil(function () dynamic_tank.incrementContainerEditMode() end, function () end)
unit.connect_coil(function () dynamic_tank.decrementContainerEditMode() end, function () end)
-- input registers --
-- multiblock properties
unit.connect_input_reg(dynamic_tank.getLength)
unit.connect_input_reg(dynamic_tank.getWidth)
unit.connect_input_reg(dynamic_tank.getHeight)
unit.connect_input_reg(dynamic_tank.getMinPos)
unit.connect_input_reg(dynamic_tank.getMaxPos)
-- build properties
unit.connect_input_reg(dynamic_tank.getTankCapacity)
unit.connect_input_reg(dynamic_tank.getChemicalTankCapacity)
-- tanks/containers
unit.connect_input_reg(dynamic_tank.getStored)
unit.connect_input_reg(dynamic_tank.getFilledPercentage)
-- holding registers --
unit.connect_holding_reg(dynamic_tank.getContainerEditMode, dynamic_tank.setContainerEditMode)
-- check if any calls faulted
local faulted = dynamic_tank.__p_is_faulted()
dynamic_tank.__p_clear_fault()
dynamic_tank.__p_enable_afc()
return unit.interface(), faulted
end
return dynamicv_rtu

View File

@@ -10,6 +10,7 @@ function envd_rtu.new(envd)
local unit = rtu.init_unit() local unit = rtu.init_unit()
-- disable auto fault clearing -- disable auto fault clearing
envd.__p_clear_fault()
envd.__p_disable_afc() envd.__p_disable_afc()
-- discrete inputs -- -- discrete inputs --

View File

@@ -10,6 +10,7 @@ function imatrix_rtu.new(imatrix)
local unit = rtu.init_unit() local unit = rtu.init_unit()
-- disable auto fault clearing -- disable auto fault clearing
imatrix.__p_clear_fault()
imatrix.__p_disable_afc() imatrix.__p_disable_afc()
-- discrete inputs -- -- discrete inputs --

View File

@@ -10,6 +10,7 @@ function sna_rtu.new(sna)
local unit = rtu.init_unit() local unit = rtu.init_unit()
-- disable auto fault clearing -- disable auto fault clearing
sna.__p_clear_fault()
sna.__p_disable_afc() sna.__p_disable_afc()
-- discrete inputs -- -- discrete inputs --

View File

@@ -10,6 +10,7 @@ function sps_rtu.new(sps)
local unit = rtu.init_unit() local unit = rtu.init_unit()
-- disable auto fault clearing -- disable auto fault clearing
sps.__p_clear_fault()
sps.__p_disable_afc() sps.__p_disable_afc()
-- discrete inputs -- -- discrete inputs --

View File

@@ -2,7 +2,7 @@ local rtu = require("rtu.rtu")
local turbinev_rtu = {} local turbinev_rtu = {}
-- create new turbine (mek 10.1+) device -- create new turbine device
---@nodiscard ---@nodiscard
---@param turbine table ---@param turbine table
---@return rtu_device interface, boolean faulted ---@return rtu_device interface, boolean faulted
@@ -10,6 +10,7 @@ function turbinev_rtu.new(turbine)
local unit = rtu.init_unit() local unit = rtu.init_unit()
-- disable auto fault clearing -- disable auto fault clearing
turbine.__p_clear_fault()
turbine.__p_disable_afc() turbine.__p_disable_afc()
-- discrete inputs -- -- discrete inputs --

View File

@@ -1,5 +1,5 @@
-- --
-- Main SCADA Coordinator GUI -- RTU Front Panel GUI
-- --
local types = require("scada-common.types") local types = require("scada-common.types")
@@ -26,6 +26,7 @@ local UNIT_TYPE_LABELS = {
"REDSTONE", "REDSTONE",
"BOILER", "BOILER",
"TURBINE", "TURBINE",
"DYNAMIC TANK",
"IND MATRIX", "IND MATRIX",
"SPS", "SPS",
"SNA", "SNA",
@@ -33,7 +34,7 @@ local UNIT_TYPE_LABELS = {
} }
-- create new main view -- create new front panel view
---@param panel graphics_element main displaybox ---@param panel graphics_element main displaybox
---@param units table unit list ---@param units table unit list
local function init(panel, units) local function init(panel, units)

View File

@@ -12,6 +12,7 @@ local cpair = core.cpair
-- remap global colors -- remap global colors
colors.ivory = colors.pink colors.ivory = colors.pink
colors.yellow_hc = colors.purple
colors.red_off = colors.brown colors.red_off = colors.brown
colors.yellow_off = colors.magenta colors.yellow_off = colors.magenta
colors.green_off = colors.lime colors.green_off = colors.lime
@@ -28,7 +29,7 @@ style.colors = {
{ c = colors.cyan, hex = 0x34bac8 }, { c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 }, { c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff }, { c = colors.blue, hex = 0x0096ff },
{ c = colors.purple, hex = 0xb156ee }, { c = colors.purple, hex = 0xb156ee }, -- YELLOW HIGH CONTRAST
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY { c = colors.pink, hex = 0xdcd9ca }, -- IVORY
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF { c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
-- { c = colors.white, hex = 0xdcd9ca }, -- { c = colors.white, hex = 0xdcd9ca },

View File

@@ -158,12 +158,12 @@ end
-- RTU Communications -- RTU Communications
---@nodiscard ---@nodiscard
---@param version string RTU version ---@param version string RTU version
---@param modem table modem device ---@param nic nic network interface device
---@param rtu_channel integer PLC comms channel ---@param rtu_channel integer PLC comms channel
---@param svr_channel integer supervisor server channel ---@param svr_channel integer supervisor server channel
---@param range integer trusted device connection range ---@param range integer trusted device connection range
---@param conn_watchdog watchdog watchdog reference ---@param conn_watchdog watchdog watchdog reference
function rtu.comms(version, modem, rtu_channel, svr_channel, range, conn_watchdog) function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
local self = { local self = {
sv_addr = comms.BROADCAST, sv_addr = comms.BROADCAST,
seq_num = 0, seq_num = 0,
@@ -179,12 +179,8 @@ function rtu.comms(version, modem, rtu_channel, svr_channel, range, conn_watchdo
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure modem channels -- configure modem channels
local function _conf_channels() nic.closeAll()
modem.closeAll() nic.open(rtu_channel)
modem.open(rtu_channel)
end
_conf_channels()
-- send a scada management packet -- send a scada management packet
---@param msg_type SCADA_MGMT_TYPE ---@param msg_type SCADA_MGMT_TYPE
@@ -196,7 +192,7 @@ function rtu.comms(version, modem, rtu_channel, svr_channel, range, conn_watchdo
m_pkt.make(msg_type, msg) m_pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
modem.transmit(svr_channel, rtu_channel, s_pkt.raw_sendable()) nic.transmit(svr_channel, rtu_channel, s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
end end
@@ -240,17 +236,10 @@ function rtu.comms(version, modem, rtu_channel, svr_channel, range, conn_watchdo
function public.send_modbus(m_pkt) function public.send_modbus(m_pkt)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
modem.transmit(svr_channel, rtu_channel, s_pkt.raw_sendable()) nic.transmit(svr_channel, rtu_channel, s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
end end
-- reconnect a newly connected modem
---@param new_modem table
function public.reconnect_modem(new_modem)
modem = new_modem
_conf_channels()
end
-- unlink from the server -- unlink from the server
---@param rtu_state rtu_state ---@param rtu_state rtu_state
function public.unlink(rtu_state) function public.unlink(rtu_state)
@@ -295,13 +284,10 @@ function rtu.comms(version, modem, rtu_channel, svr_channel, range, conn_watchdo
---@param distance integer ---@param distance integer
---@return modbus_frame|mgmt_frame|nil packet ---@return modbus_frame|mgmt_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance) function public.parse_packet(side, sender, reply_to, message, distance)
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
local pkt = nil local pkt = nil
local s_pkt = comms.scada_packet()
-- parse packet as generic SCADA packet if s_pkt then
s_pkt.receive(side, sender, reply_to, message, distance)
if s_pkt.is_valid() then
-- get as MODBUS TCP packet -- get as MODBUS TCP packet
if s_pkt.protocol() == PROTOCOL.MODBUS_TCP then if s_pkt.protocol() == PROTOCOL.MODBUS_TCP then
local m_pkt = comms.modbus_packet() local m_pkt = comms.modbus_packet()

View File

@@ -8,6 +8,7 @@ local comms = require("scada-common.comms")
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio") local rsio = require("scada-common.rsio")
local types = require("scada-common.types") local types = require("scada-common.types")
@@ -21,6 +22,7 @@ local rtu = require("rtu.rtu")
local threads = require("rtu.threads") local threads = require("rtu.threads")
local boilerv_rtu = require("rtu.dev.boilerv_rtu") local boilerv_rtu = require("rtu.dev.boilerv_rtu")
local dynamicv_rtu = require("rtu.dev.dynamicv_rtu")
local envd_rtu = require("rtu.dev.envd_rtu") local envd_rtu = require("rtu.dev.envd_rtu")
local imatrix_rtu = require("rtu.dev.imatrix_rtu") local imatrix_rtu = require("rtu.dev.imatrix_rtu")
local redstone_rtu = require("rtu.dev.redstone_rtu") local redstone_rtu = require("rtu.dev.redstone_rtu")
@@ -28,7 +30,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu") local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "v1.3.6" local RTU_VERSION = "v1.5.4"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
@@ -81,6 +83,19 @@ local function main()
-- mount connected devices -- mount connected devices
ppm.mount_all() ppm.mount_all()
-- message authentication init
if type(config.AUTH_KEY) == "string" then
network.init_mac(config.AUTH_KEY)
end
-- get modem
local modem = ppm.get_wireless_modem()
if modem == nil then
println("boot> wireless modem not found")
log.fatal("no wireless modem on startup")
return
end
---@class rtu_shared_memory ---@class rtu_shared_memory
local __shared_memory = { local __shared_memory = {
-- RTU system state flags -- RTU system state flags
@@ -91,16 +106,12 @@ local function main()
shutdown = false shutdown = false
}, },
-- core RTU devices
rtu_dev = {
modem = ppm.get_wireless_modem()
},
-- system objects -- system objects
rtu_sys = { rtu_sys = {
nic = network.nic(modem),
rtu_comms = nil, ---@type rtu_comms rtu_comms = nil, ---@type rtu_comms
conn_watchdog = nil, ---@type watchdog conn_watchdog = nil, ---@type watchdog
units = {} ---@type table units = {}
}, },
-- message queues -- message queues
@@ -109,16 +120,8 @@ local function main()
} }
} }
local smem_dev = __shared_memory.rtu_dev
local smem_sys = __shared_memory.rtu_sys local smem_sys = __shared_memory.rtu_sys
-- get modem
if smem_dev.modem == nil then
println("boot> wireless modem not found")
log.fatal("no wireless modem on startup")
return
end
databus.tx_hw_modem(true) databus.tx_hw_modem(true)
---------------------------------------- ----------------------------------------
@@ -236,18 +239,19 @@ local function main()
---@class rtu_unit_registry_entry ---@class rtu_unit_registry_entry
local unit = { local unit = {
uid = 0, ---@type integer uid = 0, ---@type integer
name = "redstone_io", ---@type string name = "redstone_io", ---@type string
type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE
index = entry_idx, ---@type integer index = entry_idx, ---@type integer
reactor = io_reactor, ---@type integer reactor = io_reactor, ---@type integer
device = capabilities, ---@type table use device field for redstone ports device = capabilities, ---@type table use device field for redstone ports
is_multiblock = false, ---@type boolean is_multiblock = false, ---@type boolean
formed = nil, ---@type boolean|nil formed = nil, ---@type boolean|nil
rtu = rs_rtu, ---@type rtu_device|rtu_rs_device hw_state = RTU_UNIT_HW_STATE.OK, ---@type RTU_UNIT_HW_STATE
rtu = rs_rtu, ---@type rtu_device|rtu_rs_device
modbus_io = modbus.new(rs_rtu, false), modbus_io = modbus.new(rs_rtu, false),
pkt_queue = nil, ---@type mqueue|nil pkt_queue = nil, ---@type mqueue|nil
thread = nil ---@type parallel_thread|nil thread = nil ---@type parallel_thread|nil
} }
table.insert(units, unit) table.insert(units, unit)
@@ -261,7 +265,7 @@ local function main()
unit.uid = #units unit.uid = #units
databus.tx_unit_hw_status(unit.uid, RTU_UNIT_HW_STATE.OK) databus.tx_unit_hw_status(unit.uid, unit.hw_state)
end end
end end
@@ -339,6 +343,18 @@ local function main()
log.fatal(util.c("configure> failed to check if '", name, "' is a formed turbine multiblock")) log.fatal(util.c("configure> failed to check if '", name, "' is a formed turbine multiblock"))
return false return false
end end
elseif type == "dynamicValve" then
-- dynamic tank multiblock
rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE
rtu_iface, faulted = dynamicv_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed dynamic tank multiblock"))
return false
end
elseif type == "inductionPort" then elseif type == "inductionPort" then
-- induction matrix multiblock -- induction matrix multiblock
rtu_type = RTU_UNIT_TYPE.IMATRIX rtu_type = RTU_UNIT_TYPE.IMATRIX
@@ -403,6 +419,7 @@ local function main()
device = device, ---@type table device = device, ---@type table
is_multiblock = is_multiblock, ---@type boolean is_multiblock = is_multiblock, ---@type boolean
formed = formed, ---@type boolean|nil formed = formed, ---@type boolean|nil
hw_state = RTU_UNIT_HW_STATE.OFFLINE, ---@type RTU_UNIT_HW_STATE
rtu = rtu_iface, ---@type rtu_device|rtu_rs_device rtu = rtu_iface, ---@type rtu_device|rtu_rs_device
modbus_io = modbus.new(rtu_iface, true), modbus_io = modbus.new(rtu_iface, true),
pkt_queue = mqueue.new(), ---@type mqueue|nil pkt_queue = mqueue.new(), ---@type mqueue|nil
@@ -422,19 +439,21 @@ local function main()
rtu_unit.uid = #units rtu_unit.uid = #units
-- report hardware status -- determine hardware status
if rtu_unit.type == RTU_UNIT_TYPE.VIRTUAL then if rtu_unit.type == RTU_UNIT_TYPE.VIRTUAL then
databus.tx_unit_hw_status(rtu_unit.uid, RTU_UNIT_HW_STATE.OFFLINE) rtu_unit.hw_state = RTU_UNIT_HW_STATE.OFFLINE
else else
if rtu_unit.is_multiblock then if rtu_unit.is_multiblock then
databus.tx_unit_hw_status(rtu_unit.uid, util.trinary(rtu_unit.formed == true, RTU_UNIT_HW_STATE.OK, RTU_UNIT_HW_STATE.UNFORMED)) rtu_unit.hw_state = util.trinary(rtu_unit.formed == true, RTU_UNIT_HW_STATE.OK, RTU_UNIT_HW_STATE.UNFORMED)
elseif faulted then elseif faulted then
databus.tx_unit_hw_status(rtu_unit.uid, RTU_UNIT_HW_STATE.FAULTED) rtu_unit.hw_state = RTU_UNIT_HW_STATE.FAULTED
else else
databus.tx_unit_hw_status(rtu_unit.uid, RTU_UNIT_HW_STATE.OK) rtu_unit.hw_state = RTU_UNIT_HW_STATE.OK
end end
end end
-- report hardware status
databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state)
end end
-- we made it through all that trusting-user-to-write-a-config-file chaos -- we made it through all that trusting-user-to-write-a-config-file chaos
@@ -458,7 +477,7 @@ local function main()
renderer.close_ui() renderer.close_ui()
println_ts(util.c("UI error: ", message)) println_ts(util.c("UI error: ", message))
println("startup> running without front panel") println("startup> running without front panel")
log.error(util.c("GUI crashed with error ", message)) log.error(util.c("front panel GUI render failed with error ", message))
log.info("startup> running in headless mode without front panel") log.info("startup> running in headless mode without front panel")
end end
@@ -467,7 +486,7 @@ local function main()
log.debug("startup> conn watchdog started") log.debug("startup> conn watchdog started")
-- setup comms -- setup comms
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.RTU_CHANNEL, config.SVR_CHANNEL, smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, config.RTU_CHANNEL, config.SVR_CHANNEL,
config.TRUSTED_RANGE, smem_sys.conn_watchdog) config.TRUSTED_RANGE, smem_sys.conn_watchdog)
log.debug("startup> comms init") log.debug("startup> comms init")

View File

@@ -10,6 +10,7 @@ local modbus = require("rtu.modbus")
local renderer = require("rtu.renderer") local renderer = require("rtu.renderer")
local boilerv_rtu = require("rtu.dev.boilerv_rtu") local boilerv_rtu = require("rtu.dev.boilerv_rtu")
local dynamicv_rtu = require("rtu.dev.dynamicv_rtu")
local envd_rtu = require("rtu.dev.envd_rtu") local envd_rtu = require("rtu.dev.envd_rtu")
local imatrix_rtu = require("rtu.dev.imatrix_rtu") local imatrix_rtu = require("rtu.dev.imatrix_rtu")
local sna_rtu = require("rtu.dev.sna_rtu") local sna_rtu = require("rtu.dev.sna_rtu")
@@ -46,7 +47,7 @@ function threads.thread__main(smem)
-- load in from shared memory -- load in from shared memory
local rtu_state = smem.rtu_state local rtu_state = smem.rtu_state
local rtu_dev = smem.rtu_dev local nic = smem.rtu_sys.nic
local rtu_comms = smem.rtu_sys.rtu_comms local rtu_comms = smem.rtu_sys.rtu_comms
local conn_watchdog = smem.rtu_sys.conn_watchdog local conn_watchdog = smem.rtu_sys.conn_watchdog
local units = smem.rtu_sys.units local units = smem.rtu_sys.units
@@ -93,11 +94,19 @@ function threads.thread__main(smem)
if type ~= nil and device ~= nil then if type ~= nil and device ~= nil then
if type == "modem" then if type == "modem" then
-- we only care if this is our wireless modem -- we only care if this is our wireless modem
if device == rtu_dev.modem then if nic.is_modem(device) then
println_ts("wireless modem disconnected!") nic.disconnect()
log.warning("comms modem disconnected!")
databus.tx_hw_modem(false) println_ts("wireless modem disconnected!")
log.warning("comms modem disconnected")
local other_modem = ppm.get_wireless_modem()
if other_modem then
log.info("found another wireless modem, using it for comms")
nic.connect(other_modem)
else
databus.tx_hw_modem(false)
end
else else
log.warning("non-comms modem disconnected") log.warning("non-comms modem disconnected")
end end
@@ -105,13 +114,15 @@ function threads.thread__main(smem)
for i = 1, #units do for i = 1, #units do
-- find disconnected device -- find disconnected device
if units[i].device == device then if units[i].device == device then
-- we are going to let the PPM prevent crashes -- will let the PPM prevent crashes, which will indicate failures in MODBUS queries
-- return fault flags/codes to MODBUS queries
local unit = units[i] ---@type rtu_unit_registry_entry local unit = units[i] ---@type rtu_unit_registry_entry
local type_name = types.rtu_type_to_string(unit.type) local type_name = types.rtu_type_to_string(unit.type)
println_ts(util.c("lost the ", type_name, " on interface ", unit.name)) println_ts(util.c("lost the ", type_name, " on interface ", unit.name))
log.warning(util.c("lost the ", type_name, " unit peripheral on interface ", unit.name)) log.warning(util.c("lost the ", type_name, " unit peripheral on interface ", unit.name))
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OFFLINE)
unit.hw_state = UNIT_HW_STATE.OFFLINE
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
break break
end end
end end
@@ -123,15 +134,16 @@ function threads.thread__main(smem)
if type ~= nil and device ~= nil then if type ~= nil and device ~= nil then
if type == "modem" then if type == "modem" then
if device.isWireless() then if device.isWireless() and not nic.is_connected() then
-- reconnected modem -- reconnected modem
rtu_dev.modem = device nic.connect(device)
rtu_comms.reconnect_modem(rtu_dev.modem)
println_ts("wireless modem reconnected.") println_ts("wireless modem reconnected.")
log.info("comms modem reconnected") log.info("comms modem reconnected")
databus.tx_hw_modem(true) databus.tx_hw_modem(true)
elseif device.isWireless() then
log.info("unused wireless modem reconnected")
else else
log.info("wired modem reconnected") log.info("wired modem reconnected")
end end
@@ -144,6 +156,8 @@ function threads.thread__main(smem)
-- note: cannot check isFormed as that would yield this coroutine and consume events -- note: cannot check isFormed as that would yield this coroutine and consume events
if unit.name == param1 then if unit.name == param1 then
local resend_advert = false local resend_advert = false
local faulted = false
local unknown = false
-- found, re-link -- found, re-link
unit.device = device unit.device = device
@@ -176,58 +190,60 @@ function threads.thread__main(smem)
databus.tx_unit_hw_type(unit.uid, unit.type) databus.tx_unit_hw_type(unit.uid, unit.type)
end end
-- note for multiblock structures: if not formed, indexing the multiblock functions results in a PPM fault
if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
unit.rtu = boilerv_rtu.new(device) unit.rtu, faulted = boilerv_rtu.new(device)
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault unit.formed = util.trinary(faulted, false, nil)
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED)
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
unit.rtu = turbinev_rtu.new(device) unit.rtu, faulted = turbinev_rtu.new(device)
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault unit.formed = util.trinary(faulted, false, nil)
unit.formed = util.trinary(device.__p_is_faulted(), false, nil) elseif unit.type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED) unit.rtu, faulted = dynamicv_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.IMATRIX then elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
unit.rtu = imatrix_rtu.new(device) unit.rtu, faulted = imatrix_rtu.new(device)
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault unit.formed = util.trinary(faulted, false, nil)
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED)
elseif unit.type == RTU_UNIT_TYPE.SPS then elseif unit.type == RTU_UNIT_TYPE.SPS then
unit.rtu = sps_rtu.new(device) unit.rtu, faulted = sps_rtu.new(device)
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault unit.formed = util.trinary(faulted, false, nil)
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED)
elseif unit.type == RTU_UNIT_TYPE.SNA then elseif unit.type == RTU_UNIT_TYPE.SNA then
unit.rtu = sna_rtu.new(device) unit.rtu, faulted = sna_rtu.new(device)
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK)
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
unit.rtu = envd_rtu.new(device) unit.rtu, faulted = envd_rtu.new(device)
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK)
else else
unknown = true
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true) log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
end end
if unit.is_multiblock then if unit.is_multiblock then
if (unit.formed == false) then unit.hw_state = UNIT_HW_STATE.UNFORMED
if unit.formed == false then
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing")) log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED)
end end
elseif device.__p_is_faulted() then elseif faulted then
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.FAULTED) unit.hw_state = UNIT_HW_STATE.FAULTED
elseif not unknown then
unit.hw_state = UNIT_HW_STATE.OK
else else
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK) unit.hw_state = UNIT_HW_STATE.OFFLINE
end end
unit.modbus_io = modbus.new(unit.rtu, true) databus.tx_unit_hw_status(unit.uid, unit.hw_state)
local type_name = types.rtu_type_to_string(unit.type) if not unknown then
local message = util.c("reconnected the ", type_name, " on interface ", unit.name) unit.modbus_io = modbus.new(unit.rtu, true)
println_ts(message)
log.info(message)
if resend_advert then local type_name = types.rtu_type_to_string(unit.type)
rtu_comms.send_advertisement(units) local message = util.c("reconnected the ", type_name, " on interface ", unit.name)
else println_ts(message)
rtu_comms.send_remounted(unit.uid) log.info(message)
if resend_advert then
rtu_comms.send_advertisement(units)
else
rtu_comms.send_remounted(unit.uid)
end
end end
end end
end end
@@ -391,13 +407,6 @@ function threads.thread__unit_comms(smem, unit)
-- received a packet -- received a packet
local _, reply = unit.modbus_io.handle_packet(msg.message) local _, reply = unit.modbus_io.handle_packet(msg.message)
rtu_comms.send_modbus(reply) rtu_comms.send_modbus(reply)
-- check if there was a problem and update the hardware state if so
local frame = reply.get()
if unit.formed and (bit.band(frame.func_code, types.MODBUS_FCODE.ERROR_FLAG) ~= 0) and
(frame.data[1] == types.MODBUS_EXCODE.SERVER_DEVICE_FAIL) then
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.FAULTED)
end
end end
end end
@@ -413,12 +422,10 @@ function threads.thread__unit_comms(smem, unit)
if unit.formed == nil then if unit.formed == nil then
unit.formed = is_formed unit.formed = is_formed
if is_formed then databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK) end if is_formed then unit.hw_state = UNIT_HW_STATE.OK end
end end
if not unit.formed then if not unit.formed then unit.hw_state = UNIT_HW_STATE.UNFORMED end
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED)
end
if (not unit.formed) and is_formed then if (not unit.formed) and is_formed then
-- newly re-formed -- newly re-formed
@@ -444,6 +451,12 @@ function threads.thread__unit_comms(smem, unit)
unit.rtu, faulted = turbinev_rtu.new(device) unit.rtu, faulted = turbinev_rtu.new(device)
unit.formed = device.isFormed() unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true) unit.modbus_io = modbus.new(unit.rtu, true)
elseif type == "dynamicValve" and unit.type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
-- dynamic tank multiblock
unit.device = device
unit.rtu, faulted = dynamicv_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
elseif type == "inductionPort" and unit.type == RTU_UNIT_TYPE.IMATRIX then elseif type == "inductionPort" and unit.type == RTU_UNIT_TYPE.IMATRIX then
-- induction matrix multiblock -- induction matrix multiblock
unit.device = device unit.device = device
@@ -463,11 +476,11 @@ function threads.thread__unit_comms(smem, unit)
if unit.formed and faulted then if unit.formed and faulted then
-- something is still wrong = can't mark as formed yet -- something is still wrong = can't mark as formed yet
unit.formed = false unit.formed = false
unit.hw_state = UNIT_HW_STATE.UNFORMED
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing")) log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED)
else else
unit.hw_state = UNIT_HW_STATE.OK
rtu_comms.send_remounted(unit.uid) rtu_comms.send_remounted(unit.uid)
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK)
end end
local type_name = types.rtu_type_to_string(unit.type) local type_name = types.rtu_type_to_string(unit.type)
@@ -484,6 +497,16 @@ function threads.thread__unit_comms(smem, unit)
unit.formed = is_formed unit.formed = is_formed
end end
-- check hardware status
if unit.device.__p_is_healthy() then
if unit.hw_state == UNIT_HW_STATE.FAULTED then unit.hw_state = UNIT_HW_STATE.OK end
else
if unit.hw_state == UNIT_HW_STATE.OK then unit.hw_state = UNIT_HW_STATE.FAULTED end
end
-- update hw status
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
-- check for termination request -- check for termination request
if rtu_state.shutdown then if rtu_state.shutdown then
log.info("rtu unit thread exiting -> " .. short_name) log.info("rtu unit thread exiting -> " .. short_name)

View File

@@ -7,14 +7,14 @@ local log = require("scada-common.log")
local insert = table.insert local insert = table.insert
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local C_ID = os.getComputerID() ---@type integer computer ID local COMPUTER_ID = os.getComputerID() ---@type integer computer ID
local max_distance = nil ---@type number|nil maximum acceptable transmission distance local max_distance = nil ---@type number|nil maximum acceptable transmission distance
---@class comms ---@class comms
local comms = {} local comms = {}
comms.version = "2.0.0" comms.version = "2.1.2"
---@enum PROTOCOL ---@enum PROTOCOL
local PROTOCOL = { local PROTOCOL = {
@@ -92,9 +92,11 @@ local PLC_AUTO_ACK = {
---@enum FAC_COMMAND ---@enum FAC_COMMAND
local FAC_COMMAND = { local FAC_COMMAND = {
SCRAM_ALL = 0, -- SCRAM all reactors SCRAM_ALL = 0, -- SCRAM all reactors
STOP = 1, -- stop automatic control STOP = 1, -- stop automatic process control
START = 2, -- start automatic control START = 2, -- start automatic process control
ACK_ALL_ALARMS = 3 -- acknowledge all alarms on all units ACK_ALL_ALARMS = 3, -- acknowledge all alarms on all units
SET_WASTE_MODE = 4, -- set automatic waste processing mode
SET_PU_FB = 5 -- set plutonium fallback mode
} }
---@enum UNIT_COMMAND ---@enum UNIT_COMMAND
@@ -163,8 +165,7 @@ function comms.scada_packet()
---@param payload table ---@param payload table
function public.make(dest_addr, seq_num, protocol, payload) function public.make(dest_addr, seq_num, protocol, payload)
self.valid = true self.valid = true
---@diagnostic disable-next-line: undefined-field self.src_addr = COMPUTER_ID
self.src_addr = C_ID
self.dest_addr = dest_addr self.dest_addr = dest_addr
self.seq_num = seq_num self.seq_num = seq_num
self.protocol = protocol self.protocol = protocol
@@ -219,10 +220,14 @@ function comms.scada_packet()
end end
-- check if this packet is destined for this device -- check if this packet is destined for this device
local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == C_ID) local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == COMPUTER_ID)
self.valid = is_destination and type(self.src_addr) == "number" and type(self.dest_addr) == "number" and self.valid = is_destination and
type(self.seq_num) == "number" and type(self.protocol) == "number" and type(self.payload) == "table" type(self.src_addr) == "number" and
type(self.dest_addr) == "number" and
type(self.seq_num) == "number" and
type(self.protocol) == "number" and
type(self.payload) == "table"
end end
end end
@@ -260,6 +265,112 @@ function comms.scada_packet()
return public return public
end end
-- authenticated SCADA packet object
---@nodiscard
function comms.authd_packet()
local self = {
modem_msg_in = nil, ---@type modem_message|nil
valid = false,
raw = {},
src_addr = comms.BROADCAST,
dest_addr = comms.BROADCAST,
mac = "",
payload = ""
}
---@class authd_packet
local public = {}
-- make an authenticated SCADA packet
---@param s_packet scada_packet scada packet to authenticate
---@param mac function message authentication function
function public.make(s_packet, mac)
self.valid = true
self.src_addr = s_packet.src_addr()
self.dest_addr = s_packet.dest_addr()
self.payload = textutils.serialize(s_packet.raw_sendable(), { allow_repetitions = true, compact = true })
self.mac = mac(self.payload)
self.raw = { self.src_addr, self.dest_addr, self.mac, self.payload }
end
-- parse in a modem message as an authenticated SCADA packet
---@param side string modem side
---@param sender integer sender channel
---@param reply_to integer reply channel
---@param message any message body
---@param distance integer transmission distance
---@return boolean valid valid message received
function public.receive(side, sender, reply_to, message, distance)
---@class modem_message
self.modem_msg_in = {
iface = side,
s_channel = sender,
r_channel = reply_to,
msg = message,
dist = distance
}
self.valid = false
self.raw = self.modem_msg_in.msg
if (type(max_distance) == "number") and (distance > max_distance) then
-- outside of maximum allowable transmission distance
-- log.debug("comms.authd_packet.receive(): discarding packet with distance " .. distance .. " outside of trusted range")
else
if type(self.raw) == "table" then
if #self.raw == 4 then
self.src_addr = self.raw[1]
self.dest_addr = self.raw[2]
self.mac = self.raw[3]
self.payload = self.raw[4]
else
self.src_addr = nil
self.dest_addr = nil
self.mac = ""
self.payload = ""
end
-- check if this packet is destined for this device
local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == COMPUTER_ID)
self.valid = is_destination and
type(self.src_addr) == "number" and
type(self.dest_addr) == "number" and
type(self.mac) == "string" and
type(self.payload) == "string"
end
end
return self.valid
end
-- public accessors --
---@nodiscard
function public.modem_event() return self.modem_msg_in end
---@nodiscard
function public.raw_sendable() return self.raw end
---@nodiscard
function public.local_channel() return self.modem_msg_in.s_channel end
---@nodiscard
function public.remote_channel() return self.modem_msg_in.r_channel end
---@nodiscard
function public.is_valid() return self.valid end
---@nodiscard
function public.src_addr() return self.src_addr end
---@nodiscard
function public.dest_addr() return self.dest_addr end
---@nodiscard
function public.mac() return self.mac end
---@nodiscard
function public.data() return self.payload end
return public
end
-- MODBUS packet<br> -- MODBUS packet<br>
-- modeled after MODBUS TCP packet -- modeled after MODBUS TCP packet
---@nodiscard ---@nodiscard

View File

@@ -6,6 +6,8 @@ local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core")
local crash = {} local crash = {}
local app = "unknown" local app = "unknown"
@@ -32,6 +34,7 @@ function crash.handler(error)
log.info(util.c("APPLICATION: ", app)) log.info(util.c("APPLICATION: ", app))
log.info(util.c("FIRMWARE VERSION: ", ver)) log.info(util.c("FIRMWARE VERSION: ", ver))
log.info(util.c("COMMS VERSION: ", comms.version)) log.info(util.c("COMMS VERSION: ", comms.version))
log.info(util.c("GRAPHICS VERSION: ", core.version))
log.info("----------------------------------") log.info("----------------------------------")
log.info(debug.traceback("--- begin debug trace ---", 1)) log.info(debug.traceback("--- begin debug trace ---", 1))
log.info("--- end debug trace ---") log.info("--- end debug trace ---")

View File

@@ -1,244 +0,0 @@
--
-- 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_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
---@nodiscard
---@param plaintext string
---@return table 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
---@nodiscard
---@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
---@nodiscard
---@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)
---@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 reconnected_modem table
---@diagnostic disable-next-line: redefined-local
function public.wrap(reconnected_modem)
modem = reconnected_modem
for key, func in pairs(modem) do
public[key] = func
end
end
-- wrap modem functions, then we replace transmit
public.wrap(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 computed_hmac = crypto.hmac(iv .. ciphertext)
modem.transmit(channel, reply_channel, { computed_hmac, iv, ciphertext })
end
-- parse in a modem message as a network packet
---@nodiscard
---@param side string modem side
---@param sender integer sender port
---@param reply_to integer reply port
---@param message any encrypted packet sent with secure_modem.transmit
---@param distance integer transmission distance
---@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 rx_hmac = message[1]
local iv = message[2]
local ciphertext = message[3]
local computed_hmac = crypto.hmac(iv .. ciphertext)
if rx_hmac == computed_hmac then
-- message intact
local plaintext = crypto.decrypt(iv, ciphertext)
body = textutils.unserialize(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

@@ -20,7 +20,9 @@ local logger = {
mode = MODE.APPEND, mode = MODE.APPEND,
debug = false, debug = false,
file = nil, file = nil,
dmesg_out = nil dmesg_out = nil,
dmesg_restore_coord = { 1, 1 },
dmesg_scroll_count = 0
} }
---@type function ---@type function
@@ -158,6 +160,7 @@ function log.dmesg(msg, tag, tag_color)
if cur_y == out_h then if cur_y == out_h then
out.scroll(1) out.scroll(1)
out.setCursorPos(1, cur_y) out.setCursorPos(1, cur_y)
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1
else else
out.setCursorPos(1, cur_y + 1) out.setCursorPos(1, cur_y + 1)
end end
@@ -193,6 +196,7 @@ function log.dmesg(msg, tag, tag_color)
if cur_y == out_h then if cur_y == out_h then
out.scroll(1) out.scroll(1)
out.setCursorPos(1, cur_y) out.setCursorPos(1, cur_y)
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1
else else
out.setCursorPos(1, cur_y + 1) out.setCursorPos(1, cur_y + 1)
end end
@@ -201,6 +205,8 @@ function log.dmesg(msg, tag, tag_color)
out.write(lines[i]) out.write(lines[i])
end end
logger.dmesg_restore_coord = { out.getCursorPos() }
_log(util.c("[", t_stamp, "] [", tag, "] ", msg)) _log(util.c("[", t_stamp, "] [", tag, "] ", msg))
end end
@@ -215,6 +221,7 @@ end
---@return function update, function done ---@return function update, function done
function log.dmesg_working(msg, tag, tag_color) function log.dmesg_working(msg, tag, tag_color)
local ts_coord = log.dmesg(msg, tag, tag_color) local ts_coord = log.dmesg(msg, tag, tag_color)
local initial_scroll = logger.dmesg_scroll_count
local out = logger.dmesg_out local out = logger.dmesg_out
local width = (ts_coord.x2 - ts_coord.x1) + 1 local width = (ts_coord.x2 - ts_coord.x1) + 1
@@ -225,11 +232,14 @@ function log.dmesg_working(msg, tag, tag_color)
local counter = 0 local counter = 0
local function update(sec_remaining) local function update(sec_remaining)
local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll)
if new_y < 1 then return end
local time = util.sprintf("%ds", sec_remaining) local time = util.sprintf("%ds", sec_remaining)
local available = width - (string.len(time) + 2) local available = width - (string.len(time) + 2)
local progress = "" local progress = ""
out.setCursorPos(ts_coord.x1, ts_coord.y) out.setCursorPos(ts_coord.x1, new_y)
out.write(" ") out.write(" ")
if counter % 4 == 0 then if counter % 4 == 0 then
@@ -249,10 +259,15 @@ function log.dmesg_working(msg, tag, tag_color)
out.setTextColor(initial_color) out.setTextColor(initial_color)
counter = counter + 1 counter = counter + 1
out.setCursorPos(table.unpack(logger.dmesg_restore_coord))
end end
local function done(ok) local function done(ok)
out.setCursorPos(ts_coord.x1, ts_coord.y) local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll)
if new_y < 1 then return end
out.setCursorPos(ts_coord.x1, new_y)
if ok or ok == nil then if ok or ok == nil then
out.setTextColor(colors.green) out.setTextColor(colors.green)
@@ -263,6 +278,8 @@ function log.dmesg_working(msg, tag, tag_color)
end end
out.setTextColor(initial_color) out.setTextColor(initial_color)
out.setCursorPos(table.unpack(logger.dmesg_restore_coord))
end end
return update, done return update, done

233
scada-common/network.lua Normal file
View File

@@ -0,0 +1,233 @@
--
-- Network Communications
--
local md5 = require("lockbox.digest.md5")
local sha256 = require("lockbox.digest.sha2_256")
local pbkdf2 = require("lockbox.kdf.pbkdf2")
local hmac = require("lockbox.mac.hmac")
local stream = require("lockbox.util.stream")
local array = require("lockbox.util.array")
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local util = require("scada-common.util")
local network = {}
local c_eng = {
key = nil,
hmac = nil
}
-- initialize message authentication system
---@param passkey string facility passkey
---@return integer init_time milliseconds init took
function network.init_mac(passkey)
local start = util.time_ms()
local key_deriv = pbkdf2()
-- setup PBKDF2
key_deriv.setPassword(passkey)
key_deriv.setSalt("pepper")
key_deriv.setIterations(32)
key_deriv.setBlockLen(8)
key_deriv.setDKeyLen(16)
key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha256))
key_deriv.finish()
c_eng.key = array.fromHex(key_deriv.asHex())
-- initialize HMAC
c_eng.hmac = hmac()
c_eng.hmac.setBlockSize(64)
c_eng.hmac.setDigest(md5)
c_eng.hmac.setKey(c_eng.key)
local init_time = util.time_ms() - start
log.info("network.init_mac completed in " .. init_time .. "ms")
return init_time
end
-- generate HMAC of message
---@nodiscard
---@param message string initial value concatenated with ciphertext
local function compute_hmac(message)
-- local start = util.time_ms()
c_eng.hmac.init()
c_eng.hmac.update(stream.fromString(message))
c_eng.hmac.finish()
local hash = c_eng.hmac.asHex()
-- log.debug("compute_hmac(): hmac-md5 = " .. util.strval(hash) .. " (took " .. (util.time_ms() - start) .. "ms)")
return hash
end
-- NIC: Network Interface Controller<br>
-- utilizes HMAC-MD5 for message authentication, if enabled
---@param modem table modem to use
function network.nic(modem)
local self = {
connected = true, -- used to avoid costly MAC calculations if modem isn't even present
channels = {}
}
---@class nic
---@field open function
---@field isOpen function
---@field close function
---@field closeAll function
---@field isWireless function
---@field getNameLocal function
---@field getNamesRemote function
---@field isPresentRemote function
---@field getTypeRemote function
---@field hasTypeRemote function
---@field getMethodsRemote function
---@field callRemote function
local public = {}
-- check if this NIC has a connected modem
---@nodiscard
function public.is_connected() return self.connected end
-- connect to a modem peripheral
---@param reconnected_modem table
function public.connect(reconnected_modem)
modem = reconnected_modem
self.connected = true
-- open previously opened channels
for _, channel in ipairs(self.channels) do
modem.open(channel)
end
-- link all public functions except for transmit
for key, func in pairs(modem) do
if key ~= "transmit" and key ~= "open" and key ~= "close" and key ~= "closeAll" then public[key] = func end
end
end
-- flag this NIC as no longer having a connected modem (usually do to peripheral disconnect)
function public.disconnect() self.connected = false end
-- check if a peripheral is this modem
---@nodiscard
---@param device table
function public.is_modem(device) return device == modem end
-- wrap modem functions, then create custom functions
public.connect(modem)
-- open a channel on the modem<br>
-- if disconnected *after* opening, previousy opened channels will be re-opened on reconnection
---@param channel integer
function public.open(channel)
modem.open(channel)
local already_open = false
for i = 1, #self.channels do
if self.channels[i] == channel then
already_open = true
break
end
end
if not already_open then
table.insert(self.channels, channel)
end
end
-- close a channel on the modem
---@param channel integer
function public.close(channel)
modem.close(channel)
for i = 1, #self.channels do
if self.channels[i] == channel then
table.remove(self.channels, i)
return
end
end
end
-- close all channels on the modem
function public.closeAll()
modem.closeAll()
self.channels = {}
end
-- send a packet, with message authentication if configured
---@param dest_channel integer destination channel
---@param local_channel integer local channel
---@param packet scada_packet packet
function public.transmit(dest_channel, local_channel, packet)
if self.connected then
local tx_packet = packet ---@type authd_packet|scada_packet
if c_eng.hmac ~= nil then
-- local start = util.time_ms()
tx_packet = comms.authd_packet()
---@cast tx_packet authd_packet
tx_packet.make(packet, compute_hmac)
-- log.debug("crypto.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms")
end
modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable())
end
end
-- parse in a modem message as a network packet
---@nodiscard
---@param side string modem side
---@param sender integer sender channel
---@param reply_to integer reply channel
---@param message any packet sent with or without message authentication
---@param distance integer transmission distance
---@return scada_packet|nil packet received packet if valid and passed authentication check
function public.receive(side, sender, reply_to, message, distance)
local packet = nil
if self.connected then
local s_packet = comms.scada_packet()
if c_eng.hmac ~= nil then
-- parse packet as an authenticated SCADA packet
local a_packet = comms.authd_packet()
a_packet.receive(side, sender, reply_to, message, distance)
if a_packet.is_valid() then
-- local start = util.time_ms()
local packet_hmac = a_packet.mac()
local msg = a_packet.data()
local computed_hmac = compute_hmac(msg)
if packet_hmac == computed_hmac then
-- log.debug("crypto.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms")
s_packet.receive(side, sender, reply_to, textutils.unserialize(msg), distance)
else
-- log.debug("crypto.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
end
end
else
-- parse packet as a generic SCADA packet
s_packet.receive(side, sender, reply_to, message, distance)
end
if s_packet.is_valid() then packet = s_packet end
end
return packet
end
return public
end
return network

View File

@@ -101,22 +101,31 @@ local function peri_init(iface)
end end
end end
-- fault management functions -- fault management & monitoring functions
local function clear_fault() self.faulted = false end local function clear_fault() self.faulted = false end
local function get_last_fault() return self.last_fault end local function get_last_fault() return self.last_fault end
local function is_faulted() return self.faulted end local function is_faulted() return self.faulted end
local function is_ok() return not self.faulted end local function is_ok() return not self.faulted end
-- check if a peripheral has any faulted functions<br>
-- contrasted with is_faulted() and is_ok() as those only check if the last operation failed,
-- unless auto fault clearing is disabled, at which point faults become sticky faults
local function is_healthy()
for _, v in pairs(self.fault_counts) do if v > 0 then return false end end
return true
end
local function enable_afc() self.auto_cf = true end local function enable_afc() self.auto_cf = true end
local function disable_afc() self.auto_cf = false end local function disable_afc() self.auto_cf = false end
-- append to device functions -- append PPM functions to device functions
self.device.__p_clear_fault = clear_fault self.device.__p_clear_fault = clear_fault
self.device.__p_last_fault = get_last_fault self.device.__p_last_fault = get_last_fault
self.device.__p_is_faulted = is_faulted self.device.__p_is_faulted = is_faulted
self.device.__p_is_ok = is_ok self.device.__p_is_ok = is_ok
self.device.__p_is_healthy = is_healthy
self.device.__p_enable_afc = enable_afc self.device.__p_enable_afc = enable_afc
self.device.__p_disable_afc = disable_afc self.device.__p_disable_afc = disable_afc
@@ -156,7 +165,7 @@ local function peri_init(iface)
return { return {
type = self.type, type = self.type,
dev = self.device dev = self.device
} }
end end

View File

@@ -89,16 +89,18 @@ types.RTU_UNIT_TYPE = {
REDSTONE = 1, -- redstone I/O REDSTONE = 1, -- redstone I/O
BOILER_VALVE = 2, -- boiler mekanism 10.1+ BOILER_VALVE = 2, -- boiler mekanism 10.1+
TURBINE_VALVE = 3, -- turbine, mekanism 10.1+ TURBINE_VALVE = 3, -- turbine, mekanism 10.1+
IMATRIX = 4, -- induction matrix DYNAMIC_VALVE = 4, -- dynamic tank, mekanism 10.1+
SPS = 5, -- SPS IMATRIX = 5, -- induction matrix
SNA = 6, -- SNA SPS = 6, -- SPS
ENV_DETECTOR = 7 -- environment detector SNA = 7, -- SNA
ENV_DETECTOR = 8 -- environment detector
} }
types.RTU_UNIT_NAMES = { types.RTU_UNIT_NAMES = {
"redstone", "redstone",
"boiler_valve", "boiler_valve",
"turbine_valve", "turbine_valve",
"dynamic_valve",
"induction_matrix", "induction_matrix",
"sps", "sps",
"sna", "sna",
@@ -115,6 +117,7 @@ function types.rtu_type_to_string(utype)
elseif utype == types.RTU_UNIT_TYPE.REDSTONE or elseif utype == types.RTU_UNIT_TYPE.REDSTONE or
utype == types.RTU_UNIT_TYPE.BOILER_VALVE or utype == types.RTU_UNIT_TYPE.BOILER_VALVE or
utype == types.RTU_UNIT_TYPE.TURBINE_VALVE or utype == types.RTU_UNIT_TYPE.TURBINE_VALVE or
utype == types.RTU_UNIT_TYPE.DYNAMIC_VALVE or
utype == types.RTU_UNIT_TYPE.IMATRIX or utype == types.RTU_UNIT_TYPE.IMATRIX or
utype == types.RTU_UNIT_TYPE.SPS or utype == types.RTU_UNIT_TYPE.SPS or
utype == types.RTU_UNIT_TYPE.SNA or utype == types.RTU_UNIT_TYPE.SNA or
@@ -158,13 +161,26 @@ types.PROCESS_NAMES = {
---@enum WASTE_MODE ---@enum WASTE_MODE
types.WASTE_MODE = { types.WASTE_MODE = {
AUTO = 1, AUTO = 1,
PLUTONIUM = 2, MANUAL_PLUTONIUM = 2,
POLONIUM = 3, MANUAL_POLONIUM = 3,
ANTI_MATTER = 4 MANUAL_ANTI_MATTER = 4
} }
types.WASTE_MODE_NAMES = { types.WASTE_MODE_NAMES = {
"AUTO", "AUTO",
"MANUAL_PLUTONIUM",
"MANUAL_POLONIUM",
"MANUAL_ANTI_MATTER"
}
---@enum WASTE_PRODUCT
types.WASTE_PRODUCT = {
PLUTONIUM = 1,
POLONIUM = 2,
ANTI_MATTER = 3
}
types.WASTE_PRODUCT_NAMES = {
"PLUTONIUM", "PLUTONIUM",
"POLONIUM", "POLONIUM",
"ANTI_MATTER" "ANTI_MATTER"
@@ -315,6 +331,17 @@ types.RPS_TRIP_CAUSE = {
FORCE_DISABLED = "force_disabled" FORCE_DISABLED = "force_disabled"
} }
---@alias container_mode
---| "BOTH"
---| "FILL"
---| "EMPTY"
types.CONTAINER_MODE = {
BOTH = "BOTH",
FILL = "FILL",
EMPTY = "EMPTY"
}
---@alias dumping_mode ---@alias dumping_mode
---| "IDLE" ---| "IDLE"
---| "DUMPING" ---| "DUMPING"

View File

@@ -17,6 +17,10 @@ config.PLC_TIMEOUT = 5
config.RTU_TIMEOUT = 5 config.RTU_TIMEOUT = 5
config.CRD_TIMEOUT = 5 config.CRD_TIMEOUT = 5
config.PKT_TIMEOUT = 5 config.PKT_TIMEOUT = 5
-- facility authentication key (do NOT use one of your passwords)
-- this enables verifying that messages are authentic
-- all devices on the same network must use the same key
-- config.AUTH_KEY = "SCADAfacility123"
-- expected number of reactors -- expected number of reactors
config.NUM_REACTORS = 4 config.NUM_REACTORS = 4

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